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.
This commit is contained in:
Resaec 2025-08-23 21:54:07 +02:00
parent 7528388eb1
commit e3fe9b69bf
2 changed files with 94 additions and 97 deletions

View file

@ -8,21 +8,11 @@ import scodec.bits.BitVector
import scodec.codecs._ import scodec.codecs._
import shapeless.{::, HNil} 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( final case class OutfitMemberEvent(
packet_type: Int, // only 0 is known // TODO: needs implementation packet_type: OutfitMemberEvent.PacketType.Type,
outfit_id: Long, outfit_id: Long,
member_id: Long, member_id: Long,
member_name: String, // from here is packet_type == 0 only action: OutfitMemberEventAction
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
) extends PlanetSideGamePacket { ) extends PlanetSideGamePacket {
type Packet = OutfitMemberEvent type Packet = OutfitMemberEvent
@ -34,12 +24,30 @@ final case class OutfitMemberEvent(
abstract class OutfitMemberEventAction(val code: Int) abstract class OutfitMemberEventAction(val code: Int)
object OutfitMemberEventAction { 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( 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) ) extends OutfitMemberEventAction(code = 0)
final case class Padding( final case class Unk1(
padding: Int
) extends OutfitMemberEventAction(code = 1) ) extends OutfitMemberEventAction(code = 1)
final case class Unknown(badCode: Int, data: BitVector) extends OutfitMemberEventAction(badCode) final case class Unknown(badCode: Int, data: BitVector) extends OutfitMemberEventAction(badCode)
@ -51,31 +59,25 @@ object OutfitMemberEventAction {
object Codecs { object Codecs {
private val everFailCondition = conditional(included = false, bool) private val everFailCondition = conditional(included = false, bool)
val UnkNonPaddingCodec: Codec[Unk0] = ( val Unk0Codec: Codec[Unk0] = (
("unk0" | uint32L) ("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]( ).xmap[Unk0](
{ {
case u0 => case member_name :: rank :: points :: last_login :: action :: padding :: HNil =>
Unk0(u0) Unk0(member_name, rank, points, last_login, action, padding)
}, },
{ {
case Unk0(u0) => case Unk0(member_name, rank, points, last_login, action, padding) =>
u0 member_name :: rank :: points :: last_login :: action :: padding :: HNil
} }
) )
val PaddingCodec: Codec[Padding] = ( val Unk1Codec: Codec[Unk1] = PacketHelpers.emptyCodec(Unk1())
("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. * 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 type Type = Value
val Unk0: PacketType.Value = Value(0) 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] = { private def selectFromType(code: Int): Codec[OutfitMemberEventAction] = {
@ -119,34 +121,27 @@ object OutfitMemberEvent extends Marshallable[OutfitMemberEvent] {
import scala.annotation.switch import scala.annotation.switch
((code: @switch) match { ((code: @switch) match {
case 0 => UnkNonPaddingCodec case 0 => Unk0Codec
case 1 => PaddingCodec case 1 => Unk1Codec
case _ => failureCodec(code) case _ => failureCodec(code)
}).asInstanceOf[Codec[OutfitMemberEventAction]] }).asInstanceOf[Codec[OutfitMemberEventAction]]
} }
implicit val codec: Codec[OutfitMemberEvent] = ( implicit val codec: Codec[OutfitMemberEvent] = (
("packet_type" | uintL(2)) :: // this should selectFromType ("packet_type" | PacketType.codec) >>:~ { packet_type =>
("outfit_id" | uint32L) :: ("outfit_id" | uint32L) ::
("member_id" | uint32L) :: ("member_id" | uint32L) ::
("member_name" | PacketHelpers.encodedWideStringAligned(6)) :: // from here is packet_type == 0 only ("action" | selectFromType(packet_type.id)).hlist
("rank" | uint(3)) :: }
("points" | uint32L) ::
("last_login" | uint32L) ::
(("action" | PacketType.codec) >>:~ { action =>
("action_part" | selectFromType(action.id)).hlist
})
).xmap[OutfitMemberEvent]( ).xmap[OutfitMemberEvent](
{ {
case packet_type :: outfit_id :: member_id :: member_name :: rank :: points :: last_login :: action :: unk0_padding :: HNil => case packet_type :: outfit_id :: member_id:: action :: HNil =>
OutfitMemberEvent(packet_type, outfit_id, member_id, member_name, rank, points, last_login, action, unk0_padding) OutfitMemberEvent(packet_type, outfit_id, member_id, action)
}, },
{ {
// TODO: remove once implemented case OutfitMemberEvent(packet_type, outfit_id, member_id, action) =>
// ensure we send packet_type 0 only packet_type :: outfit_id :: member_id :: action :: HNil
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
} }
) )
} }

View file

@ -2,8 +2,8 @@
package game package game
import net.psforever.packet._ import net.psforever.packet._
import net.psforever.packet.game.OutfitMemberEvent import net.psforever.packet.game.{OutfitMemberEvent, OutfitMemberEventAction}
import net.psforever.packet.game.OutfitMemberEventAction.Padding import net.psforever.packet.game.OutfitMemberEventAction._
import org.specs2.mutable._ import org.specs2.mutable._
import scodec.bits._ 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 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 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 { "decode Lazer padding" in {
PacketCoding.decodePacket(Lazer).require match { PacketCoding.decodePacket(Lazer).require match {
case OutfitMemberEvent(packet_type, outfit_id, member_id, member_name, rank, points, last_login, action, unk0_padding) => case OutfitMemberEvent(packet_type, outfit_id, member_id, Unk0(member_name, rank, points, last_login, action, padding)) =>
packet_type mustEqual 0 packet_type mustEqual OutfitMemberEvent.PacketType.Unk0
outfit_id mustEqual 6418 outfit_id mustEqual 6418
member_id mustEqual 705344 member_id mustEqual 705344
member_name mustEqual "Lazer1982" member_name mustEqual "Lazer1982"
rank mustEqual 7 rank mustEqual 7
points mustEqual 3134113 points mustEqual 3134113
last_login mustEqual 156506 last_login mustEqual 156506
action mustEqual OutfitMemberEvent.PacketType.Padding action mustEqual OutfitMemberEventAction.PacketType.Padding
unk0_padding mustEqual Padding(0) padding mustEqual 0
case _ => case _ =>
ko ko
} }
@ -39,15 +39,17 @@ class OutfitMemberEventTest extends Specification {
"encode Lazer padding" in { "encode Lazer padding" in {
val msg = OutfitMemberEvent( val msg = OutfitMemberEvent(
packet_type = 0, packet_type = OutfitMemberEvent.PacketType.Unk0,
outfit_id = 6418, outfit_id = 6418,
member_id = 705344, member_id = 705344,
member_name = "Lazer1982", Unk0(
rank = 7, member_name = "Lazer1982",
points = 3134113, rank = 7,
last_login = 156506, points = 3134113,
action = OutfitMemberEvent.PacketType.Padding, last_login = 156506,
unk0_padding = Padding(0) action = OutfitMemberEventAction.PacketType.Padding,
padding = 0
)
) )
val pkt = PacketCoding.encodePacket(msg).require.toByteVector val pkt = PacketCoding.encodePacket(msg).require.toByteVector
@ -56,16 +58,16 @@ class OutfitMemberEventTest extends Specification {
"decode OpolE padding" in { "decode OpolE padding" in {
PacketCoding.decodePacket(OpolE).require match { PacketCoding.decodePacket(OpolE).require match {
case OutfitMemberEvent(packet_type, outfit_id, member_id, member_name, rank, points, last_login, action, unk0_padding) => case OutfitMemberEvent(packet_type, outfit_id, member_id, Unk0(member_name, rank, points, last_login, action, unk0_padding)) =>
packet_type mustEqual 0 packet_type mustEqual OutfitMemberEvent.PacketType.Unk0
outfit_id mustEqual 6418 outfit_id mustEqual 6418
member_id mustEqual 42644970 member_id mustEqual 42644970
member_name mustEqual "OpolE" member_name mustEqual "OpolE"
rank mustEqual 6 rank mustEqual 6
points mustEqual 461901 points mustEqual 461901
last_login mustEqual 137576 last_login mustEqual 137576
action mustEqual OutfitMemberEvent.PacketType.Padding action mustEqual OutfitMemberEventAction.PacketType.Padding
unk0_padding mustEqual Padding(0) unk0_padding mustEqual 0
case _ => case _ =>
ko ko
} }
@ -73,44 +75,44 @@ class OutfitMemberEventTest extends Specification {
"encode OpolE padding" in { "encode OpolE padding" in {
val msg = OutfitMemberEvent( val msg = OutfitMemberEvent(
packet_type = 0, packet_type = OutfitMemberEvent.PacketType.Unk0,
outfit_id = 6418, outfit_id = 6418,
member_id = 42644970, member_id = 42644970,
member_name = "OpolE", Unk0(
rank = 6, member_name = "OpolE",
points = 461901, rank = 6,
last_login = 137576, points = 461901,
action = OutfitMemberEvent.PacketType.Padding, last_login = 137576,
unk0_padding = Padding(0) action = OutfitMemberEventAction.PacketType.Padding,
padding = 0
)
) )
val pkt = PacketCoding.encodePacket(msg).require.toByteVector val pkt = PacketCoding.encodePacket(msg).require.toByteVector
pkt mustEqual OpolE pkt mustEqual OpolE
} }
// TODO: these are broken because the decoder can only handle packet_type 0 "decode Unk1" in {
PacketCoding.decodePacket(unk1).require match {
/* case OutfitMemberEvent(packet_type,outfit_id, member_id, Unk1()) =>
"decode Unk0" in { packet_type mustEqual OutfitMemberEvent.PacketType.Unk1
PacketCoding.decodePacket(Unk1).require match { outfit_id mustEqual 529744
case OutfitMemberEvent(packet_type, outfit_id, member_id) => member_id mustEqual 41605263
packet_type mustEqual 1
outfit_id mustEqual 6418
member_id mustEqual 42644970
case _ => case _ =>
ko ko
} }
} }
"encode Unk0" in { "encode Unk1" in {
val msg = OutfitMemberEvent( val msg = OutfitMemberEvent(
packet_type = 1, packet_type = OutfitMemberEvent.PacketType.Unk1,
outfit_id = 6418, outfit_id = 529744,
member_id = 42644970, member_id = 41605263,
Unk1()
) )
val pkt = PacketCoding.encodePacket(msg).require.toByteVector val pkt = PacketCoding.encodePacket(msg).require.toByteVector
pkt mustEqual Unk1 pkt mustEqual unk1
} }
*/
} }