OutfitMembershipResponse packet

This commit is contained in:
Resaec 2024-03-21 22:40:28 +01:00
parent 6bb002e2e2
commit 0884047ed0
5 changed files with 390 additions and 21 deletions

View file

@ -469,7 +469,7 @@ 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)

View file

@ -13,7 +13,7 @@ final case class OutfitMembershipRequest(
request_type: OutfitMembershipRequest.RequestType.Type,
avatar_guid: PlanetSideGUID,
unk1: Int,
action: OutfitAction
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"))
)
@ -136,19 +139,19 @@ 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))
}
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] = (
@ -171,7 +174,7 @@ object OutfitMembershipRequest extends Marshallable[OutfitMembershipRequest] {
("unk1" | uint16L) ::
("action" | selectFromType(request_type.id))
}
).xmap[OutfitMembershipRequest](
).xmap[OutfitMembershipRequest](
{
case request_type :: avatar_guid :: u1 :: action :: HNil =>
OutfitMembershipRequest(request_type, avatar_guid, u1, action)

View file

@ -0,0 +1,253 @@
// Copyright (c) 2023 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,
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)
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)
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
}
)
// ).xmap[OutfitMembershipResponse](
// {
// case response_type :: u0 :: avatar_guid :: outfit_guid_1 :: target_guid :: u3 :: u4 :: action :: HNil =>
// OutfitMembershipResponse(response_type, u0, avatar_guid, outfit_guid_1, target_guid, u3, u4, action)
// },
// {
// case OutfitMembershipResponse(response_type, u0, avatar_guid, u1, u2, u3, u4, action) =>
// response_type :: u0 :: avatar_guid :: u1 :: u2 :: u3 :: u4 :: action :: HNil
// }
// )
}

View file

@ -3,7 +3,7 @@ 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.OutfitMembershipRequestAction.{AcceptOutfitInvite, CancelOutfitInvite, CreateOutfit, FormOutfit, RejectOutfitInvite}
import net.psforever.packet.game.OutfitMembershipRequest.RequestType
import net.psforever.types.PlanetSideGUID
import org.specs2.mutable._

View file

