diff --git a/common/src/main/scala/net/psforever/packet/PSPacket.scala b/common/src/main/scala/net/psforever/packet/PSPacket.scala
index 331931292..8942a681e 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 4c520e539..d475a001e 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 1490b5b2d..6369c8762 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
}