mirror of
https://github.com/2revoemag/PSF-BotServer.git
synced 2026-01-19 18:14:44 +00:00
OutfitMembershipResponse rework, tests added
This commit is contained in:
parent
17682c08d6
commit
f3eed484af
|
|
@ -3,18 +3,20 @@ package net.psforever.packet.game
|
||||||
|
|
||||||
import net.psforever.packet.GamePacketOpcode.Type
|
import net.psforever.packet.GamePacketOpcode.Type
|
||||||
import net.psforever.packet.{GamePacketOpcode, Marshallable, PacketHelpers, PlanetSideGamePacket}
|
import net.psforever.packet.{GamePacketOpcode, Marshallable, PacketHelpers, PlanetSideGamePacket}
|
||||||
import scodec.{Attempt, Codec, Err}
|
import scodec.{Attempt, Codec}
|
||||||
import scodec.bits.BitVector
|
import scodec.bits.BitVector
|
||||||
import scodec.codecs._
|
import scodec.codecs._
|
||||||
import shapeless.{::, HNil}
|
import shapeless.{::, HNil}
|
||||||
|
|
||||||
final case class OutfitMembershipResponse(
|
final case class OutfitMembershipResponse(
|
||||||
response_type: OutfitMembershipResponse.ResponseType.Type,
|
packet_type: OutfitMembershipResponse.PacketType.Type,
|
||||||
unk0: Int,
|
unk0: Int,
|
||||||
unk1: Int,
|
unk1: Int,
|
||||||
outfit_id: Long,
|
outfit_id: Long,
|
||||||
target_id: Long,
|
target_id: Long,
|
||||||
action: OutfitMembershipResponseAction
|
str1: String,
|
||||||
|
str2: String,
|
||||||
|
flag: Boolean
|
||||||
) extends PlanetSideGamePacket {
|
) extends PlanetSideGamePacket {
|
||||||
type Packet = OutfitMembershipResponse
|
type Packet = OutfitMembershipResponse
|
||||||
|
|
||||||
|
|
@ -23,226 +25,40 @@ final case class OutfitMembershipResponse(
|
||||||
def encode: Attempt[BitVector] = OutfitMembershipResponse.encode(this)
|
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 OutfitMembershipResponse extends Marshallable[OutfitMembershipResponse] {
|
||||||
|
|
||||||
object ResponseType extends Enumeration {
|
object PacketType extends Enumeration {
|
||||||
type Type = Value
|
type Type = Value
|
||||||
|
|
||||||
val CreateResponse: ResponseType.Value = Value(0)
|
val CreateResponse: PacketType.Value = Value(0)
|
||||||
val Unk1: ResponseType.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
|
||||||
val Unk2: ResponseType.Value = Value(2) // Invited / Accepted / Added
|
val Unk2: PacketType.Value = Value(2) // Invited / Accepted / Added
|
||||||
val Unk3: ResponseType.Value = Value(3)
|
val Unk3: PacketType.Value = Value(3)
|
||||||
val Unk4: ResponseType.Value = Value(4)
|
val Unk4: PacketType.Value = Value(4)
|
||||||
val Unk5: ResponseType.Value = Value(5)
|
val Unk5: PacketType.Value = Value(5)
|
||||||
val Unk6: ResponseType.Value = Value(6) // 6 and 7 seen as failed decodes, validity unknown
|
val Unk6: PacketType.Value = Value(6) // 6 and 7 seen as failed decodes, validity unknown
|
||||||
val Unk7: ResponseType.Value = Value(7)
|
val Unk7: PacketType.Value = Value(7)
|
||||||
|
|
||||||
implicit val codec: Codec[Type] = PacketHelpers.createEnumerationCodec(this, uintL(3))
|
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] = (
|
implicit val codec: Codec[OutfitMembershipResponse] = (
|
||||||
("response_type" | ResponseType.codec) >>:~ { response_type =>
|
("response_type" | PacketType.codec) ::
|
||||||
("unk0" | uintL(5)) ::
|
("unk0" | uintL(5)) ::
|
||||||
("unk1" | uintL(3)) ::
|
("unk1" | uintL(3)) ::
|
||||||
("outfit_id" | uint32L) ::
|
("outfit_id" | uint32L) ::
|
||||||
("target_id" | uint32L) ::
|
("target_id" | uint32L) ::
|
||||||
("action" | selectFromType(response_type.id))
|
("str1" | PacketHelpers.encodedWideStringAligned(5)) ::
|
||||||
}
|
("str2" | PacketHelpers.encodedWideString) ::
|
||||||
|
("flag" | bool)
|
||||||
).xmap[OutfitMembershipResponse](
|
).xmap[OutfitMembershipResponse](
|
||||||
{
|
{
|
||||||
case response_type :: u0 :: u1 :: outfit_id :: target_id :: action :: HNil =>
|
case response_type :: u0 :: u1 :: outfit_id :: target_id :: str1 :: str2 :: flag :: HNil =>
|
||||||
OutfitMembershipResponse(response_type, u0, u1, outfit_id, target_id, action)
|
OutfitMembershipResponse(response_type, u0, u1, outfit_id, target_id, str1, str2, flag)
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
case OutfitMembershipResponse(response_type, u0, u1, outfit_id, target_id, action) =>
|
case OutfitMembershipResponse(response_type, u0, u1, outfit_id, target_id, str1, str2, flag) =>
|
||||||
response_type :: u0 :: u1 :: outfit_id :: target_id :: action :: HNil
|
response_type :: u0 :: u1 :: outfit_id :: target_id :: str1 :: str2 :: flag :: HNil
|
||||||
}
|
}
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
|
||||||
156
src/test/scala/game/OutfitMembershipResponseTest.scala
Normal file
156
src/test/scala/game/OutfitMembershipResponseTest.scala
Normal 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
|
||||||
|
}
|
||||||
|
}
|
||||||
Loading…
Reference in a new issue