Add OutfitEvent packet

Add OutfitMemberUpdate packet
This commit is contained in:
Resaec 2024-04-22 03:12:55 +02:00
parent 06eea3b3e7
commit 4bc272c2f0
5 changed files with 249 additions and 7 deletions

View file

@ -471,11 +471,11 @@ object GamePacketOpcode extends Enumeration {
case 0x8c => game.OutfitMembershipRequest.decode
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 0x91 => game.OutfitMemberUpdate.decode
case 0x92 => game.PlanetsideStringAttributeMessage.decode
case 0x93 => game.DataChallengeMessage.decode
case 0x94 => game.DataChallengeMessageResp.decode

View file

@ -0,0 +1,177 @@
// Copyright (c) 2024 PSForever
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.bits.BitVector
import scodec.codecs._
import shapeless.{::, HNil}
final case class OutfitEvent(
request_type: OutfitEvent.RequestType.Type,
unk1: Int,
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 CreatedOutfit(
unk2: Int,
unk3: Int,
unk4: Int,
outfit_name: String,
unk6: Long,
unk7: Long,
members: Int,
unk9: Int,
unk10: String,
unk11: String,
unk12: String,
unk13: String,
unk14: String,
unk15: String,
unk16: String,
unk17: String,
unk18: String,
unk19: Int,
unk20: Int,
unk21: Long,
unk22: Long,
unk23: Long,
unk24: Long,
unk25: Int,
) extends OutfitEventAction(code = 4)
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)
val CreatedOutfitCodec: Codec[CreatedOutfit] =
(uint8L ::
uint4L ::
uintL(3) ::
PacketHelpers.encodedWideStringAligned(5) ::
uint32L ::
uint32L ::
uint16L ::
uint16L ::
PacketHelpers.encodedString ::
PacketHelpers.encodedString ::
PacketHelpers.encodedString ::
PacketHelpers.encodedString ::
PacketHelpers.encodedString ::
PacketHelpers.encodedString ::
PacketHelpers.encodedString ::
PacketHelpers.encodedString ::
PacketHelpers.encodedString ::
uint16L ::
uint16L ::
uint32L ::
uint32L ::
uint32L ::
uint32L ::
uint16L
).xmap[CreatedOutfit](
{
case u2 :: u3 :: u4 :: outfit_name :: u6 :: u7 :: members :: u9 :: u10 :: u11 :: u12 :: u13 :: u14 :: u15 :: u16 :: u17 :: u18 :: u19 :: u20 :: u21 :: u22 :: u23 :: u24 :: u25 :: HNil =>
CreatedOutfit(u2, u3, u4, outfit_name, u6, u7, members, u9, u10, u11, u12, u13, u14, u15, u16, u17, u18, u19, u20, u21, u22, u23, u24, u25)
},
{
case CreatedOutfit(u2, u3, u4, outfit_name, u6, u7, members, u9, u10, u11, u12, u13, u14, u15, u16, u17, u18, u19, u20, u21, u22, u23, u24, u25) =>
u2 :: u3 :: u4 :: outfit_name :: u6 :: u7 :: members :: u9 :: u10 :: u11 :: u12 :: u13 :: u14 :: u15 :: u16 :: u17 :: u18 :: u19 :: u20 :: u21 :: u22 :: u23 :: u24 :: u25 :: 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)
val unk1: RequestType.Value = Value(1)
val unk2: RequestType.Value = Value(2)
val unk3: RequestType.Value = Value(3)
val CreatedOutfit: 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, uint4L)
}
private def selectFromType(code: Int): Codec[OutfitEventAction] = {
import OutfitEventAction.Codecs._
import scala.annotation.switch
((code: @switch) match {
case 0 => unknownCodec(action = code)
case 1 => unknownCodec(action = code)
case 2 => unknownCodec(action = code)
case 3 => unknownCodec(action = code)
case 4 => CreatedOutfitCodec // sent after /outfitcreate
case 5 => unknownCodec(action = code)
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 =>
("avatar_guid" | uint16L) ::
("action" | selectFromType(request_type.id))
}
).xmap[OutfitEvent](
{
case request_type :: avatar_guid :: action :: HNil =>
OutfitEvent(request_type, avatar_guid, action)
},
{
case OutfitEvent(request_type, avatar_guid, action) =>
request_type :: avatar_guid :: action :: HNil
}
)
}

