diff --git a/src/main/scala/net/psforever/packet/GamePacketOpcode.scala b/src/main/scala/net/psforever/packet/GamePacketOpcode.scala index 6bc06e6d3..0253d2b80 100644 --- a/src/main/scala/net/psforever/packet/GamePacketOpcode.scala +++ b/src/main/scala/net/psforever/packet/GamePacketOpcode.scala @@ -1,4 +1,4 @@ -// Copyright (c) 2017 PSForever +// Copyright (c) 2017-2025 PSForever package net.psforever.packet import scodec.{Attempt, Codec, DecodeResult, Err} @@ -469,13 +469,13 @@ object GamePacketOpcode extends Enumeration { case 0x8a => game.PlayerStasisMessage.decode case 0x8b => noDecoder(UnknownMessage139) case 0x8c => game.OutfitMembershipRequest.decode - case 0x8d => noDecoder(OutfitMembershipResponse) + 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 0x90 => game.OutfitMemberEvent.decode + case 0x91 => game.OutfitMemberUpdate.decode case 0x92 => game.PlanetsideStringAttributeMessage.decode case 0x93 => game.DataChallengeMessage.decode case 0x94 => game.DataChallengeMessageResp.decode @@ -483,7 +483,7 @@ object GamePacketOpcode extends Enumeration { case 0x96 => game.SimDataChallenge.decode case 0x97 => game.SimDataChallengeResp.decode // 0x98 - case 0x98 => noDecoder(OutfitListEvent) + case 0x98 => game.OutfitListEvent.decode case 0x99 => noDecoder(EmpireIncentivesMessage) case 0x9a => game.InvalidTerrainMessage.decode case 0x9b => noDecoder(SyncMessage) 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..cfbd647b9 --- /dev/null +++ b/src/main/scala/net/psforever/packet/game/OutfitEvent.scala @@ -0,0 +1,358 @@ +// Copyright (c) 2025 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} +import scodec.bits.BitVector +import scodec.codecs._ +import shapeless.{::, HNil} + +final case class OutfitEvent( + request_type: OutfitEvent.RequestType.Type, + outfit_guid: PlanetSideGUID, + 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 OutfitRankNames( + rank1: String, + rank2: String, + rank3: String, + rank4: String, + rank5: String, + rank6: String, + rank7: String, + rank8: String, + ) + + final case class OutfitInfo( + unk1: Int, + unk2: Int, + outfit_name: String, + unk6: Long, + unk7: Long, + member_count: Int, + unk9: Int, + outfit_rank_names: OutfitRankNames, + motd: String, + owner_guid: PlanetSideGUID, // ? + unk20: Int, + unk21: Int, + unk21_2: Int, + created_timestamp: Long, + unk23: Long, + unk24: Long, + unk25: Long, + u123: Int, + ) + + final case class Unk0( + outfitInfo: OutfitInfo + ) extends OutfitEventAction(code = 0) + + final case class Unk1( + unk0: Int, + unk1: Int, + unk2: Int, + unk3: Boolean, + ) extends OutfitEventAction(code = 1) + + final case class Unk2( + outfitInfo: OutfitInfo, + ) extends OutfitEventAction(code = 2) + + final case class Unk3( + unk0: Int, + unk1: Int, + unk2: Int, + unk3: Boolean, + data: BitVector, + ) extends OutfitEventAction(code = 3) + + final case class Unk4( + unk0: Int, + unk1: Int, + unk2: Int, + unk3: Int, + unk4: Boolean, + data: BitVector, + ) extends OutfitEventAction(code = 4) + + final case class Unk5( + unk0: Int, + unk1: Int, + unk2: Int, + unk3: Int, + unk4: Boolean, + data: BitVector, + ) extends OutfitEventAction(code = 5) + + 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) + + private val OutfitRankNamesCodec: Codec[OutfitRankNames] = ( + PacketHelpers.encodedWideString :: + PacketHelpers.encodedWideString :: + PacketHelpers.encodedWideString :: + PacketHelpers.encodedWideString :: + PacketHelpers.encodedWideString :: + PacketHelpers.encodedWideString :: + PacketHelpers.encodedWideString :: + PacketHelpers.encodedWideString + ).xmap[OutfitRankNames]( + { + case u0 :: u1 :: u2 :: u3 :: u4 :: u5 :: u6 :: u7 :: HNil => + OutfitRankNames(u0, u1, u2, u3, u4, u5, u6, u7) + }, + { + case OutfitRankNames(u0, u1, u2, u3, u4, u5, u6, u7) => + u0 :: u1 :: u2 :: u3 :: u4 :: u5 :: u6 :: u7 :: HNil + } + ) + + private val InfoCodec: Codec[OutfitInfo] = ( + uint8L :: + uint8L :: + PacketHelpers.encodedWideStringAligned(5) :: + uint32L :: + uint32L :: + uint16L :: + uint16L :: + OutfitRankNamesCodec :: + PacketHelpers.encodedWideString :: + PlanetSideGUID.codec :: + uint16L :: // + uint8L :: // bool somewhere here + uintL(1) :: // + ("created_timestamp" | uint32L) :: + uint32L :: + uint32L :: + uint32L :: + uintL(7) + ).xmap[OutfitInfo]( + { + case u1 :: u2 :: outfit_name :: u6 :: u7 :: member_count :: u9 :: outfit_rank_names :: motd :: u19 :: u20 :: u21 :: u21_2 :: created_timestamp :: u23 :: u24 :: u25 :: u123 :: HNil => + OutfitInfo(u1, u2, outfit_name, u6, u7, member_count, u9, outfit_rank_names, motd, u19, u20, u21, u21_2, created_timestamp, u23, u24, u25, u123) + }, + { + case OutfitInfo(u1, u2, outfit_name, u6, u7, member_count, u9, outfit_rank_names, motd, u19, u20, u21, u21_2, created_timestamp, u23, u24, u25, u123) => + u1 :: u2 :: outfit_name :: u6 :: u7 :: member_count :: u9 :: outfit_rank_names :: motd :: u19 :: u20 :: u21 :: u21_2 :: created_timestamp :: u23 :: u24 :: u25 :: u123 :: HNil + } + ) + + val Unk0Codec: Codec[Unk0] = ( + InfoCodec + ).xmap[Unk0]( + { + case info => + Unk0(info) + }, + { + case Unk0(info) => + info + } + ) + + val Unk1Codec: Codec[Unk1] = ( + uint8L :: + uint8L :: + uint4L :: + bool + ).xmap[Unk1]( + { + case u0 :: u1 :: u2 :: u3 :: HNil => + Unk1(u0, u1, u2, u3) + }, + { + case Unk1(u0, u1, u2, u3) => + u0 :: u1 :: u2 :: u3 :: HNil + } + ) + + val Unk2Codec: Codec[Unk2] = ( + InfoCodec + ).xmap[Unk2]( + { + case info => + Unk2(info) + }, + { + case Unk2(info) => + info + } + ) + + val Unk3Codec: Codec[Unk3] = ( + uint8L :: + uint8L :: + uint4L :: + bool :: + bits + ).xmap[Unk3]( + { + case u0 :: u1 :: u2 :: u3 :: data :: HNil => + Unk3(u0, u1, u2, u3, data) + }, + { + case Unk3(u0, u1, u2, u3, data) => + u0 :: u1 :: u2 :: u3 :: data :: HNil + } + ) + + val Unk4Codec: Codec[Unk4] = ( + uint16L :: + uint16L :: + uint16L :: + uint4L :: + bool :: + bits + ).xmap[Unk4]( + { + case u0 :: u1 :: u2 :: u3 :: u4 :: data :: HNil => + Unk4(u0, u1, u2, u3, u4, data) + }, + { + case Unk4(u0, u1, u2, u3, u4, data) => + u0 :: u1 :: u2 ::u3 :: u4 :: data :: HNil + } + ) + + val Unk5Codec: Codec[Unk5] = ( + uint16L :: + uint16L :: + uint16L :: + uint4L :: + bool :: + bits + ).xmap[Unk5]( + { + case u0 :: u1 :: u2 :: u3 :: u4 :: data :: HNil => + Unk5(u0, u1, u2, u3, u4, data) + }, + { + case Unk5(u0, u1, u2, u3, u4, data) => + u0 :: u1 :: u2 :: u3 :: u4 :: data :: 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) // start listing of members + val Unk1: RequestType.Value = Value(1) // end listing of members + val Unk2: RequestType.Value = Value(2) // send after creating an outfit // normal info, same as Unk0 + val Unk3: RequestType.Value = Value(3) // below + val Unk4: RequestType.Value = Value(4) + val Unk5: RequestType.Value = Value(5) + val unk6: RequestType.Value = Value(6) + val unk7: RequestType.Value = Value(7) + + /* + + OutfitEvent(Unk0, ValidPlanetSideGUID(18361), Unk0(OutfitInfo(0, 0, The Black Ravens, 338420223, 338420223, 433, 0, OutfitRankNames(Corporal (No Ventrilo), Sergeant - SGT, Advance Medical, , Master Sgt - MSG, Captain, Trusted Officer, OutFit Leader), TBR website..... http://trravens.darkbb.com ventrilo info: evolve.typefrag.com port: 45694 (vent pw dotaftw) Channel PW: zeroenigma : if you guys wants to contact me, my email is zero_overkill99@yahoo.com, ValidPlanetSideGUID(32787), 0, 0, 0, 1133571390, 0, 0, 0, 0))) + OutfitEvent(Unk2, ValidPlanetSideGUID(18361), Unk2(OutfitInfo(0, 0, The Black Ravens, 338420486, 338420486, 433, 0, OutfitRankNames(Corporal (No Ventrilo), Sergeant - SGT, Advance Medical, , Master Sgt - MSG, Captain, Trusted Officer, OutFit Leader), TBR website..... http://trravens.darkbb.com ventrilo info: evolve.typefrag.com port: 45694 (vent pw dotaftw) Channel PW: zeroenigma : if you guys wants to contact me, my email is zero_overkill99@yahoo.com, ValidPlanetSideGUID(32787), 0, 0, 0, 1133571390, 0, 0, 0, 0))) + + + unk3 -- #66162 PSCap-2016-02-28_02-58-10-PM.txt + + MP( + SMP( + MPEx( + OutfitMembershipResponse, + OutfitEvent, + SquadMemberEvent + ) + ), + SMP( + MPEx( + PlanetsideAttributeMessage x 3 + PlanetsideStringAttributeMessage + ) + ) + ) + ) + */ + + implicit val codec: Codec[Type] = PacketHelpers.createEnumerationCodec(this, uintL(3)) + } + + private def selectFromType(code: Int): Codec[OutfitEventAction] = { + import OutfitEventAction.Codecs._ + import scala.annotation.switch + + ((code: @switch) match { + case 0 => Unk0Codec + case 1 => Unk1Codec + case 2 => Unk2Codec // sent after /outfitcreate ? + case 3 => Unk3Codec + case 4 => Unk4Codec + case 5 => Unk5Codec + 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 => + ("outfit_guid" | PlanetSideGUID.codec) :: + ("action" | selectFromType(request_type.id)) + } + ).xmap[OutfitEvent]( + { + case request_type :: outfit_guid :: action :: HNil => + OutfitEvent(request_type, outfit_guid, action) + }, + { + case OutfitEvent(request_type, outfit_guid, action) => + request_type :: outfit_guid :: action :: HNil + } + ) +} diff --git a/src/main/scala/net/psforever/packet/game/OutfitListEvent.scala b/src/main/scala/net/psforever/packet/game/OutfitListEvent.scala new file mode 100644 index 000000000..52ea9929e --- /dev/null +++ b/src/main/scala/net/psforever/packet/game/OutfitListEvent.scala @@ -0,0 +1,35 @@ +// 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.bits.BitVector +import scodec.codecs._ +import scodec.{Attempt, Codec} + +// 98 5ec300000d01a020004000001 12056002e0053002e0053002e0055002e004400 845400680069006f009 85ee300001e2b 858000800000110041002e0027002e0041002e0027002e00 8448006900720075009 84003200005a540000060000011a0530065006300720065007400200043006800690065006600730085530069006c00610073009840a32000001953476fe0c00011c041007a0075007200650020005400770069006c006900670068007400874600720061006e0063006b006f009840c3200000d3a4c000c00000106030002e006f0085410074006c0061007300984183200011d9296000c0000011e0570061007200720069006f007200270073002000430072006500650064008653006500760061006b00690098442320001bf40e000080000013203100330033003700740068002000410072006d006f0072006500640020004400690076006900730069006f006e002d004b008548006f0073002d004b009844c320001b3d2c200060000012a03300330031007300740020004d0069006e006e00650073006f0074006100200054007200690062006500864d006100670069002d0045009846c3200009e206c00040000010c04100720065006100350031008942006c00610063 + +final case class OutfitListEvent( + outfit_score: Long, + unk1: Long, + unk2: Long, + unk3: Int, + outfit_name: String, + outfit_leader: String, + ) extends PlanetSideGamePacket { + type Packet = OutfitListEvent + + def opcode: Type = GamePacketOpcode.OutfitListEvent + + def encode: Attempt[BitVector] = OutfitListEvent.encode(this) +} +object OutfitListEvent extends Marshallable[OutfitListEvent] { + implicit val codec: Codec[OutfitListEvent] = ( + ("outfit_score" | uint32) :: + ("unk1" | uint32L) :: + ("unk2" | uint32L) :: + ("unk3" | uint(3)) :: + ("outfit_name" | PacketHelpers.encodedWideStringAligned(5)) :: + ("outfit_leader" | PacketHelpers.encodedWideString) + ).as[OutfitListEvent] +} diff --git a/src/main/scala/net/psforever/packet/game/OutfitMemberEvent.scala b/src/main/scala/net/psforever/packet/game/OutfitMemberEvent.scala new file mode 100644 index 000000000..737a72da8 --- /dev/null +++ b/src/main/scala/net/psforever/packet/game/OutfitMemberEvent.scala @@ -0,0 +1,52 @@ +// Copyright (c) 2025 PSForever +package net.psforever.packet.game + +import net.psforever.packet.{GamePacketOpcode, Marshallable, PacketHelpers, PlanetSideGamePacket} +import scodec.Codec +import scodec.codecs._ +import shapeless.{::, HNil} + +final case class OutfitMemberEvent( + unk00: Int, + outfit_id: Long, + unk3: Int, + unk4: Int, + unk5: Int, + unk6: Int, + member_name: String, + unk7: Int, + unk8: Int, + unk9: Int, + unk10: Int, + unk11: Int, +) extends PlanetSideGamePacket { + type Packet = OutfitMemberEvent + def opcode = GamePacketOpcode.OutfitMemberEvent + def encode = OutfitMemberEvent.encode(this) +} + +object OutfitMemberEvent extends Marshallable[OutfitMemberEvent] { + implicit val codec: Codec[OutfitMemberEvent] = ( + ("unk00" | uintL(2)) :: + ("outfit_id" | uint32L) :: + ("unk3" | uint8L) :: + ("unk4" | uint8L) :: + ("unk5" | uint8L) :: + ("unk6" | uint8L) :: + ("member_name" | PacketHelpers.encodedWideStringAligned(6)) :: + ("unk7" | uint16L) :: + ("unk8" | uint16L) :: + ("unk9" | uint16L) :: + ("unk10" | uint16L) :: + ("unk11" | uint8L) + ).xmap[OutfitMemberEvent]( + { + case unk00 :: outfit_id :: u3 :: u4 :: u5 :: u6 :: member_name :: u7 :: u8 :: u9 :: u10 :: u11 :: HNil => + OutfitMemberEvent(unk00, outfit_id, u3, u4, u5, u6, member_name, u7, u8, u9, u10, u11) + }, + { + case OutfitMemberEvent(unk00, outfit_id, u3, u4, u5, u6, member_name, u7, u8, u9, u10, u11) => + unk00 :: outfit_id :: u3 :: u4 :: u5 :: u6 :: member_name :: u7 :: u8 :: u9 :: u10 :: u11 :: 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..9fc1a72ee --- /dev/null +++ b/src/main/scala/net/psforever/packet/game/OutfitMemberUpdate.scala @@ -0,0 +1,37 @@ +// Copyright (c) 2025 PSForever +package net.psforever.packet.game + +import net.psforever.packet.{GamePacketOpcode, Marshallable, PlanetSideGamePacket} +import net.psforever.types.PlanetSideGUID +import scodec.Codec +import scodec.codecs._ +import shapeless.{::, HNil} + +final case class OutfitMemberUpdate( + outfit_guid: PlanetSideGUID, + unk1: Int, + avatar_guid: PlanetSideGUID, + 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_guid" | PlanetSideGUID.codec) :: + ("unk1" | uint16L) :: + ("avatar_guid" | PlanetSideGUID.codec) :: + ("unk3" | uint8L) + ).xmap[OutfitMemberUpdate]( + { + case outfit_guid :: u1 :: u2 :: u3 :: HNil => + OutfitMemberUpdate(outfit_guid, u1, u2, u3) + }, + { + case OutfitMemberUpdate(outfit_guid, u1, u2, u3) => + outfit_guid :: u1 :: u2 :: u3 :: HNil + } + ) +} diff --git a/src/main/scala/net/psforever/packet/game/OutfitMembershipRequest.scala b/src/main/scala/net/psforever/packet/game/OutfitMembershipRequest.scala index 55989fc66..b64a68cec 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) 2023-2025 PSForever package net.psforever.packet.game import net.psforever.packet.GamePacketOpcode.Type @@ -11,9 +11,9 @@ import shapeless.{::, HNil} final case class OutfitMembershipRequest( request_type: OutfitMembershipRequest.RequestType.Type, - avatar_guid: PlanetSideGUID, - unk1: Int, - action: OutfitAction + avatar_guid: PlanetSideGUID, // avatar_guid and unk1 are related, might be Long instead + unk1: Int, // + action: OutfitMembershipRequestAction ) extends PlanetSideGamePacket { type Packet = OutfitMembershipRequest @@ -22,20 +22,21 @@ final case class OutfitMembershipRequest( def encode: Attempt[BitVector] = OutfitMembershipRequest.encode(this) } -abstract class OutfitAction(val code: Int) -object OutfitAction { +abstract class OutfitMembershipRequestAction(val code: Int) - final case class CreateOutfit(unk2: String, unk3: Int, unk4: Boolean, outfit_name: String) extends OutfitAction(code = 0) +object OutfitMembershipRequestAction { - final case class FormOutfit(unk2: String, unk3: Int, unk4: Boolean, outfit_name: String) extends OutfitAction(code = 1) + final case class CreateOutfit(unk2: String, unk3: Int, unk4: Boolean, outfit_name: String) extends OutfitMembershipRequestAction(code = 0) - final case class AcceptOutfitInvite(unk2: String) extends OutfitAction(code = 3) + final case class FormOutfit(unk2: String, unk3: Int, unk4: Boolean, outfit_name: String) extends OutfitMembershipRequestAction(code = 1) - final case class RejectOutfitInvite(unk2: String) extends OutfitAction(code = 4) + final case class AcceptOutfitInvite(unk2: String) extends OutfitMembershipRequestAction(code = 3) - final case class CancelOutfitInvite(unk5: Int, unk6: Int, outfit_name: String) extends OutfitAction(code = 5) + final case class RejectOutfitInvite(unk2: String) extends OutfitMembershipRequestAction(code = 4) - final case class Unknown(badCode: Int, data: BitVector) extends OutfitAction(badCode) + final case class CancelOutfitInvite(unk5: Int, unk6: Int, outfit_name: String) extends OutfitMembershipRequestAction(code = 5) + + final case class Unknown(badCode: Int, data: BitVector) extends OutfitMembershipRequestAction(badCode) /** * The `Codec`s used to transform the input stream into the context of a specific action @@ -106,6 +107,7 @@ object OutfitAction { /** * 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 */ @@ -119,11 +121,12 @@ object OutfitAction { /** * The action code was completely unanticipated! + * * @param action the action behavior code * @return nothing; always fail */ - def failureCodec(action: Int): Codec[OutfitAction] = - everFailCondition.exmap[OutfitAction]( + def failureCodec(action: Int): Codec[OutfitMembershipRequestAction] = + everFailCondition.exmap[OutfitMembershipRequestAction]( _ => 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")) ) @@ -147,8 +150,8 @@ object OutfitMembershipRequest extends Marshallable[OutfitMembershipRequest] { implicit val codec: Codec[Type] = PacketHelpers.createEnumerationCodec(this, uintL(3)) } - private def selectFromType(code: Int): Codec[OutfitAction] = { - import OutfitAction.Codecs._ + private def selectFromType(code: Int): Codec[OutfitMembershipRequestAction] = { + import OutfitMembershipRequestAction.Codecs._ import scala.annotation.switch ((code: @switch) match { @@ -162,7 +165,7 @@ object OutfitMembershipRequest extends Marshallable[OutfitMembershipRequest] { case 7 => unknownCodec(action = code) // 3 bit limit case _ => failureCodec(code) - }).asInstanceOf[Codec[OutfitAction]] + }).asInstanceOf[Codec[OutfitMembershipRequestAction]] } implicit val codec: Codec[OutfitMembershipRequest] = ( diff --git a/src/main/scala/net/psforever/packet/game/OutfitMembershipResponse.scala b/src/main/scala/net/psforever/packet/game/OutfitMembershipResponse.scala new file mode 100644 index 000000000..0e9430e65 --- /dev/null +++ b/src/main/scala/net/psforever/packet/game/OutfitMembershipResponse.scala @@ -0,0 +1,243 @@ +// Copyright (c) 2025 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} +import scodec.bits.BitVector +import scodec.codecs._ +import shapeless.{::, HNil} + +final case class OutfitMembershipResponse( + response_type: OutfitMembershipResponse.ResponseType.Type, + unk0: Int, + avatar_guid: PlanetSideGUID, // avatar_guid and unk1 are related, might be Long instead + unk1: PlanetSideGUID, // + unk2: PlanetSideGUID, + unk3: Int, + //unk4: Boolean, + action: OutfitMembershipResponseAction + ) extends PlanetSideGamePacket { + type Packet = OutfitMembershipResponse + + def opcode: Type = GamePacketOpcode.OutfitMembershipResponse + + def encode: Attempt[BitVector] = OutfitMembershipResponse.encode(this) +} + +abstract class OutfitMembershipResponseAction(val code: Int) +object OutfitMembershipResponseAction { + + final case class CreateOutfitResponse(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 Unk5OutfitResponse() extends OutfitMembershipResponseAction(code = 5) + + final case class Unk6OutfitResponse() extends OutfitMembershipResponseAction(code = 6) + + final case class Unk7OutfitResponse() extends OutfitMembershipResponseAction(code = 7) + + 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 Unk0OutfitCodec: Codec[CreateOutfitResponse] = ( + PacketHelpers.encodedWideStringAligned(5) :: + PacketHelpers.encodedWideString :: + PacketHelpers.encodedWideString + ).xmap[CreateOutfitResponse]( + { + case str1 :: str2 :: str3 :: HNil => + CreateOutfitResponse(str1, str2, str3) + }, + { + case CreateOutfitResponse(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.encodedWideString.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 + } + ) + +// val Unk5OutfitCodec: Codec[Unk5OutfitResponse] = +// (uint16L :: uint16L :: PacketHelpers.encodedWideStringAligned(5)).xmap[Unk5OutfitResponse]( +// { +// case unk5 :: unk6 :: outfit_name :: HNil => +// Unk5OutfitResponse(unk5, unk6, outfit_name) +// }, +// { +// case Unk5OutfitResponse(unk5, unk6, outfit_name) => +// unk5 :: unk6 :: outfit_name :: HNil +// } +// ) +// +// val Unk6OutfitCodec: Codec[Unk6OutfitResponse] = +// (uint16L :: uint16L :: PacketHelpers.encodedWideStringAligned(5)).xmap[Unk6OutfitResponse]( +// { +// case _ => +// Unk6OutfitResponse() +// }, +// { +// case Unk6OutfitResponse() => +// _ +// } +// ) +// +// val Unk7OutfitCodec: Codec[Unk7OutfitResponse] = +// (uint16L :: uint16L :: PacketHelpers.encodedWideStringAligned(5)).xmap[Unk7OutfitResponse]( +// { +// case _ => +// Unk7OutfitResponse() +// }, +// { +// case Unk7OutfitResponse() => +// _ +// } +// ) + + /** + * 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 { + type Type = Value + + val CreateResponse: ResponseType.Value = Value(0) + val Unk1: ResponseType.Value = Value(1) + 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) + + 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 => Unk0OutfitCodec // 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 => + ("unk0" | uint8L) :: + ("avatar_guid" | PlanetSideGUID.codec) :: + ("outfit_guid-1" | PlanetSideGUID.codec) :: + ("target_guid" | PlanetSideGUID.codec) :: + ("unk3" | uint16L) :: + //("unk4" | bool) :: + ("action" | selectFromType(response_type.id)) + } + ).xmap[OutfitMembershipResponse]( + { + case response_type :: u0 :: avatar_guid :: outfit_guid_1 :: target_guid :: u3 :: action :: HNil => + OutfitMembershipResponse(response_type, u0, avatar_guid, outfit_guid_1, target_guid, u3, action) + }, + { + case OutfitMembershipResponse(response_type, u0, avatar_guid, u1, u2, u3, action) => + response_type :: u0 :: avatar_guid :: u1 :: u2 :: u3 :: action :: HNil + } + ) +} diff --git a/src/main/scala/net/psforever/packet/game/OutfitRequest.scala b/src/main/scala/net/psforever/packet/game/OutfitRequest.scala index 05ebf66aa..585ed5ee5 100644 --- a/src/main/scala/net/psforever/packet/game/OutfitRequest.scala +++ b/src/main/scala/net/psforever/packet/game/OutfitRequest.scala @@ -1,4 +1,4 @@ -// Copyright (c) 2023 PSForever +// Copyright (c) 2023-2025 PSForever package net.psforever.packet.game import net.psforever.packet.{GamePacketOpcode, Marshallable, PacketHelpers, PlanetSideGamePacket} @@ -7,77 +7,74 @@ import scodec.bits.ByteVector import scodec.codecs._ import shapeless.{::, HNil} -/** - * na - */ -abstract class OutfitRequestForm(val code: Int) - -object OutfitRequestForm { - /** - * na - * @param str na - */ - final case class Unk0(str: String) extends OutfitRequestForm(code = 0) - /** - * na - * @param list na - */ - final case class Unk1(list: List[Option[String]]) extends OutfitRequestForm(code = 1) - /** - * na - * @param unk na - */ - final case class Unk2(unk: Int) extends OutfitRequestForm(code = 2) - /** - * na - * @param unk na - */ - final case class Unk3(unk: Boolean) extends OutfitRequestForm(code = 3) - /** - * na - * @param unk na - */ - final case class Unk4(unk: Boolean) extends OutfitRequestForm(code = 4) - /** - * na - * @param unk na - */ - final case class Fail(unk: ByteVector) extends OutfitRequestForm(code = -1) +final case class OutfitRequest( + id: Long, + action: OutfitRequestAction + ) extends PlanetSideGamePacket { + type Packet = OutfitRequest + def opcode = GamePacketOpcode.OutfitRequest + def encode = OutfitRequest.encode(this) } /** * na - * @param id na - * @param info na */ -final case class OutfitRequest(id: Long, info: OutfitRequestForm) - extends PlanetSideGamePacket { - type Packet = OrbitalStrikeWaypointMessage - def opcode = GamePacketOpcode.OutfitRequest - def encode = OutfitRequest.encode(this) +abstract class OutfitRequestAction(val code: Int) + +object OutfitRequestAction { + /** + * na + * @param str na + */ + final case class Motd(str: String) extends OutfitRequestAction(code = 0) + /** + * na + * @param list na + */ + final case class Ranks(list: List[Option[String]]) extends OutfitRequestAction(code = 1) + /** + * na + * @param unk na + */ + final case class Unk2(unk: Int) extends OutfitRequestAction(code = 2) + /** + * na + * @param unk na + */ + final case class Unk3(menuOpen: Boolean) extends OutfitRequestAction(code = 3) + /** + * na + * @param unk na + */ + final case class Unk4(menuOpen: Boolean) extends OutfitRequestAction(code = 4) + /** + * na + * @param unk na + */ + final case class Fail(unk: ByteVector) extends OutfitRequestAction(code = -1) } object OutfitRequest extends Marshallable[OutfitRequest] { /** * na */ - private val unk0Codec: Codec[OutfitRequestForm] = PacketHelpers.encodedWideStringAligned(adjustment = 5).hlist - .xmap[OutfitRequestForm] ( + private val MotdCodec: Codec[OutfitRequestAction] = PacketHelpers.encodedWideStringAligned(adjustment = 5).hlist + .xmap[OutfitRequestAction] ( { - case value :: HNil => OutfitRequestForm.Unk0(value) + case value :: HNil => OutfitRequestAction.Motd(value) }, { - case OutfitRequestForm.Unk0(value) => value :: HNil + case OutfitRequestAction.Motd(value) => value :: HNil } ) /** * na */ - private val unk1Codec: Codec[OutfitRequestForm] = unk1PaddedEntryCodec(len = 8, pad = 5).xmap[OutfitRequestForm] ( - list => OutfitRequestForm.Unk1(list), + private val RankCodec: Codec[OutfitRequestAction] = unk1PaddedEntryCodec(len = 8, pad = 5).xmap[OutfitRequestAction] ( + list => OutfitRequestAction.Ranks(list), { - case OutfitRequestForm.Unk1(list) => list + case OutfitRequestAction.Ranks(list) => list } ) @@ -104,54 +101,66 @@ object OutfitRequest extends Marshallable[OutfitRequest] { /** * na */ - private val unk2Codec: Codec[OutfitRequestForm] = uint8.hlist.xmap[OutfitRequestForm] ( + private val unk2Codec: Codec[OutfitRequestAction] = uint8.hlist.xmap[OutfitRequestAction] ( { - case value :: HNil => OutfitRequestForm.Unk2(value) + case value :: HNil => OutfitRequestAction.Unk2(value) }, { - case OutfitRequestForm.Unk2(value) => value :: HNil + case OutfitRequestAction.Unk2(value) => value :: HNil } ) /** * na */ - private val unk3Codec: Codec[OutfitRequestForm] = bool.hlist.xmap[OutfitRequestForm] ( + private val unk3Codec: Codec[OutfitRequestAction] = bool.hlist.xmap[OutfitRequestAction] ( { - case value :: HNil => OutfitRequestForm.Unk3(value) + case value :: HNil => OutfitRequestAction.Unk3(value) }, { - case OutfitRequestForm.Unk3(value) => value :: HNil + case OutfitRequestAction.Unk3(value) => value :: HNil } ) /** * na */ - private val unk4Codec: Codec[OutfitRequestForm] = bool.hlist.xmap[OutfitRequestForm] ( + private val unk4Codec: Codec[OutfitRequestAction] = bool.hlist.xmap[OutfitRequestAction] ( { - case value :: HNil => OutfitRequestForm.Unk4(value) + case value :: HNil => OutfitRequestAction.Unk4(value) }, { - case OutfitRequestForm.Unk4(value) => value :: HNil + case OutfitRequestAction.Unk4(value) => value :: HNil } ) /** * na */ - private def failCodec(code: Int): Codec[OutfitRequestForm] = conditional(included = false, bool).exmap[OutfitRequestForm]( - _ => Attempt.Failure(Err(s"can not decode $code-type info - what is this thing?")), - _ => Attempt.Failure(Err(s"can not encode $code-type info - no such thing")) + private def failCodec(action: Int): Codec[OutfitRequestAction] = conditional(included = false, bool).exmap[OutfitRequestAction]( + _ => Attempt.Failure(Err(s"can not decode $action-type info - what is this thing?")), + _ => Attempt.Failure(Err(s"can not encode $action-type info - no such thing")) ) + object RequestType extends Enumeration { + type Type = Value + + val Motd: RequestType.Value = Value(0) + val Rank: RequestType.Value = Value(1) + val Unk2: RequestType.Value = Value(2) + val Detail: RequestType.Value = Value(3) + val List: RequestType.Value = Value(4) + + implicit val codec: Codec[Type] = PacketHelpers.createEnumerationCodec(this, uintL(3)) + } + /** * na */ - private def infoCodec(code: Int): Codec[OutfitRequestForm] = { + private def selectFromType(code: Int): Codec[OutfitRequestAction] = { code match { - case 0 => unk0Codec - case 1 => unk1Codec + case 0 => MotdCodec + case 1 => RankCodec case 2 => unk2Codec case 3 => unk3Codec case 4 => unk4Codec @@ -162,16 +171,16 @@ object OutfitRequest extends Marshallable[OutfitRequest] { implicit val codec: Codec[OutfitRequest] = ( uint(bits = 3) >>:~ { code => ("id" | uint32L) :: - ("info" | infoCodec(code)) + ("action" | selectFromType(code)) } ).xmap[OutfitRequest]( { - case _:: id:: info :: HNil => - OutfitRequest(id, info) + case _:: id:: action :: HNil => + OutfitRequest(id, action) }, { - case OutfitRequest(id, info) => - info.code :: id :: info :: HNil + case OutfitRequest(id, action) => + action.code :: id :: action :: HNil } ) } diff --git a/src/main/scala/net/psforever/packet/game/SquadMemberEvent.scala b/src/main/scala/net/psforever/packet/game/SquadMemberEvent.scala index 69117ced1..d3fb49a4c 100644 --- a/src/main/scala/net/psforever/packet/game/SquadMemberEvent.scala +++ b/src/main/scala/net/psforever/packet/game/SquadMemberEvent.scala @@ -82,9 +82,7 @@ object SquadMemberEvent extends Marshallable[SquadMemberEvent] { Some(outfit_id) ) => Attempt.Successful( - MemberEvent.Add :: unk2 :: char_id :: member_position :: Some(player_name) :: Some(zone_number) :: Some( - outfit_id - ) :: HNil + MemberEvent.Add :: unk2 :: char_id :: member_position :: Some(player_name) :: Some(zone_number) :: Some(outfit_id) :: HNil ) case SquadMemberEvent(MemberEvent.UpdateZone, unk2, char_id, member_position, None, Some(zone_number), None) => Attempt.Successful( diff --git a/src/test/scala/game/OutfitEventTest.scala b/src/test/scala/game/OutfitEventTest.scala new file mode 100644 index 000000000..e493c2d77 --- /dev/null +++ b/src/test/scala/game/OutfitEventTest.scala @@ -0,0 +1,294 @@ +// Copyright (c) 2025 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 net.psforever.types.PlanetSideGUID +import org.specs2.mutable._ +import scodec.bits._ + +class OutfitEventTest extends Specification { + val unk0_ABC: ByteVector = ByteVector.fromValidHex( + "8f 1 a8c2 0001" + // packet head + "2a 0 42006c00610063006b002000410072006d006f0072006500640020005200650061007000650072007300" + // Black Armored Reapers + "1d9c4d0d" + + "1d9c4d0d" + + "ab00 0000" + + "88 44006f00670020004d00650061007400" + // Dog Meat + "87 5200750073007300690061006e00" + // Russian + "80" + // + "80" + // + "8d 5300710075006100640020004c00650061006400650072007300" + // Squad Leaders + "91 41006300740069006e006700200043006f006d006d0061006e006400650072007300" + // Acting Commanders + "87 5200650061007000650072007300" + // Reapers + "80" + // + "00" + + "9c 5c0023003000300030003000660066004d0075006d0062006c00650020005c00230030003000330033006600660049006e0066006f0020005c0023003000300036003600660066006900730020005c0023003000300039003900660066007400680065006d006f006f00730065002e00740079007000650066007200610067002e0063006f006d0020005c00230030003000630063006600660070006f007200740020005c002300300030006600660066006600390033003500300020005c0023003000300063006300660066006a006f0069006e0020005c0023003000300039003900660066006900740020005c0023003000300036003600660066006f00720020005c0023003000300033003300660066006200650020005c0023003000300030003000660066006b00690063006b00650064002e00" + + "0f80" + + "0000 00737296 24000000 00000000 00000000 0000") + val unk1_ABC: ByteVector = hex"8f 2 302a 10 00 0" + val unk2_ABC: ByteVector = ByteVector.fromValidHex( + "8f 4 0201 feff" + + "2e 0 50006c0061006e006500740053006900640065005f0046006f00720065007600650072005f00560061006e007500" + // PlanetSide_Forever_Vanu + "00000000" + + "00000000" + + "0100 0000" + + "80" + + "80" + + "80" + + "80" + + "80" + + "80" + + "80" + + "80" + + "80" + + "0070" + + "4982 00000000 00000000 00000000 00000000 0000") + val unk3_ABC: ByteVector = hex"8f 6 0201 fe fe 0" + val unk4_ABC: ByteVector = hex"8f 8 0201 fefe a02a 1000 0" + val unk5_ABC: ByteVector = hex"8f a 0201 fefe 0400 0000 0" + + "decode Unk0 ABC" in { + PacketCoding.decodePacket(unk0_ABC).require match { + case OutfitEvent(request_type, outfit_guid, action) => + request_type mustEqual RequestType.Unk0 + outfit_guid mustEqual PlanetSideGUID(25044) + action mustEqual Unk0( + OutfitInfo( + unk1 = 0, + unk2 = 0, + outfit_name = "Black Armored Reapers", + unk6 = 223190045, + unk7 = 223190045, + member_count = 171, + unk9 = 0, + OutfitRankNames("Dog Meat","Russian","","","Squad Leaders","Acting Commanders","Reapers",""), + "\\#0000ffMumble \\#0033ffInfo \\#0066ffis \\#0099ffthemoose.typefrag.com \\#00ccffport \\#00ffff9350 \\#00ccffjoin \\#0099ffit \\#0066ffor \\#0033ffbe \\#0000ffkicked.", + PlanetSideGUID(32783), + 0, + 0, + 0, + 1210901990, + 0, + 0, + 0, + 0, + ) + ) + case _ => + ko + } + } + + "encode Unk0 ABC" in { + val msg = OutfitEvent( + RequestType.Unk0, + PlanetSideGUID(25044), + Unk0( + OutfitInfo( + unk1 = 0, + unk2 = 0, + outfit_name = "Black Armored Reapers", + unk6 = 223190045, + unk7 = 223190045, + member_count = 171, + unk9 = 0, + OutfitRankNames("Dog Meat","Russian","","","Squad Leaders","Acting Commanders","Reapers",""), + "\\#0000ffMumble \\#0033ffInfo \\#0066ffis \\#0099ffthemoose.typefrag.com \\#00ccffport \\#00ffff9350 \\#00ccffjoin \\#0099ffit \\#0066ffor \\#0033ffbe \\#0000ffkicked.", + PlanetSideGUID(32783), + 0, + 0, + 0, + 1210901990, + 0, + 0, + 0, + 0, + ) + ) + ) + val pkt = PacketCoding.encodePacket(msg).require.toByteVector + + pkt mustEqual unk0_ABC + } + + "decode Unk1 ABC" in { + PacketCoding.decodePacket(unk1_ABC).require match { + case OutfitEvent(request_type, outfit_guid, action) => + request_type mustEqual RequestType.Unk1 + outfit_guid mustEqual PlanetSideGUID(5400) + action mustEqual Unk1(unk0 = 8, unk1 = 0, unk2 = 0, unk3 = false) + case _ => + ko + } + } + + "encode Unk1 ABC" in { + val msg = OutfitEvent( + RequestType.Unk1, + PlanetSideGUID(5400), + Unk1( + unk0 = 8, + unk1 = 0, + unk2 = 0, + unk3 = false, + ) + ) + val pkt = PacketCoding.encodePacket(msg).require.toByteVector + + pkt mustEqual unk1_ABC + } + + "decode Unk2 ABC" in { + PacketCoding.decodePacket(unk2_ABC).require match { + case OutfitEvent(request_type, outfit_guid, action) => + request_type mustEqual RequestType.Unk2 + outfit_guid mustEqual PlanetSideGUID(1) + action mustEqual Unk2(OutfitInfo(unk1 = 255, unk2 = 127, outfit_name = "PlanetSide_Forever_Vanu", + unk6 = 0, unk7 = 0, member_count = 1, unk9 = 0, OutfitRankNames("","","","","","","",""), + "", PlanetSideGUID(28672), 33353, 0, 0, 0, 0, 0, 0, 0)) + case _ => + ko + } + } + + "encode Unk2 ABC" in { + val msg = OutfitEvent( + RequestType.Unk2, + PlanetSideGUID(1), + Unk2( + OutfitInfo( + unk1 = 255, + unk2 = 127, + outfit_name = "PlanetSide_Forever_Vanu", + unk6 = 0, + unk7 = 0, + member_count = 1, + unk9 = 0, + OutfitRankNames("","","","","","","",""), + "", + PlanetSideGUID(28672), + 33353, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + ) + ) + ) + val pkt = PacketCoding.encodePacket(msg).require.toByteVector + + pkt mustEqual unk2_ABC + } + + "decode Unk3 ABC" in { + PacketCoding.decodePacket(unk3_ABC).require match { + case OutfitEvent(request_type, outfit_guid, action) => + request_type mustEqual RequestType.Unk3 + outfit_guid mustEqual PlanetSideGUID(1) + action mustEqual Unk3( + unk0 = 255, + unk1 = 127, + unk2 = 0, + unk3 = false, + BitVector.fromValidHex("") + ) + case _ => + ko + } + } + + "encode Unk3 ABC" in { + val msg = OutfitEvent( + RequestType.Unk3, + PlanetSideGUID(1), + Unk3( + unk0 = 255, + unk1 = 127, + unk2 = 0, + unk3 = false, + BitVector.fromValidHex("") + ) + ) + val pkt = PacketCoding.encodePacket(msg).require.toByteVector + + pkt mustEqual unk3_ABC + } + + "decode Unk4 ABC" in { + PacketCoding.decodePacket(unk4_ABC).require match { + case OutfitEvent(request_type, outfit_guid, action) => + request_type mustEqual RequestType.Unk4 + outfit_guid mustEqual PlanetSideGUID(1) + action mustEqual Unk4( + unk0 = 32767, + unk1 = 5456, + unk2 = 8, + 0, + unk4 = false, + BitVector.fromValidHex("") + ) + case _ => + ko + } + } + + "encode Unk4 ABC" in { + val msg = OutfitEvent( + RequestType.Unk4, + PlanetSideGUID(1), + Unk4( + unk0 = 32767, + unk1 = 5456, + unk2 = 8, + unk3 = 0, + unk4 = false, + BitVector.fromValidHex("") + ) + ) + val pkt = PacketCoding.encodePacket(msg).require.toByteVector + + pkt mustEqual unk4_ABC + } + + "decode Unk5 ABC" in { + PacketCoding.decodePacket(unk5_ABC).require match { + case OutfitEvent(request_type, outfit_guid, action) => + request_type mustEqual RequestType.Unk5 + outfit_guid mustEqual PlanetSideGUID(1) + action mustEqual Unk5( + unk0 = 32767, + unk1 = 2, + unk2 = 0, + unk3 = 0, + unk4 = false, + BitVector.fromValidHex("") // OR f88c2a0417c1a06101001f20f4b8c00000404090ac9c6745dea88cadf0f810e03e0200f92 with bool at the back + ) + case _ => + ko + } + } + + "encode Unk5 ABC" in { + val msg = OutfitEvent( + RequestType.Unk5, + PlanetSideGUID(1), + Unk5( + unk0 = 32767, + unk1 = 2, + unk2 = 0, + unk3 = 0, + unk4 = false, + BitVector.fromValidHex("") + ) + ) + val pkt = PacketCoding.encodePacket(msg).require.toByteVector + + pkt mustEqual unk5_ABC + } +} diff --git a/src/test/scala/game/OutfitListEventTest.scala b/src/test/scala/game/OutfitListEventTest.scala new file mode 100644 index 000000000..7935df126 --- /dev/null +++ b/src/test/scala/game/OutfitListEventTest.scala @@ -0,0 +1,42 @@ +// Copyright (c) 2025 PSForever +package game + +import net.psforever.packet._ +import net.psforever.packet.game._ +import org.specs2.mutable._ +import scodec.bits.ByteVector + +class OutfitListEventTest extends Specification { + val unk0_ABC: ByteVector = ByteVector.fromValidHex("98 5e83a000 0000 e180 0080 0000 11404e0069006700680074004c006f00720064007300 854e005900430061007400") + val unk0_DEF: ByteVector = ByteVector.fromValidHex("98 4ec28100 151a 6280 0340 0000 11a0490052004f004e004600490053005400200043006c0061006e00 8654006f006c006a00") + val unk1_ABC: ByteVector = ByteVector.fromValidHex("98 4723c000 02aa 81e0 0220 0000 11006900470061006d00650073002d004500 906900670061006d006500730043005400460057006800610063006b002d004500") + val unk2_ABC: ByteVector = ByteVector.fromValidHex("98 49a3c000 116d a4e0 0040 0000 11a042006c006f006f00640020006f0066002000560061006e007500 864b00610072006e002d004500") + val unk3_ABC: ByteVector = ByteVector.fromValidHex("98 49c3c000 0df5 87c0 0140 0000 11a054006800650020004e00650076006500720068006f006f006400 8e6f00460058006f00530074006f006e0065004d0061006e002d004700") + val unk4_ABC: ByteVector = ByteVector.fromValidHex("98 4c03c000 0240 6040 0060 0000 1220540068006500200042006c00610063006b0020004b006e0069006700680074007300 874400720061007a00760065006e00") + val unk5_ABC: ByteVector = ByteVector.fromValidHex("98 5383c000 14b7 09a0 00c0 0000 10a03e005400760053003c00 89430061007000650062006f00610074007300") + val unk6_ABC: ByteVector = ByteVector.fromValidHex("98 5b03c000 035d 6700 0040 0000 11404c006f0073007400200043006100750073006500 895a00650072006f004b00650077006c006c00") + val unk7_ABC: ByteVector = ByteVector.fromValidHex("98 4043e000 19fb 8261 6140 0000 11e0540068006500200042006c00610063006b00200054006f00770065007200 874b00720075007000680065007800") + val unk8_ABC: ByteVector = ByteVector.fromValidHex("98 4a03e000 17e2") // broken, limit of SMP + + "decode unk0_ABC" in { + PacketCoding.decodePacket(unk0_ABC).require match { + case OutfitListEvent(outfit_score, unk1, unk2, unk3, outfit_name, outfit_leader) => + outfit_score mustEqual 1585684480L + unk1 mustEqual 2162229248L + unk2 mustEqual 32768 + unk3 mustEqual 0 + outfit_name mustEqual "NightLords" + outfit_leader mustEqual "NYCat" + case _ => + ko + } + } + + "encode unk0_ABC" in { + val msg = OutfitListEvent(1585684480L, 2162229248L, 32768, 0, "NightLords", "NYCat") + val pkt = PacketCoding.encodePacket(msg).require.toByteVector + + pkt mustEqual unk0_ABC + } + +} diff --git a/src/test/scala/game/OutfitMemberEventTest.scala b/src/test/scala/game/OutfitMemberEventTest.scala new file mode 100644 index 000000000..9b07ec9dc --- /dev/null +++ b/src/test/scala/game/OutfitMemberEventTest.scala @@ -0,0 +1,71 @@ +// Copyright (c) 2025 PSForever +package game + +import net.psforever.packet._ + +import net.psforever.packet.game._ +import org.specs2.mutable._ +import scodec.bits._ + +class OutfitMemberEventTest extends Specification { + + //val unk0_ABC: ByteVector = hex"90 3518 4000 1a4e 4100 2 180 450078007000650072007400 8483 07e0 119d bfe0 70" // 0x90048640001030c28022404c0061007a00650072003100390038003200f43a45e00b4c604010 +val unk0_ABC_Lazer: ByteVector = hex"90 048640001030c28022404c0061007a00650072003100390038003200f43a45e00b4c604010" + + val OpolE = hex"90 0 4864 0003 aad6 280a 14 0 4f0070006f006c004500 c9a1 80e0 0d03 2040 10" + val Billy = hex"90 0 4864 0003 a41a 280a 20 0 620069006c006c007900320035003600 935f 6000 186a b040 50" + val Lazer = hex"90 0 4864 0001 030c 2802 24 0 4c0061007a00650072003100390038003200 e6dc 25a0 153e 6040 10" + val Virus = hex"90 0 4864 0002 1b64 4c02 28 0 5600690072007500730047006900760065007200 2f89 0080 0000 0000 10" + val PvtPa = hex"90 0 4864 0000 1e69 e80a 2c 0 500076007400500061006e00630061006b0065007300 705e a080 0a85 e060 10" + val Night = hex"90 0 4864 0002 4cf0 3802 28 0 4e006900670068007400770069006e0067003100 b8fb 9a40 0da6 ec80 50" + + /* + OutfitMemberEvent(0, ValidPlanetSideGUID(6418), 0, 0, 64, 195, 10, 0, Lazer1982, 230, 220, 37, 160, 21, 62, 96, 64, 16, BitVector(empty)) + OutfitMemberEvent(0, ValidPlanetSideGUID(6418), 0, 0, 7, 154, 122, 2, PvtPancakes, 112, 94, 160, 128, 10, 133, 224, 96, 16, BitVector(empty)) + OutfitMemberEvent(0, ValidPlanetSideGUID(6418), 0, 0, 134, 217, 19, 0, VirusGiver, 47, 137, 0, 128, 0, 0, 0, 0, 16, BitVector(empty)) + OutfitMemberEvent(0, ValidPlanetSideGUID(6418), 0, 0, 234, 181, 138, 2, OpolE, 201, 161, 128, 224, 13, 3, 32, 64, 16, BitVector(empty)) + OutfitMemberEvent(0, ValidPlanetSideGUID(6418), 0, 0, 233, 6, 138, 2, billy256, 147, 95, 96, 0, 24, 106, 176, 64, 80, BitVector(empty)) + + + */ + + "decode Unk0 ABC" in { + PacketCoding.decodePacket(unk0_ABC_Lazer).require match { + case OutfitMemberEvent(unk00, outfit_guid, unk3, unk4, unk5, unk6, member_name, unk7, unk8, unk9, unk10, unk11) => + unk00 mustEqual 0 + outfit_guid mustEqual 6418L + unk3 mustEqual 64 + unk4 mustEqual 195 + unk5 mustEqual 10 + unk6 mustEqual 0 + member_name mustEqual "Lazer1982" + unk7 mustEqual 15092 + unk8 mustEqual 57413 + unk9 mustEqual 19467 + unk10 mustEqual 16480 + unk11 mustEqual 16 + case _ => + ko + } + } + + "encode Unk0 ABC" in { + val msg = OutfitMemberEvent( + unk00 = 0, + outfit_id = 6418L, + unk3 = 64, + unk4 = 195, + unk5 = 10, + unk6 = 0, + member_name = "Lazer1982", + unk7 = 15092, + unk8 = 57413, + unk9 = 19467, + unk10 = 16480, + unk11 = 16, + ) + val pkt = PacketCoding.encodePacket(msg).require.toByteVector + + pkt mustEqual unk0_ABC_Lazer + } +} diff --git a/src/test/scala/game/OutfitMembershipRequestTest.scala b/src/test/scala/game/OutfitMembershipRequestTest.scala index 4b90df4ee..5e8f48ac9 100644 --- a/src/test/scala/game/OutfitMembershipRequestTest.scala +++ b/src/test/scala/game/OutfitMembershipRequestTest.scala @@ -1,10 +1,10 @@ -// Copyright (c) 2023 PSForever +// Copyright (c) 2023-2025 PSForever package game import net.psforever.packet._ 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.packet.game.OutfitMembershipRequestAction._ import net.psforever.types.PlanetSideGUID import org.specs2.mutable._ import scodec.bits._ diff --git a/src/test/scala/game/OutfitRequesTest.scala b/src/test/scala/game/OutfitRequesTest.scala index eea16519e..0928b3f55 100644 --- a/src/test/scala/game/OutfitRequesTest.scala +++ b/src/test/scala/game/OutfitRequesTest.scala @@ -1,22 +1,21 @@ -// Copyright (c) 2023 PSForever +// Copyright (c) 2023-2025 PSForever package game import org.specs2.mutable._ import net.psforever.packet._ import net.psforever.packet.game._ -import net.psforever.types.PlanetSideGUID import scodec.bits._ class OutfitRequestTest extends Specification { - val string0 = hex"8e02b54f40401780560061006e00750020006f0075007400660069007400200066006f0072002000740068006500200070006c0061006e00650074007300690064006500200066006f00720065007600650072002000700072006f006a006500630074002100200020002000200020002000200020002000200020002000200020002000200020002000200020002000200020002000200020002000200020002000200020002000200020002000200020002000200020002000200020002000200020002000200020002000200020002000200020002000200020002000200020002000200020002000200020002000200020002000200020002000200020002000200020002000200020002000200020002d00660069006e00640020006f007500740020006d006f00720065002000610062006f0075007400200074006800650020005000530045004d0055002000700072006f006a0065006300740020006100740020005000530066006f00720065007600650072002e006e0065007400" - val string2 = hex"8e22b54f405800c000c000c000c000c000c000c000" - val string4 = hex"8e42b54f404aa0" //faked by modifying the previous example - val string6 = hex"8e649e822010" - val string8 = hex"8e81b2cf4050" + val setMotd = hex"8e 02b54f40401780560061006e00750020006f0075007400660069007400200066006f0072002000740068006500200070006c0061006e00650074007300690064006500200066006f00720065007600650072002000700072006f006a006500630074002100200020002000200020002000200020002000200020002000200020002000200020002000200020002000200020002000200020002000200020002000200020002000200020002000200020002000200020002000200020002000200020002000200020002000200020002000200020002000200020002000200020002000200020002000200020002000200020002000200020002000200020002000200020002000200020002000200020002d00660069006e00640020006f007500740020006d006f00720065002000610062006f0075007400200074006800650020005000530045004d0055002000700072006f006a0065006300740020006100740020005000530066006f00720065007600650072002e006e0065007400" + val setRanks = hex"8e 22b54f405800c000c000c000c000c000c000c000" + val string4 = hex"8e 42b54f404aa0" //faked by modifying the previous example + val string6 = hex"8e 649e822010" + val string8 = hex"8e 81b2cf4050" "decode 0" in { - PacketCoding.decodePacket(string0).require match { - case OutfitRequest(id, OutfitRequestForm.Unk0(str)) => + PacketCoding.decodePacket(setMotd).require match { + case OutfitRequest(id, OutfitRequestAction.Motd(str)) => id mustEqual 41593365L str mustEqual "Vanu outfit for the planetside forever project! -find out more about the PSEMU project at PSforever.net" case _ => @@ -25,8 +24,8 @@ class OutfitRequestTest extends Specification { } "decode 1" in { - PacketCoding.decodePacket(string2).require match { - case OutfitRequest(id, OutfitRequestForm.Unk1(list)) => + PacketCoding.decodePacket(setRanks).require match { + case OutfitRequest(id, OutfitRequestAction.Ranks(list)) => id mustEqual 41593365L list mustEqual List(Some(""), Some(""), Some(""), Some(""), Some(""), Some(""), Some(""), Some("")) case _ => @@ -36,7 +35,7 @@ class OutfitRequestTest extends Specification { "decode 2 (fake)" in { PacketCoding.decodePacket(string4).require match { - case OutfitRequest(id, OutfitRequestForm.Unk2(value)) => + case OutfitRequest(id, OutfitRequestAction.Unk2(value)) => id mustEqual 41593365L value mustEqual 85 case _ => @@ -46,7 +45,7 @@ class OutfitRequestTest extends Specification { "decode 3" in { PacketCoding.decodePacket(string6).require match { - case OutfitRequest(id, OutfitRequestForm.Unk3(value)) => + case OutfitRequest(id, OutfitRequestAction.Unk3(value)) => id mustEqual 1176612L value mustEqual true case _ => @@ -56,7 +55,7 @@ class OutfitRequestTest extends Specification { "decode 4" in { PacketCoding.decodePacket(string8).require match { - case OutfitRequest(id, OutfitRequestForm.Unk4(value)) => + case OutfitRequest(id, OutfitRequestAction.Unk4(value)) => id mustEqual 41588237L value mustEqual true case _ => @@ -65,37 +64,37 @@ class OutfitRequestTest extends Specification { } "encode 0" in { - val msg = OutfitRequest(41593365L, OutfitRequestForm.Unk0( + val msg = OutfitRequest(41593365L, OutfitRequestAction.Motd( "Vanu outfit for the planetside forever project! -find out more about the PSEMU project at PSforever.net" )) val pkt = PacketCoding.encodePacket(msg).require.toByteVector - pkt mustEqual string0 + pkt mustEqual setMotd } "encode 1" in { - val msg = OutfitRequest(41593365L, OutfitRequestForm.Unk1(List(Some(""), Some(""), Some(""), Some(""), Some(""), Some(""), Some(""), Some("")))) + val msg = OutfitRequest(41593365L, OutfitRequestAction.Ranks(List(Some(""), Some(""), Some(""), Some(""), Some(""), Some(""), Some(""), Some("")))) val pkt = PacketCoding.encodePacket(msg).require.toByteVector - pkt mustEqual string2 + pkt mustEqual setRanks } "encode 2 (fake)" in { - val msg = OutfitRequest(41593365L, OutfitRequestForm.Unk2(85)) + val msg = OutfitRequest(41593365L, OutfitRequestAction.Unk2(85)) val pkt = PacketCoding.encodePacket(msg).require.toByteVector pkt mustEqual string4 } "encode 3" in { - val msg = OutfitRequest(1176612L, OutfitRequestForm.Unk3(true)) + val msg = OutfitRequest(1176612L, OutfitRequestAction.Unk3(true)) val pkt = PacketCoding.encodePacket(msg).require.toByteVector pkt mustEqual string6 } "encode 4" in { - val msg = OutfitRequest(41588237L, OutfitRequestForm.Unk4(true)) + val msg = OutfitRequest(41588237L, OutfitRequestAction.Unk4(true)) val pkt = PacketCoding.encodePacket(msg).require.toByteVector pkt mustEqual string8 diff --git a/src/test/scala/game/SquadMemberEventTest.scala b/src/test/scala/game/SquadMemberEventTest.scala index d0ae6d78c..a8e2cfef9 100644 --- a/src/test/scala/game/SquadMemberEventTest.scala +++ b/src/test/scala/game/SquadMemberEventTest.scala @@ -1,4 +1,4 @@ -// Copyright (c) 2019 PSForever +// Copyright (c) 2019-2025 PSForever package game import net.psforever.packet._ @@ -11,14 +11,14 @@ class SquadMemberEventTest extends Specification { "decode" in { PacketCoding.decodePacket(string).require match { - case SquadMemberEvent(u1, u2, u3, u4, u5, u6, u7) => - u1 mustEqual MemberEvent.Add + case SquadMemberEvent(event, u2, char_id, position, player_name, zone_number, outfit_id) => + event mustEqual MemberEvent.Add u2 mustEqual 7 - u3 mustEqual 42771010L - u4 mustEqual 0 - u5.contains("HofD") mustEqual true - u6.contains(7) mustEqual true - u7.contains(529745L) mustEqual true + char_id mustEqual 42771010L + position mustEqual 0 + player_name.contains("HofD") mustEqual true + zone_number.contains(7) mustEqual true + outfit_id.contains(529745L) mustEqual true case _ => ko }