diff --git a/common/src/main/scala/net/psforever/packet/PSPacket.scala b/common/src/main/scala/net/psforever/packet/PSPacket.scala index ac25b1b85..0c14f10fa 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 a2ee72373..31dfb2895 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 1197b1fe0..53bb8b8e4 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