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, 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
|
||||
}
|
||||
)
|
||||
}
|
||||
|
|
|
|||
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