From 48d320c7b1bb7d920295f0e823f16b16c4b75b7d Mon Sep 17 00:00:00 2001 From: Fate-JH Date: Mon, 22 May 2023 16:27:06 -0400 Subject: [PATCH] initial packet and tests for OutfitRequest --- .../actors/session/SessionActor.scala | 2 + .../psforever/packet/GamePacketOpcode.scala | 2 +- .../psforever/packet/game/OutfitRequest.scala | 177 ++++++++++++++++++ src/test/scala/game/OutfitRequesTest.scala | 103 ++++++++++ 4 files changed, 283 insertions(+), 1 deletion(-) create mode 100644 src/main/scala/net/psforever/packet/game/OutfitRequest.scala create mode 100644 src/test/scala/game/OutfitRequesTest.scala diff --git a/src/main/scala/net/psforever/actors/session/SessionActor.scala b/src/main/scala/net/psforever/actors/session/SessionActor.scala index 36d860e1..29d69bc6 100644 --- a/src/main/scala/net/psforever/actors/session/SessionActor.scala +++ b/src/main/scala/net/psforever/actors/session/SessionActor.scala @@ -591,6 +591,8 @@ class SessionActor(middlewareActor: typed.ActorRef[MiddlewareActor.Command], con case packet: HitHint => sessionFuncs.handleHitHint(packet) + case _: OutfitRequest => () + case pkt => log.warning(s"Unhandled GamePacket $pkt") } diff --git a/src/main/scala/net/psforever/packet/GamePacketOpcode.scala b/src/main/scala/net/psforever/packet/GamePacketOpcode.scala index 8ed59f9b..3fcd0449 100644 --- a/src/main/scala/net/psforever/packet/GamePacketOpcode.scala +++ b/src/main/scala/net/psforever/packet/GamePacketOpcode.scala @@ -470,7 +470,7 @@ object GamePacketOpcode extends Enumeration { case 0x8b => noDecoder(UnknownMessage139) case 0x8c => noDecoder(OutfitMembershipRequest) case 0x8d => noDecoder(OutfitMembershipResponse) - case 0x8e => noDecoder(OutfitRequest) + case 0x8e => game.OutfitRequest.decode case 0x8f => noDecoder(OutfitEvent) // OPCODES 0x90-9f diff --git a/src/main/scala/net/psforever/packet/game/OutfitRequest.scala b/src/main/scala/net/psforever/packet/game/OutfitRequest.scala new file mode 100644 index 00000000..05ebf66a --- /dev/null +++ b/src/main/scala/net/psforever/packet/game/OutfitRequest.scala @@ -0,0 +1,177 @@ +// Copyright (c) 2023 PSForever +package net.psforever.packet.game + +import net.psforever.packet.{GamePacketOpcode, Marshallable, PacketHelpers, PlanetSideGamePacket} +import scodec.{Attempt, Codec, Err} +import scodec.bits.ByteVector +import scodec.codecs._ +import shapeless.{::, HNil} + +/** + * na + */ +abstract class OutfitRequestForm(val code: Int) + +object OutfitRequestForm { + /** + * na + * @param str na + */ + final case class Unk0(str: String) extends OutfitRequestForm(code = 0) + /** + * na + * @param list na + */ + final case class Unk1(list: List[Option[String]]) extends OutfitRequestForm(code = 1) + /** + * na + * @param unk na + */ + final case class Unk2(unk: Int) extends OutfitRequestForm(code = 2) + /** + * na + * @param unk na + */ + final case class Unk3(unk: Boolean) extends OutfitRequestForm(code = 3) + /** + * na + * @param unk na + */ + final case class Unk4(unk: Boolean) extends OutfitRequestForm(code = 4) + /** + * na + * @param unk na + */ + final case class Fail(unk: ByteVector) extends OutfitRequestForm(code = -1) +} + +/** + * na + * @param id na + * @param info na + */ +final case class OutfitRequest(id: Long, info: OutfitRequestForm) + extends PlanetSideGamePacket { + type Packet = OrbitalStrikeWaypointMessage + def opcode = GamePacketOpcode.OutfitRequest + def encode = OutfitRequest.encode(this) +} + +object OutfitRequest extends Marshallable[OutfitRequest] { + /** + * na + */ + private val unk0Codec: Codec[OutfitRequestForm] = PacketHelpers.encodedWideStringAligned(adjustment = 5).hlist + .xmap[OutfitRequestForm] ( + { + case value :: HNil => OutfitRequestForm.Unk0(value) + }, + { + case OutfitRequestForm.Unk0(value) => value :: HNil + } + ) + + /** + * na + */ + private val unk1Codec: Codec[OutfitRequestForm] = unk1PaddedEntryCodec(len = 8, pad = 5).xmap[OutfitRequestForm] ( + list => OutfitRequestForm.Unk1(list), + { + case OutfitRequestForm.Unk1(list) => list + } + ) + + /** + * na + */ + private def unk1PaddedEntryCodec(len: Int, pad: Int): Codec[List[Option[String]]] = + { + val nextPad = if (pad == 0) 7 else pad - 1 + optional(bool, PacketHelpers.encodedWideStringAligned(nextPad)) >>:~ { strOpt => + (strOpt match { + case None if len > 1 => unk1PaddedEntryCodec(len - 1, nextPad) + case Some(_) if len > 1 => unk1PaddedEntryCodec(len - 1, pad = 8) + case _ => PacketHelpers.listOfNSized(size = 0L, optional(bool, PacketHelpers.encodedWideString)) + }).hlist + } + }.xmap[List[Option[String]]]( + { + case head :: tail :: HNil => head +: tail + }, + list => list.head :: list.tail :: HNil + ) + + /** + * na + */ + private val unk2Codec: Codec[OutfitRequestForm] = uint8.hlist.xmap[OutfitRequestForm] ( + { + case value :: HNil => OutfitRequestForm.Unk2(value) + }, + { + case OutfitRequestForm.Unk2(value) => value :: HNil + } + ) + + /** + * na + */ + private val unk3Codec: Codec[OutfitRequestForm] = bool.hlist.xmap[OutfitRequestForm] ( + { + case value :: HNil => OutfitRequestForm.Unk3(value) + }, + { + case OutfitRequestForm.Unk3(value) => value :: HNil + } + ) + + /** + * na + */ + private val unk4Codec: Codec[OutfitRequestForm] = bool.hlist.xmap[OutfitRequestForm] ( + { + case value :: HNil => OutfitRequestForm.Unk4(value) + }, + { + case OutfitRequestForm.Unk4(value) => value :: HNil + } + ) + + /** + * na + */ + private def failCodec(code: Int): Codec[OutfitRequestForm] = conditional(included = false, bool).exmap[OutfitRequestForm]( + _ => Attempt.Failure(Err(s"can not decode $code-type info - what is this thing?")), + _ => Attempt.Failure(Err(s"can not encode $code-type info - no such thing")) + ) + + /** + * na + */ + private def infoCodec(code: Int): Codec[OutfitRequestForm] = { + code match { + case 0 => unk0Codec + case 1 => unk1Codec + case 2 => unk2Codec + case 3 => unk3Codec + case 4 => unk4Codec + case _ => failCodec(code) + } + } + + implicit val codec: Codec[OutfitRequest] = ( + uint(bits = 3) >>:~ { code => + ("id" | uint32L) :: + ("info" | infoCodec(code)) + } + ).xmap[OutfitRequest]( + { + case _:: id:: info :: HNil => + OutfitRequest(id, info) + }, + { + case OutfitRequest(id, info) => + info.code :: id :: info :: HNil + } + ) +} diff --git a/src/test/scala/game/OutfitRequesTest.scala b/src/test/scala/game/OutfitRequesTest.scala new file mode 100644 index 00000000..eea16519 --- /dev/null +++ b/src/test/scala/game/OutfitRequesTest.scala @@ -0,0 +1,103 @@ +// Copyright (c) 2023 PSForever +package game + +import org.specs2.mutable._ +import net.psforever.packet._ +import net.psforever.packet.game._ +import net.psforever.types.PlanetSideGUID +import scodec.bits._ + +class OutfitRequestTest extends Specification { + val string0 = hex"8e02b54f40401780560061006e00750020006f0075007400660069007400200066006f0072002000740068006500200070006c0061006e00650074007300690064006500200066006f00720065007600650072002000700072006f006a006500630074002100200020002000200020002000200020002000200020002000200020002000200020002000200020002000200020002000200020002000200020002000200020002000200020002000200020002000200020002000200020002000200020002000200020002000200020002000200020002000200020002000200020002000200020002000200020002000200020002000200020002000200020002000200020002000200020002000200020002d00660069006e00640020006f007500740020006d006f00720065002000610062006f0075007400200074006800650020005000530045004d0055002000700072006f006a0065006300740020006100740020005000530066006f00720065007600650072002e006e0065007400" + val string2 = hex"8e22b54f405800c000c000c000c000c000c000c000" + val string4 = hex"8e42b54f404aa0" //faked by modifying the previous example + val string6 = hex"8e649e822010" + val string8 = hex"8e81b2cf4050" + + "decode 0" in { + PacketCoding.decodePacket(string0).require match { + case OutfitRequest(id, OutfitRequestForm.Unk0(str)) => + id mustEqual 41593365L + str mustEqual "Vanu outfit for the planetside forever project! -find out more about the PSEMU project at PSforever.net" + case _ => + ko + } + } + + "decode 1" in { + PacketCoding.decodePacket(string2).require match { + case OutfitRequest(id, OutfitRequestForm.Unk1(list)) => + id mustEqual 41593365L + list mustEqual List(Some(""), Some(""), Some(""), Some(""), Some(""), Some(""), Some(""), Some("")) + case _ => + ko + } + } + + "decode 2 (fake)" in { + PacketCoding.decodePacket(string4).require match { + case OutfitRequest(id, OutfitRequestForm.Unk2(value)) => + id mustEqual 41593365L + value mustEqual 85 + case _ => + ko + } + } + + "decode 3" in { + PacketCoding.decodePacket(string6).require match { + case OutfitRequest(id, OutfitRequestForm.Unk3(value)) => + id mustEqual 1176612L + value mustEqual true + case _ => + ko + } + } + + "decode 4" in { + PacketCoding.decodePacket(string8).require match { + case OutfitRequest(id, OutfitRequestForm.Unk4(value)) => + id mustEqual 41588237L + value mustEqual true + case _ => + ko + } + } + + "encode 0" in { + val msg = OutfitRequest(41593365L, OutfitRequestForm.Unk0( + "Vanu outfit for the planetside forever project! -find out more about the PSEMU project at PSforever.net" + )) + val pkt = PacketCoding.encodePacket(msg).require.toByteVector + + pkt mustEqual string0 + } + + "encode 1" in { + val msg = OutfitRequest(41593365L, OutfitRequestForm.Unk1(List(Some(""), Some(""), Some(""), Some(""), Some(""), Some(""), Some(""), Some("")))) + val pkt = PacketCoding.encodePacket(msg).require.toByteVector + + pkt mustEqual string2 + } + + "encode 2 (fake)" in { + val msg = OutfitRequest(41593365L, OutfitRequestForm.Unk2(85)) + val pkt = PacketCoding.encodePacket(msg).require.toByteVector + + pkt mustEqual string4 + } + + "encode 3" in { + val msg = OutfitRequest(1176612L, OutfitRequestForm.Unk3(true)) + val pkt = PacketCoding.encodePacket(msg).require.toByteVector + + pkt mustEqual string6 + } + + "encode 4" in { + val msg = OutfitRequest(41588237L, OutfitRequestForm.Unk4(true)) + val pkt = PacketCoding.encodePacket(msg).require.toByteVector + + pkt mustEqual string8 + } +}