From f7f734296b2f249c10dd1402f72bad664df7ea46 Mon Sep 17 00:00:00 2001 From: Resaec Date: Thu, 28 Dec 2023 23:02:48 +0100 Subject: [PATCH] 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 + } }