View file

@ -0,0 +1,33 @@
// Copyright (c) 2024 PSForever
package net.psforever.packet.game
import net.psforever.packet.{GamePacketOpcode, Marshallable, PacketHelpers, PlanetSideGamePacket}
import net.psforever.types.PlanetSideGUID
import scodec.Codec
import scodec.codecs._
import shapeless.{::, HNil}
final case class OutfitMemberUpdate(outfit_id: PlanetSideGUID, unk1: Int, unk2: Int, 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_id" | PlanetSideGUID.codec) ::
("unk1" | uint16L) ::
("unk2" | uint16L) ::
("unk3" | uint8L)
).xmap[OutfitMemberUpdate](
{
case u0 :: u1 :: u2 :: u3 :: HNil =>
OutfitMemberUpdate(u0, u1, u2, u3)
},
{
case OutfitMemberUpdate(u0, u1, u2, u3) =>
u0 :: u1 :: u2 :: u3 :: HNil
}
)
// ).as[OutfitMemberUpdate]
}

View file

@ -1,4 +1,4 @@
// Copyright (c) 2023 PSForever
// Copyright (c) 2024 PSForever
package net.psforever.packet.game
import net.psforever.packet.GamePacketOpcode.Type
@ -139,13 +139,13 @@ object OutfitMembershipRequest extends Marshallable[OutfitMembershipRequest] {
type Type = Value
val Create: RequestType.Value = Value(0)
val Form: RequestType.Value = Value(1)
val Unk2: RequestType.Value = Value(2)
val Form: RequestType.Value = Value(1)
val Unk2: 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 Unk6: RequestType.Value = Value(6) // 6 and 7 seen as failed decodes, validity unknown
val Unk7: RequestType.Value = Value(7)
implicit val codec: Codec[Type] = PacketHelpers.createEnumerationCodec(this, uintL(3))
}

View file

@ -0,0 +1,32 @@
// Copyright (c) 2023 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 org.specs2.mutable._
import scodec.bits._
class OutfitEventTest extends Specification {
val created_ABC = hex"8f 4 0201 feff 2e 0 50006c0061006e006500740053006900640065005f0046006f00720065007600650072005f00560061006e007500 00000000 00000000 0100 0000 80 80 80 80 80 80 80 80 80 0070 4982 00000000 00000000 00000000 00000000 0000"
"decode CreatedOutfit ABC" in {
PacketCoding.decodePacket(created_ABC).require match {
case OutfitEvent(request_type, unk1, action) =>
request_type mustEqual RequestType.CreatedOutfit
unk1 mustEqual 258
action mustEqual CreatedOutfit(unk2 = 254, unk3 = 15, unk4 = 7, outfit_name = "PlanetSide_Forever_Vanu", unk6 = 0, unk7 = 0, members = 1, unk9 = 0, "", "", "", "", "", "", "", "", "", 28672, 33353, 0, 0, 0, 0, 0)
case _ =>
ko
}
}
"encode CreatedOutfit ABC" in {
val msg = OutfitEvent(RequestType.CreatedOutfit, 258, CreatedOutfit(unk2 = 254, unk3 = 15, unk4 = 7, outfit_name = "PlanetSide_Forever_Vanu", unk6 = 0, unk7 = 0, members = 1, unk9 = 0, "", "", "", "", "", "", "", "", "", 28672, 33353, 0, 0, 0, 0, 0))
val pkt = PacketCoding.encodePacket(msg).require.toByteVector
pkt mustEqual created_ABC
}
}