From 5564d9839508d0a261f616c6c106ec0d94b14605 Mon Sep 17 00:00:00 2001 From: FateJH Date: Sat, 8 Oct 2016 17:40:55 -0400 Subject: [PATCH 1/8] initial FriendsResponse packet and tests; though the packet works, it displays odd behavior when decoded by client; might be intentional --- .../psforever/packet/GamePacketOpcode.scala | 2 +- .../scala/net/psforever/packet/PSPacket.scala | 13 +++++++++ .../packet/game/FriendsResponse.scala | 29 +++++++++++++++++++ common/src/test/scala/GamePacketTest.scala | 23 +++++++++++++++ 4 files changed, 66 insertions(+), 1 deletion(-) create mode 100644 common/src/main/scala/net/psforever/packet/game/FriendsResponse.scala diff --git a/common/src/main/scala/net/psforever/packet/GamePacketOpcode.scala b/common/src/main/scala/net/psforever/packet/GamePacketOpcode.scala index 6e9984e5..22eb0489 100644 --- a/common/src/main/scala/net/psforever/packet/GamePacketOpcode.scala +++ b/common/src/main/scala/net/psforever/packet/GamePacketOpcode.scala @@ -455,7 +455,7 @@ object GamePacketOpcode extends Enumeration { case 0x70 => noDecoder(SquadMemberEvent) case 0x71 => noDecoder(PlatoonEvent) case 0x72 => noDecoder(FriendsRequest) - case 0x73 => noDecoder(FriendsResponse) + case 0x73 => game.FriendsReponse.decode case 0x74 => noDecoder(TriggerEnvironmentalDamageMessage) case 0x75 => noDecoder(TrainingZoneMessage) case 0x76 => noDecoder(DeployableObjectsInfoMessage) diff --git a/common/src/main/scala/net/psforever/packet/PSPacket.scala b/common/src/main/scala/net/psforever/packet/PSPacket.scala index 2fe9a563..e59cf734 100644 --- a/common/src/main/scala/net/psforever/packet/PSPacket.scala +++ b/common/src/main/scala/net/psforever/packet/PSPacket.scala @@ -203,4 +203,17 @@ object PacketHelpers { def encodedStringWithLimit(limit : Int) : Codec[String] = variableSizeBytes(encodedStringSizeWithLimit(limit), ascii) */ + + /** + * Construct a `Codec` for reading `wchar_t` (wide character) `Strings` whose length field are constrained to specific bit size proportions. + * Padding may also exist between the length field and the beginning of the contents. + * @param lenSize a codec that defines the bit size that encodes the length + * @param adjustment the optional alignment for padding; defaults to 0 + * @return the `String` `Codec` + */ + def specSizeWideStringAligned(lenSize : Codec[Int], adjustment : Int = 0) : Codec[String] = + variableSizeBytes((lenSize <~ ignore(adjustment)).xmap( + insize => insize*2, // number of symbols -> number of bytes (decode) + outSize => outSize/2 // number of bytes -> number of symbols (encode) + ), utf16) } diff --git a/common/src/main/scala/net/psforever/packet/game/FriendsResponse.scala b/common/src/main/scala/net/psforever/packet/game/FriendsResponse.scala new file mode 100644 index 00000000..402f194d --- /dev/null +++ b/common/src/main/scala/net/psforever/packet/game/FriendsResponse.scala @@ -0,0 +1,29 @@ +// Copyright (c) 2016 PSForever.net to present +package net.psforever.packet.game + +import net.psforever.packet.{GamePacketOpcode, Marshallable, PacketHelpers, PlanetSideGamePacket} +import scodec.Codec +import scodec.codecs._ + +/** + * na + * @param player_guid the player + * @param friend the name of the friend + * @param unk na + */ +final case class FriendsResponse(player_guid : PlanetSideGUID, + friend : String, + unk : Int) + extends PlanetSideGamePacket { + type Packet = FriendsResponse + def opcode = GamePacketOpcode.FriendsResponse + def encode = FriendsReponse.encode(this) +} + +object FriendsReponse extends Marshallable[FriendsResponse] { + implicit val codec : Codec[FriendsResponse] = ( + ("player_guid" | PlanetSideGUID.codec) :: + ("friend" | PacketHelpers.specSizeWideStringAligned(uint(5), 3)) :: + ("unk" | uint8L) + ).as[FriendsResponse] +} diff --git a/common/src/test/scala/GamePacketTest.scala b/common/src/test/scala/GamePacketTest.scala index 2f7e8a11..a90801fd 100644 --- a/common/src/test/scala/GamePacketTest.scala +++ b/common/src/test/scala/GamePacketTest.scala @@ -830,6 +830,29 @@ class GamePacketTest extends Specification { } } + "FriendsResponse" should { + val string = hex"73 618C 60 4B007500720074004800650063007400690063002D004700 00" + + "decode" in { + PacketCoding.DecodePacket(string).require match { + case FriendsResponse(player_guid, friend, unk) => + player_guid mustEqual PlanetSideGUID(35937) + friend.length mustEqual 12 + friend mustEqual "KurtHectic-G" + unk mustEqual 0 + case default => + ko + } + } + + "encode" in { + val msg = FriendsResponse(PlanetSideGUID(35937), "KurtHectic-G", 0) + val pkt = PacketCoding.EncodePacket(msg).require.toByteVector + + pkt mustEqual string + } + } + "WeaponDryFireMessage" should { val string = hex"52 4C00" From b444b4b503303f4c2d48e95880d27e73bbfbce92 Mon Sep 17 00:00:00 2001 From: FateJH Date: Sun, 9 Oct 2016 00:22:04 -0400 Subject: [PATCH 2/8] changed last field to boolean --- .../scala/net/psforever/packet/game/FriendsResponse.scala | 4 ++-- common/src/test/scala/GamePacketTest.scala | 4 ++-- 2 files changed, 4 insertions(+), 4 deletions(-) diff --git a/common/src/main/scala/net/psforever/packet/game/FriendsResponse.scala b/common/src/main/scala/net/psforever/packet/game/FriendsResponse.scala index 402f194d..611c2541 100644 --- a/common/src/main/scala/net/psforever/packet/game/FriendsResponse.scala +++ b/common/src/main/scala/net/psforever/packet/game/FriendsResponse.scala @@ -13,7 +13,7 @@ import scodec.codecs._ */ final case class FriendsResponse(player_guid : PlanetSideGUID, friend : String, - unk : Int) + unk : Boolean) extends PlanetSideGamePacket { type Packet = FriendsResponse def opcode = GamePacketOpcode.FriendsResponse @@ -24,6 +24,6 @@ object FriendsReponse extends Marshallable[FriendsResponse] { implicit val codec : Codec[FriendsResponse] = ( ("player_guid" | PlanetSideGUID.codec) :: ("friend" | PacketHelpers.specSizeWideStringAligned(uint(5), 3)) :: - ("unk" | uint8L) + ("unk" | bool) ).as[FriendsResponse] } diff --git a/common/src/test/scala/GamePacketTest.scala b/common/src/test/scala/GamePacketTest.scala index a90801fd..671983b4 100644 --- a/common/src/test/scala/GamePacketTest.scala +++ b/common/src/test/scala/GamePacketTest.scala @@ -839,14 +839,14 @@ class GamePacketTest extends Specification { player_guid mustEqual PlanetSideGUID(35937) friend.length mustEqual 12 friend mustEqual "KurtHectic-G" - unk mustEqual 0 + unk mustEqual false case default => ko } } "encode" in { - val msg = FriendsResponse(PlanetSideGUID(35937), "KurtHectic-G", 0) + val msg = FriendsResponse(PlanetSideGUID(35937), "KurtHectic-G", false) val pkt = PacketCoding.EncodePacket(msg).require.toByteVector pkt mustEqual string From 3e84e999fba78d8d1ed30b08fd3cdf1da1b8aa44 Mon Sep 17 00:00:00 2001 From: FateJH Date: Tue, 11 Oct 2016 15:20:05 -0400 Subject: [PATCH 3/8] solved encoding issues, but am not perfectly happy with the solution; tests pass for one friend name and many friend names; check new codec for sizedList --- .../scala/net/psforever/packet/PSPacket.scala | 25 ++++---- .../packet/game/FriendsResponse.scala | 42 +++++++++---- common/src/test/scala/GamePacketTest.scala | 61 +++++++++++++++---- 3 files changed, 94 insertions(+), 34 deletions(-) diff --git a/common/src/main/scala/net/psforever/packet/PSPacket.scala b/common/src/main/scala/net/psforever/packet/PSPacket.scala index e59cf734..78f8ae29 100644 --- a/common/src/main/scala/net/psforever/packet/PSPacket.scala +++ b/common/src/main/scala/net/psforever/packet/PSPacket.scala @@ -3,7 +3,8 @@ package net.psforever.packet import java.nio.charset.Charset -import scodec.{DecodeResult, Err, Codec, Attempt} +import scodec.Attempt.Successful +import scodec.{Attempt, Codec, DecodeResult, Err} import scodec.bits._ import scodec.codecs._ import scodec._ @@ -205,15 +206,17 @@ object PacketHelpers { */ /** - * Construct a `Codec` for reading `wchar_t` (wide character) `Strings` whose length field are constrained to specific bit size proportions. - * Padding may also exist between the length field and the beginning of the contents. - * @param lenSize a codec that defines the bit size that encodes the length - * @param adjustment the optional alignment for padding; defaults to 0 - * @return the `String` `Codec` + * Codec that encodes/decodes a list of `n` elements, where `n` is known at compile time.
+ *
+ * This function is copied almost verbatim from its source, with exception of swapping the parameter that is normally a `Nat` `literal`. + * The modified function takes a normal unsigned `Integer` and assures that the parameter is a non-negative before further processing. + * @param size the fixed size of the `List` + * @param codec a codec that describes each of the contents of the `List` + * @tparam A the type of the `List` contents + * @see codec\package.scala, sizedList + * @see codec\package.scala, listOfN + * @see codec\package.scala, provides + * @return a codec that works on a List of A but excludes the size from the encoding */ - def specSizeWideStringAligned(lenSize : Codec[Int], adjustment : Int = 0) : Codec[String] = - variableSizeBytes((lenSize <~ ignore(adjustment)).xmap( - insize => insize*2, // number of symbols -> number of bytes (decode) - outSize => outSize/2 // number of bytes -> number of symbols (encode) - ), utf16) + def sizedList[A](size : Int, codec : Codec[A]) : Codec[List[A]] = listOfN(provide(if(size < 0) 0 else size), codec) } diff --git a/common/src/main/scala/net/psforever/packet/game/FriendsResponse.scala b/common/src/main/scala/net/psforever/packet/game/FriendsResponse.scala index 611c2541..43a978bf 100644 --- a/common/src/main/scala/net/psforever/packet/game/FriendsResponse.scala +++ b/common/src/main/scala/net/psforever/packet/game/FriendsResponse.scala @@ -5,25 +5,43 @@ import net.psforever.packet.{GamePacketOpcode, Marshallable, PacketHelpers, Plan import scodec.Codec import scodec.codecs._ -/** - * na - * @param player_guid the player - * @param friend the name of the friend - * @param unk na - */ -final case class FriendsResponse(player_guid : PlanetSideGUID, - friend : String, - unk : Boolean) +final case class Friend(name : String = "", + online : Boolean = false) + +final case class FriendsResponse(unk1 : Int, + unk2 : Int, + unk3 : Boolean, + unk4 : Boolean, + number_of_friends : Int, + friend : Friend, + friends : List[Friend] = Nil) extends PlanetSideGamePacket { type Packet = FriendsResponse def opcode = GamePacketOpcode.FriendsResponse def encode = FriendsReponse.encode(this) } +object Friend extends Marshallable[Friend] { + implicit val codec : Codec[Friend] = ( + ("name" | PacketHelpers.encodedWideStringAligned(3)) :: + ("online" | bool) + ).as[Friend] + + implicit val codec_list : Codec[Friend] = ( + ("name" | PacketHelpers.encodedWideStringAligned(7)) :: + ("online" | bool) + ).as[Friend] +} + object FriendsReponse extends Marshallable[FriendsResponse] { implicit val codec : Codec[FriendsResponse] = ( - ("player_guid" | PlanetSideGUID.codec) :: - ("friend" | PacketHelpers.specSizeWideStringAligned(uint(5), 3)) :: - ("unk" | bool) + ("unk1" | uintL(3)) :: + ("unk2" | uintL(4)) :: + ("unk3" | bool) :: + ("unk4" | bool) :: + (("number_of_friends" | uintL(4)) >>:~ { len => + ("friend" | Friend.codec) :: + ("friends" | PacketHelpers.sizedList(len-1, Friend.codec_list)) + }) ).as[FriendsResponse] } diff --git a/common/src/test/scala/GamePacketTest.scala b/common/src/test/scala/GamePacketTest.scala index 671983b4..a903f55b 100644 --- a/common/src/test/scala/GamePacketTest.scala +++ b/common/src/test/scala/GamePacketTest.scala @@ -831,25 +831,64 @@ class GamePacketTest extends Specification { } "FriendsResponse" should { - val string = hex"73 618C 60 4B007500720074004800650063007400690063002D004700 00" + val stringOneFriend = hex"73 61 8C 60 4B007500720074004800650063007400690063002D004700 00" + val stringManyFriends = hex"73 01 AC 48 4100 6E00 6700 6500 6C00 6C00 6F00 2D00 5700 47 00 7400 6800 6500 7000 6800 6100 7400 7400 7000 6800 7200 6F00 6700 6700 46 80 4B00 6900 6D00 7000 6F00 7300 7300 6900 6200 6C00 6500 3100 3200 45 00 5A00 6500 6100 7200 7400 6800 6C00 6900 6E00 6700 46 00 4B00 7500 7200 7400 4800 6500 6300 7400 6900 6300 2D00 4700 00" - "decode" in { - PacketCoding.DecodePacket(string).require match { - case FriendsResponse(player_guid, friend, unk) => - player_guid mustEqual PlanetSideGUID(35937) - friend.length mustEqual 12 - friend mustEqual "KurtHectic-G" - unk mustEqual false + "decode (one friend)" in { + PacketCoding.DecodePacket(stringOneFriend).require match { + case FriendsResponse(unk1, unk2, unk3, unk4, number_of_friends, friend, list) => + unk1 mustEqual 3 + unk2 mustEqual 0 + unk3 mustEqual true + unk4 mustEqual true + number_of_friends mustEqual 1 + friend.name mustEqual "KurtHectic-G" + friend.online mustEqual false + list.size mustEqual 0 case default => ko } } - "encode" in { - val msg = FriendsResponse(PlanetSideGUID(35937), "KurtHectic-G", false) + "decode (multiple friends)" in { + PacketCoding.DecodePacket(stringManyFriends).require match { + case FriendsResponse(unk1, unk2, unk3, unk4, number_of_friends, friend, list) => + unk1 mustEqual 0 + unk2 mustEqual 0 + unk3 mustEqual true + unk4 mustEqual true + number_of_friends mustEqual 5 + friend.name mustEqual "Angello-W" + friend.online mustEqual false + list.size mustEqual 4 + list.head.name mustEqual "thephattphrogg" + list.head.online mustEqual false + list(1).name mustEqual "Kimpossible12" + list(1).online mustEqual false + list(2).name mustEqual "Zearthling" + list(2).online mustEqual false + list(3).name mustEqual "KurtHectic-G" + list(3).online mustEqual false + case default => + ko + } + } + + "encode (one friend)" in { + val msg = FriendsResponse(3, 0, true, true, 1, Friend("KurtHectic-G", false)) val pkt = PacketCoding.EncodePacket(msg).require.toByteVector - pkt mustEqual string + pkt mustEqual stringOneFriend + } + + "encode (multiple friends)" in { + val msg = FriendsResponse(0, 0, true, true, 5, Friend("Angello-W", false), Friend("thephattphrogg", false) :: + Friend("Kimpossible12", false) :: + Friend("Zearthling", false) :: + Friend("KurtHectic-G", false) :: Nil) + val pkt = PacketCoding.EncodePacket(msg).require.toByteVector + + pkt mustEqual stringManyFriends } } From 7050027235c92661b60170dc6d566aa06f979b9b Mon Sep 17 00:00:00 2001 From: FateJH Date: Tue, 11 Oct 2016 20:51:24 -0400 Subject: [PATCH 4/8] correct packet fields to support and added tests for 'no entry' packet; added commentary --- .../scala/net/psforever/packet/PSPacket.scala | 4 +- .../packet/game/FriendsResponse.scala | 56 +++++++++++++++---- common/src/test/scala/GamePacketTest.scala | 43 +++++++++++--- 3 files changed, 81 insertions(+), 22 deletions(-) diff --git a/common/src/main/scala/net/psforever/packet/PSPacket.scala b/common/src/main/scala/net/psforever/packet/PSPacket.scala index 78f8ae29..a730a849 100644 --- a/common/src/main/scala/net/psforever/packet/PSPacket.scala +++ b/common/src/main/scala/net/psforever/packet/PSPacket.scala @@ -209,8 +209,8 @@ object PacketHelpers { * Codec that encodes/decodes a list of `n` elements, where `n` is known at compile time.
*
* This function is copied almost verbatim from its source, with exception of swapping the parameter that is normally a `Nat` `literal`. - * The modified function takes a normal unsigned `Integer` and assures that the parameter is a non-negative before further processing. - * @param size the fixed size of the `List` + * The modified function takes a normal unsigned `Integer` and assures that the parameter is non-negative before further processing. + * @param size the known size of the `List` * @param codec a codec that describes each of the contents of the `List` * @tparam A the type of the `List` contents * @see codec\package.scala, sizedList diff --git a/common/src/main/scala/net/psforever/packet/game/FriendsResponse.scala b/common/src/main/scala/net/psforever/packet/game/FriendsResponse.scala index 43a978bf..bf2f5b56 100644 --- a/common/src/main/scala/net/psforever/packet/game/FriendsResponse.scala +++ b/common/src/main/scala/net/psforever/packet/game/FriendsResponse.scala @@ -5,15 +5,45 @@ import net.psforever.packet.{GamePacketOpcode, Marshallable, PacketHelpers, Plan import scodec.Codec import scodec.codecs._ -final case class Friend(name : String = "", +/** + * An entry in the list of players known to and tracked by this player. + * They're called "friends" even though they can be used for a list of ignored players as well. + * @param name the name of the player + * @param online the player's current state of activity; defaults to `false`, or offline + */ +final case class Friend(name : String, online : Boolean = false) -final case class FriendsResponse(unk1 : Int, - unk2 : Int, +/** + * Manage the lists of other players whose names are retained by the given player.
+ *
+ * Friends can be remembered and their current playing status can be reported. + * Ignored players will have their comments stifled in the given player's chat window. + * This does not handle outfit member lists.
+ *
+ * Actions:
+ * 0 - initialize friends list (no logging)
+ * 1 - add entry to friends list
+ * 2 - remove entry from friends list
+ * 3 - update status of player in friends list; + * if player is not listed, he is not added
+ * 4 - initialize ignored players list (no logging)
+ * 5 - add entry to ignored players list
+ * 6 - remove entry from ignored players list
+ * @param action the purpose of the entry(s) in this packet + * @param unk1 na; always 0? + * @param unk2 na; always `true`? + * @param unk3 na; always `true`? + * @param number_of_friends the number of `Friend` entries handled by this packet; max is 15 per packet + * @param friend the first `Friend` entry + * @param friends all the other `Friend` entries + */ +final case class FriendsResponse(action : Int, + unk1 : Int, + unk2 : Boolean, unk3 : Boolean, - unk4 : Boolean, number_of_friends : Int, - friend : Friend, + friend : Option[Friend] = None, friends : List[Friend] = Nil) extends PlanetSideGamePacket { type Packet = FriendsResponse @@ -27,6 +57,10 @@ object Friend extends Marshallable[Friend] { ("online" | bool) ).as[Friend] + /** + * This codec is used for the "`List` of other `Friends`." + * Initial byte-alignment creates padding differences which requires a second `Codec`. + */ implicit val codec_list : Codec[Friend] = ( ("name" | PacketHelpers.encodedWideStringAligned(7)) :: ("online" | bool) @@ -35,13 +69,13 @@ object Friend extends Marshallable[Friend] { object FriendsReponse extends Marshallable[FriendsResponse] { implicit val codec : Codec[FriendsResponse] = ( - ("unk1" | uintL(3)) :: - ("unk2" | uintL(4)) :: + ("action" | uintL(3)) :: + ("unk1" | uint4L) :: + ("unk2" | bool) :: ("unk3" | bool) :: - ("unk4" | bool) :: - (("number_of_friends" | uintL(4)) >>:~ { len => - ("friend" | Friend.codec) :: - ("friends" | PacketHelpers.sizedList(len-1, Friend.codec_list)) + (("number_of_friends" | uint4L) >>:~ { len => + conditional(len > 0, "friend" | Friend.codec) :: + ("friends" | PacketHelpers.sizedList(len-1, Friend.codec_list)) //List of 'Friend(String, Boolean)'s without a size field when encoded }) ).as[FriendsResponse] } diff --git a/common/src/test/scala/GamePacketTest.scala b/common/src/test/scala/GamePacketTest.scala index a903f55b..1197b1fe 100644 --- a/common/src/test/scala/GamePacketTest.scala +++ b/common/src/test/scala/GamePacketTest.scala @@ -833,6 +833,7 @@ class GamePacketTest extends Specification { "FriendsResponse" should { val stringOneFriend = hex"73 61 8C 60 4B007500720074004800650063007400690063002D004700 00" val stringManyFriends = hex"73 01 AC 48 4100 6E00 6700 6500 6C00 6C00 6F00 2D00 5700 47 00 7400 6800 6500 7000 6800 6100 7400 7400 7000 6800 7200 6F00 6700 6700 46 80 4B00 6900 6D00 7000 6F00 7300 7300 6900 6200 6C00 6500 3100 3200 45 00 5A00 6500 6100 7200 7400 6800 6C00 6900 6E00 6700 46 00 4B00 7500 7200 7400 4800 6500 6300 7400 6900 6300 2D00 4700 00" + val stringShort = hex"73 81 80" "decode (one friend)" in { PacketCoding.DecodePacket(stringOneFriend).require match { @@ -842,8 +843,9 @@ class GamePacketTest extends Specification { unk3 mustEqual true unk4 mustEqual true number_of_friends mustEqual 1 - friend.name mustEqual "KurtHectic-G" - friend.online mustEqual false + friend.isDefined mustEqual true + friend.get.name mustEqual "KurtHectic-G" + friend.get.online mustEqual false list.size mustEqual 0 case default => ko @@ -858,8 +860,9 @@ class GamePacketTest extends Specification { unk3 mustEqual true unk4 mustEqual true number_of_friends mustEqual 5 - friend.name mustEqual "Angello-W" - friend.online mustEqual false + friend.isDefined mustEqual true + friend.get.name mustEqual "Angello-W" + friend.get.online mustEqual false list.size mustEqual 4 list.head.name mustEqual "thephattphrogg" list.head.online mustEqual false @@ -874,22 +877,44 @@ class GamePacketTest extends Specification { } } + "decode (short)" in { + PacketCoding.DecodePacket(stringShort).require match { + case FriendsResponse(unk1, unk2, unk3, unk4, number_of_friends, friend, list) => + unk1 mustEqual 4 + unk2 mustEqual 0 + unk3 mustEqual true + unk4 mustEqual true + number_of_friends mustEqual 0 + friend.isDefined mustEqual false + list.size mustEqual 0 + case default => + ko + } + } + "encode (one friend)" in { - val msg = FriendsResponse(3, 0, true, true, 1, Friend("KurtHectic-G", false)) + val msg = FriendsResponse(3, 0, true, true, 1, Option(Friend("KurtHectic-G", false))) val pkt = PacketCoding.EncodePacket(msg).require.toByteVector pkt mustEqual stringOneFriend } "encode (multiple friends)" in { - val msg = FriendsResponse(0, 0, true, true, 5, Friend("Angello-W", false), Friend("thephattphrogg", false) :: - Friend("Kimpossible12", false) :: - Friend("Zearthling", false) :: - Friend("KurtHectic-G", false) :: Nil) + val msg = FriendsResponse(0, 0, true, true, 5, Option(Friend("Angello-W", false)), Friend("thephattphrogg", false) :: + Friend("Kimpossible12", false) :: + Friend("Zearthling", false) :: + Friend("KurtHectic-G", false) :: Nil) val pkt = PacketCoding.EncodePacket(msg).require.toByteVector pkt mustEqual stringManyFriends } + + "encode (short)" in { + val msg = FriendsResponse(4, 0, true, true, 0) + val pkt = PacketCoding.EncodePacket(msg).require.toByteVector + + pkt mustEqual stringShort + } } "WeaponDryFireMessage" should { From 61a779b311c006ca1d27909ce37b93b581e4af6c Mon Sep 17 00:00:00 2001 From: FateJH Date: Tue, 11 Oct 2016 20:54:08 -0400 Subject: [PATCH 5/8] removed excessive import from PSPacket.scala --- common/src/main/scala/net/psforever/packet/PSPacket.scala | 1 - 1 file changed, 1 deletion(-) diff --git a/common/src/main/scala/net/psforever/packet/PSPacket.scala b/common/src/main/scala/net/psforever/packet/PSPacket.scala index a730a849..16861762 100644 --- a/common/src/main/scala/net/psforever/packet/PSPacket.scala +++ b/common/src/main/scala/net/psforever/packet/PSPacket.scala @@ -3,7 +3,6 @@ package net.psforever.packet import java.nio.charset.Charset -import scodec.Attempt.Successful import scodec.{Attempt, Codec, DecodeResult, Err} import scodec.bits._ import scodec.codecs._ From 0831b1ac8679f941204b58789880ef67829533d1 Mon Sep 17 00:00:00 2001 From: FateJH Date: Wed, 12 Oct 2016 20:02:07 -0400 Subject: [PATCH 6/8] restoring the order of imported assets to original --- common/src/main/scala/net/psforever/packet/PSPacket.scala | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/common/src/main/scala/net/psforever/packet/PSPacket.scala b/common/src/main/scala/net/psforever/packet/PSPacket.scala index 16861762..ac25b1b8 100644 --- a/common/src/main/scala/net/psforever/packet/PSPacket.scala +++ b/common/src/main/scala/net/psforever/packet/PSPacket.scala @@ -3,7 +3,7 @@ package net.psforever.packet import java.nio.charset.Charset -import scodec.{Attempt, Codec, DecodeResult, Err} +import scodec.{DecodeResult, Err, Codec, Attempt} import scodec.bits._ import scodec.codecs._ import scodec._ From 58d7d922e429f9369e7d72748acfce7cd13acf4d Mon Sep 17 00:00:00 2001 From: FateJH Date: Sat, 22 Oct 2016 16:25:38 -0400 Subject: [PATCH 7/8] fixing a long-standing typo --- .../main/scala/net/psforever/packet/GamePacketOpcode.scala | 2 +- .../scala/net/psforever/packet/game/FriendsResponse.scala | 4 ++-- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/common/src/main/scala/net/psforever/packet/GamePacketOpcode.scala b/common/src/main/scala/net/psforever/packet/GamePacketOpcode.scala index 22eb0489..24a382eb 100644 --- a/common/src/main/scala/net/psforever/packet/GamePacketOpcode.scala +++ b/common/src/main/scala/net/psforever/packet/GamePacketOpcode.scala @@ -455,7 +455,7 @@ object GamePacketOpcode extends Enumeration { case 0x70 => noDecoder(SquadMemberEvent) case 0x71 => noDecoder(PlatoonEvent) case 0x72 => noDecoder(FriendsRequest) - case 0x73 => game.FriendsReponse.decode + case 0x73 => game.FriendsResponse.decode case 0x74 => noDecoder(TriggerEnvironmentalDamageMessage) case 0x75 => noDecoder(TrainingZoneMessage) case 0x76 => noDecoder(DeployableObjectsInfoMessage) diff --git a/common/src/main/scala/net/psforever/packet/game/FriendsResponse.scala b/common/src/main/scala/net/psforever/packet/game/FriendsResponse.scala index bf2f5b56..a2ee7237 100644 --- a/common/src/main/scala/net/psforever/packet/game/FriendsResponse.scala +++ b/common/src/main/scala/net/psforever/packet/game/FriendsResponse.scala @@ -48,7 +48,7 @@ final case class FriendsResponse(action : Int, extends PlanetSideGamePacket { type Packet = FriendsResponse def opcode = GamePacketOpcode.FriendsResponse - def encode = FriendsReponse.encode(this) + def encode = FriendsResponse.encode(this) } object Friend extends Marshallable[Friend] { @@ -67,7 +67,7 @@ object Friend extends Marshallable[Friend] { ).as[Friend] } -object FriendsReponse extends Marshallable[FriendsResponse] { +object FriendsResponse extends Marshallable[FriendsResponse] { implicit val codec : Codec[FriendsResponse] = ( ("action" | uintL(3)) :: ("unk1" | uint4L) :: From 8cb5165c8b4a13f96340697a8b2022ca8a4e3257 Mon Sep 17 00:00:00 2001 From: FateJH Date: Mon, 9 Jan 2017 23:39:16 -0500 Subject: [PATCH 8/8] combined initial friend and formal list of friends into one list --- .../scala/net/psforever/packet/PSPacket.scala | 123 +++++++++++++++++- .../packet/game/FriendsResponse.scala | 27 +++- common/src/test/scala/GamePacketTest.scala | 51 ++++---- 3 files changed, 163 insertions(+), 38 deletions(-) diff --git a/common/src/main/scala/net/psforever/packet/PSPacket.scala b/common/src/main/scala/net/psforever/packet/PSPacket.scala index ac25b1b8..0c14f10f 100644 --- a/common/src/main/scala/net/psforever/packet/PSPacket.scala +++ b/common/src/main/scala/net/psforever/packet/PSPacket.scala @@ -209,13 +209,130 @@ object PacketHelpers { *
* This function is copied almost verbatim from its source, with exception of swapping the parameter that is normally a `Nat` `literal`. * The modified function takes a normal unsigned `Integer` and assures that the parameter is non-negative before further processing. - * @param size the known size of the `List` + * It casts to a `Long` and passes onto an overloaded method. + * @param size the known size of the `List` * @param codec a codec that describes each of the contents of the `List` * @tparam A the type of the `List` contents * @see codec\package.scala, sizedList * @see codec\package.scala, listOfN - * @see codec\package.scala, provides * @return a codec that works on a List of A but excludes the size from the encoding */ - def sizedList[A](size : Int, codec : Codec[A]) : Codec[List[A]] = listOfN(provide(if(size < 0) 0 else size), codec) + def listOfNSized[A](size : Int, codec : Codec[A]) : Codec[List[A]] = listOfNSized(if(size < 0) 0L else size.asInstanceOf[Long], codec) + + /** + * Codec that encodes/decodes a list of `n` elements, where `n` is known at compile time.
+ *
+ * This function is copied almost verbatim from its source, with exception of swapping the parameter that is normally a `Nat` `literal`. + * The modified function takes a normal unsigned `Long` and assures that the parameter is non-negative before further processing. + * @param size the known size of the `List` + * @param codec a codec that describes each of the contents of the `List` + * @tparam A the type of the `List` contents + * @see codec\package.scala, sizedList + * @see codec\package.scala, listOfN + * @see codec\package.scala, provide + * @return a codec that works on a List of A but excludes the size from the encoding + */ + def listOfNSized[A](size : Long, codec : Codec[A]) : Codec[List[A]] = listOfNAligned(provide(if(size < 0) 0 else size), 0, codec) + + /** + * Encode and decode a byte-aligned `List`.
+ *
+ * This function is copied almost verbatim from its source, but swapping the normal `ListCodec` for a new `AlignedListCodec`. + * It also changes the type of the list length `Codec` from `Int` to `Long`. + * Due to type erasure, this method can not be overloaded for both `Codec[Int]` and `Codec[Long]`. + * The compiler would resolve both internally into type `Codec[T]` and their function definitions would be identical. + * For the purposes of use, `longL(n)` will cast to an `Int` for the same acceptable values of `n` as in `uintL(n)`. + * @param countCodec the codec that represents the prefixed size of the `List` + * @param alignment the number of bits padded between the `List` size and the `List` contents + * @param valueCodec a codec that describes each of the contents of the `List` + * @tparam A the type of the `List` contents + * @see codec\package.scala, listOfN + * @return a codec that works on a List of A + */ + def listOfNAligned[A](countCodec : Codec[Long], alignment : Int, valueCodec : Codec[A]) : Codec[List[A]] = { + countCodec. + flatZip { + count => + new AlignedListCodec(countCodec, valueCodec, alignment, Some(count)) + }. + narrow[List[A]] ( + { + case (cnt, xs) => + if(xs.size == cnt) + Attempt.successful(xs) + else + Attempt.failure(Err(s"Insufficient number of elements: decoded ${xs.size} but should have decoded $cnt")) + }, + { + xs => + (xs.size, xs) + } + ). + withToString(s"listOfN($countCodec, $valueCodec)") + } +} + +/** + * The greater `Codec` class that encodes and decodes a byte-aligned `List`.
+ *
+ * This class is copied almost verbatim from its source, with two major modifications. + * First, heavy modifications to its `encode` process account for the alignment value. + * Second, the length field is parsed as a `Codec[Long]` value and type conversion is accounted for at several points. + * @param countCodec the codec that represents the prefixed size of the `List` + * @param valueCodec a codec that describes each of the contents of the `List` + * @param alignment the number of bits padded between the `List` size and the `List` contents (on successful) + * @param limit the number of elements in the `List` + * @tparam A the type of the `List` contents + * @see ListCodec.scala + */ +private class AlignedListCodec[A](countCodec : Codec[Long], valueCodec: Codec[A], alignment : Int, limit: Option[Long] = None) extends Codec[List[A]] { + /** + * Convert a `List` of elements into a byte-aligned `BitVector`.
+ *
+ * Bit padding after the encoded size of the `List` is only added if the `alignment` value is greater than zero and the initial encoding process was successful. + * The padding is rather heavy-handed and a completely different `BitVector` is returned if successful. + * @param list the `List` to be encoded + * @return the `BitVector` encoding, if successful + */ + override def encode(list : List[A]) : Attempt[BitVector] = { + var solve : Attempt[BitVector] = Encoder.encodeSeq(valueCodec)(list) + if(alignment > 0) { + solve match { + case Attempt.Successful(vector) => + val countCodecSize : Long = countCodec.sizeBound.lowerBound + solve = Attempt.successful(vector.take(countCodecSize) ++ BitVector.fill(alignment)(false) ++ vector.drop(countCodecSize)) + case _ => + solve = Attempt.failure(Err("failed to create a list")) + } + } + solve + } + + /** + * Convert a byte-aligned `BitVector` into a `List` of elements. + * @param buffer the encoded bits in the `List`, preceded by the alignment bits + * @return the decoded `List` + */ + override def decode(buffer: BitVector) = { + val lim = Option( if(limit.isDefined) limit.get.asInstanceOf[Int] else 0 ) //TODO potentially unsafe size conversion + Decoder.decodeCollect[List, A](valueCodec, lim)(buffer.drop(alignment)) + } + + /** + * The size of the encoded `List`. + * @return the size as calculated by the size of each element for each element + */ + override def sizeBound = limit match { + case None => + SizeBound.unknown + case Some(lim : Long) => + valueCodec.sizeBound * lim + } + + /** + * Get a `String` representation of this `List`. + * Unchanged from original. + * @return the `String` representation + */ + override def toString = s"list($valueCodec)" } diff --git a/common/src/main/scala/net/psforever/packet/game/FriendsResponse.scala b/common/src/main/scala/net/psforever/packet/game/FriendsResponse.scala index a2ee7237..31dfb289 100644 --- a/common/src/main/scala/net/psforever/packet/game/FriendsResponse.scala +++ b/common/src/main/scala/net/psforever/packet/game/FriendsResponse.scala @@ -4,6 +4,7 @@ package net.psforever.packet.game import net.psforever.packet.{GamePacketOpcode, Marshallable, PacketHelpers, PlanetSideGamePacket} import scodec.Codec import scodec.codecs._ +import shapeless.{::, HNil} /** * An entry in the list of players known to and tracked by this player. @@ -34,16 +35,12 @@ final case class Friend(name : String, * @param unk1 na; always 0? * @param unk2 na; always `true`? * @param unk3 na; always `true`? - * @param number_of_friends the number of `Friend` entries handled by this packet; max is 15 per packet - * @param friend the first `Friend` entry - * @param friends all the other `Friend` entries + * @param friends a list of `Friend`s */ final case class FriendsResponse(action : Int, unk1 : Int, unk2 : Boolean, unk3 : Boolean, - number_of_friends : Int, - friend : Option[Friend] = None, friends : List[Friend] = Nil) extends PlanetSideGamePacket { type Packet = FriendsResponse @@ -75,7 +72,23 @@ object FriendsResponse extends Marshallable[FriendsResponse] { ("unk3" | bool) :: (("number_of_friends" | uint4L) >>:~ { len => conditional(len > 0, "friend" | Friend.codec) :: - ("friends" | PacketHelpers.sizedList(len-1, Friend.codec_list)) //List of 'Friend(String, Boolean)'s without a size field when encoded + ("friends" | PacketHelpers.listOfNSized(len-1, Friend.codec_list)) }) - ).as[FriendsResponse] + ).xmap[FriendsResponse] ( + { + case act :: u1 :: u2 :: u3 :: num :: friend1 :: friends :: HNil => + val friendList : List[Friend] = if(friend1.isDefined) { friend1.get :: friends } else { friends } + FriendsResponse(act, u1, u2, u3, friendList) + }, + { + case FriendsResponse(act, u1, u2, u3, friends) => + var friend1 : Option[Friend] = None + var friendList : List[Friend] = Nil + if(friends.nonEmpty) { + friend1 = Some(friends.head) + friendList = friends.drop(1) + } + act :: u1 :: u2 :: u3 :: friends.size :: friend1 :: friendList :: HNil + } + ) } diff --git a/common/src/test/scala/GamePacketTest.scala b/common/src/test/scala/GamePacketTest.scala index 1197b1fe..53bb8b8e 100644 --- a/common/src/test/scala/GamePacketTest.scala +++ b/common/src/test/scala/GamePacketTest.scala @@ -837,16 +837,14 @@ class GamePacketTest extends Specification { "decode (one friend)" in { PacketCoding.DecodePacket(stringOneFriend).require match { - case FriendsResponse(unk1, unk2, unk3, unk4, number_of_friends, friend, list) => - unk1 mustEqual 3 + case FriendsResponse(action, unk2, unk3, unk4, list) => + action mustEqual 3 unk2 mustEqual 0 unk3 mustEqual true unk4 mustEqual true - number_of_friends mustEqual 1 - friend.isDefined mustEqual true - friend.get.name mustEqual "KurtHectic-G" - friend.get.online mustEqual false - list.size mustEqual 0 + list.size mustEqual 1 + list.head.name mustEqual "KurtHectic-G" + list.head.online mustEqual false case default => ko } @@ -854,24 +852,22 @@ class GamePacketTest extends Specification { "decode (multiple friends)" in { PacketCoding.DecodePacket(stringManyFriends).require match { - case FriendsResponse(unk1, unk2, unk3, unk4, number_of_friends, friend, list) => - unk1 mustEqual 0 + case FriendsResponse(action, unk2, unk3, unk4, list) => + action mustEqual 0 unk2 mustEqual 0 unk3 mustEqual true unk4 mustEqual true - number_of_friends mustEqual 5 - friend.isDefined mustEqual true - friend.get.name mustEqual "Angello-W" - friend.get.online mustEqual false - list.size mustEqual 4 - list.head.name mustEqual "thephattphrogg" + list.size mustEqual 5 + list.head.name mustEqual "Angello-W" list.head.online mustEqual false - list(1).name mustEqual "Kimpossible12" + list(1).name mustEqual "thephattphrogg" list(1).online mustEqual false - list(2).name mustEqual "Zearthling" + list(2).name mustEqual "Kimpossible12" list(2).online mustEqual false - list(3).name mustEqual "KurtHectic-G" + list(3).name mustEqual "Zearthling" list(3).online mustEqual false + list(4).name mustEqual "KurtHectic-G" + list(4).online mustEqual false case default => ko } @@ -879,13 +875,11 @@ class GamePacketTest extends Specification { "decode (short)" in { PacketCoding.DecodePacket(stringShort).require match { - case FriendsResponse(unk1, unk2, unk3, unk4, number_of_friends, friend, list) => - unk1 mustEqual 4 + case FriendsResponse(action, unk2, unk3, unk4, list) => + action mustEqual 4 unk2 mustEqual 0 unk3 mustEqual true unk4 mustEqual true - number_of_friends mustEqual 0 - friend.isDefined mustEqual false list.size mustEqual 0 case default => ko @@ -893,24 +887,25 @@ class GamePacketTest extends Specification { } "encode (one friend)" in { - val msg = FriendsResponse(3, 0, true, true, 1, Option(Friend("KurtHectic-G", false))) + val msg = FriendsResponse(3, 0, true, true, Friend("KurtHectic-G", false) :: Nil) val pkt = PacketCoding.EncodePacket(msg).require.toByteVector pkt mustEqual stringOneFriend } "encode (multiple friends)" in { - val msg = FriendsResponse(0, 0, true, true, 5, Option(Friend("Angello-W", false)), Friend("thephattphrogg", false) :: - Friend("Kimpossible12", false) :: - Friend("Zearthling", false) :: - Friend("KurtHectic-G", false) :: Nil) + val msg = FriendsResponse(0, 0, true, true, Friend("Angello-W", false) :: + Friend("thephattphrogg", false) :: + Friend("Kimpossible12", false) :: + Friend("Zearthling", false) :: + Friend("KurtHectic-G", false) :: Nil) val pkt = PacketCoding.EncodePacket(msg).require.toByteVector pkt mustEqual stringManyFriends } "encode (short)" in { - val msg = FriendsResponse(4, 0, true, true, 0) + val msg = FriendsResponse(4, 0, true, true) val pkt = PacketCoding.EncodePacket(msg).require.toByteVector pkt mustEqual stringShort