OutfitMembershipResponse rework, tests added

This commit is contained in:
Resaec 2025-08-21 23:04:29 +02:00
parent 17682c08d6
commit f3eed484af
2 changed files with 178 additions and 206 deletions

View file

@ -3,18 +3,20 @@ package net.psforever.packet.game
import net.psforever.packet.GamePacketOpcode.Type
import net.psforever.packet.{GamePacketOpcode, Marshallable, PacketHelpers, PlanetSideGamePacket}
import scodec.{Attempt, Codec, Err}
import scodec.{Attempt, Codec}
import scodec.bits.BitVector
import scodec.codecs._
import shapeless.{::, HNil}
final case class OutfitMembershipResponse(
response_type: OutfitMembershipResponse.ResponseType.Type,
packet_type: OutfitMembershipResponse.PacketType.Type,
unk0: Int,
unk1: Int,
outfit_id: Long,
target_id: Long,
action: OutfitMembershipResponseAction
str1: String,
str2: String,
flag: Boolean
) extends PlanetSideGamePacket {
type Packet = OutfitMembershipResponse
@ -23,226 +25,40 @@ final case class OutfitMembershipResponse(
def encode: Attempt[BitVector] = OutfitMembershipResponse.encode(this)
}
abstract class OutfitMembershipResponseAction(val code: Int)
object OutfitMembershipResponseAction {
final case class Universal(
str1: String,
str2: String,
flag: Boolean
) extends OutfitMembershipResponseAction(-1)
final case class CreateResponse(
str1: String,
str2: String,
str3: String
) extends OutfitMembershipResponseAction(code = 0)
final case class Unk1OutfitResponse(
player_name: String,
outfit_name: String,
unk7: Int
) extends OutfitMembershipResponseAction(code = 1)
final case class Unk2OutfitResponse(
player_name: String,
outfit_name: String,
unk7: Int
) extends OutfitMembershipResponseAction(code = 2) // unk7 = rank?
final case class Unk3OutfitResponse(
unk2: String
) extends OutfitMembershipResponseAction(code = 3)
final case class Unk4OutfitResponse(
unk5: Int,
unk6: Int,
outfit_name: String
) extends OutfitMembershipResponseAction(code = 4)
final case class Unknown(badCode: Int, data: BitVector) extends OutfitMembershipResponseAction(badCode)
/**
* The `Codec`s used to transform the input stream into the context of a specific action
* and extract the field data from that stream.
*/
object Codecs {
private val everFailCondition = conditional(included = false, bool)
val UniversalResponseCodec: Codec[OutfitMembershipResponseAction] = (
PacketHelpers.encodedWideStringAligned(5) ::
PacketHelpers.encodedWideString ::
("flag" | bool)
).xmap[OutfitMembershipResponseAction](
{
case str1 :: str2 :: flag :: HNil =>
Universal(str1, str2, flag)
},
{
case Universal(str1, str2, flag) =>
str1 :: str2 :: flag :: HNil
}
)
val CreateOutfitCodec: Codec[CreateResponse] = (
PacketHelpers.encodedWideStringAligned(5) ::
PacketHelpers.encodedWideString ::
PacketHelpers.encodedWideString
).xmap[CreateResponse](
{
case str1 :: str2 :: str3 :: HNil =>
CreateResponse(str1, str2, str3)
},
{
case CreateResponse(str1, str2, str3) =>
str1 :: str2 :: str3 :: HNil
}
)
val Unk1OutfitCodec: Codec[Unk1OutfitResponse] = (
PacketHelpers.encodedWideStringAligned(5) ::
PacketHelpers.encodedWideString ::
uint8L
).xmap[Unk1OutfitResponse](
{
case player_name :: outfit_name :: u7 :: HNil =>
Unk1OutfitResponse(player_name, outfit_name, u7)
},
{
case Unk1OutfitResponse(player_name, outfit_name, u7) =>
player_name :: outfit_name :: u7 :: HNil
}
)
val Unk2OutfitCodec: Codec[Unk2OutfitResponse] = (
PacketHelpers.encodedWideStringAligned(5) ::
PacketHelpers.encodedWideString ::
uint8L
).xmap[Unk2OutfitResponse](
{
case player_name :: outfit_name :: u7 :: HNil =>
Unk2OutfitResponse(player_name, outfit_name, u7)
},
{
case Unk2OutfitResponse(player_name, outfit_name, u7) =>
player_name :: outfit_name :: u7 :: HNil
}
)
val Unk3OutfitCodec: Codec[Unk3OutfitResponse] =
PacketHelpers.encodedWideStringAligned(5).xmap[Unk3OutfitResponse](
{
case unk2 =>
Unk3OutfitResponse(unk2)
},
{
case Unk3OutfitResponse(unk2) =>
unk2
}
)
val Unk4OutfitCodec: Codec[Unk4OutfitResponse] = (
uint16L ::
uint16L ::
PacketHelpers.encodedWideStringAligned(5)
).xmap[Unk4OutfitResponse](
{
case unk5 :: unk6 :: outfit_name :: HNil =>
Unk4OutfitResponse(unk5, unk6, outfit_name)
},
{
case Unk4OutfitResponse(unk5, unk6, outfit_name) =>
unk5 :: unk6 :: outfit_name :: HNil
}
)
/**
* A common form for known action code indexes with an unknown purpose and transformation is an "Unknown" object.
* @param action the action behavior code
* @return a transformation between the action code and the unknown bit data
*/
def unknownCodec(action: Int): Codec[Unknown] =
bits.xmap[Unknown](
data => Unknown(action, data),
{
case Unknown(_, data) => data
}
)
/**
* The action code was completely unanticipated!
* @param action the action behavior code
* @return nothing; always fail
*/
def failureCodec(action: Int): Codec[OutfitMembershipResponseAction] =
everFailCondition.exmap[OutfitMembershipResponseAction](
_ => Attempt.failure(Err(s"can not match a codec pattern for decoding $action")),
_ => Attempt.failure(Err(s"can not match a codec pattern for encoding $action"))
)
}
}
object OutfitMembershipResponse extends Marshallable[OutfitMembershipResponse] {
object ResponseType extends Enumeration {
object PacketType extends Enumeration {
type Type = Value
val CreateResponse: ResponseType.Value = Value(0)
val Unk1: ResponseType.Value = Value(1) // Info: Player has been invited / response to OutfitMembershipRequest Unk2 for that player
val Unk2: ResponseType.Value = Value(2) // Invited / Accepted / Added
val Unk3: ResponseType.Value = Value(3)
val Unk4: ResponseType.Value = Value(4)
val Unk5: ResponseType.Value = Value(5)
val Unk6: ResponseType.Value = Value(6) // 6 and 7 seen as failed decodes, validity unknown
val Unk7: ResponseType.Value = Value(7)
val CreateResponse: PacketType.Value = Value(0)
val Unk1: PacketType.Value = Value(1) // Info: Player has been invited / response to OutfitMembershipRequest Unk2 for that player
val Unk2: PacketType.Value = Value(2) // Invited / Accepted / Added
val Unk3: PacketType.Value = Value(3)
val Unk4: PacketType.Value = Value(4)
val Unk5: PacketType.Value = Value(5)
val Unk6: PacketType.Value = Value(6) // 6 and 7 seen as failed decodes, validity unknown
val Unk7: PacketType.Value = Value(7)
implicit val codec: Codec[Type] = PacketHelpers.createEnumerationCodec(this, uintL(3))
}
private def selectFromType(code: Int): Codec[OutfitMembershipResponseAction] = {
import OutfitMembershipResponseAction.Codecs._
import scala.annotation.switch
((code: @switch) match {
case 0 => UniversalResponseCodec
case 1 => UniversalResponseCodec
case 2 => UniversalResponseCodec
case 3 => UniversalResponseCodec
case 4 => UniversalResponseCodec
case 5 => UniversalResponseCodec
case 6 => UniversalResponseCodec
case 7 => UniversalResponseCodec
// case 0 => CreateOutfitCodec // seem as OMReq Create response
// case 1 => Unk1OutfitCodec
// case 2 => Unk2OutfitCodec
// case 3 => Unk3OutfitCodec
// case 4 => Unk4OutfitCodec
// case 5 => unknownCodec(action = code)
// case 6 => unknownCodec(action = code)
// case 7 => unknownCodec(action = code)
// 3 bit limit
case _ => failureCodec(code)
}).asInstanceOf[Codec[OutfitMembershipResponseAction]]
}
implicit val codec: Codec[OutfitMembershipResponse] = (
("response_type" | ResponseType.codec) >>:~ { response_type =>
("response_type" | PacketType.codec) ::
("unk0" | uintL(5)) ::
("unk1" | uintL(3)) ::
("outfit_id" | uint32L) ::
("target_id" | uint32L) ::
("action" | selectFromType(response_type.id))
}
("str1" | PacketHelpers.encodedWideStringAligned(5)) ::
("str2" | PacketHelpers.encodedWideString) ::
("flag" | bool)
).xmap[OutfitMembershipResponse](
{
case response_type :: u0 :: u1 :: outfit_id :: target_id :: action :: HNil =>
OutfitMembershipResponse(response_type, u0, u1, outfit_id, target_id, action)
case response_type :: u0 :: u1 :: outfit_id :: target_id :: str1 :: str2 :: flag :: HNil =>
OutfitMembershipResponse(response_type, u0, u1, outfit_id, target_id, str1, str2, flag)
},
{
case OutfitMembershipResponse(response_type, u0, u1, outfit_id, target_id, action) =>
response_type :: u0 :: u1 :: outfit_id :: target_id :: action :: HNil
case OutfitMembershipResponse(response_type, u0, u1, outfit_id, target_id, str1, str2, flag) =>
response_type :: u0 :: u1 :: outfit_id :: target_id :: str1 :: str2 :: flag :: HNil
}
)
}

View file

@ -0,0 +1,156 @@
// Copyright (c) 2023-2025 PSForever
package game
import net.psforever.packet._
import net.psforever.packet.game.OutfitMembershipResponse.PacketType
import net.psforever.packet.game._
import org.specs2.mutable._
import scodec.bits._
class OutfitMembershipResponseTest extends Specification {
val createResponse = hex"8d 0 002b54f404000000010008080"
val unk1 = hex"8d 2 01bb399e03ddb4f4050a078004e00690063006b009550006c0061006e006500740053006900640065005f0046006f00720065007600650072005f005400520000"
val unk2 = hex"8d 4 0049b0f4042b54f4051405a006500720067006c0069006e006700390032009750006c0061006e006500740053006900640065005f0046006f00720065007600650072005f00560061006e00750000"
val unk3 = hex"8d 6 00e8c2f40510d3b6030008080"
val unk4 = hex"8d 8 002b54f404000000010008080"
val unk5 = hex"8d a 022b54f4051fb0f4051c05000530046006f0075007400660069007400740065007300740031008080"
"decode CreateResponse" in {
PacketCoding.decodePacket(createResponse).require match {
case OutfitMembershipResponse(packet_type, unk0, unk1, outfit_id, target_id, str1, str2, flag) =>
packet_type mustEqual PacketType.CreateResponse
unk0 mustEqual 0
unk1 mustEqual 0
outfit_id mustEqual 41593365
target_id mustEqual 0
str1 mustEqual ""
str2 mustEqual ""
flag mustEqual true
case _ =>
ko
}
}
"encode CreateResponse" in {
val msg = OutfitMembershipResponse(PacketType.CreateResponse, 0, 0, 41593365, 0, "", "", flag = true)
val pkt = PacketCoding.encodePacket(msg).require.toByteVector
pkt mustEqual createResponse
}
"decode unk1" in {
PacketCoding.decodePacket(unk1).require match {
case OutfitMembershipResponse(packet_type, unk0, unk1, outfit_id, target_id, str1, str2, flag) =>
packet_type mustEqual PacketType.Unk1
unk0 mustEqual 0
unk1 mustEqual 0
outfit_id mustEqual 30383325
target_id mustEqual 41605870
str1 mustEqual "xNick"
str2 mustEqual "PlanetSide_Forever_TR"
flag mustEqual false
case _ =>
ko
}
}
"encode unk1" in {
val msg = OutfitMembershipResponse(PacketType.Unk1, 0, 0, 30383325, 41605870, "xNick", "PlanetSide_Forever_TR", flag = false)
val pkt = PacketCoding.encodePacket(msg).require.toByteVector
pkt mustEqual unk1
}
"decode unk2" in {
PacketCoding.decodePacket(unk2).require match {
case OutfitMembershipResponse(packet_type, unk0, unk1, outfit_id, target_id, str1, str2, flag) =>
packet_type mustEqual PacketType.Unk2
unk0 mustEqual 0
unk1 mustEqual 0
outfit_id mustEqual 41605156
target_id mustEqual 41593365
str1 mustEqual "Zergling92"
str2 mustEqual "PlanetSide_Forever_Vanu"
flag mustEqual false
case _ =>
ko
}
}
"encode unk2" in {
val msg = OutfitMembershipResponse(PacketType.Unk2, 0, 0, 41605156, 41593365, "Zergling92", "PlanetSide_Forever_Vanu", flag = false)
val pkt = PacketCoding.encodePacket(msg).require.toByteVector
pkt mustEqual unk2
}
"decode unk3" in {
PacketCoding.decodePacket(unk3).require match {
case OutfitMembershipResponse(packet_type, unk0, unk1, outfit_id, target_id, str1, str2, flag) =>
packet_type mustEqual PacketType.Unk3
unk0 mustEqual 0
unk1 mustEqual 0
outfit_id mustEqual 41574772
target_id mustEqual 31156616
str1 mustEqual ""
str2 mustEqual ""
flag mustEqual true
case _ =>
ko
}
}
"encode unk3" in {
val msg = OutfitMembershipResponse(PacketType.Unk3, 0, 0, 41574772, 31156616, "", "", flag = true)
val pkt = PacketCoding.encodePacket(msg).require.toByteVector
pkt mustEqual unk3
}
"decode unk4" in {
PacketCoding.decodePacket(unk4).require match {
case OutfitMembershipResponse(packet_type, unk0, unk1, outfit_id, target_id, str1, str2, flag) =>
packet_type mustEqual PacketType.Unk4
unk0 mustEqual 0
unk1 mustEqual 0
outfit_id mustEqual 41593365
target_id mustEqual 0
str1 mustEqual ""
str2 mustEqual ""
flag mustEqual true
case _ =>
ko
}
}
"encode unk4" in {
val msg = OutfitMembershipResponse(PacketType.Unk4, 0, 0, 41593365, 0, "", "", flag = true)
val pkt = PacketCoding.encodePacket(msg).require.toByteVector
pkt mustEqual unk4
}
"decode unk5" in {
PacketCoding.decodePacket(unk5).require match {
case OutfitMembershipResponse(packet_type, unk0, unk1, outfit_id, target_id, str1, str2, flag) =>
packet_type mustEqual PacketType.Unk5
unk0 mustEqual 0
unk1 mustEqual 1
outfit_id mustEqual 41593365
target_id mustEqual 41605263
str1 mustEqual "PSFoutfittest1"
str2 mustEqual ""
flag mustEqual true
case _ =>
ko
}
}
"encode unk5" in {
val msg = OutfitMembershipResponse(PacketType.Unk5, 0, 1, 41593365, 41605263, "PSFoutfittest1", "", flag = true)
val pkt = PacketCoding.encodePacket(msg).require.toByteVector
pkt mustEqual unk5
}
}