From f977ea8e6fd59a2dc0a62aef973131bf61b0b0c0 Mon Sep 17 00:00:00 2001 From: Resaec Date: Sun, 10 Aug 2025 22:57:01 +0200 Subject: [PATCH 01/22] clean up, fetch up and even more packets --- .../psforever/packet/GamePacketOpcode.scala | 12 +- .../psforever/packet/game/OutfitEvent.scala | 358 ++++++++++++++++++ .../packet/game/OutfitListEvent.scala | 35 ++ .../packet/game/OutfitMemberEvent.scala | 52 +++ .../packet/game/OutfitMemberUpdate.scala | 37 ++ .../packet/game/OutfitMembershipRequest.scala | 37 +- .../game/OutfitMembershipResponse.scala | 243 ++++++++++++ .../psforever/packet/game/OutfitRequest.scala | 151 ++++---- .../packet/game/SquadMemberEvent.scala | 4 +- src/test/scala/game/OutfitEventTest.scala | 294 ++++++++++++++ src/test/scala/game/OutfitListEventTest.scala | 42 ++ .../scala/game/OutfitMemberEventTest.scala | 71 ++++ .../game/OutfitMembershipRequestTest.scala | 4 +- src/test/scala/game/OutfitRequesTest.scala | 41 +- .../scala/game/SquadMemberEventTest.scala | 16 +- 15 files changed, 1269 insertions(+), 128 deletions(-) create mode 100644 src/main/scala/net/psforever/packet/game/OutfitEvent.scala create mode 100644 src/main/scala/net/psforever/packet/game/OutfitListEvent.scala create mode 100644 src/main/scala/net/psforever/packet/game/OutfitMemberEvent.scala create mode 100644 src/main/scala/net/psforever/packet/game/OutfitMemberUpdate.scala create mode 100644 src/main/scala/net/psforever/packet/game/OutfitMembershipResponse.scala create mode 100644 src/test/scala/game/OutfitEventTest.scala create mode 100644 src/test/scala/game/OutfitListEventTest.scala create mode 100644 src/test/scala/game/OutfitMemberEventTest.scala diff --git a/src/main/scala/net/psforever/packet/GamePacketOpcode.scala b/src/main/scala/net/psforever/packet/GamePacketOpcode.scala index 6bc06e6d..0253d2b8 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 00000000..cfbd647b --- /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 00000000..52ea9929 --- /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 00000000..737a72da --- /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 00000000..9fc1a72e --- /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 55989fc6..b64a68ce 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 00000000..0e9430e6 --- /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 05ebf66a..585ed5ee 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 69117ced..d3fb49a4 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 00000000..e493c2d7 --- /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 00000000..7935df12 --- /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 00000000..9b07ec9d --- /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 4b90df4e..5e8f48ac 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 eea16519..0928b3f5 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 d0ae6d78..a8e2cfef 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 } From 6e23b701a667d8cf7d61dca0544654f5c95ebd35 Mon Sep 17 00:00:00 2001 From: Resaec Date: Sat, 16 Aug 2025 00:19:13 +0200 Subject: [PATCH 02/22] outfit_id is uint32L --- .../psforever/packet/game/OutfitEvent.scala | 68 ++++++---------- .../packet/game/OutfitMembershipRequest.scala | 15 ++-- .../game/OutfitMembershipResponse.scala | 14 ++-- src/test/scala/game/OutfitEventTest.scala | 72 ++++++++--------- .../game/OutfitMembershipRequestTest.scala | 78 ++++++++----------- 5 files changed, 106 insertions(+), 141 deletions(-) diff --git a/src/main/scala/net/psforever/packet/game/OutfitEvent.scala b/src/main/scala/net/psforever/packet/game/OutfitEvent.scala index cfbd647b..90cffd4b 100644 --- a/src/main/scala/net/psforever/packet/game/OutfitEvent.scala +++ b/src/main/scala/net/psforever/packet/game/OutfitEvent.scala @@ -11,7 +11,7 @@ import shapeless.{::, HNil} final case class OutfitEvent( request_type: OutfitEvent.RequestType.Type, - outfit_guid: PlanetSideGUID, + outfit_guid: Long, action: OutfitEventAction ) extends PlanetSideGamePacket { type Packet = OutfitEvent @@ -37,8 +37,6 @@ object OutfitEventAction { ) final case class OutfitInfo( - unk1: Int, - unk2: Int, outfit_name: String, unk6: Long, unk7: Long, @@ -62,8 +60,6 @@ object OutfitEventAction { ) extends OutfitEventAction(code = 0) final case class Unk1( - unk0: Int, - unk1: Int, unk2: Int, unk3: Boolean, ) extends OutfitEventAction(code = 1) @@ -73,24 +69,19 @@ object OutfitEventAction { ) 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, + new_outfit_id: Long, unk3: Int, unk4: Boolean, data: BitVector, ) extends OutfitEventAction(code = 4) final case class Unk5( - unk0: Int, unk1: Int, unk2: Int, unk3: Int, @@ -128,8 +119,6 @@ object OutfitEventAction { ) private val InfoCodec: Codec[OutfitInfo] = ( - uint8L :: - uint8L :: PacketHelpers.encodedWideStringAligned(5) :: uint32L :: uint32L :: @@ -148,12 +137,12 @@ object OutfitEventAction { 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 outfit_name :: u6 :: u7 :: member_count :: u9 :: outfit_rank_names :: motd :: u19 :: u20 :: u21 :: u21_2 :: created_timestamp :: u23 :: u24 :: u25 :: u123 :: HNil => + OutfitInfo(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 + case OutfitInfo(outfit_name, u6, u7, member_count, u9, outfit_rank_names, motd, u19, u20, u21, u21_2, created_timestamp, u23, u24, u25, u123) => + outfit_name :: u6 :: u7 :: member_count :: u9 :: outfit_rank_names :: motd :: u19 :: u20 :: u21 :: u21_2 :: created_timestamp :: u23 :: u24 :: u25 :: u123 :: HNil } ) @@ -171,18 +160,16 @@ object OutfitEventAction { ) val Unk1Codec: Codec[Unk1] = ( - uint8L :: - uint8L :: uint4L :: bool ).xmap[Unk1]( { - case u0 :: u1 :: u2 :: u3 :: HNil => - Unk1(u0, u1, u2, u3) + case u2 :: u3 :: HNil => + Unk1(u2, u3) }, { - case Unk1(u0, u1, u2, u3) => - u0 :: u1 :: u2 :: u3 :: HNil + case Unk1(u2, u3) => + u2 :: u3 :: HNil } ) @@ -200,42 +187,37 @@ object OutfitEventAction { ) 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 u2 :: u3 :: data :: HNil => + Unk3(u2, u3, data) }, { - case Unk3(u0, u1, u2, u3, data) => - u0 :: u1 :: u2 :: u3 :: data :: HNil + case Unk3(u2, u3, data) => + u2 :: u3 :: data :: HNil } ) - val Unk4Codec: Codec[Unk4] = ( - uint16L :: - uint16L :: - uint16L :: + val Unk4Codec: Codec[Unk4] = ( // update outfit_id? // 2016.03.18 #10640 // after this packet the referenced id changes to the new one, old is not used again + uint32L :: // real / other outfit_id uint4L :: bool :: bits ).xmap[Unk4]( { - case u0 :: u1 :: u2 :: u3 :: u4 :: data :: HNil => - Unk4(u0, u1, u2, u3, u4, data) + case new_outfit_id :: u3 :: u4 :: data :: HNil => + Unk4(new_outfit_id, u3, u4, data) }, { - case Unk4(u0, u1, u2, u3, u4, data) => - u0 :: u1 :: u2 ::u3 :: u4 :: data :: HNil + case Unk4(new_outfit_id, u3, u4, data) => + new_outfit_id ::u3 :: u4 :: data :: HNil } ) val Unk5Codec: Codec[Unk5] = ( - uint16L :: uint16L :: uint16L :: uint4L :: @@ -243,12 +225,12 @@ object OutfitEventAction { bits ).xmap[Unk5]( { - case u0 :: u1 :: u2 :: u3 :: u4 :: data :: HNil => - Unk5(u0, u1, u2, u3, u4, data) + case u1 :: u2 :: u3 :: u4 :: data :: HNil => + Unk5(u1, u2, u3, u4, data) }, { - case Unk5(u0, u1, u2, u3, u4, data) => - u0 :: u1 :: u2 :: u3 :: u4 :: data :: HNil + case Unk5(u1, u2, u3, u4, data) => + u1 :: u2 :: u3 :: u4 :: data :: HNil } ) @@ -342,7 +324,7 @@ object OutfitEvent extends Marshallable[OutfitEvent] { implicit val codec: Codec[OutfitEvent] = ( ("request_type" | RequestType.codec) >>:~ { request_type => - ("outfit_guid" | PlanetSideGUID.codec) :: + ("outfit_guid" | uint32L) :: ("action" | selectFromType(request_type.id)) } ).xmap[OutfitEvent]( diff --git a/src/main/scala/net/psforever/packet/game/OutfitMembershipRequest.scala b/src/main/scala/net/psforever/packet/game/OutfitMembershipRequest.scala index b64a68ce..adeaca48 100644 --- a/src/main/scala/net/psforever/packet/game/OutfitMembershipRequest.scala +++ b/src/main/scala/net/psforever/packet/game/OutfitMembershipRequest.scala @@ -3,7 +3,6 @@ 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._ @@ -11,8 +10,7 @@ import shapeless.{::, HNil} final case class OutfitMembershipRequest( request_type: OutfitMembershipRequest.RequestType.Type, - avatar_guid: PlanetSideGUID, // avatar_guid and unk1 are related, might be Long instead - unk1: Int, // + outfit_id: Long, action: OutfitMembershipRequestAction ) extends PlanetSideGamePacket { type Packet = OutfitMembershipRequest @@ -170,18 +168,17 @@ object OutfitMembershipRequest extends Marshallable[OutfitMembershipRequest] { implicit val codec: Codec[OutfitMembershipRequest] = ( ("request_type" | RequestType.codec) >>:~ { request_type => - ("avatar_guid" | PlanetSideGUID.codec) :: - ("unk1" | uint16L) :: + ("outfit_id" | uint32L) :: ("action" | selectFromType(request_type.id)) } ).xmap[OutfitMembershipRequest]( { - case request_type :: avatar_guid :: u1 :: action :: HNil => - OutfitMembershipRequest(request_type, avatar_guid, u1, action) + case request_type :: outfit_id :: action :: HNil => + OutfitMembershipRequest(request_type, outfit_id, action) }, { - case OutfitMembershipRequest(request_type, avatar_guid, u1, action) => - request_type :: avatar_guid :: u1 :: action :: HNil + case OutfitMembershipRequest(request_type, outfit_id, action) => + request_type :: outfit_id :: action :: HNil } ) } diff --git a/src/main/scala/net/psforever/packet/game/OutfitMembershipResponse.scala b/src/main/scala/net/psforever/packet/game/OutfitMembershipResponse.scala index 0e9430e6..1a75371a 100644 --- a/src/main/scala/net/psforever/packet/game/OutfitMembershipResponse.scala +++ b/src/main/scala/net/psforever/packet/game/OutfitMembershipResponse.scala @@ -12,8 +12,7 @@ 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, // + outfit_id: Long, unk2: PlanetSideGUID, unk3: Int, //unk4: Boolean, @@ -223,8 +222,7 @@ object OutfitMembershipResponse extends Marshallable[OutfitMembershipResponse] { implicit val codec: Codec[OutfitMembershipResponse] = ( ("response_type" | ResponseType.codec) >>:~ { response_type => ("unk0" | uint8L) :: - ("avatar_guid" | PlanetSideGUID.codec) :: - ("outfit_guid-1" | PlanetSideGUID.codec) :: + ("outfit_id" | uint32L) :: ("target_guid" | PlanetSideGUID.codec) :: ("unk3" | uint16L) :: //("unk4" | bool) :: @@ -232,12 +230,12 @@ object OutfitMembershipResponse extends Marshallable[OutfitMembershipResponse] { } ).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 response_type :: u0 :: outfit_id :: target_guid :: u3 :: action :: HNil => + OutfitMembershipResponse(response_type, u0, outfit_id, 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 + case OutfitMembershipResponse(response_type, u0, outfit_id, u2, u3, action) => + response_type :: u0 :: outfit_id :: u2 :: u3 :: action :: HNil } ) } diff --git a/src/test/scala/game/OutfitEventTest.scala b/src/test/scala/game/OutfitEventTest.scala index e493c2d7..eab4f75a 100644 --- a/src/test/scala/game/OutfitEventTest.scala +++ b/src/test/scala/game/OutfitEventTest.scala @@ -54,11 +54,9 @@ class OutfitEventTest extends Specification { PacketCoding.decodePacket(unk0_ABC).require match { case OutfitEvent(request_type, outfit_guid, action) => request_type mustEqual RequestType.Unk0 - outfit_guid mustEqual PlanetSideGUID(25044) + outfit_guid mustEqual 25044 action mustEqual Unk0( OutfitInfo( - unk1 = 0, - unk2 = 0, outfit_name = "Black Armored Reapers", unk6 = 223190045, unk7 = 223190045, @@ -85,11 +83,9 @@ class OutfitEventTest extends Specification { "encode Unk0 ABC" in { val msg = OutfitEvent( RequestType.Unk0, - PlanetSideGUID(25044), + 25044, Unk0( OutfitInfo( - unk1 = 0, - unk2 = 0, outfit_name = "Black Armored Reapers", unk6 = 223190045, unk7 = 223190045, @@ -118,8 +114,11 @@ class OutfitEventTest extends Specification { 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) + outfit_guid mustEqual 529688L + action mustEqual Unk1( + unk2 = 0, + unk3 = false + ) case _ => ko } @@ -128,10 +127,8 @@ class OutfitEventTest extends Specification { "encode Unk1 ABC" in { val msg = OutfitEvent( RequestType.Unk1, - PlanetSideGUID(5400), + 529688L, Unk1( - unk0 = 8, - unk1 = 0, unk2 = 0, unk3 = false, ) @@ -145,10 +142,25 @@ class OutfitEventTest extends Specification { 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)) + outfit_guid mustEqual 2147418113L + action mustEqual Unk2(OutfitInfo( + 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 } @@ -157,11 +169,9 @@ class OutfitEventTest extends Specification { "encode Unk2 ABC" in { val msg = OutfitEvent( RequestType.Unk2, - PlanetSideGUID(1), + 2147418113L, Unk2( OutfitInfo( - unk1 = 255, - unk2 = 127, outfit_name = "PlanetSide_Forever_Vanu", unk6 = 0, unk7 = 0, @@ -190,10 +200,8 @@ class OutfitEventTest extends Specification { PacketCoding.decodePacket(unk3_ABC).require match { case OutfitEvent(request_type, outfit_guid, action) => request_type mustEqual RequestType.Unk3 - outfit_guid mustEqual PlanetSideGUID(1) + outfit_guid mustEqual 2147418113L action mustEqual Unk3( - unk0 = 255, - unk1 = 127, unk2 = 0, unk3 = false, BitVector.fromValidHex("") @@ -206,10 +214,8 @@ class OutfitEventTest extends Specification { "encode Unk3 ABC" in { val msg = OutfitEvent( RequestType.Unk3, - PlanetSideGUID(1), + 2147418113L, Unk3( - unk0 = 255, - unk1 = 127, unk2 = 0, unk3 = false, BitVector.fromValidHex("") @@ -224,11 +230,9 @@ class OutfitEventTest extends Specification { PacketCoding.decodePacket(unk4_ABC).require match { case OutfitEvent(request_type, outfit_guid, action) => request_type mustEqual RequestType.Unk4 - outfit_guid mustEqual PlanetSideGUID(1) + outfit_guid mustEqual 2147418113L action mustEqual Unk4( - unk0 = 32767, - unk1 = 5456, - unk2 = 8, + new_outfit_id = 529744L, 0, unk4 = false, BitVector.fromValidHex("") @@ -241,11 +245,9 @@ class OutfitEventTest extends Specification { "encode Unk4 ABC" in { val msg = OutfitEvent( RequestType.Unk4, - PlanetSideGUID(1), + 2147418113L, Unk4( - unk0 = 32767, - unk1 = 5456, - unk2 = 8, + new_outfit_id = 529744L, unk3 = 0, unk4 = false, BitVector.fromValidHex("") @@ -260,9 +262,8 @@ class OutfitEventTest extends Specification { PacketCoding.decodePacket(unk5_ABC).require match { case OutfitEvent(request_type, outfit_guid, action) => request_type mustEqual RequestType.Unk5 - outfit_guid mustEqual PlanetSideGUID(1) + outfit_guid mustEqual 2147418113L action mustEqual Unk5( - unk0 = 32767, unk1 = 2, unk2 = 0, unk3 = 0, @@ -277,9 +278,8 @@ class OutfitEventTest extends Specification { "encode Unk5 ABC" in { val msg = OutfitEvent( RequestType.Unk5, - PlanetSideGUID(1), + 2147418113L, Unk5( - unk0 = 32767, unk1 = 2, unk2 = 0, unk3 = 0, diff --git a/src/test/scala/game/OutfitMembershipRequestTest.scala b/src/test/scala/game/OutfitMembershipRequestTest.scala index 5e8f48ac..163e4c45 100644 --- a/src/test/scala/game/OutfitMembershipRequestTest.scala +++ b/src/test/scala/game/OutfitMembershipRequestTest.scala @@ -5,7 +5,6 @@ import net.psforever.packet._ import net.psforever.packet.game._ 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._ @@ -25,10 +24,9 @@ class OutfitMembershipRequestTest extends Specification { "decode CreateOutfit ABC" in { PacketCoding.decodePacket(create_ABC).require match { - case OutfitMembershipRequest(request_type, avatar_id, unk1, action) => + case OutfitMembershipRequest(request_type, avatar_id, action) => request_type mustEqual RequestType.Create - avatar_id mustEqual PlanetSideGUID(1) - unk1 mustEqual 0 + avatar_id mustEqual 1 action mustEqual CreateOutfit("", 0, unk4 = false, "ABC") case _ => ko @@ -36,7 +34,7 @@ class OutfitMembershipRequestTest extends Specification { } "encode CreateOutfit ABC" in { - val msg = OutfitMembershipRequest(RequestType.Create, PlanetSideGUID(1), 0, CreateOutfit("", 0, unk4 = false, "ABC")) + val msg = OutfitMembershipRequest(RequestType.Create, 1, CreateOutfit("", 0, unk4 = false, "ABC")) val pkt = PacketCoding.encodePacket(msg).require.toByteVector pkt mustEqual create_ABC @@ -44,10 +42,9 @@ class OutfitMembershipRequestTest extends Specification { "decode CreateOutfit 2222" in { PacketCoding.decodePacket(create_2222).require match { - case OutfitMembershipRequest(request_type, avatar_id, unk1, action) => + case OutfitMembershipRequest(request_type, avatar_id, action) => request_type mustEqual RequestType.Create - avatar_id mustEqual PlanetSideGUID(8) - unk1 mustEqual 0 + avatar_id mustEqual 8 action mustEqual CreateOutfit("", 0, unk4 = false, "2222") case _ => ko @@ -55,7 +52,7 @@ class OutfitMembershipRequestTest extends Specification { } "encode CreateOutfit 2222" in { - val msg = OutfitMembershipRequest(RequestType.Create, PlanetSideGUID(8), 0, CreateOutfit("", 0, unk4 = false, "2222")) + val msg = OutfitMembershipRequest(RequestType.Create, 8, CreateOutfit("", 0, unk4 = false, "2222")) val pkt = PacketCoding.encodePacket(msg).require.toByteVector pkt mustEqual create_2222 @@ -63,10 +60,9 @@ class OutfitMembershipRequestTest extends Specification { "decode FormOutfit abc" in { PacketCoding.decodePacket(form_abc).require match { - case OutfitMembershipRequest(request_type, avatar_id, unk1, action) => + case OutfitMembershipRequest(request_type, avatar_id, action) => request_type mustEqual RequestType.Form - avatar_id mustEqual PlanetSideGUID(1) - unk1 mustEqual 0 + avatar_id mustEqual 1 action mustEqual FormOutfit("", 0, unk4 = false, "abc") case _ => ko @@ -74,7 +70,7 @@ class OutfitMembershipRequestTest extends Specification { } "encode FormOutfit abc" in { - val msg = OutfitMembershipRequest(RequestType.Form, PlanetSideGUID(1), 0, FormOutfit("", 0, unk4 = false, "abc")) + val msg = OutfitMembershipRequest(RequestType.Form, 1, FormOutfit("", 0, unk4 = false, "abc")) val pkt = PacketCoding.encodePacket(msg).require.toByteVector pkt mustEqual form_abc @@ -82,10 +78,9 @@ class OutfitMembershipRequestTest extends Specification { "decode FormOutfit 1" in { PacketCoding.decodePacket(form_1).require match { - case OutfitMembershipRequest(request_type, avatar_id, unk1, action) => + case OutfitMembershipRequest(request_type, avatar_id, action) => request_type mustEqual RequestType.Form - avatar_id mustEqual PlanetSideGUID(8) - unk1 mustEqual 0 + avatar_id mustEqual 8 action mustEqual FormOutfit("", 0, unk4 = false, "1") case _ => ko @@ -93,7 +88,7 @@ class OutfitMembershipRequestTest extends Specification { } "encode FormOutfit 1" in { - val msg = OutfitMembershipRequest(RequestType.Form, PlanetSideGUID(8), 0, FormOutfit("", 0, unk4 = false, "1")) + val msg = OutfitMembershipRequest(RequestType.Form, 8, FormOutfit("", 0, unk4 = false, "1")) val pkt = PacketCoding.encodePacket(msg).require.toByteVector pkt mustEqual form_1 @@ -101,10 +96,9 @@ class OutfitMembershipRequestTest extends Specification { "decode AcceptOutfitInvite 1" in { PacketCoding.decodePacket(accept_1).require match { - case OutfitMembershipRequest(request_type, avatar_id, unk1, action) => + case OutfitMembershipRequest(request_type, avatar_id, action) => request_type mustEqual RequestType.Accept - avatar_id mustEqual PlanetSideGUID(1) - unk1 mustEqual 0 + avatar_id mustEqual 1 action mustEqual AcceptOutfitInvite("") case _ => ko @@ -112,7 +106,7 @@ class OutfitMembershipRequestTest extends Specification { } "encode AcceptOutfitInvite 1" in { - val msg = OutfitMembershipRequest(RequestType.Accept, PlanetSideGUID(1), 0, AcceptOutfitInvite("")) + val msg = OutfitMembershipRequest(RequestType.Accept, 1, AcceptOutfitInvite("")) val pkt = PacketCoding.encodePacket(msg).require.toByteVector pkt mustEqual accept_1 @@ -120,10 +114,9 @@ class OutfitMembershipRequestTest extends Specification { "decode AcceptOutfitInvite 2" in { PacketCoding.decodePacket(accept_2).require match { - case OutfitMembershipRequest(request_type, avatar_id, unk1, action) => + case OutfitMembershipRequest(request_type, avatar_id, action) => request_type mustEqual RequestType.Accept - avatar_id mustEqual PlanetSideGUID(2) - unk1 mustEqual 0 + avatar_id mustEqual 2 action mustEqual AcceptOutfitInvite("") case _ => ko @@ -131,7 +124,7 @@ class OutfitMembershipRequestTest extends Specification { } "encode AcceptOutfitInvite 2" in { - val msg = OutfitMembershipRequest(RequestType.Accept, PlanetSideGUID(2), 0, AcceptOutfitInvite("")) + val msg = OutfitMembershipRequest(RequestType.Accept, 2, AcceptOutfitInvite("")) val pkt = PacketCoding.encodePacket(msg).require.toByteVector pkt mustEqual accept_2 @@ -139,10 +132,9 @@ class OutfitMembershipRequestTest extends Specification { "decode RejectOutfitInvite 1" in { PacketCoding.decodePacket(reject_1).require match { - case OutfitMembershipRequest(request_type, avatar_id, unk1, action) => + case OutfitMembershipRequest(request_type, avatar_id, action) => request_type mustEqual RequestType.Reject - avatar_id mustEqual PlanetSideGUID(1) - unk1 mustEqual 0 + avatar_id mustEqual 1 action mustEqual RejectOutfitInvite("") case _ => ko @@ -150,7 +142,7 @@ class OutfitMembershipRequestTest extends Specification { } "encode RejectOutfitInvite 1" in { - val msg = OutfitMembershipRequest(RequestType.Reject, PlanetSideGUID(1), 0, RejectOutfitInvite("")) + val msg = OutfitMembershipRequest(RequestType.Reject, 1, RejectOutfitInvite("")) val pkt = PacketCoding.encodePacket(msg).require.toByteVector pkt mustEqual reject_1 @@ -158,10 +150,9 @@ class OutfitMembershipRequestTest extends Specification { "decode RejectOutfitInvite 2" in { PacketCoding.decodePacket(reject_2).require match { - case OutfitMembershipRequest(request_type, avatar_id, unk1, action) => + case OutfitMembershipRequest(request_type, avatar_id, action) => request_type mustEqual RequestType.Reject - avatar_id mustEqual PlanetSideGUID(2) - unk1 mustEqual 0 + avatar_id mustEqual 2 action mustEqual RejectOutfitInvite("") case _ => ko @@ -169,7 +160,7 @@ class OutfitMembershipRequestTest extends Specification { } "encode RejectOutfitInvite 2" in { - val msg = OutfitMembershipRequest(RequestType.Reject, PlanetSideGUID(2), 0, RejectOutfitInvite("")) + val msg = OutfitMembershipRequest(RequestType.Reject, 2, RejectOutfitInvite("")) val pkt = PacketCoding.encodePacket(msg).require.toByteVector pkt mustEqual reject_2 @@ -177,10 +168,9 @@ class OutfitMembershipRequestTest extends Specification { "decode CancelOutfitInvite 3" in { PacketCoding.decodePacket(cancel_3).require match { - case OutfitMembershipRequest(request_type, avatar_id, unk1, action) => + case OutfitMembershipRequest(request_type, avatar_id, action) => request_type mustEqual RequestType.Cancel - avatar_id mustEqual PlanetSideGUID(3) - unk1 mustEqual 0 + avatar_id mustEqual 3 action mustEqual CancelOutfitInvite(0, 0, "") case _ => ko @@ -188,7 +178,7 @@ class OutfitMembershipRequestTest extends Specification { } "encode CancelOutfitInvite 3" in { - val msg = OutfitMembershipRequest(RequestType.Cancel, PlanetSideGUID(3), 0, CancelOutfitInvite(0, 0, "")) + val msg = OutfitMembershipRequest(RequestType.Cancel, 3, CancelOutfitInvite(0, 0, "")) val pkt = PacketCoding.encodePacket(msg).require.toByteVector pkt mustEqual cancel_3 @@ -196,10 +186,9 @@ class OutfitMembershipRequestTest extends Specification { "decode CancelOutfitInvite 1 abc" in { PacketCoding.decodePacket(cancel_1_abc).require match { - case OutfitMembershipRequest(request_type, avatar_id, unk1, action) => + case OutfitMembershipRequest(request_type, avatar_id, action) => request_type mustEqual RequestType.Cancel - avatar_id mustEqual PlanetSideGUID(1) - unk1 mustEqual 0 + avatar_id mustEqual 1 action mustEqual CancelOutfitInvite(0, 0, "abc") case _ => ko @@ -207,7 +196,7 @@ class OutfitMembershipRequestTest extends Specification { } "encode CancelOutfitInvite 1 abc" in { - val msg = OutfitMembershipRequest(RequestType.Cancel, PlanetSideGUID(1), 0, CancelOutfitInvite(0, 0, "abc")) + val msg = OutfitMembershipRequest(RequestType.Cancel, 1, CancelOutfitInvite(0, 0, "abc")) val pkt = PacketCoding.encodePacket(msg).require.toByteVector pkt mustEqual cancel_1_abc @@ -215,10 +204,9 @@ class OutfitMembershipRequestTest extends Specification { "decode CancelOutfitInvite 3 def" in { PacketCoding.decodePacket(cancel_3_def).require match { - case OutfitMembershipRequest(request_type, avatar_id, unk1, action) => + case OutfitMembershipRequest(request_type, avatar_id, action) => request_type mustEqual RequestType.Cancel - avatar_id mustEqual PlanetSideGUID(3) - unk1 mustEqual 0 + avatar_id mustEqual 3 action mustEqual CancelOutfitInvite(0, 0, "def") case _ => ko @@ -226,7 +214,7 @@ class OutfitMembershipRequestTest extends Specification { } "encode CancelOutfitInvite 3 def" in { - val msg = OutfitMembershipRequest(RequestType.Cancel, PlanetSideGUID(3), 0, CancelOutfitInvite(0, 0, "def")) + val msg = OutfitMembershipRequest(RequestType.Cancel, 3, CancelOutfitInvite(0, 0, "def")) val pkt = PacketCoding.encodePacket(msg).require.toByteVector pkt mustEqual cancel_3_def From b242c4c4bf241b6dbe47db5664159997772354f0 Mon Sep 17 00:00:00 2001 From: Resaec Date: Sun, 17 Aug 2025 14:07:48 +0200 Subject: [PATCH 03/22] outfit_(gu)id is uint32L (too) --- .../psforever/packet/game/OutfitMemberUpdate.scala | 14 ++++++-------- 1 file changed, 6 insertions(+), 8 deletions(-) diff --git a/src/main/scala/net/psforever/packet/game/OutfitMemberUpdate.scala b/src/main/scala/net/psforever/packet/game/OutfitMemberUpdate.scala index 9fc1a72e..fe8b9d50 100644 --- a/src/main/scala/net/psforever/packet/game/OutfitMemberUpdate.scala +++ b/src/main/scala/net/psforever/packet/game/OutfitMemberUpdate.scala @@ -8,8 +8,7 @@ import scodec.codecs._ import shapeless.{::, HNil} final case class OutfitMemberUpdate( - outfit_guid: PlanetSideGUID, - unk1: Int, + outfit_guid: Long, avatar_guid: PlanetSideGUID, unk3: Int, ) extends PlanetSideGamePacket { @@ -20,18 +19,17 @@ final case class OutfitMemberUpdate( object OutfitMemberUpdate extends Marshallable[OutfitMemberUpdate] { implicit val codec: Codec[OutfitMemberUpdate] = ( - ("outfit_guid" | PlanetSideGUID.codec) :: - ("unk1" | uint16L) :: + ("outfit_guid" | uint32L) :: ("avatar_guid" | PlanetSideGUID.codec) :: ("unk3" | uint8L) ).xmap[OutfitMemberUpdate]( { - case outfit_guid :: u1 :: u2 :: u3 :: HNil => - OutfitMemberUpdate(outfit_guid, u1, u2, u3) + case outfit_guid :: u2 :: u3 :: HNil => + OutfitMemberUpdate(outfit_guid, u2, u3) }, { - case OutfitMemberUpdate(outfit_guid, u1, u2, u3) => - outfit_guid :: u1 :: u2 :: u3 :: HNil + case OutfitMemberUpdate(outfit_guid, u2, u3) => + outfit_guid :: u2 :: u3 :: HNil } ) } From 868439bb215bb079708ee31ff63aee1b8654f66c Mon Sep 17 00:00:00 2001 From: Resaec Date: Sun, 17 Aug 2025 15:07:42 +0200 Subject: [PATCH 04/22] missed some bytes of the packet, avatar_guid is wrong --- .../packet/game/OutfitMemberUpdate.scala | 20 ++++++++++++------- 1 file changed, 13 insertions(+), 7 deletions(-) diff --git a/src/main/scala/net/psforever/packet/game/OutfitMemberUpdate.scala b/src/main/scala/net/psforever/packet/game/OutfitMemberUpdate.scala index fe8b9d50..bf94b3a8 100644 --- a/src/main/scala/net/psforever/packet/game/OutfitMemberUpdate.scala +++ b/src/main/scala/net/psforever/packet/game/OutfitMemberUpdate.scala @@ -9,8 +9,11 @@ import shapeless.{::, HNil} final case class OutfitMemberUpdate( outfit_guid: Long, - avatar_guid: PlanetSideGUID, + unk1: Int, + unk2: Int, unk3: Int, + unk4: Int, + unk5: Int, ) extends PlanetSideGamePacket { type Packet = OutfitMemberUpdate def opcode = GamePacketOpcode.OutfitMemberUpdate @@ -20,16 +23,19 @@ final case class OutfitMemberUpdate( object OutfitMemberUpdate extends Marshallable[OutfitMemberUpdate] { implicit val codec: Codec[OutfitMemberUpdate] = ( ("outfit_guid" | uint32L) :: - ("avatar_guid" | PlanetSideGUID.codec) :: - ("unk3" | uint8L) + ("unk1" | uint8L) :: + ("unk2" | uint8L) :: + ("unk3" | uint8L) :: + ("unk4" | uint8L) :: + ("unk5" | uint8L) ).xmap[OutfitMemberUpdate]( { - case outfit_guid :: u2 :: u3 :: HNil => - OutfitMemberUpdate(outfit_guid, u2, u3) + case outfit_guid :: u1 :: u2 :: u3 :: u4 :: u5 :: HNil => + OutfitMemberUpdate(outfit_guid, u1, u2, u3, u4, u5) }, { - case OutfitMemberUpdate(outfit_guid, u2, u3) => - outfit_guid :: u2 :: u3 :: HNil + case OutfitMemberUpdate(outfit_guid, u1, u2, u3, u4, u5) => + outfit_guid :: u1 :: u2 :: u3 :: u4 :: u5 :: HNil } ) } From f2001dbc5f72cbc42d2c556a24494cadf91162c0 Mon Sep 17 00:00:00 2001 From: Resaec Date: Sun, 17 Aug 2025 18:58:36 +0200 Subject: [PATCH 05/22] random values :( --- .../packet/game/OutfitMemberEvent.scala | 28 ++++++++++++------- 1 file changed, 18 insertions(+), 10 deletions(-) diff --git a/src/main/scala/net/psforever/packet/game/OutfitMemberEvent.scala b/src/main/scala/net/psforever/packet/game/OutfitMemberEvent.scala index 737a72da..ddce733f 100644 --- a/src/main/scala/net/psforever/packet/game/OutfitMemberEvent.scala +++ b/src/main/scala/net/psforever/packet/game/OutfitMemberEvent.scala @@ -14,11 +14,15 @@ final case class OutfitMemberEvent( unk5: Int, unk6: Int, member_name: String, - unk7: Int, unk8: Int, unk9: Int, unk10: Int, unk11: Int, + unk12: Int, + unk13: Int, + unk14: Int, + unk15: Int, + unk16: Int, ) extends PlanetSideGamePacket { type Packet = OutfitMemberEvent def opcode = GamePacketOpcode.OutfitMemberEvent @@ -34,19 +38,23 @@ object OutfitMemberEvent extends Marshallable[OutfitMemberEvent] { ("unk5" | uint8L) :: ("unk6" | uint8L) :: ("member_name" | PacketHelpers.encodedWideStringAligned(6)) :: - ("unk7" | uint16L) :: - ("unk8" | uint16L) :: - ("unk9" | uint16L) :: - ("unk10" | uint16L) :: - ("unk11" | uint8L) + ("unk8" | uint8L) :: + ("unk9" | uint8L) :: + ("unk10" | uint8L) :: + ("unk11" | uint8L) :: + ("unk12" | uint8L) :: + ("unk13" | uint8L) :: + ("unk14" | uint8L) :: + ("unk15" | uint8L) :: + ("unk16" | 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 unk00 :: outfit_id :: u3 :: u4 :: u5 :: u6 :: member_name :: u8 :: u9 :: u10 :: u11 :: u12 :: u13 :: u14 :: u15 :: u16 :: HNil => + OutfitMemberEvent(unk00, outfit_id, u3, u4, u5, u6, member_name, u8, u9, u10, u11, u12, u13, u14, u15, u16) }, { - 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 + case OutfitMemberEvent(unk00, outfit_id, u3, u4, u5, u6, member_name, u8, u9, u10, u11, u12, u13, u14, u15, u16) => + unk00 :: outfit_id :: u3 :: u4 :: u5 :: u6 :: member_name :: u8 :: u9 :: u10 :: u11 :: u12 :: u13 :: u14 :: u15 :: u16 :: HNil } ) } From d19cd744d57abd6204fd6aafaa6510845f4c2424 Mon Sep 17 00:00:00 2001 From: Resaec Date: Sun, 17 Aug 2025 19:51:20 +0200 Subject: [PATCH 06/22] extend OMR with Unk2 codec --- .../packet/game/OutfitMembershipRequest.scala | 71 ++++++++++++++++--- .../scala/game/OutfitMemberEventTest.scala | 30 +++++--- .../game/OutfitMembershipRequestTest.scala | 21 +++++- 3 files changed, 101 insertions(+), 21 deletions(-) diff --git a/src/main/scala/net/psforever/packet/game/OutfitMembershipRequest.scala b/src/main/scala/net/psforever/packet/game/OutfitMembershipRequest.scala index adeaca48..6d3c17ea 100644 --- a/src/main/scala/net/psforever/packet/game/OutfitMembershipRequest.scala +++ b/src/main/scala/net/psforever/packet/game/OutfitMembershipRequest.scala @@ -24,15 +24,38 @@ abstract class OutfitMembershipRequestAction(val code: Int) object OutfitMembershipRequestAction { - final case class CreateOutfit(unk2: String, unk3: Int, unk4: Boolean, outfit_name: String) extends OutfitMembershipRequestAction(code = 0) + final case class CreateOutfit( + unk2: String, + unk3: Int, + unk4: Boolean, + outfit_name: String + ) extends OutfitMembershipRequestAction(code = 0) - final case class FormOutfit(unk2: String, unk3: Int, unk4: Boolean, outfit_name: String) extends OutfitMembershipRequestAction(code = 1) + final case class FormOutfit( + unk2: String, + unk3: Int, + unk4: Boolean, + outfit_name: String + ) extends OutfitMembershipRequestAction(code = 1) - final case class AcceptOutfitInvite(unk2: String) extends OutfitMembershipRequestAction(code = 3) + final case class Unk2( + unk2: Int, + unk3: Int, + member_name: String, + ) extends OutfitMembershipRequestAction(code = 2) + final case class AcceptOutfitInvite( + unk2: String + ) extends OutfitMembershipRequestAction(code = 3) - final case class RejectOutfitInvite(unk2: String) extends OutfitMembershipRequestAction(code = 4) + final case class RejectOutfitInvite( + unk2: String + ) extends OutfitMembershipRequestAction(code = 4) - final case class CancelOutfitInvite(unk5: Int, unk6: Int, outfit_name: String) extends OutfitMembershipRequestAction(code = 5) + 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) @@ -44,7 +67,12 @@ object OutfitMembershipRequestAction { private val everFailCondition = conditional(included = false, bool) val CreateOutfitCodec: Codec[CreateOutfit] = - (PacketHelpers.encodedWideString :: uint4L :: bool :: PacketHelpers.encodedWideString).xmap[CreateOutfit]( + ( + PacketHelpers.encodedWideString :: + uint4L :: + bool :: + PacketHelpers.encodedWideString + ).xmap[CreateOutfit]( { case unk2 :: unk3 :: unk4 :: outfit_name :: HNil => CreateOutfit(unk2, unk3, unk4, outfit_name) @@ -56,7 +84,12 @@ object OutfitMembershipRequestAction { ) val FormOutfitCodec: Codec[FormOutfit] = - (PacketHelpers.encodedWideString :: uint4L :: bool :: PacketHelpers.encodedWideString).xmap[FormOutfit]( + ( + PacketHelpers.encodedWideString :: + uint4L :: + bool :: + PacketHelpers.encodedWideString + ).xmap[FormOutfit]( { case unk2 :: unk3 :: unk4 :: outfit_name :: HNil => FormOutfit(unk2, unk3, unk4, outfit_name) @@ -67,6 +100,22 @@ object OutfitMembershipRequestAction { } ) + val Unk2Codec: Codec[Unk2] = + ( + uint16L :: + uint16L :: + PacketHelpers.encodedWideStringAligned(5) + ).xmap[Unk2]( + { + case unk2 :: unk3 :: member_name :: HNil => + Unk2(unk2, unk3, member_name) + }, + { + case Unk2(unk2, unk3, member_name) => + unk2 :: unk3 :: member_name :: HNil + } + ) + val AcceptOutfitCodec: Codec[AcceptOutfitInvite] = PacketHelpers.encodedWideString.xmap[AcceptOutfitInvite]( { @@ -92,7 +141,11 @@ object OutfitMembershipRequestAction { ) val CancelOutfitCodec: Codec[CancelOutfitInvite] = - (uint16L :: uint16L :: PacketHelpers.encodedWideStringAligned(5)).xmap[CancelOutfitInvite]( + ( + uint16L :: + uint16L :: + PacketHelpers.encodedWideStringAligned(5) + ).xmap[CancelOutfitInvite]( { case unk5 :: unk6 :: outfit_name :: HNil => CancelOutfitInvite(unk5, unk6, outfit_name) @@ -155,7 +208,7 @@ object OutfitMembershipRequest extends Marshallable[OutfitMembershipRequest] { ((code: @switch) match { case 0 => CreateOutfitCodec case 1 => FormOutfitCodec // so far same as Create - case 2 => unknownCodec(action = code) + case 2 => Unk2Codec case 3 => AcceptOutfitCodec case 4 => RejectOutfitCodec // so far same as Accept case 5 => CancelOutfitCodec diff --git a/src/test/scala/game/OutfitMemberEventTest.scala b/src/test/scala/game/OutfitMemberEventTest.scala index 9b07ec9d..4fe48a80 100644 --- a/src/test/scala/game/OutfitMemberEventTest.scala +++ b/src/test/scala/game/OutfitMemberEventTest.scala @@ -31,7 +31,7 @@ val unk0_ABC_Lazer: ByteVector = hex"90 048640001030c28022404c0061007a00650 "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) => + case OutfitMemberEvent(unk00, outfit_guid, unk3, unk4, unk5, unk6, member_name, unk8, unk9, unk10, unk11, unk12, unk13,unk14,unk15,unk16) => unk00 mustEqual 0 outfit_guid mustEqual 6418L unk3 mustEqual 64 @@ -39,11 +39,15 @@ val unk0_ABC_Lazer: ByteVector = hex"90 048640001030c28022404c0061007a00650 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 + unk8 mustEqual 244 + unk9 mustEqual 58 + unk10 mustEqual 69 + unk11 mustEqual 224 + unk12 mustEqual 11 + unk13 mustEqual 76 + unk14 mustEqual 96 + unk15 mustEqual 64 + unk16 mustEqual 16 case _ => ko } @@ -58,11 +62,15 @@ val unk0_ABC_Lazer: ByteVector = hex"90 048640001030c28022404c0061007a00650 unk5 = 10, unk6 = 0, member_name = "Lazer1982", - unk7 = 15092, - unk8 = 57413, - unk9 = 19467, - unk10 = 16480, - unk11 = 16, + unk8 = 244, + unk9 = 58, + unk10 = 69, + unk11 = 224, + unk12 = 11, + unk13 = 76, + unk14 = 96, + unk15 = 64, + unk16 = 16, ) val pkt = PacketCoding.encodePacket(msg).require.toByteVector diff --git a/src/test/scala/game/OutfitMembershipRequestTest.scala b/src/test/scala/game/OutfitMembershipRequestTest.scala index 163e4c45..bef384a5 100644 --- a/src/test/scala/game/OutfitMembershipRequestTest.scala +++ b/src/test/scala/game/OutfitMembershipRequestTest.scala @@ -13,7 +13,8 @@ class OutfitMembershipRequestTest extends Specification { val create_2222 = hex"8c 0 1000 000 1000 84 3200320032003200" val form_abc = hex"8c 2 0200 000 1000 83 610062006300" val form_1 = hex"8c 2 1000 000 1000 81 3100" - val unk3 = hex"8c 5 bb39 9e0 2000 0000 1080 750072006f006200" // -- "urob" -- could be false positive -- seems to gets an OMSResp -> 0x8d271bb399e025af8f405080550072006f0062008080 + val unk2 = hex"8c 5 bb399e0 2000 0000 1140 7600690072007500730067006900760065007200" // -- virusgiver + val unk3 = hex"8c 5 bb399e0 2000 0000 1080 750072006f006200" // -- "urob" -- could be false positive -- seems to gets an OMSResp -> 0x8d271bb399e025af8f405080550072006f0062008080 val accept_1 = hex"8c 6 0200 000 1000" val accept_2 = hex"8c 6 0400 000 1000" val reject_1 = hex"8c 8 0200 000 1000" @@ -94,6 +95,24 @@ class OutfitMembershipRequestTest extends Specification { pkt mustEqual form_1 } + "decode Unk2" in { + PacketCoding.decodePacket(unk2).require match { + case OutfitMembershipRequest(request_type, outfit_id, action) => + request_type mustEqual RequestType.Unk2 + outfit_id mustEqual 30383325L + action mustEqual Unk2(0, 0, "virusgiver") + case _ => + ko + } + } + + "encode Unk2" in { + val msg = OutfitMembershipRequest(RequestType.Unk2, 30383325L, Unk2(0, 0, "virusgiver")) + val pkt = PacketCoding.encodePacket(msg).require.toByteVector + + pkt mustEqual unk2 + } + "decode AcceptOutfitInvite 1" in { PacketCoding.decodePacket(accept_1).require match { case OutfitMembershipRequest(request_type, avatar_id, action) => From 308ea20deed7f89b97f6496ceb1d15d0a3cc9c5d Mon Sep 17 00:00:00 2001 From: Resaec Date: Sun, 17 Aug 2025 21:20:29 +0200 Subject: [PATCH 07/22] same same, same same... uint32L? --- .../packet/game/OutfitMemberEvent.scala | 20 ++++++++----------- .../game/OutfitMembershipResponse.scala | 4 ++-- .../scala/game/OutfitMemberEventTest.scala | 10 +++------- 3 files changed, 13 insertions(+), 21 deletions(-) diff --git a/src/main/scala/net/psforever/packet/game/OutfitMemberEvent.scala b/src/main/scala/net/psforever/packet/game/OutfitMemberEvent.scala index ddce733f..a16ade5b 100644 --- a/src/main/scala/net/psforever/packet/game/OutfitMemberEvent.scala +++ b/src/main/scala/net/psforever/packet/game/OutfitMemberEvent.scala @@ -9,10 +9,8 @@ import shapeless.{::, HNil} final case class OutfitMemberEvent( unk00: Int, outfit_id: Long, - unk3: Int, - unk4: Int, - unk5: Int, - unk6: Int, + unk3: Int, // OMR(Unk1) target_guid + unk5: Int, // OMR(Unk1) unk3 member_name: String, unk8: Int, unk9: Int, @@ -33,10 +31,8 @@ object OutfitMemberEvent extends Marshallable[OutfitMemberEvent] { implicit val codec: Codec[OutfitMemberEvent] = ( ("unk00" | uintL(2)) :: ("outfit_id" | uint32L) :: - ("unk3" | uint8L) :: - ("unk4" | uint8L) :: - ("unk5" | uint8L) :: - ("unk6" | uint8L) :: + ("unk3" | uint16L) :: // OMR(Unk1) unk2 + ("unk5" | uint16L) :: ("member_name" | PacketHelpers.encodedWideStringAligned(6)) :: ("unk8" | uint8L) :: ("unk9" | uint8L) :: @@ -49,12 +45,12 @@ object OutfitMemberEvent extends Marshallable[OutfitMemberEvent] { ("unk16" | uint8L) ).xmap[OutfitMemberEvent]( { - case unk00 :: outfit_id :: u3 :: u4 :: u5 :: u6 :: member_name :: u8 :: u9 :: u10 :: u11 :: u12 :: u13 :: u14 :: u15 :: u16 :: HNil => - OutfitMemberEvent(unk00, outfit_id, u3, u4, u5, u6, member_name, u8, u9, u10, u11, u12, u13, u14, u15, u16) + case unk00 :: outfit_id :: u3 :: u5 :: member_name :: u8 :: u9 :: u10 :: u11 :: u12 :: u13 :: u14 :: u15 :: u16 :: HNil => + OutfitMemberEvent(unk00, outfit_id, u3, u5, member_name, u8, u9, u10, u11, u12, u13, u14, u15, u16) }, { - case OutfitMemberEvent(unk00, outfit_id, u3, u4, u5, u6, member_name, u8, u9, u10, u11, u12, u13, u14, u15, u16) => - unk00 :: outfit_id :: u3 :: u4 :: u5 :: u6 :: member_name :: u8 :: u9 :: u10 :: u11 :: u12 :: u13 :: u14 :: u15 :: u16 :: HNil + case OutfitMemberEvent(unk00, outfit_id, u3, u5, member_name, u8, u9, u10, u11, u12, u13, u14, u15, u16) => + unk00 :: outfit_id :: u3 :: u5 :: member_name :: u8 :: u9 :: u10 :: u11 :: u12 :: u13 :: u14 :: u15 :: u16 :: HNil } ) } diff --git a/src/main/scala/net/psforever/packet/game/OutfitMembershipResponse.scala b/src/main/scala/net/psforever/packet/game/OutfitMembershipResponse.scala index 1a75371a..086d42ed 100644 --- a/src/main/scala/net/psforever/packet/game/OutfitMembershipResponse.scala +++ b/src/main/scala/net/psforever/packet/game/OutfitMembershipResponse.scala @@ -13,7 +13,7 @@ final case class OutfitMembershipResponse( response_type: OutfitMembershipResponse.ResponseType.Type, unk0: Int, outfit_id: Long, - unk2: PlanetSideGUID, + target_guid: PlanetSideGUID, unk3: Int, //unk4: Boolean, action: OutfitMembershipResponseAction @@ -190,7 +190,7 @@ object OutfitMembershipResponse extends Marshallable[OutfitMembershipResponse] { type Type = Value val CreateResponse: ResponseType.Value = Value(0) - val Unk1: ResponseType.Value = Value(1) + val Unk1: ResponseType.Value = Value(1) // Info: Player has been invited / response to OutfitMembershipRequest Unk2 for that player val Unk2: ResponseType.Value = Value(2) // Invited / Accepted / Added val Unk3: ResponseType.Value = Value(3) val Unk4: ResponseType.Value = Value(4) diff --git a/src/test/scala/game/OutfitMemberEventTest.scala b/src/test/scala/game/OutfitMemberEventTest.scala index 4fe48a80..0317b834 100644 --- a/src/test/scala/game/OutfitMemberEventTest.scala +++ b/src/test/scala/game/OutfitMemberEventTest.scala @@ -31,13 +31,11 @@ val unk0_ABC_Lazer: ByteVector = hex"90 048640001030c28022404c0061007a00650 "decode Unk0 ABC" in { PacketCoding.decodePacket(unk0_ABC_Lazer).require match { - case OutfitMemberEvent(unk00, outfit_guid, unk3, unk4, unk5, unk6, member_name, unk8, unk9, unk10, unk11, unk12, unk13,unk14,unk15,unk16) => + case OutfitMemberEvent(unk00, outfit_guid, unk3, unk5, member_name, unk8, unk9, unk10, unk11, unk12, unk13,unk14,unk15,unk16) => unk00 mustEqual 0 outfit_guid mustEqual 6418L - unk3 mustEqual 64 - unk4 mustEqual 195 + unk3 mustEqual 49984 unk5 mustEqual 10 - unk6 mustEqual 0 member_name mustEqual "Lazer1982" unk8 mustEqual 244 unk9 mustEqual 58 @@ -57,10 +55,8 @@ val unk0_ABC_Lazer: ByteVector = hex"90 048640001030c28022404c0061007a00650 val msg = OutfitMemberEvent( unk00 = 0, outfit_id = 6418L, - unk3 = 64, - unk4 = 195, + unk3 = 49984, unk5 = 10, - unk6 = 0, member_name = "Lazer1982", unk8 = 244, unk9 = 58, From d450a1b6e5c536d76bc9f23b5604dcdfa0df64d0 Mon Sep 17 00:00:00 2001 From: Resaec Date: Wed, 20 Aug 2025 00:03:17 +0200 Subject: [PATCH 08/22] OutfitListEvent ListElementOutfit decoded --- .../psforever/packet/game/OutfitEvent.scala | 29 +--- .../packet/game/OutfitListEvent.scala | 162 ++++++++++++++++-- .../psforever/packet/game/OutfitRequest.scala | 4 +- src/test/scala/game/OutfitEventTest.scala | 16 +- src/test/scala/game/OutfitListEventTest.scala | 35 ++-- 5 files changed, 174 insertions(+), 72 deletions(-) diff --git a/src/main/scala/net/psforever/packet/game/OutfitEvent.scala b/src/main/scala/net/psforever/packet/game/OutfitEvent.scala index 90cffd4b..3117bbb4 100644 --- a/src/main/scala/net/psforever/packet/game/OutfitEvent.scala +++ b/src/main/scala/net/psforever/packet/game/OutfitEvent.scala @@ -38,8 +38,8 @@ object OutfitEventAction { final case class OutfitInfo( outfit_name: String, - unk6: Long, - unk7: Long, + outfit_points1: Long, + outfit_points2: Long, // same as outfit_points1 member_count: Int, unk9: Int, outfit_rank_names: OutfitRankNames, @@ -276,31 +276,6 @@ object OutfitEvent extends Marshallable[OutfitEvent] { 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)) } diff --git a/src/main/scala/net/psforever/packet/game/OutfitListEvent.scala b/src/main/scala/net/psforever/packet/game/OutfitListEvent.scala index 52ea9929..c040f2e5 100644 --- a/src/main/scala/net/psforever/packet/game/OutfitListEvent.scala +++ b/src/main/scala/net/psforever/packet/game/OutfitListEvent.scala @@ -3,19 +3,14 @@ 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.{Attempt, Codec, Err} +import scodec.bits.{BitVector, ByteVector} import scodec.codecs._ -import scodec.{Attempt, Codec} - -// 98 5ec300000d01a020004000001 12056002e0053002e0053002e0055002e004400 845400680069006f009 85ee300001e2b 858000800000110041002e0027002e0041002e0027002e00 8448006900720075009 84003200005a540000060000011a0530065006300720065007400200043006800690065006600730085530069006c00610073009840a32000001953476fe0c00011c041007a0075007200650020005400770069006c006900670068007400874600720061006e0063006b006f009840c3200000d3a4c000c00000106030002e006f0085410074006c0061007300984183200011d9296000c0000011e0570061007200720069006f007200270073002000430072006500650064008653006500760061006b00690098442320001bf40e000080000013203100330033003700740068002000410072006d006f0072006500640020004400690076006900730069006f006e002d004b008548006f0073002d004b009844c320001b3d2c200060000012a03300330031007300740020004d0069006e006e00650073006f0074006100200054007200690062006500864d006100670069002d0045009846c3200009e206c00040000010c04100720065006100350031008942006c00610063 +import shapeless.{::, HNil} final case class OutfitListEvent( - outfit_score: Long, - unk1: Long, - unk2: Long, - unk3: Int, - outfit_name: String, - outfit_leader: String, + request_type: OutfitListEvent.RequestType.Type, + action: OutfitListEventAction ) extends PlanetSideGamePacket { type Packet = OutfitListEvent @@ -23,13 +18,144 @@ final case class OutfitListEvent( def encode: Attempt[BitVector] = OutfitListEvent.encode(this) } -object OutfitListEvent extends Marshallable[OutfitListEvent] { - implicit val codec: Codec[OutfitListEvent] = ( - ("outfit_score" | uint32) :: + +abstract class OutfitListEventAction(val code: Int) + +object OutfitListEventAction { + + final case class ListElementOutfit( + outfit_id: Long, + points: Long, + members: Long, + outfit_name: String, + outfit_leader: String, + ) extends OutfitListEventAction(code = 2) + + /* + TODO: Check packet when bundle packet has been implemented (packet containing OutfitListEvent packets back to back) + For now it seems like there is no valid packet captured + */ + final case class Unk3( + unk1: Long, + data: ByteVector + ) extends OutfitListEventAction(code = 3) + + final case class Unknown(badCode: Int, data: BitVector) extends OutfitListEventAction(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 ListElementOutfitCodec: Codec[ListElementOutfit] = ( ("unk1" | uint32L) :: - ("unk2" | uint32L) :: - ("unk3" | uint(3)) :: - ("outfit_name" | PacketHelpers.encodedWideStringAligned(5)) :: - ("outfit_leader" | PacketHelpers.encodedWideString) - ).as[OutfitListEvent] + ("points" | uint32L) :: + ("members" | uint32L) :: + ("outfit_name" | PacketHelpers.encodedWideStringAligned(5)) :: + ("outfit_leader" | PacketHelpers.encodedWideString) + ).xmap[ListElementOutfit]( + { + case u1 :: points :: members :: outfit_name :: outfit_leader :: HNil => + ListElementOutfit(u1, points, members, outfit_name, outfit_leader) + }, + { + case ListElementOutfit(u1, points, members, outfit_name, outfit_leader) => + u1 :: points :: members :: outfit_name :: outfit_leader :: HNil + } + ) + + val Unk3Codec: Codec[Unk3] = ( + ("unk1" | uint32L) :: + ("data" | bytes) + ).xmap[Unk3]( + { + case u1 :: data :: HNil => + Unk3(u1, data) + }, + { + case Unk3(u1, data) => + u1 :: 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[OutfitListEventAction] = + everFailCondition.exmap[OutfitListEventAction]( + _ => 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 OutfitListEvent extends Marshallable[OutfitListEvent] { + import shapeless.{::, HNil} + + object RequestType extends Enumeration { + type Type = Value + + val Unk0: RequestType.Value = Value(0) + val Unk1: RequestType.Value = Value(1) + val ListElementOutfit: RequestType.Value = Value(2) + val Unk3: RequestType.Value = Value(3) + val Unk4: 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, uintL(3)) + } + + private def selectFromType(code: Int): Codec[OutfitListEventAction] = { + import OutfitListEventAction.Codecs._ + import scala.annotation.switch + + ((code: @switch) match { + case 0 => unknownCodec(action = code) + case 1 => unknownCodec(action = code) + case 2 => ListElementOutfitCodec + case 3 => Unk3Codec // indicated in code + case 4 => unknownCodec(action = code) + case 5 => unknownCodec(action = code) + case 6 => unknownCodec(action = code) + case 7 => unknownCodec(action = code) + + case _ => failureCodec(code) + }).asInstanceOf[Codec[OutfitListEventAction]] + } + + implicit val codec: Codec[OutfitListEvent] = ( + ("request_type" | RequestType.codec) >>:~ { request_type => + ("action" | selectFromType(request_type.id)).hlist + } + ).xmap[OutfitListEvent]( + { + case request_type :: action :: HNil => + OutfitListEvent(request_type, action) + }, + { + case OutfitListEvent(request_type, action) => + request_type :: 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 585ed5ee..c62b1fda 100644 --- a/src/main/scala/net/psforever/packet/game/OutfitRequest.scala +++ b/src/main/scala/net/psforever/packet/game/OutfitRequest.scala @@ -149,7 +149,7 @@ object OutfitRequest extends Marshallable[OutfitRequest] { val Rank: RequestType.Value = Value(1) val Unk2: RequestType.Value = Value(2) val Detail: RequestType.Value = Value(3) - val List: RequestType.Value = Value(4) + val List: RequestType.Value = Value(4) // sent by client if menu is either open (true) or closed (false) implicit val codec: Codec[Type] = PacketHelpers.createEnumerationCodec(this, uintL(3)) } @@ -175,7 +175,7 @@ object OutfitRequest extends Marshallable[OutfitRequest] { } ).xmap[OutfitRequest]( { - case _:: id:: action :: HNil => + case _ :: id:: action :: HNil => OutfitRequest(id, action) }, { diff --git a/src/test/scala/game/OutfitEventTest.scala b/src/test/scala/game/OutfitEventTest.scala index eab4f75a..8837b93c 100644 --- a/src/test/scala/game/OutfitEventTest.scala +++ b/src/test/scala/game/OutfitEventTest.scala @@ -58,8 +58,8 @@ class OutfitEventTest extends Specification { action mustEqual Unk0( OutfitInfo( outfit_name = "Black Armored Reapers", - unk6 = 223190045, - unk7 = 223190045, + outfit_points1 = 223190045, + outfit_points2 = 223190045, member_count = 171, unk9 = 0, OutfitRankNames("Dog Meat","Russian","","","Squad Leaders","Acting Commanders","Reapers",""), @@ -87,8 +87,8 @@ class OutfitEventTest extends Specification { Unk0( OutfitInfo( outfit_name = "Black Armored Reapers", - unk6 = 223190045, - unk7 = 223190045, + outfit_points1 = 223190045, + outfit_points2 = 223190045, member_count = 171, unk9 = 0, OutfitRankNames("Dog Meat","Russian","","","Squad Leaders","Acting Commanders","Reapers",""), @@ -145,8 +145,8 @@ class OutfitEventTest extends Specification { outfit_guid mustEqual 2147418113L action mustEqual Unk2(OutfitInfo( outfit_name = "PlanetSide_Forever_Vanu", - unk6 = 0, - unk7 = 0, + outfit_points1 = 0, + outfit_points2 = 0, member_count = 1, unk9 = 0, OutfitRankNames("","","","","","","",""), @@ -173,8 +173,8 @@ class OutfitEventTest extends Specification { Unk2( OutfitInfo( outfit_name = "PlanetSide_Forever_Vanu", - unk6 = 0, - unk7 = 0, + outfit_points1 = 0, + outfit_points2 = 0, member_count = 1, unk9 = 0, OutfitRankNames("","","","","","","",""), diff --git a/src/test/scala/game/OutfitListEventTest.scala b/src/test/scala/game/OutfitListEventTest.scala index 7935df12..e7731a2d 100644 --- a/src/test/scala/game/OutfitListEventTest.scala +++ b/src/test/scala/game/OutfitListEventTest.scala @@ -2,29 +2,30 @@ package game import net.psforever.packet._ -import net.psforever.packet.game._ +import net.psforever.packet.game.OutfitListEvent +import net.psforever.packet.game.OutfitListEvent.RequestType +import net.psforever.packet.game.OutfitListEventAction.ListElementOutfit 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 + val unk0_ABC: ByteVector = ByteVector.fromValidHex("98 5 e83a0000 000e1800 0800000 11404e0069006700680074004c006f00720064007300 854e005900430061007400") + val unk0_DEF: ByteVector = ByteVector.fromValidHex("98 4 ec281001 51a62800 3400000 11a0490052004f004e004600490053005400200043006c0061006e00 8654006f006c006a00") + val unk1_ABC: ByteVector = ByteVector.fromValidHex("98 4 723c0000 2aa81e00 2200000 11006900470061006d00650073002d004500 906900670061006d006500730043005400460057006800610063006b002d004500") + val unk2_ABC: ByteVector = ByteVector.fromValidHex("98 4 9a3c0001 16da4e00 0400000 11a042006c006f006f00640020006f0066002000560061006e007500 864b00610072006e002d004500") + val unk3_ABC: ByteVector = ByteVector.fromValidHex("98 4 9c3c0000 df587c00 1400000 11a054006800650020004e00650076006500720068006f006f006400 8e6f00460058006f00530074006f006e0065004d0061006e002d004700") + val unk4_ABC: ByteVector = ByteVector.fromValidHex("98 4 c03c0000 24060400 0600000 1220540068006500200042006c00610063006b0020004b006e0069006700680074007300 874400720061007a00760065006e00") + val unk5_ABC: ByteVector = ByteVector.fromValidHex("98 5 383c0001 4b709a00 0c00000 10a03e005400760053003c00 89430061007000650062006f00610074007300") + val unk6_ABC: ByteVector = ByteVector.fromValidHex("98 5 b03c0000 35d67000 0400000 11404c006f0073007400200043006100750073006500 895a00650072006f004b00650077006c006c00") + val unk7_ABC: ByteVector = ByteVector.fromValidHex("98 4 043e0001 9fb82616 1400000 11e0540068006500200042006c00610063006b00200054006f00770065007200 874b00720075007000680065007800") "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 + case OutfitListEvent(code, ListElementOutfit(unk1, points, members, outfit_name, outfit_leader)) => + code mustEqual OutfitListEvent.RequestType.ListElementOutfit + unk1 mustEqual 7668 + points mustEqual 788224 + members mustEqual 4 outfit_name mustEqual "NightLords" outfit_leader mustEqual "NYCat" case _ => @@ -33,7 +34,7 @@ class OutfitListEventTest extends Specification { } "encode unk0_ABC" in { - val msg = OutfitListEvent(1585684480L, 2162229248L, 32768, 0, "NightLords", "NYCat") + val msg = OutfitListEvent(RequestType.ListElementOutfit, ListElementOutfit(7668, 788224, 4, "NightLords", "NYCat")) val pkt = PacketCoding.encodePacket(msg).require.toByteVector pkt mustEqual unk0_ABC From b070834a8a73cda87a80ac2460146c972f29f462 Mon Sep 17 00:00:00 2001 From: ScrawnyRonnie Date: Wed, 20 Aug 2025 12:31:08 -0400 Subject: [PATCH 09/22] member packets --- .../psforever/packet/game/OutfitEvent.scala | 24 ++++++----- .../packet/game/OutfitMemberEvent.scala | 42 +++++++------------ .../packet/game/OutfitMemberUpdate.scala | 22 ++++------ 3 files changed, 38 insertions(+), 50 deletions(-) diff --git a/src/main/scala/net/psforever/packet/game/OutfitEvent.scala b/src/main/scala/net/psforever/packet/game/OutfitEvent.scala index 3117bbb4..79850237 100644 --- a/src/main/scala/net/psforever/packet/game/OutfitEvent.scala +++ b/src/main/scala/net/psforever/packet/game/OutfitEvent.scala @@ -44,8 +44,10 @@ object OutfitEventAction { unk9: Int, outfit_rank_names: OutfitRankNames, motd: String, - owner_guid: PlanetSideGUID, // ? - unk20: Int, + unk10: Int, + unk11: Int, + unk12: Int, + unk13: Int,// ? unk21: Int, unk21_2: Int, created_timestamp: Long, @@ -126,8 +128,10 @@ object OutfitEventAction { uint16L :: OutfitRankNamesCodec :: PacketHelpers.encodedWideString :: - PlanetSideGUID.codec :: - uint16L :: // + uint8L :: + uint8L :: + uint8L :: + uint8L :: uint8L :: // bool somewhere here uintL(1) :: // ("created_timestamp" | uint32L) :: @@ -137,12 +141,12 @@ object OutfitEventAction { uintL(7) ).xmap[OutfitInfo]( { - case outfit_name :: u6 :: u7 :: member_count :: u9 :: outfit_rank_names :: motd :: u19 :: u20 :: u21 :: u21_2 :: created_timestamp :: u23 :: u24 :: u25 :: u123 :: HNil => - OutfitInfo(outfit_name, u6, u7, member_count, u9, outfit_rank_names, motd, u19, u20, u21, u21_2, created_timestamp, u23, u24, u25, u123) + case outfit_name :: u6 :: u7 :: member_count :: u9 :: outfit_rank_names :: motd :: u10 :: u11 :: u12 :: u13 :: u21 :: u21_2 :: created_timestamp :: u23 :: u24 :: u25 :: u123 :: HNil => + OutfitInfo(outfit_name, u6, u7, member_count, u9, outfit_rank_names, motd, u10, u11, u12, u13, u21, u21_2, created_timestamp, u23, u24, u25, u123) }, { - case OutfitInfo(outfit_name, u6, u7, member_count, u9, outfit_rank_names, motd, u19, u20, u21, u21_2, created_timestamp, u23, u24, u25, u123) => - outfit_name :: u6 :: u7 :: member_count :: u9 :: outfit_rank_names :: motd :: u19 :: u20 :: u21 :: u21_2 :: created_timestamp :: u23 :: u24 :: u25 :: u123 :: HNil + case OutfitInfo(outfit_name, u6, u7, member_count, u9, outfit_rank_names, motd, u10, u11, u12, u13, u21, u21_2, created_timestamp, u23, u24, u25, u123) => + outfit_name :: u6 :: u7 :: member_count :: u9 :: outfit_rank_names :: motd :: u10 :: u11 :: u12 :: u13 :: u21 :: u21_2 :: created_timestamp :: u23 :: u24 :: u25 :: u123 :: HNil } ) @@ -284,9 +288,9 @@ object OutfitEvent extends Marshallable[OutfitEvent] { import scala.annotation.switch ((code: @switch) match { - case 0 => Unk0Codec + case 0 => Unk0Codec // view outfit window and members case 1 => Unk1Codec - case 2 => Unk2Codec // sent after /outfitcreate ? + case 2 => Unk2Codec // sent after /outfitcreate and on login if in an outfit case 3 => Unk3Codec case 4 => Unk4Codec case 5 => Unk5Codec diff --git a/src/main/scala/net/psforever/packet/game/OutfitMemberEvent.scala b/src/main/scala/net/psforever/packet/game/OutfitMemberEvent.scala index a16ade5b..67c95820 100644 --- a/src/main/scala/net/psforever/packet/game/OutfitMemberEvent.scala +++ b/src/main/scala/net/psforever/packet/game/OutfitMemberEvent.scala @@ -9,18 +9,12 @@ import shapeless.{::, HNil} final case class OutfitMemberEvent( unk00: Int, outfit_id: Long, - unk3: Int, // OMR(Unk1) target_guid - unk5: Int, // OMR(Unk1) unk3 + member_id: Long, member_name: String, - unk8: Int, - unk9: Int, - unk10: Int, - unk11: Int, - unk12: Int, - unk13: Int, - unk14: Int, - unk15: Int, - unk16: Int, + rank: Int, // 0-7 + points: Long, // client divides this by 100 + last_login: Long, // seconds ago from current time, 0 if online + unk1: Int, ) extends PlanetSideGamePacket { type Packet = OutfitMemberEvent def opcode = GamePacketOpcode.OutfitMemberEvent @@ -31,26 +25,20 @@ object OutfitMemberEvent extends Marshallable[OutfitMemberEvent] { implicit val codec: Codec[OutfitMemberEvent] = ( ("unk00" | uintL(2)) :: ("outfit_id" | uint32L) :: - ("unk3" | uint16L) :: // OMR(Unk1) unk2 - ("unk5" | uint16L) :: - ("member_name" | PacketHelpers.encodedWideStringAligned(6)) :: - ("unk8" | uint8L) :: - ("unk9" | uint8L) :: - ("unk10" | uint8L) :: - ("unk11" | uint8L) :: - ("unk12" | uint8L) :: - ("unk13" | uint8L) :: - ("unk14" | uint8L) :: - ("unk15" | uint8L) :: - ("unk16" | uint8L) + ("member_id" | uint32L) :: + ("member_name" | PacketHelpers.encodedWideStringAligned(6)) :: + ("rank" | uint(3)) :: + ("points" | uint32L) :: + ("last_login" | uint32L) :: + ("unk1" | uint(5)) ).xmap[OutfitMemberEvent]( { - case unk00 :: outfit_id :: u3 :: u5 :: member_name :: u8 :: u9 :: u10 :: u11 :: u12 :: u13 :: u14 :: u15 :: u16 :: HNil => - OutfitMemberEvent(unk00, outfit_id, u3, u5, member_name, u8, u9, u10, u11, u12, u13, u14, u15, u16) + case unk00 :: outfit_id :: member_id :: member_name :: rank :: points :: last_login :: u1 :: HNil => + OutfitMemberEvent(unk00, outfit_id, member_id, member_name, rank, points, last_login, u1) }, { - case OutfitMemberEvent(unk00, outfit_id, u3, u5, member_name, u8, u9, u10, u11, u12, u13, u14, u15, u16) => - unk00 :: outfit_id :: u3 :: u5 :: member_name :: u8 :: u9 :: u10 :: u11 :: u12 :: u13 :: u14 :: u15 :: u16 :: HNil + case OutfitMemberEvent(unk00, outfit_id, member_id, member_name, rank, points, last_login, u1) => + unk00 :: outfit_id :: member_id :: member_name :: rank :: points :: last_login :: u1 :: HNil } ) } diff --git a/src/main/scala/net/psforever/packet/game/OutfitMemberUpdate.scala b/src/main/scala/net/psforever/packet/game/OutfitMemberUpdate.scala index bf94b3a8..39f68296 100644 --- a/src/main/scala/net/psforever/packet/game/OutfitMemberUpdate.scala +++ b/src/main/scala/net/psforever/packet/game/OutfitMemberUpdate.scala @@ -9,11 +9,9 @@ import shapeless.{::, HNil} final case class OutfitMemberUpdate( outfit_guid: Long, + char_id: Long, + rank: Int, // 0-7 unk1: Int, - unk2: Int, - unk3: Int, - unk4: Int, - unk5: Int, ) extends PlanetSideGamePacket { type Packet = OutfitMemberUpdate def opcode = GamePacketOpcode.OutfitMemberUpdate @@ -23,19 +21,17 @@ final case class OutfitMemberUpdate( object OutfitMemberUpdate extends Marshallable[OutfitMemberUpdate] { implicit val codec: Codec[OutfitMemberUpdate] = ( ("outfit_guid" | uint32L) :: - ("unk1" | uint8L) :: - ("unk2" | uint8L) :: - ("unk3" | uint8L) :: - ("unk4" | uint8L) :: - ("unk5" | uint8L) + ("char_id" | uint32L) :: + ("rank" | uint(3)) :: + ("unk1" | uint(5)) ).xmap[OutfitMemberUpdate]( { - case outfit_guid :: u1 :: u2 :: u3 :: u4 :: u5 :: HNil => - OutfitMemberUpdate(outfit_guid, u1, u2, u3, u4, u5) + case outfit_guid :: char_id :: rank :: u1 :: HNil => + OutfitMemberUpdate(outfit_guid, char_id, rank, u1) }, { - case OutfitMemberUpdate(outfit_guid, u1, u2, u3, u4, u5) => - outfit_guid :: u1 :: u2 :: u3 :: u4 :: u5 :: HNil + case OutfitMemberUpdate(outfit_guid, char_id, rank, u1) => + outfit_guid :: char_id :: rank :: u1 :: HNil } ) } From 17682c08d647fa7e003aff35afd26356def2ab02 Mon Sep 17 00:00:00 2001 From: Resaec Date: Thu, 21 Aug 2025 02:23:32 +0200 Subject: [PATCH 10/22] OutfitMembershipRequest all packets known! OutfitMembershipResponse decoded, needs rework OutfitMemberEvent decoded, needs rework Tests reworked --- .../psforever/packet/game/OutfitEvent.scala | 1 - .../packet/game/OutfitMemberEvent.scala | 52 +++-- .../packet/game/OutfitMemberUpdate.scala | 1 - .../packet/game/OutfitMembershipRequest.scala | 189 +++++++++++------- .../game/OutfitMembershipResponse.scala | 155 +++++++------- src/test/scala/game/OutfitEventTest.scala | 23 ++- src/test/scala/game/OutfitListEventTest.scala | 22 +- .../scala/game/OutfitMemberEventTest.scala | 66 +++--- .../game/OutfitMembershipRequestTest.scala | 171 +++++++++++----- 9 files changed, 393 insertions(+), 287 deletions(-) diff --git a/src/main/scala/net/psforever/packet/game/OutfitEvent.scala b/src/main/scala/net/psforever/packet/game/OutfitEvent.scala index 79850237..1a633fa2 100644 --- a/src/main/scala/net/psforever/packet/game/OutfitEvent.scala +++ b/src/main/scala/net/psforever/packet/game/OutfitEvent.scala @@ -3,7 +3,6 @@ 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._ diff --git a/src/main/scala/net/psforever/packet/game/OutfitMemberEvent.scala b/src/main/scala/net/psforever/packet/game/OutfitMemberEvent.scala index 67c95820..0a0f31f2 100644 --- a/src/main/scala/net/psforever/packet/game/OutfitMemberEvent.scala +++ b/src/main/scala/net/psforever/packet/game/OutfitMemberEvent.scala @@ -3,42 +3,52 @@ package net.psforever.packet.game import net.psforever.packet.{GamePacketOpcode, Marshallable, PacketHelpers, PlanetSideGamePacket} import scodec.Codec +import scodec.bits.ByteVector import scodec.codecs._ import shapeless.{::, HNil} +/* + action is unimplemented! if action == 0 only outfit_id and member_id are sent + action2 is unimplemented! if action2 == 0 unk2 will contain one additional uint32L + unk2 contains one byte of padding. may contain 4byte of unknown data depending on action2 + */ final case class OutfitMemberEvent( - unk00: Int, - outfit_id: Long, - member_id: Long, - member_name: String, - rank: Int, // 0-7 - points: Long, // client divides this by 100 - last_login: Long, // seconds ago from current time, 0 if online - unk1: Int, -) extends PlanetSideGamePacket { + action: Int, // action is unimplemented + outfit_id: Long, + member_id: Long, + member_name: String, + rank: Int, // 0-7 + points: Long, // client divides this by 100 + last_login: Long, // seconds ago from current time, 0 if online + action2: Int, // this should always be 1, otherwise there will be actual data in unk2! + padding: ByteVector, // only contains information if unk1 is 0, 1 byte of padding otherwise + ) 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) :: - ("member_id" | uint32L) :: - ("member_name" | PacketHelpers.encodedWideStringAligned(6)) :: - ("rank" | uint(3)) :: - ("points" | uint32L) :: - ("last_login" | uint32L) :: - ("unk1" | uint(5)) + ("action" | uintL(2)) :: + ("outfit_id" | uint32L) :: + ("member_id" | uint32L) :: + ("member_name" | PacketHelpers.encodedWideStringAligned(6)) :: + ("rank" | uint(3)) :: + ("points" | uint32L) :: + ("last_login" | uint32L) :: + ("action2" | uintL(1)) :: + ("padding" | bytes) ).xmap[OutfitMemberEvent]( { - case unk00 :: outfit_id :: member_id :: member_name :: rank :: points :: last_login :: u1 :: HNil => - OutfitMemberEvent(unk00, outfit_id, member_id, member_name, rank, points, last_login, u1) + case unk00 :: outfit_id :: member_id :: member_name :: rank :: points :: last_login :: u1 :: padding :: HNil => + OutfitMemberEvent(unk00, outfit_id, member_id, member_name, rank, points, last_login, u1, padding) }, { - case OutfitMemberEvent(unk00, outfit_id, member_id, member_name, rank, points, last_login, u1) => - unk00 :: outfit_id :: member_id :: member_name :: rank :: points :: last_login :: u1 :: HNil + case OutfitMemberEvent(unk00, outfit_id, member_id, member_name, rank, points, last_login, action2, padding) => + unk00 :: outfit_id :: member_id :: member_name :: rank :: points :: last_login :: action2 :: padding :: HNil } ) } diff --git a/src/main/scala/net/psforever/packet/game/OutfitMemberUpdate.scala b/src/main/scala/net/psforever/packet/game/OutfitMemberUpdate.scala index 39f68296..88cae632 100644 --- a/src/main/scala/net/psforever/packet/game/OutfitMemberUpdate.scala +++ b/src/main/scala/net/psforever/packet/game/OutfitMemberUpdate.scala @@ -2,7 +2,6 @@ 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} diff --git a/src/main/scala/net/psforever/packet/game/OutfitMembershipRequest.scala b/src/main/scala/net/psforever/packet/game/OutfitMembershipRequest.scala index 6d3c17ea..5e0c8b9b 100644 --- a/src/main/scala/net/psforever/packet/game/OutfitMembershipRequest.scala +++ b/src/main/scala/net/psforever/packet/game/OutfitMembershipRequest.scala @@ -22,41 +22,50 @@ final case class OutfitMembershipRequest( abstract class OutfitMembershipRequestAction(val code: Int) +/* + Codecs 2,5,6,7 can either work off of the avatar_id (if GUI was used) or member_name (if chat command was used) + */ object OutfitMembershipRequestAction { - final case class CreateOutfit( - unk2: String, - unk3: Int, - unk4: Boolean, + final case class Create( + unk1: String, outfit_name: String ) extends OutfitMembershipRequestAction(code = 0) - final case class FormOutfit( - unk2: String, - unk3: Int, - unk4: Boolean, + final case class Form( + unk1: String, outfit_name: String ) extends OutfitMembershipRequestAction(code = 1) - final case class Unk2( - unk2: Int, - unk3: Int, + final case class Invite( + avatar_id: Long, member_name: String, ) extends OutfitMembershipRequestAction(code = 2) - final case class AcceptOutfitInvite( - unk2: String + + final case class AcceptInvite( + member_name: String ) extends OutfitMembershipRequestAction(code = 3) - final case class RejectOutfitInvite( - unk2: String + final case class RejectInvite( + member_name: String ) extends OutfitMembershipRequestAction(code = 4) - final case class CancelOutfitInvite( - unk5: Int, - unk6: Int, - outfit_name: String + final case class CancelInvite( + avatar_id: Long, + member_name: String, ) extends OutfitMembershipRequestAction(code = 5) + final case class Kick( + avatar_id: Long, + member_name: String, + ) extends OutfitMembershipRequestAction(code = 6) + + final case class SetRank( + avatar_id: Long, // 32 + rank: Int, // 3 + member_name: String, + ) extends OutfitMembershipRequestAction(code = 7) + final case class Unknown(badCode: Int, data: BitVector) extends OutfitMembershipRequestAction(badCode) /** @@ -66,96 +75,122 @@ object OutfitMembershipRequestAction { object Codecs { private val everFailCondition = conditional(included = false, bool) - val CreateOutfitCodec: Codec[CreateOutfit] = + val CreateCodec: Codec[Create] = ( - PacketHelpers.encodedWideString :: - uint4L :: - bool :: + PacketHelpers.encodedWideStringAligned(5) :: PacketHelpers.encodedWideString - ).xmap[CreateOutfit]( + ).xmap[Create]( { - case unk2 :: unk3 :: unk4 :: outfit_name :: HNil => - CreateOutfit(unk2, unk3, unk4, outfit_name) + case u1 :: outfit_name :: HNil => + Create(u1, outfit_name) }, { - case CreateOutfit(unk2, unk3, unk4, outfit_name) => - unk2 :: unk3 :: unk4 :: outfit_name :: HNil + case Create(u1, outfit_name) => + u1 :: outfit_name :: HNil } ) - val FormOutfitCodec: Codec[FormOutfit] = + val FormCodec: Codec[Form] = ( - PacketHelpers.encodedWideString :: - uint4L :: - bool :: + PacketHelpers.encodedWideStringAligned(5) :: PacketHelpers.encodedWideString - ).xmap[FormOutfit]( + ).xmap[Form]( { - case unk2 :: unk3 :: unk4 :: outfit_name :: HNil => - FormOutfit(unk2, unk3, unk4, outfit_name) + case u1 :: outfit_name :: HNil => + Form(u1, outfit_name) }, { - case FormOutfit(unk2, unk3, unk4, outfit_name) => - unk2 :: unk3 :: unk4 :: outfit_name :: HNil + case Form(u1, outfit_name) => + u1 :: outfit_name :: HNil } ) - val Unk2Codec: Codec[Unk2] = + val InviteCodec: Codec[Invite] = ( - uint16L :: - uint16L :: + uint32L :: PacketHelpers.encodedWideStringAligned(5) - ).xmap[Unk2]( + ).xmap[Invite]( { - case unk2 :: unk3 :: member_name :: HNil => - Unk2(unk2, unk3, member_name) + case u1 :: member_name :: HNil => + Invite(u1, member_name) }, { - case Unk2(unk2, unk3, member_name) => - unk2 :: unk3 :: member_name :: HNil + case Invite(u1, member_name) => + u1 :: member_name :: HNil } ) - val AcceptOutfitCodec: Codec[AcceptOutfitInvite] = - PacketHelpers.encodedWideString.xmap[AcceptOutfitInvite]( + val AcceptInviteCodec: Codec[AcceptInvite] = + PacketHelpers.encodedWideString.xmap[AcceptInvite]( { - case unk2 => - AcceptOutfitInvite(unk2) + case u1 => + AcceptInvite(u1) }, { - case AcceptOutfitInvite(unk2) => - unk2 + case AcceptInvite(u1) => + u1 } ) - val RejectOutfitCodec: Codec[RejectOutfitInvite] = - PacketHelpers.encodedWideString.xmap[RejectOutfitInvite]( + val RejectInviteCodec: Codec[RejectInvite] = + PacketHelpers.encodedWideString.xmap[RejectInvite]( { - case unk2 => - RejectOutfitInvite(unk2) + case u1 => + RejectInvite(u1) }, { - case RejectOutfitInvite(unk2) => - unk2 + case RejectInvite(u1) => + u1 } ) - val CancelOutfitCodec: Codec[CancelOutfitInvite] = + val CancelInviteCodec: Codec[CancelInvite] = ( - uint16L :: - uint16L :: + uint32L :: PacketHelpers.encodedWideStringAligned(5) - ).xmap[CancelOutfitInvite]( + ).xmap[CancelInvite]( { - case unk5 :: unk6 :: outfit_name :: HNil => - CancelOutfitInvite(unk5, unk6, outfit_name) + case u1 :: outfit_name :: HNil => + CancelInvite(u1, outfit_name) }, { - case CancelOutfitInvite(unk5, unk6, outfit_name) => - unk5 :: unk6 :: outfit_name :: HNil + case CancelInvite(u1, outfit_name) => + u1 :: outfit_name :: HNil } ) + val KickCodec: Codec[Kick] = + ( + uint32L :: + PacketHelpers.encodedWideStringAligned(5) + ).xmap[Kick]( + { + case u1 :: member_name :: HNil => + Kick(u1, member_name) + }, + { + case Kick(u1, member_name) => + u1 :: member_name :: HNil + } + ) + + val SetRankCodec: Codec[SetRank] = + ( + uint32L :: + uintL(3) :: + PacketHelpers.encodedWideStringAligned(2) + ).xmap[SetRank]( + { + case u1 :: rank :: member_name :: HNil => + SetRank(u1, rank, member_name) + }, + { + case SetRank(u1, rank, member_name) => + u1 :: rank :: member_name :: HNil + } + ) + + /** * A common form for known action code indexes with an unknown purpose and transformation is an "Unknown" object. * @@ -191,12 +226,12 @@ object OutfitMembershipRequest extends Marshallable[OutfitMembershipRequest] { val Create: RequestType.Value = Value(0) val Form: RequestType.Value = Value(1) - val Unk2: RequestType.Value = Value(2) + val Invite: 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 Kick: RequestType.Value = Value(6) + val SetRank: RequestType.Value = Value(7) implicit val codec: Codec[Type] = PacketHelpers.createEnumerationCodec(this, uintL(3)) } @@ -206,15 +241,15 @@ object OutfitMembershipRequest extends Marshallable[OutfitMembershipRequest] { import scala.annotation.switch ((code: @switch) match { - case 0 => CreateOutfitCodec - case 1 => FormOutfitCodec // so far same as Create - case 2 => Unk2Codec - 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 0 => CreateCodec + case 1 => FormCodec // so far same as Create + case 2 => InviteCodec + case 3 => AcceptInviteCodec + case 4 => RejectInviteCodec + case 5 => CancelInviteCodec + case 6 => KickCodec + case 7 => SetRankCodec + case _ => failureCodec(code) }).asInstanceOf[Codec[OutfitMembershipRequestAction]] } diff --git a/src/main/scala/net/psforever/packet/game/OutfitMembershipResponse.scala b/src/main/scala/net/psforever/packet/game/OutfitMembershipResponse.scala index 086d42ed..5b2aec24 100644 --- a/src/main/scala/net/psforever/packet/game/OutfitMembershipResponse.scala +++ b/src/main/scala/net/psforever/packet/game/OutfitMembershipResponse.scala @@ -3,7 +3,6 @@ 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._ @@ -12,10 +11,9 @@ import shapeless.{::, HNil} final case class OutfitMembershipResponse( response_type: OutfitMembershipResponse.ResponseType.Type, unk0: Int, + unk1: Int, outfit_id: Long, - target_guid: PlanetSideGUID, - unk3: Int, - //unk4: Boolean, + target_id: Long, action: OutfitMembershipResponseAction ) extends PlanetSideGamePacket { type Packet = OutfitMembershipResponse @@ -28,21 +26,39 @@ final case class OutfitMembershipResponse( 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 Universal( + str1: String, + str2: String, + flag: Boolean + ) extends OutfitMembershipResponseAction(-1) - final case class Unk1OutfitResponse(player_name: String, outfit_name: String, unk7: Int) extends OutfitMembershipResponseAction(code = 1) + final case class CreateResponse( + str1: String, + str2: String, + str3: String + ) extends OutfitMembershipResponseAction(code = 0) - final case class Unk2OutfitResponse(player_name: String, outfit_name: String, unk7: Int) extends OutfitMembershipResponseAction(code = 2) // unk7 = rank? + final case class Unk1OutfitResponse( + player_name: String, + outfit_name: String, + unk7: Int + ) extends OutfitMembershipResponseAction(code = 1) - final case class Unk3OutfitResponse(unk2: String) extends OutfitMembershipResponseAction(code = 3) + final case class Unk2OutfitResponse( + player_name: String, + outfit_name: String, + unk7: Int + ) extends OutfitMembershipResponseAction(code = 2) // unk7 = rank? - final case class Unk4OutfitResponse(unk5: Int, unk6: Int, outfit_name: String) extends OutfitMembershipResponseAction(code = 4) + final case class Unk3OutfitResponse( + unk2: String + ) extends OutfitMembershipResponseAction(code = 3) - 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 Unk4OutfitResponse( + unk5: Int, + unk6: Int, + outfit_name: String + ) extends OutfitMembershipResponseAction(code = 4) final case class Unknown(badCode: Int, data: BitVector) extends OutfitMembershipResponseAction(badCode) @@ -53,17 +69,32 @@ object OutfitMembershipResponseAction { object Codecs { private val everFailCondition = conditional(included = false, bool) - val Unk0OutfitCodec: Codec[CreateOutfitResponse] = ( + val UniversalResponseCodec: Codec[OutfitMembershipResponseAction] = ( + PacketHelpers.encodedWideStringAligned(5) :: + PacketHelpers.encodedWideString :: + ("flag" | bool) + ).xmap[OutfitMembershipResponseAction]( + { + case str1 :: str2 :: flag :: HNil => + Universal(str1, str2, flag) + }, + { + case Universal(str1, str2, flag) => + str1 :: str2 :: flag :: HNil + } + ) + + val CreateOutfitCodec: Codec[CreateResponse] = ( PacketHelpers.encodedWideStringAligned(5) :: PacketHelpers.encodedWideString :: PacketHelpers.encodedWideString - ).xmap[CreateOutfitResponse]( + ).xmap[CreateResponse]( { case str1 :: str2 :: str3 :: HNil => - CreateOutfitResponse(str1, str2, str3) + CreateResponse(str1, str2, str3) }, { - case CreateOutfitResponse(str1, str2, str3) => + case CreateResponse(str1, str2, str3) => str1 :: str2 :: str3 :: HNil } ) @@ -99,7 +130,7 @@ object OutfitMembershipResponseAction { ) val Unk3OutfitCodec: Codec[Unk3OutfitResponse] = - PacketHelpers.encodedWideString.xmap[Unk3OutfitResponse]( + PacketHelpers.encodedWideStringAligned(5).xmap[Unk3OutfitResponse]( { case unk2 => Unk3OutfitResponse(unk2) @@ -110,8 +141,11 @@ object OutfitMembershipResponseAction { } ) - val Unk4OutfitCodec: Codec[Unk4OutfitResponse] = - (uint16L :: uint16L :: PacketHelpers.encodedWideStringAligned(5)).xmap[Unk4OutfitResponse]( + val Unk4OutfitCodec: Codec[Unk4OutfitResponse] = ( + uint16L :: + uint16L :: + PacketHelpers.encodedWideStringAligned(5) + ).xmap[Unk4OutfitResponse]( { case unk5 :: unk6 :: outfit_name :: HNil => Unk4OutfitResponse(unk5, unk6, outfit_name) @@ -122,42 +156,6 @@ object OutfitMembershipResponseAction { } ) -// 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 @@ -206,14 +204,24 @@ object OutfitMembershipResponse extends Marshallable[OutfitMembershipResponse] { 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) + case 0 => UniversalResponseCodec + case 1 => UniversalResponseCodec + case 2 => UniversalResponseCodec + case 3 => UniversalResponseCodec + case 4 => UniversalResponseCodec + case 5 => UniversalResponseCodec + case 6 => UniversalResponseCodec + case 7 => UniversalResponseCodec + +// case 0 => CreateOutfitCodec // 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]] @@ -221,21 +229,20 @@ object OutfitMembershipResponse extends Marshallable[OutfitMembershipResponse] { implicit val codec: Codec[OutfitMembershipResponse] = ( ("response_type" | ResponseType.codec) >>:~ { response_type => - ("unk0" | uint8L) :: + ("unk0" | uintL(5)) :: + ("unk1" | uintL(3)) :: ("outfit_id" | uint32L) :: - ("target_guid" | PlanetSideGUID.codec) :: - ("unk3" | uint16L) :: - //("unk4" | bool) :: - ("action" | selectFromType(response_type.id)) + ("target_id" | uint32L) :: + ("action" | selectFromType(response_type.id)) } ).xmap[OutfitMembershipResponse]( { - case response_type :: u0 :: outfit_id :: target_guid :: u3 :: action :: HNil => - OutfitMembershipResponse(response_type, u0, outfit_id, target_guid, u3, action) + case response_type :: u0 :: u1 :: outfit_id :: target_id :: action :: HNil => + OutfitMembershipResponse(response_type, u0, u1, outfit_id, target_id, action) }, { - case OutfitMembershipResponse(response_type, u0, outfit_id, u2, u3, action) => - response_type :: u0 :: outfit_id :: u2 :: u3 :: action :: HNil + case OutfitMembershipResponse(response_type, u0, u1, outfit_id, target_id, action) => + response_type :: u0 :: u1 :: outfit_id :: target_id :: action :: HNil } ) } diff --git a/src/test/scala/game/OutfitEventTest.scala b/src/test/scala/game/OutfitEventTest.scala index 8837b93c..e4a86cec 100644 --- a/src/test/scala/game/OutfitEventTest.scala +++ b/src/test/scala/game/OutfitEventTest.scala @@ -5,7 +5,6 @@ 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._ @@ -64,7 +63,9 @@ class OutfitEventTest extends Specification { 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), + 15, + 128, + 0, 0, 0, 0, @@ -93,7 +94,9 @@ class OutfitEventTest extends Specification { 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), + 15, + 128, + 0, 0, 0, 0, @@ -151,8 +154,10 @@ class OutfitEventTest extends Specification { unk9 = 0, OutfitRankNames("","","","","","","",""), "", - PlanetSideGUID(28672), - 33353, + 0, + 112, + 73, + 130, 0, 0, 0, @@ -179,15 +184,17 @@ class OutfitEventTest extends Specification { unk9 = 0, OutfitRankNames("","","","","","","",""), "", - PlanetSideGUID(28672), - 33353, - 0, + 0, + 112, + 73, + 130, 0, 0, 0, 0, 0, 0, + 0 ) ) ) diff --git a/src/test/scala/game/OutfitListEventTest.scala b/src/test/scala/game/OutfitListEventTest.scala index e7731a2d..c5713a55 100644 --- a/src/test/scala/game/OutfitListEventTest.scala +++ b/src/test/scala/game/OutfitListEventTest.scala @@ -9,18 +9,18 @@ import org.specs2.mutable._ import scodec.bits.ByteVector class OutfitListEventTest extends Specification { - val unk0_ABC: ByteVector = ByteVector.fromValidHex("98 5 e83a0000 000e1800 0800000 11404e0069006700680074004c006f00720064007300 854e005900430061007400") - val unk0_DEF: ByteVector = ByteVector.fromValidHex("98 4 ec281001 51a62800 3400000 11a0490052004f004e004600490053005400200043006c0061006e00 8654006f006c006a00") - val unk1_ABC: ByteVector = ByteVector.fromValidHex("98 4 723c0000 2aa81e00 2200000 11006900470061006d00650073002d004500 906900670061006d006500730043005400460057006800610063006b002d004500") - val unk2_ABC: ByteVector = ByteVector.fromValidHex("98 4 9a3c0001 16da4e00 0400000 11a042006c006f006f00640020006f0066002000560061006e007500 864b00610072006e002d004500") - val unk3_ABC: ByteVector = ByteVector.fromValidHex("98 4 9c3c0000 df587c00 1400000 11a054006800650020004e00650076006500720068006f006f006400 8e6f00460058006f00530074006f006e0065004d0061006e002d004700") - val unk4_ABC: ByteVector = ByteVector.fromValidHex("98 4 c03c0000 24060400 0600000 1220540068006500200042006c00610063006b0020004b006e0069006700680074007300 874400720061007a00760065006e00") - val unk5_ABC: ByteVector = ByteVector.fromValidHex("98 5 383c0001 4b709a00 0c00000 10a03e005400760053003c00 89430061007000650062006f00610074007300") - val unk6_ABC: ByteVector = ByteVector.fromValidHex("98 5 b03c0000 35d67000 0400000 11404c006f0073007400200043006100750073006500 895a00650072006f004b00650077006c006c00") - val unk7_ABC: ByteVector = ByteVector.fromValidHex("98 4 043e0001 9fb82616 1400000 11e0540068006500200042006c00610063006b00200054006f00770065007200 874b00720075007000680065007800") + val unk2_0_ABC: ByteVector = ByteVector.fromValidHex("98 5 e83a0000 000e1800 0800000 11404e0069006700680074004c006f00720064007300 854e005900430061007400") + val unk2_0_DEF: ByteVector = ByteVector.fromValidHex("98 4 ec281001 51a62800 3400000 11a0490052004f004e004600490053005400200043006c0061006e00 8654006f006c006a00") + val unk2_1_ABC: ByteVector = ByteVector.fromValidHex("98 4 723c0000 2aa81e00 2200000 11006900470061006d00650073002d004500 906900670061006d006500730043005400460057006800610063006b002d004500") + val unk2_2_ABC: ByteVector = ByteVector.fromValidHex("98 4 9a3c0001 16da4e00 0400000 11a042006c006f006f00640020006f0066002000560061006e007500 864b00610072006e002d004500") + val unk2_3_ABC: ByteVector = ByteVector.fromValidHex("98 4 9c3c0000 df587c00 1400000 11a054006800650020004e00650076006500720068006f006f006400 8e6f00460058006f00530074006f006e0065004d0061006e002d004700") + val unk2_4_ABC: ByteVector = ByteVector.fromValidHex("98 4 c03c0000 24060400 0600000 1220540068006500200042006c00610063006b0020004b006e0069006700680074007300 874400720061007a00760065006e00") + val unk2_5_ABC: ByteVector = ByteVector.fromValidHex("98 5 383c0001 4b709a00 0c00000 10a03e005400760053003c00 89430061007000650062006f00610074007300") + val unk2_6_ABC: ByteVector = ByteVector.fromValidHex("98 5 b03c0000 35d67000 0400000 11404c006f0073007400200043006100750073006500 895a00650072006f004b00650077006c006c00") + val unk2_7_ABC: ByteVector = ByteVector.fromValidHex("98 4 043e0001 9fb82616 1400000 11e0540068006500200042006c00610063006b00200054006f00770065007200 874b00720075007000680065007800") "decode unk0_ABC" in { - PacketCoding.decodePacket(unk0_ABC).require match { + PacketCoding.decodePacket(unk2_0_ABC).require match { case OutfitListEvent(code, ListElementOutfit(unk1, points, members, outfit_name, outfit_leader)) => code mustEqual OutfitListEvent.RequestType.ListElementOutfit unk1 mustEqual 7668 @@ -37,7 +37,7 @@ class OutfitListEventTest extends Specification { val msg = OutfitListEvent(RequestType.ListElementOutfit, ListElementOutfit(7668, 788224, 4, "NightLords", "NYCat")) val pkt = PacketCoding.encodePacket(msg).require.toByteVector - pkt mustEqual unk0_ABC + pkt mustEqual unk2_0_ABC } } diff --git a/src/test/scala/game/OutfitMemberEventTest.scala b/src/test/scala/game/OutfitMemberEventTest.scala index 0317b834..5dbcef00 100644 --- a/src/test/scala/game/OutfitMemberEventTest.scala +++ b/src/test/scala/game/OutfitMemberEventTest.scala @@ -10,42 +10,29 @@ 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 unk0_ABC_Lazer = hex"90 0 4864 0001 030c 2802 24 0 4c0061007a00650072003100390038003200 f43a 45e0 0b4c 6040 10" - 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" + 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)) - - - */ + val Unk0 = hex"90 5 40542002 3f61e808 0" "decode Unk0 ABC" in { PacketCoding.decodePacket(unk0_ABC_Lazer).require match { - case OutfitMemberEvent(unk00, outfit_guid, unk3, unk5, member_name, unk8, unk9, unk10, unk11, unk12, unk13,unk14,unk15,unk16) => - unk00 mustEqual 0 - outfit_guid mustEqual 6418L - unk3 mustEqual 49984 - unk5 mustEqual 10 + case OutfitMemberEvent(action, outfit_id, member_id, member_name, rank, points, last_login, action2, padding) => + action mustEqual 0 + outfit_id mustEqual 6418L + member_id mustEqual 705344 member_name mustEqual "Lazer1982" - unk8 mustEqual 244 - unk9 mustEqual 58 - unk10 mustEqual 69 - unk11 mustEqual 224 - unk12 mustEqual 11 - unk13 mustEqual 76 - unk14 mustEqual 96 - unk15 mustEqual 64 - unk16 mustEqual 16 + rank mustEqual 7 + points mustEqual 3134113 + last_login mustEqual 156506 + action2 mustEqual 1 + padding mustEqual ByteVector(0x0) case _ => ko } @@ -53,20 +40,15 @@ val unk0_ABC_Lazer: ByteVector = hex"90 048640001030c28022404c0061007a00650 "encode Unk0 ABC" in { val msg = OutfitMemberEvent( - unk00 = 0, + action = 0, outfit_id = 6418L, - unk3 = 49984, - unk5 = 10, + member_id = 705344, member_name = "Lazer1982", - unk8 = 244, - unk9 = 58, - unk10 = 69, - unk11 = 224, - unk12 = 11, - unk13 = 76, - unk14 = 96, - unk15 = 64, - unk16 = 16, + rank = 7, + points = 3134113, + last_login = 156506, + action2 = 1, + ByteVector.empty ) val pkt = PacketCoding.encodePacket(msg).require.toByteVector diff --git a/src/test/scala/game/OutfitMembershipRequestTest.scala b/src/test/scala/game/OutfitMembershipRequestTest.scala index bef384a5..0e8af6e4 100644 --- a/src/test/scala/game/OutfitMembershipRequestTest.scala +++ b/src/test/scala/game/OutfitMembershipRequestTest.scala @@ -13,7 +13,7 @@ class OutfitMembershipRequestTest extends Specification { val create_2222 = hex"8c 0 1000 000 1000 84 3200320032003200" val form_abc = hex"8c 2 0200 000 1000 83 610062006300" val form_1 = hex"8c 2 1000 000 1000 81 3100" - val unk2 = hex"8c 5 bb399e0 2000 0000 1140 7600690072007500730067006900760065007200" // -- virusgiver + val invite_old = hex"8c 5 bb399e0 2000 0000 1140 7600690072007500730067006900760065007200" // -- virusgiver val unk3 = hex"8c 5 bb399e0 2000 0000 1080 750072006f006200" // -- "urob" -- could be false positive -- seems to gets an OMSResp -> 0x8d271bb399e025af8f405080550072006f0062008080 val accept_1 = hex"8c 6 0200 000 1000" val accept_2 = hex"8c 6 0400 000 1000" @@ -23,19 +23,24 @@ class OutfitMembershipRequestTest extends Specification { 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 + // dumped from half implemented outfit + val invite = hex"8c4020000000000000116069006e00760069007400650054006500730074003100" + val kick = hex"8cc020000017ac8f405000" + val setrank = hex"8ce020000017ac8f404600" // setting rank from 0 to 1 + "decode CreateOutfit ABC" in { PacketCoding.decodePacket(create_ABC).require match { - case OutfitMembershipRequest(request_type, avatar_id, action) => + case OutfitMembershipRequest(request_type, outfit_id, action) => request_type mustEqual RequestType.Create - avatar_id mustEqual 1 - action mustEqual CreateOutfit("", 0, unk4 = false, "ABC") + outfit_id mustEqual 1 + action mustEqual Create("", "ABC") case _ => ko } } "encode CreateOutfit ABC" in { - val msg = OutfitMembershipRequest(RequestType.Create, 1, CreateOutfit("", 0, unk4 = false, "ABC")) + val msg = OutfitMembershipRequest(RequestType.Create, 1, Create("", "ABC")) val pkt = PacketCoding.encodePacket(msg).require.toByteVector pkt mustEqual create_ABC @@ -43,17 +48,17 @@ class OutfitMembershipRequestTest extends Specification { "decode CreateOutfit 2222" in { PacketCoding.decodePacket(create_2222).require match { - case OutfitMembershipRequest(request_type, avatar_id, action) => + case OutfitMembershipRequest(request_type, outfit_id, action) => request_type mustEqual RequestType.Create - avatar_id mustEqual 8 - action mustEqual CreateOutfit("", 0, unk4 = false, "2222") + outfit_id mustEqual 8 + action mustEqual Create("", "2222") case _ => ko } } "encode CreateOutfit 2222" in { - val msg = OutfitMembershipRequest(RequestType.Create, 8, CreateOutfit("", 0, unk4 = false, "2222")) + val msg = OutfitMembershipRequest(RequestType.Create, 8, Create("", "2222")) val pkt = PacketCoding.encodePacket(msg).require.toByteVector pkt mustEqual create_2222 @@ -61,17 +66,17 @@ class OutfitMembershipRequestTest extends Specification { "decode FormOutfit abc" in { PacketCoding.decodePacket(form_abc).require match { - case OutfitMembershipRequest(request_type, avatar_id, action) => + case OutfitMembershipRequest(request_type, outfit_id, action) => request_type mustEqual RequestType.Form - avatar_id mustEqual 1 - action mustEqual FormOutfit("", 0, unk4 = false, "abc") + outfit_id mustEqual 1 + action mustEqual Form("", "abc") case _ => ko } } "encode FormOutfit abc" in { - val msg = OutfitMembershipRequest(RequestType.Form, 1, FormOutfit("", 0, unk4 = false, "abc")) + val msg = OutfitMembershipRequest(RequestType.Form, 1, Form("", "abc")) val pkt = PacketCoding.encodePacket(msg).require.toByteVector pkt mustEqual form_abc @@ -79,53 +84,53 @@ class OutfitMembershipRequestTest extends Specification { "decode FormOutfit 1" in { PacketCoding.decodePacket(form_1).require match { - case OutfitMembershipRequest(request_type, avatar_id, action) => + case OutfitMembershipRequest(request_type, outfit_id, action) => request_type mustEqual RequestType.Form - avatar_id mustEqual 8 - action mustEqual FormOutfit("", 0, unk4 = false, "1") + outfit_id mustEqual 8 + action mustEqual Form("", "1") case _ => ko } } "encode FormOutfit 1" in { - val msg = OutfitMembershipRequest(RequestType.Form, 8, FormOutfit("", 0, unk4 = false, "1")) + val msg = OutfitMembershipRequest(RequestType.Form, 8, Form("", "1")) val pkt = PacketCoding.encodePacket(msg).require.toByteVector pkt mustEqual form_1 } - "decode Unk2" in { - PacketCoding.decodePacket(unk2).require match { + "decode Invite" in { + PacketCoding.decodePacket(invite_old).require match { case OutfitMembershipRequest(request_type, outfit_id, action) => - request_type mustEqual RequestType.Unk2 + request_type mustEqual RequestType.Invite outfit_id mustEqual 30383325L - action mustEqual Unk2(0, 0, "virusgiver") + action mustEqual Invite(0, "virusgiver") case _ => ko } } - "encode Unk2" in { - val msg = OutfitMembershipRequest(RequestType.Unk2, 30383325L, Unk2(0, 0, "virusgiver")) + "encode Invite" in { + val msg = OutfitMembershipRequest(RequestType.Invite, 30383325L, Invite(0, "virusgiver")) val pkt = PacketCoding.encodePacket(msg).require.toByteVector - pkt mustEqual unk2 + pkt mustEqual invite_old } "decode AcceptOutfitInvite 1" in { PacketCoding.decodePacket(accept_1).require match { - case OutfitMembershipRequest(request_type, avatar_id, action) => + case OutfitMembershipRequest(request_type, outfit_id, action) => request_type mustEqual RequestType.Accept - avatar_id mustEqual 1 - action mustEqual AcceptOutfitInvite("") + outfit_id mustEqual 1 + action mustEqual AcceptInvite("") case _ => ko } } "encode AcceptOutfitInvite 1" in { - val msg = OutfitMembershipRequest(RequestType.Accept, 1, AcceptOutfitInvite("")) + val msg = OutfitMembershipRequest(RequestType.Accept, 1, AcceptInvite("")) val pkt = PacketCoding.encodePacket(msg).require.toByteVector pkt mustEqual accept_1 @@ -133,17 +138,17 @@ class OutfitMembershipRequestTest extends Specification { "decode AcceptOutfitInvite 2" in { PacketCoding.decodePacket(accept_2).require match { - case OutfitMembershipRequest(request_type, avatar_id, action) => + case OutfitMembershipRequest(request_type, outfit_id, action) => request_type mustEqual RequestType.Accept - avatar_id mustEqual 2 - action mustEqual AcceptOutfitInvite("") + outfit_id mustEqual 2 + action mustEqual AcceptInvite("") case _ => ko } } "encode AcceptOutfitInvite 2" in { - val msg = OutfitMembershipRequest(RequestType.Accept, 2, AcceptOutfitInvite("")) + val msg = OutfitMembershipRequest(RequestType.Accept, 2, AcceptInvite("")) val pkt = PacketCoding.encodePacket(msg).require.toByteVector pkt mustEqual accept_2 @@ -151,17 +156,17 @@ class OutfitMembershipRequestTest extends Specification { "decode RejectOutfitInvite 1" in { PacketCoding.decodePacket(reject_1).require match { - case OutfitMembershipRequest(request_type, avatar_id, action) => + case OutfitMembershipRequest(request_type, outfit_id, action) => request_type mustEqual RequestType.Reject - avatar_id mustEqual 1 - action mustEqual RejectOutfitInvite("") + outfit_id mustEqual 1 + action mustEqual RejectInvite("") case _ => ko } } "encode RejectOutfitInvite 1" in { - val msg = OutfitMembershipRequest(RequestType.Reject, 1, RejectOutfitInvite("")) + val msg = OutfitMembershipRequest(RequestType.Reject, 1, RejectInvite("")) val pkt = PacketCoding.encodePacket(msg).require.toByteVector pkt mustEqual reject_1 @@ -169,17 +174,17 @@ class OutfitMembershipRequestTest extends Specification { "decode RejectOutfitInvite 2" in { PacketCoding.decodePacket(reject_2).require match { - case OutfitMembershipRequest(request_type, avatar_id, action) => + case OutfitMembershipRequest(request_type, outfit_id, action) => request_type mustEqual RequestType.Reject - avatar_id mustEqual 2 - action mustEqual RejectOutfitInvite("") + outfit_id mustEqual 2 + action mustEqual RejectInvite("") case _ => ko } } "encode RejectOutfitInvite 2" in { - val msg = OutfitMembershipRequest(RequestType.Reject, 2, RejectOutfitInvite("")) + val msg = OutfitMembershipRequest(RequestType.Reject, 2, RejectInvite("")) val pkt = PacketCoding.encodePacket(msg).require.toByteVector pkt mustEqual reject_2 @@ -187,17 +192,17 @@ class OutfitMembershipRequestTest extends Specification { "decode CancelOutfitInvite 3" in { PacketCoding.decodePacket(cancel_3).require match { - case OutfitMembershipRequest(request_type, avatar_id, action) => + case OutfitMembershipRequest(request_type, outfit_id, action) => request_type mustEqual RequestType.Cancel - avatar_id mustEqual 3 - action mustEqual CancelOutfitInvite(0, 0, "") + outfit_id mustEqual 3 + action mustEqual CancelInvite(0, "") case _ => ko } } "encode CancelOutfitInvite 3" in { - val msg = OutfitMembershipRequest(RequestType.Cancel, 3, CancelOutfitInvite(0, 0, "")) + val msg = OutfitMembershipRequest(RequestType.Cancel, 3, CancelInvite(0, "")) val pkt = PacketCoding.encodePacket(msg).require.toByteVector pkt mustEqual cancel_3 @@ -205,17 +210,17 @@ class OutfitMembershipRequestTest extends Specification { "decode CancelOutfitInvite 1 abc" in { PacketCoding.decodePacket(cancel_1_abc).require match { - case OutfitMembershipRequest(request_type, avatar_id, action) => + case OutfitMembershipRequest(request_type, outfit_id, action) => request_type mustEqual RequestType.Cancel - avatar_id mustEqual 1 - action mustEqual CancelOutfitInvite(0, 0, "abc") + outfit_id mustEqual 1 + action mustEqual CancelInvite(0, "abc") case _ => ko } } "encode CancelOutfitInvite 1 abc" in { - val msg = OutfitMembershipRequest(RequestType.Cancel, 1, CancelOutfitInvite(0, 0, "abc")) + val msg = OutfitMembershipRequest(RequestType.Cancel, 1, CancelInvite(0, "abc")) val pkt = PacketCoding.encodePacket(msg).require.toByteVector pkt mustEqual cancel_1_abc @@ -223,19 +228,81 @@ class OutfitMembershipRequestTest extends Specification { "decode CancelOutfitInvite 3 def" in { PacketCoding.decodePacket(cancel_3_def).require match { - case OutfitMembershipRequest(request_type, avatar_id, action) => + case OutfitMembershipRequest(request_type, outfit_id, action) => request_type mustEqual RequestType.Cancel - avatar_id mustEqual 3 - action mustEqual CancelOutfitInvite(0, 0, "def") + outfit_id mustEqual 3 + action mustEqual CancelInvite(0, "def") case _ => ko } } "encode CancelOutfitInvite 3 def" in { - val msg = OutfitMembershipRequest(RequestType.Cancel, 3, CancelOutfitInvite(0, 0, "def")) + val msg = OutfitMembershipRequest(RequestType.Cancel, 3, CancelInvite(0, "def")) val pkt = PacketCoding.encodePacket(msg).require.toByteVector pkt mustEqual cancel_3_def } + + // + + "decode invite" in { + PacketCoding.decodePacket(invite).require match { + case OutfitMembershipRequest(request_type, outfit_id, Invite(unk1, member_name)) => + request_type mustEqual RequestType.Invite + outfit_id mustEqual 1 + unk1 mustEqual 0 + member_name mustEqual "inviteTest1" + case _ => + ko + } + } + + "encode invite" in { + val msg = OutfitMembershipRequest(RequestType.Invite, 1, Invite(0, "inviteTest1")) + val pkt = PacketCoding.encodePacket(msg).require.toByteVector + + pkt mustEqual invite + } + + + "decode kick" in { + PacketCoding.decodePacket(kick).require match { + case OutfitMembershipRequest(request_type, outfit_id, Kick(avatar_id, member_name)) => + request_type mustEqual RequestType.Kick + outfit_id mustEqual 1 + avatar_id mustEqual 41575613 + member_name mustEqual "" + case _ => + ko + } + } + + "encode kick" in { + val msg = OutfitMembershipRequest(RequestType.Kick, 1, Kick(41575613, "")) + val pkt = PacketCoding.encodePacket(msg).require.toByteVector + + pkt mustEqual kick + } + + "decode setrank" in { + PacketCoding.decodePacket(setrank).require match { + case OutfitMembershipRequest(request_type, outfit_id, SetRank(avatar_id, rank, member_name)) => + request_type mustEqual RequestType.SetRank + outfit_id mustEqual 1 + avatar_id mustEqual 41575613 + rank mustEqual 1 + member_name mustEqual "" + case _ => + ko + } + } + + "encode setrank" in { + val msg = OutfitMembershipRequest(RequestType.SetRank, 1, SetRank(41575613, 1, "")) + val pkt = PacketCoding.encodePacket(msg).require.toByteVector + + pkt mustEqual setrank + } + } From f3eed484af89eee5dc70581f49477f067ec34dac Mon Sep 17 00:00:00 2001 From: Resaec Date: Thu, 21 Aug 2025 23:04:29 +0200 Subject: [PATCH 11/22] OutfitMembershipResponse rework, tests added --- .../game/OutfitMembershipResponse.scala | 228 ++---------------- .../game/OutfitMembershipResponseTest.scala | 156 ++++++++++++ 2 files changed, 178 insertions(+), 206 deletions(-) create mode 100644 src/test/scala/game/OutfitMembershipResponseTest.scala diff --git a/src/main/scala/net/psforever/packet/game/OutfitMembershipResponse.scala b/src/main/scala/net/psforever/packet/game/OutfitMembershipResponse.scala index 5b2aec24..da9c27b5 100644 --- a/src/main/scala/net/psforever/packet/game/OutfitMembershipResponse.scala +++ b/src/main/scala/net/psforever/packet/game/OutfitMembershipResponse.scala @@ -3,18 +3,20 @@ 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.{Attempt, Codec} import scodec.bits.BitVector import scodec.codecs._ import shapeless.{::, HNil} final case class OutfitMembershipResponse( - response_type: OutfitMembershipResponse.ResponseType.Type, + packet_type: OutfitMembershipResponse.PacketType.Type, unk0: Int, unk1: Int, outfit_id: Long, target_id: Long, - action: OutfitMembershipResponseAction + str1: String, + str2: String, + flag: Boolean ) extends PlanetSideGamePacket { type Packet = OutfitMembershipResponse @@ -23,226 +25,40 @@ final case class OutfitMembershipResponse( def encode: Attempt[BitVector] = OutfitMembershipResponse.encode(this) } -abstract class OutfitMembershipResponseAction(val code: Int) -object OutfitMembershipResponseAction { - - final case class Universal( - str1: String, - str2: String, - flag: Boolean - ) extends OutfitMembershipResponseAction(-1) - - final case class CreateResponse( - 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 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 UniversalResponseCodec: Codec[OutfitMembershipResponseAction] = ( - PacketHelpers.encodedWideStringAligned(5) :: - PacketHelpers.encodedWideString :: - ("flag" | bool) - ).xmap[OutfitMembershipResponseAction]( - { - case str1 :: str2 :: flag :: HNil => - Universal(str1, str2, flag) - }, - { - case Universal(str1, str2, flag) => - str1 :: str2 :: flag :: HNil - } - ) - - val CreateOutfitCodec: Codec[CreateResponse] = ( - PacketHelpers.encodedWideStringAligned(5) :: - PacketHelpers.encodedWideString :: - PacketHelpers.encodedWideString - ).xmap[CreateResponse]( - { - case str1 :: str2 :: str3 :: HNil => - CreateResponse(str1, str2, str3) - }, - { - case CreateResponse(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.encodedWideStringAligned(5).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 - } - ) - - /** - * 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 { + object PacketType extends Enumeration { type Type = Value - val CreateResponse: ResponseType.Value = Value(0) - val Unk1: ResponseType.Value = Value(1) // Info: Player has been invited / response to OutfitMembershipRequest Unk2 for that player - 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) + val CreateResponse: PacketType.Value = Value(0) + val Unk1: PacketType.Value = Value(1) // Info: Player has been invited / response to OutfitMembershipRequest Unk2 for that player + val Unk2: PacketType.Value = Value(2) // Invited / Accepted / Added + val Unk3: PacketType.Value = Value(3) + val Unk4: PacketType.Value = Value(4) + val Unk5: PacketType.Value = Value(5) + val Unk6: PacketType.Value = Value(6) // 6 and 7 seen as failed decodes, validity unknown + val Unk7: PacketType.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 => UniversalResponseCodec - case 1 => UniversalResponseCodec - case 2 => UniversalResponseCodec - case 3 => UniversalResponseCodec - case 4 => UniversalResponseCodec - case 5 => UniversalResponseCodec - case 6 => UniversalResponseCodec - case 7 => UniversalResponseCodec - -// case 0 => CreateOutfitCodec // 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 => + ("response_type" | PacketType.codec) :: ("unk0" | uintL(5)) :: ("unk1" | uintL(3)) :: ("outfit_id" | uint32L) :: ("target_id" | uint32L) :: - ("action" | selectFromType(response_type.id)) - } + ("str1" | PacketHelpers.encodedWideStringAligned(5)) :: + ("str2" | PacketHelpers.encodedWideString) :: + ("flag" | bool) ).xmap[OutfitMembershipResponse]( { - case response_type :: u0 :: u1 :: outfit_id :: target_id :: action :: HNil => - OutfitMembershipResponse(response_type, u0, u1, outfit_id, target_id, action) + case response_type :: u0 :: u1 :: outfit_id :: target_id :: str1 :: str2 :: flag :: HNil => + OutfitMembershipResponse(response_type, u0, u1, outfit_id, target_id, str1, str2, flag) }, { - case OutfitMembershipResponse(response_type, u0, u1, outfit_id, target_id, action) => - response_type :: u0 :: u1 :: outfit_id :: target_id :: action :: HNil + case OutfitMembershipResponse(response_type, u0, u1, outfit_id, target_id, str1, str2, flag) => + response_type :: u0 :: u1 :: outfit_id :: target_id :: str1 :: str2 :: flag :: HNil } ) } diff --git a/src/test/scala/game/OutfitMembershipResponseTest.scala b/src/test/scala/game/OutfitMembershipResponseTest.scala new file mode 100644 index 00000000..7646a96e --- /dev/null +++ b/src/test/scala/game/OutfitMembershipResponseTest.scala @@ -0,0 +1,156 @@ +// Copyright (c) 2023-2025 PSForever +package game + +import net.psforever.packet._ +import net.psforever.packet.game.OutfitMembershipResponse.PacketType +import net.psforever.packet.game._ +import org.specs2.mutable._ +import scodec.bits._ + +class OutfitMembershipResponseTest extends Specification { + + val createResponse = hex"8d 0 002b54f404000000010008080" + val unk1 = hex"8d 2 01bb399e03ddb4f4050a078004e00690063006b009550006c0061006e006500740053006900640065005f0046006f00720065007600650072005f005400520000" + val unk2 = hex"8d 4 0049b0f4042b54f4051405a006500720067006c0069006e006700390032009750006c0061006e006500740053006900640065005f0046006f00720065007600650072005f00560061006e00750000" + val unk3 = hex"8d 6 00e8c2f40510d3b6030008080" + val unk4 = hex"8d 8 002b54f404000000010008080" + val unk5 = hex"8d a 022b54f4051fb0f4051c05000530046006f0075007400660069007400740065007300740031008080" + + "decode CreateResponse" in { + PacketCoding.decodePacket(createResponse).require match { + case OutfitMembershipResponse(packet_type, unk0, unk1, outfit_id, target_id, str1, str2, flag) => + packet_type mustEqual PacketType.CreateResponse + unk0 mustEqual 0 + unk1 mustEqual 0 + outfit_id mustEqual 41593365 + target_id mustEqual 0 + str1 mustEqual "" + str2 mustEqual "" + flag mustEqual true + case _ => + ko + } + } + + "encode CreateResponse" in { + val msg = OutfitMembershipResponse(PacketType.CreateResponse, 0, 0, 41593365, 0, "", "", flag = true) + val pkt = PacketCoding.encodePacket(msg).require.toByteVector + + pkt mustEqual createResponse + } + + "decode unk1" in { + PacketCoding.decodePacket(unk1).require match { + case OutfitMembershipResponse(packet_type, unk0, unk1, outfit_id, target_id, str1, str2, flag) => + packet_type mustEqual PacketType.Unk1 + unk0 mustEqual 0 + unk1 mustEqual 0 + outfit_id mustEqual 30383325 + target_id mustEqual 41605870 + str1 mustEqual "xNick" + str2 mustEqual "PlanetSide_Forever_TR" + flag mustEqual false + case _ => + ko + } + } + + "encode unk1" in { + val msg = OutfitMembershipResponse(PacketType.Unk1, 0, 0, 30383325, 41605870, "xNick", "PlanetSide_Forever_TR", flag = false) + val pkt = PacketCoding.encodePacket(msg).require.toByteVector + + pkt mustEqual unk1 + } + + "decode unk2" in { + PacketCoding.decodePacket(unk2).require match { + case OutfitMembershipResponse(packet_type, unk0, unk1, outfit_id, target_id, str1, str2, flag) => + packet_type mustEqual PacketType.Unk2 + unk0 mustEqual 0 + unk1 mustEqual 0 + outfit_id mustEqual 41605156 + target_id mustEqual 41593365 + str1 mustEqual "Zergling92" + str2 mustEqual "PlanetSide_Forever_Vanu" + flag mustEqual false + case _ => + ko + } + } + + "encode unk2" in { + val msg = OutfitMembershipResponse(PacketType.Unk2, 0, 0, 41605156, 41593365, "Zergling92", "PlanetSide_Forever_Vanu", flag = false) + val pkt = PacketCoding.encodePacket(msg).require.toByteVector + + pkt mustEqual unk2 + } + + "decode unk3" in { + PacketCoding.decodePacket(unk3).require match { + case OutfitMembershipResponse(packet_type, unk0, unk1, outfit_id, target_id, str1, str2, flag) => + packet_type mustEqual PacketType.Unk3 + unk0 mustEqual 0 + unk1 mustEqual 0 + outfit_id mustEqual 41574772 + target_id mustEqual 31156616 + str1 mustEqual "" + str2 mustEqual "" + flag mustEqual true + case _ => + ko + } + } + + "encode unk3" in { + val msg = OutfitMembershipResponse(PacketType.Unk3, 0, 0, 41574772, 31156616, "", "", flag = true) + val pkt = PacketCoding.encodePacket(msg).require.toByteVector + + pkt mustEqual unk3 + } + + "decode unk4" in { + PacketCoding.decodePacket(unk4).require match { + case OutfitMembershipResponse(packet_type, unk0, unk1, outfit_id, target_id, str1, str2, flag) => + packet_type mustEqual PacketType.Unk4 + unk0 mustEqual 0 + unk1 mustEqual 0 + outfit_id mustEqual 41593365 + target_id mustEqual 0 + str1 mustEqual "" + str2 mustEqual "" + flag mustEqual true + case _ => + ko + } + } + + "encode unk4" in { + val msg = OutfitMembershipResponse(PacketType.Unk4, 0, 0, 41593365, 0, "", "", flag = true) + val pkt = PacketCoding.encodePacket(msg).require.toByteVector + + pkt mustEqual unk4 + } + + "decode unk5" in { + PacketCoding.decodePacket(unk5).require match { + case OutfitMembershipResponse(packet_type, unk0, unk1, outfit_id, target_id, str1, str2, flag) => + packet_type mustEqual PacketType.Unk5 + unk0 mustEqual 0 + unk1 mustEqual 1 + outfit_id mustEqual 41593365 + target_id mustEqual 41605263 + str1 mustEqual "PSFoutfittest1" + str2 mustEqual "" + flag mustEqual true + case _ => + ko + } + } + + "encode unk5" in { + val msg = OutfitMembershipResponse(PacketType.Unk5, 0, 1, 41593365, 41605263, "PSFoutfittest1", "", flag = true) + val pkt = PacketCoding.encodePacket(msg).require.toByteVector + + pkt mustEqual unk5 + } +} From 7528388eb1dc15d182c5fbb5b150aed1623b8448 Mon Sep 17 00:00:00 2001 From: Resaec Date: Fri, 22 Aug 2025 01:28:27 +0200 Subject: [PATCH 12/22] OutfitMemberEvent I failed horribly implementing two type conditionals, please send help --- .../packet/game/OutfitMemberEvent.scala | 136 +++++++++++++++--- .../scala/game/OutfitMemberEventTest.scala | 117 +++++++++++---- 2 files changed, 205 insertions(+), 48 deletions(-) diff --git a/src/main/scala/net/psforever/packet/game/OutfitMemberEvent.scala b/src/main/scala/net/psforever/packet/game/OutfitMemberEvent.scala index 0a0f31f2..6f11c730 100644 --- a/src/main/scala/net/psforever/packet/game/OutfitMemberEvent.scala +++ b/src/main/scala/net/psforever/packet/game/OutfitMemberEvent.scala @@ -1,54 +1,152 @@ // 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.Codec -import scodec.bits.ByteVector +import scodec.{Attempt, Codec, Err} +import scodec.bits.BitVector import scodec.codecs._ import shapeless.{::, HNil} /* - action is unimplemented! if action == 0 only outfit_id and member_id are sent - action2 is unimplemented! if action2 == 0 unk2 will contain one additional uint32L - unk2 contains one byte of padding. may contain 4byte of unknown data depending on action2 + packet_type is unimplemented! if packet_type == 0 only outfit_id and member_id are sent + action is unimplemented! if action == 0 unk2 will contain one additional uint32L + unk0_padding contains one byte of padding. may contain 4byte of unknown data depending on action */ final case class OutfitMemberEvent( - action: Int, // action is unimplemented + packet_type: Int, // only 0 is known // TODO: needs implementation outfit_id: Long, member_id: Long, - member_name: String, + member_name: String, // from here is packet_type == 0 only rank: Int, // 0-7 points: Long, // client divides this by 100 last_login: Long, // seconds ago from current time, 0 if online - action2: Int, // this should always be 1, otherwise there will be actual data in unk2! - padding: ByteVector, // only contains information if unk1 is 0, 1 byte of padding otherwise + action: OutfitMemberEvent.PacketType.Type, // this should always be 1, otherwise there will be actual data in unk0_padding! + unk0_padding: OutfitMemberEventAction // only contains information if action is 0, 1 byte of padding otherwise ) extends PlanetSideGamePacket { type Packet = OutfitMemberEvent - def opcode = GamePacketOpcode.OutfitMemberEvent + def opcode: Type = GamePacketOpcode.OutfitMemberEvent - def encode = OutfitMemberEvent.encode(this) + def encode: Attempt[BitVector] = OutfitMemberEvent.encode(this) +} + +abstract class OutfitMemberEventAction(val code: Int) +object OutfitMemberEventAction { + + final case class Unk0( + unk0: Long + ) extends OutfitMemberEventAction(code = 0) + + final case class Padding( + padding: Int + ) extends OutfitMemberEventAction(code = 1) + + final case class Unknown(badCode: Int, data: BitVector) extends OutfitMemberEventAction(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 UnkNonPaddingCodec: Codec[Unk0] = ( + ("unk0" | uint32L) + ).xmap[Unk0]( + { + case u0 => + Unk0(u0) + }, + { + case Unk0(u0) => + u0 + } + ) + + val PaddingCodec: Codec[Padding] = ( + ("padding" | uint4L) + ).xmap[Padding]( + { + case padding => + Padding(padding) + }, + { + case Padding(padding) => + padding + } + ) + + /** + * 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[OutfitMemberEventAction] = + everFailCondition.exmap[OutfitMemberEventAction]( + _ => 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 OutfitMemberEvent extends Marshallable[OutfitMemberEvent] { + + object PacketType extends Enumeration { + type Type = Value + + val Unk0: PacketType.Value = Value(0) + val Padding: PacketType.Value = Value(1) // Info: Player has been invited / response to OutfitMembershipRequest Unk2 for that player + + implicit val codec: Codec[Type] = PacketHelpers.createEnumerationCodec(this, uintL(1)) + } + + private def selectFromType(code: Int): Codec[OutfitMemberEventAction] = { + import OutfitMemberEventAction.Codecs._ + import scala.annotation.switch + + ((code: @switch) match { + case 0 => UnkNonPaddingCodec + case 1 => PaddingCodec + + case _ => failureCodec(code) + }).asInstanceOf[Codec[OutfitMemberEventAction]] + } + implicit val codec: Codec[OutfitMemberEvent] = ( - ("action" | uintL(2)) :: + ("packet_type" | uintL(2)) :: // this should selectFromType ("outfit_id" | uint32L) :: ("member_id" | uint32L) :: - ("member_name" | PacketHelpers.encodedWideStringAligned(6)) :: + ("member_name" | PacketHelpers.encodedWideStringAligned(6)) :: // from here is packet_type == 0 only ("rank" | uint(3)) :: ("points" | uint32L) :: ("last_login" | uint32L) :: - ("action2" | uintL(1)) :: - ("padding" | bytes) + (("action" | PacketType.codec) >>:~ { action => + ("action_part" | selectFromType(action.id)).hlist + }) ).xmap[OutfitMemberEvent]( { - case unk00 :: outfit_id :: member_id :: member_name :: rank :: points :: last_login :: u1 :: padding :: HNil => - OutfitMemberEvent(unk00, outfit_id, member_id, member_name, rank, points, last_login, u1, padding) + case packet_type :: outfit_id :: member_id :: member_name :: rank :: points :: last_login :: action :: unk0_padding :: HNil => + OutfitMemberEvent(packet_type, outfit_id, member_id, member_name, rank, points, last_login, action, unk0_padding) }, { - case OutfitMemberEvent(unk00, outfit_id, member_id, member_name, rank, points, last_login, action2, padding) => - unk00 :: outfit_id :: member_id :: member_name :: rank :: points :: last_login :: action2 :: padding :: HNil + // TODO: remove once implemented + // ensure we send packet_type 0 only + case OutfitMemberEvent(_, outfit_id, member_id, member_name, rank, points, last_login, action, unk0_padding) => + 0 :: outfit_id :: member_id :: member_name :: rank :: points :: last_login :: action :: unk0_padding :: HNil } ) } diff --git a/src/test/scala/game/OutfitMemberEventTest.scala b/src/test/scala/game/OutfitMemberEventTest.scala index 5dbcef00..f51fe952 100644 --- a/src/test/scala/game/OutfitMemberEventTest.scala +++ b/src/test/scala/game/OutfitMemberEventTest.scala @@ -2,56 +2,115 @@ package game import net.psforever.packet._ - -import net.psforever.packet.game._ +import net.psforever.packet.game.OutfitMemberEvent +import net.psforever.packet.game.OutfitMemberEventAction.Padding 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 = hex"90 0 4864 0001 030c 2802 24 0 4c0061007a00650072003100390038003200 f43a 45e0 0b4c 6040 10" + val Lazer = hex"90 0 4864 0001 030c 2802 24 0 4c0061007a00650072003100390038003200 f43a 45e0 0b4c 6040 10" + val Lazer2 = hex"90 0 4864 0001 030c 2802 24 0 4c0061007a00650072003100390038003200 e6dc 25a0 153e 6040 10" + 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 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" - 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" + val Unk1 = hex"90 5 40542002 3f61e808 0" - val Unk0 = hex"90 5 40542002 3f61e808 0" - - "decode Unk0 ABC" in { - PacketCoding.decodePacket(unk0_ABC_Lazer).require match { - case OutfitMemberEvent(action, outfit_id, member_id, member_name, rank, points, last_login, action2, padding) => - action mustEqual 0 - outfit_id mustEqual 6418L - member_id mustEqual 705344 - member_name mustEqual "Lazer1982" - rank mustEqual 7 - points mustEqual 3134113 - last_login mustEqual 156506 - action2 mustEqual 1 - padding mustEqual ByteVector(0x0) + "decode Lazer padding" in { + PacketCoding.decodePacket(Lazer).require match { + case OutfitMemberEvent(packet_type, outfit_id, member_id, member_name, rank, points, last_login, action, unk0_padding) => + packet_type mustEqual 0 + outfit_id mustEqual 6418 + member_id mustEqual 705344 + member_name mustEqual "Lazer1982" + rank mustEqual 7 + points mustEqual 3134113 + last_login mustEqual 156506 + action mustEqual OutfitMemberEvent.PacketType.Padding + unk0_padding mustEqual Padding(0) case _ => ko } } - "encode Unk0 ABC" in { + "encode Lazer padding" in { val msg = OutfitMemberEvent( - action = 0, - outfit_id = 6418L, + packet_type = 0, + outfit_id = 6418, member_id = 705344, member_name = "Lazer1982", rank = 7, points = 3134113, last_login = 156506, - action2 = 1, - ByteVector.empty + action = OutfitMemberEvent.PacketType.Padding, + unk0_padding = Padding(0) ) val pkt = PacketCoding.encodePacket(msg).require.toByteVector - pkt mustEqual unk0_ABC_Lazer + pkt mustEqual Lazer } + + "decode OpolE padding" in { + PacketCoding.decodePacket(OpolE).require match { + case OutfitMemberEvent(packet_type, outfit_id, member_id, member_name, rank, points, last_login, action, unk0_padding) => + packet_type mustEqual 0 + outfit_id mustEqual 6418 + member_id mustEqual 42644970 + member_name mustEqual "OpolE" + rank mustEqual 6 + points mustEqual 461901 + last_login mustEqual 137576 + action mustEqual OutfitMemberEvent.PacketType.Padding + unk0_padding mustEqual Padding(0) + case _ => + ko + } + } + + "encode OpolE padding" in { + val msg = OutfitMemberEvent( + packet_type = 0, + outfit_id = 6418, + member_id = 42644970, + member_name = "OpolE", + rank = 6, + points = 461901, + last_login = 137576, + action = OutfitMemberEvent.PacketType.Padding, + unk0_padding = Padding(0) + ) + val pkt = PacketCoding.encodePacket(msg).require.toByteVector + + pkt mustEqual OpolE + } + + // TODO: these are broken because the decoder can only handle packet_type 0 + + /* + "decode Unk0" in { + PacketCoding.decodePacket(Unk1).require match { + case OutfitMemberEvent(packet_type, outfit_id, member_id) => + packet_type mustEqual 1 + outfit_id mustEqual 6418 + member_id mustEqual 42644970 + case _ => + ko + } + } + + "encode Unk0" in { + val msg = OutfitMemberEvent( + packet_type = 1, + outfit_id = 6418, + member_id = 42644970, + ) + val pkt = PacketCoding.encodePacket(msg).require.toByteVector + + pkt mustEqual Unk1 + } + */ } From e3fe9b69bfd41023144e4eb5f0b537702cd0b645 Mon Sep 17 00:00:00 2001 From: Resaec Date: Sat, 23 Aug 2025 21:54:07 +0200 Subject: [PATCH 13/22] OutfitMemberEvent now supports the two main packet types Unk0 and Unk1. Support for Unk0's subtypes Unk0 and Padding have been removed in favour of the main type. Should be reimplemented at some point, but I don't know how yet. --- .../packet/game/OutfitMemberEvent.scala | 97 +++++++++---------- .../scala/game/OutfitMemberEventTest.scala | 94 +++++++++--------- 2 files changed, 94 insertions(+), 97 deletions(-) diff --git a/src/main/scala/net/psforever/packet/game/OutfitMemberEvent.scala b/src/main/scala/net/psforever/packet/game/OutfitMemberEvent.scala index 6f11c730..f40498a4 100644 --- a/src/main/scala/net/psforever/packet/game/OutfitMemberEvent.scala +++ b/src/main/scala/net/psforever/packet/game/OutfitMemberEvent.scala @@ -8,21 +8,11 @@ import scodec.bits.BitVector import scodec.codecs._ import shapeless.{::, HNil} -/* - packet_type is unimplemented! if packet_type == 0 only outfit_id and member_id are sent - action is unimplemented! if action == 0 unk2 will contain one additional uint32L - unk0_padding contains one byte of padding. may contain 4byte of unknown data depending on action - */ final case class OutfitMemberEvent( - packet_type: Int, // only 0 is known // TODO: needs implementation + packet_type: OutfitMemberEvent.PacketType.Type, outfit_id: Long, member_id: Long, - member_name: String, // from here is packet_type == 0 only - rank: Int, // 0-7 - points: Long, // client divides this by 100 - last_login: Long, // seconds ago from current time, 0 if online - action: OutfitMemberEvent.PacketType.Type, // this should always be 1, otherwise there will be actual data in unk0_padding! - unk0_padding: OutfitMemberEventAction // only contains information if action is 0, 1 byte of padding otherwise + action: OutfitMemberEventAction ) extends PlanetSideGamePacket { type Packet = OutfitMemberEvent @@ -34,12 +24,30 @@ final case class OutfitMemberEvent( abstract class OutfitMemberEventAction(val code: Int) object OutfitMemberEventAction { + object PacketType extends Enumeration { + type Type = Value + + val Unk0: PacketType.Value = Value(0) + val Padding: PacketType.Value = Value(1) + + implicit val codec: Codec[Type] = PacketHelpers.createEnumerationCodec(this, uintL(1)) + + } + + /* + action is unimplemented! if action == 0 unk2 will contain one additional uint32L + unk0_padding contains one byte of padding. may contain 4byte of unknown data depending on action + */ final case class Unk0( - unk0: Long + member_name: String, + rank: Int, + points: Long, // client divides this by 100 + last_login: Long, // seconds ago from current time, 0 if online + action: PacketType.Type, // should always be 1, otherwise there will be actual data in padding. not implemented! + padding: Int // should always be 0, 4 bits of padding // only contains data if action is 0 ) extends OutfitMemberEventAction(code = 0) - final case class Padding( - padding: Int + final case class Unk1( ) extends OutfitMemberEventAction(code = 1) final case class Unknown(badCode: Int, data: BitVector) extends OutfitMemberEventAction(badCode) @@ -51,31 +59,25 @@ object OutfitMemberEventAction { object Codecs { private val everFailCondition = conditional(included = false, bool) - val UnkNonPaddingCodec: Codec[Unk0] = ( - ("unk0" | uint32L) + val Unk0Codec: Codec[Unk0] = ( + ("member_name" | PacketHelpers.encodedWideStringAligned(6)) :: // from here is packet_type == 0 only + ("rank" | uint(3)) :: + ("points" | uint32L) :: + ("last_login" | uint32L) :: + ("action" | OutfitMemberEventAction.PacketType.codec) :: + ("padding" | uint4L) ).xmap[Unk0]( { - case u0 => - Unk0(u0) + case member_name :: rank :: points :: last_login :: action :: padding :: HNil => + Unk0(member_name, rank, points, last_login, action, padding) }, { - case Unk0(u0) => - u0 + case Unk0(member_name, rank, points, last_login, action, padding) => + member_name :: rank :: points :: last_login :: action :: padding :: HNil } ) - val PaddingCodec: Codec[Padding] = ( - ("padding" | uint4L) - ).xmap[Padding]( - { - case padding => - Padding(padding) - }, - { - case Padding(padding) => - padding - } - ) + val Unk1Codec: Codec[Unk1] = PacketHelpers.emptyCodec(Unk1()) /** * A common form for known action code indexes with an unknown purpose and transformation is an "Unknown" object. @@ -109,9 +111,9 @@ object OutfitMemberEvent extends Marshallable[OutfitMemberEvent] { type Type = Value val Unk0: PacketType.Value = Value(0) - val Padding: PacketType.Value = Value(1) // Info: Player has been invited / response to OutfitMembershipRequest Unk2 for that player + val Unk1: PacketType.Value = Value(1) // Info: Player has been invited / response to OutfitMembershipRequest Unk2 for that player - implicit val codec: Codec[Type] = PacketHelpers.createEnumerationCodec(this, uintL(1)) + implicit val codec: Codec[Type] = PacketHelpers.createEnumerationCodec(this, uintL(2)) } private def selectFromType(code: Int): Codec[OutfitMemberEventAction] = { @@ -119,34 +121,27 @@ object OutfitMemberEvent extends Marshallable[OutfitMemberEvent] { import scala.annotation.switch ((code: @switch) match { - case 0 => UnkNonPaddingCodec - case 1 => PaddingCodec + case 0 => Unk0Codec + case 1 => Unk1Codec case _ => failureCodec(code) }).asInstanceOf[Codec[OutfitMemberEventAction]] } implicit val codec: Codec[OutfitMemberEvent] = ( - ("packet_type" | uintL(2)) :: // this should selectFromType + ("packet_type" | PacketType.codec) >>:~ { packet_type => ("outfit_id" | uint32L) :: ("member_id" | uint32L) :: - ("member_name" | PacketHelpers.encodedWideStringAligned(6)) :: // from here is packet_type == 0 only - ("rank" | uint(3)) :: - ("points" | uint32L) :: - ("last_login" | uint32L) :: - (("action" | PacketType.codec) >>:~ { action => - ("action_part" | selectFromType(action.id)).hlist - }) + ("action" | selectFromType(packet_type.id)).hlist + } ).xmap[OutfitMemberEvent]( { - case packet_type :: outfit_id :: member_id :: member_name :: rank :: points :: last_login :: action :: unk0_padding :: HNil => - OutfitMemberEvent(packet_type, outfit_id, member_id, member_name, rank, points, last_login, action, unk0_padding) + case packet_type :: outfit_id :: member_id:: action :: HNil => + OutfitMemberEvent(packet_type, outfit_id, member_id, action) }, { - // TODO: remove once implemented - // ensure we send packet_type 0 only - case OutfitMemberEvent(_, outfit_id, member_id, member_name, rank, points, last_login, action, unk0_padding) => - 0 :: outfit_id :: member_id :: member_name :: rank :: points :: last_login :: action :: unk0_padding :: HNil + case OutfitMemberEvent(packet_type, outfit_id, member_id, action) => + packet_type :: outfit_id :: member_id :: action :: HNil } ) } diff --git a/src/test/scala/game/OutfitMemberEventTest.scala b/src/test/scala/game/OutfitMemberEventTest.scala index f51fe952..726d0a78 100644 --- a/src/test/scala/game/OutfitMemberEventTest.scala +++ b/src/test/scala/game/OutfitMemberEventTest.scala @@ -2,8 +2,8 @@ package game import net.psforever.packet._ -import net.psforever.packet.game.OutfitMemberEvent -import net.psforever.packet.game.OutfitMemberEventAction.Padding +import net.psforever.packet.game.{OutfitMemberEvent, OutfitMemberEventAction} +import net.psforever.packet.game.OutfitMemberEventAction._ import org.specs2.mutable._ import scodec.bits._ @@ -18,20 +18,20 @@ class OutfitMemberEventTest extends Specification { 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" - val Unk1 = hex"90 5 40542002 3f61e808 0" + val unk1 = hex"90 5 40542002 3f61e808 0" "decode Lazer padding" in { PacketCoding.decodePacket(Lazer).require match { - case OutfitMemberEvent(packet_type, outfit_id, member_id, member_name, rank, points, last_login, action, unk0_padding) => - packet_type mustEqual 0 - outfit_id mustEqual 6418 - member_id mustEqual 705344 - member_name mustEqual "Lazer1982" - rank mustEqual 7 - points mustEqual 3134113 - last_login mustEqual 156506 - action mustEqual OutfitMemberEvent.PacketType.Padding - unk0_padding mustEqual Padding(0) + case OutfitMemberEvent(packet_type, outfit_id, member_id, Unk0(member_name, rank, points, last_login, action, padding)) => + packet_type mustEqual OutfitMemberEvent.PacketType.Unk0 + outfit_id mustEqual 6418 + member_id mustEqual 705344 + member_name mustEqual "Lazer1982" + rank mustEqual 7 + points mustEqual 3134113 + last_login mustEqual 156506 + action mustEqual OutfitMemberEventAction.PacketType.Padding + padding mustEqual 0 case _ => ko } @@ -39,15 +39,17 @@ class OutfitMemberEventTest extends Specification { "encode Lazer padding" in { val msg = OutfitMemberEvent( - packet_type = 0, + packet_type = OutfitMemberEvent.PacketType.Unk0, outfit_id = 6418, member_id = 705344, - member_name = "Lazer1982", - rank = 7, - points = 3134113, - last_login = 156506, - action = OutfitMemberEvent.PacketType.Padding, - unk0_padding = Padding(0) + Unk0( + member_name = "Lazer1982", + rank = 7, + points = 3134113, + last_login = 156506, + action = OutfitMemberEventAction.PacketType.Padding, + padding = 0 + ) ) val pkt = PacketCoding.encodePacket(msg).require.toByteVector @@ -56,16 +58,16 @@ class OutfitMemberEventTest extends Specification { "decode OpolE padding" in { PacketCoding.decodePacket(OpolE).require match { - case OutfitMemberEvent(packet_type, outfit_id, member_id, member_name, rank, points, last_login, action, unk0_padding) => - packet_type mustEqual 0 + case OutfitMemberEvent(packet_type, outfit_id, member_id, Unk0(member_name, rank, points, last_login, action, unk0_padding)) => + packet_type mustEqual OutfitMemberEvent.PacketType.Unk0 outfit_id mustEqual 6418 member_id mustEqual 42644970 member_name mustEqual "OpolE" rank mustEqual 6 points mustEqual 461901 last_login mustEqual 137576 - action mustEqual OutfitMemberEvent.PacketType.Padding - unk0_padding mustEqual Padding(0) + action mustEqual OutfitMemberEventAction.PacketType.Padding + unk0_padding mustEqual 0 case _ => ko } @@ -73,44 +75,44 @@ class OutfitMemberEventTest extends Specification { "encode OpolE padding" in { val msg = OutfitMemberEvent( - packet_type = 0, + packet_type = OutfitMemberEvent.PacketType.Unk0, outfit_id = 6418, member_id = 42644970, - member_name = "OpolE", - rank = 6, - points = 461901, - last_login = 137576, - action = OutfitMemberEvent.PacketType.Padding, - unk0_padding = Padding(0) + Unk0( + member_name = "OpolE", + rank = 6, + points = 461901, + last_login = 137576, + action = OutfitMemberEventAction.PacketType.Padding, + padding = 0 + ) + ) val pkt = PacketCoding.encodePacket(msg).require.toByteVector pkt mustEqual OpolE } - // TODO: these are broken because the decoder can only handle packet_type 0 - - /* - "decode Unk0" in { - PacketCoding.decodePacket(Unk1).require match { - case OutfitMemberEvent(packet_type, outfit_id, member_id) => - packet_type mustEqual 1 - outfit_id mustEqual 6418 - member_id mustEqual 42644970 + "decode Unk1" in { + PacketCoding.decodePacket(unk1).require match { + case OutfitMemberEvent(packet_type,outfit_id, member_id, Unk1()) => + packet_type mustEqual OutfitMemberEvent.PacketType.Unk1 + outfit_id mustEqual 529744 + member_id mustEqual 41605263 case _ => ko } } - "encode Unk0" in { + "encode Unk1" in { val msg = OutfitMemberEvent( - packet_type = 1, - outfit_id = 6418, - member_id = 42644970, + packet_type = OutfitMemberEvent.PacketType.Unk1, + outfit_id = 529744, + member_id = 41605263, + Unk1() ) val pkt = PacketCoding.encodePacket(msg).require.toByteVector - pkt mustEqual Unk1 + pkt mustEqual unk1 } - */ } From 72a8a7cd8920d55397c8fa1e754643f6c5fcb83f Mon Sep 17 00:00:00 2001 From: Resaec Date: Sun, 24 Aug 2025 00:23:38 +0200 Subject: [PATCH 14/22] OutfitEvent rework renamed type Unk4 to UpdateOutfitId --- .../psforever/packet/game/OutfitEvent.scala | 146 ++++++------------ src/test/scala/game/OutfitEventTest.scala | 90 +++-------- 2 files changed, 67 insertions(+), 169 deletions(-) diff --git a/src/main/scala/net/psforever/packet/game/OutfitEvent.scala b/src/main/scala/net/psforever/packet/game/OutfitEvent.scala index 1a633fa2..8b278074 100644 --- a/src/main/scala/net/psforever/packet/game/OutfitEvent.scala +++ b/src/main/scala/net/psforever/packet/game/OutfitEvent.scala @@ -39,55 +39,38 @@ object OutfitEventAction { outfit_name: String, outfit_points1: Long, outfit_points2: Long, // same as outfit_points1 - member_count: Int, - unk9: Int, + member_count: Long, outfit_rank_names: OutfitRankNames, motd: String, unk10: Int, - unk11: Int, - unk12: Int, - unk13: Int,// ? - unk21: Int, - unk21_2: Int, + unk11: Boolean, + unk12: Long, // only set if unk11 is false created_timestamp: Long, unk23: Long, unk24: Long, unk25: Long, - u123: Int, ) final case class Unk0( - outfitInfo: OutfitInfo + outfit_info: OutfitInfo ) extends OutfitEventAction(code = 0) final case class Unk1( - unk2: Int, - unk3: Boolean, ) extends OutfitEventAction(code = 1) final case class Unk2( - outfitInfo: OutfitInfo, + outfit_info: OutfitInfo, ) extends OutfitEventAction(code = 2) final case class Unk3( - unk2: Int, - unk3: Boolean, - data: BitVector, ) extends OutfitEventAction(code = 3) - final case class Unk4( + final case class UpdateOutfitId( new_outfit_id: Long, - unk3: Int, - unk4: Boolean, - data: BitVector, ) extends OutfitEventAction(code = 4) final case class Unk5( - unk1: Int, - unk2: Int, - unk3: Int, - unk4: Boolean, - data: BitVector, + unk1: Long, ) extends OutfitEventAction(code = 5) final case class Unknown(badCode: Int, data: BitVector) extends OutfitEventAction(badCode) @@ -120,38 +103,33 @@ object OutfitEventAction { ) private val InfoCodec: Codec[OutfitInfo] = ( - PacketHelpers.encodedWideStringAligned(5) :: - uint32L :: - uint32L :: - uint16L :: - uint16L :: - OutfitRankNamesCodec :: - PacketHelpers.encodedWideString :: - uint8L :: - uint8L :: - uint8L :: - uint8L :: - uint8L :: // bool somewhere here - uintL(1) :: // + ("outfit_name" | PacketHelpers.encodedWideStringAligned(5)) :: + ("outfit_points1" | uint32L) :: + ("outfit_points2" | uint32L) :: + ("member_count" | uint32L) :: + ("outfit_rank_names" | OutfitRankNamesCodec) :: + ("motd" | PacketHelpers.encodedWideString) :: + ("" | uint8L) :: + ("" | bool) :: + ("" | uint32L) :: ("created_timestamp" | uint32L) :: - uint32L :: - uint32L :: - uint32L :: - uintL(7) + ("" | uint32L) :: + ("" | uint32L) :: + ("" | uint32L) ).xmap[OutfitInfo]( { - case outfit_name :: u6 :: u7 :: member_count :: u9 :: outfit_rank_names :: motd :: u10 :: u11 :: u12 :: u13 :: u21 :: u21_2 :: created_timestamp :: u23 :: u24 :: u25 :: u123 :: HNil => - OutfitInfo(outfit_name, u6, u7, member_count, u9, outfit_rank_names, motd, u10, u11, u12, u13, u21, u21_2, created_timestamp, u23, u24, u25, u123) + case outfit_name :: outfit_points1 :: outfit_points2 :: member_count :: outfit_rank_names :: motd :: u10 :: u11 :: u12 :: created_timestamp :: u23 :: u24 :: u25 :: HNil => + OutfitInfo(outfit_name, outfit_points1, outfit_points2, member_count, outfit_rank_names, motd, u10, u11, u12, created_timestamp, u23, u24, u25) }, { - case OutfitInfo(outfit_name, u6, u7, member_count, u9, outfit_rank_names, motd, u10, u11, u12, u13, u21, u21_2, created_timestamp, u23, u24, u25, u123) => - outfit_name :: u6 :: u7 :: member_count :: u9 :: outfit_rank_names :: motd :: u10 :: u11 :: u12 :: u13 :: u21 :: u21_2 :: created_timestamp :: u23 :: u24 :: u25 :: u123 :: HNil + case OutfitInfo(outfit_name, outfit_points1, outfit_points2, member_count, outfit_rank_names, motd, u10, u11, u12, created_timestamp, u23, u24, u25) => + outfit_name :: outfit_points1 :: outfit_points2 :: member_count :: outfit_rank_names :: motd :: u10 :: u11 :: u12 :: created_timestamp :: u23 :: u24 :: u25 :: HNil } ) val Unk0Codec: Codec[Unk0] = ( - InfoCodec - ).xmap[Unk0]( + ("outfit_info" | InfoCodec) + ).xmap[Unk0]( { case info => Unk0(info) @@ -162,23 +140,11 @@ object OutfitEventAction { } ) - val Unk1Codec: Codec[Unk1] = ( - uint4L :: - bool - ).xmap[Unk1]( - { - case u2 :: u3 :: HNil => - Unk1(u2, u3) - }, - { - case Unk1(u2, u3) => - u2 :: u3 :: HNil - } - ) + val Unk1Codec: Codec[Unk1] = PacketHelpers.emptyCodec(Unk1()) val Unk2Codec: Codec[Unk2] = ( - InfoCodec - ).xmap[Unk2]( + ("outfit_info" | InfoCodec) + ).xmap[Unk2]( { case info => Unk2(info) @@ -189,51 +155,31 @@ object OutfitEventAction { } ) - val Unk3Codec: Codec[Unk3] = ( - uint4L :: - bool :: - bits - ).xmap[Unk3]( - { - case u2 :: u3 :: data :: HNil => - Unk3(u2, u3, data) - }, - { - case Unk3(u2, u3, data) => - u2 :: u3 :: data :: HNil - } - ) + val Unk3Codec: Codec[Unk3] = PacketHelpers.emptyCodec(Unk3()) - val Unk4Codec: Codec[Unk4] = ( // update outfit_id? // 2016.03.18 #10640 // after this packet the referenced id changes to the new one, old is not used again - uint32L :: // real / other outfit_id - uint4L :: - bool :: - bits - ).xmap[Unk4]( + val UpdateOutfitIdCodec: Codec[UpdateOutfitId] = ( // update outfit_id? // 2016.03.18 #10640 // after this packet the referenced id changes to the new one, old is not used again + ("new_outfit_id" | uint32L) + ).xmap[UpdateOutfitId]( { - case new_outfit_id :: u3 :: u4 :: data :: HNil => - Unk4(new_outfit_id, u3, u4, data) + case new_outfit_id => + UpdateOutfitId(new_outfit_id) }, { - case Unk4(new_outfit_id, u3, u4, data) => - new_outfit_id ::u3 :: u4 :: data :: HNil + case UpdateOutfitId(new_outfit_id) => + new_outfit_id } ) val Unk5Codec: Codec[Unk5] = ( - uint16L :: - uint16L :: - uint4L :: - bool :: - bits - ).xmap[Unk5]( + ("" | uint32L) + ).xmap[Unk5]( { - case u1 :: u2 :: u3 :: u4 :: data :: HNil => - Unk5(u1, u2, u3, u4, data) + case u1 => + Unk5(u1) }, { - case Unk5(u1, u2, u3, u4, data) => - u1 :: u2 :: u3 :: u4 :: data :: HNil + case Unk5(u1) => + u1 } ) @@ -274,10 +220,10 @@ object OutfitEvent extends Marshallable[OutfitEvent] { 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 UpdateOutfitId: RequestType.Value = Value(4) val Unk5: RequestType.Value = Value(5) - val unk6: RequestType.Value = Value(6) - val unk7: RequestType.Value = Value(7) + val Unk6: RequestType.Value = Value(6) + val Unk7: RequestType.Value = Value(7) implicit val codec: Codec[Type] = PacketHelpers.createEnumerationCodec(this, uintL(3)) } @@ -291,7 +237,7 @@ object OutfitEvent extends Marshallable[OutfitEvent] { case 1 => Unk1Codec case 2 => Unk2Codec // sent after /outfitcreate and on login if in an outfit case 3 => Unk3Codec - case 4 => Unk4Codec + case 4 => UpdateOutfitIdCodec case 5 => Unk5Codec case 6 => unknownCodec(action = code) case 7 => unknownCodec(action = code) diff --git a/src/test/scala/game/OutfitEventTest.scala b/src/test/scala/game/OutfitEventTest.scala index e4a86cec..2b3e2dd8 100644 --- a/src/test/scala/game/OutfitEventTest.scala +++ b/src/test/scala/game/OutfitEventTest.scala @@ -29,7 +29,7 @@ class OutfitEventTest extends Specification { "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" + + "8f 4 0201feff" + "2e 0 50006c0061006e006500740053006900640065005f0046006f00720065007600650072005f00560061006e007500" + // PlanetSide_Forever_Vanu "00000000" + "00000000" + @@ -60,17 +60,12 @@ class OutfitEventTest extends Specification { outfit_points1 = 223190045, outfit_points2 = 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.", 15, - 128, - 0, - 0, - 0, - 0, - 1210901990, - 0, + unk11 = true, + unk12 = 0, + created_timestamp = 1210901990, 0, 0, 0, @@ -91,17 +86,12 @@ class OutfitEventTest extends Specification { outfit_points1 = 223190045, outfit_points2 = 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.", 15, - 128, - 0, - 0, - 0, - 0, - 1210901990, - 0, + unk11 = true, + unk12 = 0, + created_timestamp = 1210901990, 0, 0, 0, @@ -118,10 +108,7 @@ class OutfitEventTest extends Specification { case OutfitEvent(request_type, outfit_guid, action) => request_type mustEqual RequestType.Unk1 outfit_guid mustEqual 529688L - action mustEqual Unk1( - unk2 = 0, - unk3 = false - ) + action mustEqual Unk1() case _ => ko } @@ -131,10 +118,7 @@ class OutfitEventTest extends Specification { val msg = OutfitEvent( RequestType.Unk1, 529688L, - Unk1( - unk2 = 0, - unk3 = false, - ) + Unk1() ) val pkt = PacketCoding.encodePacket(msg).require.toByteVector @@ -151,20 +135,15 @@ class OutfitEventTest extends Specification { outfit_points1 = 0, outfit_points2 = 0, member_count = 1, - unk9 = 0, OutfitRankNames("","","","","","","",""), "", 0, - 112, - 73, - 130, + unk11 = false, + unk12 = 300000, + created_timestamp = 0, 0, 0, 0, - 0, - 0, - 0, - 0 )) case _ => ko @@ -181,20 +160,15 @@ class OutfitEventTest extends Specification { outfit_points1 = 0, outfit_points2 = 0, member_count = 1, - unk9 = 0, OutfitRankNames("","","","","","","",""), "", 0, - 112, - 73, - 130, + unk11 = false, + unk12 = 300000, + created_timestamp = 0, 0, 0, 0, - 0, - 0, - 0, - 0 ) ) ) @@ -208,11 +182,7 @@ class OutfitEventTest extends Specification { case OutfitEvent(request_type, outfit_guid, action) => request_type mustEqual RequestType.Unk3 outfit_guid mustEqual 2147418113L - action mustEqual Unk3( - unk2 = 0, - unk3 = false, - BitVector.fromValidHex("") - ) + action mustEqual Unk3() case _ => ko } @@ -222,11 +192,7 @@ class OutfitEventTest extends Specification { val msg = OutfitEvent( RequestType.Unk3, 2147418113L, - Unk3( - unk2 = 0, - unk3 = false, - BitVector.fromValidHex("") - ) + Unk3() ) val pkt = PacketCoding.encodePacket(msg).require.toByteVector @@ -236,13 +202,10 @@ class OutfitEventTest extends Specification { "decode Unk4 ABC" in { PacketCoding.decodePacket(unk4_ABC).require match { case OutfitEvent(request_type, outfit_guid, action) => - request_type mustEqual RequestType.Unk4 + request_type mustEqual RequestType.UpdateOutfitId outfit_guid mustEqual 2147418113L - action mustEqual Unk4( + action mustEqual UpdateOutfitId( new_outfit_id = 529744L, - 0, - unk4 = false, - BitVector.fromValidHex("") ) case _ => ko @@ -251,13 +214,10 @@ class OutfitEventTest extends Specification { "encode Unk4 ABC" in { val msg = OutfitEvent( - RequestType.Unk4, + RequestType.UpdateOutfitId, 2147418113L, - Unk4( + UpdateOutfitId( new_outfit_id = 529744L, - unk3 = 0, - unk4 = false, - BitVector.fromValidHex("") ) ) val pkt = PacketCoding.encodePacket(msg).require.toByteVector @@ -272,10 +232,6 @@ class OutfitEventTest extends Specification { outfit_guid mustEqual 2147418113L action mustEqual Unk5( unk1 = 2, - unk2 = 0, - unk3 = 0, - unk4 = false, - BitVector.fromValidHex("") // OR f88c2a0417c1a06101001f20f4b8c00000404090ac9c6745dea88cadf0f810e03e0200f92 with bool at the back ) case _ => ko @@ -288,10 +244,6 @@ class OutfitEventTest extends Specification { 2147418113L, Unk5( unk1 = 2, - unk2 = 0, - unk3 = 0, - unk4 = false, - BitVector.fromValidHex("") ) ) val pkt = PacketCoding.encodePacket(msg).require.toByteVector From 8de797087fab93035154dd60b94edb1321c930d4 Mon Sep 17 00:00:00 2001 From: Resaec Date: Sun, 24 Aug 2025 03:43:50 +0200 Subject: [PATCH 15/22] OutfitListEvent cleanup --- .../net/psforever/packet/game/OutfitListEvent.scala | 12 +++++------- 1 file changed, 5 insertions(+), 7 deletions(-) diff --git a/src/main/scala/net/psforever/packet/game/OutfitListEvent.scala b/src/main/scala/net/psforever/packet/game/OutfitListEvent.scala index c040f2e5..42d7cc29 100644 --- a/src/main/scala/net/psforever/packet/game/OutfitListEvent.scala +++ b/src/main/scala/net/psforever/packet/game/OutfitListEvent.scala @@ -37,7 +37,6 @@ object OutfitListEventAction { */ final case class Unk3( unk1: Long, - data: ByteVector ) extends OutfitListEventAction(code = 3) final case class Unknown(badCode: Int, data: BitVector) extends OutfitListEventAction(badCode) @@ -67,16 +66,15 @@ object OutfitListEventAction { ) val Unk3Codec: Codec[Unk3] = ( - ("unk1" | uint32L) :: - ("data" | bytes) + ("unk1" | uint32L) ).xmap[Unk3]( { - case u1 :: data :: HNil => - Unk3(u1, data) + case u1 => + Unk3(u1) }, { - case Unk3(u1, data) => - u1 :: data :: HNil + case Unk3(u1) => + u1 } ) From cc16040ec3ac7d1e55b5f94cb51d7a044388a2eb Mon Sep 17 00:00:00 2001 From: Resaec Date: Sun, 24 Aug 2025 16:43:24 +0200 Subject: [PATCH 16/22] Simplified Outfit packet usage Outfit cleanup More tests --- .../psforever/packet/game/OutfitEvent.scala | 77 +++--- .../packet/game/OutfitListEvent.scala | 47 ++-- .../packet/game/OutfitMemberEvent.scala | 33 +-- .../packet/game/OutfitMemberUpdate.scala | 26 +- .../packet/game/OutfitMembershipRequest.scala | 245 +++++++++--------- .../game/OutfitMembershipResponse.scala | 26 +- .../psforever/packet/game/OutfitRequest.scala | 23 +- src/test/scala/game/OutfitEventTest.scala | 28 +- src/test/scala/game/OutfitListEventTest.scala | 15 +- .../scala/game/OutfitMemberEventTest.scala | 16 +- .../scala/game/OutfitMemberUpdateTest.scala | 51 ++++ .../game/OutfitMembershipRequestTest.scala | 80 +++--- .../game/OutfitMembershipResponseTest.scala | 2 +- ...quesTest.scala => OutfitRequestTest.scala} | 23 +- 14 files changed, 359 insertions(+), 333 deletions(-) create mode 100644 src/test/scala/game/OutfitMemberUpdateTest.scala rename src/test/scala/game/{OutfitRequesTest.scala => OutfitRequestTest.scala} (92%) diff --git a/src/main/scala/net/psforever/packet/game/OutfitEvent.scala b/src/main/scala/net/psforever/packet/game/OutfitEvent.scala index 8b278074..be267fce 100644 --- a/src/main/scala/net/psforever/packet/game/OutfitEvent.scala +++ b/src/main/scala/net/psforever/packet/game/OutfitEvent.scala @@ -9,8 +9,7 @@ import scodec.codecs._ import shapeless.{::, HNil} final case class OutfitEvent( - request_type: OutfitEvent.RequestType.Type, - outfit_guid: Long, + outfit_id: Long, action: OutfitEventAction ) extends PlanetSideGamePacket { type Packet = OutfitEvent @@ -84,13 +83,13 @@ object OutfitEventAction { private val OutfitRankNamesCodec: Codec[OutfitRankNames] = ( PacketHelpers.encodedWideString :: - PacketHelpers.encodedWideString :: - PacketHelpers.encodedWideString :: - PacketHelpers.encodedWideString :: - PacketHelpers.encodedWideString :: - PacketHelpers.encodedWideString :: - PacketHelpers.encodedWideString :: - 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 => @@ -103,20 +102,20 @@ object OutfitEventAction { ) private val InfoCodec: Codec[OutfitInfo] = ( - ("outfit_name" | PacketHelpers.encodedWideStringAligned(5)) :: - ("outfit_points1" | uint32L) :: - ("outfit_points2" | uint32L) :: - ("member_count" | uint32L) :: - ("outfit_rank_names" | OutfitRankNamesCodec) :: - ("motd" | PacketHelpers.encodedWideString) :: - ("" | uint8L) :: - ("" | bool) :: - ("" | uint32L) :: - ("created_timestamp" | uint32L) :: - ("" | uint32L) :: - ("" | uint32L) :: - ("" | uint32L) - ).xmap[OutfitInfo]( + ("outfit_name" | PacketHelpers.encodedWideStringAligned(5)) :: + ("outfit_points1" | uint32L) :: + ("outfit_points2" | uint32L) :: + ("member_count" | uint32L) :: + ("outfit_rank_names" | OutfitRankNamesCodec) :: + ("motd" | PacketHelpers.encodedWideString) :: + ("" | uint8L) :: + ("" | bool) :: + ("" | uint32L) :: + ("created_timestamp" | uint32L) :: + ("" | uint32L) :: + ("" | uint32L) :: + ("" | uint32L) + ).xmap[OutfitInfo]( { case outfit_name :: outfit_points1 :: outfit_points2 :: member_count :: outfit_rank_names :: motd :: u10 :: u11 :: u12 :: created_timestamp :: u23 :: u24 :: u25 :: HNil => OutfitInfo(outfit_name, outfit_points1, outfit_points2, member_count, outfit_rank_names, motd, u10, u11, u12, created_timestamp, u23, u24, u25) @@ -213,17 +212,17 @@ object OutfitEventAction { object OutfitEvent extends Marshallable[OutfitEvent] { - object RequestType extends Enumeration { + object PacketType 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 UpdateOutfitId: RequestType.Value = Value(4) - val Unk5: RequestType.Value = Value(5) - val Unk6: RequestType.Value = Value(6) - val Unk7: RequestType.Value = Value(7) + val Unk0: PacketType.Value = Value(0) // start listing of members + val Unk1: PacketType.Value = Value(1) // end listing of members + val Unk2: PacketType.Value = Value(2) // send after creating an outfit // normal info, same as Unk0 + val Unk3: PacketType.Value = Value(3) // below + val UpdateOutfitId: PacketType.Value = Value(4) + val Unk5: PacketType.Value = Value(5) + val Unk6: PacketType.Value = Value(6) + val Unk7: PacketType.Value = Value(7) implicit val codec: Codec[Type] = PacketHelpers.createEnumerationCodec(this, uintL(3)) } @@ -247,18 +246,18 @@ object OutfitEvent extends Marshallable[OutfitEvent] { } implicit val codec: Codec[OutfitEvent] = ( - ("request_type" | RequestType.codec) >>:~ { request_type => + ("packet_type" | PacketType.codec) >>:~ { packet_type => ("outfit_guid" | uint32L) :: - ("action" | selectFromType(request_type.id)) + ("action" | selectFromType(packet_type.id)) } - ).xmap[OutfitEvent]( + ).xmap[OutfitEvent]( { - case request_type :: outfit_guid :: action :: HNil => - OutfitEvent(request_type, outfit_guid, action) + case _ :: outfit_guid :: action :: HNil => + OutfitEvent(outfit_guid, action) }, { - case OutfitEvent(request_type, outfit_guid, action) => - request_type :: outfit_guid :: action :: HNil + case OutfitEvent(outfit_guid, action) => + OutfitEvent.PacketType(action.code) :: 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 index 42d7cc29..acd9ed99 100644 --- a/src/main/scala/net/psforever/packet/game/OutfitListEvent.scala +++ b/src/main/scala/net/psforever/packet/game/OutfitListEvent.scala @@ -9,7 +9,6 @@ import scodec.codecs._ import shapeless.{::, HNil} final case class OutfitListEvent( - request_type: OutfitListEvent.RequestType.Type, action: OutfitListEventAction ) extends PlanetSideGamePacket { type Packet = OutfitListEvent @@ -50,11 +49,11 @@ object OutfitListEventAction { val ListElementOutfitCodec: Codec[ListElementOutfit] = ( ("unk1" | uint32L) :: - ("points" | uint32L) :: - ("members" | uint32L) :: - ("outfit_name" | PacketHelpers.encodedWideStringAligned(5)) :: - ("outfit_leader" | PacketHelpers.encodedWideString) - ).xmap[ListElementOutfit]( + ("points" | uint32L) :: + ("members" | uint32L) :: + ("outfit_name" | PacketHelpers.encodedWideStringAligned(5)) :: + ("outfit_leader" | PacketHelpers.encodedWideString) + ).xmap[ListElementOutfit]( { case u1 :: points :: members :: outfit_name :: outfit_leader :: HNil => ListElementOutfit(u1, points, members, outfit_name, outfit_leader) @@ -67,7 +66,7 @@ object OutfitListEventAction { val Unk3Codec: Codec[Unk3] = ( ("unk1" | uint32L) - ).xmap[Unk3]( + ).xmap[Unk3]( { case u1 => Unk3(u1) @@ -109,17 +108,17 @@ object OutfitListEventAction { object OutfitListEvent extends Marshallable[OutfitListEvent] { import shapeless.{::, HNil} - object RequestType extends Enumeration { + object PacketType extends Enumeration { type Type = Value - val Unk0: RequestType.Value = Value(0) - val Unk1: RequestType.Value = Value(1) - val ListElementOutfit: RequestType.Value = Value(2) - val Unk3: RequestType.Value = Value(3) - val Unk4: RequestType.Value = Value(4) - val Unk5: RequestType.Value = Value(5) - val unk6: RequestType.Value = Value(6) - val unk7: RequestType.Value = Value(7) + val Unk0: PacketType.Value = Value(0) + val Unk1: PacketType.Value = Value(1) + val ListElementOutfit: PacketType.Value = Value(2) + val Unk3: PacketType.Value = Value(3) + val Unk4: PacketType.Value = Value(4) + val Unk5: PacketType.Value = Value(5) + val unk6: PacketType.Value = Value(6) + val unk7: PacketType.Value = Value(7) implicit val codec: Codec[Type] = PacketHelpers.createEnumerationCodec(this, uintL(3)) } @@ -132,7 +131,7 @@ object OutfitListEvent extends Marshallable[OutfitListEvent] { case 0 => unknownCodec(action = code) case 1 => unknownCodec(action = code) case 2 => ListElementOutfitCodec - case 3 => Unk3Codec // indicated in code + case 3 => Unk3Codec case 4 => unknownCodec(action = code) case 5 => unknownCodec(action = code) case 6 => unknownCodec(action = code) @@ -143,17 +142,17 @@ object OutfitListEvent extends Marshallable[OutfitListEvent] { } implicit val codec: Codec[OutfitListEvent] = ( - ("request_type" | RequestType.codec) >>:~ { request_type => - ("action" | selectFromType(request_type.id)).hlist + ("packet_type" | PacketType.codec) >>:~ { packet_type => + ("action" | selectFromType(packet_type.id)).hlist } - ).xmap[OutfitListEvent]( + ).xmap[OutfitListEvent]( { - case request_type :: action :: HNil => - OutfitListEvent(request_type, action) + case _ :: action :: HNil => + OutfitListEvent(action) }, { - case OutfitListEvent(request_type, action) => - request_type :: action :: HNil + case OutfitListEvent(action) => + OutfitListEvent.PacketType(action.code) :: action :: HNil } ) } diff --git a/src/main/scala/net/psforever/packet/game/OutfitMemberEvent.scala b/src/main/scala/net/psforever/packet/game/OutfitMemberEvent.scala index f40498a4..0ae68085 100644 --- a/src/main/scala/net/psforever/packet/game/OutfitMemberEvent.scala +++ b/src/main/scala/net/psforever/packet/game/OutfitMemberEvent.scala @@ -9,7 +9,6 @@ import scodec.codecs._ import shapeless.{::, HNil} final case class OutfitMemberEvent( - packet_type: OutfitMemberEvent.PacketType.Type, outfit_id: Long, member_id: Long, action: OutfitMemberEventAction @@ -36,13 +35,13 @@ object OutfitMemberEventAction { /* action is unimplemented! if action == 0 unk2 will contain one additional uint32L - unk0_padding contains one byte of padding. may contain 4byte of unknown data depending on action + padding contains one uint4L of padding. may contain uint32L of unknown data depending on action */ final case class Unk0( member_name: String, rank: Int, points: Long, // client divides this by 100 - last_login: Long, // seconds ago from current time, 0 if online + last_online: Long, // seconds ago from current time, 0 if online action: PacketType.Type, // should always be 1, otherwise there will be actual data in padding. not implemented! padding: Int // should always be 0, 4 bits of padding // only contains data if action is 0 ) extends OutfitMemberEventAction(code = 0) @@ -60,13 +59,13 @@ object OutfitMemberEventAction { private val everFailCondition = conditional(included = false, bool) val Unk0Codec: Codec[Unk0] = ( - ("member_name" | PacketHelpers.encodedWideStringAligned(6)) :: // from here is packet_type == 0 only - ("rank" | uint(3)) :: - ("points" | uint32L) :: - ("last_login" | uint32L) :: - ("action" | OutfitMemberEventAction.PacketType.codec) :: - ("padding" | uint4L) - ).xmap[Unk0]( + ("member_name" | PacketHelpers.encodedWideStringAligned(6)) :: // from here is packet_type == 0 only + ("rank" | uint(3)) :: + ("points" | uint32L) :: + ("last_login" | uint32L) :: + ("action" | OutfitMemberEventAction.PacketType.codec) :: + ("padding" | uint4L) + ).xmap[Unk0]( { case member_name :: rank :: points :: last_login :: action :: padding :: HNil => Unk0(member_name, rank, points, last_login, action, padding) @@ -112,6 +111,8 @@ object OutfitMemberEvent extends Marshallable[OutfitMemberEvent] { val Unk0: PacketType.Value = Value(0) val Unk1: PacketType.Value = Value(1) // Info: Player has been invited / response to OutfitMembershipRequest Unk2 for that player + val Unk2: PacketType.Value = Value(2) + val Unk3: PacketType.Value = Value(3) implicit val codec: Codec[Type] = PacketHelpers.createEnumerationCodec(this, uintL(2)) } @@ -123,6 +124,8 @@ object OutfitMemberEvent extends Marshallable[OutfitMemberEvent] { ((code: @switch) match { case 0 => Unk0Codec case 1 => Unk1Codec + case 2 => unknownCodec(code) + case 3 => unknownCodec(code) case _ => failureCodec(code) }).asInstanceOf[Codec[OutfitMemberEventAction]] @@ -134,14 +137,14 @@ object OutfitMemberEvent extends Marshallable[OutfitMemberEvent] { ("member_id" | uint32L) :: ("action" | selectFromType(packet_type.id)).hlist } - ).xmap[OutfitMemberEvent]( + ).xmap[OutfitMemberEvent]( { - case packet_type :: outfit_id :: member_id:: action :: HNil => - OutfitMemberEvent(packet_type, outfit_id, member_id, action) + case _ :: outfit_id :: member_id:: action :: HNil => + OutfitMemberEvent(outfit_id, member_id, action) }, { - case OutfitMemberEvent(packet_type, outfit_id, member_id, action) => - packet_type :: outfit_id :: member_id :: action :: HNil + case OutfitMemberEvent(outfit_id, member_id, action) => + OutfitMemberEvent.PacketType(action.code) :: outfit_id :: member_id :: action :: HNil } ) } diff --git a/src/main/scala/net/psforever/packet/game/OutfitMemberUpdate.scala b/src/main/scala/net/psforever/packet/game/OutfitMemberUpdate.scala index 88cae632..979d26f9 100644 --- a/src/main/scala/net/psforever/packet/game/OutfitMemberUpdate.scala +++ b/src/main/scala/net/psforever/packet/game/OutfitMemberUpdate.scala @@ -7,10 +7,10 @@ import scodec.codecs._ import shapeless.{::, HNil} final case class OutfitMemberUpdate( - outfit_guid: Long, - char_id: Long, - rank: Int, // 0-7 - unk1: Int, + outfit_id: Long, + char_id: Long, + rank: Int, // 0-7 + flag: Boolean, ) extends PlanetSideGamePacket { type Packet = OutfitMemberUpdate def opcode = GamePacketOpcode.OutfitMemberUpdate @@ -19,18 +19,18 @@ final case class OutfitMemberUpdate( object OutfitMemberUpdate extends Marshallable[OutfitMemberUpdate] { implicit val codec: Codec[OutfitMemberUpdate] = ( - ("outfit_guid" | uint32L) :: - ("char_id" | uint32L) :: - ("rank" | uint(3)) :: - ("unk1" | uint(5)) - ).xmap[OutfitMemberUpdate]( + ("outfit_id" | uint32L) :: + ("char_id" | uint32L) :: + ("rank" | uint(3)) :: + ("flag" | bool) + ).xmap[OutfitMemberUpdate]( { - case outfit_guid :: char_id :: rank :: u1 :: HNil => - OutfitMemberUpdate(outfit_guid, char_id, rank, u1) + case outfit_id :: char_id :: rank :: flag :: HNil => + OutfitMemberUpdate(outfit_id, char_id, rank, flag) }, { - case OutfitMemberUpdate(outfit_guid, char_id, rank, u1) => - outfit_guid :: char_id :: rank :: u1 :: HNil + case OutfitMemberUpdate(outfit_id, char_id, rank, flag) => + outfit_id :: char_id :: rank :: flag :: HNil } ) } diff --git a/src/main/scala/net/psforever/packet/game/OutfitMembershipRequest.scala b/src/main/scala/net/psforever/packet/game/OutfitMembershipRequest.scala index 5e0c8b9b..41cb6b17 100644 --- a/src/main/scala/net/psforever/packet/game/OutfitMembershipRequest.scala +++ b/src/main/scala/net/psforever/packet/game/OutfitMembershipRequest.scala @@ -9,7 +9,6 @@ import scodec.codecs._ import shapeless.{::, HNil} final case class OutfitMembershipRequest( - request_type: OutfitMembershipRequest.RequestType.Type, outfit_id: Long, action: OutfitMembershipRequestAction ) extends PlanetSideGamePacket { @@ -61,8 +60,8 @@ object OutfitMembershipRequestAction { ) extends OutfitMembershipRequestAction(code = 6) final case class SetRank( - avatar_id: Long, // 32 - rank: Int, // 3 + avatar_id: Long, + rank: Int, member_name: String, ) extends OutfitMembershipRequestAction(code = 7) @@ -75,120 +74,116 @@ object OutfitMembershipRequestAction { object Codecs { private val everFailCondition = conditional(included = false, bool) - val CreateCodec: Codec[Create] = - ( - PacketHelpers.encodedWideStringAligned(5) :: - PacketHelpers.encodedWideString - ).xmap[Create]( - { - case u1 :: outfit_name :: HNil => - Create(u1, outfit_name) - }, - { - case Create(u1, outfit_name) => - u1 :: outfit_name :: HNil - } - ) + val CreateCodec: Codec[Create] = ( + PacketHelpers.encodedWideStringAligned(5) :: + PacketHelpers.encodedWideString + ).xmap[Create]( + { + case u1 :: outfit_name :: HNil => + Create(u1, outfit_name) + }, + { + case Create(u1, outfit_name) => + u1 :: outfit_name :: HNil + } + ) - val FormCodec: Codec[Form] = - ( - PacketHelpers.encodedWideStringAligned(5) :: - PacketHelpers.encodedWideString - ).xmap[Form]( - { - case u1 :: outfit_name :: HNil => - Form(u1, outfit_name) - }, - { - case Form(u1, outfit_name) => - u1 :: outfit_name :: HNil - } - ) + val FormCodec: Codec[Form] = ( + PacketHelpers.encodedWideStringAligned(5) :: + PacketHelpers.encodedWideString + ).xmap[Form]( + { + case u1 :: outfit_name :: HNil => + Form(u1, outfit_name) + }, + { + case Form(u1, outfit_name) => + u1 :: outfit_name :: HNil + } + ) - val InviteCodec: Codec[Invite] = - ( - uint32L :: - PacketHelpers.encodedWideStringAligned(5) - ).xmap[Invite]( - { - case u1 :: member_name :: HNil => - Invite(u1, member_name) - }, - { - case Invite(u1, member_name) => - u1 :: member_name :: HNil - } - ) + val InviteCodec: Codec[Invite] = ( + uint32L :: + PacketHelpers.encodedWideStringAligned(5) + ).xmap[Invite]( + { + case avatar_id :: member_name :: HNil => + Invite(avatar_id, member_name) + }, + { + case Invite(avatar_id, member_name) => + avatar_id :: member_name :: HNil + } + ) - val AcceptInviteCodec: Codec[AcceptInvite] = - PacketHelpers.encodedWideString.xmap[AcceptInvite]( - { - case u1 => - AcceptInvite(u1) - }, - { - case AcceptInvite(u1) => - u1 - } - ) + val AcceptInviteCodec: Codec[AcceptInvite] = ( + PacketHelpers.encodedWideString + ).xmap[AcceptInvite]( + { + case member_name => + AcceptInvite(member_name) + }, + { + case AcceptInvite(member_name) => + member_name + } + ) - val RejectInviteCodec: Codec[RejectInvite] = - PacketHelpers.encodedWideString.xmap[RejectInvite]( - { - case u1 => - RejectInvite(u1) - }, - { - case RejectInvite(u1) => - u1 - } - ) + val RejectInviteCodec: Codec[RejectInvite] = ( + PacketHelpers.encodedWideString + ).xmap[RejectInvite]( + { + case member_name => + RejectInvite(member_name) + }, + { + case RejectInvite(member_name) => + member_name + } + ) - val CancelInviteCodec: Codec[CancelInvite] = - ( - uint32L :: - PacketHelpers.encodedWideStringAligned(5) - ).xmap[CancelInvite]( - { - case u1 :: outfit_name :: HNil => - CancelInvite(u1, outfit_name) - }, - { - case CancelInvite(u1, outfit_name) => - u1 :: outfit_name :: HNil - } - ) + val CancelInviteCodec: Codec[CancelInvite] = ( + uint32L :: + PacketHelpers.encodedWideStringAligned(5) + ).xmap[CancelInvite]( + { + case avatar_id :: outfit_name :: HNil => + CancelInvite(avatar_id, outfit_name) + }, + { + case CancelInvite(avatar_id, outfit_name) => + avatar_id :: outfit_name :: HNil + } + ) - val KickCodec: Codec[Kick] = - ( - uint32L :: - PacketHelpers.encodedWideStringAligned(5) - ).xmap[Kick]( - { - case u1 :: member_name :: HNil => - Kick(u1, member_name) - }, - { - case Kick(u1, member_name) => - u1 :: member_name :: HNil - } - ) + val KickCodec: Codec[Kick] = ( + uint32L :: + PacketHelpers.encodedWideStringAligned(5) + ).xmap[Kick]( + { + case avatar_id :: member_name :: HNil => + Kick(avatar_id, member_name) + }, + { + case Kick(avatar_id, member_name) => + avatar_id :: member_name :: HNil + } + ) - val SetRankCodec: Codec[SetRank] = - ( - uint32L :: - uintL(3) :: - PacketHelpers.encodedWideStringAligned(2) - ).xmap[SetRank]( - { - case u1 :: rank :: member_name :: HNil => - SetRank(u1, rank, member_name) - }, - { - case SetRank(u1, rank, member_name) => - u1 :: rank :: member_name :: HNil - } - ) + val SetRankCodec: Codec[SetRank] = ( + uint32L :: + uintL(3) :: + PacketHelpers.encodedWideStringAligned(2) + ).xmap[SetRank]( + { + case avatar_id :: rank :: member_name :: HNil => + SetRank(avatar_id, rank, member_name) + }, + { + case SetRank(avatar_id, rank, member_name) => + avatar_id :: rank :: member_name :: HNil + } + ) /** @@ -221,17 +216,17 @@ object OutfitMembershipRequestAction { object OutfitMembershipRequest extends Marshallable[OutfitMembershipRequest] { - object RequestType extends Enumeration { + object PacketType extends Enumeration { type Type = Value - val Create: RequestType.Value = Value(0) - val Form: RequestType.Value = Value(1) - val Invite: RequestType.Value = Value(2) - val Accept: RequestType.Value = Value(3) - val Reject: RequestType.Value = Value(4) - val Cancel: RequestType.Value = Value(5) - val Kick: RequestType.Value = Value(6) - val SetRank: RequestType.Value = Value(7) + val Create: PacketType.Value = Value(0) + val Form: PacketType.Value = Value(1) + val Invite: PacketType.Value = Value(2) + val Accept: PacketType.Value = Value(3) + val Reject: PacketType.Value = Value(4) + val Cancel: PacketType.Value = Value(5) + val Kick: PacketType.Value = Value(6) + val SetRank: PacketType.Value = Value(7) implicit val codec: Codec[Type] = PacketHelpers.createEnumerationCodec(this, uintL(3)) } @@ -255,18 +250,18 @@ object OutfitMembershipRequest extends Marshallable[OutfitMembershipRequest] { } implicit val codec: Codec[OutfitMembershipRequest] = ( - ("request_type" | RequestType.codec) >>:~ { request_type => + ("packet_type" | PacketType.codec) >>:~ { packet_type => ("outfit_id" | uint32L) :: - ("action" | selectFromType(request_type.id)) + ("action" | selectFromType(packet_type.id)) } ).xmap[OutfitMembershipRequest]( { - case request_type :: outfit_id :: action :: HNil => - OutfitMembershipRequest(request_type, outfit_id, action) + case _ :: outfit_id :: action :: HNil => + OutfitMembershipRequest(outfit_id, action) }, { - case OutfitMembershipRequest(request_type, outfit_id, action) => - request_type :: outfit_id :: action :: HNil + case OutfitMembershipRequest(outfit_id, action) => + OutfitMembershipRequest.PacketType(action.code) :: outfit_id :: action :: HNil } ) } diff --git a/src/main/scala/net/psforever/packet/game/OutfitMembershipResponse.scala b/src/main/scala/net/psforever/packet/game/OutfitMembershipResponse.scala index da9c27b5..1608cbf9 100644 --- a/src/main/scala/net/psforever/packet/game/OutfitMembershipResponse.scala +++ b/src/main/scala/net/psforever/packet/game/OutfitMembershipResponse.scala @@ -43,22 +43,22 @@ object OutfitMembershipResponse extends Marshallable[OutfitMembershipResponse] { } implicit val codec: Codec[OutfitMembershipResponse] = ( - ("response_type" | PacketType.codec) :: - ("unk0" | uintL(5)) :: - ("unk1" | uintL(3)) :: - ("outfit_id" | uint32L) :: - ("target_id" | uint32L) :: - ("str1" | PacketHelpers.encodedWideStringAligned(5)) :: - ("str2" | PacketHelpers.encodedWideString) :: - ("flag" | bool) - ).xmap[OutfitMembershipResponse]( + ("packet_type" | PacketType.codec) :: + ("unk0" | uintL(5)) :: + ("unk1" | uintL(3)) :: + ("outfit_id" | uint32L) :: + ("target_id" | uint32L) :: + ("str1" | PacketHelpers.encodedWideStringAligned(5)) :: + ("str2" | PacketHelpers.encodedWideString) :: + ("flag" | bool) + ).xmap[OutfitMembershipResponse]( { - case response_type :: u0 :: u1 :: outfit_id :: target_id :: str1 :: str2 :: flag :: HNil => - OutfitMembershipResponse(response_type, u0, u1, outfit_id, target_id, str1, str2, flag) + case packet_type :: u0 :: u1 :: outfit_id :: target_id :: str1 :: str2 :: flag :: HNil => + OutfitMembershipResponse(packet_type, u0, u1, outfit_id, target_id, str1, str2, flag) }, { - case OutfitMembershipResponse(response_type, u0, u1, outfit_id, target_id, str1, str2, flag) => - response_type :: u0 :: u1 :: outfit_id :: target_id :: str1 :: str2 :: flag :: HNil + case OutfitMembershipResponse(packet_type, u0, u1, outfit_id, target_id, str1, str2, flag) => + packet_type :: u0 :: u1 :: outfit_id :: target_id :: str1 :: str2 :: flag :: HNil } ) } diff --git a/src/main/scala/net/psforever/packet/game/OutfitRequest.scala b/src/main/scala/net/psforever/packet/game/OutfitRequest.scala index c62b1fda..7d6b4cd0 100644 --- a/src/main/scala/net/psforever/packet/game/OutfitRequest.scala +++ b/src/main/scala/net/psforever/packet/game/OutfitRequest.scala @@ -37,16 +37,19 @@ object OutfitRequestAction { * @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 @@ -142,14 +145,14 @@ object OutfitRequest extends Marshallable[OutfitRequest] { _ => Attempt.Failure(Err(s"can not encode $action-type info - no such thing")) ) - object RequestType extends Enumeration { + object PacketType 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) // sent by client if menu is either open (true) or closed (false) + val Motd: PacketType.Value = Value(0) + val Rank: PacketType.Value = Value(1) + val Unk2: PacketType.Value = Value(2) + val Detail: PacketType.Value = Value(3) + val List: PacketType.Value = Value(4) // sent by client if menu is either open (true) or closed (false) implicit val codec: Codec[Type] = PacketHelpers.createEnumerationCodec(this, uintL(3)) } @@ -169,18 +172,18 @@ object OutfitRequest extends Marshallable[OutfitRequest] { } implicit val codec: Codec[OutfitRequest] = ( - uint(bits = 3) >>:~ { code => + ("packet_type" | PacketType.codec) >>:~ { packet_type => ("id" | uint32L) :: - ("action" | selectFromType(code)) + ("action" | selectFromType(packet_type.id)).hlist } - ).xmap[OutfitRequest]( + ).xmap[OutfitRequest]( { case _ :: id:: action :: HNil => OutfitRequest(id, action) }, { case OutfitRequest(id, action) => - action.code :: id :: action :: HNil + OutfitRequest.PacketType(action.code) :: id :: action :: HNil } ) } diff --git a/src/test/scala/game/OutfitEventTest.scala b/src/test/scala/game/OutfitEventTest.scala index 2b3e2dd8..27039f04 100644 --- a/src/test/scala/game/OutfitEventTest.scala +++ b/src/test/scala/game/OutfitEventTest.scala @@ -2,13 +2,13 @@ package game import net.psforever.packet._ -import net.psforever.packet.game.OutfitEvent.RequestType +import net.psforever.packet.game.OutfitEvent import net.psforever.packet.game.OutfitEventAction._ -import net.psforever.packet.game._ 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 @@ -51,8 +51,7 @@ class OutfitEventTest extends Specification { "decode Unk0 ABC" in { PacketCoding.decodePacket(unk0_ABC).require match { - case OutfitEvent(request_type, outfit_guid, action) => - request_type mustEqual RequestType.Unk0 + case OutfitEvent(outfit_guid, action) => outfit_guid mustEqual 25044 action mustEqual Unk0( OutfitInfo( @@ -78,7 +77,6 @@ class OutfitEventTest extends Specification { "encode Unk0 ABC" in { val msg = OutfitEvent( - RequestType.Unk0, 25044, Unk0( OutfitInfo( @@ -105,8 +103,7 @@ class OutfitEventTest extends Specification { "decode Unk1 ABC" in { PacketCoding.decodePacket(unk1_ABC).require match { - case OutfitEvent(request_type, outfit_guid, action) => - request_type mustEqual RequestType.Unk1 + case OutfitEvent(outfit_guid, action) => outfit_guid mustEqual 529688L action mustEqual Unk1() case _ => @@ -116,7 +113,6 @@ class OutfitEventTest extends Specification { "encode Unk1 ABC" in { val msg = OutfitEvent( - RequestType.Unk1, 529688L, Unk1() ) @@ -127,8 +123,7 @@ class OutfitEventTest extends Specification { "decode Unk2 ABC" in { PacketCoding.decodePacket(unk2_ABC).require match { - case OutfitEvent(request_type, outfit_guid, action) => - request_type mustEqual RequestType.Unk2 + case OutfitEvent(outfit_guid, action) => outfit_guid mustEqual 2147418113L action mustEqual Unk2(OutfitInfo( outfit_name = "PlanetSide_Forever_Vanu", @@ -152,7 +147,6 @@ class OutfitEventTest extends Specification { "encode Unk2 ABC" in { val msg = OutfitEvent( - RequestType.Unk2, 2147418113L, Unk2( OutfitInfo( @@ -179,8 +173,7 @@ class OutfitEventTest extends Specification { "decode Unk3 ABC" in { PacketCoding.decodePacket(unk3_ABC).require match { - case OutfitEvent(request_type, outfit_guid, action) => - request_type mustEqual RequestType.Unk3 + case OutfitEvent(outfit_guid, action) => outfit_guid mustEqual 2147418113L action mustEqual Unk3() case _ => @@ -190,7 +183,6 @@ class OutfitEventTest extends Specification { "encode Unk3 ABC" in { val msg = OutfitEvent( - RequestType.Unk3, 2147418113L, Unk3() ) @@ -201,8 +193,7 @@ class OutfitEventTest extends Specification { "decode Unk4 ABC" in { PacketCoding.decodePacket(unk4_ABC).require match { - case OutfitEvent(request_type, outfit_guid, action) => - request_type mustEqual RequestType.UpdateOutfitId + case OutfitEvent(outfit_guid, action) => outfit_guid mustEqual 2147418113L action mustEqual UpdateOutfitId( new_outfit_id = 529744L, @@ -214,7 +205,6 @@ class OutfitEventTest extends Specification { "encode Unk4 ABC" in { val msg = OutfitEvent( - RequestType.UpdateOutfitId, 2147418113L, UpdateOutfitId( new_outfit_id = 529744L, @@ -227,8 +217,7 @@ class OutfitEventTest extends Specification { "decode Unk5 ABC" in { PacketCoding.decodePacket(unk5_ABC).require match { - case OutfitEvent(request_type, outfit_guid, action) => - request_type mustEqual RequestType.Unk5 + case OutfitEvent(outfit_guid, action) => outfit_guid mustEqual 2147418113L action mustEqual Unk5( unk1 = 2, @@ -240,7 +229,6 @@ class OutfitEventTest extends Specification { "encode Unk5 ABC" in { val msg = OutfitEvent( - RequestType.Unk5, 2147418113L, Unk5( unk1 = 2, diff --git a/src/test/scala/game/OutfitListEventTest.scala b/src/test/scala/game/OutfitListEventTest.scala index c5713a55..79b5d037 100644 --- a/src/test/scala/game/OutfitListEventTest.scala +++ b/src/test/scala/game/OutfitListEventTest.scala @@ -3,12 +3,12 @@ package game import net.psforever.packet._ import net.psforever.packet.game.OutfitListEvent -import net.psforever.packet.game.OutfitListEvent.RequestType import net.psforever.packet.game.OutfitListEventAction.ListElementOutfit import org.specs2.mutable._ import scodec.bits.ByteVector class OutfitListEventTest extends Specification { + val unk2_0_ABC: ByteVector = ByteVector.fromValidHex("98 5 e83a0000 000e1800 0800000 11404e0069006700680074004c006f00720064007300 854e005900430061007400") val unk2_0_DEF: ByteVector = ByteVector.fromValidHex("98 4 ec281001 51a62800 3400000 11a0490052004f004e004600490053005400200043006c0061006e00 8654006f006c006a00") val unk2_1_ABC: ByteVector = ByteVector.fromValidHex("98 4 723c0000 2aa81e00 2200000 11006900470061006d00650073002d004500 906900670061006d006500730043005400460057006800610063006b002d004500") @@ -21,8 +21,7 @@ class OutfitListEventTest extends Specification { "decode unk0_ABC" in { PacketCoding.decodePacket(unk2_0_ABC).require match { - case OutfitListEvent(code, ListElementOutfit(unk1, points, members, outfit_name, outfit_leader)) => - code mustEqual OutfitListEvent.RequestType.ListElementOutfit + case OutfitListEvent(ListElementOutfit(unk1, points, members, outfit_name, outfit_leader)) => unk1 mustEqual 7668 points mustEqual 788224 members mustEqual 4 @@ -34,7 +33,15 @@ class OutfitListEventTest extends Specification { } "encode unk0_ABC" in { - val msg = OutfitListEvent(RequestType.ListElementOutfit, ListElementOutfit(7668, 788224, 4, "NightLords", "NYCat")) + val msg = OutfitListEvent( + ListElementOutfit( + 7668, + 788224, + 4, + "NightLords", + "NYCat" + ) + ) val pkt = PacketCoding.encodePacket(msg).require.toByteVector pkt mustEqual unk2_0_ABC diff --git a/src/test/scala/game/OutfitMemberEventTest.scala b/src/test/scala/game/OutfitMemberEventTest.scala index 726d0a78..510e7278 100644 --- a/src/test/scala/game/OutfitMemberEventTest.scala +++ b/src/test/scala/game/OutfitMemberEventTest.scala @@ -22,8 +22,7 @@ class OutfitMemberEventTest extends Specification { "decode Lazer padding" in { PacketCoding.decodePacket(Lazer).require match { - case OutfitMemberEvent(packet_type, outfit_id, member_id, Unk0(member_name, rank, points, last_login, action, padding)) => - packet_type mustEqual OutfitMemberEvent.PacketType.Unk0 + case OutfitMemberEvent(outfit_id, member_id, Unk0(member_name, rank, points, last_login, action, padding)) => outfit_id mustEqual 6418 member_id mustEqual 705344 member_name mustEqual "Lazer1982" @@ -39,14 +38,13 @@ class OutfitMemberEventTest extends Specification { "encode Lazer padding" in { val msg = OutfitMemberEvent( - packet_type = OutfitMemberEvent.PacketType.Unk0, outfit_id = 6418, member_id = 705344, Unk0( member_name = "Lazer1982", rank = 7, points = 3134113, - last_login = 156506, + last_online = 156506, action = OutfitMemberEventAction.PacketType.Padding, padding = 0 ) @@ -58,8 +56,7 @@ class OutfitMemberEventTest extends Specification { "decode OpolE padding" in { PacketCoding.decodePacket(OpolE).require match { - case OutfitMemberEvent(packet_type, outfit_id, member_id, Unk0(member_name, rank, points, last_login, action, unk0_padding)) => - packet_type mustEqual OutfitMemberEvent.PacketType.Unk0 + case OutfitMemberEvent(outfit_id, member_id, Unk0(member_name, rank, points, last_login, action, unk0_padding)) => outfit_id mustEqual 6418 member_id mustEqual 42644970 member_name mustEqual "OpolE" @@ -75,14 +72,13 @@ class OutfitMemberEventTest extends Specification { "encode OpolE padding" in { val msg = OutfitMemberEvent( - packet_type = OutfitMemberEvent.PacketType.Unk0, outfit_id = 6418, member_id = 42644970, Unk0( member_name = "OpolE", rank = 6, points = 461901, - last_login = 137576, + last_online = 137576, action = OutfitMemberEventAction.PacketType.Padding, padding = 0 ) @@ -95,8 +91,7 @@ class OutfitMemberEventTest extends Specification { "decode Unk1" in { PacketCoding.decodePacket(unk1).require match { - case OutfitMemberEvent(packet_type,outfit_id, member_id, Unk1()) => - packet_type mustEqual OutfitMemberEvent.PacketType.Unk1 + case OutfitMemberEvent(outfit_id, member_id, Unk1()) => outfit_id mustEqual 529744 member_id mustEqual 41605263 case _ => @@ -106,7 +101,6 @@ class OutfitMemberEventTest extends Specification { "encode Unk1" in { val msg = OutfitMemberEvent( - packet_type = OutfitMemberEvent.PacketType.Unk1, outfit_id = 529744, member_id = 41605263, Unk1() diff --git a/src/test/scala/game/OutfitMemberUpdateTest.scala b/src/test/scala/game/OutfitMemberUpdateTest.scala new file mode 100644 index 00000000..39e0ca3d --- /dev/null +++ b/src/test/scala/game/OutfitMemberUpdateTest.scala @@ -0,0 +1,51 @@ +// Copyright (c) 2023-2025 PSForever +package game + +import net.psforever.packet._ +import net.psforever.packet.game.OutfitMemberUpdate +import org.specs2.mutable._ +import scodec.bits._ + +class OutfitMemberUpdateTest extends Specification { + + val updateRankToOwnerOfOutfitInFormation = hex"91 0100ff7f15aa7a02f0" + val normalRankChange = hex"91 1219000086d9130090" + + "decode updateOwnerOfOutfitInFormation" in { + PacketCoding.decodePacket(updateRankToOwnerOfOutfitInFormation).require match { + case OutfitMemberUpdate(outfit_id, char_id, rank, flag) => + outfit_id mustEqual 2147418113 + char_id mustEqual 41593365 + rank mustEqual 7 + flag mustEqual true + case _ => + ko + } + } + + "encode updateOwnerOfOutfitInFormation" in { + val msg = OutfitMemberUpdate(outfit_id = 2147418113, char_id = 41593365, rank = 7, flag = true) + val pkt = PacketCoding.encodePacket(msg).require.toByteVector + + pkt mustEqual updateRankToOwnerOfOutfitInFormation + } + + "decode normalRankChange" in { + PacketCoding.decodePacket(normalRankChange).require match { + case OutfitMemberUpdate(outfit_id, char_id, rank, flag) => + outfit_id mustEqual 6418 + char_id mustEqual 1300870 + rank mustEqual 4 + flag mustEqual true + case _ => + ko + } + } + + "encode normalRankChange" in { + val msg = OutfitMemberUpdate(outfit_id = 6418, char_id = 1300870, rank = 4, flag = true) + val pkt = PacketCoding.encodePacket(msg).require.toByteVector + + pkt mustEqual normalRankChange + } +} diff --git a/src/test/scala/game/OutfitMembershipRequestTest.scala b/src/test/scala/game/OutfitMembershipRequestTest.scala index 0e8af6e4..b733c4d8 100644 --- a/src/test/scala/game/OutfitMembershipRequestTest.scala +++ b/src/test/scala/game/OutfitMembershipRequestTest.scala @@ -2,13 +2,13 @@ package game import net.psforever.packet._ -import net.psforever.packet.game._ -import net.psforever.packet.game.OutfitMembershipRequest.RequestType +import net.psforever.packet.game.OutfitMembershipRequest import net.psforever.packet.game.OutfitMembershipRequestAction._ import org.specs2.mutable._ import scodec.bits._ class OutfitMembershipRequestTest extends Specification { + val create_ABC = hex"8c 0 0200 000 1000 83 410042004300" val create_2222 = hex"8c 0 1000 000 1000 84 3200320032003200" val form_abc = hex"8c 2 0200 000 1000 83 610062006300" @@ -30,8 +30,8 @@ class OutfitMembershipRequestTest extends Specification { "decode CreateOutfit ABC" in { PacketCoding.decodePacket(create_ABC).require match { - case OutfitMembershipRequest(request_type, outfit_id, action) => - request_type mustEqual RequestType.Create + case OutfitMembershipRequest(outfit_id, action) => + outfit_id mustEqual 1 action mustEqual Create("", "ABC") case _ => @@ -40,7 +40,7 @@ class OutfitMembershipRequestTest extends Specification { } "encode CreateOutfit ABC" in { - val msg = OutfitMembershipRequest(RequestType.Create, 1, Create("", "ABC")) + val msg = OutfitMembershipRequest(1, Create("", "ABC")) val pkt = PacketCoding.encodePacket(msg).require.toByteVector pkt mustEqual create_ABC @@ -48,8 +48,7 @@ class OutfitMembershipRequestTest extends Specification { "decode CreateOutfit 2222" in { PacketCoding.decodePacket(create_2222).require match { - case OutfitMembershipRequest(request_type, outfit_id, action) => - request_type mustEqual RequestType.Create + case OutfitMembershipRequest(outfit_id, action) => outfit_id mustEqual 8 action mustEqual Create("", "2222") case _ => @@ -58,7 +57,7 @@ class OutfitMembershipRequestTest extends Specification { } "encode CreateOutfit 2222" in { - val msg = OutfitMembershipRequest(RequestType.Create, 8, Create("", "2222")) + val msg = OutfitMembershipRequest(8, Create("", "2222")) val pkt = PacketCoding.encodePacket(msg).require.toByteVector pkt mustEqual create_2222 @@ -66,8 +65,7 @@ class OutfitMembershipRequestTest extends Specification { "decode FormOutfit abc" in { PacketCoding.decodePacket(form_abc).require match { - case OutfitMembershipRequest(request_type, outfit_id, action) => - request_type mustEqual RequestType.Form + case OutfitMembershipRequest(outfit_id, action) => outfit_id mustEqual 1 action mustEqual Form("", "abc") case _ => @@ -76,7 +74,7 @@ class OutfitMembershipRequestTest extends Specification { } "encode FormOutfit abc" in { - val msg = OutfitMembershipRequest(RequestType.Form, 1, Form("", "abc")) + val msg = OutfitMembershipRequest(1, Form("", "abc")) val pkt = PacketCoding.encodePacket(msg).require.toByteVector pkt mustEqual form_abc @@ -84,8 +82,7 @@ class OutfitMembershipRequestTest extends Specification { "decode FormOutfit 1" in { PacketCoding.decodePacket(form_1).require match { - case OutfitMembershipRequest(request_type, outfit_id, action) => - request_type mustEqual RequestType.Form + case OutfitMembershipRequest(outfit_id, action) => outfit_id mustEqual 8 action mustEqual Form("", "1") case _ => @@ -94,7 +91,7 @@ class OutfitMembershipRequestTest extends Specification { } "encode FormOutfit 1" in { - val msg = OutfitMembershipRequest(RequestType.Form, 8, Form("", "1")) + val msg = OutfitMembershipRequest(8, Form("", "1")) val pkt = PacketCoding.encodePacket(msg).require.toByteVector pkt mustEqual form_1 @@ -102,8 +99,7 @@ class OutfitMembershipRequestTest extends Specification { "decode Invite" in { PacketCoding.decodePacket(invite_old).require match { - case OutfitMembershipRequest(request_type, outfit_id, action) => - request_type mustEqual RequestType.Invite + case OutfitMembershipRequest(outfit_id, action) => outfit_id mustEqual 30383325L action mustEqual Invite(0, "virusgiver") case _ => @@ -112,7 +108,7 @@ class OutfitMembershipRequestTest extends Specification { } "encode Invite" in { - val msg = OutfitMembershipRequest(RequestType.Invite, 30383325L, Invite(0, "virusgiver")) + val msg = OutfitMembershipRequest(30383325L, Invite(0, "virusgiver")) val pkt = PacketCoding.encodePacket(msg).require.toByteVector pkt mustEqual invite_old @@ -120,8 +116,7 @@ class OutfitMembershipRequestTest extends Specification { "decode AcceptOutfitInvite 1" in { PacketCoding.decodePacket(accept_1).require match { - case OutfitMembershipRequest(request_type, outfit_id, action) => - request_type mustEqual RequestType.Accept + case OutfitMembershipRequest(outfit_id, action) => outfit_id mustEqual 1 action mustEqual AcceptInvite("") case _ => @@ -130,7 +125,7 @@ class OutfitMembershipRequestTest extends Specification { } "encode AcceptOutfitInvite 1" in { - val msg = OutfitMembershipRequest(RequestType.Accept, 1, AcceptInvite("")) + val msg = OutfitMembershipRequest(1, AcceptInvite("")) val pkt = PacketCoding.encodePacket(msg).require.toByteVector pkt mustEqual accept_1 @@ -138,8 +133,7 @@ class OutfitMembershipRequestTest extends Specification { "decode AcceptOutfitInvite 2" in { PacketCoding.decodePacket(accept_2).require match { - case OutfitMembershipRequest(request_type, outfit_id, action) => - request_type mustEqual RequestType.Accept + case OutfitMembershipRequest(outfit_id, action) => outfit_id mustEqual 2 action mustEqual AcceptInvite("") case _ => @@ -148,7 +142,7 @@ class OutfitMembershipRequestTest extends Specification { } "encode AcceptOutfitInvite 2" in { - val msg = OutfitMembershipRequest(RequestType.Accept, 2, AcceptInvite("")) + val msg = OutfitMembershipRequest(2, AcceptInvite("")) val pkt = PacketCoding.encodePacket(msg).require.toByteVector pkt mustEqual accept_2 @@ -156,8 +150,7 @@ class OutfitMembershipRequestTest extends Specification { "decode RejectOutfitInvite 1" in { PacketCoding.decodePacket(reject_1).require match { - case OutfitMembershipRequest(request_type, outfit_id, action) => - request_type mustEqual RequestType.Reject + case OutfitMembershipRequest(outfit_id, action) => outfit_id mustEqual 1 action mustEqual RejectInvite("") case _ => @@ -166,7 +159,7 @@ class OutfitMembershipRequestTest extends Specification { } "encode RejectOutfitInvite 1" in { - val msg = OutfitMembershipRequest(RequestType.Reject, 1, RejectInvite("")) + val msg = OutfitMembershipRequest(1, RejectInvite("")) val pkt = PacketCoding.encodePacket(msg).require.toByteVector pkt mustEqual reject_1 @@ -174,8 +167,7 @@ class OutfitMembershipRequestTest extends Specification { "decode RejectOutfitInvite 2" in { PacketCoding.decodePacket(reject_2).require match { - case OutfitMembershipRequest(request_type, outfit_id, action) => - request_type mustEqual RequestType.Reject + case OutfitMembershipRequest(outfit_id, action) => outfit_id mustEqual 2 action mustEqual RejectInvite("") case _ => @@ -184,7 +176,7 @@ class OutfitMembershipRequestTest extends Specification { } "encode RejectOutfitInvite 2" in { - val msg = OutfitMembershipRequest(RequestType.Reject, 2, RejectInvite("")) + val msg = OutfitMembershipRequest(2, RejectInvite("")) val pkt = PacketCoding.encodePacket(msg).require.toByteVector pkt mustEqual reject_2 @@ -192,8 +184,7 @@ class OutfitMembershipRequestTest extends Specification { "decode CancelOutfitInvite 3" in { PacketCoding.decodePacket(cancel_3).require match { - case OutfitMembershipRequest(request_type, outfit_id, action) => - request_type mustEqual RequestType.Cancel + case OutfitMembershipRequest(outfit_id, action) => outfit_id mustEqual 3 action mustEqual CancelInvite(0, "") case _ => @@ -202,7 +193,7 @@ class OutfitMembershipRequestTest extends Specification { } "encode CancelOutfitInvite 3" in { - val msg = OutfitMembershipRequest(RequestType.Cancel, 3, CancelInvite(0, "")) + val msg = OutfitMembershipRequest(3, CancelInvite(0, "")) val pkt = PacketCoding.encodePacket(msg).require.toByteVector pkt mustEqual cancel_3 @@ -210,8 +201,7 @@ class OutfitMembershipRequestTest extends Specification { "decode CancelOutfitInvite 1 abc" in { PacketCoding.decodePacket(cancel_1_abc).require match { - case OutfitMembershipRequest(request_type, outfit_id, action) => - request_type mustEqual RequestType.Cancel + case OutfitMembershipRequest(outfit_id, action) => outfit_id mustEqual 1 action mustEqual CancelInvite(0, "abc") case _ => @@ -220,7 +210,7 @@ class OutfitMembershipRequestTest extends Specification { } "encode CancelOutfitInvite 1 abc" in { - val msg = OutfitMembershipRequest(RequestType.Cancel, 1, CancelInvite(0, "abc")) + val msg = OutfitMembershipRequest(1, CancelInvite(0, "abc")) val pkt = PacketCoding.encodePacket(msg).require.toByteVector pkt mustEqual cancel_1_abc @@ -228,8 +218,7 @@ class OutfitMembershipRequestTest extends Specification { "decode CancelOutfitInvite 3 def" in { PacketCoding.decodePacket(cancel_3_def).require match { - case OutfitMembershipRequest(request_type, outfit_id, action) => - request_type mustEqual RequestType.Cancel + case OutfitMembershipRequest(outfit_id, action) => outfit_id mustEqual 3 action mustEqual CancelInvite(0, "def") case _ => @@ -238,7 +227,7 @@ class OutfitMembershipRequestTest extends Specification { } "encode CancelOutfitInvite 3 def" in { - val msg = OutfitMembershipRequest(RequestType.Cancel, 3, CancelInvite(0, "def")) + val msg = OutfitMembershipRequest(3, CancelInvite(0, "def")) val pkt = PacketCoding.encodePacket(msg).require.toByteVector pkt mustEqual cancel_3_def @@ -248,8 +237,7 @@ class OutfitMembershipRequestTest extends Specification { "decode invite" in { PacketCoding.decodePacket(invite).require match { - case OutfitMembershipRequest(request_type, outfit_id, Invite(unk1, member_name)) => - request_type mustEqual RequestType.Invite + case OutfitMembershipRequest(outfit_id, Invite(unk1, member_name)) => outfit_id mustEqual 1 unk1 mustEqual 0 member_name mustEqual "inviteTest1" @@ -259,7 +247,7 @@ class OutfitMembershipRequestTest extends Specification { } "encode invite" in { - val msg = OutfitMembershipRequest(RequestType.Invite, 1, Invite(0, "inviteTest1")) + val msg = OutfitMembershipRequest(1, Invite(0, "inviteTest1")) val pkt = PacketCoding.encodePacket(msg).require.toByteVector pkt mustEqual invite @@ -268,8 +256,7 @@ class OutfitMembershipRequestTest extends Specification { "decode kick" in { PacketCoding.decodePacket(kick).require match { - case OutfitMembershipRequest(request_type, outfit_id, Kick(avatar_id, member_name)) => - request_type mustEqual RequestType.Kick + case OutfitMembershipRequest(outfit_id, Kick(avatar_id, member_name)) => outfit_id mustEqual 1 avatar_id mustEqual 41575613 member_name mustEqual "" @@ -279,7 +266,7 @@ class OutfitMembershipRequestTest extends Specification { } "encode kick" in { - val msg = OutfitMembershipRequest(RequestType.Kick, 1, Kick(41575613, "")) + val msg = OutfitMembershipRequest(1, Kick(41575613, "")) val pkt = PacketCoding.encodePacket(msg).require.toByteVector pkt mustEqual kick @@ -287,8 +274,7 @@ class OutfitMembershipRequestTest extends Specification { "decode setrank" in { PacketCoding.decodePacket(setrank).require match { - case OutfitMembershipRequest(request_type, outfit_id, SetRank(avatar_id, rank, member_name)) => - request_type mustEqual RequestType.SetRank + case OutfitMembershipRequest(outfit_id, SetRank(avatar_id, rank, member_name)) => outfit_id mustEqual 1 avatar_id mustEqual 41575613 rank mustEqual 1 @@ -299,7 +285,7 @@ class OutfitMembershipRequestTest extends Specification { } "encode setrank" in { - val msg = OutfitMembershipRequest(RequestType.SetRank, 1, SetRank(41575613, 1, "")) + val msg = OutfitMembershipRequest(1, SetRank(41575613, 1, "")) val pkt = PacketCoding.encodePacket(msg).require.toByteVector pkt mustEqual setrank diff --git a/src/test/scala/game/OutfitMembershipResponseTest.scala b/src/test/scala/game/OutfitMembershipResponseTest.scala index 7646a96e..bd16286a 100644 --- a/src/test/scala/game/OutfitMembershipResponseTest.scala +++ b/src/test/scala/game/OutfitMembershipResponseTest.scala @@ -2,8 +2,8 @@ package game import net.psforever.packet._ +import net.psforever.packet.game.OutfitMembershipResponse import net.psforever.packet.game.OutfitMembershipResponse.PacketType -import net.psforever.packet.game._ import org.specs2.mutable._ import scodec.bits._ diff --git a/src/test/scala/game/OutfitRequesTest.scala b/src/test/scala/game/OutfitRequestTest.scala similarity index 92% rename from src/test/scala/game/OutfitRequesTest.scala rename to src/test/scala/game/OutfitRequestTest.scala index 0928b3f5..4c4adcb1 100644 --- a/src/test/scala/game/OutfitRequesTest.scala +++ b/src/test/scala/game/OutfitRequestTest.scala @@ -3,17 +3,18 @@ package game import org.specs2.mutable._ import net.psforever.packet._ -import net.psforever.packet.game._ +import net.psforever.packet.game.{OutfitRequest, OutfitRequestAction} import scodec.bits._ class OutfitRequestTest extends Specification { + 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 { + "decode Motd" in { PacketCoding.decodePacket(setMotd).require match { case OutfitRequest(id, OutfitRequestAction.Motd(str)) => id mustEqual 41593365L @@ -23,7 +24,7 @@ class OutfitRequestTest extends Specification { } } - "decode 1" in { + "decode Ranks" in { PacketCoding.decodePacket(setRanks).require match { case OutfitRequest(id, OutfitRequestAction.Ranks(list)) => id mustEqual 41593365L @@ -33,7 +34,7 @@ class OutfitRequestTest extends Specification { } } - "decode 2 (fake)" in { + "decode Unk2 (fake)" in { PacketCoding.decodePacket(string4).require match { case OutfitRequest(id, OutfitRequestAction.Unk2(value)) => id mustEqual 41593365L @@ -43,7 +44,7 @@ class OutfitRequestTest extends Specification { } } - "decode 3" in { + "decode Unk3" in { PacketCoding.decodePacket(string6).require match { case OutfitRequest(id, OutfitRequestAction.Unk3(value)) => id mustEqual 1176612L @@ -53,7 +54,7 @@ class OutfitRequestTest extends Specification { } } - "decode 4" in { + "decode Unk4" in { PacketCoding.decodePacket(string8).require match { case OutfitRequest(id, OutfitRequestAction.Unk4(value)) => id mustEqual 41588237L @@ -63,7 +64,7 @@ class OutfitRequestTest extends Specification { } } - "encode 0" in { + "encode Motd" in { val msg = OutfitRequest(41593365L, OutfitRequestAction.Motd( "Vanu outfit for the planetside forever project! -find out more about the PSEMU project at PSforever.net" )) @@ -72,28 +73,28 @@ class OutfitRequestTest extends Specification { pkt mustEqual setMotd } - "encode 1" in { + "encode Ranks" in { val msg = OutfitRequest(41593365L, OutfitRequestAction.Ranks(List(Some(""), Some(""), Some(""), Some(""), Some(""), Some(""), Some(""), Some("")))) val pkt = PacketCoding.encodePacket(msg).require.toByteVector pkt mustEqual setRanks } - "encode 2 (fake)" in { + "encode Unk2 (fake)" in { val msg = OutfitRequest(41593365L, OutfitRequestAction.Unk2(85)) val pkt = PacketCoding.encodePacket(msg).require.toByteVector pkt mustEqual string4 } - "encode 3" in { + "encode Unk3" in { val msg = OutfitRequest(1176612L, OutfitRequestAction.Unk3(true)) val pkt = PacketCoding.encodePacket(msg).require.toByteVector pkt mustEqual string6 } - "encode 4" in { + "encode Unk4" in { val msg = OutfitRequest(41588237L, OutfitRequestAction.Unk4(true)) val pkt = PacketCoding.encodePacket(msg).require.toByteVector From 57b3fd69abc8116264c7086672ad81174fd86e24 Mon Sep 17 00:00:00 2001 From: Resaec Date: Thu, 28 Aug 2025 00:16:53 +0200 Subject: [PATCH 17/22] add Outfit DB structures --- .../db/migration/V015__OutfitStructure.sql | 107 ++++++++++++++++++ 1 file changed, 107 insertions(+) create mode 100644 server/src/main/resources/db/migration/V015__OutfitStructure.sql diff --git a/server/src/main/resources/db/migration/V015__OutfitStructure.sql b/server/src/main/resources/db/migration/V015__OutfitStructure.sql new file mode 100644 index 00000000..f44ee4af --- /dev/null +++ b/server/src/main/resources/db/migration/V015__OutfitStructure.sql @@ -0,0 +1,107 @@ +-- Tables for Outfits +-- +-- Includes Outfit, OutfitMember and OutfitPoints + +/* + +This migration allows for the storage of all outfit relevant data. + +Outfit +- each outfit has one entry in the outfit table +- each name is unique and ranks are inlined (static 1:n, join unnecessary) +- faction is limited to 0,1,2,3 +- decal is limited to 0 through 26 (inclusive) + +OutfitMember +- each avatar can at most be a member in one outfit +- each outfit can only have one rank 7 (leader) member +- rank is limited to 0 through 7 (inclusive) +- there is a "quick access" index on outfit and avatar for rank 7 (leader) + +OutfitPoint +- each (outfit, avatar) combination can only exist once +- a (outfit, NULL) combination can not be limited yet (not a big deal) +- deleting a avatar will have his points remain as (outfit, NULL) +- leaving an outfit will have the points remain as (outfit, NULL) + +*/ + +-- OUTFIT + +CREATE TABLE outfit ( + "id" SERIAL PRIMARY KEY, + "created" TIMESTAMP NOT NULL DEFAULT CURRENT_TIMESTAMP, + "deleted" BOOLEAN NOT NULL DEFAULT FALSE, -- allow for recovery of accidentially deleted outfits + "faction" SMALLINT NOT NULL, + "owner_id" INTEGER NOT NULL, + "decal" SMALLINT NOT NULL DEFAULT 0, + "name" VARCHAR(32) NOT NULL, + "motd" VARCHAR(255) NULL, + "rank0" VARCHAR(32) NULL, -- Non-Officer Rank 1 + "rank1" VARCHAR(32) NULL, + "rank2" VARCHAR(32) NULL, + "rank3" VARCHAR(32) NULL, -- Non-Officer Rank 4 + "rank4" VARCHAR(32) NULL, -- Fourth In Command + "rank5" VARCHAR(32) NULL, + "rank6" VARCHAR(32) NULL, -- Second In Command + "rank7" VARCHAR(32) NULL, -- Outfit Leader + + CONSTRAINT "outfit_faction_check" CHECK("faction" BETWEEN 0 AND 3), -- allowed faction IDs + CONSTRAINT "outfit_decal_check" CHECK("decal" BETWEEN 0 AND 26), -- allowed decal IDs + + CONSTRAINT "outfit_owner_id_avatar_id_fkey" FOREIGN KEY ("owner_id") REFERENCES avatar ("id") +); + +CREATE INDEX "outfit_created_brin_idx" ON "outfit" USING BRIN ("created"); -- super small, index for physically sequential data +CREATE INDEX "outfit_faction_deleted_idx" ON "outfit" ("faction", "deleted"); -- optimize index for search: SELECT * FROM "outfit" WHERE "faction" = ? AND "deleted" = false; + +-- OUTFITMEMBER + +CREATE TABLE outfitmember ( + "id" BIGSERIAL PRIMARY KEY, + "created" TIMESTAMP NOT NULL DEFAULT CURRENT_TIMESTAMP, + "outfit_id" INTEGER NOT NULL, + "avatar_id" INTEGER NOT NULL, + "rank" SMALLINT NOT NULL DEFAULT 0, -- lowest rank + + CONSTRAINT "outfitmember_rank_check" CHECK("rank" BETWEEN 0 AND 7), -- allowed ranks + + CONSTRAINT "outfitmember_outfit_id_outfit_id_fkey" FOREIGN KEY ("outfit_id") REFERENCES outfit ("id") ON DELETE CASCADE, + CONSTRAINT "outfitmember_avatar_id_avatar_id_fkey" FOREIGN KEY ("avatar_id") REFERENCES avatar ("id") ON DELETE RESTRICT +); + +CREATE INDEX "outfitmember_outfit_id_idx" ON "outfitmember" ("outfit_id"); -- FK index +CREATE UNIQUE INDEX "outfitmember_avatar_id_unique" ON "outfitmember" ("avatar_id"); -- FK index, enforce one outfit per avatar +CREATE UNIQUE INDEX "outfitmember_outfit_id_rank_partial_leader_unique" ON "outfitmember" ("outfit_id", "rank") WHERE "rank" = 7; -- quick access to outfit leader and ony one leader per outfit + +-- OUTFITPOINT + +CREATE TABLE outfitpoint ( + "id" BIGSERIAL PRIMARY KEY, + "outfit_id" INTEGER NOT NULL, + "avatar_id" INTEGER NULL, + "points" INTEGER NOT NULL DEFAULT 0, + + CONSTRAINT "outfitpoint_points_check" CHECK ("points" >= 0), + + -- enforce unique combinations (left side index) + CONSTRAINT "outfitpoint_outfit_avatar_unique_idx" UNIQUE ("outfit_id", "avatar_id"), -- UNIQUE NULLS NOT DISTINCT + + CONSTRAINT "outfitpoint_outfit_fkey" FOREIGN KEY ("outfit_id") REFERENCES outfit ("id") ON DELETE CASCADE, -- delete points of outfit when outfit is deleted + CONSTRAINT "outfitpoint_avatar_fkey" FOREIGN KEY ("avatar_id") REFERENCES avatar ("id") ON DELETE SET NULL -- keep points for outfit when player is deleted +); + +-- add right side index (avatar_id) +CREATE INDEX "outfitpoint_avatar_idx" ON "outfitpoint" ("avatar_id"); + +-- MATERIALIZED VIEW for OUTFITPOINT + +CREATE MATERIALIZED VIEW outfitpoint_mv AS + SELECT + "outfit_id", + SUM("points") as "points" + FROM + "outfitpoint" + GROUP BY "outfit_id"; + +CREATE INDEX "outfitpoint_mv_outfit_id_idx" ON "outfitpoint_mv" ("outfit_id"); From 402259e33824ccc5ae69b790a2df7febb2667ab8 Mon Sep 17 00:00:00 2001 From: ScrawnyRonnie Date: Thu, 28 Aug 2025 21:06:19 -0400 Subject: [PATCH 18/22] outfit checkpoint --- .../actors/session/SessionActor.scala | 11 +- .../actors/session/csr/GeneralLogic.scala | 17 +- .../actors/session/normal/ChatLogic.scala | 7 +- .../actors/session/normal/GeneralLogic.scala | 55 ++- .../session/spectator/GeneralLogic.scala | 8 +- .../session/support/ChatOperations.scala | 10 +- .../session/support/GeneralOperations.scala | 6 + .../session/support/OutfitInvites.scala | 34 ++ .../support/SessionOutfitHandlers.scala | 390 ++++++++++++++++++ .../scala/net/psforever/objects/Player.scala | 2 + .../converter/AvatarConverter.scala | 4 +- .../game/OutfitMembershipResponse.scala | 4 +- .../psforever/services/chat/ChatChannel.scala | 2 + .../psforever/services/chat/ChatService.scala | 4 + 14 files changed, 541 insertions(+), 13 deletions(-) create mode 100644 src/main/scala/net/psforever/actors/session/support/OutfitInvites.scala create mode 100644 src/main/scala/net/psforever/actors/session/support/SessionOutfitHandlers.scala diff --git a/src/main/scala/net/psforever/actors/session/SessionActor.scala b/src/main/scala/net/psforever/actors/session/SessionActor.scala index 25b5c377..21093f14 100644 --- a/src/main/scala/net/psforever/actors/session/SessionActor.scala +++ b/src/main/scala/net/psforever/actors/session/SessionActor.scala @@ -12,7 +12,7 @@ import net.psforever.objects.serverobject.mount.Mountable import net.psforever.objects.serverobject.terminals.{ProximityUnit, Terminal} import net.psforever.objects.zones.Zone import net.psforever.packet.PlanetSideGamePacket -import net.psforever.packet.game.{AIDamage, ActionCancelMessage, AvatarFirstTimeEventMessage, AvatarGrenadeStateMessage, AvatarImplantMessage, AvatarJumpMessage, BattleplanMessage, BeginZoningMessage, BindPlayerMessage, BugReportMessage, ChangeAmmoMessage, ChangeFireModeMessage, ChangeFireStateMessage_Start, ChangeFireStateMessage_Stop, ChangeShortcutBankMessage, CharacterCreateRequestMessage, CharacterRequestMessage, ChatMsg, ChildObjectStateMessage, ConnectToWorldRequestMessage, CreateShortcutMessage, DeployObjectMessage, DeployRequestMessage, DismountVehicleCargoMsg, DismountVehicleMsg, DisplayedAwardMessage, DropItemMessage, DroppodLaunchRequestMessage, EmoteMsg, FacilityBenefitShieldChargeRequestMessage, FavoritesRequest, FrameVehicleStateMessage, FriendsRequest, GenericActionMessage, GenericCollisionMsg, GenericObjectActionAtPositionMessage, GenericObjectActionMessage, GenericObjectStateMsg, HitHint, HitMessage, InvalidTerrainMessage, ItemTransactionMessage, LashMessage, LongRangeProjectileInfoMessage, LootItemMessage, MountVehicleCargoMsg, MountVehicleMsg, MoveItemMessage, ObjectDetectedMessage, ObjectHeldMessage, OutfitRequest, PickupItemMessage, PlanetsideAttributeMessage, PlayerStateMessageUpstream, ProjectileStateMessage, ProximityTerminalUseMessage, ReleaseAvatarRequestMessage, ReloadMessage, RequestDestroyMessage, SetChatFilterMessage, SpawnRequestMessage, SplashHitMessage, SquadDefinitionActionMessage, SquadMembershipRequest, SquadWaypointRequest, TargetingImplantRequest, TradeMessage, UnuseItemMessage, UplinkRequest, UseItemMessage, VehicleStateMessage, VehicleSubStateMessage, VoiceHostInfo, VoiceHostRequest, WarpgateRequest, WeaponDelayFireMessage, WeaponDryFireMessage, WeaponFireMessage, WeaponLazeTargetPositionMessage, ZipLineMessage} +import net.psforever.packet.game.{AIDamage, ActionCancelMessage, AvatarFirstTimeEventMessage, AvatarGrenadeStateMessage, AvatarImplantMessage, AvatarJumpMessage, BattleplanMessage, BeginZoningMessage, BindPlayerMessage, BugReportMessage, ChangeAmmoMessage, ChangeFireModeMessage, ChangeFireStateMessage_Start, ChangeFireStateMessage_Stop, ChangeShortcutBankMessage, CharacterCreateRequestMessage, CharacterRequestMessage, ChatMsg, ChildObjectStateMessage, ConnectToWorldRequestMessage, CreateShortcutMessage, DeployObjectMessage, DeployRequestMessage, DismountVehicleCargoMsg, DismountVehicleMsg, DisplayedAwardMessage, DropItemMessage, DroppodLaunchRequestMessage, EmoteMsg, FacilityBenefitShieldChargeRequestMessage, FavoritesRequest, FrameVehicleStateMessage, FriendsRequest, GenericActionMessage, GenericCollisionMsg, GenericObjectActionAtPositionMessage, GenericObjectActionMessage, GenericObjectStateMsg, HitHint, HitMessage, InvalidTerrainMessage, ItemTransactionMessage, LashMessage, LongRangeProjectileInfoMessage, LootItemMessage, MountVehicleCargoMsg, MountVehicleMsg, MoveItemMessage, ObjectDetectedMessage, ObjectHeldMessage, OutfitMembershipRequest, OutfitMembershipResponse, OutfitRequest, PickupItemMessage, PlanetsideAttributeMessage, PlayerStateMessageUpstream, ProjectileStateMessage, ProximityTerminalUseMessage, ReleaseAvatarRequestMessage, ReloadMessage, RequestDestroyMessage, SetChatFilterMessage, SpawnRequestMessage, SplashHitMessage, SquadDefinitionActionMessage, SquadMembershipRequest, SquadWaypointRequest, TargetingImplantRequest, TradeMessage, UnuseItemMessage, UplinkRequest, UseItemMessage, VehicleStateMessage, VehicleSubStateMessage, VoiceHostInfo, VoiceHostRequest, WarpgateRequest, WeaponDelayFireMessage, WeaponDryFireMessage, WeaponFireMessage, WeaponLazeTargetPositionMessage, ZipLineMessage} import net.psforever.services.{InterstellarClusterService => ICS} import net.psforever.services.CavernRotationService import net.psforever.services.CavernRotationService.SendCavernRotationUpdates @@ -610,7 +610,14 @@ class SessionActor(middlewareActor: typed.ActorRef[MiddlewareActor.Command], con case packet: HitHint => logic.general.handleHitHint(packet) - case _: OutfitRequest => () + case packet: OutfitRequest => + logic.general.handleOutfitRequest(packet) + + case packet: OutfitMembershipRequest => + logic.general.handleOutfitMembershipRequest(packet) + + case packet: OutfitMembershipResponse => + logic.general.handleOutfitMembershipResponse(packet) case pkt => data.log.warn(s"Unhandled GamePacket $pkt") diff --git a/src/main/scala/net/psforever/actors/session/csr/GeneralLogic.scala b/src/main/scala/net/psforever/actors/session/csr/GeneralLogic.scala index 2cd1782e..f32bdef9 100644 --- a/src/main/scala/net/psforever/actors/session/csr/GeneralLogic.scala +++ b/src/main/scala/net/psforever/actors/session/csr/GeneralLogic.scala @@ -30,11 +30,13 @@ import net.psforever.objects.vehicles.Utility import net.psforever.objects.vital.Vitality import net.psforever.objects.zones.{ZoneProjectile, Zoning} import net.psforever.packet.PlanetSideGamePacket -import net.psforever.packet.game.{ActionCancelMessage, AvatarFirstTimeEventMessage, AvatarImplantMessage, AvatarJumpMessage, BattleplanMessage, BindPlayerMessage, BugReportMessage, ChangeFireModeMessage, ChangeShortcutBankMessage, CharacterCreateRequestMessage, CharacterRequestMessage, ChatMsg, CollisionIs, ConnectToWorldRequestMessage, CreateShortcutMessage, DeadState, DeployObjectMessage, DisplayedAwardMessage, DropItemMessage, EmoteMsg, FacilityBenefitShieldChargeRequestMessage, FriendsRequest, GenericAction, GenericActionMessage, GenericCollisionMsg, GenericObjectActionAtPositionMessage, GenericObjectActionMessage, GenericObjectStateMsg, HitHint, InvalidTerrainMessage, LootItemMessage, MoveItemMessage, ObjectDetectedMessage, ObjectHeldMessage, PickupItemMessage, PlanetsideAttributeMessage, PlayerStateMessageUpstream, RequestDestroyMessage, TargetingImplantRequest, TerrainCondition, TradeMessage, UnuseItemMessage, UseItemMessage, VoiceHostInfo, VoiceHostRequest, ZipLineMessage} +import net.psforever.packet.game.OutfitEventAction.{OutfitInfo, OutfitRankNames, Unk0, Unk1} +import net.psforever.packet.game.{ActionCancelMessage, AvatarFirstTimeEventMessage, AvatarImplantMessage, AvatarJumpMessage, BattleplanMessage, BindPlayerMessage, BugReportMessage, ChangeFireModeMessage, ChangeShortcutBankMessage, CharacterCreateRequestMessage, CharacterRequestMessage, ChatMsg, CollisionIs, ConnectToWorldRequestMessage, CreateShortcutMessage, DeadState, DeployObjectMessage, DisplayedAwardMessage, DropItemMessage, EmoteMsg, FacilityBenefitShieldChargeRequestMessage, FriendsRequest, GenericAction, GenericActionMessage, GenericCollisionMsg, GenericObjectActionAtPositionMessage, GenericObjectActionMessage, GenericObjectStateMsg, HitHint, InvalidTerrainMessage, LootItemMessage, MoveItemMessage, ObjectDetectedMessage, ObjectHeldMessage, OutfitEvent, OutfitMemberEvent, OutfitMembershipRequest, OutfitMembershipResponse, OutfitRequest, OutfitRequestAction, PickupItemMessage, PlanetsideAttributeMessage, PlayerStateMessageUpstream, RequestDestroyMessage, TargetingImplantRequest, TerrainCondition, TradeMessage, UnuseItemMessage, UseItemMessage, VoiceHostInfo, VoiceHostRequest, ZipLineMessage} import net.psforever.services.RemoverActor import net.psforever.services.avatar.{AvatarAction, AvatarServiceMessage} import net.psforever.services.local.{LocalAction, LocalServiceMessage} import net.psforever.types.{CapacitorStateType, ChatMessageType, Cosmetic, ExoSuitType, PlanetSideEmpire, PlanetSideGUID, Vector3} +import scodec.bits.ByteVector import scala.util.Success @@ -665,6 +667,19 @@ class GeneralLogic(val ops: GeneralOperations, implicit val context: ActorContex val HitHint(_, _) = pkt } + def handleOutfitMembershipRequest(pkt: OutfitMembershipRequest): Unit = {} + + def handleOutfitMembershipResponse(pkt: OutfitMembershipResponse): Unit = {} + + def handleOutfitRequest(pkt: OutfitRequest): Unit = { + pkt match { + case OutfitRequest(_, OutfitRequestAction.Unk3(true)) => + + case OutfitRequest(_, OutfitRequestAction.Unk3(false)) => + + case _ => + } + } /* messages */ def handleRenewCharSavedTimer(): Unit = { /* */ } diff --git a/src/main/scala/net/psforever/actors/session/normal/ChatLogic.scala b/src/main/scala/net/psforever/actors/session/normal/ChatLogic.scala index 0d0cd12d..55206318 100644 --- a/src/main/scala/net/psforever/actors/session/normal/ChatLogic.scala +++ b/src/main/scala/net/psforever/actors/session/normal/ChatLogic.scala @@ -7,7 +7,7 @@ import net.psforever.actors.session.spectator.SpectatorMode import net.psforever.actors.session.support.{ChatFunctions, ChatOperations, SessionData} import net.psforever.objects.Session import net.psforever.packet.game.{ChatMsg, ServerType, SetChatFilterMessage} -import net.psforever.services.chat.{DefaultChannel, SquadChannel} +import net.psforever.services.chat.{DefaultChannel, OutfitChannel, SquadChannel} import net.psforever.types.ChatMessageType import net.psforever.types.ChatMessageType.{CMT_TOGGLESPECTATORMODE, CMT_TOGGLE_GM} import net.psforever.util.Config @@ -79,6 +79,9 @@ class ChatLogic(val ops: ChatOperations, implicit val context: ActorContext) ext case (CMT_SQUAD, _, _) => ops.commandSquad(session, message, SquadChannel(sessionLogic.squad.squad_guid)) + case (CMT_OUTFIT, _, _) => + ops.commandOutfit(session, message, OutfitChannel(sessionLogic.player.outfit_id)) + case (CMT_WHO | CMT_WHO_CSR | CMT_WHO_CR | CMT_WHO_PLATOONLEADERS | CMT_WHO_SQUADLEADERS | CMT_WHO_TEAMS, _, _) => ops.commandWho(session) @@ -100,7 +103,7 @@ class ChatLogic(val ops: ChatOperations, implicit val context: ActorContext) ext def handleIncomingMessage(message: ChatMsg, fromSession: Session): Unit = { import ChatMessageType._ message.messageType match { - case CMT_BROADCAST | CMT_SQUAD | CMT_PLATOON | CMT_COMMAND | CMT_NOTE => + case CMT_BROADCAST | CMT_SQUAD | CMT_PLATOON | CMT_COMMAND | CMT_NOTE | CMT_OUTFIT => ops.commandIncomingSendAllIfOnline(session, message) case CMT_OPEN => diff --git a/src/main/scala/net/psforever/actors/session/normal/GeneralLogic.scala b/src/main/scala/net/psforever/actors/session/normal/GeneralLogic.scala index 608f06d0..ac21b6e2 100644 --- a/src/main/scala/net/psforever/actors/session/normal/GeneralLogic.scala +++ b/src/main/scala/net/psforever/actors/session/normal/GeneralLogic.scala @@ -4,7 +4,7 @@ package net.psforever.actors.session.normal import akka.actor.typed.scaladsl.adapter._ import akka.actor.{ActorContext, ActorRef, typed} import net.psforever.actors.session.{AvatarActor, SessionActor} -import net.psforever.actors.session.support.{GeneralFunctions, GeneralOperations, SessionData} +import net.psforever.actors.session.support.{GeneralFunctions, GeneralOperations, SessionData, SessionOutfitHandlers} import net.psforever.objects.{Account, BoomerDeployable, BoomerTrigger, ConstructionItem, GlobalDefinitions, LivePlayerList, Player, SensorDeployable, ShieldGeneratorDeployable, SpecialEmp, TelepadDeployable, Tool, TrapDeployable, TurretDeployable, Vehicle} import net.psforever.objects.avatar.{Avatar, PlayerControl, SpecialCarry} import net.psforever.objects.ballistics.Projectile @@ -37,13 +37,16 @@ import net.psforever.objects.vital.etc.SuicideReason import net.psforever.objects.vital.interaction.DamageInteraction import net.psforever.objects.zones.{ZoneProjectile, Zoning} import net.psforever.packet.PlanetSideGamePacket -import net.psforever.packet.game.{ActionCancelMessage, ActionResultMessage, AvatarFirstTimeEventMessage, AvatarImplantMessage, AvatarJumpMessage, BattleplanMessage, BindPlayerMessage, BugReportMessage, ChangeFireModeMessage, ChangeShortcutBankMessage, CharacterCreateRequestMessage, CharacterRequestAction, CharacterRequestMessage, ChatMsg, CollisionIs, ConnectToWorldRequestMessage, CreateShortcutMessage, DeadState, DeployObjectMessage, DisplayedAwardMessage, DropItemMessage, EmoteMsg, FacilityBenefitShieldChargeRequestMessage, FriendsRequest, GenericAction, GenericActionMessage, GenericCollisionMsg, GenericObjectActionAtPositionMessage, GenericObjectActionMessage, GenericObjectStateMsg, HitHint, InvalidTerrainMessage, LootItemMessage, MoveItemMessage, ObjectDetectedMessage, ObjectHeldMessage, PickupItemMessage, PlanetsideAttributeMessage, PlayerStateMessageUpstream, RequestDestroyMessage, TargetingImplantRequest, TerrainCondition, TradeMessage, UnuseItemMessage, UseItemMessage, VoiceHostInfo, VoiceHostRequest, ZipLineMessage} +import net.psforever.packet.game.OutfitEventAction.{OutfitInfo, OutfitRankNames, Unk2} +import net.psforever.packet.game.{ActionCancelMessage, ActionResultMessage, AvatarFirstTimeEventMessage, AvatarImplantMessage, AvatarJumpMessage, BattleplanMessage, BindPlayerMessage, BugReportMessage, ChangeFireModeMessage, ChangeShortcutBankMessage, CharacterCreateRequestMessage, CharacterRequestAction, CharacterRequestMessage, ChatMsg, CollisionIs, ConnectToWorldRequestMessage, CreateShortcutMessage, DeadState, DeployObjectMessage, DisplayedAwardMessage, DropItemMessage, EmoteMsg, FacilityBenefitShieldChargeRequestMessage, FriendsRequest, GenericAction, GenericActionMessage, GenericCollisionMsg, GenericObjectActionAtPositionMessage, GenericObjectActionMessage, GenericObjectStateMsg, HitHint, InvalidTerrainMessage, LootItemMessage, MoveItemMessage, ObjectDetectedMessage, ObjectHeldMessage, OutfitEvent, OutfitMembershipRequest, OutfitMembershipRequestAction, OutfitMembershipResponse, OutfitRequest, OutfitRequestAction, PickupItemMessage, PlanetsideAttributeMessage, PlayerStateMessageUpstream, RequestDestroyMessage, TargetingImplantRequest, TerrainCondition, TradeMessage, UnuseItemMessage, UseItemMessage, VoiceHostInfo, VoiceHostRequest, ZipLineMessage} import net.psforever.services.account.{AccountPersistenceService, RetrieveAccountData} import net.psforever.services.avatar.{AvatarAction, AvatarServiceMessage} +import net.psforever.services.chat.OutfitChannel import net.psforever.services.local.{LocalAction, LocalServiceMessage} import net.psforever.services.local.support.CaptureFlagManager import net.psforever.types.{CapacitorStateType, ChatMessageType, Cosmetic, ExoSuitType, ImplantType, PlanetSideEmpire, PlanetSideGUID, Vector3} import net.psforever.util.Config +import net.psforever.zones.Zones.zones import scala.concurrent.duration._ @@ -796,6 +799,54 @@ class GeneralLogic(val ops: GeneralOperations, implicit val context: ActorContex val HitHint(_, _) = pkt } + def handleOutfitMembershipRequest(pkt: OutfitMembershipRequest): Unit = { + pkt match { + case OutfitMembershipRequest(_, OutfitMembershipRequestAction.Form(_, outfitName)) => + if (player.outfit_id == 0) { + SessionOutfitHandlers.HandleOutfitForm(outfitName, player, sessionLogic) + } + + case OutfitMembershipRequest(_, OutfitMembershipRequestAction.Invite(_, invitedName)) => + SessionOutfitHandlers.HandleOutfitInvite(zones, invitedName, player) + + case OutfitMembershipRequest(_, OutfitMembershipRequestAction.Kick(memberId, _)) => + SessionOutfitHandlers.HandleOutfitKick(zones, memberId, player) + + case OutfitMembershipRequest(_, OutfitMembershipRequestAction.SetRank(memberId, newRank, _)) => + SessionOutfitHandlers.HandleOutfitPromote(zones, memberId, newRank, player) + + case OutfitMembershipRequest(_, OutfitMembershipRequestAction.AcceptInvite(_)) => + SessionOutfitHandlers.HandleOutfitInviteAccept(player, sessionLogic) + + case _ => + } + } + + def handleOutfitMembershipResponse(pkt: OutfitMembershipResponse): Unit = {} + + def handleOutfitRequest(pkt: OutfitRequest): Unit = { + pkt match { + + case OutfitRequest(_, OutfitRequestAction.Ranks(List(r1, r2, r3, r4, r5, r6, r7, r8))) => + // update db + //sendResponse(OutfitEvent(6418, Unk2(OutfitInfo(player.outfit_name, 0, 0, 1, OutfitRankNames(r1.getOrElse(""), r2.getOrElse(""), r3.getOrElse(""), r4.getOrElse(""), r5.getOrElse(""), r6.getOrElse(""), r7.getOrElse(""), r8.getOrElse("")), "Welcome to the first PSForever Outfit!", 0, unk11=true, 0, 8888888, 0, 0, 0)))) + + case OutfitRequest(_, OutfitRequestAction.Motd(message)) => + // update db + //sendResponse(OutfitEvent(6418, Unk2(OutfitInfo(player.outfit_name, 0, 0, 1, OutfitRankNames("", "", "", "", "", "", "", ""), message, 0, unk11=true, 0, 8888888, 0, 0, 0)))) + + case OutfitRequest(_, OutfitRequestAction.Unk3(true)) => + SessionOutfitHandlers.HandleViewOutfitWindow(zones, player, player.outfit_id) + + case OutfitRequest(_, OutfitRequestAction.Unk3(false)) => + + case OutfitRequest(_, OutfitRequestAction.Unk4(true)) => + SessionOutfitHandlers.HandleGetOutfitList(player) + + case _ => + } + } + /* messages */ def handleRenewCharSavedTimer(): Unit = { diff --git a/src/main/scala/net/psforever/actors/session/spectator/GeneralLogic.scala b/src/main/scala/net/psforever/actors/session/spectator/GeneralLogic.scala index e1f6415f..8c332585 100644 --- a/src/main/scala/net/psforever/actors/session/spectator/GeneralLogic.scala +++ b/src/main/scala/net/psforever/actors/session/spectator/GeneralLogic.scala @@ -13,7 +13,7 @@ import net.psforever.objects.serverobject.doors.Door import net.psforever.objects.vehicles.Utility import net.psforever.objects.zones.ZoneProjectile import net.psforever.packet.PlanetSideGamePacket -import net.psforever.packet.game.{ActionCancelMessage, AvatarFirstTimeEventMessage, AvatarImplantMessage, AvatarJumpMessage, BattleplanMessage, BindPlayerMessage, BugReportMessage, ChangeFireModeMessage, ChangeShortcutBankMessage, CharacterCreateRequestMessage, CharacterRequestMessage, ConnectToWorldRequestMessage, CreateShortcutMessage, DeployObjectMessage, DisplayedAwardMessage, DropItemMessage, EmoteMsg, FacilityBenefitShieldChargeRequestMessage, FriendsRequest, GenericAction, GenericActionMessage, GenericCollisionMsg, GenericObjectActionAtPositionMessage, GenericObjectActionMessage, GenericObjectStateMsg, HitHint, ImplantAction, InvalidTerrainMessage, LootItemMessage, MoveItemMessage, ObjectDetectedMessage, ObjectHeldMessage, PickupItemMessage, PlanetsideAttributeMessage, PlayerStateMessageUpstream, RequestDestroyMessage, TargetingImplantRequest, TradeMessage, UnuseItemMessage, UseItemMessage, VoiceHostInfo, VoiceHostRequest, ZipLineMessage} +import net.psforever.packet.game.{ActionCancelMessage, AvatarFirstTimeEventMessage, AvatarImplantMessage, AvatarJumpMessage, BattleplanMessage, BindPlayerMessage, BugReportMessage, ChangeFireModeMessage, ChangeShortcutBankMessage, CharacterCreateRequestMessage, CharacterRequestMessage, ConnectToWorldRequestMessage, CreateShortcutMessage, DeployObjectMessage, DisplayedAwardMessage, DropItemMessage, EmoteMsg, FacilityBenefitShieldChargeRequestMessage, FriendsRequest, GenericAction, GenericActionMessage, GenericCollisionMsg, GenericObjectActionAtPositionMessage, GenericObjectActionMessage, GenericObjectStateMsg, HitHint, ImplantAction, InvalidTerrainMessage, LootItemMessage, MoveItemMessage, ObjectDetectedMessage, ObjectHeldMessage, OutfitMembershipRequest, OutfitMembershipResponse, OutfitRequest, PickupItemMessage, PlanetsideAttributeMessage, PlayerStateMessageUpstream, RequestDestroyMessage, TargetingImplantRequest, TradeMessage, UnuseItemMessage, UseItemMessage, VoiceHostInfo, VoiceHostRequest, ZipLineMessage} import net.psforever.services.account.AccountPersistenceService import net.psforever.services.avatar.{AvatarAction, AvatarServiceMessage} import net.psforever.types.{ExoSuitType, Vector3} @@ -375,6 +375,12 @@ class GeneralLogic(val ops: GeneralOperations, implicit val context: ActorContex def handleHitHint(pkt: HitHint): Unit = { /* intentionally blank */ } + def handleOutfitMembershipRequest(pkt: OutfitMembershipRequest): Unit = {} + + def handleOutfitMembershipResponse(pkt: OutfitMembershipResponse): Unit = {} + + def handleOutfitRequest(pkt: OutfitRequest): Unit = {} + /* messages */ def handleRenewCharSavedTimer(): Unit = { /* intentionally blank */ } diff --git a/src/main/scala/net/psforever/actors/session/support/ChatOperations.scala b/src/main/scala/net/psforever/actors/session/support/ChatOperations.scala index 48a724db..bdaa6f2e 100644 --- a/src/main/scala/net/psforever/actors/session/support/ChatOperations.scala +++ b/src/main/scala/net/psforever/actors/session/support/ChatOperations.scala @@ -14,7 +14,7 @@ import net.psforever.objects.LivePlayerList import net.psforever.objects.sourcing.PlayerSource import net.psforever.objects.zones.ZoneInfo import net.psforever.packet.game.SetChatFilterMessage -import net.psforever.services.chat.{DefaultChannel, SquadChannel} +import net.psforever.services.chat.{DefaultChannel, OutfitChannel, SquadChannel} import net.psforever.services.local.{LocalAction, LocalServiceMessage} import net.psforever.services.teamwork.{SquadResponse, SquadService, SquadServiceResponse} import net.psforever.types.ChatMessageType.CMT_QUIT @@ -446,6 +446,14 @@ class ChatOperations( } } + def commandOutfit(session: Session, message: ChatMsg, toChannel: ChatChannel): Unit = { + channels.foreach { + case _/*channel*/: OutfitChannel => + commandSendToRecipient(session, message, toChannel) + case _ => () + } + } + def commandWho(session: Session): Unit = { val players = session.zone.Players val popTR = players.count(_.faction == PlanetSideEmpire.TR) diff --git a/src/main/scala/net/psforever/actors/session/support/GeneralOperations.scala b/src/main/scala/net/psforever/actors/session/support/GeneralOperations.scala index e72e57a5..9f5ce393 100644 --- a/src/main/scala/net/psforever/actors/session/support/GeneralOperations.scala +++ b/src/main/scala/net/psforever/actors/session/support/GeneralOperations.scala @@ -166,6 +166,12 @@ trait GeneralFunctions extends CommonSessionInterfacingFunctionality { def handleCanNotPutItemInSlot(msg: Containable.CanNotPutItemInSlot): Unit def handleReceiveDefaultMessage(default: Any, sender: ActorRef): Unit + + def handleOutfitMembershipRequest(pkt: OutfitMembershipRequest): Unit + + def handleOutfitMembershipResponse(pkt: OutfitMembershipResponse): Unit + + def handleOutfitRequest(pkt: OutfitRequest): Unit } class GeneralOperations( diff --git a/src/main/scala/net/psforever/actors/session/support/OutfitInvites.scala b/src/main/scala/net/psforever/actors/session/support/OutfitInvites.scala new file mode 100644 index 00000000..5465cee5 --- /dev/null +++ b/src/main/scala/net/psforever/actors/session/support/OutfitInvites.scala @@ -0,0 +1,34 @@ +package net.psforever.actors.session.support + +import net.psforever.objects.Player +import scala.collection.mutable + +case class OutfitInvite( + sentTo: Player, + sentFrom: Player, + timestamp: Long = System.currentTimeMillis() / 1000 + ) + +object OutfitInviteManager { + private val invites = mutable.Map[Long, OutfitInvite]() + private val ExpirationSeconds = 320 + + def addOutfitInvite(invite: OutfitInvite): Boolean = { + invites.get(invite.sentTo.CharId) match { + case Some(existing) if (System.currentTimeMillis() / 1000 - existing.timestamp) < ExpirationSeconds => + false // Reject new invite (previous one is still valid) + case _ => + invites(invite.sentTo.CharId) = invite + true + } + } + + def removeOutfitInvite(sentToId: Long): Unit = { + invites.remove(sentToId) + } + + def getOutfitInvite(sentToId: Long): Option[OutfitInvite] = invites.get(sentToId) + + def getAllOutfitInvites: List[OutfitInvite] = invites.values.toList +} + diff --git a/src/main/scala/net/psforever/actors/session/support/SessionOutfitHandlers.scala b/src/main/scala/net/psforever/actors/session/support/SessionOutfitHandlers.scala new file mode 100644 index 00000000..4b83e5a0 --- /dev/null +++ b/src/main/scala/net/psforever/actors/session/support/SessionOutfitHandlers.scala @@ -0,0 +1,390 @@ +// Copyright (c) 2025 PSForever +package net.psforever.actors.session.support + +import io.getquill.{ActionReturning, EntityQuery, Insert, PostgresJAsyncContext, Query, Quoted, SnakeCase} +import net.psforever.objects.avatar.PlayerControl +import net.psforever.objects.zones.Zone +import net.psforever.objects.Player +import net.psforever.packet.game.OutfitEventAction.{OutfitInfo, OutfitRankNames, Unk0, Unk1, Unk2} +import net.psforever.packet.game.OutfitMembershipResponse.PacketType.CreateResponse +import net.psforever.packet.game._ +import net.psforever.services.avatar.{AvatarAction, AvatarServiceMessage} +import net.psforever.services.chat.OutfitChannel +import net.psforever.types.ChatMessageType +import net.psforever.util.Config + +import java.time.LocalDateTime +import scala.util.{Failure, Success} + +object SessionOutfitHandlers { + + case class Avatar(id: Long, name: String, faction_id: Int, last_login: java.time.LocalDateTime) + case class Outfit(id: Long, name: String, faction: Int, owner_id: Long, motd: Option[String], created: java.time.LocalDateTime, + rank0: Option[String], + rank1: Option[String], + rank2: Option[String], + rank3: Option[String], + rank4: Option[String], + rank5: Option[String], + rank6: Option[String], + rank7: Option[String]) + case class Outfitmember(id: Long, outfit_id: Long, avatar_id: Long, rank: Int) + case class Outfitpoint(id: Long, outfit_id: Long, avatar_id: Long, points: Long) + case class OutfitpointMv(outfit_id: Long, points: Long) + + val ctx = new PostgresJAsyncContext(SnakeCase, Config.config.getConfig("database")) + import ctx._ + + import scala.concurrent.ExecutionContext.Implicits.global + import scala.concurrent.Future + + def HandleOutfitForm(outfitName: String, player: Player, session: SessionData): Unit = { + val cleanedName = sanitizeOutfitName(outfitName) + + cleanedName match { + case Some(validName) => + ctx.run(findOutfitByName(validName)).flatMap { + case existing if existing.nonEmpty => + PlayerControl.sendResponse(player.Zone, player.Name, + ChatMsg(ChatMessageType.UNK_227, "@OutfitErrorNameAlreadyTaken")) + Future.successful(()) + + case _ => + createNewOutfit(validName, player.Faction.id, player.CharId).map { outfit => + val seconds: Long = + outfit.created.atZone(java.time.ZoneOffset.UTC).toInstant.toEpochMilli / 1000 + + PlayerControl.sendResponse(player.Zone, player.Name, + OutfitEvent(outfit.id, Unk2( + OutfitInfo( + outfit.name, 0, 0, 1, + OutfitRankNames("", "", "", "", "", "", "", ""), + "", + 14, unk11 = true, 0, seconds, 0, 0, 0)))) + + PlayerControl.sendResponse(player.Zone, player.Name, + OutfitMemberUpdate(outfit.id, player.CharId, 7, flag = true)) + + PlayerControl.sendResponse(player.Zone, player.Name, + ChatMsg(ChatMessageType.UNK_227, "@OutfitCreateSuccess")) + + PlayerControl.sendResponse(player.Zone, player.Name, + OutfitMembershipResponse(CreateResponse, 0, 0, player.CharId, 0, "", "", flag = true)) + + player.outfit_id = outfit.id + player.outfit_name = outfit.name + + session.chat.JoinChannel(OutfitChannel(player.outfit_id)) + } + .recover { case e => + e.printStackTrace() + PlayerControl.sendResponse(player.Zone, player.Name, + ChatMsg(ChatMessageType.UNK_227, "@OutfitCreateFailure")) + } + } + case None => + PlayerControl.sendResponse(player.Zone, player.Name, + ChatMsg(ChatMessageType.UNK_227, "@OutfitCreateFailure")) + } + } + + def HandleOutfitInvite(zones: Seq[Zone], invitedName: String, sentFrom: Player): Unit = { + findPlayerByNameForOutfitAction(zones, invitedName, sentFrom).foreach { invitedPlayer => + + PlayerControl.sendResponse(invitedPlayer.Zone, invitedPlayer.Name, + OutfitMembershipResponse(OutfitMembershipResponse.PacketType.Invite, 0, 0, + sentFrom.CharId, sentFrom.CharId, sentFrom.Name, sentFrom.outfit_name, flag = false)) + + PlayerControl.sendResponse(sentFrom.Zone, sentFrom.Name, + OutfitMembershipResponse(OutfitMembershipResponse.PacketType.Invite, 0, 0, + sentFrom.CharId, invitedPlayer.CharId, invitedPlayer.Name, sentFrom.outfit_name, flag = true)) + + val outfitInvite = OutfitInvite(invitedPlayer, sentFrom) + OutfitInviteManager.addOutfitInvite(outfitInvite) + } + } + + def HandleOutfitInviteAccept(invited: Player, session: SessionData): Unit = { + OutfitInviteManager.getOutfitInvite(invited.CharId) match { + case Some(outfitInvite) => + val outfitId = outfitInvite.sentFrom.outfit_id + + (for { + _ <- addMemberToOutfit(outfitId, invited.CharId) + outfitOpt <- ctx.run(getOutfitById(outfitId)).map(_.headOption) + memberCount <- ctx.run(getOutfitMemberCount(outfitId)) + points <- ctx.run(getOutfitPoints(outfitId)).map(_.headOption.map(_.points).getOrElse(0L)) + } yield (outfitOpt, memberCount, points)) + .map { + case (Some(outfit), memberCount, points) => + + PlayerControl.sendResponse(outfitInvite.sentFrom.Zone, outfitInvite.sentFrom.Name, + OutfitMembershipResponse( + OutfitMembershipResponse.PacketType.Unk2, 0, 0, + invited.CharId, outfitInvite.sentFrom.CharId, invited.Name, outfit.name, flag = false)) + + PlayerControl.sendResponse(invited.Zone, invited.Name, + OutfitMembershipResponse( + OutfitMembershipResponse.PacketType.Unk2, 0, 0, + invited.CharId, outfitInvite.sentFrom.CharId, invited.Name, outfit.name, flag = true)) + + PlayerControl.sendResponse(outfitInvite.sentFrom.Zone, outfitInvite.sentFrom.Name, + OutfitEvent(outfitId, OutfitEventAction.Unk5(memberCount))) + + PlayerControl.sendResponse(outfitInvite.sentFrom.Zone, outfitInvite.sentFrom.Name, + OutfitMemberEvent(outfitId, invited.CharId, + OutfitMemberEventAction.Unk0(invited.Name, 0, 0, 0, + OutfitMemberEventAction.PacketType.Padding, 0))) + + val seconds: Long = outfit.created.atZone(java.time.ZoneOffset.UTC).toInstant.toEpochMilli / 1000 + PlayerControl.sendResponse(invited.Zone, invited.Name, + OutfitEvent(outfitId, Unk0(OutfitInfo( + outfit.name, points, points, memberCount, + OutfitRankNames("", "", "", "", "", "", "", ""), + outfit.motd.getOrElse(""), + 14, unk11 = true, 0, seconds, 0, 0, 0)))) + + PlayerControl.sendResponse(invited.Zone, invited.Name, + OutfitMemberUpdate(outfit.id, invited.CharId, 0, flag=true)) + + OutfitInviteManager.removeOutfitInvite(invited.CharId) + + session.chat.JoinChannel(OutfitChannel(outfit.id)) + invited.outfit_id = outfit.id + invited.outfit_name = outfit.name + case (None, _, _) => + + PlayerControl.sendResponse(invited.Zone, invited.Name, + ChatMsg(ChatMessageType.UNK_227, "Failed to join outfit")) + } + .recover { case _ => + PlayerControl.sendResponse(invited.Zone, invited.Name, + ChatMsg(ChatMessageType.UNK_227, "Failed to join outfit")) + } + case None => + } + } + + def HandleOutfitKick(zones: Seq[Zone], kickedId: Long, kickedBy: Player): Unit = { + // if same id, player has left the outfit by their own choice + if (kickedId == kickedBy.CharId) { + // db stuff first + PlayerControl.sendResponse(kickedBy.Zone, kickedBy.Name, OutfitMemberEvent(kickedBy.outfit_id, kickedId, OutfitMemberEventAction.Unk1())) + } + else { + // db stuff first + // tell player they've been kicked (if online) + findPlayerByIdForOutfitAction(zones, kickedId, kickedBy).foreach { kicked => + PlayerControl.sendResponse(kicked.Zone, kicked.Name, OutfitMembershipResponse(OutfitMembershipResponse.PacketType.Kick, 0, 1, kickedBy.CharId, kicked.CharId, kicked.Name, kickedBy.Name, flag = false)) + kicked.Zone.AvatarEvents ! AvatarServiceMessage(kicked.Zone.id, AvatarAction.PlanetsideAttributeToAll(kicked.GUID, 39, 0)) + //kicked.Zone.AvatarEvents ! AvatarServiceMessage(kicked.Zone.id, AvatarAction.PlanetsideStringAttributeMessage(kicked.GUID, 0, "")) + kicked.outfit_id = 0 + kicked.outfit_name = "" + PlayerControl.sendResponse(kicked.Zone, kicked.Name, OutfitMemberEvent(kickedBy.outfit_id, kickedId, OutfitMemberEventAction.Unk1())) + + // move this out of foreach - db will provide kicked char details + PlayerControl.sendResponse(kickedBy.Zone, kickedBy.Name, OutfitMembershipResponse(OutfitMembershipResponse.PacketType.Kick, 0, 1, kickedBy.CharId, kicked.CharId, kicked.Name, "", flag = true)) + PlayerControl.sendResponse(kickedBy.Zone, kickedBy.Name, OutfitMemberEvent(kickedBy.outfit_id, kickedId, OutfitMemberEventAction.Unk1())) + // new number of outfit members? + PlayerControl.sendResponse(kickedBy.Zone, kickedBy.Name, OutfitEvent(kickedBy.outfit_id, OutfitEventAction.Unk5(34))) + } + } + } + + def HandleOutfitPromote(zones: Seq[Zone], promotedId: Long, newRank: Int, promoter: Player): Unit = { + // send to all online players in outfit + findPlayerByIdForOutfitAction(zones, promotedId, promoter).foreach { promoted => + PlayerControl.sendResponse(promoted.Zone, promoted.Name, OutfitMemberEvent(6418, promotedId, OutfitMemberEventAction.Unk0(promoted.Name, newRank, 1032432, 0, OutfitMemberEventAction.PacketType.Padding, 0))) + PlayerControl.sendResponse(promoter.Zone, promoter.Name, OutfitMemberEvent(6418, promotedId, OutfitMemberEventAction.Unk0(promoted.Name, newRank, 1032432, 0, OutfitMemberEventAction.PacketType.Padding, 0))) + } + } + + def HandleViewOutfitWindow(zones: Seq[Zone], player: Player, outfitId: Long): Unit = { + val outfitDetailsF = for { + outfitOpt <- ctx.run(getOutfitById(outfitId)).map(_.headOption) + memberCount <- ctx.run(query[Outfitmember].filter(_.outfit_id == lift(outfitId)).size) + pointsTotal <- ctx.run(querySchema[OutfitpointMv]("outfitpoint_mv").filter(_.outfit_id == lift(outfitId))) + } yield (outfitOpt, memberCount, pointsTotal.headOption.map(_.points).getOrElse(0L)) + + val membersF = ctx.run(getOutfitMembersWithDetails(outfitId)) + + for { + (outfitOpt, memberCount, totalPoints) <- outfitDetailsF + members <- membersF + } yield { + outfitOpt.foreach { outfit => + val seconds: Long = outfit.created.atZone(java.time.ZoneOffset.UTC).toInstant.toEpochMilli / 1000 + + PlayerControl.sendResponse(player.Zone, player.Name, + OutfitEvent(outfit.id, Unk0(OutfitInfo( + outfit.name, + totalPoints, + totalPoints, + memberCount, + OutfitRankNames("", "", "", "", "", "", "", ""), + outfit.motd.getOrElse(""), + 14, unk11 = true, 0, seconds, 0, 0, 0)))) + + members.foreach { case (avatarId, avatarName, points, rank, login) => + val lastLogin = findPlayerByIdForOutfitAction(zones, avatarId, player) match { + case Some(_) => 0L + case None if player.Name == avatarName => 0L + case None => (System.currentTimeMillis() - login.atZone(java.time.ZoneOffset.UTC).toInstant.toEpochMilli) / 1000 + } + PlayerControl.sendResponse(player.Zone, player.Name, + OutfitMemberEvent(outfit.id, avatarId, + OutfitMemberEventAction.Unk0( + avatarName, + rank, + points, + lastLogin, + OutfitMemberEventAction.PacketType.Padding, 0))) + } + PlayerControl.sendResponse(player.Zone, player.Name, + OutfitEvent(outfit.id, Unk1())) + } + } + } + + def HandleGetOutfitList(player: Player): Unit = { + val q = getOutfitsByEmpire(player.Faction.id) + val futureResult = ctx.run(q) + + futureResult.onComplete { + case Success(rows) => + rows.foreach { case (outfitId, points, name, leaderName, memberCount) => + PlayerControl.sendResponse(player.Zone, player.Name, + OutfitListEvent( + OutfitListEventAction.ListElementOutfit( + outfitId, + points, + memberCount, + name, + leaderName))) + } + + case Failure(_) => + PlayerControl.sendResponse(player.Zone, player.Name, + ChatMsg(ChatMessageType.UNK_227, "Outfit list failed to return") + ) + } + } + + /* supporting functions */ + + def sanitizeOutfitName(name: String): Option[String] = { + val cleaned = name + .replaceAll("""[^A-Za-z0-9\-="\;\[\]\(\)\. ]""", "") // Remove disallowed chars + .replaceAll(" +", " ") // Collapse multiple spaces to one + .trim // Remove leading/trailing spaces + if (cleaned.length >= 2 && cleaned.length <= 32) Some(cleaned) else None + } + + def findPlayerByNameForOutfitAction(zones: Iterable[Zone], targetName: String, inviter: Player): Option[Player] = { + zones + .flatMap(_.LivePlayers) + .find(p => + p.Name.equalsIgnoreCase(targetName) && p.Name != inviter.Name && + p.Faction == inviter.Faction && p.outfit_id == 0 + ) + } + + def findPlayerByIdForOutfitAction(zones: Iterable[Zone], targetId: Long, initiator: Player): Option[Player] = { + zones + .flatMap(_.LivePlayers) + .find(p => + p.CharId == targetId && p.Name != initiator.Name && + p.Faction == initiator.Faction && p.outfit_id == initiator.outfit_id + ) + } + + /* db actions */ + + def findOutfitByName(name: String): Quoted[EntityQuery[Outfit]] = quote { + query[Outfit].filter(outfit => lift(name).toLowerCase == outfit.name.toLowerCase) + } + + def insertNewOutfit(name: String, faction: Int, owner_id: Long): Quoted[ActionReturning[Outfit, Outfit]] = quote { + query[Outfit] + .insert(_.name -> lift(name), _.faction -> lift(faction), _.owner_id -> lift(owner_id)) + .returning(outfit => outfit) + } + + def insertOutfitMember(outfit_id: Long, avatar_id: Long, rank: Int): Quoted[Insert[Outfitmember]] = quote { + query[Outfitmember].insert( + _.outfit_id -> lift(outfit_id), + _.avatar_id -> lift(avatar_id), + _.rank -> lift(rank) + ) + } + + def insertOutfitPoint(outfit_id: Long, avatar_id: Long): Quoted[Insert[Outfitpoint]] = quote { + query[Outfitpoint].insert( + _.outfit_id -> lift(outfit_id), + _.avatar_id -> lift(avatar_id) + ) + } + + def createNewOutfit(name: String, faction: Int, owner_id: Long): Future[Outfit] = { + ctx.transaction { implicit ec => + for { + outfit <- ctx.run(insertNewOutfit(name, faction, owner_id)) + _ <- ctx.run(insertOutfitMember(outfit.id, owner_id, rank=7)) + _ <- ctx.run(insertOutfitPoint(outfit.id, owner_id)) + } yield outfit + } + } + + def addMemberToOutfit(outfit_id: Long, avatar_id: Long): Future[Unit] = { + ctx.transaction { implicit ec => + for { + _ <- ctx.run(insertOutfitMember(outfit_id, avatar_id, rank=0)) + _ <- ctx.run(insertOutfitPoint(outfit_id, avatar_id)) + } yield () + } + } + + def getOutfitById(id: Long): Quoted[EntityQuery[Outfit]] = quote { + query[Outfit].filter(_.id == lift(id)) + } + + def getOutfitMemberCount(id: Long): Quoted[Long] = quote { + query[Outfitmember].filter(_.outfit_id == lift(id)).size + } + + def getOutfitPoints(id: Long): Quoted[EntityQuery[OutfitpointMv]] = quote { + querySchema[OutfitpointMv]("outfitpoint_mv").filter(_.outfit_id == lift(id)) + } + + def getOutfitMembersWithDetails(outfitId: Long): Quoted[Query[(Long, String, Long, Int, LocalDateTime)]] = quote { + query[Outfitmember] + .filter(_.outfit_id == lift(outfitId)) + .join(query[Avatar]).on(_.avatar_id == _.id) + .leftJoin(query[Outfitpoint]).on { + case ((member, _), points) => + points.outfit_id == member.outfit_id && points.avatar_id == member.avatar_id + } + .map { + case ((member, avatar), pointsOpt) => + (member.avatar_id, avatar.name, pointsOpt.map(_.points).getOrElse(0L), member.rank, avatar.last_login) + } + } + + def getOutfitsByEmpire(playerEmpireId: Int): Quoted[Query[(Long, Long, String, String, Long)]] = quote { + query[Outfit] + .filter(_.faction == lift(playerEmpireId)) + .join(query[Avatar]).on((outfit, avatar) => outfit.owner_id == avatar.id) + .leftJoin( + query[Outfitmember] + .groupBy(_.outfit_id) + .map { case (oid, members) => (oid, members.size) } + ).on { case ((outfit, _), (oid, _)) => oid == outfit.id } + .leftJoin(querySchema[OutfitpointMv]("outfitpoint_mv")).on { + case (((outfit, _), _), points) => points.outfit_id == outfit.id + } + .map { + case (((outfit, leader), memberCounts), points) => + (outfit.id, points.map(_.points).getOrElse(0L), outfit.name, leader.name, memberCounts.map(_._2).getOrElse(0L)) + } + } +} diff --git a/src/main/scala/net/psforever/objects/Player.scala b/src/main/scala/net/psforever/objects/Player.scala index fd00fb54..d4847ad4 100644 --- a/src/main/scala/net/psforever/objects/Player.scala +++ b/src/main/scala/net/psforever/objects/Player.scala @@ -85,6 +85,8 @@ class Player(var avatar: Avatar) var silenced: Boolean = false var death_by: Int = 0 var lastShotSeq_time: Int = -1 + var outfit_name: String = "" + var outfit_id: Long = 0 /** From PlanetsideAttributeMessage */ var PlanetsideAttribute: Array[Long] = Array.ofDim(120) diff --git a/src/main/scala/net/psforever/objects/definition/converter/AvatarConverter.scala b/src/main/scala/net/psforever/objects/definition/converter/AvatarConverter.scala index 1d2f16fe..ea96ad0e 100644 --- a/src/main/scala/net/psforever/objects/definition/converter/AvatarConverter.scala +++ b/src/main/scala/net/psforever/objects/definition/converter/AvatarConverter.scala @@ -93,8 +93,8 @@ object AvatarConverter { 0 ) val ab: (Boolean, Int) => CharacterAppearanceB = CharacterAppearanceB( - 0L, - outfit_name = "", + obj.outfit_id, + obj.outfit_name, outfit_logo = 0, unk1 = false, obj.isBackpack, diff --git a/src/main/scala/net/psforever/packet/game/OutfitMembershipResponse.scala b/src/main/scala/net/psforever/packet/game/OutfitMembershipResponse.scala index 1608cbf9..ebde8270 100644 --- a/src/main/scala/net/psforever/packet/game/OutfitMembershipResponse.scala +++ b/src/main/scala/net/psforever/packet/game/OutfitMembershipResponse.scala @@ -31,11 +31,11 @@ object OutfitMembershipResponse extends Marshallable[OutfitMembershipResponse] { type Type = Value val CreateResponse: PacketType.Value = Value(0) - val Unk1: PacketType.Value = Value(1) // Info: Player has been invited / response to OutfitMembershipRequest Unk2 for that player + val Invite: PacketType.Value = Value(1) // Info: Player has been invited / response to OutfitMembershipRequest Unk2 for that player val Unk2: PacketType.Value = Value(2) // Invited / Accepted / Added val Unk3: PacketType.Value = Value(3) val Unk4: PacketType.Value = Value(4) - val Unk5: PacketType.Value = Value(5) + val Kick: PacketType.Value = Value(5) val Unk6: PacketType.Value = Value(6) // 6 and 7 seen as failed decodes, validity unknown val Unk7: PacketType.Value = Value(7) diff --git a/src/main/scala/net/psforever/services/chat/ChatChannel.scala b/src/main/scala/net/psforever/services/chat/ChatChannel.scala index 3fbea3e6..fab599fd 100644 --- a/src/main/scala/net/psforever/services/chat/ChatChannel.scala +++ b/src/main/scala/net/psforever/services/chat/ChatChannel.scala @@ -12,3 +12,5 @@ final case class SquadChannel(guid: PlanetSideGUID) extends ChatChannel case object SpectatorChannel extends ChatChannel case object CustomerServiceChannel extends ChatChannel + +final case class OutfitChannel(id: Long) extends ChatChannel diff --git a/src/main/scala/net/psforever/services/chat/ChatService.scala b/src/main/scala/net/psforever/services/chat/ChatService.scala index 8803c366..53c105f9 100644 --- a/src/main/scala/net/psforever/services/chat/ChatService.scala +++ b/src/main/scala/net/psforever/services/chat/ChatService.scala @@ -57,6 +57,7 @@ class ChatService(context: ActorContext[ChatService.Command]) extends AbstractBe (channel, message.messageType) match { case (SquadChannel(_), CMT_SQUAD) => () case (SquadChannel(_), CMT_VOICE) if message.contents.startsWith("SH") => () + case (OutfitChannel(_), CMT_OUTFIT) => () case (DefaultChannel, messageType) if messageType != CMT_SQUAD => () case (SpectatorChannel, messageType) if messageType != CMT_SQUAD => () case _ => @@ -158,6 +159,9 @@ class ChatService(context: ActorContext[ChatService.Command]) extends AbstractBe case CMT_SQUAD => subs.foreach(_.actor ! MessageResponse(session, message, channel)) + case CMT_OUTFIT => + subs.foreach(_.actor ! MessageResponse(session, message, channel)) + case CMT_NOTE => subs .filter(_.sessionSource.session.player.Name == message.recipient) From ad52c8076c7dc87cc2d95fed0f56a2b3f792058d Mon Sep 17 00:00:00 2001 From: ScrawnyRonnie Date: Fri, 29 Aug 2025 15:31:26 -0400 Subject: [PATCH 19/22] mostly kick from outfit --- .../actors/session/normal/GeneralLogic.scala | 11 +- .../support/SessionOutfitHandlers.scala | 147 +++++++++++++++--- .../scala/net/psforever/objects/Player.scala | 2 + .../avatar/AvatarServiceMessage.scala | 2 + 4 files changed, 136 insertions(+), 26 deletions(-) diff --git a/src/main/scala/net/psforever/actors/session/normal/GeneralLogic.scala b/src/main/scala/net/psforever/actors/session/normal/GeneralLogic.scala index ac21b6e2..3b3794b1 100644 --- a/src/main/scala/net/psforever/actors/session/normal/GeneralLogic.scala +++ b/src/main/scala/net/psforever/actors/session/normal/GeneralLogic.scala @@ -809,15 +809,18 @@ class GeneralLogic(val ops: GeneralOperations, implicit val context: ActorContex case OutfitMembershipRequest(_, OutfitMembershipRequestAction.Invite(_, invitedName)) => SessionOutfitHandlers.HandleOutfitInvite(zones, invitedName, player) + case OutfitMembershipRequest(_, OutfitMembershipRequestAction.AcceptInvite(_)) => + SessionOutfitHandlers.HandleOutfitInviteAccept(player, sessionLogic) + + case OutfitMembershipRequest(_, OutfitMembershipRequestAction.RejectInvite(_)) => + SessionOutfitHandlers.HandleOutfitInviteReject(player) + case OutfitMembershipRequest(_, OutfitMembershipRequestAction.Kick(memberId, _)) => - SessionOutfitHandlers.HandleOutfitKick(zones, memberId, player) + SessionOutfitHandlers.HandleOutfitKick(zones, memberId, player, sessionLogic) case OutfitMembershipRequest(_, OutfitMembershipRequestAction.SetRank(memberId, newRank, _)) => SessionOutfitHandlers.HandleOutfitPromote(zones, memberId, newRank, player) - case OutfitMembershipRequest(_, OutfitMembershipRequestAction.AcceptInvite(_)) => - SessionOutfitHandlers.HandleOutfitInviteAccept(player, sessionLogic) - case _ => } } diff --git a/src/main/scala/net/psforever/actors/session/support/SessionOutfitHandlers.scala b/src/main/scala/net/psforever/actors/session/support/SessionOutfitHandlers.scala index 4b83e5a0..553d6eec 100644 --- a/src/main/scala/net/psforever/actors/session/support/SessionOutfitHandlers.scala +++ b/src/main/scala/net/psforever/actors/session/support/SessionOutfitHandlers.scala @@ -29,7 +29,7 @@ object SessionOutfitHandlers { rank6: Option[String], rank7: Option[String]) case class Outfitmember(id: Long, outfit_id: Long, avatar_id: Long, rank: Int) - case class Outfitpoint(id: Long, outfit_id: Long, avatar_id: Long, points: Long) + case class Outfitpoint(id: Long, outfit_id: Long, avatar_id: Option[Long], points: Long) case class OutfitpointMv(outfit_id: Long, points: Long) val ctx = new PostgresJAsyncContext(SnakeCase, Config.config.getConfig("database")) @@ -74,6 +74,12 @@ object SessionOutfitHandlers { player.outfit_id = outfit.id player.outfit_name = outfit.name + player.Zone.AvatarEvents ! AvatarServiceMessage(player.Zone.id, + AvatarAction.PlanetsideAttributeToAll(player.GUID, 39, player.outfit_id)) + + player.Zone.AvatarEvents ! AvatarServiceMessage(player.Zone.id, + AvatarAction.PlanetsideStringAttribute(player.GUID, 0, player.outfit_name)) + session.chat.JoinChannel(OutfitChannel(player.outfit_id)) } .recover { case e => @@ -152,6 +158,12 @@ object SessionOutfitHandlers { session.chat.JoinChannel(OutfitChannel(outfit.id)) invited.outfit_id = outfit.id invited.outfit_name = outfit.name + + invited.Zone.AvatarEvents ! AvatarServiceMessage(invited.Zone.id, + AvatarAction.PlanetsideAttributeToAll(invited.GUID, 39, invited.outfit_id)) + + invited.Zone.AvatarEvents ! AvatarServiceMessage(invited.Zone.id, + AvatarAction.PlanetsideStringAttribute(invited.GUID, 0, invited.outfit_name)) case (None, _, _) => PlayerControl.sendResponse(invited.Zone, invited.Name, @@ -165,28 +177,101 @@ object SessionOutfitHandlers { } } - def HandleOutfitKick(zones: Seq[Zone], kickedId: Long, kickedBy: Player): Unit = { + def HandleOutfitInviteReject(invited: Player): Unit = { + OutfitInviteManager.getOutfitInvite(invited.CharId) match { + case Some(outfitInvite) => + + PlayerControl.sendResponse(outfitInvite.sentFrom.Zone, outfitInvite.sentFrom.Name, + OutfitMembershipResponse( + OutfitMembershipResponse.PacketType.Unk3, 0, 0, + invited.CharId, outfitInvite.sentFrom.CharId, invited.Name, "", flag = false)) + + PlayerControl.sendResponse(invited.Zone, invited.Name, + OutfitMembershipResponse( + OutfitMembershipResponse.PacketType.Unk3, 0, 0, + invited.CharId, outfitInvite.sentFrom.CharId, invited.Name, "", flag = true)) + + OutfitInviteManager.removeOutfitInvite(invited.CharId) + case None => + } + } + + def HandleOutfitKick(zones: Seq[Zone], kickedId: Long, kickedBy: Player, session: SessionData): Unit = { // if same id, player has left the outfit by their own choice if (kickedId == kickedBy.CharId) { - // db stuff first - PlayerControl.sendResponse(kickedBy.Zone, kickedBy.Name, OutfitMemberEvent(kickedBy.outfit_id, kickedId, OutfitMemberEventAction.Unk1())) + removeMemberFromOutfit(kickedBy.outfit_id, kickedId).map { + case (deleted, _) => + if (deleted > 0) { + PlayerControl.sendResponse(kickedBy.Zone, kickedBy.Name, + OutfitMemberEvent(kickedBy.outfit_id, kickedId, OutfitMemberEventAction.Unk1())) + + zones.filter(z => z.AllPlayers.nonEmpty).flatMap(_.AllPlayers) + .filter(p => p.outfit_id == kickedBy.outfit_id).foreach(outfitMember => + PlayerControl.sendResponse(outfitMember.Zone, outfitMember.Name, + OutfitMemberEvent(kickedBy.outfit_id, kickedId, OutfitMemberEventAction.Unk1())) + ) + + session.chat.LeaveChannel(OutfitChannel(kickedBy.outfit_id)) + kickedBy.outfit_name = "" + kickedBy.outfit_id = 0 + + val pZone = zones.filter(z => z.id == kickedBy.Zone.id).head + pZone.AllPlayers.filter(p => p.Faction == kickedBy.Faction).foreach { friendly => + PlayerControl.sendResponse(friendly.Zone, friendly.Name, + PlanetsideAttributeMessage(kickedBy.GUID, 39, 0)) + + PlayerControl.sendResponse(friendly.Zone, friendly.Name, + PlanetsideStringAttributeMessage(kickedBy.GUID, 0, "")) + } + } + }.recover { case e => + e.printStackTrace() + } } else { - // db stuff first - // tell player they've been kicked (if online) - findPlayerByIdForOutfitAction(zones, kickedId, kickedBy).foreach { kicked => - PlayerControl.sendResponse(kicked.Zone, kicked.Name, OutfitMembershipResponse(OutfitMembershipResponse.PacketType.Kick, 0, 1, kickedBy.CharId, kicked.CharId, kicked.Name, kickedBy.Name, flag = false)) - kicked.Zone.AvatarEvents ! AvatarServiceMessage(kicked.Zone.id, AvatarAction.PlanetsideAttributeToAll(kicked.GUID, 39, 0)) - //kicked.Zone.AvatarEvents ! AvatarServiceMessage(kicked.Zone.id, AvatarAction.PlanetsideStringAttributeMessage(kicked.GUID, 0, "")) - kicked.outfit_id = 0 - kicked.outfit_name = "" - PlayerControl.sendResponse(kicked.Zone, kicked.Name, OutfitMemberEvent(kickedBy.outfit_id, kickedId, OutfitMemberEventAction.Unk1())) + removeMemberFromOutfit(kickedBy.outfit_id, kickedId).map { + case (deleted, _) => + if (deleted > 0) { + findPlayerByIdForOutfitAction(zones, kickedId, kickedBy).foreach { kicked => + PlayerControl.sendResponse(kicked.Zone, kicked.Name, + OutfitMembershipResponse(OutfitMembershipResponse.PacketType.Kick, 0, 1, + kickedBy.CharId, kicked.CharId, kicked.Name, kickedBy.Name, flag = false)) - // move this out of foreach - db will provide kicked char details - PlayerControl.sendResponse(kickedBy.Zone, kickedBy.Name, OutfitMembershipResponse(OutfitMembershipResponse.PacketType.Kick, 0, 1, kickedBy.CharId, kicked.CharId, kicked.Name, "", flag = true)) - PlayerControl.sendResponse(kickedBy.Zone, kickedBy.Name, OutfitMemberEvent(kickedBy.outfit_id, kickedId, OutfitMemberEventAction.Unk1())) - // new number of outfit members? - PlayerControl.sendResponse(kickedBy.Zone, kickedBy.Name, OutfitEvent(kickedBy.outfit_id, OutfitEventAction.Unk5(34))) + kicked.Zone.AvatarEvents ! AvatarServiceMessage(kicked.Zone.id, + AvatarAction.PlanetsideAttributeToAll(kicked.GUID, 39, 0)) + + kicked.Zone.AvatarEvents ! AvatarServiceMessage(kicked.Zone.id, + AvatarAction.PlanetsideStringAttribute(kicked.GUID, 0, "")) + + kicked.outfit_id = 0 + kicked.outfit_name = "" + PlayerControl.sendResponse(kicked.Zone, kicked.Name, + OutfitMemberEvent(kickedBy.outfit_id, kickedId, OutfitMemberEventAction.Unk1())) + } + val avatarName: Future[Option[String]] = + ctx.run( + quote { query[Avatar].filter(_.id == lift(kickedId)).map(_.name) } + ).map(_.headOption) + + avatarName.foreach { + case Some(name) => PlayerControl.sendResponse(kickedBy.Zone, kickedBy.Name, + OutfitMembershipResponse(OutfitMembershipResponse.PacketType.Kick, 0, 1, kickedBy.CharId, kickedId, name, "", flag = true)) + + case None => PlayerControl.sendResponse(kickedBy.Zone, kickedBy.Name, + OutfitMembershipResponse(OutfitMembershipResponse.PacketType.Kick, 0, 1, kickedBy.CharId, kickedId, "NameNotFound", "", flag = true)) + } + zones.filter(z => z.AllPlayers.nonEmpty).flatMap(_.AllPlayers) + .filter(p => p.outfit_id == kickedBy.outfit_id).foreach(outfitMember => + PlayerControl.sendResponse(outfitMember.Zone, outfitMember.Name, + OutfitMemberEvent(kickedBy.outfit_id, kickedId, OutfitMemberEventAction.Unk1())) + ) + // this needs to be the kicked player + // session.chat.LeaveChannel(OutfitChannel(kickedBy.outfit_id)) + // new number of outfit members? + //PlayerControl.sendResponse(kickedBy.Zone, kickedBy.Name, OutfitEvent(kickedBy.outfit_id, OutfitEventAction.Unk5(34))) + } + }.recover { case e => + e.printStackTrace() } } } @@ -282,7 +367,7 @@ object SessionOutfitHandlers { def findPlayerByNameForOutfitAction(zones: Iterable[Zone], targetName: String, inviter: Player): Option[Player] = { zones - .flatMap(_.LivePlayers) + .flatMap(_.AllPlayers) .find(p => p.Name.equalsIgnoreCase(targetName) && p.Name != inviter.Name && p.Faction == inviter.Faction && p.outfit_id == 0 @@ -291,7 +376,7 @@ object SessionOutfitHandlers { def findPlayerByIdForOutfitAction(zones: Iterable[Zone], targetId: Long, initiator: Player): Option[Player] = { zones - .flatMap(_.LivePlayers) + .flatMap(_.AllPlayers) .find(p => p.CharId == targetId && p.Name != initiator.Name && p.Faction == initiator.Faction && p.outfit_id == initiator.outfit_id @@ -321,7 +406,7 @@ object SessionOutfitHandlers { def insertOutfitPoint(outfit_id: Long, avatar_id: Long): Quoted[Insert[Outfitpoint]] = quote { query[Outfitpoint].insert( _.outfit_id -> lift(outfit_id), - _.avatar_id -> lift(avatar_id) + _.avatar_id -> lift(Some(avatar_id): Option[Long]) ) } @@ -344,6 +429,24 @@ object SessionOutfitHandlers { } } + def removeMemberFromOutfit(outfit_id: Long, avatar_id: Long): Future[(Long, Long)] = { + val avatarOpt: Option[Long] = Some(avatar_id) + ctx.transaction { _ => + for { + deleted <- ctx.run( + query[Outfitmember] + .filter(m => m.outfit_id == lift(outfit_id) && m.avatar_id == lift(avatar_id)) + .delete + ) + updated <- ctx.run( + query[Outfitpoint] + .filter(p => p.outfit_id == lift(outfit_id) && p.avatar_id == lift(avatarOpt)) + .update(_.avatar_id -> None) + ) + } yield (deleted, updated) + } + } + def getOutfitById(id: Long): Quoted[EntityQuery[Outfit]] = quote { query[Outfit].filter(_.id == lift(id)) } @@ -362,7 +465,7 @@ object SessionOutfitHandlers { .join(query[Avatar]).on(_.avatar_id == _.id) .leftJoin(query[Outfitpoint]).on { case ((member, _), points) => - points.outfit_id == member.outfit_id && points.avatar_id == member.avatar_id + points.outfit_id == member.outfit_id && points.avatar_id.getOrElse(0L) == member.avatar_id } .map { case ((member, avatar), pointsOpt) => diff --git a/src/main/scala/net/psforever/objects/Player.scala b/src/main/scala/net/psforever/objects/Player.scala index d4847ad4..074937ed 100644 --- a/src/main/scala/net/psforever/objects/Player.scala +++ b/src/main/scala/net/psforever/objects/Player.scala @@ -648,6 +648,8 @@ object Player { obj.silenced = player.silenced obj.allowInteraction = player.allowInteraction obj.avatar.scorecard.respawn() + obj.outfit_name = player.outfit_name + obj.outfit_id = player.outfit_id obj } else { player diff --git a/src/main/scala/net/psforever/services/avatar/AvatarServiceMessage.scala b/src/main/scala/net/psforever/services/avatar/AvatarServiceMessage.scala index 5cea9482..c307c7b3 100644 --- a/src/main/scala/net/psforever/services/avatar/AvatarServiceMessage.scala +++ b/src/main/scala/net/psforever/services/avatar/AvatarServiceMessage.scala @@ -78,6 +78,8 @@ object AvatarAction { extends Action final case class PlanetsideAttributeSelf(player_guid: PlanetSideGUID, attribute_type: Int, attribute_value: Long) extends Action + final case class PlanetsideStringAttribute(player_guid: PlanetSideGUID, attribute_type: Int, attribute_value: String) + extends Action final case class PlayerState( player_guid: PlanetSideGUID, pos: Vector3, From 18dd426d13cd5294bd5ff8c798bd2226a2e485f5 Mon Sep 17 00:00:00 2001 From: Resaec Date: Sat, 30 Aug 2025 01:35:51 +0200 Subject: [PATCH 20/22] fix outfit rank names not representing DB values add MOTD handling renaming OMR packet types with known uses handling outfit promotions (setrank) handle outfit owner changes changing the migration to change to unique index. allows concurrent refresh of MV --- .../db/migration/V015__OutfitStructure.sql | 2 +- .../actors/session/normal/GeneralLogic.scala | 7 +- .../support/SessionOutfitHandlers.scala | 231 ++++++++++++++++-- .../game/OutfitMembershipResponse.scala | 6 +- .../psforever/packet/game/OutfitRequest.scala | 2 +- .../game/OutfitMembershipResponseTest.scala | 16 +- 6 files changed, 227 insertions(+), 37 deletions(-) diff --git a/server/src/main/resources/db/migration/V015__OutfitStructure.sql b/server/src/main/resources/db/migration/V015__OutfitStructure.sql index f44ee4af..7a0a1186 100644 --- a/server/src/main/resources/db/migration/V015__OutfitStructure.sql +++ b/server/src/main/resources/db/migration/V015__OutfitStructure.sql @@ -104,4 +104,4 @@ CREATE MATERIALIZED VIEW outfitpoint_mv AS "outfitpoint" GROUP BY "outfit_id"; -CREATE INDEX "outfitpoint_mv_outfit_id_idx" ON "outfitpoint_mv" ("outfit_id"); +CREATE UNIQUE INDEX "outfitpoint_mv_outfit_id_unique" ON "outfitpoint_mv" ("outfit_id"); diff --git a/src/main/scala/net/psforever/actors/session/normal/GeneralLogic.scala b/src/main/scala/net/psforever/actors/session/normal/GeneralLogic.scala index 3b3794b1..b16386d7 100644 --- a/src/main/scala/net/psforever/actors/session/normal/GeneralLogic.scala +++ b/src/main/scala/net/psforever/actors/session/normal/GeneralLogic.scala @@ -830,14 +830,13 @@ class GeneralLogic(val ops: GeneralOperations, implicit val context: ActorContex def handleOutfitRequest(pkt: OutfitRequest): Unit = { pkt match { + case OutfitRequest(_, OutfitRequestAction.Motd(message)) => + SessionOutfitHandlers.HandleOutfitMotd(zones, message, player) + case OutfitRequest(_, OutfitRequestAction.Ranks(List(r1, r2, r3, r4, r5, r6, r7, r8))) => // update db //sendResponse(OutfitEvent(6418, Unk2(OutfitInfo(player.outfit_name, 0, 0, 1, OutfitRankNames(r1.getOrElse(""), r2.getOrElse(""), r3.getOrElse(""), r4.getOrElse(""), r5.getOrElse(""), r6.getOrElse(""), r7.getOrElse(""), r8.getOrElse("")), "Welcome to the first PSForever Outfit!", 0, unk11=true, 0, 8888888, 0, 0, 0)))) - case OutfitRequest(_, OutfitRequestAction.Motd(message)) => - // update db - //sendResponse(OutfitEvent(6418, Unk2(OutfitInfo(player.outfit_name, 0, 0, 1, OutfitRankNames("", "", "", "", "", "", "", ""), message, 0, unk11=true, 0, 8888888, 0, 0, 0)))) - case OutfitRequest(_, OutfitRequestAction.Unk3(true)) => SessionOutfitHandlers.HandleViewOutfitWindow(zones, player, player.outfit_id) diff --git a/src/main/scala/net/psforever/actors/session/support/SessionOutfitHandlers.scala b/src/main/scala/net/psforever/actors/session/support/SessionOutfitHandlers.scala index 553d6eec..e9249d6d 100644 --- a/src/main/scala/net/psforever/actors/session/support/SessionOutfitHandlers.scala +++ b/src/main/scala/net/psforever/actors/session/support/SessionOutfitHandlers.scala @@ -1,7 +1,7 @@ // Copyright (c) 2025 PSForever package net.psforever.actors.session.support -import io.getquill.{ActionReturning, EntityQuery, Insert, PostgresJAsyncContext, Query, Quoted, SnakeCase} +import io.getquill.{ActionReturning, EntityQuery, Insert, PostgresJAsyncContext, Query, Quoted, SnakeCase, Update} import net.psforever.objects.avatar.PlayerControl import net.psforever.objects.zones.Zone import net.psforever.objects.Player @@ -19,15 +19,23 @@ import scala.util.{Failure, Success} object SessionOutfitHandlers { case class Avatar(id: Long, name: String, faction_id: Int, last_login: java.time.LocalDateTime) - case class Outfit(id: Long, name: String, faction: Int, owner_id: Long, motd: Option[String], created: java.time.LocalDateTime, - rank0: Option[String], - rank1: Option[String], - rank2: Option[String], - rank3: Option[String], - rank4: Option[String], - rank5: Option[String], - rank6: Option[String], - rank7: Option[String]) + case class Outfit( + id: Long, + name: String, + faction: Int, + owner_id: Long, + motd: Option[String], + created: java.time.LocalDateTime, + deleted: Boolean, + rank0: Option[String], + rank1: Option[String], + rank2: Option[String], + rank3: Option[String], + rank4: Option[String], + rank5: Option[String], + rank6: Option[String], + rank7: Option[String] + ) case class Outfitmember(id: Long, outfit_id: Long, avatar_id: Long, rank: Int) case class Outfitpoint(id: Long, outfit_id: Long, avatar_id: Option[Long], points: Long) case class OutfitpointMv(outfit_id: Long, points: Long) @@ -126,12 +134,12 @@ object SessionOutfitHandlers { PlayerControl.sendResponse(outfitInvite.sentFrom.Zone, outfitInvite.sentFrom.Name, OutfitMembershipResponse( - OutfitMembershipResponse.PacketType.Unk2, 0, 0, + OutfitMembershipResponse.PacketType.InviteAccepted, 0, 0, invited.CharId, outfitInvite.sentFrom.CharId, invited.Name, outfit.name, flag = false)) PlayerControl.sendResponse(invited.Zone, invited.Name, OutfitMembershipResponse( - OutfitMembershipResponse.PacketType.Unk2, 0, 0, + OutfitMembershipResponse.PacketType.InviteAccepted, 0, 0, invited.CharId, outfitInvite.sentFrom.CharId, invited.Name, outfit.name, flag = true)) PlayerControl.sendResponse(outfitInvite.sentFrom.Zone, outfitInvite.sentFrom.Name, @@ -183,12 +191,12 @@ object SessionOutfitHandlers { PlayerControl.sendResponse(outfitInvite.sentFrom.Zone, outfitInvite.sentFrom.Name, OutfitMembershipResponse( - OutfitMembershipResponse.PacketType.Unk3, 0, 0, + OutfitMembershipResponse.PacketType.InviteRejected, 0, 0, invited.CharId, outfitInvite.sentFrom.CharId, invited.Name, "", flag = false)) PlayerControl.sendResponse(invited.Zone, invited.Name, OutfitMembershipResponse( - OutfitMembershipResponse.PacketType.Unk3, 0, 0, + OutfitMembershipResponse.PacketType.InviteRejected, 0, 0, invited.CharId, outfitInvite.sentFrom.CharId, invited.Name, "", flag = true)) OutfitInviteManager.removeOutfitInvite(invited.CharId) @@ -277,10 +285,60 @@ object SessionOutfitHandlers { } def HandleOutfitPromote(zones: Seq[Zone], promotedId: Long, newRank: Int, promoter: Player): Unit = { - // send to all online players in outfit + + val outfit_id = promoter.outfit_id + findPlayerByIdForOutfitAction(zones, promotedId, promoter).foreach { promoted => - PlayerControl.sendResponse(promoted.Zone, promoted.Name, OutfitMemberEvent(6418, promotedId, OutfitMemberEventAction.Unk0(promoted.Name, newRank, 1032432, 0, OutfitMemberEventAction.PacketType.Padding, 0))) - PlayerControl.sendResponse(promoter.Zone, promoter.Name, OutfitMemberEvent(6418, promotedId, OutfitMemberEventAction.Unk0(promoted.Name, newRank, 1032432, 0, OutfitMemberEventAction.PacketType.Padding, 0))) + + if (newRank == 7) { + + // demote owner to rank 6 + // promote promoted to rank 7 + // update outfit + updateOutfitOwner(outfit_id, promoter.avatar.id, promoted.avatar.id) + + // TODO: does every member get the notification like this? + getOutfitMemberPoints(outfit_id, promoter.avatar.id).map { + owner_points => + // announce owner rank change + zones.foreach(zone => { + zone.AllPlayers.filter(_.outfit_id == outfit_id).foreach(outfitMember => { + PlayerControl.sendResponse( + zone, outfitMember.Name, + OutfitMemberEvent(outfit_id, promoter.avatar.id, + OutfitMemberEventAction.Unk0(promoter.Name, 6, owner_points, 0, OutfitMemberEventAction.PacketType.Padding, 0))) + }) + }) + } + + // update promoter rank + PlayerControl.sendResponse( + promoter.Zone, promoter.Name, + OutfitMemberUpdate(outfit_id, promoter.avatar.id, rank = 6, flag = true)) + } + else { + // promote promoted + updateOutfitMemberRank(outfit_id, promoted.avatar.id, rank = newRank) + } + + // TODO: does every member get the notification like this? + getOutfitMemberPoints(outfit_id, promoted.avatar.id).map { + member_points => + // tell everyone about the new rank of the promoted member + zones.foreach(zone => { + zone.AllPlayers.filter(_.outfit_id == outfit_id).foreach(player => { + PlayerControl.sendResponse( + zone, player.Name, + OutfitMemberEvent(outfit_id, promoted.avatar.id, + OutfitMemberEventAction.Unk0(promoted.Name, newRank, member_points, 0, OutfitMemberEventAction.PacketType.Padding, 0))) + }) + }) + } + + // update promoted rank + PlayerControl.sendResponse( + promoted.Zone, promoted.Name, + OutfitMemberUpdate(outfit_id, promoted.avatar.id, rank = newRank, flag = true)) } } @@ -306,7 +364,16 @@ object SessionOutfitHandlers { totalPoints, totalPoints, memberCount, - OutfitRankNames("", "", "", "", "", "", "", ""), + OutfitRankNames( + outfit.rank0.getOrElse(""), + outfit.rank1.getOrElse(""), + outfit.rank2.getOrElse(""), + outfit.rank3.getOrElse(""), + outfit.rank4.getOrElse(""), + outfit.rank5.getOrElse(""), + outfit.rank6.getOrElse(""), + outfit.rank7.getOrElse(""), + ), outfit.motd.getOrElse(""), 14, unk11 = true, 0, seconds, 0, 0, 0)))) @@ -355,6 +422,71 @@ object SessionOutfitHandlers { } } + def HandleOutfitMotd(zones: Seq[Zone], message: String, player: Player): Unit = { + + val outfit_id = player.outfit_id + + // update MOTD + updateOutfitMotd(outfit_id, message) + + // TODO this does not notify clients with open windows. Do they update in the first place? + val outfitDetails = for { + outfitOpt <- ctx.run(getOutfitById(outfit_id)).map(_.headOption) + memberCount <- ctx.run(query[Outfitmember].filter(_.outfit_id == lift(outfit_id)).size) + pointsTotal <- ctx.run(querySchema[OutfitpointMv]("outfitpoint_mv").filter(_.outfit_id == lift(outfit_id))) + } yield (outfitOpt, memberCount, pointsTotal.headOption.map(_.points).getOrElse(0L)) + + for { + (outfitOpt, memberCount, totalPoints) <- outfitDetails + } yield { + outfitOpt.foreach { outfit => + + // send to all online players in outfit + val outfit_event = OutfitEvent( + outfit_id, + Unk2( + OutfitInfo( + outfit_name = outfit.name, + outfit_points1 = totalPoints, + outfit_points2 = totalPoints, + member_count = memberCount, + outfit_rank_names = OutfitRankNames( + outfit.rank0.getOrElse(""), + outfit.rank1.getOrElse(""), + outfit.rank2.getOrElse(""), + outfit.rank3.getOrElse(""), + outfit.rank4.getOrElse(""), + outfit.rank5.getOrElse(""), + outfit.rank6.getOrElse(""), + outfit.rank7.getOrElse(""), + ), + motd = outfit.motd.getOrElse(""), + unk10 = 0, + unk11 = true, + unk12 = 0, + created_timestamp = outfit.created.atZone(java.time.ZoneOffset.UTC).toInstant.toEpochMilli / 1000, + unk23 = 0, + unk24 = 0, + unk25 = 0 + ) + ) + ) + + zones.foreach(zone => { + zone.AllPlayers.filter(_.outfit_id == outfit_id).foreach(player => { + PlayerControl.sendResponse( + zone, player.Name, + outfit_event + ) + }) + }) + } + } + + // C >> S OutfitRequest(41593365, Motd(Vanu outfit for the planetside forever project! -find out more about the PSEMU project at PSforever.net)) + // S >> C OutfitEvent(Unk2, 529744, Unk2(OutfitInfo(PlanetSide_Forever_Vanu, 0, 0, 3, OutfitRankNames(, , , , , , , ), Vanu outfit for the planetside forever project! -find out more about the PSEMU project at PSforever.net, 0, 1, 0, 1458331641, 0, 0, 0))) + } + /* supporting functions */ def sanitizeOutfitName(name: String): Option[String] = { @@ -435,12 +567,14 @@ object SessionOutfitHandlers { for { deleted <- ctx.run( query[Outfitmember] - .filter(m => m.outfit_id == lift(outfit_id) && m.avatar_id == lift(avatar_id)) + .filter(_.outfit_id == lift(outfit_id)) + .filter(_.avatar_id == lift(avatar_id)) .delete ) updated <- ctx.run( query[Outfitpoint] - .filter(p => p.outfit_id == lift(outfit_id) && p.avatar_id == lift(avatarOpt)) + .filter(_.outfit_id == lift(outfit_id)) + .filter(_.avatar_id == lift(avatarOpt)) .update(_.avatar_id -> None) ) } yield (deleted, updated) @@ -455,6 +589,18 @@ object SessionOutfitHandlers { query[Outfitmember].filter(_.outfit_id == lift(id)).size } + def getOutfitMemberPoints(outfit_id: Long, avatar_id: Long): Future[Long] = { + val avatarOpt: Option[Long] = Some(avatar_id) + for { + points <- ctx.run( + query[Outfitpoint] + .filter(_.outfit_id == lift(outfit_id)) + .filter(_.avatar_id == lift(avatarOpt)) + .map(_.points) + ) + } yield (points.headOption.getOrElse(0)) + } + def getOutfitPoints(id: Long): Quoted[EntityQuery[OutfitpointMv]] = quote { querySchema[OutfitpointMv]("outfitpoint_mv").filter(_.outfit_id == lift(id)) } @@ -490,4 +636,49 @@ object SessionOutfitHandlers { (outfit.id, points.map(_.points).getOrElse(0L), outfit.name, leader.name, memberCounts.map(_._2).getOrElse(0L)) } } + + def updateMemberRankById(outfit_id: Long, avatar_id: Long, rank: Int): Quoted[Update[Outfitmember]] = quote { + query[Outfitmember] + .filter(_.outfit_id == lift(outfit_id)) + .filter(_.avatar_id == lift(avatar_id)) + .update(_.rank -> lift(rank)) + } + + def updateOutfitMemberRank(outfit_id: Long, avatar_id: Long, rank: Int): Future[Unit] = { + ctx.transaction { implicit ec => + for { + _ <- ctx.run(updateMemberRankById(outfit_id, avatar_id, rank)) + } yield () + } + } + + def updateOutfitOwnerById(outfit_id: Long, owner_id: Long): Quoted[Update[Outfit]] = quote { + query[Outfit] + .filter(_.id == lift(outfit_id)) + .update(_.owner_id -> lift(owner_id)) + } + + def updateOutfitOwner(outfit_id: Long, owner_id: Long, new_owner_id: Long): Future[Unit] = { + ctx.transaction { implicit ec => + for { + _ <- ctx.run(updateMemberRankById(outfit_id, owner_id, 6)) + _ <- ctx.run(updateMemberRankById(outfit_id, new_owner_id, 7)) + _ <- ctx.run(updateOutfitOwnerById(outfit_id, new_owner_id)) + } yield () + } + } + + def updateOutfitMotdById(outfit_id: Long, motd: Option[String]): Quoted[Update[Outfit]] = quote { + query[Outfit] + .filter(_.id == lift(outfit_id)) + .update(_.motd -> lift(motd)) + } + + def updateOutfitMotd(outfit_id: Long, motd: String): Future[Unit] = { + ctx.transaction { implicit ec => + for { + _ <- ctx.run(updateOutfitMotdById(outfit_id, Some(motd))) + } yield () + } + } } diff --git a/src/main/scala/net/psforever/packet/game/OutfitMembershipResponse.scala b/src/main/scala/net/psforever/packet/game/OutfitMembershipResponse.scala index ebde8270..90f59295 100644 --- a/src/main/scala/net/psforever/packet/game/OutfitMembershipResponse.scala +++ b/src/main/scala/net/psforever/packet/game/OutfitMembershipResponse.scala @@ -31,9 +31,9 @@ object OutfitMembershipResponse extends Marshallable[OutfitMembershipResponse] { type Type = Value val CreateResponse: PacketType.Value = Value(0) - val Invite: PacketType.Value = Value(1) // Info: Player has been invited / response to OutfitMembershipRequest Unk2 for that player - val Unk2: PacketType.Value = Value(2) // Invited / Accepted / Added - val Unk3: PacketType.Value = Value(3) + val Invite: PacketType.Value = Value(1) // response to OutfitMembershipRequest Unk2 for that player + val InviteAccepted: PacketType.Value = Value(2) + val InviteRejected: PacketType.Value = Value(3) val Unk4: PacketType.Value = Value(4) val Kick: PacketType.Value = Value(5) val Unk6: PacketType.Value = Value(6) // 6 and 7 seen as failed decodes, validity unknown diff --git a/src/main/scala/net/psforever/packet/game/OutfitRequest.scala b/src/main/scala/net/psforever/packet/game/OutfitRequest.scala index 7d6b4cd0..3e8d5f51 100644 --- a/src/main/scala/net/psforever/packet/game/OutfitRequest.scala +++ b/src/main/scala/net/psforever/packet/game/OutfitRequest.scala @@ -8,7 +8,7 @@ import scodec.codecs._ import shapeless.{::, HNil} final case class OutfitRequest( - id: Long, + outfit_id: Long, action: OutfitRequestAction ) extends PlanetSideGamePacket { type Packet = OutfitRequest diff --git a/src/test/scala/game/OutfitMembershipResponseTest.scala b/src/test/scala/game/OutfitMembershipResponseTest.scala index bd16286a..948cd41c 100644 --- a/src/test/scala/game/OutfitMembershipResponseTest.scala +++ b/src/test/scala/game/OutfitMembershipResponseTest.scala @@ -42,7 +42,7 @@ class OutfitMembershipResponseTest extends Specification { "decode unk1" in { PacketCoding.decodePacket(unk1).require match { case OutfitMembershipResponse(packet_type, unk0, unk1, outfit_id, target_id, str1, str2, flag) => - packet_type mustEqual PacketType.Unk1 + packet_type mustEqual PacketType.Invite unk0 mustEqual 0 unk1 mustEqual 0 outfit_id mustEqual 30383325 @@ -56,7 +56,7 @@ class OutfitMembershipResponseTest extends Specification { } "encode unk1" in { - val msg = OutfitMembershipResponse(PacketType.Unk1, 0, 0, 30383325, 41605870, "xNick", "PlanetSide_Forever_TR", flag = false) + val msg = OutfitMembershipResponse(PacketType.Invite, 0, 0, 30383325, 41605870, "xNick", "PlanetSide_Forever_TR", flag = false) val pkt = PacketCoding.encodePacket(msg).require.toByteVector pkt mustEqual unk1 @@ -65,7 +65,7 @@ class OutfitMembershipResponseTest extends Specification { "decode unk2" in { PacketCoding.decodePacket(unk2).require match { case OutfitMembershipResponse(packet_type, unk0, unk1, outfit_id, target_id, str1, str2, flag) => - packet_type mustEqual PacketType.Unk2 + packet_type mustEqual PacketType.InviteAccepted unk0 mustEqual 0 unk1 mustEqual 0 outfit_id mustEqual 41605156 @@ -79,7 +79,7 @@ class OutfitMembershipResponseTest extends Specification { } "encode unk2" in { - val msg = OutfitMembershipResponse(PacketType.Unk2, 0, 0, 41605156, 41593365, "Zergling92", "PlanetSide_Forever_Vanu", flag = false) + val msg = OutfitMembershipResponse(PacketType.InviteAccepted, 0, 0, 41605156, 41593365, "Zergling92", "PlanetSide_Forever_Vanu", flag = false) val pkt = PacketCoding.encodePacket(msg).require.toByteVector pkt mustEqual unk2 @@ -88,7 +88,7 @@ class OutfitMembershipResponseTest extends Specification { "decode unk3" in { PacketCoding.decodePacket(unk3).require match { case OutfitMembershipResponse(packet_type, unk0, unk1, outfit_id, target_id, str1, str2, flag) => - packet_type mustEqual PacketType.Unk3 + packet_type mustEqual PacketType.InviteRejected unk0 mustEqual 0 unk1 mustEqual 0 outfit_id mustEqual 41574772 @@ -102,7 +102,7 @@ class OutfitMembershipResponseTest extends Specification { } "encode unk3" in { - val msg = OutfitMembershipResponse(PacketType.Unk3, 0, 0, 41574772, 31156616, "", "", flag = true) + val msg = OutfitMembershipResponse(PacketType.InviteRejected, 0, 0, 41574772, 31156616, "", "", flag = true) val pkt = PacketCoding.encodePacket(msg).require.toByteVector pkt mustEqual unk3 @@ -134,7 +134,7 @@ class OutfitMembershipResponseTest extends Specification { "decode unk5" in { PacketCoding.decodePacket(unk5).require match { case OutfitMembershipResponse(packet_type, unk0, unk1, outfit_id, target_id, str1, str2, flag) => - packet_type mustEqual PacketType.Unk5 + packet_type mustEqual PacketType.Kick unk0 mustEqual 0 unk1 mustEqual 1 outfit_id mustEqual 41593365 @@ -148,7 +148,7 @@ class OutfitMembershipResponseTest extends Specification { } "encode unk5" in { - val msg = OutfitMembershipResponse(PacketType.Unk5, 0, 1, 41593365, 41605263, "PSFoutfittest1", "", flag = true) + val msg = OutfitMembershipResponse(PacketType.Kick, 0, 1, 41593365, 41605263, "PSFoutfittest1", "", flag = true) val pkt = PacketCoding.encodePacket(msg).require.toByteVector pkt mustEqual unk5 From 16900cd9184f4d2630c5b05a1f6209db03f76b68 Mon Sep 17 00:00:00 2001 From: ScrawnyRonnie Date: Fri, 29 Aug 2025 22:05:40 -0400 Subject: [PATCH 21/22] outfit persistence and minor fixes --- .../session/normal/AvatarHandlerLogic.scala | 5 +- .../support/SessionOutfitHandlers.scala | 78 +++++++++++++++---- .../session/support/ZoningOperations.scala | 4 +- .../services/avatar/AvatarService.scala | 8 ++ .../avatar/AvatarServiceResponse.scala | 3 +- 5 files changed, 79 insertions(+), 19 deletions(-) diff --git a/src/main/scala/net/psforever/actors/session/normal/AvatarHandlerLogic.scala b/src/main/scala/net/psforever/actors/session/normal/AvatarHandlerLogic.scala index e9936cda..c81be928 100644 --- a/src/main/scala/net/psforever/actors/session/normal/AvatarHandlerLogic.scala +++ b/src/main/scala/net/psforever/actors/session/normal/AvatarHandlerLogic.scala @@ -10,7 +10,7 @@ import net.psforever.objects.serverobject.containable.ContainableBehavior import net.psforever.objects.serverobject.mount.Mountable import net.psforever.objects.sourcing.PlayerSource import net.psforever.objects.vital.interaction.Adversarial -import net.psforever.packet.game.{AvatarImplantMessage, CreateShortcutMessage, ImplantAction} +import net.psforever.packet.game.{AvatarImplantMessage, CreateShortcutMessage, ImplantAction, PlanetsideStringAttributeMessage} import net.psforever.services.avatar.{AvatarAction, AvatarServiceMessage} import net.psforever.types.ImplantType @@ -252,6 +252,9 @@ class AvatarHandlerLogic(val ops: SessionAvatarHandlers, implicit val context: A case AvatarResponse.PlanetsideAttributeSelf(attributeType, attributeValue) if isSameTarget => sendResponse(PlanetsideAttributeMessage(guid, attributeType, attributeValue)) + case AvatarResponse.PlanetsideStringAttribute(attributeType, attributeValue) => + sendResponse(PlanetsideStringAttributeMessage(guid, attributeType, attributeValue)) + case AvatarResponse.GenericObjectAction(objectGuid, actionCode) if isNotSameTarget => sendResponse(GenericObjectActionMessage(objectGuid, actionCode)) diff --git a/src/main/scala/net/psforever/actors/session/support/SessionOutfitHandlers.scala b/src/main/scala/net/psforever/actors/session/support/SessionOutfitHandlers.scala index e9249d6d..b3bedc36 100644 --- a/src/main/scala/net/psforever/actors/session/support/SessionOutfitHandlers.scala +++ b/src/main/scala/net/psforever/actors/session/support/SessionOutfitHandlers.scala @@ -83,10 +83,10 @@ object SessionOutfitHandlers { player.outfit_name = outfit.name player.Zone.AvatarEvents ! AvatarServiceMessage(player.Zone.id, - AvatarAction.PlanetsideAttributeToAll(player.GUID, 39, player.outfit_id)) + AvatarAction.PlanetsideAttributeToAll(player.GUID, 39, outfit.id)) player.Zone.AvatarEvents ! AvatarServiceMessage(player.Zone.id, - AvatarAction.PlanetsideStringAttribute(player.GUID, 0, player.outfit_name)) + AvatarAction.PlanetsideStringAttribute(player.GUID, 0, outfit.name)) session.chat.JoinChannel(OutfitChannel(player.outfit_id)) } @@ -168,10 +168,10 @@ object SessionOutfitHandlers { invited.outfit_name = outfit.name invited.Zone.AvatarEvents ! AvatarServiceMessage(invited.Zone.id, - AvatarAction.PlanetsideAttributeToAll(invited.GUID, 39, invited.outfit_id)) + AvatarAction.PlanetsideAttributeToAll(invited.GUID, 39, outfit.id)) invited.Zone.AvatarEvents ! AvatarServiceMessage(invited.Zone.id, - AvatarAction.PlanetsideStringAttribute(invited.GUID, 0, invited.outfit_name)) + AvatarAction.PlanetsideStringAttribute(invited.GUID, 0, outfit.name)) case (None, _, _) => PlayerControl.sendResponse(invited.Zone, invited.Name, @@ -223,14 +223,11 @@ object SessionOutfitHandlers { kickedBy.outfit_name = "" kickedBy.outfit_id = 0 - val pZone = zones.filter(z => z.id == kickedBy.Zone.id).head - pZone.AllPlayers.filter(p => p.Faction == kickedBy.Faction).foreach { friendly => - PlayerControl.sendResponse(friendly.Zone, friendly.Name, - PlanetsideAttributeMessage(kickedBy.GUID, 39, 0)) + kickedBy.Zone.AvatarEvents ! AvatarServiceMessage(kickedBy.Zone.id, + AvatarAction.PlanetsideAttributeToAll(kickedBy.GUID, 39, 0)) - PlayerControl.sendResponse(friendly.Zone, friendly.Name, - PlanetsideStringAttributeMessage(kickedBy.GUID, 0, "")) - } + kickedBy.Zone.AvatarEvents ! AvatarServiceMessage(kickedBy.Zone.id, + AvatarAction.PlanetsideStringAttribute(kickedBy.GUID, 0, "")) } }.recover { case e => e.printStackTrace() @@ -426,11 +423,8 @@ object SessionOutfitHandlers { val outfit_id = player.outfit_id - // update MOTD - updateOutfitMotd(outfit_id, message) - - // TODO this does not notify clients with open windows. Do they update in the first place? val outfitDetails = for { + _ <- updateOutfitMotd(outfit_id, message) outfitOpt <- ctx.run(getOutfitById(outfit_id)).map(_.headOption) memberCount <- ctx.run(query[Outfitmember].filter(_.outfit_id == lift(outfit_id)).size) pointsTotal <- ctx.run(querySchema[OutfitpointMv]("outfitpoint_mv").filter(_.outfit_id == lift(outfit_id))) @@ -487,6 +481,56 @@ object SessionOutfitHandlers { // S >> C OutfitEvent(Unk2, 529744, Unk2(OutfitInfo(PlanetSide_Forever_Vanu, 0, 0, 3, OutfitRankNames(, , , , , , , ), Vanu outfit for the planetside forever project! -find out more about the PSEMU project at PSforever.net, 0, 1, 0, 1458331641, 0, 0, 0))) } + def HandleLoginOutfitCheck(player: Player, session: SessionData): Unit = { + ctx.run(getOutfitOnLogin(player.avatar.id)).flatMap { memberships => + memberships.headOption match { + case Some(membership) => + val outfitId = membership.outfit_id + (for { + outfitOpt <- ctx.run(getOutfitById(outfitId)).map(_.headOption) + memberCount <- ctx.run(getOutfitMemberCount(outfitId)) + points <- ctx.run(getOutfitPoints(outfitId)).map(_.headOption.map(_.points).getOrElse(0L)) + } yield (outfitOpt, memberCount, points)) + .map { + case (Some(outfit), memberCount, points) => + val seconds: Long = outfit.created.atZone(java.time.ZoneOffset.UTC).toInstant.toEpochMilli / 1000 + + PlayerControl.sendResponse(player.Zone, player.Name, + OutfitEvent(outfitId, Unk2(OutfitInfo( + outfit.name, points, points, memberCount, + OutfitRankNames(outfit.rank0.getOrElse(""), outfit.rank1.getOrElse(""), outfit.rank2.getOrElse(""), + outfit.rank3.getOrElse(""), outfit.rank4.getOrElse(""), outfit.rank5.getOrElse(""), + outfit.rank6.getOrElse(""), outfit.rank7.getOrElse("")), + outfit.motd.getOrElse(""), + 14, unk11 = true, 0, seconds, 0, 0, 0)))) + + PlayerControl.sendResponse(player.Zone, player.Name, + OutfitMemberUpdate(outfit.id, player.CharId, membership.rank, flag = true)) + + session.chat.JoinChannel(OutfitChannel(outfit.id)) + player.outfit_id = outfit.id + player.outfit_name = outfit.name + + player.Zone.AvatarEvents ! AvatarServiceMessage(player.Zone.id, + AvatarAction.PlanetsideAttributeToAll(player.GUID, 39, outfit.id)) + + player.Zone.AvatarEvents ! AvatarServiceMessage(player.Zone.id, + AvatarAction.PlanetsideStringAttribute(player.GUID, 0, outfit.name)) + + case (None, _, _) => + PlayerControl.sendResponse(player.Zone, player.Name, + ChatMsg(ChatMessageType.UNK_227, "Failed to load outfit")) + } + .recover { case _ => + PlayerControl.sendResponse(player.Zone, player.Name, + ChatMsg(ChatMessageType.UNK_227, "Failed to load outfit")) + } + case None => + Future.successful(()) + } + } + } + /* supporting functions */ def sanitizeOutfitName(name: String): Option[String] = { @@ -605,6 +649,10 @@ object SessionOutfitHandlers { querySchema[OutfitpointMv]("outfitpoint_mv").filter(_.outfit_id == lift(id)) } + def getOutfitOnLogin(avatarId: Long): Quoted[EntityQuery[Outfitmember]] = quote { + query[Outfitmember].filter(_.avatar_id == lift(avatarId)) + } + def getOutfitMembersWithDetails(outfitId: Long): Quoted[Query[(Long, String, Long, Int, LocalDateTime)]] = quote { query[Outfitmember] .filter(_.outfit_id == lift(outfitId)) diff --git a/src/main/scala/net/psforever/actors/session/support/ZoningOperations.scala b/src/main/scala/net/psforever/actors/session/support/ZoningOperations.scala index f9dd3a06..95502b19 100644 --- a/src/main/scala/net/psforever/actors/session/support/ZoningOperations.scala +++ b/src/main/scala/net/psforever/actors/session/support/ZoningOperations.scala @@ -2532,6 +2532,7 @@ class ZoningOperations( sessionLogic.general.toggleTeleportSystem(obj, TelepadLike.AppraiseTeleportationSystem(obj, continent)) } } + SessionOutfitHandlers.HandleLoginOutfitCheck(player, sessionLogic) //make weather happen sendResponse(WeatherMessage(List(),List( StormInfo(Vector3(0.1f, 0.15f, 0.0f), 240, 217), @@ -2655,6 +2656,7 @@ class ZoningOperations( log.debug(s"AvatarRejoin: ${player.Name} - $guid -> $data") } setupAvatarFunc = AvatarCreate + SessionOutfitHandlers.HandleLoginOutfitCheck(player, sessionLogic) //make weather happen sendResponse(WeatherMessage(List(),List( StormInfo(Vector3(0.1f, 0.15f, 0.0f), 240, 217), @@ -3196,8 +3198,6 @@ class ZoningOperations( continent.AllPlayers.filter(_.ExoSuit == ExoSuitType.MAX).foreach(max => sendResponse(PlanetsideAttributeMessage(max.GUID, 4, max.Armor))) // AvatarAwardMessage //populateAvatarAwardRibbonsFunc(1, 20L) - - sendResponse(PlanetsideStringAttributeMessage(guid, 0, "Outfit Name")) //squad stuff (loadouts, assignment) sessionLogic.squad.squadSetup() //MapObjectStateBlockMessage and ObjectCreateMessage? diff --git a/src/main/scala/net/psforever/services/avatar/AvatarService.scala b/src/main/scala/net/psforever/services/avatar/AvatarService.scala index f61e4394..54afc5bf 100644 --- a/src/main/scala/net/psforever/services/avatar/AvatarService.scala +++ b/src/main/scala/net/psforever/services/avatar/AvatarService.scala @@ -198,6 +198,14 @@ class AvatarService(zone: Zone) extends Actor { AvatarResponse.PlanetsideAttributeSelf(attribute_type, attribute_value) ) ) + case AvatarAction.PlanetsideStringAttribute(guid, attribute_type, attribute_value) => + AvatarEvents.publish( + AvatarServiceResponse( + s"/$forChannel/Avatar", + guid, + AvatarResponse.PlanetsideStringAttribute(attribute_type, attribute_value) + ) + ) case AvatarAction.PlayerState( guid, pos, diff --git a/src/main/scala/net/psforever/services/avatar/AvatarServiceResponse.scala b/src/main/scala/net/psforever/services/avatar/AvatarServiceResponse.scala index 5c26d9a0..cce788e7 100644 --- a/src/main/scala/net/psforever/services/avatar/AvatarServiceResponse.scala +++ b/src/main/scala/net/psforever/services/avatar/AvatarServiceResponse.scala @@ -47,7 +47,7 @@ object AvatarResponse { final case class EquipmentInHand(pkt: ObjectCreateMessage) extends Response final case class GenericObjectAction(object_guid: PlanetSideGUID, action_code: Int) extends Response final case class HitHint(source_guid: PlanetSideGUID) extends Response - final case class Killed(cause: DamageResult, mount_guid: Option[PlanetSideGUID]) extends Response + final case class Killed(cause: DamageResult, mount_guid: Option[PlanetSideGUID]) extends Response final case class LoadPlayer(pkt: ObjectCreateMessage) extends Response final case class LoadProjectile(pkt: ObjectCreateMessage) extends Response final case class ObjectDelete(item_guid: PlanetSideGUID, unk: Int) extends Response @@ -56,6 +56,7 @@ object AvatarResponse { final case class PlanetsideAttribute(attribute_type: Int, attribute_value: Long) extends Response final case class PlanetsideAttributeToAll(attribute_type: Int, attribute_value: Long) extends Response final case class PlanetsideAttributeSelf(attribute_type: Int, attribute_value: Long) extends Response + final case class PlanetsideStringAttribute(attribute_type: Int, attribute_value: String) extends Response final case class PlayerState( pos: Vector3, vel: Option[Vector3], From e5909bdac3a05a40591acc9bc1e9dfd7353b32f0 Mon Sep 17 00:00:00 2001 From: ScrawnyRonnie Date: Sat, 30 Aug 2025 08:03:40 -0400 Subject: [PATCH 22/22] gain outfit points --- .../actors/session/AvatarActor.scala | 31 +++++++++++++++++++ 1 file changed, 31 insertions(+) diff --git a/src/main/scala/net/psforever/actors/session/AvatarActor.scala b/src/main/scala/net/psforever/actors/session/AvatarActor.scala index 12291c4a..a39541c9 100644 --- a/src/main/scala/net/psforever/actors/session/AvatarActor.scala +++ b/src/main/scala/net/psforever/actors/session/AvatarActor.scala @@ -253,6 +253,8 @@ object AvatarActor { final case class SupportExperienceDeposit(bep: Long, delay: Long) extends Command + case class Outfitpoint(id: Long, outfit_id: Long, avatar_id: Option[Long], points: Long) + /** * A player loadout represents all of the items in the player's hands (equipment slots) * and all of the items in the player's backpack (inventory) @@ -970,6 +972,22 @@ object AvatarActor { } } + def setOutfitPoints(avatarId: Long, exp: Long): Future[Unit] = { + import ctx._ + import scala.concurrent.ExecutionContext.Implicits.global + val avatarOpt: Option[Long] = Some(avatarId) + ctx.transaction { implicit ec => + for { + currOp <- ctx.run(query[Outfitpoint].filter(_.avatar_id == lift(avatarOpt)).map(_.points)) + .map(_.headOption.getOrElse(0L)) + + newOp = currOp + exp + + _ <- ctx.run(query[Outfitpoint].filter(_.avatar_id == lift(avatarOpt)).update(_.points -> lift(newOp))) + } yield () + } + } + def loadExperienceDebt(avatarId: Long): Future[Long] = { import ctx._ import scala.concurrent.ExecutionContext.Implicits.global @@ -2994,6 +3012,13 @@ class AvatarActor( } } avatar = avatar.copy(bep = newBep, implants = implants) + + if (player.outfit_id != 0) { + setOutfitPoints(player.avatar.id, bep).onComplete { + case Success(_) => + case Failure(exception) => log.error(exception)("db failure") + } + } case Failure(exception) => log.error(exception)("db failure") } @@ -3010,6 +3035,12 @@ class AvatarActor( zone.id, AvatarAction.PlanetsideAttributeToAll(sess.player.GUID, 18, cep) ) + if (sess.player.outfit_id != 0) { + setOutfitPoints(sess.player.avatar.id, cep * 2).onComplete { + case Success(_) => + case Failure(exception) => log.error(exception)("db failure") + } + } case Failure(exception) => log.error(exception)("db failure") }