diff --git a/src/main/scala/net/psforever/packet/game/OutfitMembershipResponse.scala b/src/main/scala/net/psforever/packet/game/OutfitMembershipResponse.scala index 5b2aec24..da9c27b5 100644 --- a/src/main/scala/net/psforever/packet/game/OutfitMembershipResponse.scala +++ b/src/main/scala/net/psforever/packet/game/OutfitMembershipResponse.scala @@ -3,18 +3,20 @@ package net.psforever.packet.game import net.psforever.packet.GamePacketOpcode.Type import net.psforever.packet.{GamePacketOpcode, Marshallable, PacketHelpers, PlanetSideGamePacket} -import scodec.{Attempt, Codec, Err} +import scodec.{Attempt, Codec} import scodec.bits.BitVector import scodec.codecs._ import shapeless.{::, HNil} final case class OutfitMembershipResponse( - response_type: OutfitMembershipResponse.ResponseType.Type, + packet_type: OutfitMembershipResponse.PacketType.Type, unk0: Int, unk1: Int, outfit_id: Long, target_id: Long, - action: OutfitMembershipResponseAction + str1: String, + str2: String, + flag: Boolean ) extends PlanetSideGamePacket { type Packet = OutfitMembershipResponse @@ -23,226 +25,40 @@ final case class OutfitMembershipResponse( def encode: Attempt[BitVector] = OutfitMembershipResponse.encode(this) } -abstract class OutfitMembershipResponseAction(val code: Int) -object OutfitMembershipResponseAction { - - final case class Universal( - str1: String, - str2: String, - flag: Boolean - ) extends OutfitMembershipResponseAction(-1) - - final case class CreateResponse( - 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) // unk7 = rank? - - 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 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 UniversalResponseCodec: Codec[OutfitMembershipResponseAction] = ( - PacketHelpers.encodedWideStringAligned(5) :: - PacketHelpers.encodedWideString :: - ("flag" | bool) - ).xmap[OutfitMembershipResponseAction]( - { - case str1 :: str2 :: flag :: HNil => - Universal(str1, str2, flag) - }, - { - case Universal(str1, str2, flag) => - str1 :: str2 :: flag :: HNil - } - ) - - val CreateOutfitCodec: Codec[CreateResponse] = ( - PacketHelpers.encodedWideStringAligned(5) :: - PacketHelpers.encodedWideString :: - PacketHelpers.encodedWideString - ).xmap[CreateResponse]( - { - case str1 :: str2 :: str3 :: HNil => - CreateResponse(str1, str2, str3) - }, - { - case CreateResponse(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.encodedWideStringAligned(5).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 - } - ) - - /** - * 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 { + object PacketType extends Enumeration { type Type = Value - val CreateResponse: ResponseType.Value = Value(0) - val Unk1: ResponseType.Value = Value(1) // Info: Player has been invited / response to OutfitMembershipRequest Unk2 for that player - val Unk2: ResponseType.Value = Value(2) // Invited / Accepted / Added - 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) + val CreateResponse: PacketType.Value = Value(0) + val Unk1: PacketType.Value = Value(1) // Info: Player has been invited / response to OutfitMembershipRequest Unk2 for that player + val Unk2: PacketType.Value = Value(2) // Invited / Accepted / Added + val Unk3: PacketType.Value = Value(3) + val Unk4: PacketType.Value = Value(4) + val Unk5: PacketType.Value = Value(5) + val Unk6: PacketType.Value = Value(6) // 6 and 7 seen as failed decodes, validity unknown + val Unk7: PacketType.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 => UniversalResponseCodec - case 1 => UniversalResponseCodec - case 2 => UniversalResponseCodec - case 3 => UniversalResponseCodec - case 4 => UniversalResponseCodec - case 5 => UniversalResponseCodec - case 6 => UniversalResponseCodec - case 7 => UniversalResponseCodec - -// case 0 => CreateOutfitCodec // 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 => + ("response_type" | PacketType.codec) :: ("unk0" | uintL(5)) :: ("unk1" | uintL(3)) :: ("outfit_id" | uint32L) :: ("target_id" | uint32L) :: - ("action" | selectFromType(response_type.id)) - } + ("str1" | PacketHelpers.encodedWideStringAligned(5)) :: + ("str2" | PacketHelpers.encodedWideString) :: + ("flag" | bool) ).xmap[OutfitMembershipResponse]( { - case response_type :: u0 :: u1 :: outfit_id :: target_id :: action :: HNil => - OutfitMembershipResponse(response_type, u0, u1, outfit_id, target_id, action) + case response_type :: u0 :: u1 :: outfit_id :: target_id :: str1 :: str2 :: flag :: HNil => + OutfitMembershipResponse(response_type, u0, u1, outfit_id, target_id, str1, str2, flag) }, { - case OutfitMembershipResponse(response_type, u0, u1, outfit_id, target_id, action) => - response_type :: u0 :: u1 :: outfit_id :: target_id :: action :: HNil + case OutfitMembershipResponse(response_type, u0, u1, outfit_id, target_id, str1, str2, flag) => + response_type :: u0 :: u1 :: outfit_id :: target_id :: str1 :: str2 :: flag :: HNil } ) } diff --git a/src/test/scala/game/OutfitMembershipResponseTest.scala b/src/test/scala/game/OutfitMembershipResponseTest.scala new file mode 100644 index 00000000..7646a96e --- /dev/null +++ b/src/test/scala/game/OutfitMembershipResponseTest.scala @@ -0,0 +1,156 @@ +// Copyright (c) 2023-2025 PSForever +package game + +import net.psforever.packet._ +import net.psforever.packet.game.OutfitMembershipResponse.PacketType +import net.psforever.packet.game._ +import org.specs2.mutable._ +import scodec.bits._ + +class OutfitMembershipResponseTest extends Specification { + + val createResponse = hex"8d 0 002b54f404000000010008080" + val unk1 = hex"8d 2 01bb399e03ddb4f4050a078004e00690063006b009550006c0061006e006500740053006900640065005f0046006f00720065007600650072005f005400520000" + val unk2 = hex"8d 4 0049b0f4042b54f4051405a006500720067006c0069006e006700390032009750006c0061006e006500740053006900640065005f0046006f00720065007600650072005f00560061006e00750000" + val unk3 = hex"8d 6 00e8c2f40510d3b6030008080" + val unk4 = hex"8d 8 002b54f404000000010008080" + val unk5 = hex"8d a 022b54f4051fb0f4051c05000530046006f0075007400660069007400740065007300740031008080" + + "decode CreateResponse" in { + PacketCoding.decodePacket(createResponse).require match { + case OutfitMembershipResponse(packet_type, unk0, unk1, outfit_id, target_id, str1, str2, flag) => + packet_type mustEqual PacketType.CreateResponse + unk0 mustEqual 0 + unk1 mustEqual 0 + outfit_id mustEqual 41593365 + target_id mustEqual 0 + str1 mustEqual "" + str2 mustEqual "" + flag mustEqual true + case _ => + ko + } + } + + "encode CreateResponse" in { + val msg = OutfitMembershipResponse(PacketType.CreateResponse, 0, 0, 41593365, 0, "", "", flag = true) + val pkt = PacketCoding.encodePacket(msg).require.toByteVector + + pkt mustEqual createResponse + } + + "decode unk1" in { + PacketCoding.decodePacket(unk1).require match { + case OutfitMembershipResponse(packet_type, unk0, unk1, outfit_id, target_id, str1, str2, flag) => + packet_type mustEqual PacketType.Unk1 + unk0 mustEqual 0 + unk1 mustEqual 0 + outfit_id mustEqual 30383325 + target_id mustEqual 41605870 + str1 mustEqual "xNick" + str2 mustEqual "PlanetSide_Forever_TR" + flag mustEqual false + case _ => + ko + } + } + + "encode unk1" in { + val msg = OutfitMembershipResponse(PacketType.Unk1, 0, 0, 30383325, 41605870, "xNick", "PlanetSide_Forever_TR", flag = false) + val pkt = PacketCoding.encodePacket(msg).require.toByteVector + + pkt mustEqual unk1 + } + + "decode unk2" in { + PacketCoding.decodePacket(unk2).require match { + case OutfitMembershipResponse(packet_type, unk0, unk1, outfit_id, target_id, str1, str2, flag) => + packet_type mustEqual PacketType.Unk2 + unk0 mustEqual 0 + unk1 mustEqual 0 + outfit_id mustEqual 41605156 + target_id mustEqual 41593365 + str1 mustEqual "Zergling92" + str2 mustEqual "PlanetSide_Forever_Vanu" + flag mustEqual false + case _ => + ko + } + } + + "encode unk2" in { + val msg = OutfitMembershipResponse(PacketType.Unk2, 0, 0, 41605156, 41593365, "Zergling92", "PlanetSide_Forever_Vanu", flag = false) + val pkt = PacketCoding.encodePacket(msg).require.toByteVector + + pkt mustEqual unk2 + } + + "decode unk3" in { + PacketCoding.decodePacket(unk3).require match { + case OutfitMembershipResponse(packet_type, unk0, unk1, outfit_id, target_id, str1, str2, flag) => + packet_type mustEqual PacketType.Unk3 + unk0 mustEqual 0 + unk1 mustEqual 0 + outfit_id mustEqual 41574772 + target_id mustEqual 31156616 + str1 mustEqual "" + str2 mustEqual "" + flag mustEqual true + case _ => + ko + } + } + + "encode unk3" in { + val msg = OutfitMembershipResponse(PacketType.Unk3, 0, 0, 41574772, 31156616, "", "", flag = true) + val pkt = PacketCoding.encodePacket(msg).require.toByteVector + + pkt mustEqual unk3 + } + + "decode unk4" in { + PacketCoding.decodePacket(unk4).require match { + case OutfitMembershipResponse(packet_type, unk0, unk1, outfit_id, target_id, str1, str2, flag) => + packet_type mustEqual PacketType.Unk4 + unk0 mustEqual 0 + unk1 mustEqual 0 + outfit_id mustEqual 41593365 + target_id mustEqual 0 + str1 mustEqual "" + str2 mustEqual "" + flag mustEqual true + case _ => + ko + } + } + + "encode unk4" in { + val msg = OutfitMembershipResponse(PacketType.Unk4, 0, 0, 41593365, 0, "", "", flag = true) + val pkt = PacketCoding.encodePacket(msg).require.toByteVector + + pkt mustEqual unk4 + } + + "decode unk5" in { + PacketCoding.decodePacket(unk5).require match { + case OutfitMembershipResponse(packet_type, unk0, unk1, outfit_id, target_id, str1, str2, flag) => + packet_type mustEqual PacketType.Unk5 + unk0 mustEqual 0 + unk1 mustEqual 1 + outfit_id mustEqual 41593365 + target_id mustEqual 41605263 + str1 mustEqual "PSFoutfittest1" + str2 mustEqual "" + flag mustEqual true + case _ => + ko + } + } + + "encode unk5" in { + val msg = OutfitMembershipResponse(PacketType.Unk5, 0, 1, 41593365, 41605263, "PSFoutfittest1", "", flag = true) + val pkt = PacketCoding.encodePacket(msg).require.toByteVector + + pkt mustEqual unk5 + } +}