diff --git a/src/main/scala/net/psforever/packet/GamePacketOpcode.scala b/src/main/scala/net/psforever/packet/GamePacketOpcode.scala index 291928c6c..e2f6bea2b 100644 --- a/src/main/scala/net/psforever/packet/GamePacketOpcode.scala +++ b/src/main/scala/net/psforever/packet/GamePacketOpcode.scala @@ -469,7 +469,7 @@ object GamePacketOpcode extends Enumeration { case 0x8a => game.PlayerStasisMessage.decode case 0x8b => noDecoder(UnknownMessage139) case 0x8c => game.OutfitMembershipRequest.decode - case 0x8d => noDecoder(OutfitMembershipResponse) + case 0x8d => game.OutfitMembershipResponse.decode case 0x8e => game.OutfitRequest.decode case 0x8f => noDecoder(OutfitEvent) diff --git a/src/main/scala/net/psforever/packet/game/OutfitMembershipRequest.scala b/src/main/scala/net/psforever/packet/game/OutfitMembershipRequest.scala index 55989fc66..420270ee8 100644 --- a/src/main/scala/net/psforever/packet/game/OutfitMembershipRequest.scala +++ b/src/main/scala/net/psforever/packet/game/OutfitMembershipRequest.scala @@ -13,7 +13,7 @@ final case class OutfitMembershipRequest( request_type: OutfitMembershipRequest.RequestType.Type, avatar_guid: PlanetSideGUID, unk1: Int, - action: OutfitAction + action: OutfitMembershipRequestAction ) extends PlanetSideGamePacket { type Packet = OutfitMembershipRequest @@ -22,20 +22,21 @@ final case class OutfitMembershipRequest( def encode: Attempt[BitVector] = OutfitMembershipRequest.encode(this) } -abstract class OutfitAction(val code: Int) -object OutfitAction { +abstract class OutfitMembershipRequestAction(val code: Int) - final case class CreateOutfit(unk2: String, unk3: Int, unk4: Boolean, outfit_name: String) extends OutfitAction(code = 0) +object OutfitMembershipRequestAction { - final case class FormOutfit(unk2: String, unk3: Int, unk4: Boolean, outfit_name: String) extends OutfitAction(code = 1) + final case class CreateOutfit(unk2: String, unk3: Int, unk4: Boolean, outfit_name: String) extends OutfitMembershipRequestAction(code = 0) - final case class AcceptOutfitInvite(unk2: String) extends OutfitAction(code = 3) + final case class FormOutfit(unk2: String, unk3: Int, unk4: Boolean, outfit_name: String) extends OutfitMembershipRequestAction(code = 1) - final case class RejectOutfitInvite(unk2: String) extends OutfitAction(code = 4) + final case class AcceptOutfitInvite(unk2: String) extends OutfitMembershipRequestAction(code = 3) - final case class CancelOutfitInvite(unk5: Int, unk6: Int, outfit_name: String) extends OutfitAction(code = 5) + final case class RejectOutfitInvite(unk2: String) extends OutfitMembershipRequestAction(code = 4) - final case class Unknown(badCode: Int, data: BitVector) extends OutfitAction(badCode) + final case class CancelOutfitInvite(unk5: Int, unk6: Int, outfit_name: String) extends OutfitMembershipRequestAction(code = 5) + + final case class Unknown(badCode: Int, data: BitVector) extends OutfitMembershipRequestAction(badCode) /** * The `Codec`s used to transform the input stream into the context of a specific action @@ -106,6 +107,7 @@ object OutfitAction { /** * 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 */ @@ -119,11 +121,12 @@ object OutfitAction { /** * The action code was completely unanticipated! + * * @param action the action behavior code * @return nothing; always fail */ - def failureCodec(action: Int): Codec[OutfitAction] = - everFailCondition.exmap[OutfitAction]( + def failureCodec(action: Int): Codec[OutfitMembershipRequestAction] = + everFailCondition.exmap[OutfitMembershipRequestAction]( _ => 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")) ) @@ -136,19 +139,19 @@ object OutfitMembershipRequest extends Marshallable[OutfitMembershipRequest] { type Type = Value val Create: RequestType.Value = Value(0) - val Form: RequestType.Value = Value(1) - val Unk2: RequestType.Value = Value(2) + val Form: RequestType.Value = Value(1) + val Unk2: RequestType.Value = Value(2) val Accept: RequestType.Value = Value(3) val Reject: RequestType.Value = Value(4) val Cancel: RequestType.Value = Value(5) - val Unk6: RequestType.Value = Value(6) // 6 and 7 seen as failed decodes, validity unknown - val Unk7: RequestType.Value = Value(7) + val Unk6: RequestType.Value = Value(6) // 6 and 7 seen as failed decodes, validity unknown + val Unk7: RequestType.Value = Value(7) implicit val codec: Codec[Type] = PacketHelpers.createEnumerationCodec(this, uintL(3)) } - private def selectFromType(code: Int): Codec[OutfitAction] = { - import OutfitAction.Codecs._ + private def selectFromType(code: Int): Codec[OutfitMembershipRequestAction] = { + import OutfitMembershipRequestAction.Codecs._ import scala.annotation.switch ((code: @switch) match { @@ -162,7 +165,7 @@ object OutfitMembershipRequest extends Marshallable[OutfitMembershipRequest] { case 7 => unknownCodec(action = code) // 3 bit limit case _ => failureCodec(code) - }).asInstanceOf[Codec[OutfitAction]] + }).asInstanceOf[Codec[OutfitMembershipRequestAction]] } implicit val codec: Codec[OutfitMembershipRequest] = ( @@ -171,7 +174,7 @@ object OutfitMembershipRequest extends Marshallable[OutfitMembershipRequest] { ("unk1" | uint16L) :: ("action" | selectFromType(request_type.id)) } - ).xmap[OutfitMembershipRequest]( + ).xmap[OutfitMembershipRequest]( { case request_type :: avatar_guid :: u1 :: action :: HNil => OutfitMembershipRequest(request_type, avatar_guid, u1, action) diff --git a/src/main/scala/net/psforever/packet/game/OutfitMembershipResponse.scala b/src/main/scala/net/psforever/packet/game/OutfitMembershipResponse.scala new file mode 100644 index 000000000..b16010d8c --- /dev/null +++ b/src/main/scala/net/psforever/packet/game/OutfitMembershipResponse.scala @@ -0,0 +1,253 @@ +// Copyright (c) 2023 PSForever +package net.psforever.packet.game + +import net.psforever.packet.GamePacketOpcode.Type +import net.psforever.packet.{GamePacketOpcode, Marshallable, PacketHelpers, PlanetSideGamePacket} +import net.psforever.types.PlanetSideGUID +import scodec.{Attempt, Codec, Err} +import scodec.bits.BitVector +import scodec.codecs._ +import shapeless.{::, HNil} + +final case class OutfitMembershipResponse( + response_type: OutfitMembershipResponse.ResponseType.Type, + unk0: Int, + avatar_guid: PlanetSideGUID, + unk1: PlanetSideGUID, + unk2: PlanetSideGUID, + unk3: Int, + //unk4: Boolean, + action: OutfitMembershipResponseAction + ) extends PlanetSideGamePacket { + type Packet = OutfitMembershipResponse + + def opcode: Type = GamePacketOpcode.OutfitMembershipResponse + + def encode: Attempt[BitVector] = OutfitMembershipResponse.encode(this) +} + +abstract class OutfitMembershipResponseAction(val code: Int) +object OutfitMembershipResponseAction { + + final case class CreateOutfitResponse(str1: String, str2: String, str3: String) extends OutfitMembershipResponseAction(code = 0) + + final case class Unk1OutfitResponse(player_name: String, outfit_name: String, unk7: Int) extends OutfitMembershipResponseAction(code = 1) + + final case class Unk2OutfitResponse(player_name: String, outfit_name: String, unk7: Int) extends OutfitMembershipResponseAction(code = 2) + + final case class Unk3OutfitResponse(unk2: String) extends OutfitMembershipResponseAction(code = 3) + + final case class Unk4OutfitResponse(unk5: Int, unk6: Int, outfit_name: String) extends OutfitMembershipResponseAction(code = 4) + + final case class Unk5OutfitResponse() extends OutfitMembershipResponseAction(code = 5) + + final case class Unk6OutfitResponse() extends OutfitMembershipResponseAction(code = 6) + + final case class Unk7OutfitResponse() extends OutfitMembershipResponseAction(code = 7) + + final case class Unknown(badCode: Int, data: BitVector) extends OutfitMembershipResponseAction(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 Unk0OutfitCodec: Codec[CreateOutfitResponse] = ( + PacketHelpers.encodedWideStringAligned(5) :: + PacketHelpers.encodedWideString :: + PacketHelpers.encodedWideString + ).xmap[CreateOutfitResponse]( + { + case str1 :: str2 :: str3 :: HNil => + CreateOutfitResponse(str1, str2, str3) + }, + { + case CreateOutfitResponse(str1, str2, str3) => + str1 :: str2 :: str3 :: HNil + } + ) + + val Unk1OutfitCodec: Codec[Unk1OutfitResponse] = ( + PacketHelpers.encodedWideStringAligned(5) :: + PacketHelpers.encodedWideString :: + uint8L + ).xmap[Unk1OutfitResponse]( + { + case player_name :: outfit_name :: u7 :: HNil => + Unk1OutfitResponse(player_name, outfit_name, u7) + }, + { + case Unk1OutfitResponse(player_name, outfit_name, u7) => + player_name :: outfit_name :: u7 :: HNil + } + ) + + val Unk2OutfitCodec: Codec[Unk2OutfitResponse] = ( + PacketHelpers.encodedWideStringAligned(5) :: + PacketHelpers.encodedWideString :: + uint8L + ).xmap[Unk2OutfitResponse]( + { + case player_name :: outfit_name :: u7 :: HNil => + Unk2OutfitResponse(player_name, outfit_name, u7) + }, + { + case Unk2OutfitResponse(player_name, outfit_name, u7) => + player_name :: outfit_name :: u7 :: HNil + } + ) + + val Unk3OutfitCodec: Codec[Unk3OutfitResponse] = + PacketHelpers.encodedWideString.xmap[Unk3OutfitResponse]( + { + case unk2 => + Unk3OutfitResponse(unk2) + }, + { + case Unk3OutfitResponse(unk2) => + unk2 + } + ) + + val Unk4OutfitCodec: Codec[Unk4OutfitResponse] = + (uint16L :: uint16L :: PacketHelpers.encodedWideStringAligned(5)).xmap[Unk4OutfitResponse]( + { + case unk5 :: unk6 :: outfit_name :: HNil => + Unk4OutfitResponse(unk5, unk6, outfit_name) + }, + { + case Unk4OutfitResponse(unk5, unk6, outfit_name) => + unk5 :: unk6 :: outfit_name :: HNil + } + ) + +// val Unk5OutfitCodec: Codec[Unk5OutfitResponse] = +// (uint16L :: uint16L :: PacketHelpers.encodedWideStringAligned(5)).xmap[Unk5OutfitResponse]( +// { +// case unk5 :: unk6 :: outfit_name :: HNil => +// Unk5OutfitResponse(unk5, unk6, outfit_name) +// }, +// { +// case Unk5OutfitResponse(unk5, unk6, outfit_name) => +// unk5 :: unk6 :: outfit_name :: HNil +// } +// ) +// +// val Unk6OutfitCodec: Codec[Unk6OutfitResponse] = +// (uint16L :: uint16L :: PacketHelpers.encodedWideStringAligned(5)).xmap[Unk6OutfitResponse]( +// { +// case _ => +// Unk6OutfitResponse() +// }, +// { +// case Unk6OutfitResponse() => +// _ +// } +// ) +// +// val Unk7OutfitCodec: Codec[Unk7OutfitResponse] = +// (uint16L :: uint16L :: PacketHelpers.encodedWideStringAligned(5)).xmap[Unk7OutfitResponse]( +// { +// case _ => +// Unk7OutfitResponse() +// }, +// { +// case Unk7OutfitResponse() => +// _ +// } +// ) + + /** + * 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): Codec[Unknown] = + 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): Codec[OutfitMembershipResponseAction] = + everFailCondition.exmap[OutfitMembershipResponseAction]( + _ => 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 OutfitMembershipResponse extends Marshallable[OutfitMembershipResponse] { + + object ResponseType extends Enumeration { + type Type = Value + + val CreateResponse: ResponseType.Value = Value(0) + val Unk1: ResponseType.Value = Value(1) + val Unk2: ResponseType.Value = Value(2) + val Unk3: ResponseType.Value = Value(3) + val Unk4: ResponseType.Value = Value(4) + val Unk5: ResponseType.Value = Value(5) + val Unk6: ResponseType.Value = Value(6) // 6 and 7 seen as failed decodes, validity unknown + val Unk7: ResponseType.Value = Value(7) + + implicit val codec: Codec[Type] = PacketHelpers.createEnumerationCodec(this, uintL(3)) + } + + private def selectFromType(code: Int): Codec[OutfitMembershipResponseAction] = { + import OutfitMembershipResponseAction.Codecs._ + import scala.annotation.switch + + ((code: @switch) match { + case 0 => Unk0OutfitCodec // seem as OMReq Create response + case 1 => Unk1OutfitCodec + case 2 => Unk2OutfitCodec + case 3 => Unk3OutfitCodec + case 4 => Unk4OutfitCodec + case 5 => unknownCodec(action = code) + case 6 => unknownCodec(action = code) + case 7 => unknownCodec(action = code) + // 3 bit limit + case _ => failureCodec(code) + }).asInstanceOf[Codec[OutfitMembershipResponseAction]] + } + + implicit val codec: Codec[OutfitMembershipResponse] = ( + ("response_type" | ResponseType.codec) >>:~ { response_type => + ("unk0" | uint8L) :: + ("avatar_guid" | PlanetSideGUID.codec) :: + ("outfit_guid-1" | PlanetSideGUID.codec) :: + ("target_guid" | PlanetSideGUID.codec) :: + ("unk3" | uint16L) :: + //("unk4" | bool) :: + ("action" | selectFromType(response_type.id)) + } + ).xmap[OutfitMembershipResponse]( + { + case response_type :: u0 :: avatar_guid :: outfit_guid_1 :: target_guid :: u3 :: action :: HNil => + OutfitMembershipResponse(response_type, u0, avatar_guid, outfit_guid_1, target_guid, u3, action) + }, + { + case OutfitMembershipResponse(response_type, u0, avatar_guid, u1, u2, u3, action) => + response_type :: u0 :: avatar_guid :: u1 :: u2 :: u3 :: action :: HNil + } + ) +// ).xmap[OutfitMembershipResponse]( +// { +// case response_type :: u0 :: avatar_guid :: outfit_guid_1 :: target_guid :: u3 :: u4 :: action :: HNil => +// OutfitMembershipResponse(response_type, u0, avatar_guid, outfit_guid_1, target_guid, u3, u4, action) +// }, +// { +// case OutfitMembershipResponse(response_type, u0, avatar_guid, u1, u2, u3, u4, action) => +// response_type :: u0 :: avatar_guid :: u1 :: u2 :: u3 :: u4 :: action :: HNil +// } +// ) +} diff --git a/src/test/scala/game/OutfitMembershipRequestTest.scala b/src/test/scala/game/OutfitMembershipRequestTest.scala index 4b90df4ee..cdcf6bff3 100644 --- a/src/test/scala/game/OutfitMembershipRequestTest.scala +++ b/src/test/scala/game/OutfitMembershipRequestTest.scala @@ -3,7 +3,7 @@ package game import net.psforever.packet._ import net.psforever.packet.game._ -import net.psforever.packet.game.OutfitAction.{AcceptOutfitInvite, CancelOutfitInvite, CreateOutfit, FormOutfit, RejectOutfitInvite} +import net.psforever.packet.game.OutfitMembershipRequestAction.{AcceptOutfitInvite, CancelOutfitInvite, CreateOutfit, FormOutfit, RejectOutfitInvite} import net.psforever.packet.game.OutfitMembershipRequest.RequestType import net.psforever.types.PlanetSideGUID import org.specs2.mutable._ diff --git a/src/test/scala/game/OutfitMembershipResponseTest.scala b/src/test/scala/game/OutfitMembershipResponseTest.scala new file mode 100644 index 000000000..a647826d9 --- /dev/null +++ b/src/test/scala/game/OutfitMembershipResponseTest.scala @@ -0,0 +1,113 @@ +// Copyright (c) 2023 PSForever +package game + +import net.psforever.packet._ +import net.psforever.packet.game.OutfitMembershipResponseAction._ +import net.psforever.packet.game.OutfitMembershipResponse.ResponseType +import net.psforever.packet.game._ +import net.psforever.types.PlanetSideGUID +import org.specs2.mutable._ +import scodec.bits._ + +class OutfitMembershipResponseTest extends Specification { + + /* + Outfit Create request that results in "someResponse" packet below + C >> S + OutfitMembershipRequest(Type = Create (0), AvatarID = ValidPlanetSideGUID(43541), 634, CreateOutfit(, 0, false, PlanetSide_Forever_Vanu)) + 0x8c 0 2b54 f405 000 97 50006c0061006e006500740053006900640065005f0046006f00720065007600650072005f00560061006e007500)) + */ + val createResponse = hex"8d 0 00 2b54 f404 0000 0001 00 0 80 80" // response to create + // validity unknown + //val unk0_0 = hex"8d 0 11 2600 0000 c2b8 1a02 28c0 0000 a037 2340 4598 0000 0010 1284 dd0d 4060 0000 280d 2080 1176 0000 0004 04a1 3021 9018 0000 0" + + // ? ? ? xNick PlanetSide_Forever_TR + val new2 = hex"8d 2 01 bb39 9e03 ddb4 f405 0a 0 78004e00690063006b00 95 50006c0061006e006500740053006900640065005f0046006f00720065007600650072005f0054005200 00" + + // AvatarID OutfitID-1 TargetID/LeaderID OutfitID Zergling92) PlanetSide_Forever_Vanu + val someOther = hex"8d 4 00 49b0 f404 2b54 f405 14 0 5a006500720067006c0069006e00670039003200 97 50006c0061006e006500740053006900640065005f0046006f00720065007600650072005f00560061006e007500 00" + val someOther2 = hex"8d 4 01 ddb4 f405 bb39 9e03 14 0 48006100480061004100540052004d0061007800 95 50006c0061006e006500740053006900640065005f0046006f00720065007600650072005f0054005200 80" + // HaHaATRMax PlanetSide_Forever_TR + + // unk validity + val muh6 = hex"8d 6 64 b351 2cf3 f2ef 8040 80 0 201bb4088d1abe638d8b6b62133d81ffad501e3e1f0000083014d5948e886b3517d6404b0004028020059408681a38a68db8cb7c133f807fba501bff110aec70450569c2a000314e569f1187e9f9c00380083c30aa83879c3b6213ebbeecf8040d5fe0076408d40ccc948b488b35170381001590" + val muh61 = hex"8d 6 00 e8c2 f405 10d3 b603 00 0 80 80" + + // + val blubOther = hex"8d 8 00 2b54 f404 0000 0001 0 00 80 80" + + // PlayerName (PSFoutfittest1) + val yetAnOther = hex"8d a 02 2b54 f405 1fb0 f405 1 c0 5000530046006f007500740066006900740074006500730074003100 80 80" + // VSsulferix + val blubBlah = hex"8d a 03 8afa f404 2b54 f405 1 40 56005300730075006c0066006500720069007800 80 00" + + // validity unknown + val blah = hex"8d e 37 0660 3000 0002 6b38 4 a050 0000 0020 2429 6010 80c0 0000 2c91 a0f8 2400 0000 0808 383cc7a0300000082a68420d00000002023785e2280c000006a115138440000000808388588203000001ca6520661b0000000202439351580c00000299331287c00000008090e5b84e03000000ac650662300000002020a88c2280c000002a1bb1789c00000008082a35c3e03000000a86484e2b00000002020a8f81280c000000" + + + "decode CreateResponse" in { + PacketCoding.decodePacket(createResponse).require match { + case OutfitMembershipResponse(request_type, u0, avatar_id, outfit_guid_1, target_guid, u3, action) => + request_type mustEqual ResponseType.CreateResponse + u0 mustEqual 0 + avatar_id mustEqual PlanetSideGUID(43541) + outfit_guid_1 mustEqual PlanetSideGUID(634) + target_guid mustEqual PlanetSideGUID(0) + u3 mustEqual 0 + action mustEqual CreateOutfitResponse("", "", "") + case _ => + ko + } + } + + "encode CreateResponse" in { + val msg = OutfitMembershipResponse(ResponseType.CreateResponse, 0, PlanetSideGUID(43541), PlanetSideGUID(634), PlanetSideGUID(0), 0, CreateOutfitResponse("", "", "")) + val pkt = PacketCoding.encodePacket(msg).require.toByteVector + + pkt mustEqual createResponse + } + + "decode Unk1" in { + PacketCoding.decodePacket(new2).require match { + case OutfitMembershipResponse(request_type, u0, avatar_id, outfit_guid_1, target_guid, u3, action) => + request_type mustEqual ResponseType.Unk1 + u0 mustEqual 0 + avatar_id mustEqual PlanetSideGUID(40157) + outfit_guid_1 mustEqual PlanetSideGUID(463) + target_guid mustEqual PlanetSideGUID(56046) + u3 mustEqual 634 + action mustEqual Unk1OutfitResponse("xNick", "PlanetSide_Forever_TR", 0) + case _ => + ko + } + } + + "encode Unk1" in { + val msg = OutfitMembershipResponse(ResponseType.Unk1, 0, PlanetSideGUID(40157), PlanetSideGUID(463), PlanetSideGUID(56046), 634, Unk1OutfitResponse("xNick", "PlanetSide_Forever_TR", 0)) + val pkt = PacketCoding.encodePacket(msg).require.toByteVector + + pkt mustEqual new2 + } + + "decode Unk2" in { + PacketCoding.decodePacket(someOther).require match { + case OutfitMembershipResponse(request_type, u0, avatar_id, outfit_guid_1, target_guid, u3, action) => + request_type mustEqual ResponseType.Unk2 + u0 mustEqual 0 + avatar_id mustEqual PlanetSideGUID(55332) + outfit_guid_1 mustEqual PlanetSideGUID(634) + target_guid mustEqual PlanetSideGUID(43541) + u3 mustEqual 634 + action mustEqual Unk2OutfitResponse("Zergling92", "PlanetSide_Forever_Vanu", 0) + case _ => + ko + } + } + + "encode Unk2" in { + val msg = OutfitMembershipResponse(ResponseType.Unk2, 0, PlanetSideGUID(55332), PlanetSideGUID(634), PlanetSideGUID(43541), 634, Unk2OutfitResponse("Zergling92", "PlanetSide_Forever_Vanu", 0)) + val pkt = PacketCoding.encodePacket(msg).require.toByteVector + + pkt mustEqual someOther + } +}