From 4bc272c2f0e3f0b9fa7d2dc93668d0abcb4e26b0 Mon Sep 17 00:00:00 2001 From: Resaec Date: Mon, 22 Apr 2024 03:12:55 +0200 Subject: [PATCH] Add OutfitEvent packet Add OutfitMemberUpdate packet --- .../psforever/packet/GamePacketOpcode.scala | 4 +- .../psforever/packet/game/OutfitEvent.scala | 177 ++++++++++++++++++ .../packet/game/OutfitMemberUpdate.scala | 33 ++++ .../packet/game/OutfitMembershipRequest.scala | 10 +- src/test/scala/game/OutfitEventTest.scala | 32 ++++ 5 files changed, 249 insertions(+), 7 deletions(-) create mode 100644 src/main/scala/net/psforever/packet/game/OutfitEvent.scala create mode 100644 src/main/scala/net/psforever/packet/game/OutfitMemberUpdate.scala create mode 100644 src/test/scala/game/OutfitEventTest.scala diff --git a/src/main/scala/net/psforever/packet/GamePacketOpcode.scala b/src/main/scala/net/psforever/packet/GamePacketOpcode.scala index e2f6bea2b..2dda57664 100644 --- a/src/main/scala/net/psforever/packet/GamePacketOpcode.scala +++ b/src/main/scala/net/psforever/packet/GamePacketOpcode.scala @@ -471,11 +471,11 @@ object GamePacketOpcode extends Enumeration { case 0x8c => game.OutfitMembershipRequest.decode case 0x8d => game.OutfitMembershipResponse.decode case 0x8e => game.OutfitRequest.decode - case 0x8f => noDecoder(OutfitEvent) + case 0x8f => game.OutfitEvent.decode // OPCODES 0x90-9f case 0x90 => noDecoder(OutfitMemberEvent) - case 0x91 => noDecoder(OutfitMemberUpdate) + case 0x91 => game.OutfitMemberUpdate.decode case 0x92 => game.PlanetsideStringAttributeMessage.decode case 0x93 => game.DataChallengeMessage.decode case 0x94 => game.DataChallengeMessageResp.decode diff --git a/src/main/scala/net/psforever/packet/game/OutfitEvent.scala b/src/main/scala/net/psforever/packet/game/OutfitEvent.scala new file mode 100644 index 000000000..98ead8850 --- /dev/null +++ b/src/main/scala/net/psforever/packet/game/OutfitEvent.scala @@ -0,0 +1,177 @@ +// Copyright (c) 2024 PSForever +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.bits.BitVector +import scodec.codecs._ +import shapeless.{::, HNil} + +final case class OutfitEvent( + request_type: OutfitEvent.RequestType.Type, + unk1: Int, + action: OutfitEventAction + ) extends PlanetSideGamePacket { + type Packet = OutfitEvent + + def opcode: Type = GamePacketOpcode.OutfitEvent + + def encode: Attempt[BitVector] = OutfitEvent.encode(this) +} + +abstract class OutfitEventAction(val code: Int) + +object OutfitEventAction { + + final case class CreatedOutfit( + unk2: Int, + unk3: Int, + unk4: Int, + outfit_name: String, + unk6: Long, + unk7: Long, + members: Int, + unk9: Int, + unk10: String, + unk11: String, + unk12: String, + unk13: String, + unk14: String, + unk15: String, + unk16: String, + unk17: String, + unk18: String, + unk19: Int, + unk20: Int, + unk21: Long, + unk22: Long, + unk23: Long, + unk24: Long, + unk25: Int, + ) extends OutfitEventAction(code = 4) + + final case class Unknown(badCode: Int, data: BitVector) extends OutfitEventAction(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 CreatedOutfitCodec: Codec[CreatedOutfit] = + (uint8L :: + uint4L :: + uintL(3) :: + PacketHelpers.encodedWideStringAligned(5) :: + uint32L :: + uint32L :: + uint16L :: + uint16L :: + PacketHelpers.encodedString :: + PacketHelpers.encodedString :: + PacketHelpers.encodedString :: + PacketHelpers.encodedString :: + PacketHelpers.encodedString :: + PacketHelpers.encodedString :: + PacketHelpers.encodedString :: + PacketHelpers.encodedString :: + PacketHelpers.encodedString :: + uint16L :: + uint16L :: + uint32L :: + uint32L :: + uint32L :: + uint32L :: + uint16L + ).xmap[CreatedOutfit]( + { + case u2 :: u3 :: u4 :: outfit_name :: u6 :: u7 :: members :: u9 :: u10 :: u11 :: u12 :: u13 :: u14 :: u15 :: u16 :: u17 :: u18 :: u19 :: u20 :: u21 :: u22 :: u23 :: u24 :: u25 :: HNil => + CreatedOutfit(u2, u3, u4, outfit_name, u6, u7, members, u9, u10, u11, u12, u13, u14, u15, u16, u17, u18, u19, u20, u21, u22, u23, u24, u25) + }, + { + case CreatedOutfit(u2, u3, u4, outfit_name, u6, u7, members, u9, u10, u11, u12, u13, u14, u15, u16, u17, u18, u19, u20, u21, u22, u23, u24, u25) => + u2 :: u3 :: u4 :: outfit_name :: u6 :: u7 :: members :: u9 :: u10 :: u11 :: u12 :: u13 :: u14 :: u15 :: u16 :: u17 :: u18 :: u19 :: u20 :: u21 :: u22 :: u23 :: u24 :: u25 :: 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[OutfitEventAction] = + everFailCondition.exmap[OutfitEventAction]( + _ => 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 OutfitEvent extends Marshallable[OutfitEvent] { + + object RequestType extends Enumeration { + type Type = Value + + val unk0: RequestType.Value = Value(0) + val unk1: RequestType.Value = Value(1) + val unk2: RequestType.Value = Value(2) + val unk3: RequestType.Value = Value(3) + val CreatedOutfit: RequestType.Value = Value(4) + val unk5: RequestType.Value = Value(5) + val Unk6: RequestType.Value = Value(6) + val Unk7: RequestType.Value = Value(7) + + implicit val codec: Codec[Type] = PacketHelpers.createEnumerationCodec(this, uint4L) + } + + private def selectFromType(code: Int): Codec[OutfitEventAction] = { + import OutfitEventAction.Codecs._ + import scala.annotation.switch + + ((code: @switch) match { + case 0 => unknownCodec(action = code) + case 1 => unknownCodec(action = code) + case 2 => unknownCodec(action = code) + case 3 => unknownCodec(action = code) + case 4 => CreatedOutfitCodec // sent after /outfitcreate + case 5 => unknownCodec(action = code) + case 6 => unknownCodec(action = code) + case 7 => unknownCodec(action = code) + + case _ => failureCodec(code) + }).asInstanceOf[Codec[OutfitEventAction]] + } + + implicit val codec: Codec[OutfitEvent] = ( + ("request_type" | RequestType.codec) >>:~ { request_type => + ("avatar_guid" | uint16L) :: + ("action" | selectFromType(request_type.id)) + } + ).xmap[OutfitEvent]( + { + case request_type :: avatar_guid :: action :: HNil => + OutfitEvent(request_type, avatar_guid, action) + }, + { + case OutfitEvent(request_type, avatar_guid, action) => + request_type :: avatar_guid :: action :: HNil + } + ) +} diff --git a/src/main/scala/net/psforever/packet/game/OutfitMemberUpdate.scala b/src/main/scala/net/psforever/packet/game/OutfitMemberUpdate.scala new file mode 100644 index 000000000..b4012fc06 --- /dev/null +++ b/src/main/scala/net/psforever/packet/game/OutfitMemberUpdate.scala @@ -0,0 +1,33 @@ +// Copyright (c) 2024 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._ +import shapeless.{::, HNil} + +final case class OutfitMemberUpdate(outfit_id: PlanetSideGUID, unk1: Int, unk2: Int, unk3: Int) extends PlanetSideGamePacket { + type Packet = OutfitMemberUpdate + def opcode = GamePacketOpcode.OutfitMemberUpdate + def encode = OutfitMemberUpdate.encode(this) +} + +object OutfitMemberUpdate extends Marshallable[OutfitMemberUpdate] { + implicit val codec: Codec[OutfitMemberUpdate] = ( + ("outfit_id" | PlanetSideGUID.codec) :: + ("unk1" | uint16L) :: + ("unk2" | uint16L) :: + ("unk3" | uint8L) + ).xmap[OutfitMemberUpdate]( + { + case u0 :: u1 :: u2 :: u3 :: HNil => + OutfitMemberUpdate(u0, u1, u2, u3) + }, + { + case OutfitMemberUpdate(u0, u1, u2, u3) => + u0 :: u1 :: u2 :: u3 :: HNil + } + ) +// ).as[OutfitMemberUpdate] +} diff --git a/src/main/scala/net/psforever/packet/game/OutfitMembershipRequest.scala b/src/main/scala/net/psforever/packet/game/OutfitMembershipRequest.scala index 420270ee8..979f1417d 100644 --- a/src/main/scala/net/psforever/packet/game/OutfitMembershipRequest.scala +++ b/src/main/scala/net/psforever/packet/game/OutfitMembershipRequest.scala @@ -1,4 +1,4 @@ -// Copyright (c) 2023 PSForever +// Copyright (c) 2024 PSForever package net.psforever.packet.game import net.psforever.packet.GamePacketOpcode.Type @@ -139,13 +139,13 @@ 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)) } diff --git a/src/test/scala/game/OutfitEventTest.scala b/src/test/scala/game/OutfitEventTest.scala new file mode 100644 index 000000000..38494ac61 --- /dev/null +++ b/src/test/scala/game/OutfitEventTest.scala @@ -0,0 +1,32 @@ +// Copyright (c) 2023 PSForever +package game + +import net.psforever.packet._ +import net.psforever.packet.game.OutfitEvent.RequestType +import net.psforever.packet.game.OutfitEventAction._ +import net.psforever.packet.game._ +import org.specs2.mutable._ +import scodec.bits._ + +class OutfitEventTest extends Specification { + val created_ABC = hex"8f 4 0201 feff 2e 0 50006c0061006e006500740053006900640065005f0046006f00720065007600650072005f00560061006e007500 00000000 00000000 0100 0000 80 80 80 80 80 80 80 80 80 0070 4982 00000000 00000000 00000000 00000000 0000" + + + "decode CreatedOutfit ABC" in { + PacketCoding.decodePacket(created_ABC).require match { + case OutfitEvent(request_type, unk1, action) => + request_type mustEqual RequestType.CreatedOutfit + unk1 mustEqual 258 + action mustEqual CreatedOutfit(unk2 = 254, unk3 = 15, unk4 = 7, outfit_name = "PlanetSide_Forever_Vanu", unk6 = 0, unk7 = 0, members = 1, unk9 = 0, "", "", "", "", "", "", "", "", "", 28672, 33353, 0, 0, 0, 0, 0) + case _ => + ko + } + } + + "encode CreatedOutfit ABC" in { + val msg = OutfitEvent(RequestType.CreatedOutfit, 258, CreatedOutfit(unk2 = 254, unk3 = 15, unk4 = 7, outfit_name = "PlanetSide_Forever_Vanu", unk6 = 0, unk7 = 0, members = 1, unk9 = 0, "", "", "", "", "", "", "", "", "", 28672, 33353, 0, 0, 0, 0, 0)) + val pkt = PacketCoding.encodePacket(msg).require.toByteVector + + pkt mustEqual created_ABC + } +}