diff --git a/src/main/scala/net/psforever/packet/GamePacketOpcode.scala b/src/main/scala/net/psforever/packet/GamePacketOpcode.scala index 0253d2b80..a1662d308 100644 --- a/src/main/scala/net/psforever/packet/GamePacketOpcode.scala +++ b/src/main/scala/net/psforever/packet/GamePacketOpcode.scala @@ -437,7 +437,7 @@ object GamePacketOpcode extends Enumeration { // OPCODES 0x70-7f case 0x70 => game.SquadMemberEvent.decode - case 0x71 => noDecoder(PlatoonEvent) + case 0x71 => game.PlatoonEvent.decode case 0x72 => game.FriendsRequest.decode case 0x73 => game.FriendsResponse.decode case 0x74 => game.TriggerEnvironmentalDamageMessage.decode diff --git a/src/main/scala/net/psforever/packet/game/PlatoonEvent.scala b/src/main/scala/net/psforever/packet/game/PlatoonEvent.scala new file mode 100644 index 000000000..c225f155d --- /dev/null +++ b/src/main/scala/net/psforever/packet/game/PlatoonEvent.scala @@ -0,0 +1,51 @@ +// Copyright (c) 2025 PSForever +package net.psforever.packet.game + +import net.psforever.packet.GamePacketOpcode.Type +import net.psforever.packet.{GamePacketOpcode, Marshallable, PacketHelpers, PlanetSideGamePacket} +import scodec.{Attempt, Codec} +import scodec.bits.BitVector +import scodec.codecs._ +import shapeless.{::, HNil} + +final case class PlatoonEvent( + packet_type: PlatoonEvent.PacketType.Type, // only seen 0 and 1 + unk0: Int, // squad size? + squad_supplement_id: Int, + squad_ui_index: Int + ) extends PlanetSideGamePacket { + type Packet = PlatoonEvent + + def opcode: Type = GamePacketOpcode.PlatoonEvent + def encode: Attempt[BitVector] = PlatoonEvent.encode(this) +} + +object PlatoonEvent extends Marshallable[PlatoonEvent] { + + object PacketType extends Enumeration { + type Type = Value + + val AddSquad: PacketType.Value = Value(0) // Add / Update? + val RemoveSquad: PacketType.Value = Value(1) + val Unk2: PacketType.Value = Value(2) + val Unk3: PacketType.Value = Value(3) // seen as decode error, Squad ID too high + + implicit val codec: Codec[Type] = PacketHelpers.createEnumerationCodec(this, uintL(2)) + } + + implicit val codec: Codec[PlatoonEvent] = ( + ("packet_type" | PacketType.codec) :: + ("unk0" | uint16L) :: + ("squad_supplement_id" | uint16L) :: + ("squad_ui_index" | uintL(2)) + ).xmap[PlatoonEvent]( + { + case packet_type :: u0 :: squad_supplement_id :: squad_ui_index :: HNil => + PlatoonEvent(packet_type, u0, squad_supplement_id, squad_ui_index) + }, + { + case PlatoonEvent(packet_type, u0, squad_supplement_id, squad_ui_index) => + packet_type :: u0 :: squad_supplement_id :: squad_ui_index :: HNil + } + ) +} diff --git a/src/test/scala/game/PlatoonEventTest.scala b/src/test/scala/game/PlatoonEventTest.scala new file mode 100644 index 000000000..868c5ea9a --- /dev/null +++ b/src/test/scala/game/PlatoonEventTest.scala @@ -0,0 +1,63 @@ +// Copyright (c) 2025 PSForever +package game + +import net.psforever.packet._ +import net.psforever.packet.game.PlatoonEvent +import net.psforever.packet.game.PlatoonEvent.PacketType._ +import org.specs2.mutable._ +import scodec.bits.ByteVector + +class PlatoonEventTest extends Specification { + + val add_squad: ByteVector = ByteVector.fromValidHex("71 0200 0040 00") + val remove_squad: ByteVector = ByteVector.fromValidHex("71 4280 0300 20") + + "decode addSquad" in { + PacketCoding.decodePacket(add_squad).require match { + case PlatoonEvent(action, unk0, squad_supplement_id, squad_ui_index) => + action mustEqual AddSquad + unk0 mustEqual 8 + squad_supplement_id mustEqual 1 + squad_ui_index mustEqual 0 + case _ => + ko + } + } + + "encode addSquad" in { + val msg = PlatoonEvent( + AddSquad, + unk0 = 8, + squad_supplement_id = 1, + squad_ui_index = 0 + ) + val pkt = PacketCoding.encodePacket(msg).require.toByteVector + + pkt mustEqual add_squad + } + + "decode removeSquad" in { + PacketCoding.decodePacket(remove_squad).require match { + case PlatoonEvent(action, unk0, squad_supplement_id, squad_ui_index) => + action mustEqual RemoveSquad + unk0 mustEqual 10 + squad_supplement_id mustEqual 12 + squad_ui_index mustEqual 2 + case _ => + ko + } + } + + "encode removeSquad" in { + val msg = PlatoonEvent( + RemoveSquad, + unk0 = 10, + squad_supplement_id = 12, + squad_ui_index = 2 + ) + val pkt = PacketCoding.encodePacket(msg).require.toByteVector + + pkt mustEqual remove_squad + } + +}