From 9703ac402d9f3e69a4d36a7c7fc5357c28cbc96d Mon Sep 17 00:00:00 2001 From: Resaec Date: Wed, 27 Dec 2023 06:25:40 +0100 Subject: [PATCH 1/4] OutfitMembershipRequest packet start --- .../psforever/packet/GamePacketOpcode.scala | 2 +- .../packet/game/OutfitMembershipRequest.scala | 102 +++++++++ .../game/OutfitMembershipRequestTest.scala | 200 ++++++++++++++++++ 3 files changed, 303 insertions(+), 1 deletion(-) create mode 100644 src/main/scala/net/psforever/packet/game/OutfitMembershipRequest.scala create mode 100644 src/test/scala/game/OutfitMembershipRequestTest.scala diff --git a/src/main/scala/net/psforever/packet/GamePacketOpcode.scala b/src/main/scala/net/psforever/packet/GamePacketOpcode.scala index 3fcd0449..a2beebc0 100644 --- a/src/main/scala/net/psforever/packet/GamePacketOpcode.scala +++ b/src/main/scala/net/psforever/packet/GamePacketOpcode.scala @@ -468,7 +468,7 @@ object GamePacketOpcode extends Enumeration { case 0x89 => game.BugReportMessage.decode case 0x8a => game.PlayerStasisMessage.decode case 0x8b => noDecoder(UnknownMessage139) - case 0x8c => noDecoder(OutfitMembershipRequest) + case 0x8c => game.OutfitMembershipRequest.decode case 0x8d => noDecoder(OutfitMembershipResponse) 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 new file mode 100644 index 00000000..a6e6bded --- /dev/null +++ b/src/main/scala/net/psforever/packet/game/OutfitMembershipRequest.scala @@ -0,0 +1,102 @@ +// Copyright (c) 2017 PSForever +package net.psforever.packet.game + +import net.psforever.packet.{GamePacketOpcode, Marshallable, PacketHelpers, PlanetSideGamePacket} +import net.psforever.types.PlanetSideGUID +import scodec.Codec +import scodec.codecs._ + +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 { + type Packet = OutfitMembershipRequest + + def opcode = GamePacketOpcode.OutfitMembershipRequest + + def encode = OutfitMembershipRequest.encode(this) +} + +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) + + implicit val codec: Codec[Type] = PacketHelpers.createEnumerationCodec(this, uintL(3)) + } + + 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] +} + +/* + +/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 + + */ + diff --git a/src/test/scala/game/OutfitMembershipRequestTest.scala b/src/test/scala/game/OutfitMembershipRequestTest.scala new file mode 100644 index 00000000..4ef80419 --- /dev/null +++ b/src/test/scala/game/OutfitMembershipRequestTest.scala @@ -0,0 +1,200 @@ +// Copyright (c) 2017 PSForever +package game + +import net.psforever.packet._ +import net.psforever.packet.game.OutfitMembershipRequest.RequestType +import net.psforever.packet.game._ +import net.psforever.types.PlanetSideGUID +import org.specs2.mutable._ +import scodec.bits._ + +class OutfitMembershipRequestTest extends Specification { + val create_ABC = hex"8c 0 0200 000 1000 83 410042004300" + val create_2222 = hex"8c 0 1000 000 1000 84 3200320032003200" + val form_abc = hex"8c 2 0200 000 1000 83 610062006300" + val form_1 = hex"8c 2 1000 000 1000 81 3100" + val accept_1 = hex"8c 6 0200 000 1000" + 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" + + "decode create ABC" in { + PacketCoding.decodePacket(create_ABC).require match { + case OutfitMembershipRequest(request_type, avatar_id, unk1, unk2, unk3, unk4, outfit_name) => + request_type mustEqual RequestType.Create + avatar_id mustEqual 1 + unk1 mustEqual 0 + unk2 mustEqual "" + unk3 mustEqual 0 + unk4 mustEqual false + outfit_name mustEqual "ABC" + case _ => + ko + } + } + + "encode create ABC" in { + val msg = OutfitMembershipRequest(RequestType.Create, PlanetSideGUID(1), 0, "", 0, unk4 = false, "ABC") + val pkt = PacketCoding.encodePacket(msg).require.toByteVector + + pkt mustEqual create_ABC + } + + "decode create 2222" in { + PacketCoding.decodePacket(create_2222).require match { + case OutfitMembershipRequest(request_type, avatar_id, unk1, unk2, unk3, unk4, outfit_name) => + 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" + case _ => + ko + } + } + + "encode create 2222" in { + val msg = OutfitMembershipRequest(RequestType.Create, PlanetSideGUID(8), 0, "", 0, unk4 = false, "2222") + val pkt = PacketCoding.encodePacket(msg).require.toByteVector + + pkt mustEqual create_2222 + } + + "decode form abc" in { + PacketCoding.decodePacket(form_abc).require match { + case OutfitMembershipRequest(request_type, avatar_id, unk1, unk2, unk3, unk4, outfit_name) => + 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" + case _ => + ko + } + } + + "encode form abc" in { + val msg = OutfitMembershipRequest(RequestType.Form, PlanetSideGUID(1), 0, "", 0, unk4 = false, "abc") + val pkt = PacketCoding.encodePacket(msg).require.toByteVector + + pkt mustEqual form_abc + } + + "decode form 1" in { + PacketCoding.decodePacket(form_1).require match { + case OutfitMembershipRequest(request_type, avatar_id, unk1, unk2, unk3, unk4, outfit_name) => + 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" + case _ => + ko + } + } + + "encode form 1" in { + val msg = OutfitMembershipRequest(RequestType.Form, PlanetSideGUID(8), 0, "", 0, unk4 = false, "1") + val pkt = PacketCoding.encodePacket(msg).require.toByteVector + + pkt mustEqual form_1 + } + + "decode accept 1" in { + PacketCoding.decodePacket(accept_1).require match { + case OutfitMembershipRequest(request_type, avatar_id, unk1, unk2, unk3, unk4, outfit_name) => + request_type mustEqual RequestType.Accept + avatar_id mustEqual PlanetSideGUID(1) + unk1 mustEqual 0 + unk2 mustEqual "" + unk3 mustEqual 0 + unk4 mustEqual false + outfit_name mustEqual "" + case _ => + ko + } + } + + "decode accept 2" in { + PacketCoding.decodePacket(accept_2).require match { + case OutfitMembershipRequest(request_type, avatar_id, unk1, unk2, unk3, unk4, outfit_name) => + request_type mustEqual RequestType.Accept + avatar_id mustEqual 2 + unk1 mustEqual 0 + unk2 mustEqual "" + unk3 mustEqual 0 + unk4 mustEqual false + outfit_name mustEqual "" + case _ => + ko + } + } + + "decode reject 1" in { + PacketCoding.decodePacket(reject_1).require match { + case OutfitMembershipRequest(request_type, avatar_id, unk1, unk2, unk3, unk4, outfit_name) => + request_type mustEqual RequestType.Reject + avatar_id mustEqual 1 + unk1 mustEqual 0 + unk2 mustEqual "" + unk3 mustEqual 0 + unk4 mustEqual false + outfit_name mustEqual "" + case _ => + ko + } + } + + "decode reject 2" in { + PacketCoding.decodePacket(reject_2).require match { + case OutfitMembershipRequest(request_type, avatar_id, unk1, unk2, unk3, unk4, outfit_name) => + request_type mustEqual RequestType.Reject + avatar_id mustEqual 2 + unk1 mustEqual 0 + unk2 mustEqual "" + unk3 mustEqual 0 + unk4 mustEqual false + outfit_name mustEqual "" + case _ => + ko + } + } + + "decode cancel 5" in { + PacketCoding.decodePacket(cancel_5).require match { + case OutfitMembershipRequest(request_type, avatar_id, unk1, unk2, unk3, unk4, outfit_name) => + request_type mustEqual RequestType.Cancel + avatar_id mustEqual 5 + unk1 mustEqual 0 + unk2 mustEqual "" + unk3 mustEqual 0 + unk4 mustEqual false + outfit_name mustEqual "" + case _ => + ko + } + } + + "decode reject 1 abc" in { + PacketCoding.decodePacket(cancel_1_abc).require match { + case OutfitMembershipRequest(request_type, avatar_id, unk1, unk2, unk3, unk4, outfit_name) => + request_type mustEqual RequestType.Cancel + avatar_id mustEqual 1 + unk1 mustEqual 0 + unk2 mustEqual "" + unk3 mustEqual 0 + unk4 mustEqual false + outfit_name mustEqual "abc" + case _ => + ko + } + } +} From f7f734296b2f249c10dd1402f72bad664df7ea46 Mon Sep 17 00:00:00 2001 From: Resaec Date: Thu, 28 Dec 2023 23:02:48 +0100 Subject: [PATCH 2/4] OutfitMembershipRequest packet codec finished --- .../packet/game/OutfitMembershipRequest.scala | 227 ++++++++++++------ .../game/OutfitMembershipRequestTest.scala | 191 +++++++++------ 2 files changed, 265 insertions(+), 153 deletions(-) diff --git a/src/main/scala/net/psforever/packet/game/OutfitMembershipRequest.scala b/src/main/scala/net/psforever/packet/game/OutfitMembershipRequest.scala index a6e6bded..eb0fdc12 100644 --- a/src/main/scala/net/psforever/packet/game/OutfitMembershipRequest.scala +++ b/src/main/scala/net/psforever/packet/game/OutfitMembershipRequest.scala @@ -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 - - */ - diff --git a/src/test/scala/game/OutfitMembershipRequestTest.scala b/src/test/scala/game/OutfitMembershipRequestTest.scala index 4ef80419..77bd48e2 100644 --- a/src/test/scala/game/OutfitMembershipRequestTest.scala +++ b/src/test/scala/game/OutfitMembershipRequestTest.scala @@ -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 + } } From 4d87fd2665ba4255e70508e28945287e13c2e9f4 Mon Sep 17 00:00:00 2001 From: Resaec Date: Mon, 1 Jan 2024 02:28:40 +0100 Subject: [PATCH 3/4] Add a potential packet to the test cases Better highlight SMR test message type in packet string --- .../game/OutfitMembershipRequestTest.scala | 1 + .../game/SquadMembershipResponseTest.scala | 34 +++++++++---------- 2 files changed, 18 insertions(+), 17 deletions(-) diff --git a/src/test/scala/game/OutfitMembershipRequestTest.scala b/src/test/scala/game/OutfitMembershipRequestTest.scala index 77bd48e2..4b90df4e 100644 --- a/src/test/scala/game/OutfitMembershipRequestTest.scala +++ b/src/test/scala/game/OutfitMembershipRequestTest.scala @@ -14,6 +14,7 @@ class OutfitMembershipRequestTest extends Specification { val create_2222 = hex"8c 0 1000 000 1000 84 3200320032003200" val form_abc = hex"8c 2 0200 000 1000 83 610062006300" val form_1 = hex"8c 2 1000 000 1000 81 3100" + val unk3 = hex"8c 5 bb39 9e0 2000 0000 1080 750072006f006200" // -- "urob" -- could be false positive -- seems to gets an OMSResp -> 0x8d271bb399e025af8f405080550072006f0062008080 val accept_1 = hex"8c 6 0200 000 1000" val accept_2 = hex"8c 6 0400 000 1000" val reject_1 = hex"8c 8 0200 000 1000" diff --git a/src/test/scala/game/SquadMembershipResponseTest.scala b/src/test/scala/game/SquadMembershipResponseTest.scala index 6492b4f9..4ef9ffd3 100644 --- a/src/test/scala/game/SquadMembershipResponseTest.scala +++ b/src/test/scala/game/SquadMembershipResponseTest.scala @@ -8,23 +8,23 @@ import org.specs2.mutable._ import scodec.bits._ class SquadMembershipResponseTest extends Specification { - val string_01 = hex"6f0 00854518050db2260108048006f006600440000" - val string_02 = hex"6f0 0049e8220112aa1e01100530050004f0049004c0045005200530080" - val string_11 = hex"6f1 995364f2040000000100080" - val string_12 = hex"6f1 90cadcf4040000000100080" - val string_21 = hex"6f2 010db2260085451805140560069007200750073004700690076006500720080" - val string_22 = hex"6f2 010db22601da03aa03140560069007200750073004700690076006500720080" - val string_31 = hex"6f3 07631db202854518050a048004d0046004900430000" - val string_32 = hex"6f3 04c34fb402854518050e0440041004e00310031003100310000" - val string_41 = hex"6f4 04cadcf405bbbef405140530041007200610069007300560061006e00750000" - val string_42 = hex"6f4 05c9c0f405d71aec0516041006900720049006e006a006500630074006f00720000" - val string_51 = hex"6f5 0249e8220049e822010e0430043005200490044004500520080" - val string_71 = hex"6f7 1049e822000000000100080" - val string_72 = hex"6f7 00cadcf4041355ae03100570069007a006b00690064003400350080" - val string_81 = hex"6f8 001355ae02cadcf405100570069007a006b00690064003400350000" - val string_91 = hex"6f9 008310080115aef40500080" - val string_92 = hex"6f9 001355ae02cadcf405100570069007a006b00690064003400350000" - val string_b1 = hex"6fb 021355ae02cadcf405140530041007200610069007300560061006e00750000" + val string_01 = hex"6f 0 00854518050db2260108048006f006600440000" + val string_02 = hex"6f 0 0049e8220112aa1e01100530050004f0049004c0045005200530080" + val string_11 = hex"6f 1 995364f2040000000100080" + val string_12 = hex"6f 1 90cadcf4040000000100080" + val string_21 = hex"6f 2 010db2260085451805140560069007200750073004700690076006500720080" + val string_22 = hex"6f 2 010db22601da03aa03140560069007200750073004700690076006500720080" + val string_31 = hex"6f 3 07631db202854518050a048004d0046004900430000" + val string_32 = hex"6f 3 04c34fb402854518050e0440041004e00310031003100310000" + val string_41 = hex"6f 4 04cadcf405bbbef405140530041007200610069007300560061006e00750000" + val string_42 = hex"6f 4 05c9c0f405d71aec0516041006900720049006e006a006500630074006f00720000" + val string_51 = hex"6f 5 0249e8220049e822010e0430043005200490044004500520080" + val string_71 = hex"6f 7 1049e822000000000100080" + val string_72 = hex"6f 7 00cadcf4041355ae03100570069007a006b00690064003400350080" + val string_81 = hex"6f 8 001355ae02cadcf405100570069007a006b00690064003400350000" + val string_91 = hex"6f 9 008310080115aef40500080" + val string_92 = hex"6f 9 001355ae02cadcf405100570069007a006b00690064003400350000" + val string_b1 = hex"6f b 021355ae02cadcf405140530041007200610069007300560061006e00750000" "SquadMembershipResponse" should { "decode (0-1)" in { From 2555875d49c8855cc11ee6d80741020c4556eea9 Mon Sep 17 00:00:00 2001 From: Resaec Date: Tue, 2 Jan 2024 02:07:04 +0100 Subject: [PATCH 4/4] Cleaned up warnings --- .../packet/game/OutfitMembershipRequest.scala | 39 ++++++++++--------- 1 file changed, 21 insertions(+), 18 deletions(-) diff --git a/src/main/scala/net/psforever/packet/game/OutfitMembershipRequest.scala b/src/main/scala/net/psforever/packet/game/OutfitMembershipRequest.scala index eb0fdc12..55989fc6 100644 --- a/src/main/scala/net/psforever/packet/game/OutfitMembershipRequest.scala +++ b/src/main/scala/net/psforever/packet/game/OutfitMembershipRequest.scala @@ -1,6 +1,7 @@ // 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} @@ -16,9 +17,9 @@ final case class OutfitMembershipRequest( ) extends PlanetSideGamePacket { type Packet = OutfitMembershipRequest - def opcode = GamePacketOpcode.OutfitMembershipRequest + def opcode: Type = GamePacketOpcode.OutfitMembershipRequest - def encode = OutfitMembershipRequest.encode(this) + def encode: Attempt[BitVector] = OutfitMembershipRequest.encode(this) } abstract class OutfitAction(val code: Int) @@ -43,7 +44,7 @@ object OutfitAction { object Codecs { private val everFailCondition = conditional(included = false, bool) - val CreateOutfitCodec = + val CreateOutfitCodec: Codec[CreateOutfit] = (PacketHelpers.encodedWideString :: uint4L :: bool :: PacketHelpers.encodedWideString).xmap[CreateOutfit]( { case unk2 :: unk3 :: unk4 :: outfit_name :: HNil => @@ -55,7 +56,7 @@ object OutfitAction { } ) - val FormOutfitCodec = + val FormOutfitCodec: Codec[FormOutfit] = (PacketHelpers.encodedWideString :: uint4L :: bool :: PacketHelpers.encodedWideString).xmap[FormOutfit]( { case unk2 :: unk3 :: unk4 :: outfit_name :: HNil => @@ -67,8 +68,8 @@ object OutfitAction { } ) - val AcceptOutfitCodec = - (PacketHelpers.encodedWideString).xmap[AcceptOutfitInvite]( + val AcceptOutfitCodec: Codec[AcceptOutfitInvite] = + PacketHelpers.encodedWideString.xmap[AcceptOutfitInvite]( { case unk2 => AcceptOutfitInvite(unk2) @@ -79,8 +80,8 @@ object OutfitAction { } ) - val RejectOutfitCodec = - (PacketHelpers.encodedWideString).xmap[RejectOutfitInvite]( + val RejectOutfitCodec: Codec[RejectOutfitInvite] = + PacketHelpers.encodedWideString.xmap[RejectOutfitInvite]( { case unk2 => RejectOutfitInvite(unk2) @@ -91,7 +92,7 @@ object OutfitAction { } ) - val CancelOutfitCodec = + val CancelOutfitCodec: Codec[CancelOutfitInvite] = (uint16L :: uint16L :: PacketHelpers.encodedWideStringAligned(5)).xmap[CancelOutfitInvite]( { case unk5 :: unk6 :: outfit_name :: HNil => @@ -108,7 +109,7 @@ object OutfitAction { * @param action the action behavior code * @return a transformation between the action code and the unknown bit data */ - def unknownCodec(action: Int) = + def unknownCodec(action: Int): Codec[Unknown] = bits.xmap[Unknown]( data => Unknown(action, data), { @@ -121,7 +122,7 @@ object OutfitAction { * @param action the action behavior code * @return nothing; always fail */ - def failureCodec(action: Int) = + def failureCodec(action: Int): Codec[OutfitAction] = 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")) @@ -134,17 +135,19 @@ object OutfitMembershipRequest extends Marshallable[OutfitMembershipRequest] { object RequestType extends Enumeration { type Type = Value - val Create = Value(0) - val Form = Value(1) - val Unk2 = Value(2) - val Accept = Value(3) - val Reject = Value(4) - val Cancel = Value(5) + val Create: RequestType.Value = Value(0) + 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) implicit val codec: Codec[Type] = PacketHelpers.createEnumerationCodec(this, uintL(3)) } - def selectFromType(code: Int): Codec[OutfitAction] = { + private def selectFromType(code: Int): Codec[OutfitAction] = { import OutfitAction.Codecs._ import scala.annotation.switch