OutfitMembershipRequest packet codec finished

This commit is contained in:
Resaec 2023-12-28 23:02:48 +01:00
parent 9703ac402d
commit f7f734296b
2 changed files with 265 additions and 153 deletions

View file

@ -1,20 +1,19 @@
// Copyright (c) 2017 PSForever
// Copyright (c) 2023 PSForever
package net.psforever.packet.game
import net.psforever.packet.{GamePacketOpcode, Marshallable, PacketHelpers, PlanetSideGamePacket}
import net.psforever.types.PlanetSideGUID
import scodec.Codec
import scodec.{Attempt, Codec, Err}
import scodec.bits.BitVector
import scodec.codecs._
import shapeless.{::, HNil}
final case class OutfitMembershipRequest(
request_type: OutfitMembershipRequest.RequestType.Type,
avatar_guid: PlanetSideGUID,
unk1: Int,
unk2: String,
unk3: Int,
unk4: Boolean,
outfit_name: String
) extends PlanetSideGamePacket {
action: OutfitAction
) extends PlanetSideGamePacket {
type Packet = OutfitMembershipRequest
def opcode = GamePacketOpcode.OutfitMembershipRequest
@ -22,81 +21,161 @@ final case class OutfitMembershipRequest(
def encode = OutfitMembershipRequest.encode(this)
}
abstract class OutfitAction(val code: Int)
object OutfitAction {
final case class CreateOutfit(unk2: String, unk3: Int, unk4: Boolean, outfit_name: String) extends OutfitAction(code = 0)
final case class FormOutfit(unk2: String, unk3: Int, unk4: Boolean, outfit_name: String) extends OutfitAction(code = 1)
final case class AcceptOutfitInvite(unk2: String) extends OutfitAction(code = 3)
final case class RejectOutfitInvite(unk2: String) extends OutfitAction(code = 4)
final case class CancelOutfitInvite(unk5: Int, unk6: Int, outfit_name: String) extends OutfitAction(code = 5)
final case class Unknown(badCode: Int, data: BitVector) extends OutfitAction(badCode)
/**
* The `Codec`s used to transform the input stream into the context of a specific action
* and extract the field data from that stream.
*/
object Codecs {
private val everFailCondition = conditional(included = false, bool)
val CreateOutfitCodec =
(PacketHelpers.encodedWideString :: uint4L :: bool :: PacketHelpers.encodedWideString).xmap[CreateOutfit](
{
case unk2 :: unk3 :: unk4 :: outfit_name :: HNil =>
CreateOutfit(unk2, unk3, unk4, outfit_name)
},
{
case CreateOutfit(unk2, unk3, unk4, outfit_name) =>
unk2 :: unk3 :: unk4 :: outfit_name :: HNil
}
)
val FormOutfitCodec =
(PacketHelpers.encodedWideString :: uint4L :: bool :: PacketHelpers.encodedWideString).xmap[FormOutfit](
{
case unk2 :: unk3 :: unk4 :: outfit_name :: HNil =>
FormOutfit(unk2, unk3, unk4, outfit_name)
},
{
case FormOutfit(unk2, unk3, unk4, outfit_name) =>
unk2 :: unk3 :: unk4 :: outfit_name :: HNil
}
)
val AcceptOutfitCodec =
(PacketHelpers.encodedWideString).xmap[AcceptOutfitInvite](
{
case unk2 =>
AcceptOutfitInvite(unk2)
},
{
case AcceptOutfitInvite(unk2) =>
unk2
}
)
val RejectOutfitCodec =
(PacketHelpers.encodedWideString).xmap[RejectOutfitInvite](
{
case unk2 =>
RejectOutfitInvite(unk2)
},
{
case RejectOutfitInvite(unk2) =>
unk2
}
)
val CancelOutfitCodec =
(uint16L :: uint16L :: PacketHelpers.encodedWideStringAligned(5)).xmap[CancelOutfitInvite](
{
case unk5 :: unk6 :: outfit_name :: HNil =>
CancelOutfitInvite(unk5, unk6, outfit_name)
},
{
case CancelOutfitInvite(unk5, unk6, outfit_name) =>
unk5 :: unk6 :: outfit_name :: HNil
}
)
/**
* A common form for known action code indexes with an unknown purpose and transformation is an "Unknown" object.
* @param action the action behavior code
* @return a transformation between the action code and the unknown bit data
*/
def unknownCodec(action: Int) =
bits.xmap[Unknown](
data => Unknown(action, data),
{
case Unknown(_, data) => data
}
)
/**
* The action code was completely unanticipated!
* @param action the action behavior code
* @return nothing; always fail
*/
def failureCodec(action: Int) =
everFailCondition.exmap[OutfitAction](
_ => Attempt.failure(Err(s"can not match a codec pattern for decoding $action")),
_ => Attempt.failure(Err(s"can not match a codec pattern for encoding $action"))
)
}
}
object OutfitMembershipRequest extends Marshallable[OutfitMembershipRequest] {
object RequestType extends Enumeration {
type Type = Value
val Create = Value(0x0)
val Form = Value(0x1)
val Accept = Value(0x3)
val Reject = Value(0x4)
val Cancel = Value(0x5)
val Create = Value(0)
val Form = Value(1)
val Unk2 = Value(2)
val Accept = Value(3)
val Reject = Value(4)
val Cancel = Value(5)
implicit val codec: Codec[Type] = PacketHelpers.createEnumerationCodec(this, uintL(3))
}
def selectFromType(code: Int): Codec[OutfitAction] = {
import OutfitAction.Codecs._
import scala.annotation.switch
((code: @switch) match {
case 0 => CreateOutfitCodec
case 1 => FormOutfitCodec // so far same as Create
case 2 => unknownCodec(action = code)
case 3 => AcceptOutfitCodec
case 4 => RejectOutfitCodec // so far same as Accept
case 5 => CancelOutfitCodec
case 6 => unknownCodec(action = code)
case 7 => unknownCodec(action = code)
// 3 bit limit
case _ => failureCodec(code)
}).asInstanceOf[Codec[OutfitAction]]
}
implicit val codec: Codec[OutfitMembershipRequest] = (
("request_type" | RequestType.codec) ::
("avatar_guid" | PlanetSideGUID.codec) :: // as in DB avatar table
("unk1" | uint16L) :: // avatar2_guid / invited player ?
("unk2" | PacketHelpers.encodedWideString) :: // could be string
("unk3" | uint4L) ::
("unk4" | bool) ::
("outfit_name" | PacketHelpers.encodedWideString)
).as[OutfitMembershipRequest]
("request_type" | RequestType.codec) >>:~ { request_type =>
("avatar_guid" | PlanetSideGUID.codec) ::
("unk1" | uint16L) ::
("action" | selectFromType(request_type.id))
}
).xmap[OutfitMembershipRequest](
{
case request_type :: avatar_guid :: u1 :: action :: HNil =>
OutfitMembershipRequest(request_type, avatar_guid, u1, action)
},
{
case OutfitMembershipRequest(request_type, avatar_guid, u1, action) =>
request_type :: avatar_guid :: u1 :: action :: HNil
}
)
}
/*
/outfitcreate
0 0200 000 1000 83 410042004300 -- /outfitcreate ABC -- from AA - TR BR 24 CR 0
0 0200 000 1000 83 410042004300 -- /outfitcreate ABC -- from AA - TR BR 24 CR 0
0 1000 000 1000 83 410042004300 -- /outfitcreate ABC -- from TTEESSTT - TR BR 24 CR 0
0 0a00 000 1000 83 310032003300 -- /outfitcreate 123 -- from BBBB - TR BR 1 CR 0
0 0a00 000 1000 83 310032003300
0 0a00 000 1000 83 310032003300
0 0a00 000 1000 83 310032003300
0 0a00 000 1000 83 410042004300 -- ABC
0 0400 000 1000 83 580059005a00 -- /outfitcreate XYZ -- from BB - VS BR 24 CR 0
0 1000 000 1000 84 3200320032003200 -- /outfitcreate 2222 -- from TTEESSTT - TR BR 24 CR 0
/outfitform
20 2000 00 1000 83 610062006300 -- /outfitform abc -- from AA - TR BR 24 CR 0
21 0000 00 1000 81 3100 -- /outfitform 1 -- from TTEESSTT - TR BR 24 CR 0
/outfitinvite
3 // guess
/outfitkick
4 // guess
/outfitaccept
60 2000 00 1000 -- from AA - TR BR 24 CR 0
60 4000 00 1000 -- from BB - VS BR 24 CR 0
/outfitreject
80 2000 00 1000 -- from AA - TR BR 24 CR 0
80 4000 00 1000 -- from BB - VS BR 24 CR 0
80 6000 00 1000 -- from BBB - NC BR 1 CR 0
/outfitcancel
a0 2000 00 0000 0000 1000 -- from AA - TR BR 24 CR 0
a0 4000 00 0000 0000 1000 -- from BB - VS BR 24 CR 0
a0 6000 00 0000 0000 1000 -- from BBB - NC BR 1 CR 0
a0 2000 00 0000 0000 1060 610064006200 -- /outfitcancel abc -- from AA - TR BR 24 CR 0
a0 2000 00 0000 0000 1080 3100320033003400 -- /outfitcancel 1234 -- from AA - TR BR 24 CR 0
*/

