clean up, fetch up and even more packets

This commit is contained in:
Resaec 2025-08-10 22:57:01 +02:00
parent d76b41365c
commit f977ea8e6f
15 changed files with 1269 additions and 128 deletions

View file

@ -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)

View file

@ -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
}
)
}

View file

@ -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]
}

View file

@ -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
}
)
}

View file

@ -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
}
)
}

View file

@ -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] = (

View file

@ -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
}
)
}

View file

@ -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
}
)
}

View file

@ -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(

View file

@ -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
}
}

View file

@ -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
}
}

View file

@ -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
}
}

View file

@ -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._

View file

@ -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

View file

@ -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
}