@ -0,0 +1,113 @@
// Copyright (c) 2023 PSForever
package game
import net.psforever.packet._
import net.psforever.packet.game.OutfitMembershipResponseAction._
import net.psforever.packet.game.OutfitMembershipResponse.ResponseType
import net.psforever.packet.game._
import net.psforever.types.PlanetSideGUID
import org.specs2.mutable._
import scodec.bits._
class OutfitMembershipResponseTest extends Specification {
/*
Outfit Create request that results in "someResponse" packet below
C >> S
OutfitMembershipRequest(Type = Create (0), AvatarID = ValidPlanetSideGUID(43541), 634, CreateOutfit(, 0, false, PlanetSide_Forever_Vanu))
0x8c 0 2b54 f405 000 97 50006c0061006e006500740053006900640065005f0046006f00720065007600650072005f00560061006e007500))
*/
val createResponse = hex"8d 0 00 2b54 f404 0000 0001 00 0 80 80" // response to create
// validity unknown
//val unk0_0 = hex"8d 0 11 2600 0000 c2b8 1a02 28c0 0000 a037 2340 4598 0000 0010 1284 dd0d 4060 0000 280d 2080 1176 0000 0004 04a1 3021 9018 0000 0"
// ? ? ? xNick PlanetSide_Forever_TR
val new2 = hex"8d 2 01 bb39 9e03 ddb4 f405 0a 0 78004e00690063006b00 95 50006c0061006e006500740053006900640065005f0046006f00720065007600650072005f0054005200 00"
// AvatarID OutfitID-1 TargetID/LeaderID OutfitID Zergling92) PlanetSide_Forever_Vanu
val someOther = hex"8d 4 00 49b0 f404 2b54 f405 14 0 5a006500720067006c0069006e00670039003200 97 50006c0061006e006500740053006900640065005f0046006f00720065007600650072005f00560061006e007500 00"
val someOther2 = hex"8d 4 01 ddb4 f405 bb39 9e03 14 0 48006100480061004100540052004d0061007800 95 50006c0061006e006500740053006900640065005f0046006f00720065007600650072005f0054005200 80"
// HaHaATRMax PlanetSide_Forever_TR
// unk validity
val muh6 = hex"8d 6 64 b351 2cf3 f2ef 8040 80 0 201bb4088d1abe638d8b6b62133d81ffad501e3e1f0000083014d5948e886b3517d6404b0004028020059408681a38a68db8cb7c133f807fba501bff110aec70450569c2a000314e569f1187e9f9c00380083c30aa83879c3b6213ebbeecf8040d5fe0076408d40ccc948b488b35170381001590"
val muh61 = hex"8d 6 00 e8c2 f405 10d3 b603 00 0 80 80"
//
val blubOther = hex"8d 8 00 2b54 f404 0000 0001 0 00 80 80"
// PlayerName (PSFoutfittest1)
val yetAnOther = hex"8d a 02 2b54 f405 1fb0 f405 1 c0 5000530046006f007500740066006900740074006500730074003100 80 80"
// VSsulferix
val blubBlah = hex"8d a 03 8afa f404 2b54 f405 1 40 56005300730075006c0066006500720069007800 80 00"
// validity unknown
val blah = hex"8d e 37 0660 3000 0002 6b38 4 a050 0000 0020 2429 6010 80c0 0000 2c91 a0f8 2400 0000 0808 383cc7a0300000082a68420d00000002023785e2280c000006a115138440000000808388588203000001ca6520661b0000000202439351580c00000299331287c00000008090e5b84e03000000ac650662300000002020a88c2280c000002a1bb1789c00000008082a35c3e03000000a86484e2b00000002020a8f81280c000000"
"decode CreateResponse" in {
PacketCoding.decodePacket(createResponse).require match {
case OutfitMembershipResponse(request_type, u0, avatar_id, outfit_guid_1, target_guid, u3, action) =>
request_type mustEqual ResponseType.CreateResponse
u0 mustEqual 0
avatar_id mustEqual PlanetSideGUID(43541)
outfit_guid_1 mustEqual PlanetSideGUID(634)
target_guid mustEqual PlanetSideGUID(0)
u3 mustEqual 0
action mustEqual CreateOutfitResponse("", "", "")
case _ =>
ko
}
}
"encode CreateResponse" in {
val msg = OutfitMembershipResponse(ResponseType.CreateResponse, 0, PlanetSideGUID(43541), PlanetSideGUID(634), PlanetSideGUID(0), 0, CreateOutfitResponse("", "", ""))
val pkt = PacketCoding.encodePacket(msg).require.toByteVector
pkt mustEqual createResponse
}
"decode Unk1" in {
PacketCoding.decodePacket(new2).require match {
case OutfitMembershipResponse(request_type, u0, avatar_id, outfit_guid_1, target_guid, u3, action) =>
request_type mustEqual ResponseType.Unk1
u0 mustEqual 0
avatar_id mustEqual PlanetSideGUID(40157)
outfit_guid_1 mustEqual PlanetSideGUID(463)
target_guid mustEqual PlanetSideGUID(56046)
u3 mustEqual 634
action mustEqual Unk1OutfitResponse("xNick", "PlanetSide_Forever_TR", 0)
case _ =>
ko
}
}
"encode Unk1" in {
val msg = OutfitMembershipResponse(ResponseType.Unk1, 0, PlanetSideGUID(40157), PlanetSideGUID(463), PlanetSideGUID(56046), 634, Unk1OutfitResponse("xNick", "PlanetSide_Forever_TR", 0))
val pkt = PacketCoding.encodePacket(msg).require.toByteVector
pkt mustEqual new2
}
"decode Unk2" in {
PacketCoding.decodePacket(someOther).require match {
case OutfitMembershipResponse(request_type, u0, avatar_id, outfit_guid_1, target_guid, u3, action) =>
request_type mustEqual ResponseType.Unk2
u0 mustEqual 0
avatar_id mustEqual PlanetSideGUID(55332)
outfit_guid_1 mustEqual PlanetSideGUID(634)
target_guid mustEqual PlanetSideGUID(43541)
u3 mustEqual 634
action mustEqual Unk2OutfitResponse("Zergling92", "PlanetSide_Forever_Vanu", 0)
case _ =>
ko
}
}
"encode Unk2" in {
val msg = OutfitMembershipResponse(ResponseType.Unk2, 0, PlanetSideGUID(55332), PlanetSideGUID(634), PlanetSideGUID(43541), 634, Unk2OutfitResponse("Zergling92", "PlanetSide_Forever_Vanu", 0))
val pkt = PacketCoding.encodePacket(msg).require.toByteVector
pkt mustEqual someOther
}
}