View file

@ -1,9 +1,10 @@
// Copyright (c) 2017 PSForever
// Copyright (c) 2023 PSForever
package game
import net.psforever.packet._
import net.psforever.packet.game.OutfitMembershipRequest.RequestType
import net.psforever.packet.game._
import net.psforever.packet.game.OutfitAction.{AcceptOutfitInvite, CancelOutfitInvite, CreateOutfit, FormOutfit, RejectOutfitInvite}
import net.psforever.packet.game.OutfitMembershipRequest.RequestType
import net.psforever.types.PlanetSideGUID
import org.specs2.mutable._
import scodec.bits._
@ -17,184 +18,216 @@ class OutfitMembershipRequestTest extends Specification {
val accept_2 = hex"8c 6 0400 000 1000"
val reject_1 = hex"8c 8 0200 000 1000"
val reject_2 = hex"8c 8 0400 000 1000"
val cancel_5 = hex"8c a 0600 000 0000 0000 1000"
val cancel_1_abc = hex"8c a 0200 000 0000 0000 1060 610064006200"
val cancel_3 = hex"8c a 0600 000 0000 0000 1000"
val cancel_1_abc = hex"8c a 0200 000 0000 0000 1060 610062006300"
val cancel_3_def = hex"8c a 0600 000 0000 0000 1060 640065006600" // /outfitcancel 123 def -- first parameter is skipped
"decode create ABC" in {
"decode CreateOutfit ABC" in {
PacketCoding.decodePacket(create_ABC).require match {
case OutfitMembershipRequest(request_type, avatar_id, unk1, unk2, unk3, unk4, outfit_name) =>
case OutfitMembershipRequest(request_type, avatar_id, unk1, action) =>
request_type mustEqual RequestType.Create
avatar_id mustEqual 1
avatar_id mustEqual PlanetSideGUID(1)
unk1 mustEqual 0
unk2 mustEqual ""
unk3 mustEqual 0
unk4 mustEqual false
outfit_name mustEqual "ABC"
action mustEqual CreateOutfit("", 0, unk4 = false, "ABC")
case _ =>
ko
}
}
"encode create ABC" in {
val msg = OutfitMembershipRequest(RequestType.Create, PlanetSideGUID(1), 0, "", 0, unk4 = false, "ABC")
"encode CreateOutfit ABC" in {
val msg = OutfitMembershipRequest(RequestType.Create, PlanetSideGUID(1), 0, CreateOutfit("", 0, unk4 = false, "ABC"))
val pkt = PacketCoding.encodePacket(msg).require.toByteVector
pkt mustEqual create_ABC
}
"decode create 2222" in {
"decode CreateOutfit 2222" in {
PacketCoding.decodePacket(create_2222).require match {
case OutfitMembershipRequest(request_type, avatar_id, unk1, unk2, unk3, unk4, outfit_name) =>
case OutfitMembershipRequest(request_type, avatar_id, unk1, action) =>
request_type mustEqual RequestType.Create
avatar_id mustEqual PlanetSideGUID(8)
unk1 mustEqual 0
unk2 mustEqual ""
unk3 mustEqual 0
unk4 mustEqual false
outfit_name mustEqual "2222"
action mustEqual CreateOutfit("", 0, unk4 = false, "2222")
case _ =>
ko
}
}
"encode create 2222" in {
val msg = OutfitMembershipRequest(RequestType.Create, PlanetSideGUID(8), 0, "", 0, unk4 = false, "2222")
"encode CreateOutfit 2222" in {
val msg = OutfitMembershipRequest(RequestType.Create, PlanetSideGUID(8), 0, CreateOutfit("", 0, unk4 = false, "2222"))
val pkt = PacketCoding.encodePacket(msg).require.toByteVector
pkt mustEqual create_2222
}
"decode form abc" in {
"decode FormOutfit abc" in {
PacketCoding.decodePacket(form_abc).require match {
case OutfitMembershipRequest(request_type, avatar_id, unk1, unk2, unk3, unk4, outfit_name) =>
case OutfitMembershipRequest(request_type, avatar_id, unk1, action) =>
request_type mustEqual RequestType.Form
avatar_id mustEqual PlanetSideGUID(1)
unk1 mustEqual 0
unk2 mustEqual ""
unk3 mustEqual 0
unk4 mustEqual false
outfit_name mustEqual "abc"
action mustEqual FormOutfit("", 0, unk4 = false, "abc")
case _ =>
ko
}
}
"encode form abc" in {
val msg = OutfitMembershipRequest(RequestType.Form, PlanetSideGUID(1), 0, "", 0, unk4 = false, "abc")
"encode FormOutfit abc" in {
val msg = OutfitMembershipRequest(RequestType.Form, PlanetSideGUID(1), 0, FormOutfit("", 0, unk4 = false, "abc"))
val pkt = PacketCoding.encodePacket(msg).require.toByteVector
pkt mustEqual form_abc
}
"decode form 1" in {
"decode FormOutfit 1" in {
PacketCoding.decodePacket(form_1).require match {
case OutfitMembershipRequest(request_type, avatar_id, unk1, unk2, unk3, unk4, outfit_name) =>
case OutfitMembershipRequest(request_type, avatar_id, unk1, action) =>
request_type mustEqual RequestType.Form
avatar_id mustEqual PlanetSideGUID(8)
unk1 mustEqual 0
unk2 mustEqual ""
unk3 mustEqual 0
unk4 mustEqual false
outfit_name mustEqual "1"
action mustEqual FormOutfit("", 0, unk4 = false, "1")
case _ =>
ko
}
}
"encode form 1" in {
val msg = OutfitMembershipRequest(RequestType.Form, PlanetSideGUID(8), 0, "", 0, unk4 = false, "1")
"encode FormOutfit 1" in {
val msg = OutfitMembershipRequest(RequestType.Form, PlanetSideGUID(8), 0, FormOutfit("", 0, unk4 = false, "1"))
val pkt = PacketCoding.encodePacket(msg).require.toByteVector
pkt mustEqual form_1
}
"decode accept 1" in {
"decode AcceptOutfitInvite 1" in {
PacketCoding.decodePacket(accept_1).require match {
case OutfitMembershipRequest(request_type, avatar_id, unk1, unk2, unk3, unk4, outfit_name) =>
case OutfitMembershipRequest(request_type, avatar_id, unk1, action) =>
request_type mustEqual RequestType.Accept
avatar_id mustEqual PlanetSideGUID(1)
unk1 mustEqual 0
unk2 mustEqual ""
unk3 mustEqual 0
unk4 mustEqual false
outfit_name mustEqual ""
action mustEqual AcceptOutfitInvite("")
case _ =>
ko
}
}
"decode accept 2" in {
"encode AcceptOutfitInvite 1" in {
val msg = OutfitMembershipRequest(RequestType.Accept, PlanetSideGUID(1), 0, AcceptOutfitInvite(""))
val pkt = PacketCoding.encodePacket(msg).require.toByteVector
pkt mustEqual accept_1
}
"decode AcceptOutfitInvite 2" in {
PacketCoding.decodePacket(accept_2).require match {
case OutfitMembershipRequest(request_type, avatar_id, unk1, unk2, unk3, unk4, outfit_name) =>
case OutfitMembershipRequest(request_type, avatar_id, unk1, action) =>
request_type mustEqual RequestType.Accept
avatar_id mustEqual 2
avatar_id mustEqual PlanetSideGUID(2)
unk1 mustEqual 0
unk2 mustEqual ""
unk3 mustEqual 0
unk4 mustEqual false
outfit_name mustEqual ""
action mustEqual AcceptOutfitInvite("")
case _ =>
ko
}
}
"decode reject 1" in {
"encode AcceptOutfitInvite 2" in {
val msg = OutfitMembershipRequest(RequestType.Accept, PlanetSideGUID(2), 0, AcceptOutfitInvite(""))
val pkt = PacketCoding.encodePacket(msg).require.toByteVector
pkt mustEqual accept_2
}
"decode RejectOutfitInvite 1" in {
PacketCoding.decodePacket(reject_1).require match {
case OutfitMembershipRequest(request_type, avatar_id, unk1, unk2, unk3, unk4, outfit_name) =>
case OutfitMembershipRequest(request_type, avatar_id, unk1, action) =>
request_type mustEqual RequestType.Reject
avatar_id mustEqual 1
avatar_id mustEqual PlanetSideGUID(1)
unk1 mustEqual 0
unk2 mustEqual ""
unk3 mustEqual 0
unk4 mustEqual false
outfit_name mustEqual ""
action mustEqual RejectOutfitInvite("")
case _ =>
ko
}
}
"decode reject 2" in {
"encode RejectOutfitInvite 1" in {
val msg = OutfitMembershipRequest(RequestType.Reject, PlanetSideGUID(1), 0, RejectOutfitInvite(""))
val pkt = PacketCoding.encodePacket(msg).require.toByteVector
pkt mustEqual reject_1
}
"decode RejectOutfitInvite 2" in {
PacketCoding.decodePacket(reject_2).require match {
case OutfitMembershipRequest(request_type, avatar_id, unk1, unk2, unk3, unk4, outfit_name) =>
case OutfitMembershipRequest(request_type, avatar_id, unk1, action) =>
request_type mustEqual RequestType.Reject
avatar_id mustEqual 2
avatar_id mustEqual PlanetSideGUID(2)
unk1 mustEqual 0
unk2 mustEqual ""
unk3 mustEqual 0
unk4 mustEqual false
outfit_name mustEqual ""
action mustEqual RejectOutfitInvite("")
case _ =>
ko
}
}
"decode cancel 5" in {
PacketCoding.decodePacket(cancel_5).require match {
case OutfitMembershipRequest(request_type, avatar_id, unk1, unk2, unk3, unk4, outfit_name) =>
"encode RejectOutfitInvite 2" in {
val msg = OutfitMembershipRequest(RequestType.Reject, PlanetSideGUID(2), 0, RejectOutfitInvite(""))
val pkt = PacketCoding.encodePacket(msg).require.toByteVector
pkt mustEqual reject_2
}
"decode CancelOutfitInvite 3" in {
PacketCoding.decodePacket(cancel_3).require match {
case OutfitMembershipRequest(request_type, avatar_id, unk1, action) =>
request_type mustEqual RequestType.Cancel
avatar_id mustEqual 5
avatar_id mustEqual PlanetSideGUID(3)
unk1 mustEqual 0
unk2 mustEqual ""
unk3 mustEqual 0
unk4 mustEqual false
outfit_name mustEqual ""
action mustEqual CancelOutfitInvite(0, 0, "")
case _ =>
ko
}
}
"decode reject 1 abc" in {
"encode CancelOutfitInvite 3" in {
val msg = OutfitMembershipRequest(RequestType.Cancel, PlanetSideGUID(3), 0, CancelOutfitInvite(0, 0, ""))
val pkt = PacketCoding.encodePacket(msg).require.toByteVector
pkt mustEqual cancel_3
}
"decode CancelOutfitInvite 1 abc" in {
PacketCoding.decodePacket(cancel_1_abc).require match {
case OutfitMembershipRequest(request_type, avatar_id, unk1, unk2, unk3, unk4, outfit_name) =>
case OutfitMembershipRequest(request_type, avatar_id, unk1, action) =>
request_type mustEqual RequestType.Cancel
avatar_id mustEqual 1
avatar_id mustEqual PlanetSideGUID(1)
unk1 mustEqual 0
unk2 mustEqual ""
unk3 mustEqual 0
unk4 mustEqual false
outfit_name mustEqual "abc"
action mustEqual CancelOutfitInvite(0, 0, "abc")
case _ =>
ko
}
}
"encode CancelOutfitInvite 1 abc" in {
val msg = OutfitMembershipRequest(RequestType.Cancel, PlanetSideGUID(1), 0, CancelOutfitInvite(0, 0, "abc"))
val pkt = PacketCoding.encodePacket(msg).require.toByteVector
pkt mustEqual cancel_1_abc
}
"decode CancelOutfitInvite 3 def" in {
PacketCoding.decodePacket(cancel_3_def).require match {
case OutfitMembershipRequest(request_type, avatar_id, unk1, action) =>
request_type mustEqual RequestType.Cancel
avatar_id mustEqual PlanetSideGUID(3)
unk1 mustEqual 0
action mustEqual CancelOutfitInvite(0, 0, "def")
case _ =>
ko
}
}
"encode CancelOutfitInvite 3 def" in {
val msg = OutfitMembershipRequest(RequestType.Cancel, PlanetSideGUID(3), 0, CancelOutfitInvite(0, 0, "def"))
val pkt = PacketCoding.encodePacket(msg).require.toByteVector
pkt mustEqual cancel_3_def
}
}