From 6786d9934d99acea0ce2b4b0db62173e20d4e6ae Mon Sep 17 00:00:00 2001 From: FateJH Date: Fri, 16 Sep 2016 09:12:35 -0400 Subject: [PATCH 1/8] added initial HotSpotUpdateMessage packet and tests for the clear condition (size=0) --- .../psforever/packet/GamePacketOpcode.scala | 2 +- .../packet/game/HotSpotUpdateMessage.scala | 50 +++++++++++++++++++ common/src/test/scala/GamePacketTest.scala | 21 ++++++++ 3 files changed, 72 insertions(+), 1 deletion(-) create mode 100644 common/src/main/scala/net/psforever/packet/game/HotSpotUpdateMessage.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..412da85f 100644 --- a/common/src/main/scala/net/psforever/packet/GamePacketOpcode.scala +++ b/common/src/main/scala/net/psforever/packet/GamePacketOpcode.scala @@ -506,7 +506,7 @@ object GamePacketOpcode extends Enumeration { case 0x9c => noDecoder(DebugDrawMessage) case 0x9d => noDecoder(SoulMarkMessage) case 0x9e => noDecoder(UplinkPositionEvent) - case 0x9f => noDecoder(HotSpotUpdateMessage) + case 0x9f => game.HotSpotUpdateMessage.decode // OPCODES 0xa0-af case 0xa0 => game.BuildingInfoUpdateMessage.decode diff --git a/common/src/main/scala/net/psforever/packet/game/HotSpotUpdateMessage.scala b/common/src/main/scala/net/psforever/packet/game/HotSpotUpdateMessage.scala new file mode 100644 index 00000000..9309f3ab --- /dev/null +++ b/common/src/main/scala/net/psforever/packet/game/HotSpotUpdateMessage.scala @@ -0,0 +1,50 @@ +// Copyright (c) 2016 PSForever.net to present +package net.psforever.packet.game + +import net.psforever.packet.{GamePacketOpcode, Marshallable, PlanetSideGamePacket} +import scodec.Codec +import scodec.codecs._ + +/** + * na + * @param unk na + * @param x the x-coord of the center of the hotspot + * @param y the y-coord of the center of the hotspot + * @param scale the scaling of the hotspot graphic + */ +final case class HotSpotInfo(unk : Int, + x : Int, + y : Int, + scale : Int) + +/** + * na + * @param continent_guid the zone (continent) + * @param unk na + * @param spots a list of HotSpotInfo, or Nil if empty + */ +// TODO test for size > 0 (e.g., > hex'00') +// TODO test for size > 15 (e.g., > hex'F0') +final case class HotSpotUpdateMessage(continent_guid : PlanetSideGUID, + unk : Int, + spots : List[HotSpotInfo] = Nil) + extends PlanetSideGamePacket { + type Packet = HotSpotUpdateMessage + def opcode = GamePacketOpcode.HotSpotUpdateMessage + def encode = HotSpotUpdateMessage.encode(this) +} + +object HotSpotUpdateMessage extends Marshallable[HotSpotUpdateMessage] { + implicit val hotspot_codec : Codec[HotSpotInfo] = { + ("unk" | uint8L) :: + ("x" | uint16L) :: + ("y" | uint16L) :: + ("scale" | uintL(20)) + }.as[HotSpotInfo] + + implicit val codec : Codec[HotSpotUpdateMessage] = ( + ("continent_guid" | PlanetSideGUID.codec) :: + ("unk" | uint8L) :: + ("spots" | listOfN(uint8L, hotspot_codec)) + ).as[HotSpotUpdateMessage] +} diff --git a/common/src/test/scala/GamePacketTest.scala b/common/src/test/scala/GamePacketTest.scala index 2f7e8a11..0f88abdf 100644 --- a/common/src/test/scala/GamePacketTest.scala +++ b/common/src/test/scala/GamePacketTest.scala @@ -915,6 +915,27 @@ class GamePacketTest extends Specification { } } + "HotSpotUpdateMessage" should { + val stringClear = hex"9F 0500 10 00" + + "decode (clear)" in { + PacketCoding.DecodePacket(stringClear).require match { + case HotSpotUpdateMessage(continent_guid, unk, spots) => + continent_guid mustEqual PlanetSideGUID(5) + unk mustEqual 16 + spots.size mustEqual 0 + case _ => + ko + } + } + + "encode (clear)" in { + val msg = HotSpotUpdateMessage(PlanetSideGUID(5),16) + val pkt = PacketCoding.EncodePacket(msg).require.toByteVector + pkt mustEqual stringClear + } + } + "BuildingInfoUpdateMessage" should { val string = hex"a0 04 00 09 00 16 00 00 00 00 80 00 00 00 17 00 00 00 00 00 00 40" From 4529006490b3d2e7940872ec985506b1ccb21bad Mon Sep 17 00:00:00 2001 From: FateJH Date: Fri, 16 Sep 2016 21:57:03 -0400 Subject: [PATCH 2/8] corrections to the structure of the packet and the data in the List; comments --- .../packet/game/HotSpotUpdateMessage.scala | 45 +++++++++++++------ common/src/test/scala/GamePacketTest.scala | 7 +-- 2 files changed, 36 insertions(+), 16 deletions(-) diff --git a/common/src/main/scala/net/psforever/packet/game/HotSpotUpdateMessage.scala b/common/src/main/scala/net/psforever/packet/game/HotSpotUpdateMessage.scala index 9309f3ab..f3dc760d 100644 --- a/common/src/main/scala/net/psforever/packet/game/HotSpotUpdateMessage.scala +++ b/common/src/main/scala/net/psforever/packet/game/HotSpotUpdateMessage.scala @@ -6,25 +6,43 @@ import scodec.Codec import scodec.codecs._ /** - * na - * @param unk na + * Information for positioning a hotspot on the continental map.
+ *
+ * The coordinate values and the scaling value have different endianness than most numbers transmitted as packet data. + * The two unknown values are not part of the positioning system, at least not a part of the coordinates.
+ *
+ * The origin point is the lowest left corner of the map grid. + * On either axis, the other edge of the map is found at the maximum value 4096 (`FFF`). + * The scale is typically set as 128 (`80000`) but can also be made smaller or made absurdly big. + * @param unk1 na * @param x the x-coord of the center of the hotspot + * @param unk2 na * @param y the y-coord of the center of the hotspot - * @param scale the scaling of the hotspot graphic + * @param scale the scaling of the hotspot icon */ -final case class HotSpotInfo(unk : Int, +final case class HotSpotInfo(unk1 : Int, x : Int, + unk2 : Int, y : Int, scale : Int) /** - * na + * A list of data for creating hotspots on a continental map.
+ *
+ * The hotspot system is a forgetful all-or-nothing affair. + * The packet that is always initially sent during server login clears any would-be hotspots from the map. + * Each time a hotspot packet is received for a zone, all of the previous hotspots for that zone are forgotten. + * To simply add a hotspot, the next packet has to contain information that re-explains the packets that were originally rendered.
+ *
+ * Exploration 1:
+ * The unknown parameter has been observed with various non-zero values such as 1, 2, and 5. + * Visually, however, `unk` does not affect anything. + * Does it do something internally? * @param continent_guid the zone (continent) * @param unk na - * @param spots a list of HotSpotInfo, or Nil if empty + * @param spots a List of HotSpotInfo, or `Nil` if empty */ -// TODO test for size > 0 (e.g., > hex'00') -// TODO test for size > 15 (e.g., > hex'F0') +// TODO need aligned/padded list support final case class HotSpotUpdateMessage(continent_guid : PlanetSideGUID, unk : Int, spots : List[HotSpotInfo] = Nil) @@ -36,15 +54,16 @@ final case class HotSpotUpdateMessage(continent_guid : PlanetSideGUID, object HotSpotUpdateMessage extends Marshallable[HotSpotUpdateMessage] { implicit val hotspot_codec : Codec[HotSpotInfo] = { - ("unk" | uint8L) :: - ("x" | uint16L) :: - ("y" | uint16L) :: - ("scale" | uintL(20)) + ("unk1" | uint8) :: + ("x" | uint(12)) :: + ("unk2" | uint8) :: + ("y" | uint(12)) :: + ("scale" | uint(20)) }.as[HotSpotInfo] implicit val codec : Codec[HotSpotUpdateMessage] = ( ("continent_guid" | PlanetSideGUID.codec) :: - ("unk" | uint8L) :: + ("unk" | uint4L) :: ("spots" | listOfN(uint8L, hotspot_codec)) ).as[HotSpotUpdateMessage] } diff --git a/common/src/test/scala/GamePacketTest.scala b/common/src/test/scala/GamePacketTest.scala index 0f88abdf..288c42cf 100644 --- a/common/src/test/scala/GamePacketTest.scala +++ b/common/src/test/scala/GamePacketTest.scala @@ -916,13 +916,14 @@ class GamePacketTest extends Specification { } "HotSpotUpdateMessage" should { - val stringClear = hex"9F 0500 10 00" + val stringClear = hex"9F 0500 1 00 0" + val stringOne = hex"9F 0500 1 01 0 00 2E9 00 145 80000 0" "decode (clear)" in { PacketCoding.DecodePacket(stringClear).require match { case HotSpotUpdateMessage(continent_guid, unk, spots) => continent_guid mustEqual PlanetSideGUID(5) - unk mustEqual 16 + unk mustEqual 1 spots.size mustEqual 0 case _ => ko @@ -930,7 +931,7 @@ class GamePacketTest extends Specification { } "encode (clear)" in { - val msg = HotSpotUpdateMessage(PlanetSideGUID(5),16) + val msg = HotSpotUpdateMessage(PlanetSideGUID(5),1) val pkt = PacketCoding.EncodePacket(msg).require.toByteVector pkt mustEqual stringClear } From ae579d1780faa0898556f26126a7c7aac13aefa5 Mon Sep 17 00:00:00 2001 From: FateJH Date: Tue, 20 Sep 2016 23:47:08 -0400 Subject: [PATCH 3/8] added intentionally failing tests; the endian status of the hotspot parameters is in doubt! --- .../packet/game/HotSpotUpdateMessage.scala | 28 ++++++------ common/src/test/scala/GamePacketTest.scala | 44 +++++++++++++++++++ 2 files changed, 58 insertions(+), 14 deletions(-) diff --git a/common/src/main/scala/net/psforever/packet/game/HotSpotUpdateMessage.scala b/common/src/main/scala/net/psforever/packet/game/HotSpotUpdateMessage.scala index f3dc760d..dedccd92 100644 --- a/common/src/main/scala/net/psforever/packet/game/HotSpotUpdateMessage.scala +++ b/common/src/main/scala/net/psforever/packet/game/HotSpotUpdateMessage.scala @@ -8,17 +8,15 @@ import scodec.codecs._ /** * Information for positioning a hotspot on the continental map.
*
- * The coordinate values and the scaling value have different endianness than most numbers transmitted as packet data. - * The two unknown values are not part of the positioning system, at least not a part of the coordinates.
- *
* The origin point is the lowest left corner of the map grid. - * On either axis, the other edge of the map is found at the maximum value 4096 (`FFF`). - * The scale is typically set as 128 (`80000`) but can also be made smaller or made absurdly big. + * The coordinates of the hotspot do not match up to the map's internal coordinate system - what you learn using the `/loc` command. + * Hotspot coordinates ranges across from 0 (`000`) to 4096 (`FFF`) on both axes. + * The scale is typically set as 128 (`80000`) but can also be made smaller or even made absurdly big. * @param unk1 na * @param x the x-coord of the center of the hotspot * @param unk2 na * @param y the y-coord of the center of the hotspot - * @param scale the scaling of the hotspot icon + * @param scale how big the hotspot explosion icon appears */ final case class HotSpotInfo(unk1 : Int, x : Int, @@ -52,18 +50,20 @@ final case class HotSpotUpdateMessage(continent_guid : PlanetSideGUID, def encode = HotSpotUpdateMessage.encode(this) } -object HotSpotUpdateMessage extends Marshallable[HotSpotUpdateMessage] { - implicit val hotspot_codec : Codec[HotSpotInfo] = { - ("unk1" | uint8) :: - ("x" | uint(12)) :: - ("unk2" | uint8) :: - ("y" | uint(12)) :: - ("scale" | uint(20)) +object HotSpotInfo extends Marshallable[HotSpotInfo] { + implicit val codec : Codec[HotSpotInfo] = { + ("unk1" | uint8L) :: + ("x" | uintL(12)) :: + ("unk2" | uint8L) :: + ("y" | uintL(12)) :: + ("scale" | uintL(20)) }.as[HotSpotInfo] +} +object HotSpotUpdateMessage extends Marshallable[HotSpotUpdateMessage] { implicit val codec : Codec[HotSpotUpdateMessage] = ( ("continent_guid" | PlanetSideGUID.codec) :: ("unk" | uint4L) :: - ("spots" | listOfN(uint8L, hotspot_codec)) + ("spots" | listOfN(uint8L, HotSpotInfo.codec)) ).as[HotSpotUpdateMessage] } diff --git a/common/src/test/scala/GamePacketTest.scala b/common/src/test/scala/GamePacketTest.scala index 288c42cf..230a0d6f 100644 --- a/common/src/test/scala/GamePacketTest.scala +++ b/common/src/test/scala/GamePacketTest.scala @@ -918,6 +918,7 @@ class GamePacketTest extends Specification { "HotSpotUpdateMessage" should { val stringClear = hex"9F 0500 1 00 0" val stringOne = hex"9F 0500 1 01 0 00 2E9 00 145 80000 0" + val stringTwo = hex"9F 0500 5 02 0 00 D07 00 8CA 80000 00 BEA 00 4C4 80000" "decode (clear)" in { PacketCoding.DecodePacket(stringClear).require match { @@ -930,11 +931,54 @@ class GamePacketTest extends Specification { } } + "decode (one)" in { + PacketCoding.DecodePacket(stringOne).require match { + case HotSpotUpdateMessage(continent_guid, unk, spots) => + continent_guid mustEqual PlanetSideGUID(5) + unk mustEqual 1 + spots.size mustEqual 1 + spots.head.x mustEqual 3730 + spots.head.y mustEqual 1105 + spots.head.scale mustEqual 128 + case _ => + ko + } + } + + "decode (two)" in { + PacketCoding.DecodePacket(stringTwo).require match { + case HotSpotUpdateMessage(continent_guid, unk, spots) => + continent_guid mustEqual PlanetSideGUID(5) + unk mustEqual 5 + spots.size mustEqual 2 + spots.head.x mustEqual 125 + spots.head.y mustEqual 3240 + spots.head.scale mustEqual 128 + spots(1).x mustEqual 3755 + spots(1).y mustEqual 3140 + spots(1).scale mustEqual 128 + case _ => + ko + } + } + "encode (clear)" in { val msg = HotSpotUpdateMessage(PlanetSideGUID(5),1) val pkt = PacketCoding.EncodePacket(msg).require.toByteVector pkt mustEqual stringClear } + + "encode (one)" in { + val msg = HotSpotUpdateMessage(PlanetSideGUID(5),1, HotSpotInfo(0,3730,0,1105,128)::Nil) + val pkt = PacketCoding.EncodePacket(msg).require.toByteVector + pkt mustEqual stringOne + } + + "encode (two)" in { + val msg = HotSpotUpdateMessage(PlanetSideGUID(5),1, HotSpotInfo(0,125,0,3240,128)::HotSpotInfo(0,3755,0,3140,128)::Nil) + val pkt = PacketCoding.EncodePacket(msg).require.toByteVector + pkt mustEqual stringOne + } } "BuildingInfoUpdateMessage" should { From 6be873c4f66de869e8030329e8c1ba8c15993f68 Mon Sep 17 00:00:00 2001 From: FateJH Date: Wed, 21 Sep 2016 00:50:21 -0400 Subject: [PATCH 4/8] solved padding for decoding but not for encoding --- .../scala/net/psforever/packet/PSPacket.scala | 23 +++++++++++++++++++ .../packet/game/HotSpotUpdateMessage.scala | 5 ++-- common/src/test/scala/GamePacketTest.scala | 16 ++++++------- 3 files changed, 34 insertions(+), 10 deletions(-) diff --git a/common/src/main/scala/net/psforever/packet/PSPacket.scala b/common/src/main/scala/net/psforever/packet/PSPacket.scala index 2fe9a563..12d79609 100644 --- a/common/src/main/scala/net/psforever/packet/PSPacket.scala +++ b/common/src/main/scala/net/psforever/packet/PSPacket.scala @@ -203,4 +203,27 @@ object PacketHelpers { def encodedStringWithLimit(limit : Int) : Codec[String] = variableSizeBytes(encodedStringSizeWithLimit(limit), ascii) */ + + def listOfNAligned[A](countCodec: Codec[Int], alignment : Int, valueCodec: Codec[A]): Codec[List[A]] = { + countCodec. + flatZip { count => new AlignedListCodec(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)") + } +} + +private final class AlignedListCodec[A](codec: Codec[A], alignment : Int, limit: Option[Int] = None) extends Codec[List[A]] { + def sizeBound = limit match { + case None => SizeBound.unknown + case Some(lim) => codec.sizeBound * lim.toLong + } + + def encode(list: List[A]) = Encoder.encodeSeq(codec)(list) + + def decode(buffer: BitVector) = Decoder.decodeCollect[List, A](codec, limit)(buffer.drop(alignment)) + + override def toString = s"list($codec)" } diff --git a/common/src/main/scala/net/psforever/packet/game/HotSpotUpdateMessage.scala b/common/src/main/scala/net/psforever/packet/game/HotSpotUpdateMessage.scala index dedccd92..ec0f4f45 100644 --- a/common/src/main/scala/net/psforever/packet/game/HotSpotUpdateMessage.scala +++ b/common/src/main/scala/net/psforever/packet/game/HotSpotUpdateMessage.scala @@ -1,7 +1,7 @@ // Copyright (c) 2016 PSForever.net to present package net.psforever.packet.game -import net.psforever.packet.{GamePacketOpcode, Marshallable, PlanetSideGamePacket} +import net.psforever.packet.{GamePacketOpcode, Marshallable, PacketHelpers, PlanetSideGamePacket} import scodec.Codec import scodec.codecs._ @@ -41,6 +41,7 @@ final case class HotSpotInfo(unk1 : Int, * @param spots a List of HotSpotInfo, or `Nil` if empty */ // TODO need aligned/padded list support +// TODO test with sendRawResponse(hex"9F 0D00 5 02 0 00 D07 00 8CA 00020 00 BEA 00 4C4 40000") final case class HotSpotUpdateMessage(continent_guid : PlanetSideGUID, unk : Int, spots : List[HotSpotInfo] = Nil) @@ -64,6 +65,6 @@ object HotSpotUpdateMessage extends Marshallable[HotSpotUpdateMessage] { implicit val codec : Codec[HotSpotUpdateMessage] = ( ("continent_guid" | PlanetSideGUID.codec) :: ("unk" | uint4L) :: - ("spots" | listOfN(uint8L, HotSpotInfo.codec)) + ("spots" | PacketHelpers.listOfNAligned(uint8L, 4, HotSpotInfo.codec)) ).as[HotSpotUpdateMessage] } diff --git a/common/src/test/scala/GamePacketTest.scala b/common/src/test/scala/GamePacketTest.scala index 230a0d6f..e1afabaa 100644 --- a/common/src/test/scala/GamePacketTest.scala +++ b/common/src/test/scala/GamePacketTest.scala @@ -937,8 +937,8 @@ class GamePacketTest extends Specification { continent_guid mustEqual PlanetSideGUID(5) unk mustEqual 1 spots.size mustEqual 1 - spots.head.x mustEqual 3730 - spots.head.y mustEqual 1105 + spots.head.x mustEqual 2350 + spots.head.y mustEqual 1300 spots.head.scale mustEqual 128 case _ => ko @@ -951,11 +951,11 @@ class GamePacketTest extends Specification { continent_guid mustEqual PlanetSideGUID(5) unk mustEqual 5 spots.size mustEqual 2 - spots.head.x mustEqual 125 - spots.head.y mustEqual 3240 + spots.head.x mustEqual 2000 + spots.head.y mustEqual 2700 spots.head.scale mustEqual 128 - spots(1).x mustEqual 3755 - spots(1).y mustEqual 3140 + spots(1).x mustEqual 2750 + spots(1).y mustEqual 1100 spots(1).scale mustEqual 128 case _ => ko @@ -969,13 +969,13 @@ class GamePacketTest extends Specification { } "encode (one)" in { - val msg = HotSpotUpdateMessage(PlanetSideGUID(5),1, HotSpotInfo(0,3730,0,1105,128)::Nil) + val msg = HotSpotUpdateMessage(PlanetSideGUID(5),1, HotSpotInfo(0,2350,0,1300,128)::Nil) val pkt = PacketCoding.EncodePacket(msg).require.toByteVector pkt mustEqual stringOne } "encode (two)" in { - val msg = HotSpotUpdateMessage(PlanetSideGUID(5),1, HotSpotInfo(0,125,0,3240,128)::HotSpotInfo(0,3755,0,3140,128)::Nil) + val msg = HotSpotUpdateMessage(PlanetSideGUID(5),1, HotSpotInfo(0,2000,0,2700,128)::HotSpotInfo(0,2750,0,1100,128)::Nil) val pkt = PacketCoding.EncodePacket(msg).require.toByteVector pkt mustEqual stringOne } From d40f64c05387482787ed4bf731e0c73b25183ca4 Mon Sep 17 00:00:00 2001 From: FateJH Date: Thu, 22 Sep 2016 00:35:13 -0400 Subject: [PATCH 5/8] encoding and decoding of byte-aligned Lists now working --- .../scala/net/psforever/packet/PSPacket.scala | 79 +++++++++++++++++-- common/src/test/scala/GamePacketTest.scala | 4 +- 2 files changed, 75 insertions(+), 8 deletions(-) diff --git a/common/src/main/scala/net/psforever/packet/PSPacket.scala b/common/src/main/scala/net/psforever/packet/PSPacket.scala index 12d79609..2a34e6df 100644 --- a/common/src/main/scala/net/psforever/packet/PSPacket.scala +++ b/common/src/main/scala/net/psforever/packet/PSPacket.scala @@ -3,12 +3,15 @@ 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._ import shapeless._ +import scala.util.Success + /** The base of all packets */ sealed trait PlanetSidePacket extends Serializable { def encode : Attempt[BitVector] @@ -204,6 +207,17 @@ object PacketHelpers { def encodedStringWithLimit(limit : Int) : Codec[String] = variableSizeBytes(encodedStringSizeWithLimit(limit), ascii) */ + /** + * Encode and decode a byte-aligned `List`.
+ *
+ * This function is copied almost verbatim from its source, with exception of swapping the normal `ListCodec` for a new `AlignedListCodec`. + * @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[Int], alignment : Int, valueCodec: Codec[A]): Codec[List[A]] = { countCodec. flatZip { count => new AlignedListCodec(valueCodec, alignment, Some(count)) }. @@ -215,15 +229,68 @@ object PacketHelpers { } } -private final class AlignedListCodec[A](codec: Codec[A], alignment : Int, limit: Option[Int] = None) extends Codec[List[A]] { +/** + * The codec that encodes and decodes a byte-aligned `List`.
+ *
+ * This class is copied almost verbatim from its source, with only heavy modifications to its `encode` process. + * @param codec 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](codec: Codec[A], alignment : Int, limit: Option[Int] = 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. + * Performance hits for this complexity are not expected to be significant.
+ *
+ * __Warning__:
+ * A significant assumption is present in the code! + * The algorithm never confirms the bit size of the encoded size of the `List` and assumes it is equivalent to a `uint8`. + * The encoding is always split after its first eight bits. + * Obviously, if the bit size is a `uint16` or greater, the aligned encoding process will produce garbage. + * No `Exception`s will be thrown. + * @param list the `List` to be encoded + * @return the `BitVector` encoding, if successful + */ + def encode(list: List[A]) : Attempt[BitVector] = { + val solve : Attempt[BitVector] = Encoder.encodeSeq(codec)(list) + if(alignment > 0) { + solve match { + case Attempt.Successful(vector) => + return Successful(vector.take(8L) ++ BitVector.fill(alignment)(false) ++ vector.drop(8L)) + case _ => + } + } + 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` + */ + def decode(buffer: BitVector) = Decoder.decodeCollect[List, A](codec, limit)(buffer.drop(alignment)) + + /** + * The size of the encoded `List`.
+ *
+ * Unchanged from original. + * @return the size as calculated by the size of each element for each element + */ def sizeBound = limit match { case None => SizeBound.unknown case Some(lim) => codec.sizeBound * lim.toLong } - def encode(list: List[A]) = Encoder.encodeSeq(codec)(list) - - def decode(buffer: BitVector) = Decoder.decodeCollect[List, A](codec, limit)(buffer.drop(alignment)) - + /** + * Get a `String` representation of this `List`.
+ *
+ * Unchanged from original. + * @return the `String` representation + */ override def toString = s"list($codec)" } diff --git a/common/src/test/scala/GamePacketTest.scala b/common/src/test/scala/GamePacketTest.scala index e1afabaa..457791c5 100644 --- a/common/src/test/scala/GamePacketTest.scala +++ b/common/src/test/scala/GamePacketTest.scala @@ -975,9 +975,9 @@ class GamePacketTest extends Specification { } "encode (two)" in { - val msg = HotSpotUpdateMessage(PlanetSideGUID(5),1, HotSpotInfo(0,2000,0,2700,128)::HotSpotInfo(0,2750,0,1100,128)::Nil) + val msg = HotSpotUpdateMessage(PlanetSideGUID(5),5, HotSpotInfo(0,2000,0,2700,128)::HotSpotInfo(0,2750,0,1100,128)::Nil) val pkt = PacketCoding.EncodePacket(msg).require.toByteVector - pkt mustEqual stringOne + pkt mustEqual stringTwo } } From b1615aa4d97d9a55056c48f799c221a2062631ce Mon Sep 17 00:00:00 2001 From: FateJH Date: Thu, 22 Sep 2016 16:28:09 -0400 Subject: [PATCH 6/8] alternate constructor for HotSpotInfo; testing; updated commentary --- .../packet/game/HotSpotUpdateMessage.scala | 41 +++++++++++++------ common/src/test/scala/GamePacketTest.scala | 29 +++++++++++++ 2 files changed, 57 insertions(+), 13 deletions(-) diff --git a/common/src/main/scala/net/psforever/packet/game/HotSpotUpdateMessage.scala b/common/src/main/scala/net/psforever/packet/game/HotSpotUpdateMessage.scala index ec0f4f45..4c520e53 100644 --- a/common/src/main/scala/net/psforever/packet/game/HotSpotUpdateMessage.scala +++ b/common/src/main/scala/net/psforever/packet/game/HotSpotUpdateMessage.scala @@ -10,11 +10,14 @@ import scodec.codecs._ *
* The origin point is the lowest left corner of the map grid. * The coordinates of the hotspot do not match up to the map's internal coordinate system - what you learn using the `/loc` command. - * Hotspot coordinates ranges across from 0 (`000`) to 4096 (`FFF`) on both axes. - * The scale is typically set as 128 (`80000`) but can also be made smaller or even made absurdly big. - * @param unk1 na + * Hotspot coordinates range across from 0 (`000`) to 4096 (`FFF`) on both axes. + * The scale is typically set as 128 (`80000`) but can also be made smaller or even made absurdly big.
+ *
+ * Exploration:
+ * Are those really unknown values or are they just extraneous spacers between the components of the coordinates? + * @param unk1 na; always zero? * @param x the x-coord of the center of the hotspot - * @param unk2 na + * @param unk2 na; always zero? * @param y the y-coord of the center of the hotspot * @param scale how big the hotspot explosion icon appears */ @@ -25,23 +28,24 @@ final case class HotSpotInfo(unk1 : Int, scale : Int) /** - * A list of data for creating hotspots on a continental map.
+ * A list of data for creating hotspots on a continental map. + * Hotspots indicate player activity, almost always some form of combat or aggressive encounter.
*
- * The hotspot system is a forgetful all-or-nothing affair. - * The packet that is always initially sent during server login clears any would-be hotspots from the map. - * Each time a hotspot packet is received for a zone, all of the previous hotspots for that zone are forgotten. - * To simply add a hotspot, the next packet has to contain information that re-explains the packets that were originally rendered.
+ * The hotspot system is an all-or-nothing affair. + * The received packet indicates the hotspots to display and the map will display only those hotspots. + * Inversely, if the received packet indicates no hotspots, the map will display no hotspots at all. + * This "no hotspots" packet is always initially sent during zone setup during server login. + * To clear away only some hotspots, but retains others, a continental `List` would have to be pruned selectively for the client.
*
- * Exploration 1:
+ * Exploration:
* The unknown parameter has been observed with various non-zero values such as 1, 2, and 5. * Visually, however, `unk` does not affect anything. + * (Originally, I thought it might be a layering index but that is incorrect.) * Does it do something internally? * @param continent_guid the zone (continent) * @param unk na - * @param spots a List of HotSpotInfo, or `Nil` if empty + * @param spots a List of HotSpotInfo */ -// TODO need aligned/padded list support -// TODO test with sendRawResponse(hex"9F 0D00 5 02 0 00 D07 00 8CA 00020 00 BEA 00 4C4 40000") final case class HotSpotUpdateMessage(continent_guid : PlanetSideGUID, unk : Int, spots : List[HotSpotInfo] = Nil) @@ -59,6 +63,17 @@ object HotSpotInfo extends Marshallable[HotSpotInfo] { ("y" | uintL(12)) :: ("scale" | uintL(20)) }.as[HotSpotInfo] + + /** + * This alternate constructor ignores the unknown values. + * @param x the x-coord of the center of the hotspot + * @param y the y-coord of the center of the hotspot + * @param scale how big the hotspot explosion icon appears + * @return valid HotSpotInfo + */ + def apply(x : Int, y : Int, scale : Int) : HotSpotInfo = { + HotSpotInfo(0, x, 0 ,y, scale) + } } object HotSpotUpdateMessage extends Marshallable[HotSpotUpdateMessage] { diff --git a/common/src/test/scala/GamePacketTest.scala b/common/src/test/scala/GamePacketTest.scala index 457791c5..1490b5b2 100644 --- a/common/src/test/scala/GamePacketTest.scala +++ b/common/src/test/scala/GamePacketTest.scala @@ -5,6 +5,7 @@ import org.specs2.mutable._ import net.psforever.packet._ import net.psforever.packet.game._ import net.psforever.types._ +import scodec.Attempt import scodec.Attempt.Successful import scodec.bits._ @@ -915,6 +916,34 @@ class GamePacketTest extends Specification { } } + "HotSpotInfo" should { + val string = hex"00 D0 70 08 CA 80 00 00" // note: completing that last byte is required to avoid it being placed at the start of the vector + "decode" in { + HotSpotInfo.codec.decode(string.toBitVector) match { + case Attempt.Successful(decoded) => + decoded.value.x mustEqual 2000 + decoded.value.y mustEqual 2700 + decoded.value.scale mustEqual 128 + case _ => + ko + } + } + + "encode (long-hand)" in { + val msg = HotSpotInfo(0, 2000, 0, 2700, 128) + val pkt = HotSpotInfo.codec.encode(msg).require.toByteVector + + pkt mustEqual string + } + + "encode (short-hand)" in { + val msg = HotSpotInfo(2000, 2700, 128) + val pkt = HotSpotInfo.codec.encode(msg).require.toByteVector + + pkt mustEqual string + } + } + "HotSpotUpdateMessage" should { val stringClear = hex"9F 0500 1 00 0" val stringOne = hex"9F 0500 1 01 0 00 2E9 00 145 80000 0" From 29b4eaa4e4f9cc560761a4de76fe336b33c1c0ee Mon Sep 17 00:00:00 2001 From: FateJH Date: Sun, 2 Oct 2016 01:15:35 -0400 Subject: [PATCH 7/8] modification to AlignedListOfN to accomodate a variable codec for the length --- .../scala/net/psforever/packet/PSPacket.scala | 37 ++++++++----------- 1 file changed, 16 insertions(+), 21 deletions(-) diff --git a/common/src/main/scala/net/psforever/packet/PSPacket.scala b/common/src/main/scala/net/psforever/packet/PSPacket.scala index 2a34e6df..33193129 100644 --- a/common/src/main/scala/net/psforever/packet/PSPacket.scala +++ b/common/src/main/scala/net/psforever/packet/PSPacket.scala @@ -211,16 +211,16 @@ object PacketHelpers { * Encode and decode a byte-aligned `List`.
*
* This function is copied almost verbatim from its source, with exception of swapping the normal `ListCodec` for a new `AlignedListCodec`. - * @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 + * @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[Int], alignment : Int, valueCodec: Codec[A]): Codec[List[A]] = { countCodec. - flatZip { count => new AlignedListCodec(valueCodec, alignment, Some(count)) }. + 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")) @@ -233,35 +233,30 @@ object PacketHelpers { * The codec that encodes and decodes a byte-aligned `List`.
*
* This class is copied almost verbatim from its source, with only heavy modifications to its `encode` process. - * @param codec a codec that describes each of the contents of the `List` + * @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](codec: Codec[A], alignment : Int, limit: Option[Int] = None) extends Codec[List[A]] { +private class AlignedListCodec[A](countCodec : Codec[Int], valueCodec: Codec[A], alignment : Int, limit: Option[Int] = 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. - * Performance hits for this complexity are not expected to be significant.
- *
- * __Warning__:
- * A significant assumption is present in the code! - * The algorithm never confirms the bit size of the encoded size of the `List` and assumes it is equivalent to a `uint8`. - * The encoding is always split after its first eight bits. - * Obviously, if the bit size is a `uint16` or greater, the aligned encoding process will produce garbage. - * No `Exception`s will be thrown. + * Performance hits for this complexity are not expected to be significant. * @param list the `List` to be encoded * @return the `BitVector` encoding, if successful */ - def encode(list: List[A]) : Attempt[BitVector] = { - val solve : Attempt[BitVector] = Encoder.encodeSeq(codec)(list) + override def encode(list : List[A]) : Attempt[BitVector] = { + val solve : Attempt[BitVector] = Encoder.encodeSeq(valueCodec)(list) if(alignment > 0) { solve match { case Attempt.Successful(vector) => - return Successful(vector.take(8L) ++ BitVector.fill(alignment)(false) ++ vector.drop(8L)) + val countCodecSize : Long = countCodec.sizeBound.lowerBound + return Successful(vector.take(countCodecSize) ++ BitVector.fill(alignment)(false) ++ vector.drop(countCodecSize)) case _ => } } @@ -273,7 +268,7 @@ private class AlignedListCodec[A](codec: Codec[A], alignment : Int, limit: Optio * @param buffer the encoded bits in the `List`, preceded by the alignment bits * @return the decoded `List` */ - def decode(buffer: BitVector) = Decoder.decodeCollect[List, A](codec, limit)(buffer.drop(alignment)) + def decode(buffer: BitVector) = Decoder.decodeCollect[List, A](valueCodec, limit)(buffer.drop(alignment)) /** * The size of the encoded `List`.
@@ -283,7 +278,7 @@ private class AlignedListCodec[A](codec: Codec[A], alignment : Int, limit: Optio */ def sizeBound = limit match { case None => SizeBound.unknown - case Some(lim) => codec.sizeBound * lim.toLong + case Some(lim) => valueCodec.sizeBound * lim.toLong } /** @@ -292,5 +287,5 @@ private class AlignedListCodec[A](codec: Codec[A], alignment : Int, limit: Optio * Unchanged from original. * @return the `String` representation */ - override def toString = s"list($codec)" + override def toString = s"list($valueCodec)" } From a3ef754da3000a9c57b061b358e2aafb985937df Mon Sep 17 00:00:00 2001 From: FateJH Date: Mon, 9 Jan 2017 21:05:31 -0500 Subject: [PATCH 8/8] changing coordinate and scaling systems for spots and fixing tests --- .../scala/net/psforever/packet/PSPacket.scala | 104 +++++++++++++----- .../packet/game/HotSpotUpdateMessage.scala | 56 ++++------ common/src/test/scala/GamePacketTest.scala | 39 +++---- 3 files changed, 112 insertions(+), 87 deletions(-) diff --git a/common/src/main/scala/net/psforever/packet/PSPacket.scala b/common/src/main/scala/net/psforever/packet/PSPacket.scala index 33193129..8942a681 100644 --- a/common/src/main/scala/net/psforever/packet/PSPacket.scala +++ b/common/src/main/scala/net/psforever/packet/PSPacket.scala @@ -207,32 +207,80 @@ object PacketHelpers { def encodedStringWithLimit(limit : Int) : Codec[String] = variableSizeBytes(encodedStringSizeWithLimit(limit), ascii) */ + /** + * 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 non-negative before further processing. + * 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 + * @return a codec that works on a List of A but excludes the size from the encoding + */ + 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, with exception of swapping the normal `ListCodec` for a new `AlignedListCodec`. + * 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 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[Int], alignment : Int, valueCodec: Codec[A]): Codec[List[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)). + 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 codec that encodes and decodes a byte-aligned `List`.
+ * The greater `Codec` class that encodes and decodes a byte-aligned `List`.
*
- * This class is copied almost verbatim from its source, with only heavy modifications to its `encode` process. + * 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) @@ -240,24 +288,24 @@ object PacketHelpers { * @tparam A the type of the `List` contents * @see ListCodec.scala */ -private class AlignedListCodec[A](countCodec : Codec[Int], valueCodec: Codec[A], alignment : Int, limit: Option[Int] = None) extends Codec[List[A]] { +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. - * Performance hits for this complexity are not expected to be significant. * @param list the `List` to be encoded * @return the `BitVector` encoding, if successful */ override def encode(list : List[A]) : Attempt[BitVector] = { - val solve : Attempt[BitVector] = Encoder.encodeSeq(valueCodec)(list) + var solve : Attempt[BitVector] = Encoder.encodeSeq(valueCodec)(list) if(alignment > 0) { solve match { case Attempt.Successful(vector) => val countCodecSize : Long = countCodec.sizeBound.lowerBound - return Successful(vector.take(countCodecSize) ++ BitVector.fill(alignment)(false) ++ vector.drop(countCodecSize)) + solve = Attempt.successful(vector.take(countCodecSize) ++ BitVector.fill(alignment)(false) ++ vector.drop(countCodecSize)) case _ => + solve = Attempt.failure(Err("failed to create a list")) } } solve @@ -268,22 +316,24 @@ private class AlignedListCodec[A](countCodec : Codec[Int], valueCodec: Codec[A], * @param buffer the encoded bits in the `List`, preceded by the alignment bits * @return the decoded `List` */ - def decode(buffer: BitVector) = Decoder.decodeCollect[List, A](valueCodec, limit)(buffer.drop(alignment)) - - /** - * The size of the encoded `List`.
- *
- * Unchanged from original. - * @return the size as calculated by the size of each element for each element - */ - def sizeBound = limit match { - case None => SizeBound.unknown - case Some(lim) => valueCodec.sizeBound * lim.toLong + 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)) } /** - * Get a `String` representation of this `List`.
- *
+ * 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 */ diff --git a/common/src/main/scala/net/psforever/packet/game/HotSpotUpdateMessage.scala b/common/src/main/scala/net/psforever/packet/game/HotSpotUpdateMessage.scala index 4c520e53..d475a001 100644 --- a/common/src/main/scala/net/psforever/packet/game/HotSpotUpdateMessage.scala +++ b/common/src/main/scala/net/psforever/packet/game/HotSpotUpdateMessage.scala @@ -1,6 +1,7 @@ // Copyright (c) 2016 PSForever.net to present package net.psforever.packet.game +import net.psforever.newcodecs.newcodecs import net.psforever.packet.{GamePacketOpcode, Marshallable, PacketHelpers, PlanetSideGamePacket} import scodec.Codec import scodec.codecs._ @@ -9,23 +10,15 @@ import scodec.codecs._ * Information for positioning a hotspot on the continental map.
*
* The origin point is the lowest left corner of the map grid. - * The coordinates of the hotspot do not match up to the map's internal coordinate system - what you learn using the `/loc` command. - * Hotspot coordinates range across from 0 (`000`) to 4096 (`FFF`) on both axes. - * The scale is typically set as 128 (`80000`) but can also be made smaller or even made absurdly big.
- *
- * Exploration:
- * Are those really unknown values or are they just extraneous spacers between the components of the coordinates? - * @param unk1 na; always zero? + * The coordinates of the hotspot do necessarily match up to the map's internal coordinate system - what you learn using the `/loc` command. + * Instead, all maps use a 0 - 8192 coordinate overlay. * @param x the x-coord of the center of the hotspot - * @param unk2 na; always zero? * @param y the y-coord of the center of the hotspot * @param scale how big the hotspot explosion icon appears */ -final case class HotSpotInfo(unk1 : Int, - x : Int, - unk2 : Int, - y : Int, - scale : Int) +final case class HotSpotInfo(x : Float, + y : Float, + scale : Float) /** * A list of data for creating hotspots on a continental map. @@ -38,16 +31,13 @@ final case class HotSpotInfo(unk1 : Int, * To clear away only some hotspots, but retains others, a continental `List` would have to be pruned selectively for the client.
*
* Exploration:
- * The unknown parameter has been observed with various non-zero values such as 1, 2, and 5. - * Visually, however, `unk` does not affect anything. - * (Originally, I thought it might be a layering index but that is incorrect.) - * Does it do something internally? + * What does (zone) priority entail? * @param continent_guid the zone (continent) - * @param unk na + * @param priority na * @param spots a List of HotSpotInfo */ final case class HotSpotUpdateMessage(continent_guid : PlanetSideGUID, - unk : Int, + priority : Int, spots : List[HotSpotInfo] = Nil) extends PlanetSideGamePacket { type Packet = HotSpotUpdateMessage @@ -56,30 +46,22 @@ final case class HotSpotUpdateMessage(continent_guid : PlanetSideGUID, } object HotSpotInfo extends Marshallable[HotSpotInfo] { + /* + the scale is technically not "correct" + the client is looking for a normal 0-8192 value + we are trying to enforce a more modest graphic scale at 128.0f + */ implicit val codec : Codec[HotSpotInfo] = { - ("unk1" | uint8L) :: - ("x" | uintL(12)) :: - ("unk2" | uint8L) :: - ("y" | uintL(12)) :: - ("scale" | uintL(20)) + ("x" | newcodecs.q_float(0.0, 8192.0, 20)) :: + ("y" | newcodecs.q_float(0.0, 8192.0, 20)) :: + ("scale" | newcodecs.q_float(0.0, 524288.0, 20)) }.as[HotSpotInfo] - - /** - * This alternate constructor ignores the unknown values. - * @param x the x-coord of the center of the hotspot - * @param y the y-coord of the center of the hotspot - * @param scale how big the hotspot explosion icon appears - * @return valid HotSpotInfo - */ - def apply(x : Int, y : Int, scale : Int) : HotSpotInfo = { - HotSpotInfo(0, x, 0 ,y, scale) - } } object HotSpotUpdateMessage extends Marshallable[HotSpotUpdateMessage] { implicit val codec : Codec[HotSpotUpdateMessage] = ( ("continent_guid" | PlanetSideGUID.codec) :: - ("unk" | uint4L) :: - ("spots" | PacketHelpers.listOfNAligned(uint8L, 4, HotSpotInfo.codec)) + ("priority" | uint4L) :: + ("spots" | PacketHelpers.listOfNAligned(longL(8), 4, HotSpotInfo.codec)) ).as[HotSpotUpdateMessage] } diff --git a/common/src/test/scala/GamePacketTest.scala b/common/src/test/scala/GamePacketTest.scala index 1490b5b2..6369c876 100644 --- a/common/src/test/scala/GamePacketTest.scala +++ b/common/src/test/scala/GamePacketTest.scala @@ -921,23 +921,16 @@ class GamePacketTest extends Specification { "decode" in { HotSpotInfo.codec.decode(string.toBitVector) match { case Attempt.Successful(decoded) => - decoded.value.x mustEqual 2000 - decoded.value.y mustEqual 2700 - decoded.value.scale mustEqual 128 + decoded.value.x mustEqual 4000.0f + decoded.value.y mustEqual 5400.0f + decoded.value.scale mustEqual 64.0f case _ => ko } } - "encode (long-hand)" in { - val msg = HotSpotInfo(0, 2000, 0, 2700, 128) - val pkt = HotSpotInfo.codec.encode(msg).require.toByteVector - - pkt mustEqual string - } - - "encode (short-hand)" in { - val msg = HotSpotInfo(2000, 2700, 128) + "encode" in { + val msg = HotSpotInfo(4000.0f, 5400.0f, 64.0f) val pkt = HotSpotInfo.codec.encode(msg).require.toByteVector pkt mustEqual string @@ -966,9 +959,9 @@ class GamePacketTest extends Specification { continent_guid mustEqual PlanetSideGUID(5) unk mustEqual 1 spots.size mustEqual 1 - spots.head.x mustEqual 2350 - spots.head.y mustEqual 1300 - spots.head.scale mustEqual 128 + spots.head.x mustEqual 4700.0f + spots.head.y mustEqual 2600.0f + spots.head.scale mustEqual 64.0f case _ => ko } @@ -980,12 +973,12 @@ class GamePacketTest extends Specification { continent_guid mustEqual PlanetSideGUID(5) unk mustEqual 5 spots.size mustEqual 2 - spots.head.x mustEqual 2000 - spots.head.y mustEqual 2700 - spots.head.scale mustEqual 128 - spots(1).x mustEqual 2750 - spots(1).y mustEqual 1100 - spots(1).scale mustEqual 128 + spots.head.x mustEqual 4000.0f + spots.head.y mustEqual 5400.0f + spots.head.scale mustEqual 64.0f + spots(1).x mustEqual 5500.0f + spots(1).y mustEqual 2200.0f + spots(1).scale mustEqual 64.0f case _ => ko } @@ -998,13 +991,13 @@ class GamePacketTest extends Specification { } "encode (one)" in { - val msg = HotSpotUpdateMessage(PlanetSideGUID(5),1, HotSpotInfo(0,2350,0,1300,128)::Nil) + val msg = HotSpotUpdateMessage(PlanetSideGUID(5),1, HotSpotInfo(4700.0f, 2600.0f, 64.0f)::Nil) val pkt = PacketCoding.EncodePacket(msg).require.toByteVector pkt mustEqual stringOne } "encode (two)" in { - val msg = HotSpotUpdateMessage(PlanetSideGUID(5),5, HotSpotInfo(0,2000,0,2700,128)::HotSpotInfo(0,2750,0,1100,128)::Nil) + val msg = HotSpotUpdateMessage(PlanetSideGUID(5),5, HotSpotInfo(4000.0f, 5400.0f, 64.0f)::HotSpotInfo(5500.0f, 2200.0f, 64.0f)::Nil) val pkt = PacketCoding.EncodePacket(msg).require.toByteVector pkt mustEqual stringTwo }