From f977ea8e6fd59a2dc0a62aef973131bf61b0b0c0 Mon Sep 17 00:00:00 2001 From: Resaec Date: Sun, 10 Aug 2025 22:57:01 +0200 Subject: [PATCH 01/33] 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 6bc06e6d3..0253d2b80 100644 --- a/src/main/scala/net/psforever/packet/GamePacketOpcode.scala +++ b/src/main/scala/net/psforever/packet/GamePacketOpcode.scala @@ -1,4 +1,4 @@ -// Copyright (c) 2017 PSForever +// Copyright (c) 2017-2025 PSForever package net.psforever.packet import scodec.{Attempt, Codec, DecodeResult, Err} @@ -469,13 +469,13 @@ object GamePacketOpcode extends Enumeration { case 0x8a => game.PlayerStasisMessage.decode case 0x8b => noDecoder(UnknownMessage139) case 0x8c => game.OutfitMembershipRequest.decode - case 0x8d => noDecoder(OutfitMembershipResponse) + case 0x8d => game.OutfitMembershipResponse.decode case 0x8e => game.OutfitRequest.decode - case 0x8f => noDecoder(OutfitEvent) + case 0x8f => game.OutfitEvent.decode // OPCODES 0x90-9f - case 0x90 => noDecoder(OutfitMemberEvent) - case 0x91 => noDecoder(OutfitMemberUpdate) + case 0x90 => game.OutfitMemberEvent.decode + case 0x91 => game.OutfitMemberUpdate.decode case 0x92 => game.PlanetsideStringAttributeMessage.decode case 0x93 => game.DataChallengeMessage.decode case 0x94 => game.DataChallengeMessageResp.decode @@ -483,7 +483,7 @@ object GamePacketOpcode extends Enumeration { case 0x96 => game.SimDataChallenge.decode case 0x97 => game.SimDataChallengeResp.decode // 0x98 - case 0x98 => noDecoder(OutfitListEvent) + case 0x98 => game.OutfitListEvent.decode case 0x99 => noDecoder(EmpireIncentivesMessage) case 0x9a => game.InvalidTerrainMessage.decode case 0x9b => noDecoder(SyncMessage) diff --git a/src/main/scala/net/psforever/packet/game/OutfitEvent.scala b/src/main/scala/net/psforever/packet/game/OutfitEvent.scala new file mode 100644 index 000000000..cfbd647b9 --- /dev/null +++ b/src/main/scala/net/psforever/packet/game/OutfitEvent.scala @@ -0,0 +1,358 @@ +// Copyright (c) 2025 PSForever +package net.psforever.packet.game + +import net.psforever.packet.GamePacketOpcode.Type +import net.psforever.packet.{GamePacketOpcode, Marshallable, PacketHelpers, PlanetSideGamePacket} +import net.psforever.types.PlanetSideGUID +import scodec.{Attempt, Codec, Err} +import scodec.bits.BitVector +import scodec.codecs._ +import shapeless.{::, HNil} + +final case class OutfitEvent( + request_type: OutfitEvent.RequestType.Type, + outfit_guid: PlanetSideGUID, + action: OutfitEventAction + ) extends PlanetSideGamePacket { + type Packet = OutfitEvent + + def opcode: Type = GamePacketOpcode.OutfitEvent + + def encode: Attempt[BitVector] = OutfitEvent.encode(this) +} + +abstract class OutfitEventAction(val code: Int) + +object OutfitEventAction { + + final case class OutfitRankNames( + rank1: String, + rank2: String, + rank3: String, + rank4: String, + rank5: String, + rank6: String, + rank7: String, + rank8: String, + ) + + final case class OutfitInfo( + unk1: Int, + unk2: Int, + outfit_name: String, + unk6: Long, + unk7: Long, + member_count: Int, + unk9: Int, + outfit_rank_names: OutfitRankNames, + motd: String, + owner_guid: PlanetSideGUID, // ? + unk20: Int, + unk21: Int, + unk21_2: Int, + created_timestamp: Long, + unk23: Long, + unk24: Long, + unk25: Long, + u123: Int, + ) + + final case class Unk0( + outfitInfo: OutfitInfo + ) extends OutfitEventAction(code = 0) + + final case class Unk1( + unk0: Int, + unk1: Int, + unk2: Int, + unk3: Boolean, + ) extends OutfitEventAction(code = 1) + + final case class Unk2( + outfitInfo: OutfitInfo, + ) extends OutfitEventAction(code = 2) + + final case class Unk3( + unk0: Int, + unk1: Int, + unk2: Int, + unk3: Boolean, + data: BitVector, + ) extends OutfitEventAction(code = 3) + + final case class Unk4( + unk0: Int, + unk1: Int, + unk2: Int, + unk3: Int, + unk4: Boolean, + data: BitVector, + ) extends OutfitEventAction(code = 4) + + final case class Unk5( + unk0: Int, + unk1: Int, + unk2: Int, + unk3: Int, + unk4: Boolean, + data: BitVector, + ) extends OutfitEventAction(code = 5) + + final case class Unknown(badCode: Int, data: BitVector) extends OutfitEventAction(badCode) + + /** + * The `Codec`s used to transform the input stream into the context of a specific action + * and extract the field data from that stream. + */ + object Codecs { + private val everFailCondition = conditional(included = false, bool) + + private val OutfitRankNamesCodec: Codec[OutfitRankNames] = ( + PacketHelpers.encodedWideString :: + PacketHelpers.encodedWideString :: + PacketHelpers.encodedWideString :: + PacketHelpers.encodedWideString :: + PacketHelpers.encodedWideString :: + PacketHelpers.encodedWideString :: + PacketHelpers.encodedWideString :: + PacketHelpers.encodedWideString + ).xmap[OutfitRankNames]( + { + case u0 :: u1 :: u2 :: u3 :: u4 :: u5 :: u6 :: u7 :: HNil => + OutfitRankNames(u0, u1, u2, u3, u4, u5, u6, u7) + }, + { + case OutfitRankNames(u0, u1, u2, u3, u4, u5, u6, u7) => + u0 :: u1 :: u2 :: u3 :: u4 :: u5 :: u6 :: u7 :: HNil + } + ) + + private val InfoCodec: Codec[OutfitInfo] = ( + uint8L :: + uint8L :: + PacketHelpers.encodedWideStringAligned(5) :: + uint32L :: + uint32L :: + uint16L :: + uint16L :: + OutfitRankNamesCodec :: + PacketHelpers.encodedWideString :: + PlanetSideGUID.codec :: + uint16L :: // + uint8L :: // bool somewhere here + uintL(1) :: // + ("created_timestamp" | uint32L) :: + uint32L :: + uint32L :: + uint32L :: + uintL(7) + ).xmap[OutfitInfo]( + { + case u1 :: u2 :: outfit_name :: u6 :: u7 :: member_count :: u9 :: outfit_rank_names :: motd :: u19 :: u20 :: u21 :: u21_2 :: created_timestamp :: u23 :: u24 :: u25 :: u123 :: HNil => + OutfitInfo(u1, u2, outfit_name, u6, u7, member_count, u9, outfit_rank_names, motd, u19, u20, u21, u21_2, created_timestamp, u23, u24, u25, u123) + }, + { + case OutfitInfo(u1, u2, outfit_name, u6, u7, member_count, u9, outfit_rank_names, motd, u19, u20, u21, u21_2, created_timestamp, u23, u24, u25, u123) => + u1 :: u2 :: outfit_name :: u6 :: u7 :: member_count :: u9 :: outfit_rank_names :: motd :: u19 :: u20 :: u21 :: u21_2 :: created_timestamp :: u23 :: u24 :: u25 :: u123 :: HNil + } + ) + + val Unk0Codec: Codec[Unk0] = ( + InfoCodec + ).xmap[Unk0]( + { + case info => + Unk0(info) + }, + { + case Unk0(info) => + info + } + ) + + val Unk1Codec: Codec[Unk1] = ( + uint8L :: + uint8L :: + uint4L :: + bool + ).xmap[Unk1]( + { + case u0 :: u1 :: u2 :: u3 :: HNil => + Unk1(u0, u1, u2, u3) + }, + { + case Unk1(u0, u1, u2, u3) => + u0 :: u1 :: u2 :: u3 :: HNil + } + ) + + val Unk2Codec: Codec[Unk2] = ( + InfoCodec + ).xmap[Unk2]( + { + case info => + Unk2(info) + }, + { + case Unk2(info) => + info + } + ) + + val Unk3Codec: Codec[Unk3] = ( + uint8L :: + uint8L :: + uint4L :: + bool :: + bits + ).xmap[Unk3]( + { + case u0 :: u1 :: u2 :: u3 :: data :: HNil => + Unk3(u0, u1, u2, u3, data) + }, + { + case Unk3(u0, u1, u2, u3, data) => + u0 :: u1 :: u2 :: u3 :: data :: HNil + } + ) + + val Unk4Codec: Codec[Unk4] = ( + uint16L :: + uint16L :: + uint16L :: + uint4L :: + bool :: + bits + ).xmap[Unk4]( + { + case u0 :: u1 :: u2 :: u3 :: u4 :: data :: HNil => + Unk4(u0, u1, u2, u3, u4, data) + }, + { + case Unk4(u0, u1, u2, u3, u4, data) => + u0 :: u1 :: u2 ::u3 :: u4 :: data :: HNil + } + ) + + val Unk5Codec: Codec[Unk5] = ( + uint16L :: + uint16L :: + uint16L :: + uint4L :: + bool :: + bits + ).xmap[Unk5]( + { + case u0 :: u1 :: u2 :: u3 :: u4 :: data :: HNil => + Unk5(u0, u1, u2, u3, u4, data) + }, + { + case Unk5(u0, u1, u2, u3, u4, data) => + u0 :: u1 :: u2 :: u3 :: u4 :: data :: HNil + } + ) + + /** + * A common form for known action code indexes with an unknown purpose and transformation is an "Unknown" object. + * + * @param action the action behavior code + * @return a transformation between the action code and the unknown bit data + */ + def unknownCodec(action: Int): Codec[Unknown] = + bits.xmap[Unknown]( + data => Unknown(action, data), + { + case Unknown(_, data) => data + } + ) + + /** + * The action code was completely unanticipated! + * + * @param action the action behavior code + * @return nothing; always fail + */ + def failureCodec(action: Int): Codec[OutfitEventAction] = + everFailCondition.exmap[OutfitEventAction]( + _ => Attempt.failure(Err(s"can not match a codec pattern for decoding $action")), + _ => Attempt.failure(Err(s"can not match a codec pattern for encoding $action")) + ) + } +} + +object OutfitEvent extends Marshallable[OutfitEvent] { + + object RequestType extends Enumeration { + type Type = Value + + val Unk0: RequestType.Value = Value(0) // start listing of members + val Unk1: RequestType.Value = Value(1) // end listing of members + val Unk2: RequestType.Value = Value(2) // send after creating an outfit // normal info, same as Unk0 + val Unk3: RequestType.Value = Value(3) // below + val Unk4: RequestType.Value = Value(4) + val Unk5: RequestType.Value = Value(5) + val unk6: RequestType.Value = Value(6) + val unk7: RequestType.Value = Value(7) + + /* + + OutfitEvent(Unk0, ValidPlanetSideGUID(18361), Unk0(OutfitInfo(0, 0, The Black Ravens, 338420223, 338420223, 433, 0, OutfitRankNames(Corporal (No Ventrilo), Sergeant - SGT, Advance Medical, , Master Sgt - MSG, Captain, Trusted Officer, OutFit Leader), TBR website..... http://trravens.darkbb.com ventrilo info: evolve.typefrag.com port: 45694 (vent pw dotaftw) Channel PW: zeroenigma : if you guys wants to contact me, my email is zero_overkill99@yahoo.com, ValidPlanetSideGUID(32787), 0, 0, 0, 1133571390, 0, 0, 0, 0))) + OutfitEvent(Unk2, ValidPlanetSideGUID(18361), Unk2(OutfitInfo(0, 0, The Black Ravens, 338420486, 338420486, 433, 0, OutfitRankNames(Corporal (No Ventrilo), Sergeant - SGT, Advance Medical, , Master Sgt - MSG, Captain, Trusted Officer, OutFit Leader), TBR website..... http://trravens.darkbb.com ventrilo info: evolve.typefrag.com port: 45694 (vent pw dotaftw) Channel PW: zeroenigma : if you guys wants to contact me, my email is zero_overkill99@yahoo.com, ValidPlanetSideGUID(32787), 0, 0, 0, 1133571390, 0, 0, 0, 0))) + + + unk3 -- #66162 PSCap-2016-02-28_02-58-10-PM.txt + + MP( + SMP( + MPEx( + OutfitMembershipResponse, + OutfitEvent, + SquadMemberEvent + ) + ), + SMP( + MPEx( + PlanetsideAttributeMessage x 3 + PlanetsideStringAttributeMessage + ) + ) + ) + ) + */ + + implicit val codec: Codec[Type] = PacketHelpers.createEnumerationCodec(this, uintL(3)) + } + + private def selectFromType(code: Int): Codec[OutfitEventAction] = { + import OutfitEventAction.Codecs._ + import scala.annotation.switch + + ((code: @switch) match { + case 0 => Unk0Codec + case 1 => Unk1Codec + case 2 => Unk2Codec // sent after /outfitcreate ? + case 3 => Unk3Codec + case 4 => Unk4Codec + case 5 => Unk5Codec + case 6 => unknownCodec(action = code) + case 7 => unknownCodec(action = code) + + case _ => failureCodec(code) + }).asInstanceOf[Codec[OutfitEventAction]] + } + + implicit val codec: Codec[OutfitEvent] = ( + ("request_type" | RequestType.codec) >>:~ { request_type => + ("outfit_guid" | PlanetSideGUID.codec) :: + ("action" | selectFromType(request_type.id)) + } + ).xmap[OutfitEvent]( + { + case request_type :: outfit_guid :: action :: HNil => + OutfitEvent(request_type, outfit_guid, action) + }, + { + case OutfitEvent(request_type, outfit_guid, action) => + request_type :: outfit_guid :: action :: HNil + } + ) +} diff --git a/src/main/scala/net/psforever/packet/game/OutfitListEvent.scala b/src/main/scala/net/psforever/packet/game/OutfitListEvent.scala new file mode 100644 index 000000000..52ea9929e --- /dev/null +++ b/src/main/scala/net/psforever/packet/game/OutfitListEvent.scala @@ -0,0 +1,35 @@ +// Copyright (c) 2025 PSForever +package net.psforever.packet.game + +import net.psforever.packet.GamePacketOpcode.Type +import net.psforever.packet.{GamePacketOpcode, Marshallable, PacketHelpers, PlanetSideGamePacket} +import scodec.bits.BitVector +import scodec.codecs._ +import scodec.{Attempt, Codec} + +// 98 5ec300000d01a020004000001 12056002e0053002e0053002e0055002e004400 845400680069006f009 85ee300001e2b 858000800000110041002e0027002e0041002e0027002e00 8448006900720075009 84003200005a540000060000011a0530065006300720065007400200043006800690065006600730085530069006c00610073009840a32000001953476fe0c00011c041007a0075007200650020005400770069006c006900670068007400874600720061006e0063006b006f009840c3200000d3a4c000c00000106030002e006f0085410074006c0061007300984183200011d9296000c0000011e0570061007200720069006f007200270073002000430072006500650064008653006500760061006b00690098442320001bf40e000080000013203100330033003700740068002000410072006d006f0072006500640020004400690076006900730069006f006e002d004b008548006f0073002d004b009844c320001b3d2c200060000012a03300330031007300740020004d0069006e006e00650073006f0074006100200054007200690062006500864d006100670069002d0045009846c3200009e206c00040000010c04100720065006100350031008942006c00610063 + +final case class OutfitListEvent( + outfit_score: Long, + unk1: Long, + unk2: Long, + unk3: Int, + outfit_name: String, + outfit_leader: String, + ) extends PlanetSideGamePacket { + type Packet = OutfitListEvent + + def opcode: Type = GamePacketOpcode.OutfitListEvent + + def encode: Attempt[BitVector] = OutfitListEvent.encode(this) +} +object OutfitListEvent extends Marshallable[OutfitListEvent] { + implicit val codec: Codec[OutfitListEvent] = ( + ("outfit_score" | uint32) :: + ("unk1" | uint32L) :: + ("unk2" | uint32L) :: + ("unk3" | uint(3)) :: + ("outfit_name" | PacketHelpers.encodedWideStringAligned(5)) :: + ("outfit_leader" | PacketHelpers.encodedWideString) + ).as[OutfitListEvent] +} diff --git a/src/main/scala/net/psforever/packet/game/OutfitMemberEvent.scala b/src/main/scala/net/psforever/packet/game/OutfitMemberEvent.scala new file mode 100644 index 000000000..737a72da8 --- /dev/null +++ b/src/main/scala/net/psforever/packet/game/OutfitMemberEvent.scala @@ -0,0 +1,52 @@ +// Copyright (c) 2025 PSForever +package net.psforever.packet.game + +import net.psforever.packet.{GamePacketOpcode, Marshallable, PacketHelpers, PlanetSideGamePacket} +import scodec.Codec +import scodec.codecs._ +import shapeless.{::, HNil} + +final case class OutfitMemberEvent( + unk00: Int, + outfit_id: Long, + unk3: Int, + unk4: Int, + unk5: Int, + unk6: Int, + member_name: String, + unk7: Int, + unk8: Int, + unk9: Int, + unk10: Int, + unk11: Int, +) extends PlanetSideGamePacket { + type Packet = OutfitMemberEvent + def opcode = GamePacketOpcode.OutfitMemberEvent + def encode = OutfitMemberEvent.encode(this) +} + +object OutfitMemberEvent extends Marshallable[OutfitMemberEvent] { + implicit val codec: Codec[OutfitMemberEvent] = ( + ("unk00" | uintL(2)) :: + ("outfit_id" | uint32L) :: + ("unk3" | uint8L) :: + ("unk4" | uint8L) :: + ("unk5" | uint8L) :: + ("unk6" | uint8L) :: + ("member_name" | PacketHelpers.encodedWideStringAligned(6)) :: + ("unk7" | uint16L) :: + ("unk8" | uint16L) :: + ("unk9" | uint16L) :: + ("unk10" | uint16L) :: + ("unk11" | uint8L) + ).xmap[OutfitMemberEvent]( + { + case unk00 :: outfit_id :: u3 :: u4 :: u5 :: u6 :: member_name :: u7 :: u8 :: u9 :: u10 :: u11 :: HNil => + OutfitMemberEvent(unk00, outfit_id, u3, u4, u5, u6, member_name, u7, u8, u9, u10, u11) + }, + { + case OutfitMemberEvent(unk00, outfit_id, u3, u4, u5, u6, member_name, u7, u8, u9, u10, u11) => + unk00 :: outfit_id :: u3 :: u4 :: u5 :: u6 :: member_name :: u7 :: u8 :: u9 :: u10 :: u11 :: HNil + } + ) +} diff --git a/src/main/scala/net/psforever/packet/game/OutfitMemberUpdate.scala b/src/main/scala/net/psforever/packet/game/OutfitMemberUpdate.scala new file mode 100644 index 000000000..9fc1a72ee --- /dev/null +++ b/src/main/scala/net/psforever/packet/game/OutfitMemberUpdate.scala @@ -0,0 +1,37 @@ +// Copyright (c) 2025 PSForever +package net.psforever.packet.game + +import net.psforever.packet.{GamePacketOpcode, Marshallable, PlanetSideGamePacket} +import net.psforever.types.PlanetSideGUID +import scodec.Codec +import scodec.codecs._ +import shapeless.{::, HNil} + +final case class OutfitMemberUpdate( + outfit_guid: PlanetSideGUID, + unk1: Int, + avatar_guid: PlanetSideGUID, + unk3: Int, +) extends PlanetSideGamePacket { + type Packet = OutfitMemberUpdate + def opcode = GamePacketOpcode.OutfitMemberUpdate + def encode = OutfitMemberUpdate.encode(this) +} + +object OutfitMemberUpdate extends Marshallable[OutfitMemberUpdate] { + implicit val codec: Codec[OutfitMemberUpdate] = ( + ("outfit_guid" | PlanetSideGUID.codec) :: + ("unk1" | uint16L) :: + ("avatar_guid" | PlanetSideGUID.codec) :: + ("unk3" | uint8L) + ).xmap[OutfitMemberUpdate]( + { + case outfit_guid :: u1 :: u2 :: u3 :: HNil => + OutfitMemberUpdate(outfit_guid, u1, u2, u3) + }, + { + case OutfitMemberUpdate(outfit_guid, u1, u2, u3) => + outfit_guid :: u1 :: u2 :: u3 :: HNil + } + ) +} diff --git a/src/main/scala/net/psforever/packet/game/OutfitMembershipRequest.scala b/src/main/scala/net/psforever/packet/game/OutfitMembershipRequest.scala index 55989fc66..b64a68cec 100644 --- a/src/main/scala/net/psforever/packet/game/OutfitMembershipRequest.scala +++ b/src/main/scala/net/psforever/packet/game/OutfitMembershipRequest.scala @@ -1,4 +1,4 @@ -// Copyright (c) 2023 PSForever +// Copyright (c) 2023-2025 PSForever package net.psforever.packet.game import net.psforever.packet.GamePacketOpcode.Type @@ -11,9 +11,9 @@ import shapeless.{::, HNil} final case class OutfitMembershipRequest( request_type: OutfitMembershipRequest.RequestType.Type, - avatar_guid: PlanetSideGUID, - unk1: Int, - action: OutfitAction + avatar_guid: PlanetSideGUID, // avatar_guid and unk1 are related, might be Long instead + unk1: Int, // + action: OutfitMembershipRequestAction ) extends PlanetSideGamePacket { type Packet = OutfitMembershipRequest @@ -22,20 +22,21 @@ final case class OutfitMembershipRequest( def encode: Attempt[BitVector] = OutfitMembershipRequest.encode(this) } -abstract class OutfitAction(val code: Int) -object OutfitAction { +abstract class OutfitMembershipRequestAction(val code: Int) - final case class CreateOutfit(unk2: String, unk3: Int, unk4: Boolean, outfit_name: String) extends OutfitAction(code = 0) +object OutfitMembershipRequestAction { - final case class FormOutfit(unk2: String, unk3: Int, unk4: Boolean, outfit_name: String) extends OutfitAction(code = 1) + final case class CreateOutfit(unk2: String, unk3: Int, unk4: Boolean, outfit_name: String) extends OutfitMembershipRequestAction(code = 0) - final case class AcceptOutfitInvite(unk2: String) extends OutfitAction(code = 3) + final case class FormOutfit(unk2: String, unk3: Int, unk4: Boolean, outfit_name: String) extends OutfitMembershipRequestAction(code = 1) - final case class RejectOutfitInvite(unk2: String) extends OutfitAction(code = 4) + final case class AcceptOutfitInvite(unk2: String) extends OutfitMembershipRequestAction(code = 3) - final case class CancelOutfitInvite(unk5: Int, unk6: Int, outfit_name: String) extends OutfitAction(code = 5) + final case class RejectOutfitInvite(unk2: String) extends OutfitMembershipRequestAction(code = 4) - final case class Unknown(badCode: Int, data: BitVector) extends OutfitAction(badCode) + final case class CancelOutfitInvite(unk5: Int, unk6: Int, outfit_name: String) extends OutfitMembershipRequestAction(code = 5) + + final case class Unknown(badCode: Int, data: BitVector) extends OutfitMembershipRequestAction(badCode) /** * The `Codec`s used to transform the input stream into the context of a specific action @@ -106,6 +107,7 @@ object OutfitAction { /** * A common form for known action code indexes with an unknown purpose and transformation is an "Unknown" object. + * * @param action the action behavior code * @return a transformation between the action code and the unknown bit data */ @@ -119,11 +121,12 @@ object OutfitAction { /** * The action code was completely unanticipated! + * * @param action the action behavior code * @return nothing; always fail */ - def failureCodec(action: Int): Codec[OutfitAction] = - everFailCondition.exmap[OutfitAction]( + def failureCodec(action: Int): Codec[OutfitMembershipRequestAction] = + everFailCondition.exmap[OutfitMembershipRequestAction]( _ => Attempt.failure(Err(s"can not match a codec pattern for decoding $action")), _ => Attempt.failure(Err(s"can not match a codec pattern for encoding $action")) ) @@ -147,8 +150,8 @@ object OutfitMembershipRequest extends Marshallable[OutfitMembershipRequest] { implicit val codec: Codec[Type] = PacketHelpers.createEnumerationCodec(this, uintL(3)) } - private def selectFromType(code: Int): Codec[OutfitAction] = { - import OutfitAction.Codecs._ + private def selectFromType(code: Int): Codec[OutfitMembershipRequestAction] = { + import OutfitMembershipRequestAction.Codecs._ import scala.annotation.switch ((code: @switch) match { @@ -162,7 +165,7 @@ object OutfitMembershipRequest extends Marshallable[OutfitMembershipRequest] { case 7 => unknownCodec(action = code) // 3 bit limit case _ => failureCodec(code) - }).asInstanceOf[Codec[OutfitAction]] + }).asInstanceOf[Codec[OutfitMembershipRequestAction]] } implicit val codec: Codec[OutfitMembershipRequest] = ( diff --git a/src/main/scala/net/psforever/packet/game/OutfitMembershipResponse.scala b/src/main/scala/net/psforever/packet/game/OutfitMembershipResponse.scala new file mode 100644 index 000000000..0e9430e65 --- /dev/null +++ b/src/main/scala/net/psforever/packet/game/OutfitMembershipResponse.scala @@ -0,0 +1,243 @@ +// Copyright (c) 2025 PSForever +package net.psforever.packet.game + +import net.psforever.packet.GamePacketOpcode.Type +import net.psforever.packet.{GamePacketOpcode, Marshallable, PacketHelpers, PlanetSideGamePacket} +import net.psforever.types.PlanetSideGUID +import scodec.{Attempt, Codec, Err} +import scodec.bits.BitVector +import scodec.codecs._ +import shapeless.{::, HNil} + +final case class OutfitMembershipResponse( + response_type: OutfitMembershipResponse.ResponseType.Type, + unk0: Int, + avatar_guid: PlanetSideGUID, // avatar_guid and unk1 are related, might be Long instead + unk1: PlanetSideGUID, // + unk2: PlanetSideGUID, + unk3: Int, + //unk4: Boolean, + action: OutfitMembershipResponseAction + ) extends PlanetSideGamePacket { + type Packet = OutfitMembershipResponse + + def opcode: Type = GamePacketOpcode.OutfitMembershipResponse + + def encode: Attempt[BitVector] = OutfitMembershipResponse.encode(this) +} + +abstract class OutfitMembershipResponseAction(val code: Int) +object OutfitMembershipResponseAction { + + final case class CreateOutfitResponse(str1: String, str2: String, str3: String) extends OutfitMembershipResponseAction(code = 0) + + final case class Unk1OutfitResponse(player_name: String, outfit_name: String, unk7: Int) extends OutfitMembershipResponseAction(code = 1) + + final case class Unk2OutfitResponse(player_name: String, outfit_name: String, unk7: Int) extends OutfitMembershipResponseAction(code = 2) // unk7 = rank? + + final case class Unk3OutfitResponse(unk2: String) extends OutfitMembershipResponseAction(code = 3) + + final case class Unk4OutfitResponse(unk5: Int, unk6: Int, outfit_name: String) extends OutfitMembershipResponseAction(code = 4) + + final case class Unk5OutfitResponse() extends OutfitMembershipResponseAction(code = 5) + + final case class Unk6OutfitResponse() extends OutfitMembershipResponseAction(code = 6) + + final case class Unk7OutfitResponse() extends OutfitMembershipResponseAction(code = 7) + + final case class Unknown(badCode: Int, data: BitVector) extends OutfitMembershipResponseAction(badCode) + + /** + * The `Codec`s used to transform the input stream into the context of a specific action + * and extract the field data from that stream. + */ + object Codecs { + private val everFailCondition = conditional(included = false, bool) + + val Unk0OutfitCodec: Codec[CreateOutfitResponse] = ( + PacketHelpers.encodedWideStringAligned(5) :: + PacketHelpers.encodedWideString :: + PacketHelpers.encodedWideString + ).xmap[CreateOutfitResponse]( + { + case str1 :: str2 :: str3 :: HNil => + CreateOutfitResponse(str1, str2, str3) + }, + { + case CreateOutfitResponse(str1, str2, str3) => + str1 :: str2 :: str3 :: HNil + } + ) + + val Unk1OutfitCodec: Codec[Unk1OutfitResponse] = ( + PacketHelpers.encodedWideStringAligned(5) :: + PacketHelpers.encodedWideString :: + uint8L + ).xmap[Unk1OutfitResponse]( + { + case player_name :: outfit_name :: u7 :: HNil => + Unk1OutfitResponse(player_name, outfit_name, u7) + }, + { + case Unk1OutfitResponse(player_name, outfit_name, u7) => + player_name :: outfit_name :: u7 :: HNil + } + ) + + val Unk2OutfitCodec: Codec[Unk2OutfitResponse] = ( + PacketHelpers.encodedWideStringAligned(5) :: + PacketHelpers.encodedWideString :: + uint8L + ).xmap[Unk2OutfitResponse]( + { + case player_name :: outfit_name :: u7 :: HNil => + Unk2OutfitResponse(player_name, outfit_name, u7) + }, + { + case Unk2OutfitResponse(player_name, outfit_name, u7) => + player_name :: outfit_name :: u7 :: HNil + } + ) + + val Unk3OutfitCodec: Codec[Unk3OutfitResponse] = + PacketHelpers.encodedWideString.xmap[Unk3OutfitResponse]( + { + case unk2 => + Unk3OutfitResponse(unk2) + }, + { + case Unk3OutfitResponse(unk2) => + unk2 + } + ) + + val Unk4OutfitCodec: Codec[Unk4OutfitResponse] = + (uint16L :: uint16L :: PacketHelpers.encodedWideStringAligned(5)).xmap[Unk4OutfitResponse]( + { + case unk5 :: unk6 :: outfit_name :: HNil => + Unk4OutfitResponse(unk5, unk6, outfit_name) + }, + { + case Unk4OutfitResponse(unk5, unk6, outfit_name) => + unk5 :: unk6 :: outfit_name :: HNil + } + ) + +// val Unk5OutfitCodec: Codec[Unk5OutfitResponse] = +// (uint16L :: uint16L :: PacketHelpers.encodedWideStringAligned(5)).xmap[Unk5OutfitResponse]( +// { +// case unk5 :: unk6 :: outfit_name :: HNil => +// Unk5OutfitResponse(unk5, unk6, outfit_name) +// }, +// { +// case Unk5OutfitResponse(unk5, unk6, outfit_name) => +// unk5 :: unk6 :: outfit_name :: HNil +// } +// ) +// +// val Unk6OutfitCodec: Codec[Unk6OutfitResponse] = +// (uint16L :: uint16L :: PacketHelpers.encodedWideStringAligned(5)).xmap[Unk6OutfitResponse]( +// { +// case _ => +// Unk6OutfitResponse() +// }, +// { +// case Unk6OutfitResponse() => +// _ +// } +// ) +// +// val Unk7OutfitCodec: Codec[Unk7OutfitResponse] = +// (uint16L :: uint16L :: PacketHelpers.encodedWideStringAligned(5)).xmap[Unk7OutfitResponse]( +// { +// case _ => +// Unk7OutfitResponse() +// }, +// { +// case Unk7OutfitResponse() => +// _ +// } +// ) + + /** + * A common form for known action code indexes with an unknown purpose and transformation is an "Unknown" object. + * @param action the action behavior code + * @return a transformation between the action code and the unknown bit data + */ + def unknownCodec(action: Int): Codec[Unknown] = + bits.xmap[Unknown]( + data => Unknown(action, data), + { + case Unknown(_, data) => data + } + ) + + /** + * The action code was completely unanticipated! + * @param action the action behavior code + * @return nothing; always fail + */ + def failureCodec(action: Int): Codec[OutfitMembershipResponseAction] = + everFailCondition.exmap[OutfitMembershipResponseAction]( + _ => Attempt.failure(Err(s"can not match a codec pattern for decoding $action")), + _ => Attempt.failure(Err(s"can not match a codec pattern for encoding $action")) + ) + } +} + +object OutfitMembershipResponse extends Marshallable[OutfitMembershipResponse] { + + object ResponseType extends Enumeration { + type Type = Value + + val CreateResponse: ResponseType.Value = Value(0) + val Unk1: ResponseType.Value = Value(1) + val Unk2: ResponseType.Value = Value(2) // Invited / Accepted / Added + val Unk3: ResponseType.Value = Value(3) + val Unk4: ResponseType.Value = Value(4) + val Unk5: ResponseType.Value = Value(5) + val Unk6: ResponseType.Value = Value(6) // 6 and 7 seen as failed decodes, validity unknown + val Unk7: ResponseType.Value = Value(7) + + implicit val codec: Codec[Type] = PacketHelpers.createEnumerationCodec(this, uintL(3)) + } + + private def selectFromType(code: Int): Codec[OutfitMembershipResponseAction] = { + import OutfitMembershipResponseAction.Codecs._ + import scala.annotation.switch + + ((code: @switch) match { + case 0 => Unk0OutfitCodec // seem as OMReq Create response + case 1 => Unk1OutfitCodec + case 2 => Unk2OutfitCodec + case 3 => Unk3OutfitCodec + case 4 => Unk4OutfitCodec + case 5 => unknownCodec(action = code) + case 6 => unknownCodec(action = code) + case 7 => unknownCodec(action = code) + // 3 bit limit + case _ => failureCodec(code) + }).asInstanceOf[Codec[OutfitMembershipResponseAction]] + } + + implicit val codec: Codec[OutfitMembershipResponse] = ( + ("response_type" | ResponseType.codec) >>:~ { response_type => + ("unk0" | uint8L) :: + ("avatar_guid" | PlanetSideGUID.codec) :: + ("outfit_guid-1" | PlanetSideGUID.codec) :: + ("target_guid" | PlanetSideGUID.codec) :: + ("unk3" | uint16L) :: + //("unk4" | bool) :: + ("action" | selectFromType(response_type.id)) + } + ).xmap[OutfitMembershipResponse]( + { + case response_type :: u0 :: avatar_guid :: outfit_guid_1 :: target_guid :: u3 :: action :: HNil => + OutfitMembershipResponse(response_type, u0, avatar_guid, outfit_guid_1, target_guid, u3, action) + }, + { + case OutfitMembershipResponse(response_type, u0, avatar_guid, u1, u2, u3, action) => + response_type :: u0 :: avatar_guid :: u1 :: u2 :: u3 :: action :: HNil + } + ) +} diff --git a/src/main/scala/net/psforever/packet/game/OutfitRequest.scala b/src/main/scala/net/psforever/packet/game/OutfitRequest.scala index 05ebf66aa..585ed5ee5 100644 --- a/src/main/scala/net/psforever/packet/game/OutfitRequest.scala +++ b/src/main/scala/net/psforever/packet/game/OutfitRequest.scala @@ -1,4 +1,4 @@ -// Copyright (c) 2023 PSForever +// Copyright (c) 2023-2025 PSForever package net.psforever.packet.game import net.psforever.packet.{GamePacketOpcode, Marshallable, PacketHelpers, PlanetSideGamePacket} @@ -7,77 +7,74 @@ import scodec.bits.ByteVector import scodec.codecs._ import shapeless.{::, HNil} -/** - * na - */ -abstract class OutfitRequestForm(val code: Int) - -object OutfitRequestForm { - /** - * na - * @param str na - */ - final case class Unk0(str: String) extends OutfitRequestForm(code = 0) - /** - * na - * @param list na - */ - final case class Unk1(list: List[Option[String]]) extends OutfitRequestForm(code = 1) - /** - * na - * @param unk na - */ - final case class Unk2(unk: Int) extends OutfitRequestForm(code = 2) - /** - * na - * @param unk na - */ - final case class Unk3(unk: Boolean) extends OutfitRequestForm(code = 3) - /** - * na - * @param unk na - */ - final case class Unk4(unk: Boolean) extends OutfitRequestForm(code = 4) - /** - * na - * @param unk na - */ - final case class Fail(unk: ByteVector) extends OutfitRequestForm(code = -1) +final case class OutfitRequest( + id: Long, + action: OutfitRequestAction + ) extends PlanetSideGamePacket { + type Packet = OutfitRequest + def opcode = GamePacketOpcode.OutfitRequest + def encode = OutfitRequest.encode(this) } /** * na - * @param id na - * @param info na */ -final case class OutfitRequest(id: Long, info: OutfitRequestForm) - extends PlanetSideGamePacket { - type Packet = OrbitalStrikeWaypointMessage - def opcode = GamePacketOpcode.OutfitRequest - def encode = OutfitRequest.encode(this) +abstract class OutfitRequestAction(val code: Int) + +object OutfitRequestAction { + /** + * na + * @param str na + */ + final case class Motd(str: String) extends OutfitRequestAction(code = 0) + /** + * na + * @param list na + */ + final case class Ranks(list: List[Option[String]]) extends OutfitRequestAction(code = 1) + /** + * na + * @param unk na + */ + final case class Unk2(unk: Int) extends OutfitRequestAction(code = 2) + /** + * na + * @param unk na + */ + final case class Unk3(menuOpen: Boolean) extends OutfitRequestAction(code = 3) + /** + * na + * @param unk na + */ + final case class Unk4(menuOpen: Boolean) extends OutfitRequestAction(code = 4) + /** + * na + * @param unk na + */ + final case class Fail(unk: ByteVector) extends OutfitRequestAction(code = -1) } object OutfitRequest extends Marshallable[OutfitRequest] { /** * na */ - private val unk0Codec: Codec[OutfitRequestForm] = PacketHelpers.encodedWideStringAligned(adjustment = 5).hlist - .xmap[OutfitRequestForm] ( + private val MotdCodec: Codec[OutfitRequestAction] = PacketHelpers.encodedWideStringAligned(adjustment = 5).hlist + .xmap[OutfitRequestAction] ( { - case value :: HNil => OutfitRequestForm.Unk0(value) + case value :: HNil => OutfitRequestAction.Motd(value) }, { - case OutfitRequestForm.Unk0(value) => value :: HNil + case OutfitRequestAction.Motd(value) => value :: HNil } ) /** * na */ - private val unk1Codec: Codec[OutfitRequestForm] = unk1PaddedEntryCodec(len = 8, pad = 5).xmap[OutfitRequestForm] ( - list => OutfitRequestForm.Unk1(list), + private val RankCodec: Codec[OutfitRequestAction] = unk1PaddedEntryCodec(len = 8, pad = 5).xmap[OutfitRequestAction] ( + list => OutfitRequestAction.Ranks(list), { - case OutfitRequestForm.Unk1(list) => list + case OutfitRequestAction.Ranks(list) => list } ) @@ -104,54 +101,66 @@ object OutfitRequest extends Marshallable[OutfitRequest] { /** * na */ - private val unk2Codec: Codec[OutfitRequestForm] = uint8.hlist.xmap[OutfitRequestForm] ( + private val unk2Codec: Codec[OutfitRequestAction] = uint8.hlist.xmap[OutfitRequestAction] ( { - case value :: HNil => OutfitRequestForm.Unk2(value) + case value :: HNil => OutfitRequestAction.Unk2(value) }, { - case OutfitRequestForm.Unk2(value) => value :: HNil + case OutfitRequestAction.Unk2(value) => value :: HNil } ) /** * na */ - private val unk3Codec: Codec[OutfitRequestForm] = bool.hlist.xmap[OutfitRequestForm] ( + private val unk3Codec: Codec[OutfitRequestAction] = bool.hlist.xmap[OutfitRequestAction] ( { - case value :: HNil => OutfitRequestForm.Unk3(value) + case value :: HNil => OutfitRequestAction.Unk3(value) }, { - case OutfitRequestForm.Unk3(value) => value :: HNil + case OutfitRequestAction.Unk3(value) => value :: HNil } ) /** * na */ - private val unk4Codec: Codec[OutfitRequestForm] = bool.hlist.xmap[OutfitRequestForm] ( + private val unk4Codec: Codec[OutfitRequestAction] = bool.hlist.xmap[OutfitRequestAction] ( { - case value :: HNil => OutfitRequestForm.Unk4(value) + case value :: HNil => OutfitRequestAction.Unk4(value) }, { - case OutfitRequestForm.Unk4(value) => value :: HNil + case OutfitRequestAction.Unk4(value) => value :: HNil } ) /** * na */ - private def failCodec(code: Int): Codec[OutfitRequestForm] = conditional(included = false, bool).exmap[OutfitRequestForm]( - _ => Attempt.Failure(Err(s"can not decode $code-type info - what is this thing?")), - _ => Attempt.Failure(Err(s"can not encode $code-type info - no such thing")) + private def failCodec(action: Int): Codec[OutfitRequestAction] = conditional(included = false, bool).exmap[OutfitRequestAction]( + _ => Attempt.Failure(Err(s"can not decode $action-type info - what is this thing?")), + _ => Attempt.Failure(Err(s"can not encode $action-type info - no such thing")) ) + object RequestType extends Enumeration { + type Type = Value + + val Motd: RequestType.Value = Value(0) + val Rank: RequestType.Value = Value(1) + val Unk2: RequestType.Value = Value(2) + val Detail: RequestType.Value = Value(3) + val List: RequestType.Value = Value(4) + + implicit val codec: Codec[Type] = PacketHelpers.createEnumerationCodec(this, uintL(3)) + } + /** * na */ - private def infoCodec(code: Int): Codec[OutfitRequestForm] = { + private def selectFromType(code: Int): Codec[OutfitRequestAction] = { code match { - case 0 => unk0Codec - case 1 => unk1Codec + case 0 => MotdCodec + case 1 => RankCodec case 2 => unk2Codec case 3 => unk3Codec case 4 => unk4Codec @@ -162,16 +171,16 @@ object OutfitRequest extends Marshallable[OutfitRequest] { implicit val codec: Codec[OutfitRequest] = ( uint(bits = 3) >>:~ { code => ("id" | uint32L) :: - ("info" | infoCodec(code)) + ("action" | selectFromType(code)) } ).xmap[OutfitRequest]( { - case _:: id:: info :: HNil => - OutfitRequest(id, info) + case _:: id:: action :: HNil => + OutfitRequest(id, action) }, { - case OutfitRequest(id, info) => - info.code :: id :: info :: HNil + case OutfitRequest(id, action) => + action.code :: id :: action :: HNil } ) } diff --git a/src/main/scala/net/psforever/packet/game/SquadMemberEvent.scala b/src/main/scala/net/psforever/packet/game/SquadMemberEvent.scala index 69117ced1..d3fb49a4c 100644 --- a/src/main/scala/net/psforever/packet/game/SquadMemberEvent.scala +++ b/src/main/scala/net/psforever/packet/game/SquadMemberEvent.scala @@ -82,9 +82,7 @@ object SquadMemberEvent extends Marshallable[SquadMemberEvent] { Some(outfit_id) ) => Attempt.Successful( - MemberEvent.Add :: unk2 :: char_id :: member_position :: Some(player_name) :: Some(zone_number) :: Some( - outfit_id - ) :: HNil + MemberEvent.Add :: unk2 :: char_id :: member_position :: Some(player_name) :: Some(zone_number) :: Some(outfit_id) :: HNil ) case SquadMemberEvent(MemberEvent.UpdateZone, unk2, char_id, member_position, None, Some(zone_number), None) => Attempt.Successful( diff --git a/src/test/scala/game/OutfitEventTest.scala b/src/test/scala/game/OutfitEventTest.scala new file mode 100644 index 000000000..e493c2d77 --- /dev/null +++ b/src/test/scala/game/OutfitEventTest.scala @@ -0,0 +1,294 @@ +// Copyright (c) 2025 PSForever +package game + +import net.psforever.packet._ +import net.psforever.packet.game.OutfitEvent.RequestType +import net.psforever.packet.game.OutfitEventAction._ +import net.psforever.packet.game._ +import net.psforever.types.PlanetSideGUID +import org.specs2.mutable._ +import scodec.bits._ + +class OutfitEventTest extends Specification { + val unk0_ABC: ByteVector = ByteVector.fromValidHex( + "8f 1 a8c2 0001" + // packet head + "2a 0 42006c00610063006b002000410072006d006f0072006500640020005200650061007000650072007300" + // Black Armored Reapers + "1d9c4d0d" + + "1d9c4d0d" + + "ab00 0000" + + "88 44006f00670020004d00650061007400" + // Dog Meat + "87 5200750073007300690061006e00" + // Russian + "80" + // + "80" + // + "8d 5300710075006100640020004c00650061006400650072007300" + // Squad Leaders + "91 41006300740069006e006700200043006f006d006d0061006e006400650072007300" + // Acting Commanders + "87 5200650061007000650072007300" + // Reapers + "80" + // + "00" + + "9c 5c0023003000300030003000660066004d0075006d0062006c00650020005c00230030003000330033006600660049006e0066006f0020005c0023003000300036003600660066006900730020005c0023003000300039003900660066007400680065006d006f006f00730065002e00740079007000650066007200610067002e0063006f006d0020005c00230030003000630063006600660070006f007200740020005c002300300030006600660066006600390033003500300020005c0023003000300063006300660066006a006f0069006e0020005c0023003000300039003900660066006900740020005c0023003000300036003600660066006f00720020005c0023003000300033003300660066006200650020005c0023003000300030003000660066006b00690063006b00650064002e00" + + "0f80" + + "0000 00737296 24000000 00000000 00000000 0000") + val unk1_ABC: ByteVector = hex"8f 2 302a 10 00 0" + val unk2_ABC: ByteVector = ByteVector.fromValidHex( + "8f 4 0201 feff" + + "2e 0 50006c0061006e006500740053006900640065005f0046006f00720065007600650072005f00560061006e007500" + // PlanetSide_Forever_Vanu + "00000000" + + "00000000" + + "0100 0000" + + "80" + + "80" + + "80" + + "80" + + "80" + + "80" + + "80" + + "80" + + "80" + + "0070" + + "4982 00000000 00000000 00000000 00000000 0000") + val unk3_ABC: ByteVector = hex"8f 6 0201 fe fe 0" + val unk4_ABC: ByteVector = hex"8f 8 0201 fefe a02a 1000 0" + val unk5_ABC: ByteVector = hex"8f a 0201 fefe 0400 0000 0" + + "decode Unk0 ABC" in { + PacketCoding.decodePacket(unk0_ABC).require match { + case OutfitEvent(request_type, outfit_guid, action) => + request_type mustEqual RequestType.Unk0 + outfit_guid mustEqual PlanetSideGUID(25044) + action mustEqual Unk0( + OutfitInfo( + unk1 = 0, + unk2 = 0, + outfit_name = "Black Armored Reapers", + unk6 = 223190045, + unk7 = 223190045, + member_count = 171, + unk9 = 0, + OutfitRankNames("Dog Meat","Russian","","","Squad Leaders","Acting Commanders","Reapers",""), + "\\#0000ffMumble \\#0033ffInfo \\#0066ffis \\#0099ffthemoose.typefrag.com \\#00ccffport \\#00ffff9350 \\#00ccffjoin \\#0099ffit \\#0066ffor \\#0033ffbe \\#0000ffkicked.", + PlanetSideGUID(32783), + 0, + 0, + 0, + 1210901990, + 0, + 0, + 0, + 0, + ) + ) + case _ => + ko + } + } + + "encode Unk0 ABC" in { + val msg = OutfitEvent( + RequestType.Unk0, + PlanetSideGUID(25044), + Unk0( + OutfitInfo( + unk1 = 0, + unk2 = 0, + outfit_name = "Black Armored Reapers", + unk6 = 223190045, + unk7 = 223190045, + member_count = 171, + unk9 = 0, + OutfitRankNames("Dog Meat","Russian","","","Squad Leaders","Acting Commanders","Reapers",""), + "\\#0000ffMumble \\#0033ffInfo \\#0066ffis \\#0099ffthemoose.typefrag.com \\#00ccffport \\#00ffff9350 \\#00ccffjoin \\#0099ffit \\#0066ffor \\#0033ffbe \\#0000ffkicked.", + PlanetSideGUID(32783), + 0, + 0, + 0, + 1210901990, + 0, + 0, + 0, + 0, + ) + ) + ) + val pkt = PacketCoding.encodePacket(msg).require.toByteVector + + pkt mustEqual unk0_ABC + } + + "decode Unk1 ABC" in { + PacketCoding.decodePacket(unk1_ABC).require match { + case OutfitEvent(request_type, outfit_guid, action) => + request_type mustEqual RequestType.Unk1 + outfit_guid mustEqual PlanetSideGUID(5400) + action mustEqual Unk1(unk0 = 8, unk1 = 0, unk2 = 0, unk3 = false) + case _ => + ko + } + } + + "encode Unk1 ABC" in { + val msg = OutfitEvent( + RequestType.Unk1, + PlanetSideGUID(5400), + Unk1( + unk0 = 8, + unk1 = 0, + unk2 = 0, + unk3 = false, + ) + ) + val pkt = PacketCoding.encodePacket(msg).require.toByteVector + + pkt mustEqual unk1_ABC + } + + "decode Unk2 ABC" in { + PacketCoding.decodePacket(unk2_ABC).require match { + case OutfitEvent(request_type, outfit_guid, action) => + request_type mustEqual RequestType.Unk2 + outfit_guid mustEqual PlanetSideGUID(1) + action mustEqual Unk2(OutfitInfo(unk1 = 255, unk2 = 127, outfit_name = "PlanetSide_Forever_Vanu", + unk6 = 0, unk7 = 0, member_count = 1, unk9 = 0, OutfitRankNames("","","","","","","",""), + "", PlanetSideGUID(28672), 33353, 0, 0, 0, 0, 0, 0, 0)) + case _ => + ko + } + } + + "encode Unk2 ABC" in { + val msg = OutfitEvent( + RequestType.Unk2, + PlanetSideGUID(1), + Unk2( + OutfitInfo( + unk1 = 255, + unk2 = 127, + outfit_name = "PlanetSide_Forever_Vanu", + unk6 = 0, + unk7 = 0, + member_count = 1, + unk9 = 0, + OutfitRankNames("","","","","","","",""), + "", + PlanetSideGUID(28672), + 33353, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + ) + ) + ) + val pkt = PacketCoding.encodePacket(msg).require.toByteVector + + pkt mustEqual unk2_ABC + } + + "decode Unk3 ABC" in { + PacketCoding.decodePacket(unk3_ABC).require match { + case OutfitEvent(request_type, outfit_guid, action) => + request_type mustEqual RequestType.Unk3 + outfit_guid mustEqual PlanetSideGUID(1) + action mustEqual Unk3( + unk0 = 255, + unk1 = 127, + unk2 = 0, + unk3 = false, + BitVector.fromValidHex("") + ) + case _ => + ko + } + } + + "encode Unk3 ABC" in { + val msg = OutfitEvent( + RequestType.Unk3, + PlanetSideGUID(1), + Unk3( + unk0 = 255, + unk1 = 127, + unk2 = 0, + unk3 = false, + BitVector.fromValidHex("") + ) + ) + val pkt = PacketCoding.encodePacket(msg).require.toByteVector + + pkt mustEqual unk3_ABC + } + + "decode Unk4 ABC" in { + PacketCoding.decodePacket(unk4_ABC).require match { + case OutfitEvent(request_type, outfit_guid, action) => + request_type mustEqual RequestType.Unk4 + outfit_guid mustEqual PlanetSideGUID(1) + action mustEqual Unk4( + unk0 = 32767, + unk1 = 5456, + unk2 = 8, + 0, + unk4 = false, + BitVector.fromValidHex("") + ) + case _ => + ko + } + } + + "encode Unk4 ABC" in { + val msg = OutfitEvent( + RequestType.Unk4, + PlanetSideGUID(1), + Unk4( + unk0 = 32767, + unk1 = 5456, + unk2 = 8, + unk3 = 0, + unk4 = false, + BitVector.fromValidHex("") + ) + ) + val pkt = PacketCoding.encodePacket(msg).require.toByteVector + + pkt mustEqual unk4_ABC + } + + "decode Unk5 ABC" in { + PacketCoding.decodePacket(unk5_ABC).require match { + case OutfitEvent(request_type, outfit_guid, action) => + request_type mustEqual RequestType.Unk5 + outfit_guid mustEqual PlanetSideGUID(1) + action mustEqual Unk5( + unk0 = 32767, + unk1 = 2, + unk2 = 0, + unk3 = 0, + unk4 = false, + BitVector.fromValidHex("") // OR f88c2a0417c1a06101001f20f4b8c00000404090ac9c6745dea88cadf0f810e03e0200f92 with bool at the back + ) + case _ => + ko + } + } + + "encode Unk5 ABC" in { + val msg = OutfitEvent( + RequestType.Unk5, + PlanetSideGUID(1), + Unk5( + unk0 = 32767, + unk1 = 2, + unk2 = 0, + unk3 = 0, + unk4 = false, + BitVector.fromValidHex("") + ) + ) + val pkt = PacketCoding.encodePacket(msg).require.toByteVector + + pkt mustEqual unk5_ABC + } +} diff --git a/src/test/scala/game/OutfitListEventTest.scala b/src/test/scala/game/OutfitListEventTest.scala new file mode 100644 index 000000000..7935df126 --- /dev/null +++ b/src/test/scala/game/OutfitListEventTest.scala @@ -0,0 +1,42 @@ +// Copyright (c) 2025 PSForever +package game + +import net.psforever.packet._ +import net.psforever.packet.game._ +import org.specs2.mutable._ +import scodec.bits.ByteVector + +class OutfitListEventTest extends Specification { + val unk0_ABC: ByteVector = ByteVector.fromValidHex("98 5e83a000 0000 e180 0080 0000 11404e0069006700680074004c006f00720064007300 854e005900430061007400") + val unk0_DEF: ByteVector = ByteVector.fromValidHex("98 4ec28100 151a 6280 0340 0000 11a0490052004f004e004600490053005400200043006c0061006e00 8654006f006c006a00") + val unk1_ABC: ByteVector = ByteVector.fromValidHex("98 4723c000 02aa 81e0 0220 0000 11006900470061006d00650073002d004500 906900670061006d006500730043005400460057006800610063006b002d004500") + val unk2_ABC: ByteVector = ByteVector.fromValidHex("98 49a3c000 116d a4e0 0040 0000 11a042006c006f006f00640020006f0066002000560061006e007500 864b00610072006e002d004500") + val unk3_ABC: ByteVector = ByteVector.fromValidHex("98 49c3c000 0df5 87c0 0140 0000 11a054006800650020004e00650076006500720068006f006f006400 8e6f00460058006f00530074006f006e0065004d0061006e002d004700") + val unk4_ABC: ByteVector = ByteVector.fromValidHex("98 4c03c000 0240 6040 0060 0000 1220540068006500200042006c00610063006b0020004b006e0069006700680074007300 874400720061007a00760065006e00") + val unk5_ABC: ByteVector = ByteVector.fromValidHex("98 5383c000 14b7 09a0 00c0 0000 10a03e005400760053003c00 89430061007000650062006f00610074007300") + val unk6_ABC: ByteVector = ByteVector.fromValidHex("98 5b03c000 035d 6700 0040 0000 11404c006f0073007400200043006100750073006500 895a00650072006f004b00650077006c006c00") + val unk7_ABC: ByteVector = ByteVector.fromValidHex("98 4043e000 19fb 8261 6140 0000 11e0540068006500200042006c00610063006b00200054006f00770065007200 874b00720075007000680065007800") + val unk8_ABC: ByteVector = ByteVector.fromValidHex("98 4a03e000 17e2") // broken, limit of SMP + + "decode unk0_ABC" in { + PacketCoding.decodePacket(unk0_ABC).require match { + case OutfitListEvent(outfit_score, unk1, unk2, unk3, outfit_name, outfit_leader) => + outfit_score mustEqual 1585684480L + unk1 mustEqual 2162229248L + unk2 mustEqual 32768 + unk3 mustEqual 0 + outfit_name mustEqual "NightLords" + outfit_leader mustEqual "NYCat" + case _ => + ko + } + } + + "encode unk0_ABC" in { + val msg = OutfitListEvent(1585684480L, 2162229248L, 32768, 0, "NightLords", "NYCat") + val pkt = PacketCoding.encodePacket(msg).require.toByteVector + + pkt mustEqual unk0_ABC + } + +} diff --git a/src/test/scala/game/OutfitMemberEventTest.scala b/src/test/scala/game/OutfitMemberEventTest.scala new file mode 100644 index 000000000..9b07ec9dc --- /dev/null +++ b/src/test/scala/game/OutfitMemberEventTest.scala @@ -0,0 +1,71 @@ +// Copyright (c) 2025 PSForever +package game + +import net.psforever.packet._ + +import net.psforever.packet.game._ +import org.specs2.mutable._ +import scodec.bits._ + +class OutfitMemberEventTest extends Specification { + + //val unk0_ABC: ByteVector = hex"90 3518 4000 1a4e 4100 2 180 450078007000650072007400 8483 07e0 119d bfe0 70" // 0x90048640001030c28022404c0061007a00650072003100390038003200f43a45e00b4c604010 +val unk0_ABC_Lazer: ByteVector = hex"90 048640001030c28022404c0061007a00650072003100390038003200f43a45e00b4c604010" + + val OpolE = hex"90 0 4864 0003 aad6 280a 14 0 4f0070006f006c004500 c9a1 80e0 0d03 2040 10" + val Billy = hex"90 0 4864 0003 a41a 280a 20 0 620069006c006c007900320035003600 935f 6000 186a b040 50" + val Lazer = hex"90 0 4864 0001 030c 2802 24 0 4c0061007a00650072003100390038003200 e6dc 25a0 153e 6040 10" + val Virus = hex"90 0 4864 0002 1b64 4c02 28 0 5600690072007500730047006900760065007200 2f89 0080 0000 0000 10" + val PvtPa = hex"90 0 4864 0000 1e69 e80a 2c 0 500076007400500061006e00630061006b0065007300 705e a080 0a85 e060 10" + val Night = hex"90 0 4864 0002 4cf0 3802 28 0 4e006900670068007400770069006e0067003100 b8fb 9a40 0da6 ec80 50" + + /* + OutfitMemberEvent(0, ValidPlanetSideGUID(6418), 0, 0, 64, 195, 10, 0, Lazer1982, 230, 220, 37, 160, 21, 62, 96, 64, 16, BitVector(empty)) + OutfitMemberEvent(0, ValidPlanetSideGUID(6418), 0, 0, 7, 154, 122, 2, PvtPancakes, 112, 94, 160, 128, 10, 133, 224, 96, 16, BitVector(empty)) + OutfitMemberEvent(0, ValidPlanetSideGUID(6418), 0, 0, 134, 217, 19, 0, VirusGiver, 47, 137, 0, 128, 0, 0, 0, 0, 16, BitVector(empty)) + OutfitMemberEvent(0, ValidPlanetSideGUID(6418), 0, 0, 234, 181, 138, 2, OpolE, 201, 161, 128, 224, 13, 3, 32, 64, 16, BitVector(empty)) + OutfitMemberEvent(0, ValidPlanetSideGUID(6418), 0, 0, 233, 6, 138, 2, billy256, 147, 95, 96, 0, 24, 106, 176, 64, 80, BitVector(empty)) + + + */ + + "decode Unk0 ABC" in { + PacketCoding.decodePacket(unk0_ABC_Lazer).require match { + case OutfitMemberEvent(unk00, outfit_guid, unk3, unk4, unk5, unk6, member_name, unk7, unk8, unk9, unk10, unk11) => + unk00 mustEqual 0 + outfit_guid mustEqual 6418L + unk3 mustEqual 64 + unk4 mustEqual 195 + unk5 mustEqual 10 + unk6 mustEqual 0 + member_name mustEqual "Lazer1982" + unk7 mustEqual 15092 + unk8 mustEqual 57413 + unk9 mustEqual 19467 + unk10 mustEqual 16480 + unk11 mustEqual 16 + case _ => + ko + } + } + + "encode Unk0 ABC" in { + val msg = OutfitMemberEvent( + unk00 = 0, + outfit_id = 6418L, + unk3 = 64, + unk4 = 195, + unk5 = 10, + unk6 = 0, + member_name = "Lazer1982", + unk7 = 15092, + unk8 = 57413, + unk9 = 19467, + unk10 = 16480, + unk11 = 16, + ) + val pkt = PacketCoding.encodePacket(msg).require.toByteVector + + pkt mustEqual unk0_ABC_Lazer + } +} diff --git a/src/test/scala/game/OutfitMembershipRequestTest.scala b/src/test/scala/game/OutfitMembershipRequestTest.scala index 4b90df4ee..5e8f48ac9 100644 --- a/src/test/scala/game/OutfitMembershipRequestTest.scala +++ b/src/test/scala/game/OutfitMembershipRequestTest.scala @@ -1,10 +1,10 @@ -// Copyright (c) 2023 PSForever +// Copyright (c) 2023-2025 PSForever package game import net.psforever.packet._ import net.psforever.packet.game._ -import net.psforever.packet.game.OutfitAction.{AcceptOutfitInvite, CancelOutfitInvite, CreateOutfit, FormOutfit, RejectOutfitInvite} import net.psforever.packet.game.OutfitMembershipRequest.RequestType +import net.psforever.packet.game.OutfitMembershipRequestAction._ import net.psforever.types.PlanetSideGUID import org.specs2.mutable._ import scodec.bits._ diff --git a/src/test/scala/game/OutfitRequesTest.scala b/src/test/scala/game/OutfitRequesTest.scala index eea16519e..0928b3f55 100644 --- a/src/test/scala/game/OutfitRequesTest.scala +++ b/src/test/scala/game/OutfitRequesTest.scala @@ -1,22 +1,21 @@ -// Copyright (c) 2023 PSForever +// Copyright (c) 2023-2025 PSForever package game import org.specs2.mutable._ import net.psforever.packet._ import net.psforever.packet.game._ -import net.psforever.types.PlanetSideGUID import scodec.bits._ class OutfitRequestTest extends Specification { - val string0 = hex"8e02b54f40401780560061006e00750020006f0075007400660069007400200066006f0072002000740068006500200070006c0061006e00650074007300690064006500200066006f00720065007600650072002000700072006f006a006500630074002100200020002000200020002000200020002000200020002000200020002000200020002000200020002000200020002000200020002000200020002000200020002000200020002000200020002000200020002000200020002000200020002000200020002000200020002000200020002000200020002000200020002000200020002000200020002000200020002000200020002000200020002000200020002000200020002000200020002d00660069006e00640020006f007500740020006d006f00720065002000610062006f0075007400200074006800650020005000530045004d0055002000700072006f006a0065006300740020006100740020005000530066006f00720065007600650072002e006e0065007400" - val string2 = hex"8e22b54f405800c000c000c000c000c000c000c000" - val string4 = hex"8e42b54f404aa0" //faked by modifying the previous example - val string6 = hex"8e649e822010" - val string8 = hex"8e81b2cf4050" + val setMotd = hex"8e 02b54f40401780560061006e00750020006f0075007400660069007400200066006f0072002000740068006500200070006c0061006e00650074007300690064006500200066006f00720065007600650072002000700072006f006a006500630074002100200020002000200020002000200020002000200020002000200020002000200020002000200020002000200020002000200020002000200020002000200020002000200020002000200020002000200020002000200020002000200020002000200020002000200020002000200020002000200020002000200020002000200020002000200020002000200020002000200020002000200020002000200020002000200020002000200020002d00660069006e00640020006f007500740020006d006f00720065002000610062006f0075007400200074006800650020005000530045004d0055002000700072006f006a0065006300740020006100740020005000530066006f00720065007600650072002e006e0065007400" + val setRanks = hex"8e 22b54f405800c000c000c000c000c000c000c000" + val string4 = hex"8e 42b54f404aa0" //faked by modifying the previous example + val string6 = hex"8e 649e822010" + val string8 = hex"8e 81b2cf4050" "decode 0" in { - PacketCoding.decodePacket(string0).require match { - case OutfitRequest(id, OutfitRequestForm.Unk0(str)) => + PacketCoding.decodePacket(setMotd).require match { + case OutfitRequest(id, OutfitRequestAction.Motd(str)) => id mustEqual 41593365L str mustEqual "Vanu outfit for the planetside forever project! -find out more about the PSEMU project at PSforever.net" case _ => @@ -25,8 +24,8 @@ class OutfitRequestTest extends Specification { } "decode 1" in { - PacketCoding.decodePacket(string2).require match { - case OutfitRequest(id, OutfitRequestForm.Unk1(list)) => + PacketCoding.decodePacket(setRanks).require match { + case OutfitRequest(id, OutfitRequestAction.Ranks(list)) => id mustEqual 41593365L list mustEqual List(Some(""), Some(""), Some(""), Some(""), Some(""), Some(""), Some(""), Some("")) case _ => @@ -36,7 +35,7 @@ class OutfitRequestTest extends Specification { "decode 2 (fake)" in { PacketCoding.decodePacket(string4).require match { - case OutfitRequest(id, OutfitRequestForm.Unk2(value)) => + case OutfitRequest(id, OutfitRequestAction.Unk2(value)) => id mustEqual 41593365L value mustEqual 85 case _ => @@ -46,7 +45,7 @@ class OutfitRequestTest extends Specification { "decode 3" in { PacketCoding.decodePacket(string6).require match { - case OutfitRequest(id, OutfitRequestForm.Unk3(value)) => + case OutfitRequest(id, OutfitRequestAction.Unk3(value)) => id mustEqual 1176612L value mustEqual true case _ => @@ -56,7 +55,7 @@ class OutfitRequestTest extends Specification { "decode 4" in { PacketCoding.decodePacket(string8).require match { - case OutfitRequest(id, OutfitRequestForm.Unk4(value)) => + case OutfitRequest(id, OutfitRequestAction.Unk4(value)) => id mustEqual 41588237L value mustEqual true case _ => @@ -65,37 +64,37 @@ class OutfitRequestTest extends Specification { } "encode 0" in { - val msg = OutfitRequest(41593365L, OutfitRequestForm.Unk0( + val msg = OutfitRequest(41593365L, OutfitRequestAction.Motd( "Vanu outfit for the planetside forever project! -find out more about the PSEMU project at PSforever.net" )) val pkt = PacketCoding.encodePacket(msg).require.toByteVector - pkt mustEqual string0 + pkt mustEqual setMotd } "encode 1" in { - val msg = OutfitRequest(41593365L, OutfitRequestForm.Unk1(List(Some(""), Some(""), Some(""), Some(""), Some(""), Some(""), Some(""), Some("")))) + val msg = OutfitRequest(41593365L, OutfitRequestAction.Ranks(List(Some(""), Some(""), Some(""), Some(""), Some(""), Some(""), Some(""), Some("")))) val pkt = PacketCoding.encodePacket(msg).require.toByteVector - pkt mustEqual string2 + pkt mustEqual setRanks } "encode 2 (fake)" in { - val msg = OutfitRequest(41593365L, OutfitRequestForm.Unk2(85)) + val msg = OutfitRequest(41593365L, OutfitRequestAction.Unk2(85)) val pkt = PacketCoding.encodePacket(msg).require.toByteVector pkt mustEqual string4 } "encode 3" in { - val msg = OutfitRequest(1176612L, OutfitRequestForm.Unk3(true)) + val msg = OutfitRequest(1176612L, OutfitRequestAction.Unk3(true)) val pkt = PacketCoding.encodePacket(msg).require.toByteVector pkt mustEqual string6 } "encode 4" in { - val msg = OutfitRequest(41588237L, OutfitRequestForm.Unk4(true)) + val msg = OutfitRequest(41588237L, OutfitRequestAction.Unk4(true)) val pkt = PacketCoding.encodePacket(msg).require.toByteVector pkt mustEqual string8 diff --git a/src/test/scala/game/SquadMemberEventTest.scala b/src/test/scala/game/SquadMemberEventTest.scala index d0ae6d78c..a8e2cfef9 100644 --- a/src/test/scala/game/SquadMemberEventTest.scala +++ b/src/test/scala/game/SquadMemberEventTest.scala @@ -1,4 +1,4 @@ -// Copyright (c) 2019 PSForever +// Copyright (c) 2019-2025 PSForever package game import net.psforever.packet._ @@ -11,14 +11,14 @@ class SquadMemberEventTest extends Specification { "decode" in { PacketCoding.decodePacket(string).require match { - case SquadMemberEvent(u1, u2, u3, u4, u5, u6, u7) => - u1 mustEqual MemberEvent.Add + case SquadMemberEvent(event, u2, char_id, position, player_name, zone_number, outfit_id) => + event mustEqual MemberEvent.Add u2 mustEqual 7 - u3 mustEqual 42771010L - u4 mustEqual 0 - u5.contains("HofD") mustEqual true - u6.contains(7) mustEqual true - u7.contains(529745L) mustEqual true + char_id mustEqual 42771010L + position mustEqual 0 + player_name.contains("HofD") mustEqual true + zone_number.contains(7) mustEqual true + outfit_id.contains(529745L) mustEqual true case _ => ko } From 6e23b701a667d8cf7d61dca0544654f5c95ebd35 Mon Sep 17 00:00:00 2001 From: Resaec Date: Sat, 16 Aug 2025 00:19:13 +0200 Subject: [PATCH 02/33] 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 cfbd647b9..90cffd4b9 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 b64a68cec..adeaca488 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 0e9430e65..1a75371a7 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 e493c2d77..eab4f75a9 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 5e8f48ac9..163e4c45a 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/33] 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 9fc1a72ee..fe8b9d50a 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/33] 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 fe8b9d50a..bf94b3a84 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/33] 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 737a72da8..ddce733f9 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/33] 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 adeaca488..6d3c17ea0 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 9b07ec9dc..4fe48a805 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 163e4c45a..bef384a5b 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/33] 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 ddce733f9..a16ade5b6 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 1a75371a7..086d42eda 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 4fe48a805..0317b8343 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/33] 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 90cffd4b9..3117bbb47 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 52ea9929e..c040f2e5d 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 585ed5ee5..c62b1fda2 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 eab4f75a9..8837b93c1 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 7935df126..e7731a2db 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/33] 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 3117bbb47..798502378 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 a16ade5b6..67c95820c 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 bf94b3a84..39f682964 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/33] 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 798502378..1a633fa2c 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 67c95820c..0a0f31f25 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 39f682964..88cae6328 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 6d3c17ea0..5e0c8b9b7 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 086d42eda..5b2aec24f 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 8837b93c1..e4a86cec8 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 e7731a2db..c5713a55f 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 0317b8343..5dbcef004 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 bef384a5b..0e8af6e4c 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/33] 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 5b2aec24f..da9c27b5e 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 000000000..7646a96e4 --- /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/33] 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 0a0f31f25..6f11c730d 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 5dbcef004..f51fe952f 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/33] 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 6f11c730d..f40498a41 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 f51fe952f..726d0a787 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/33] 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 1a633fa2c..8b2780745 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 e4a86cec8..2b3e2dd8e 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/33] 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 c040f2e5d..42d7cc29e 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/33] 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 8b2780745..be267fce7 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 42d7cc29e..acd9ed99c 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 f40498a41..0ae680853 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 88cae6328..979d26f9f 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 5e0c8b9b7..41cb6b176 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 da9c27b5e..1608cbf97 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 c62b1fda2..7d6b4cd0c 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 2b3e2dd8e..27039f043 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 c5713a55f..79b5d0377 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 726d0a787..510e7278a 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 000000000..39e0ca3de --- /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 0e8af6e4c..b733c4d88 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 7646a96e4..bd16286a3 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 0928b3f55..4c4adcb11 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/33] 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 000000000..f44ee4af0 --- /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/33] 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 25b5c377e..21093f14f 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 2cd1782ec..f32bdef95 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 0d0cd12db..552063184 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 608f06d00..ac21b6e25 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 e1f6415f5..8c332585a 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 48a724db1..bdaa6f2e2 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 e72e57a56..9f5ce393f 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 000000000..5465cee5a --- /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 000000000..4b83e5a07 --- /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 fd00fb54d..d4847ad46 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 1d2f16fe6..ea96ad0e1 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 1608cbf97..ebde82702 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 3fbea3e64..fab599fdd 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 8803c3667..53c105f95 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/33] 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 ac21b6e25..3b3794b17 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 4b83e5a07..553d6eec8 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 d4847ad46..074937edd 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 5cea9482d..c307c7b3e 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/33] 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 f44ee4af0..7a0a11867 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 3b3794b17..b16386d78 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 553d6eec8..e9249d6d9 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 ebde82702..90f592954 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 7d6b4cd0c..3e8d5f513 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 bd16286a3..948cd41cc 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/33] 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 e9936cda6..c81be928c 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 e9249d6d9..b3bedc365 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 f9dd3a062..95502b190 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 f61e43949..54afc5bf1 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 5c26d9a02..cce788e73 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/33] 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 12291c4ac..a39541c98 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") } From 25ebda156280afed279358db904b2c72a5a3d80b Mon Sep 17 00:00:00 2001 From: Resaec Date: Sat, 30 Aug 2025 22:43:13 +0200 Subject: [PATCH 23/33] start From 8dcf678045d99e923fdd9869f3f6ab4a1bb321e3 Mon Sep 17 00:00:00 2001 From: Resaec Date: Sun, 31 Aug 2025 00:01:58 +0200 Subject: [PATCH 24/33] add Outfit Rank / Title updates --- .../actors/session/normal/GeneralLogic.scala | 3 +- .../support/SessionOutfitHandlers.scala | 141 ++++++++++++++---- 2 files changed, 116 insertions(+), 28 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 b16386d78..79c335505 100644 --- a/src/main/scala/net/psforever/actors/session/normal/GeneralLogic.scala +++ b/src/main/scala/net/psforever/actors/session/normal/GeneralLogic.scala @@ -834,8 +834,7 @@ class GeneralLogic(val ops: GeneralOperations, implicit val context: ActorContex 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)))) + SessionOutfitHandlers.HandleOutfitRank(zones, List(r1, r2, r3, r4, r5, r6, r7, r8), player) 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 b3bedc365..47ba310c9 100644 --- a/src/main/scala/net/psforever/actors/session/support/SessionOutfitHandlers.scala +++ b/src/main/scala/net/psforever/actors/session/support/SessionOutfitHandlers.scala @@ -437,33 +437,33 @@ object SessionOutfitHandlers { // 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 - ) + 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 => { @@ -481,6 +481,65 @@ 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 HandleOutfitRank(zones: Seq[Zone], list: List[Option[String]], player: Player): Unit = { + + val outfit_id = player.outfit_id + + val outfitDetails = for { + _ <- updateOutfitRanks(outfit_id, list) + 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 + ) + }) + }) + } + } + } + def HandleLoginOutfitCheck(player: Player, session: SessionData): Unit = { ctx.run(getOutfitOnLogin(player.avatar.id)).flatMap { memberships => memberships.headOption match { @@ -729,4 +788,34 @@ object SessionOutfitHandlers { } yield () } } + + def updateOutfitRanksById(outfit_id: Long, list: List[Option[String]]): Quoted[Update[Outfit]] = { + + // Normalize: turn empty strings into None + val normalized = list.map { + case Some(value) if value.trim.nonEmpty => Some(value) + case _ => None + } + + query[Outfit] + .filter(_.id == lift(outfit_id)) + .update( + _.rank0 -> lift(normalized(0)), + _.rank1 -> lift(normalized(1)), + _.rank2 -> lift(normalized(2)), + _.rank3 -> lift(normalized(3)), + _.rank4 -> lift(normalized(4)), + _.rank5 -> lift(normalized(5)), + _.rank6 -> lift(normalized(6)), + _.rank7 -> lift(normalized(7)) + ) + } + + def updateOutfitRanks(outfit_id: Long, list: List[Option[String]]): Future[Unit] = { + ctx.transaction { _ => + for { + _ <- ctx.run(updateOutfitRanksById(outfit_id, list)) + } yield () + } + } } From c84bf9ae74bbc8dc56f5332aef395aa9f03b1856 Mon Sep 17 00:00:00 2001 From: Resaec Date: Sun, 31 Aug 2025 03:05:31 +0200 Subject: [PATCH 25/33] evict players from the outfit. no longer shall you spy on our stale info! update packet types --- .../actors/session/csr/GeneralLogic.scala | 6 +- .../actors/session/normal/GeneralLogic.scala | 8 +-- .../support/SessionOutfitHandlers.scala | 51 +++++++------- .../psforever/packet/game/OutfitEvent.scala | 69 +++++++++++++------ .../packet/game/OutfitMemberEvent.scala | 50 ++++++++------ .../packet/game/OutfitMembershipRequest.scala | 18 ++--- .../game/OutfitMembershipResponse.scala | 6 +- .../psforever/packet/game/OutfitRequest.scala | 26 +++---- src/test/scala/game/OutfitEventTest.scala | 20 +++--- .../scala/game/OutfitMemberEventTest.scala | 12 ++-- .../game/OutfitMembershipResponseTest.scala | 8 +-- src/test/scala/game/OutfitRequestTest.scala | 8 +-- 12 files changed, 163 insertions(+), 119 deletions(-) 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 f32bdef95..a920f4fed 100644 --- a/src/main/scala/net/psforever/actors/session/csr/GeneralLogic.scala +++ b/src/main/scala/net/psforever/actors/session/csr/GeneralLogic.scala @@ -30,7 +30,7 @@ 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.OutfitEventAction.{OutfitInfo, OutfitRankNames, Unk0, Unk1} +import net.psforever.packet.game.OutfitEventAction.{OutfitInfo, OutfitRankNames, Initial, 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} @@ -673,9 +673,9 @@ class GeneralLogic(val ops: GeneralOperations, implicit val context: ActorContex def handleOutfitRequest(pkt: OutfitRequest): Unit = { pkt match { - case OutfitRequest(_, OutfitRequestAction.Unk3(true)) => + case OutfitRequest(_, OutfitRequestAction.OutfitWindowOpen(true)) => - case OutfitRequest(_, OutfitRequestAction.Unk3(false)) => + case OutfitRequest(_, OutfitRequestAction.OutfitWindowOpen(false)) => case _ => } 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 79c335505..a2f87cd16 100644 --- a/src/main/scala/net/psforever/actors/session/normal/GeneralLogic.scala +++ b/src/main/scala/net/psforever/actors/session/normal/GeneralLogic.scala @@ -37,7 +37,7 @@ 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.OutfitEventAction.{OutfitInfo, OutfitRankNames, Unk2} +import net.psforever.packet.game.OutfitEventAction.{OutfitInfo, OutfitRankNames, Update} 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} @@ -836,12 +836,12 @@ class GeneralLogic(val ops: GeneralOperations, implicit val context: ActorContex case OutfitRequest(_, OutfitRequestAction.Ranks(List(r1, r2, r3, r4, r5, r6, r7, r8))) => SessionOutfitHandlers.HandleOutfitRank(zones, List(r1, r2, r3, r4, r5, r6, r7, r8), player) - case OutfitRequest(_, OutfitRequestAction.Unk3(true)) => + case OutfitRequest(_, OutfitRequestAction.OutfitWindowOpen(true)) => SessionOutfitHandlers.HandleViewOutfitWindow(zones, player, player.outfit_id) - case OutfitRequest(_, OutfitRequestAction.Unk3(false)) => + case OutfitRequest(_, OutfitRequestAction.OutfitWindowOpen(false)) => - case OutfitRequest(_, OutfitRequestAction.Unk4(true)) => + case OutfitRequest(_, OutfitRequestAction.OutfitListWindowOpen(true)) => SessionOutfitHandlers.HandleGetOutfitList(player) 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 47ba310c9..bafd60be9 100644 --- a/src/main/scala/net/psforever/actors/session/support/SessionOutfitHandlers.scala +++ b/src/main/scala/net/psforever/actors/session/support/SessionOutfitHandlers.scala @@ -5,7 +5,7 @@ import io.getquill.{ActionReturning, EntityQuery, Insert, PostgresJAsyncContext, 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.OutfitEventAction.{Leaving, OutfitInfo, OutfitRankNames, Initial, Unk1, Update, UpdateMemberCount} import net.psforever.packet.game.OutfitMembershipResponse.PacketType.CreateResponse import net.psforever.packet.game._ import net.psforever.services.avatar.{AvatarAction, AvatarServiceMessage} @@ -63,7 +63,7 @@ object SessionOutfitHandlers { outfit.created.atZone(java.time.ZoneOffset.UTC).toInstant.toEpochMilli / 1000 PlayerControl.sendResponse(player.Zone, player.Name, - OutfitEvent(outfit.id, Unk2( + OutfitEvent(outfit.id, Update( OutfitInfo( outfit.name, 0, 0, 1, OutfitRankNames("", "", "", "", "", "", "", ""), @@ -143,16 +143,16 @@ object SessionOutfitHandlers { invited.CharId, outfitInvite.sentFrom.CharId, invited.Name, outfit.name, flag = true)) PlayerControl.sendResponse(outfitInvite.sentFrom.Zone, outfitInvite.sentFrom.Name, - OutfitEvent(outfitId, OutfitEventAction.Unk5(memberCount))) + OutfitEvent(outfitId, UpdateMemberCount(memberCount))) PlayerControl.sendResponse(outfitInvite.sentFrom.Zone, outfitInvite.sentFrom.Name, OutfitMemberEvent(outfitId, invited.CharId, - OutfitMemberEventAction.Unk0(invited.Name, 0, 0, 0, + OutfitMemberEventAction.Update(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( + OutfitEvent(outfitId, Initial(OutfitInfo( outfit.name, points, points, memberCount, OutfitRankNames("", "", "", "", "", "", "", ""), outfit.motd.getOrElse(""), @@ -211,12 +211,12 @@ object SessionOutfitHandlers { case (deleted, _) => if (deleted > 0) { PlayerControl.sendResponse(kickedBy.Zone, kickedBy.Name, - OutfitMemberEvent(kickedBy.outfit_id, kickedId, OutfitMemberEventAction.Unk1())) + OutfitMemberEvent(kickedBy.outfit_id, kickedId, OutfitMemberEventAction.Kicked())) 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())) + OutfitMemberEvent(kickedBy.outfit_id, kickedId, OutfitMemberEventAction.Kicked())) ) session.chat.LeaveChannel(OutfitChannel(kickedBy.outfit_id)) @@ -238,8 +238,13 @@ object SessionOutfitHandlers { case (deleted, _) => if (deleted > 0) { findPlayerByIdForOutfitAction(zones, kickedId, kickedBy).foreach { kicked => + PlayerControl.sendResponse(kicked.Zone, kicked.Name, - OutfitMembershipResponse(OutfitMembershipResponse.PacketType.Kick, 0, 1, + OutfitEvent(kickedBy.outfit_id, Leaving()) + ) + + PlayerControl.sendResponse(kicked.Zone, kicked.Name, + OutfitMembershipResponse(OutfitMembershipResponse.PacketType.YouGotKicked, 0, 1, kickedBy.CharId, kicked.CharId, kicked.Name, kickedBy.Name, flag = false)) kicked.Zone.AvatarEvents ! AvatarServiceMessage(kicked.Zone.id, @@ -251,7 +256,7 @@ object SessionOutfitHandlers { kicked.outfit_id = 0 kicked.outfit_name = "" PlayerControl.sendResponse(kicked.Zone, kicked.Name, - OutfitMemberEvent(kickedBy.outfit_id, kickedId, OutfitMemberEventAction.Unk1())) + OutfitMemberEvent(kickedBy.outfit_id, kickedId, OutfitMemberEventAction.Kicked())) } val avatarName: Future[Option[String]] = ctx.run( @@ -260,15 +265,15 @@ object SessionOutfitHandlers { avatarName.foreach { case Some(name) => PlayerControl.sendResponse(kickedBy.Zone, kickedBy.Name, - OutfitMembershipResponse(OutfitMembershipResponse.PacketType.Kick, 0, 1, kickedBy.CharId, kickedId, name, "", flag = true)) + OutfitMembershipResponse(OutfitMembershipResponse.PacketType.YouKicked, 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)) + OutfitMembershipResponse(OutfitMembershipResponse.PacketType.YouKicked, 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())) + OutfitMemberEvent(kickedBy.outfit_id, kickedId, OutfitMemberEventAction.Kicked())) ) // this needs to be the kicked player // session.chat.LeaveChannel(OutfitChannel(kickedBy.outfit_id)) @@ -303,7 +308,7 @@ object SessionOutfitHandlers { PlayerControl.sendResponse( zone, outfitMember.Name, OutfitMemberEvent(outfit_id, promoter.avatar.id, - OutfitMemberEventAction.Unk0(promoter.Name, 6, owner_points, 0, OutfitMemberEventAction.PacketType.Padding, 0))) + OutfitMemberEventAction.Update(promoter.Name, 6, owner_points, 0, OutfitMemberEventAction.PacketType.Padding, 0))) }) }) } @@ -327,7 +332,7 @@ object SessionOutfitHandlers { PlayerControl.sendResponse( zone, player.Name, OutfitMemberEvent(outfit_id, promoted.avatar.id, - OutfitMemberEventAction.Unk0(promoted.Name, newRank, member_points, 0, OutfitMemberEventAction.PacketType.Padding, 0))) + OutfitMemberEventAction.Update(promoted.Name, newRank, member_points, 0, OutfitMemberEventAction.PacketType.Padding, 0))) }) }) } @@ -356,7 +361,7 @@ object SessionOutfitHandlers { val seconds: Long = outfit.created.atZone(java.time.ZoneOffset.UTC).toInstant.toEpochMilli / 1000 PlayerControl.sendResponse(player.Zone, player.Name, - OutfitEvent(outfit.id, Unk0(OutfitInfo( + OutfitEvent(outfit.id, Initial(OutfitInfo( outfit.name, totalPoints, totalPoints, @@ -382,7 +387,7 @@ object SessionOutfitHandlers { } PlayerControl.sendResponse(player.Zone, player.Name, OutfitMemberEvent(outfit.id, avatarId, - OutfitMemberEventAction.Unk0( + OutfitMemberEventAction.Update( avatarName, rank, points, @@ -438,7 +443,7 @@ object SessionOutfitHandlers { // send to all online players in outfit val outfit_event = OutfitEvent( outfit_id, - Unk2( + Update( OutfitInfo( outfit_name = outfit.name, outfit_points1 = totalPoints, @@ -500,7 +505,7 @@ object SessionOutfitHandlers { // send to all online players in outfit val outfit_event = OutfitEvent( outfit_id, - Unk2( + Update( OutfitInfo( outfit_name = outfit.name, outfit_points1 = totalPoints, @@ -555,7 +560,7 @@ object SessionOutfitHandlers { val seconds: Long = outfit.created.atZone(java.time.ZoneOffset.UTC).toInstant.toEpochMilli / 1000 PlayerControl.sendResponse(player.Zone, player.Name, - OutfitEvent(outfitId, Unk2(OutfitInfo( + OutfitEvent(outfitId, Update(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(""), @@ -744,7 +749,7 @@ object SessionOutfitHandlers { } } - def updateMemberRankById(outfit_id: Long, avatar_id: Long, rank: Int): Quoted[Update[Outfitmember]] = quote { + def updateMemberRankById(outfit_id: Long, avatar_id: Long, rank: Int): Quoted[io.getquill.Update[Outfitmember]] = quote { query[Outfitmember] .filter(_.outfit_id == lift(outfit_id)) .filter(_.avatar_id == lift(avatar_id)) @@ -759,7 +764,7 @@ object SessionOutfitHandlers { } } - def updateOutfitOwnerById(outfit_id: Long, owner_id: Long): Quoted[Update[Outfit]] = quote { + def updateOutfitOwnerById(outfit_id: Long, owner_id: Long): Quoted[io.getquill.Update[Outfit]] = quote { query[Outfit] .filter(_.id == lift(outfit_id)) .update(_.owner_id -> lift(owner_id)) @@ -775,7 +780,7 @@ object SessionOutfitHandlers { } } - def updateOutfitMotdById(outfit_id: Long, motd: Option[String]): Quoted[Update[Outfit]] = quote { + def updateOutfitMotdById(outfit_id: Long, motd: Option[String]): Quoted[io.getquill.Update[Outfit]] = quote { query[Outfit] .filter(_.id == lift(outfit_id)) .update(_.motd -> lift(motd)) @@ -789,7 +794,7 @@ object SessionOutfitHandlers { } } - def updateOutfitRanksById(outfit_id: Long, list: List[Option[String]]): Quoted[Update[Outfit]] = { + def updateOutfitRanksById(outfit_id: Long, list: List[Option[String]]): Quoted[io.getquill.Update[Outfit]] = { // Normalize: turn empty strings into None val normalized = list.map { diff --git a/src/main/scala/net/psforever/packet/game/OutfitEvent.scala b/src/main/scala/net/psforever/packet/game/OutfitEvent.scala index be267fce7..1732e4c85 100644 --- a/src/main/scala/net/psforever/packet/game/OutfitEvent.scala +++ b/src/main/scala/net/psforever/packet/game/OutfitEvent.scala @@ -50,26 +50,55 @@ object OutfitEventAction { unk25: Long, ) - final case class Unk0( + /** + * Initial + * + * Send at the start of an OutfitWindow info dump. + * + * Not always complete, seen as an initialization after login, join or while outfit is in formation. + * @param outfit_info + */ + final case class Initial( outfit_info: OutfitInfo ) extends OutfitEventAction(code = 0) final case class Unk1( ) extends OutfitEventAction(code = 1) - final case class Unk2( + /** + * Update + * + * Send after changing outfit Ranks, MOTD and other situations. + * @param outfit_info + */ + final case class Update( outfit_info: OutfitInfo, ) extends OutfitEventAction(code = 2) - final case class Unk3( + /** + * Send to players to tell them they left the outfit. + * + * Resets them to behave like they have no outfit. + * Will have them open the OutfitListWindow instead of the OutfitWindow. + */ + final case class Leaving( ) extends OutfitEventAction(code = 3) + /** + * Used to switch from the temporary "invalid" outfit ID used while formation to a valid ID used from that point on. + * @param new_outfit_id the new ID that represents this specific outfit in the DB + */ final case class UpdateOutfitId( new_outfit_id: Long, ) extends OutfitEventAction(code = 4) - final case class Unk5( - unk1: Long, + /** + * Used to tell outfit members that the member count changed. + * Send after InviteAccept or Kick actions + * @param member_count + */ + final case class UpdateMemberCount( + member_count: Long, ) extends OutfitEventAction(code = 5) final case class Unknown(badCode: Int, data: BitVector) extends OutfitEventAction(badCode) @@ -126,35 +155,35 @@ object OutfitEventAction { } ) - val Unk0Codec: Codec[Unk0] = ( + val Unk0Codec: Codec[Initial] = ( ("outfit_info" | InfoCodec) - ).xmap[Unk0]( + ).xmap[Initial]( { case info => - Unk0(info) + Initial(info) }, { - case Unk0(info) => + case Initial(info) => info } ) val Unk1Codec: Codec[Unk1] = PacketHelpers.emptyCodec(Unk1()) - val Unk2Codec: Codec[Unk2] = ( + val Unk2Codec: Codec[Update] = ( ("outfit_info" | InfoCodec) - ).xmap[Unk2]( + ).xmap[Update]( { case info => - Unk2(info) + Update(info) }, { - case Unk2(info) => + case Update(info) => info } ) - val Unk3Codec: Codec[Unk3] = PacketHelpers.emptyCodec(Unk3()) + val Unk3Codec: Codec[Leaving] = PacketHelpers.emptyCodec(Leaving()) 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) @@ -169,15 +198,15 @@ object OutfitEventAction { } ) - val Unk5Codec: Codec[Unk5] = ( + val UpdateMemberCountCodec: Codec[UpdateMemberCount] = ( ("" | uint32L) - ).xmap[Unk5]( + ).xmap[UpdateMemberCount]( { case u1 => - Unk5(u1) + UpdateMemberCount(u1) }, { - case Unk5(u1) => + case UpdateMemberCount(u1) => u1 } ) @@ -220,7 +249,7 @@ object OutfitEvent extends Marshallable[OutfitEvent] { 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 UpdateMemberCount: PacketType.Value = Value(5) val Unk6: PacketType.Value = Value(6) val Unk7: PacketType.Value = Value(7) @@ -237,7 +266,7 @@ object OutfitEvent extends Marshallable[OutfitEvent] { case 2 => Unk2Codec // sent after /outfitcreate and on login if in an outfit case 3 => Unk3Codec case 4 => UpdateOutfitIdCodec - case 5 => Unk5Codec + case 5 => UpdateMemberCountCodec case 6 => unknownCodec(action = code) case 7 => unknownCodec(action = code) diff --git a/src/main/scala/net/psforever/packet/game/OutfitMemberEvent.scala b/src/main/scala/net/psforever/packet/game/OutfitMemberEvent.scala index 0ae680853..a268a6d17 100644 --- a/src/main/scala/net/psforever/packet/game/OutfitMemberEvent.scala +++ b/src/main/scala/net/psforever/packet/game/OutfitMemberEvent.scala @@ -33,20 +33,30 @@ object OutfitMemberEventAction { } - /* - action is unimplemented! if action == 0 unk2 will contain one additional uint32L - padding contains one uint4L of padding. may contain uint32L of unknown data depending on action - */ - final case class Unk0( + /** + * + * Update + * + * Update is used to inform outfit members about a new member. + * Gets send after an InviteAccept or Rank changes. + * + * @param member_name + * @param rank + * @param points client divides this by 100 + * @param last_online seconds ago from current time, 0 if online + * @param action should always be 1, otherwise there will be actual data in padding. not implemented! + * @param padding should always be 0, 4 bits of padding // only contains data if action is 0 + */ + final case class Update( member_name: String, rank: Int, - points: Long, // client divides this by 100 - 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 + points: Long, + last_online: Long, + action: PacketType.Type, + padding: Int ) extends OutfitMemberEventAction(code = 0) - final case class Unk1( + final case class Kicked( ) extends OutfitMemberEventAction(code = 1) final case class Unknown(badCode: Int, data: BitVector) extends OutfitMemberEventAction(badCode) @@ -58,25 +68,25 @@ object OutfitMemberEventAction { object Codecs { private val everFailCondition = conditional(included = false, bool) - val Unk0Codec: Codec[Unk0] = ( - ("member_name" | PacketHelpers.encodedWideStringAligned(6)) :: // from here is packet_type == 0 only + val UpdateCodec: Codec[Update] = ( + ("member_name" | PacketHelpers.encodedWideStringAligned(6)) :: ("rank" | uint(3)) :: ("points" | uint32L) :: ("last_login" | uint32L) :: ("action" | OutfitMemberEventAction.PacketType.codec) :: ("padding" | uint4L) - ).xmap[Unk0]( + ).xmap[Update]( { case member_name :: rank :: points :: last_login :: action :: padding :: HNil => - Unk0(member_name, rank, points, last_login, action, padding) + Update(member_name, rank, points, last_login, action, padding) }, { - case Unk0(member_name, rank, points, last_login, action, padding) => + case Update(member_name, rank, points, last_login, action, padding) => member_name :: rank :: points :: last_login :: action :: padding :: HNil } ) - val Unk1Codec: Codec[Unk1] = PacketHelpers.emptyCodec(Unk1()) + val KickedCodec: Codec[Kicked] = PacketHelpers.emptyCodec(Kicked()) /** * A common form for known action code indexes with an unknown purpose and transformation is an "Unknown" object. @@ -109,8 +119,8 @@ object OutfitMemberEvent extends Marshallable[OutfitMemberEvent] { object PacketType extends Enumeration { type Type = Value - 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 Update: PacketType.Value = Value(0) + val Kicked: PacketType.Value = Value(1) val Unk2: PacketType.Value = Value(2) val Unk3: PacketType.Value = Value(3) @@ -122,8 +132,8 @@ object OutfitMemberEvent extends Marshallable[OutfitMemberEvent] { import scala.annotation.switch ((code: @switch) match { - case 0 => Unk0Codec - case 1 => Unk1Codec + case 0 => UpdateCodec + case 1 => KickedCodec case 2 => unknownCodec(code) case 3 => unknownCodec(code) diff --git a/src/main/scala/net/psforever/packet/game/OutfitMembershipRequest.scala b/src/main/scala/net/psforever/packet/game/OutfitMembershipRequest.scala index 41cb6b176..8b12ec5b5 100644 --- a/src/main/scala/net/psforever/packet/game/OutfitMembershipRequest.scala +++ b/src/main/scala/net/psforever/packet/game/OutfitMembershipRequest.scala @@ -9,7 +9,7 @@ import scodec.codecs._ import shapeless.{::, HNil} final case class OutfitMembershipRequest( - outfit_id: Long, + requester_id: Long, action: OutfitMembershipRequestAction ) extends PlanetSideGamePacket { type Packet = OutfitMembershipRequest @@ -37,8 +37,8 @@ object OutfitMembershipRequestAction { ) extends OutfitMembershipRequestAction(code = 1) final case class Invite( - avatar_id: Long, - member_name: String, + target_id: Long, + target_name: String, ) extends OutfitMembershipRequestAction(code = 2) final case class AcceptInvite( @@ -50,19 +50,19 @@ object OutfitMembershipRequestAction { ) extends OutfitMembershipRequestAction(code = 4) final case class CancelInvite( - avatar_id: Long, - member_name: String, + target_id: Long, + target_name: String, ) extends OutfitMembershipRequestAction(code = 5) final case class Kick( - avatar_id: Long, - member_name: String, + target_id: Long, + target_name: String, ) extends OutfitMembershipRequestAction(code = 6) final case class SetRank( - avatar_id: Long, + target_id: Long, rank: Int, - member_name: String, + target_name: String, ) extends OutfitMembershipRequestAction(code = 7) final case class Unknown(badCode: Int, data: BitVector) extends OutfitMembershipRequestAction(badCode) diff --git a/src/main/scala/net/psforever/packet/game/OutfitMembershipResponse.scala b/src/main/scala/net/psforever/packet/game/OutfitMembershipResponse.scala index 90f592954..c38f1ba9f 100644 --- a/src/main/scala/net/psforever/packet/game/OutfitMembershipResponse.scala +++ b/src/main/scala/net/psforever/packet/game/OutfitMembershipResponse.scala @@ -12,7 +12,7 @@ final case class OutfitMembershipResponse( packet_type: OutfitMembershipResponse.PacketType.Type, unk0: Int, unk1: Int, - outfit_id: Long, + requester_id: Long, target_id: Long, str1: String, str2: String, @@ -34,8 +34,8 @@ object OutfitMembershipResponse extends Marshallable[OutfitMembershipResponse] { 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 YouGotKicked: PacketType.Value = Value(4) + val YouKicked: 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/packet/game/OutfitRequest.scala b/src/main/scala/net/psforever/packet/game/OutfitRequest.scala index 3e8d5f513..5520d98b8 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( - outfit_id: Long, + requester_id: Long, action: OutfitRequestAction ) extends PlanetSideGamePacket { type Packet = OutfitRequest @@ -42,13 +42,13 @@ object OutfitRequestAction { * na * @param unk na */ - final case class Unk3(menuOpen: Boolean) extends OutfitRequestAction(code = 3) + final case class OutfitWindowOpen(menuOpen: Boolean) extends OutfitRequestAction(code = 3) /** * na * @param unk na */ - final case class Unk4(menuOpen: Boolean) extends OutfitRequestAction(code = 4) + final case class OutfitListWindowOpen(menuOpen: Boolean) extends OutfitRequestAction(code = 4) /** * na @@ -116,24 +116,24 @@ object OutfitRequest extends Marshallable[OutfitRequest] { /** * na */ - private val unk3Codec: Codec[OutfitRequestAction] = bool.hlist.xmap[OutfitRequestAction] ( + private val OutfitWindowOpenCodec: Codec[OutfitRequestAction] = bool.hlist.xmap[OutfitRequestAction] ( { - case value :: HNil => OutfitRequestAction.Unk3(value) + case value :: HNil => OutfitRequestAction.OutfitWindowOpen(value) }, { - case OutfitRequestAction.Unk3(value) => value :: HNil + case OutfitRequestAction.OutfitWindowOpen(value) => value :: HNil } ) /** * na */ - private val unk4Codec: Codec[OutfitRequestAction] = bool.hlist.xmap[OutfitRequestAction] ( + private val OutfitListWindowOpenCodec: Codec[OutfitRequestAction] = bool.hlist.xmap[OutfitRequestAction] ( { - case value :: HNil => OutfitRequestAction.Unk4(value) + case value :: HNil => OutfitRequestAction.OutfitListWindowOpen(value) }, { - case OutfitRequestAction.Unk4(value) => value :: HNil + case OutfitRequestAction.OutfitListWindowOpen(value) => value :: HNil } ) @@ -151,8 +151,8 @@ object OutfitRequest extends Marshallable[OutfitRequest] { 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) + val OutfitWindowOpen: PacketType.Value = Value(3) + val OutfitListWindowOpen: 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)) } @@ -165,8 +165,8 @@ object OutfitRequest extends Marshallable[OutfitRequest] { case 0 => MotdCodec case 1 => RankCodec case 2 => unk2Codec - case 3 => unk3Codec - case 4 => unk4Codec + case 3 => OutfitWindowOpenCodec + case 4 => OutfitListWindowOpenCodec case _ => failCodec(code) } } diff --git a/src/test/scala/game/OutfitEventTest.scala b/src/test/scala/game/OutfitEventTest.scala index 27039f043..d54cc16f9 100644 --- a/src/test/scala/game/OutfitEventTest.scala +++ b/src/test/scala/game/OutfitEventTest.scala @@ -53,7 +53,7 @@ class OutfitEventTest extends Specification { PacketCoding.decodePacket(unk0_ABC).require match { case OutfitEvent(outfit_guid, action) => outfit_guid mustEqual 25044 - action mustEqual Unk0( + action mustEqual Initial( OutfitInfo( outfit_name = "Black Armored Reapers", outfit_points1 = 223190045, @@ -78,7 +78,7 @@ class OutfitEventTest extends Specification { "encode Unk0 ABC" in { val msg = OutfitEvent( 25044, - Unk0( + Initial( OutfitInfo( outfit_name = "Black Armored Reapers", outfit_points1 = 223190045, @@ -125,7 +125,7 @@ class OutfitEventTest extends Specification { PacketCoding.decodePacket(unk2_ABC).require match { case OutfitEvent(outfit_guid, action) => outfit_guid mustEqual 2147418113L - action mustEqual Unk2(OutfitInfo( + action mustEqual Update(OutfitInfo( outfit_name = "PlanetSide_Forever_Vanu", outfit_points1 = 0, outfit_points2 = 0, @@ -148,7 +148,7 @@ class OutfitEventTest extends Specification { "encode Unk2 ABC" in { val msg = OutfitEvent( 2147418113L, - Unk2( + Update( OutfitInfo( outfit_name = "PlanetSide_Forever_Vanu", outfit_points1 = 0, @@ -175,7 +175,7 @@ class OutfitEventTest extends Specification { PacketCoding.decodePacket(unk3_ABC).require match { case OutfitEvent(outfit_guid, action) => outfit_guid mustEqual 2147418113L - action mustEqual Unk3() + action mustEqual Leaving() case _ => ko } @@ -184,7 +184,7 @@ class OutfitEventTest extends Specification { "encode Unk3 ABC" in { val msg = OutfitEvent( 2147418113L, - Unk3() + Leaving() ) val pkt = PacketCoding.encodePacket(msg).require.toByteVector @@ -219,8 +219,8 @@ class OutfitEventTest extends Specification { PacketCoding.decodePacket(unk5_ABC).require match { case OutfitEvent(outfit_guid, action) => outfit_guid mustEqual 2147418113L - action mustEqual Unk5( - unk1 = 2, + action mustEqual UpdateMemberCount( + member_count = 2, ) case _ => ko @@ -230,8 +230,8 @@ class OutfitEventTest extends Specification { "encode Unk5 ABC" in { val msg = OutfitEvent( 2147418113L, - Unk5( - unk1 = 2, + UpdateMemberCount( + member_count = 2, ) ) val pkt = PacketCoding.encodePacket(msg).require.toByteVector diff --git a/src/test/scala/game/OutfitMemberEventTest.scala b/src/test/scala/game/OutfitMemberEventTest.scala index 510e7278a..8e0bdf545 100644 --- a/src/test/scala/game/OutfitMemberEventTest.scala +++ b/src/test/scala/game/OutfitMemberEventTest.scala @@ -22,7 +22,7 @@ class OutfitMemberEventTest extends Specification { "decode Lazer padding" in { PacketCoding.decodePacket(Lazer).require match { - case OutfitMemberEvent(outfit_id, member_id, Unk0(member_name, rank, points, last_login, action, padding)) => + case OutfitMemberEvent(outfit_id, member_id, Update(member_name, rank, points, last_login, action, padding)) => outfit_id mustEqual 6418 member_id mustEqual 705344 member_name mustEqual "Lazer1982" @@ -40,7 +40,7 @@ class OutfitMemberEventTest extends Specification { val msg = OutfitMemberEvent( outfit_id = 6418, member_id = 705344, - Unk0( + Update( member_name = "Lazer1982", rank = 7, points = 3134113, @@ -56,7 +56,7 @@ class OutfitMemberEventTest extends Specification { "decode OpolE padding" in { PacketCoding.decodePacket(OpolE).require match { - case OutfitMemberEvent(outfit_id, member_id, Unk0(member_name, rank, points, last_login, action, unk0_padding)) => + case OutfitMemberEvent(outfit_id, member_id, Update(member_name, rank, points, last_login, action, unk0_padding)) => outfit_id mustEqual 6418 member_id mustEqual 42644970 member_name mustEqual "OpolE" @@ -74,7 +74,7 @@ class OutfitMemberEventTest extends Specification { val msg = OutfitMemberEvent( outfit_id = 6418, member_id = 42644970, - Unk0( + Update( member_name = "OpolE", rank = 6, points = 461901, @@ -91,7 +91,7 @@ class OutfitMemberEventTest extends Specification { "decode Unk1" in { PacketCoding.decodePacket(unk1).require match { - case OutfitMemberEvent(outfit_id, member_id, Unk1()) => + case OutfitMemberEvent(outfit_id, member_id, Kicked()) => outfit_id mustEqual 529744 member_id mustEqual 41605263 case _ => @@ -103,7 +103,7 @@ class OutfitMemberEventTest extends Specification { val msg = OutfitMemberEvent( outfit_id = 529744, member_id = 41605263, - Unk1() + Kicked() ) val pkt = PacketCoding.encodePacket(msg).require.toByteVector diff --git a/src/test/scala/game/OutfitMembershipResponseTest.scala b/src/test/scala/game/OutfitMembershipResponseTest.scala index 948cd41cc..dae406cd7 100644 --- a/src/test/scala/game/OutfitMembershipResponseTest.scala +++ b/src/test/scala/game/OutfitMembershipResponseTest.scala @@ -111,7 +111,7 @@ class OutfitMembershipResponseTest extends Specification { "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 + packet_type mustEqual PacketType.YouGotKicked unk0 mustEqual 0 unk1 mustEqual 0 outfit_id mustEqual 41593365 @@ -125,7 +125,7 @@ class OutfitMembershipResponseTest extends Specification { } "encode unk4" in { - val msg = OutfitMembershipResponse(PacketType.Unk4, 0, 0, 41593365, 0, "", "", flag = true) + val msg = OutfitMembershipResponse(PacketType.YouGotKicked, 0, 0, 41593365, 0, "", "", flag = true) val pkt = PacketCoding.encodePacket(msg).require.toByteVector pkt mustEqual unk4 @@ -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.Kick + packet_type mustEqual PacketType.YouKicked 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.Kick, 0, 1, 41593365, 41605263, "PSFoutfittest1", "", flag = true) + val msg = OutfitMembershipResponse(PacketType.YouKicked, 0, 1, 41593365, 41605263, "PSFoutfittest1", "", flag = true) val pkt = PacketCoding.encodePacket(msg).require.toByteVector pkt mustEqual unk5 diff --git a/src/test/scala/game/OutfitRequestTest.scala b/src/test/scala/game/OutfitRequestTest.scala index 4c4adcb11..67cbd62a7 100644 --- a/src/test/scala/game/OutfitRequestTest.scala +++ b/src/test/scala/game/OutfitRequestTest.scala @@ -46,7 +46,7 @@ class OutfitRequestTest extends Specification { "decode Unk3" in { PacketCoding.decodePacket(string6).require match { - case OutfitRequest(id, OutfitRequestAction.Unk3(value)) => + case OutfitRequest(id, OutfitRequestAction.OutfitWindowOpen(value)) => id mustEqual 1176612L value mustEqual true case _ => @@ -56,7 +56,7 @@ class OutfitRequestTest extends Specification { "decode Unk4" in { PacketCoding.decodePacket(string8).require match { - case OutfitRequest(id, OutfitRequestAction.Unk4(value)) => + case OutfitRequest(id, OutfitRequestAction.OutfitListWindowOpen(value)) => id mustEqual 41588237L value mustEqual true case _ => @@ -88,14 +88,14 @@ class OutfitRequestTest extends Specification { } "encode Unk3" in { - val msg = OutfitRequest(1176612L, OutfitRequestAction.Unk3(true)) + val msg = OutfitRequest(1176612L, OutfitRequestAction.OutfitWindowOpen(true)) val pkt = PacketCoding.encodePacket(msg).require.toByteVector pkt mustEqual string6 } "encode Unk4" in { - val msg = OutfitRequest(41588237L, OutfitRequestAction.Unk4(true)) + val msg = OutfitRequest(41588237L, OutfitRequestAction.OutfitListWindowOpen(true)) val pkt = PacketCoding.encodePacket(msg).require.toByteVector pkt mustEqual string8 From c0428bd3f631d9d3fa7a6b14e13c840732c5dd8a Mon Sep 17 00:00:00 2001 From: Resaec Date: Sun, 31 Aug 2025 23:37:30 +0200 Subject: [PATCH 26/33] add outfit (list) window event subscription handling fix Kicked action send instead of Leaving action when players leave on their own --- .../actors/session/normal/GeneralLogic.scala | 6 ++ .../support/SessionOutfitHandlers.scala | 72 ++++++++++++------- .../scala/net/psforever/objects/Player.scala | 2 + 3 files changed, 54 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 a2f87cd16..6be7ec0eb 100644 --- a/src/main/scala/net/psforever/actors/session/normal/GeneralLogic.scala +++ b/src/main/scala/net/psforever/actors/session/normal/GeneralLogic.scala @@ -837,13 +837,19 @@ class GeneralLogic(val ops: GeneralOperations, implicit val context: ActorContex SessionOutfitHandlers.HandleOutfitRank(zones, List(r1, r2, r3, r4, r5, r6, r7, r8), player) case OutfitRequest(_, OutfitRequestAction.OutfitWindowOpen(true)) => + player.outfit_window_open = true SessionOutfitHandlers.HandleViewOutfitWindow(zones, player, player.outfit_id) case OutfitRequest(_, OutfitRequestAction.OutfitWindowOpen(false)) => + player.outfit_window_open = false case OutfitRequest(_, OutfitRequestAction.OutfitListWindowOpen(true)) => + player.outfit_list_open = true SessionOutfitHandlers.HandleGetOutfitList(player) + case OutfitRequest(_, OutfitRequestAction.OutfitListWindowOpen(false)) => + player.outfit_list_open = false + 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 bafd60be9..be3530ae9 100644 --- a/src/main/scala/net/psforever/actors/session/support/SessionOutfitHandlers.scala +++ b/src/main/scala/net/psforever/actors/session/support/SessionOutfitHandlers.scala @@ -1,11 +1,11 @@ // Copyright (c) 2025 PSForever package net.psforever.actors.session.support -import io.getquill.{ActionReturning, EntityQuery, Insert, PostgresJAsyncContext, Query, Quoted, SnakeCase, Update} +import io.getquill.{Action, 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.{Leaving, OutfitInfo, OutfitRankNames, Initial, Unk1, Update, UpdateMemberCount} +import net.psforever.packet.game.OutfitEventAction.{Initial, Leaving, OutfitInfo, OutfitRankNames, Unk1, Update, UpdateMemberCount} import net.psforever.packet.game.OutfitMembershipResponse.PacketType.CreateResponse import net.psforever.packet.game._ import net.psforever.services.avatar.{AvatarAction, AvatarServiceMessage} @@ -207,22 +207,28 @@ object SessionOutfitHandlers { 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) { - removeMemberFromOutfit(kickedBy.outfit_id, kickedId).map { + + // store outfit_id since it will be nulled soon + val outfit_id = kickedBy.outfit_id + + removeMemberFromOutfit(outfit_id, kickedId).map { case (deleted, _) => if (deleted > 0) { - PlayerControl.sendResponse(kickedBy.Zone, kickedBy.Name, - OutfitMemberEvent(kickedBy.outfit_id, kickedId, OutfitMemberEventAction.Kicked())) - 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.Kicked())) + PlayerControl.sendResponse(kickedBy.Zone, kickedBy.Name, + OutfitEvent(outfit_id, Leaving()) ) - session.chat.LeaveChannel(OutfitChannel(kickedBy.outfit_id)) + session.chat.LeaveChannel(OutfitChannel(outfit_id)) kickedBy.outfit_name = "" kickedBy.outfit_id = 0 + zones.filter(z => z.AllPlayers.nonEmpty).flatMap(_.AllPlayers) + .filter(p => p.outfit_id == outfit_id).foreach(outfitMember => + PlayerControl.sendResponse(outfitMember.Zone, outfitMember.Name, + OutfitMemberEvent(outfit_id, kickedId, OutfitMemberEventAction.Kicked())) + ) + kickedBy.Zone.AvatarEvents ! AvatarServiceMessage(kickedBy.Zone.id, AvatarAction.PlanetsideAttributeToAll(kickedBy.GUID, 39, 0)) @@ -304,13 +310,15 @@ object SessionOutfitHandlers { 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.Update(promoter.Name, 6, owner_points, 0, OutfitMemberEventAction.PacketType.Padding, 0))) - }) - }) + zone.AllPlayers + .filter(_.outfit_id == outfit_id) + .foreach(outfitMember => { + PlayerControl.sendResponse( + zone, outfitMember.Name, + OutfitMemberEvent(outfit_id, promoter.avatar.id, + OutfitMemberEventAction.Update(promoter.Name, 6, owner_points, 0, OutfitMemberEventAction.PacketType.Padding, 0))) + }) + }) } // update promoter rank @@ -328,12 +336,14 @@ object SessionOutfitHandlers { 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.Update(promoted.Name, newRank, member_points, 0, OutfitMemberEventAction.PacketType.Padding, 0))) - }) + zone.AllPlayers + .filter(_.outfit_id == outfit_id) + .foreach(player => { + PlayerControl.sendResponse( + zone, player.Name, + OutfitMemberEvent(outfit_id, promoted.avatar.id, + OutfitMemberEventAction.Update(promoted.Name, newRank, member_points, 0, OutfitMemberEventAction.PacketType.Padding, 0))) + }) }) } @@ -472,7 +482,10 @@ object SessionOutfitHandlers { ) zones.foreach(zone => { - zone.AllPlayers.filter(_.outfit_id == outfit_id).foreach(player => { + zone.AllPlayers + .filter(_.outfit_id == outfit_id) + .filter(_.outfit_window_open) + .foreach(player => { PlayerControl.sendResponse( zone, player.Name, outfit_event @@ -502,7 +515,7 @@ object SessionOutfitHandlers { } yield { outfitOpt.foreach { outfit => - // send to all online players in outfit + // send to all online players in outfit with window open val outfit_event = OutfitEvent( outfit_id, Update( @@ -534,7 +547,10 @@ object SessionOutfitHandlers { ) zones.foreach(zone => { - zone.AllPlayers.filter(_.outfit_id == outfit_id).foreach(player => { + zone.AllPlayers + .filter(_.outfit_id == outfit_id) + .filter(_.outfit_window_open) + .foreach(player => { PlayerControl.sendResponse( zone, player.Name, outfit_event @@ -823,4 +839,8 @@ object SessionOutfitHandlers { } yield () } } + + def updateOutfitPointMV(): Quoted[Action[Unit]] = quote( + infix"REFRESH MATERIALIZED VIEW outfitpoint_mv".as[Action[Unit]] + ) } diff --git a/src/main/scala/net/psforever/objects/Player.scala b/src/main/scala/net/psforever/objects/Player.scala index 074937edd..3cdb1b47f 100644 --- a/src/main/scala/net/psforever/objects/Player.scala +++ b/src/main/scala/net/psforever/objects/Player.scala @@ -87,6 +87,8 @@ class Player(var avatar: Avatar) var lastShotSeq_time: Int = -1 var outfit_name: String = "" var outfit_id: Long = 0 + var outfit_window_open: Boolean = false + var outfit_list_open: Boolean = false /** From PlanetsideAttributeMessage */ var PlanetsideAttribute: Array[Long] = Array.ofDim(120) From 474993a4e29b9e6de9af99ad912f052d8f769edd Mon Sep 17 00:00:00 2001 From: Resaec Date: Sun, 31 Aug 2025 23:38:52 +0200 Subject: [PATCH 27/33] keep outfit title colorization when receiving Rank action from client --- .../support/SessionOutfitHandlers.scala | 22 ++++++++++++------- 1 file changed, 14 insertions(+), 8 deletions(-) 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 be3530ae9..d382da0be 100644 --- a/src/main/scala/net/psforever/actors/session/support/SessionOutfitHandlers.scala +++ b/src/main/scala/net/psforever/actors/session/support/SessionOutfitHandlers.scala @@ -818,17 +818,23 @@ object SessionOutfitHandlers { case _ => None } + // Colorize: prepend \ in front of # if it is the fist character + val colorized = normalized.map { + case Some(s) if s.startsWith("#") => Some("\\" + s) + case other => other + } + query[Outfit] .filter(_.id == lift(outfit_id)) .update( - _.rank0 -> lift(normalized(0)), - _.rank1 -> lift(normalized(1)), - _.rank2 -> lift(normalized(2)), - _.rank3 -> lift(normalized(3)), - _.rank4 -> lift(normalized(4)), - _.rank5 -> lift(normalized(5)), - _.rank6 -> lift(normalized(6)), - _.rank7 -> lift(normalized(7)) + _.rank0 -> lift(colorized(0)), + _.rank1 -> lift(colorized(1)), + _.rank2 -> lift(colorized(2)), + _.rank3 -> lift(colorized(3)), + _.rank4 -> lift(colorized(4)), + _.rank5 -> lift(colorized(5)), + _.rank6 -> lift(colorized(6)), + _.rank7 -> lift(colorized(7)) ) } From 1556025ce6572bc3875c57dfbeb16c4cfa5b7fa7 Mon Sep 17 00:00:00 2001 From: Resaec Date: Sun, 31 Aug 2025 23:43:02 +0200 Subject: [PATCH 28/33] added schedule to update materialized view outfitpoint_mv added concurrently option to query --- .../support/SessionOutfitHandlers.scala | 21 ++++++++++++++++++- 1 file changed, 20 insertions(+), 1 deletion(-) 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 d382da0be..18495cbab 100644 --- a/src/main/scala/net/psforever/actors/session/support/SessionOutfitHandlers.scala +++ b/src/main/scala/net/psforever/actors/session/support/SessionOutfitHandlers.scala @@ -14,6 +14,7 @@ import net.psforever.types.ChatMessageType import net.psforever.util.Config import java.time.LocalDateTime +import java.util.concurrent.Executors import scala.util.{Failure, Success} object SessionOutfitHandlers { @@ -561,7 +562,25 @@ object SessionOutfitHandlers { } } + private var outfitPointSheduleStarted = false + def HandleLoginOutfitCheck(player: Player, session: SessionData): Unit = { + + // TODO: implement this the proper way, please + // start the shedule on first run of the function + if (!outfitPointSheduleStarted) { + outfitPointSheduleStarted = true + + Executors.newSingleThreadScheduledExecutor.scheduleAtFixedRate( + () => { + ctx.run(updateOutfitPointMV()) + }, + 0, + 5, + java.util.concurrent.TimeUnit.MINUTES + ) + } + ctx.run(getOutfitOnLogin(player.avatar.id)).flatMap { memberships => memberships.headOption match { case Some(membership) => @@ -847,6 +866,6 @@ object SessionOutfitHandlers { } def updateOutfitPointMV(): Quoted[Action[Unit]] = quote( - infix"REFRESH MATERIALIZED VIEW outfitpoint_mv".as[Action[Unit]] + infix"REFRESH MATERIALIZED VIEW CONCURRENTLY outfitpoint_mv".as[Action[Unit]] ) } From 6ae3a719079c2097eefdcb5f6e600c4ed4fe8c26 Mon Sep 17 00:00:00 2001 From: ScrawnyRonnie Date: Mon, 1 Sep 2025 08:18:57 -0400 Subject: [PATCH 29/33] ants share exp with squad --- .../session/normal/AvatarHandlerLogic.scala | 3 +++ .../actors/session/normal/GeneralLogic.scala | 12 +++++++----- .../support/SessionAvatarHandlers.scala | 19 +++++++++++++++++-- .../support/SessionOutfitHandlers.scala | 2 +- .../resourcesilo/ResourceSiloControl.scala | 2 ++ .../services/avatar/AvatarService.scala | 9 +++++++++ .../avatar/AvatarServiceMessage.scala | 5 +++-- .../avatar/AvatarServiceResponse.scala | 5 +++-- 8 files changed, 45 insertions(+), 12 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 c81be928c..03d6fe633 100644 --- a/src/main/scala/net/psforever/actors/session/normal/AvatarHandlerLogic.scala +++ b/src/main/scala/net/psforever/actors/session/normal/AvatarHandlerLogic.scala @@ -479,6 +479,9 @@ class AvatarHandlerLogic(val ops: SessionAvatarHandlers, implicit val context: A case AvatarResponse.ShareKillExperienceWithSquad(killer, exp) => ops.shareKillExperienceWithSquad(killer, exp) + case AvatarResponse.ShareAntExperienceWithSquad(owner, exp, vehicle) => + ops.shareAntExperienceWithSquad(owner, exp, vehicle) + case AvatarResponse.SendResponse(msg) => sendResponse(msg) 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 6be7ec0eb..a51565378 100644 --- a/src/main/scala/net/psforever/actors/session/normal/GeneralLogic.scala +++ b/src/main/scala/net/psforever/actors/session/normal/GeneralLogic.scala @@ -37,11 +37,9 @@ 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.OutfitEventAction.{OutfitInfo, OutfitRankNames, Update} -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.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, 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} @@ -807,7 +805,9 @@ class GeneralLogic(val ops: GeneralOperations, implicit val context: ActorContex } case OutfitMembershipRequest(_, OutfitMembershipRequestAction.Invite(_, invitedName)) => - SessionOutfitHandlers.HandleOutfitInvite(zones, invitedName, player) + if (player.outfit_id != 0) { + SessionOutfitHandlers.HandleOutfitInvite(zones, invitedName, player) + } case OutfitMembershipRequest(_, OutfitMembershipRequestAction.AcceptInvite(_)) => SessionOutfitHandlers.HandleOutfitInviteAccept(player, sessionLogic) @@ -816,7 +816,9 @@ class GeneralLogic(val ops: GeneralOperations, implicit val context: ActorContex SessionOutfitHandlers.HandleOutfitInviteReject(player) case OutfitMembershipRequest(_, OutfitMembershipRequestAction.Kick(memberId, _)) => - SessionOutfitHandlers.HandleOutfitKick(zones, memberId, player, sessionLogic) + if (player.outfit_id != 0) { + SessionOutfitHandlers.HandleOutfitKick(zones, memberId, player, sessionLogic) + } case OutfitMembershipRequest(_, OutfitMembershipRequestAction.SetRank(memberId, newRank, _)) => SessionOutfitHandlers.HandleOutfitPromote(zones, memberId, newRank, player) diff --git a/src/main/scala/net/psforever/actors/session/support/SessionAvatarHandlers.scala b/src/main/scala/net/psforever/actors/session/support/SessionAvatarHandlers.scala index c00b4a835..da4cc1c96 100644 --- a/src/main/scala/net/psforever/actors/session/support/SessionAvatarHandlers.scala +++ b/src/main/scala/net/psforever/actors/session/support/SessionAvatarHandlers.scala @@ -3,8 +3,8 @@ package net.psforever.actors.session.support import akka.actor.{ActorContext, typed} import net.psforever.objects.serverobject.mount.Mountable -import net.psforever.objects.{Default, PlanetSideGameObject, Player} -import net.psforever.objects.sourcing.{PlayerSource, SourceEntry} +import net.psforever.objects.{Default, PlanetSideGameObject, Player, Vehicle} +import net.psforever.objects.sourcing.{PlayerSource, SourceEntry, UniquePlayer} import net.psforever.packet.game.objectcreate.ConstructorData import net.psforever.objects.zones.exp import net.psforever.services.Service @@ -145,6 +145,21 @@ class SessionAvatarHandlers( } } + def shareAntExperienceWithSquad(driver: UniquePlayer, exp: Long, vehicle: Vehicle): Unit = { + val squadUI = sessionLogic.squad.squadUI + val squadSize = squadUI.size + if (squadSize > 1) { + val squadMembers = squadUI.filterNot(_._1 == driver.charId).map { case (_, member) => member }.toList.map(_.name) + val playersInZone = vehicle.Zone.Players.map { avatar => (avatar.id, avatar.basic.name) } + val squadMembersHere = playersInZone.filter(member => squadMembers.contains(member._2)) + squadMembersHere.foreach { member => + vehicle.Zone.AvatarEvents ! AvatarServiceMessage( + member._2, + AvatarAction.AwardBep(member._1, exp, ExperienceType.Normal)) + } + } + } + /** * Properly format a `DestroyDisplayMessage` packet * given sufficient information about a target (victim) and an actor (killer). 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 18495cbab..599adf708 100644 --- a/src/main/scala/net/psforever/actors/session/support/SessionOutfitHandlers.scala +++ b/src/main/scala/net/psforever/actors/session/support/SessionOutfitHandlers.scala @@ -252,7 +252,7 @@ object SessionOutfitHandlers { PlayerControl.sendResponse(kicked.Zone, kicked.Name, OutfitMembershipResponse(OutfitMembershipResponse.PacketType.YouGotKicked, 0, 1, - kickedBy.CharId, kicked.CharId, kicked.Name, kickedBy.Name, flag = false)) + kickedBy.CharId, kicked.CharId, kickedBy.Name, kicked.Name, flag = false)) kicked.Zone.AvatarEvents ! AvatarServiceMessage(kicked.Zone.id, AvatarAction.PlanetsideAttributeToAll(kicked.GUID, 39, 0)) diff --git a/src/main/scala/net/psforever/objects/serverobject/resourcesilo/ResourceSiloControl.scala b/src/main/scala/net/psforever/objects/serverobject/resourcesilo/ResourceSiloControl.scala index a892e21b0..ab4d3499c 100644 --- a/src/main/scala/net/psforever/objects/serverobject/resourcesilo/ResourceSiloControl.scala +++ b/src/main/scala/net/psforever/objects/serverobject/resourcesilo/ResourceSiloControl.scala @@ -197,6 +197,8 @@ class ResourceSiloControl(resourceSilo: ResourceSilo) owner.name, AvatarAction.AwardBep(owner.charId, deposit, ExperienceType.Normal) ) + vehicle.Zone.AvatarEvents ! AvatarServiceMessage( + owner.name, AvatarAction.ShareAntExperienceWithSquad(owner, deposit, vehicle)) zones.exp.ToDatabase.reportNtuActivity(owner.charId, resourceSilo.Zone.Number, resourceSilo.Owner.GUID.guid, deposit) } } diff --git a/src/main/scala/net/psforever/services/avatar/AvatarService.scala b/src/main/scala/net/psforever/services/avatar/AvatarService.scala index 54afc5bf1..6b5caaf0b 100644 --- a/src/main/scala/net/psforever/services/avatar/AvatarService.scala +++ b/src/main/scala/net/psforever/services/avatar/AvatarService.scala @@ -475,6 +475,15 @@ class AvatarService(zone: Zone) extends Actor { ) ) + case AvatarAction.ShareAntExperienceWithSquad(owner, exp, vehicle) => + AvatarEvents.publish( + AvatarServiceResponse( + s"/$forChannel/Avatar", + Service.defaultPlayerGUID, + AvatarResponse.ShareAntExperienceWithSquad(owner, exp, vehicle) + ) + ) + case _ => () } diff --git a/src/main/scala/net/psforever/services/avatar/AvatarServiceMessage.scala b/src/main/scala/net/psforever/services/avatar/AvatarServiceMessage.scala index c307c7b3e..3ab388dee 100644 --- a/src/main/scala/net/psforever/services/avatar/AvatarServiceMessage.scala +++ b/src/main/scala/net/psforever/services/avatar/AvatarServiceMessage.scala @@ -1,13 +1,13 @@ // Copyright (c) 2017 PSForever package net.psforever.services.avatar -import net.psforever.objects.Player +import net.psforever.objects.{Player, Vehicle} import net.psforever.objects.avatar.scoring.KDAStat import net.psforever.objects.ballistics.Projectile import net.psforever.objects.equipment.Equipment import net.psforever.objects.inventory.InventoryItem import net.psforever.objects.serverobject.environment.interaction.common.Watery.OxygenStateTarget -import net.psforever.objects.sourcing.SourceEntry +import net.psforever.objects.sourcing.{SourceEntry, UniquePlayer} import net.psforever.objects.vital.interaction.DamageResult import net.psforever.objects.zones.Zone import net.psforever.packet.PlanetSideGamePacket @@ -163,6 +163,7 @@ object AvatarAction { final case class AwardCep(charId: Long, bep: Long) extends Action final case class FacilityCaptureRewards(building_id: Int, zone_number: Int, exp: Long) extends Action final case class ShareKillExperienceWithSquad(killer: Player, exp: Long) extends Action + final case class ShareAntExperienceWithSquad(owner: UniquePlayer, exp: Long, vehicle: Vehicle) extends Action final case class TeardownConnection() extends Action // final case class PlayerStateShift(killer : PlanetSideGUID, victim : PlanetSideGUID) extends Action diff --git a/src/main/scala/net/psforever/services/avatar/AvatarServiceResponse.scala b/src/main/scala/net/psforever/services/avatar/AvatarServiceResponse.scala index cce788e73..370ec9fc7 100644 --- a/src/main/scala/net/psforever/services/avatar/AvatarServiceResponse.scala +++ b/src/main/scala/net/psforever/services/avatar/AvatarServiceResponse.scala @@ -1,13 +1,13 @@ // Copyright (c) 2017 PSForever package net.psforever.services.avatar -import net.psforever.objects.Player +import net.psforever.objects.{Player, Vehicle} import net.psforever.objects.avatar.scoring.KDAStat import net.psforever.objects.ballistics.Projectile import net.psforever.objects.equipment.Equipment import net.psforever.objects.inventory.InventoryItem import net.psforever.objects.serverobject.environment.interaction.common.Watery.OxygenStateTarget -import net.psforever.objects.sourcing.SourceEntry +import net.psforever.objects.sourcing.{SourceEntry, UniquePlayer} import net.psforever.objects.vital.interaction.DamageResult import net.psforever.packet.PlanetSideGamePacket import net.psforever.packet.game.objectcreate.ConstructorData @@ -134,4 +134,5 @@ object AvatarResponse { final case class AwardCep(charId: Long, bep: Long) extends Response final case class FacilityCaptureRewards(building_id: Int, zone_number: Int, exp: Long) extends Response final case class ShareKillExperienceWithSquad(killer: Player, exp: Long) extends Response + final case class ShareAntExperienceWithSquad(owner: UniquePlayer, exp: Long, vehicle: Vehicle) extends Response } From bf4a9114184d5e94de64bb52f5afe83bf39683f6 Mon Sep 17 00:00:00 2001 From: ScrawnyRonnie Date: Mon, 1 Sep 2025 11:39:44 -0400 Subject: [PATCH 30/33] check once to not join chat channel again --- .../psforever/actors/session/support/ZoningOperations.scala | 4 +++- src/main/scala/net/psforever/services/chat/ChatService.scala | 4 ++-- 2 files changed, 5 insertions(+), 3 deletions(-) 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 95502b190..f30759472 100644 --- a/src/main/scala/net/psforever/actors/session/support/ZoningOperations.scala +++ b/src/main/scala/net/psforever/actors/session/support/ZoningOperations.scala @@ -2532,7 +2532,9 @@ class ZoningOperations( sessionLogic.general.toggleTeleportSystem(obj, TelepadLike.AppraiseTeleportationSystem(obj, continent)) } } - SessionOutfitHandlers.HandleLoginOutfitCheck(player, sessionLogic) + if (player.outfit_id == 0) { + SessionOutfitHandlers.HandleLoginOutfitCheck(player, sessionLogic) + } //make weather happen sendResponse(WeatherMessage(List(),List( StormInfo(Vector3(0.1f, 0.15f, 0.0f), 240, 217), diff --git a/src/main/scala/net/psforever/services/chat/ChatService.scala b/src/main/scala/net/psforever/services/chat/ChatService.scala index 53c105f95..68fc0edbb 100644 --- a/src/main/scala/net/psforever/services/chat/ChatService.scala +++ b/src/main/scala/net/psforever/services/chat/ChatService.scala @@ -58,8 +58,8 @@ class ChatService(context: ActorContext[ChatService.Command]) extends AbstractBe 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 (DefaultChannel, messageType) if messageType != CMT_SQUAD && messageType != CMT_OUTFIT => () + case (SpectatorChannel, messageType) if messageType != CMT_SQUAD && messageType != CMT_OUTFIT => () case _ => log.error(s"invalid chat channel $channel for messageType ${message.messageType}") return this From 5a5ed7425ca561ee400a369f641bc7d093d3d415 Mon Sep 17 00:00:00 2001 From: ScrawnyRonnie Date: Mon, 1 Sep 2025 16:24:56 -0400 Subject: [PATCH 31/33] correct points reward --- .../net/psforever/actors/session/AvatarActor.scala | 12 ++++++------ 1 file changed, 6 insertions(+), 6 deletions(-) diff --git a/src/main/scala/net/psforever/actors/session/AvatarActor.scala b/src/main/scala/net/psforever/actors/session/AvatarActor.scala index a39541c98..68ffb98d3 100644 --- a/src/main/scala/net/psforever/actors/session/AvatarActor.scala +++ b/src/main/scala/net/psforever/actors/session/AvatarActor.scala @@ -1755,6 +1755,12 @@ class AvatarActor( case AwardCep(cep) => if (experienceDebt == 0L) { setCep(avatar.cep + cep) + if (session.get.player.outfit_id != 0) { + setOutfitPoints(avatar.id.toLong, cep * 2).onComplete { + case Success(_) => + case Failure(exception) => log.error(exception)("db failure") + } + } } else if (cep > 0) { sessionActor ! SessionActor.SendResponse(ExperienceAddedMessage()) } @@ -3035,12 +3041,6 @@ 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") } From 0ce5e1ab0c445c5c94c01302d0150adb00961755 Mon Sep 17 00:00:00 2001 From: ScrawnyRonnie Date: Mon, 1 Sep 2025 21:21:22 -0400 Subject: [PATCH 32/33] ensure different factions and reward changes --- .../MajorFacilityHackParticipation.scala | 35 +++++++++++++------ 1 file changed, 24 insertions(+), 11 deletions(-) diff --git a/src/main/scala/net/psforever/objects/serverobject/structures/participation/MajorFacilityHackParticipation.scala b/src/main/scala/net/psforever/objects/serverobject/structures/participation/MajorFacilityHackParticipation.scala index fdd519ec5..fcd7e619c 100644 --- a/src/main/scala/net/psforever/objects/serverobject/structures/participation/MajorFacilityHackParticipation.scala +++ b/src/main/scala/net/psforever/objects/serverobject/structures/participation/MajorFacilityHackParticipation.scala @@ -121,12 +121,25 @@ final case class MajorFacilityHackParticipation(building: Building) extends Faci } else { (defenderFaction, attackingFaction, socketOpt.nonEmpty, None) } - val (contributionVictor, contributionOpposing, _) = { - val (a, b1) = playerContribution.partition { case (_, (p, _, _)) => p.Faction == victorFaction } - val (b, c) = b1.partition { case (_, (p, _, _)) => p.Faction == opposingFaction } - (a.values, b.values, c.values) + val (contributionVictor, contributionOpposing) = { + val (a, b) = playerContribution.partition { case (_, (p, _, _)) => p.Faction == victorFaction } + //val (b, c) = b1.partition { case (_, (p, _, _)) => p.Faction == opposingFaction } + (a.values, b.values) } val contributionVictorSize = contributionVictor.size + + //sometimes attackingFaction and defenderFaction are the same *shrug* + val failSafeOpposingFaction = + contributionOpposing + .map { case (p, _, _) => p.Faction } + .groupBy(identity) + .view.mapValues(_.size) + .toSeq + .sortBy(-_._2) + .headOption + .map(_._1) + .getOrElse(PlanetSideEmpire.NEUTRAL) + if (contributionVictorSize > 0) { //setup for ... val populationIndices = playerPopulationOverTime.indices @@ -137,7 +150,7 @@ final case class MajorFacilityHackParticipation(building: Building) extends Faci val individualPopulationByLayer = allFactions.map { f => (f, populationIndices.indices.map { i => playerPopulationOverTime(i)(f) }) }.toMap[PlanetSideEmpire.Value, Seq[Int]] - (individualPopulationByLayer(victorFaction), individualPopulationByLayer(opposingFaction)) + (individualPopulationByLayer(victorFaction), individualPopulationByLayer(failSafeOpposingFaction)) } val contributionOpposingSize = contributionOpposing.size val killsByPlayersNotInTower = eliminateClosestTowerFromParticipating( @@ -147,7 +160,7 @@ final case class MajorFacilityHackParticipation(building: Building) extends Faci building.Definition.SOIRadius.toFloat, hackStart, completionTime, - opposingFaction, + failSafeOpposingFaction, contributionVictor ) ) @@ -215,7 +228,7 @@ final case class MajorFacilityHackParticipation(building: Building) extends Faci } val heatMapModifier = FacilityHackParticipation.heatMapComparison( FacilityHackParticipation.diffHeatForFactionMap(regionHeatMapProgression, victorFaction).values, - FacilityHackParticipation.diffHeatForFactionMap(regionHeatMapProgression, opposingFaction).values + FacilityHackParticipation.diffHeatForFactionMap(regionHeatMapProgression, failSafeOpposingFaction).values ) heatMapModifier * populationBalanceModifier } @@ -275,13 +288,13 @@ final case class MajorFacilityHackParticipation(building: Building) extends Faci Config.app.game.experience.cep.rate + competitionBonus ).toLong //8. reward participants that are still in the zone - val hackerId = hacker.CharId + //val hackerId = hacker.CharId val contributingPlayers = contributionVictor .filter { case (player, _, _) => player.Zone.id == building.Zone.id } .map { case (player, _, _) => player } .toList //terminal hacker (always cep) - if (contributingPlayers.exists(_.CharId == hackerId) && flagCarrier.map(_.CharId).getOrElse(0L) != hackerId) { + /*if (contributingPlayers.exists(_.CharId == hackerId) && flagCarrier.map(_.CharId).getOrElse(0L) != hackerId) { ToDatabase.reportFacilityCapture( hackerId, zoneNumber, @@ -290,10 +303,10 @@ final case class MajorFacilityHackParticipation(building: Building) extends Faci expType = "cep" ) events ! AvatarServiceMessage(hacker.Name, AvatarAction.AwardCep(hackerId, finalCep)) - } + }*/ //bystanders (cep if squad leader, bep otherwise) contributingPlayers - .filterNot { _.CharId == hackerId } + //.filterNot { _.CharId == hackerId } .foreach { player => val charId = player.CharId val contributionMultiplier = contributionPerPlayerByTime.getOrElse(charId, 1f) From 036d226de14293afc2b4e431ca544ced51cecafa Mon Sep 17 00:00:00 2001 From: ScrawnyRonnie Date: Tue, 2 Sep 2025 09:19:52 -0400 Subject: [PATCH 33/33] kicked player leaves outfit chat --- .../actors/session/normal/AvatarHandlerLogic.scala | 3 +++ .../actors/session/support/SessionAvatarHandlers.scala | 5 +++++ .../actors/session/support/SessionOutfitHandlers.scala | 3 +++ .../net/psforever/services/avatar/AvatarService.scala | 9 +++++++++ .../psforever/services/avatar/AvatarServiceMessage.scala | 1 + .../services/avatar/AvatarServiceResponse.scala | 1 + 6 files changed, 22 insertions(+) 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 03d6fe633..fdf8ed17e 100644 --- a/src/main/scala/net/psforever/actors/session/normal/AvatarHandlerLogic.scala +++ b/src/main/scala/net/psforever/actors/session/normal/AvatarHandlerLogic.scala @@ -482,6 +482,9 @@ class AvatarHandlerLogic(val ops: SessionAvatarHandlers, implicit val context: A case AvatarResponse.ShareAntExperienceWithSquad(owner, exp, vehicle) => ops.shareAntExperienceWithSquad(owner, exp, vehicle) + case AvatarResponse.RemoveFromOutfitChat(outfit_id) => + ops.removeFromOutfitChat(outfit_id) + case AvatarResponse.SendResponse(msg) => sendResponse(msg) diff --git a/src/main/scala/net/psforever/actors/session/support/SessionAvatarHandlers.scala b/src/main/scala/net/psforever/actors/session/support/SessionAvatarHandlers.scala index da4cc1c96..9c54321db 100644 --- a/src/main/scala/net/psforever/actors/session/support/SessionAvatarHandlers.scala +++ b/src/main/scala/net/psforever/actors/session/support/SessionAvatarHandlers.scala @@ -9,6 +9,7 @@ import net.psforever.packet.game.objectcreate.ConstructorData import net.psforever.objects.zones.exp import net.psforever.services.Service import net.psforever.services.avatar.{AvatarAction, AvatarServiceMessage, AvatarServiceResponse} +import net.psforever.services.chat.OutfitChannel import scala.collection.mutable // @@ -237,6 +238,10 @@ class SessionAvatarHandlers( } player.VehicleSeated = None } + + def removeFromOutfitChat(outfit_id: Long): Unit = { + sessionLogic.chat.LeaveChannel(OutfitChannel(outfit_id)) + } } object SessionAvatarHandlers { 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 599adf708..a3bbbb0aa 100644 --- a/src/main/scala/net/psforever/actors/session/support/SessionOutfitHandlers.scala +++ b/src/main/scala/net/psforever/actors/session/support/SessionOutfitHandlers.scala @@ -260,6 +260,9 @@ object SessionOutfitHandlers { kicked.Zone.AvatarEvents ! AvatarServiceMessage(kicked.Zone.id, AvatarAction.PlanetsideStringAttribute(kicked.GUID, 0, "")) + kicked.Zone.AvatarEvents ! AvatarServiceMessage( + kicked.Name, AvatarAction.RemoveFromOutfitChat(kickedBy.outfit_id)) + kicked.outfit_id = 0 kicked.outfit_name = "" PlayerControl.sendResponse(kicked.Zone, kicked.Name, diff --git a/src/main/scala/net/psforever/services/avatar/AvatarService.scala b/src/main/scala/net/psforever/services/avatar/AvatarService.scala index 6b5caaf0b..bbdf1f99a 100644 --- a/src/main/scala/net/psforever/services/avatar/AvatarService.scala +++ b/src/main/scala/net/psforever/services/avatar/AvatarService.scala @@ -484,6 +484,15 @@ class AvatarService(zone: Zone) extends Actor { ) ) + case AvatarAction.RemoveFromOutfitChat(outfit_id) => + AvatarEvents.publish( + AvatarServiceResponse( + s"/$forChannel/Avatar", + Service.defaultPlayerGUID, + AvatarResponse.RemoveFromOutfitChat(outfit_id) + ) + ) + case _ => () } diff --git a/src/main/scala/net/psforever/services/avatar/AvatarServiceMessage.scala b/src/main/scala/net/psforever/services/avatar/AvatarServiceMessage.scala index 3ab388dee..e17cfe0aa 100644 --- a/src/main/scala/net/psforever/services/avatar/AvatarServiceMessage.scala +++ b/src/main/scala/net/psforever/services/avatar/AvatarServiceMessage.scala @@ -164,6 +164,7 @@ object AvatarAction { final case class FacilityCaptureRewards(building_id: Int, zone_number: Int, exp: Long) extends Action final case class ShareKillExperienceWithSquad(killer: Player, exp: Long) extends Action final case class ShareAntExperienceWithSquad(owner: UniquePlayer, exp: Long, vehicle: Vehicle) extends Action + final case class RemoveFromOutfitChat(outfit_id: Long) extends Action final case class TeardownConnection() extends Action // final case class PlayerStateShift(killer : PlanetSideGUID, victim : PlanetSideGUID) extends Action diff --git a/src/main/scala/net/psforever/services/avatar/AvatarServiceResponse.scala b/src/main/scala/net/psforever/services/avatar/AvatarServiceResponse.scala index 370ec9fc7..87560d293 100644 --- a/src/main/scala/net/psforever/services/avatar/AvatarServiceResponse.scala +++ b/src/main/scala/net/psforever/services/avatar/AvatarServiceResponse.scala @@ -135,4 +135,5 @@ object AvatarResponse { final case class FacilityCaptureRewards(building_id: Int, zone_number: Int, exp: Long) extends Response final case class ShareKillExperienceWithSquad(killer: Player, exp: Long) extends Response final case class ShareAntExperienceWithSquad(owner: UniquePlayer, exp: Long, vehicle: Vehicle) extends Response + final case class RemoveFromOutfitChat(outfit_id: Long) extends Response }