changing coordinate and scaling systems for spots and fixing tests

This commit is contained in:
FateJH 2017-01-09 21:05:31 -05:00
parent 29b4eaa4e4
commit a3ef754da3
3 changed files with 112 additions and 87 deletions

View file

@ -207,32 +207,80 @@ object PacketHelpers {
def encodedStringWithLimit(limit : Int) : Codec[String] = variableSizeBytes(encodedStringSizeWithLimit(limit), ascii) 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.<br>
* <br>
* 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.<br>
* <br>
* 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`.<br> * Encode and decode a byte-aligned `List`.<br>
* <br> * <br>
* 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 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` * @param valueCodec a codec that describes each of the contents of the `List`
* @tparam A the type of the `List` contents * @tparam A the type of the `List` contents
* @see codec\package.scala, listOfN * @see codec\package.scala, listOfN
* @return a codec that works on a List of A * @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. countCodec.
flatZip { count => new AlignedListCodec(countCodec, valueCodec, alignment, Some(count)) }. flatZip {
narrow[List[A]]({ case (cnt, xs) => count =>
if (xs.size == cnt) Attempt.successful(xs) new AlignedListCodec(countCodec, valueCodec, alignment, Some(count))
else Attempt.failure(Err(s"Insufficient number of elements: decoded ${xs.size} but should have decoded $cnt")) }.
}, xs => (xs.size, xs)). 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)") withToString(s"listOfN($countCodec, $valueCodec)")
} }
} }
/** /**
* The codec that encodes and decodes a byte-aligned `List`.<br> * The greater `Codec` class that encodes and decodes a byte-aligned `List`.<br>
* <br> * <br>
* 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 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 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 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 * @tparam A the type of the `List` contents
* @see ListCodec.scala * @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`.<br> * Convert a `List` of elements into a byte-aligned `BitVector`.<br>
* <br> * <br>
* 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. * 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. * 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 * @param list the `List` to be encoded
* @return the `BitVector` encoding, if successful * @return the `BitVector` encoding, if successful
*/ */
override def encode(list : List[A]) : Attempt[BitVector] = { 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) { if(alignment > 0) {
solve match { solve match {
case Attempt.Successful(vector) => case Attempt.Successful(vector) =>
val countCodecSize : Long = countCodec.sizeBound.lowerBound 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 _ => case _ =>
solve = Attempt.failure(Err("failed to create a list"))
} }
} }
solve 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 * @param buffer the encoded bits in the `List`, preceded by the alignment bits
* @return the decoded `List` * @return the decoded `List`
*/ */
def decode(buffer: BitVector) = Decoder.decodeCollect[List, A](valueCodec, limit)(buffer.drop(alignment)) 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`.<br>
* <br>
* 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
} }
/** /**
* Get a `String` representation of this `List`.<br> * The size of the encoded `List`.
* <br> * @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. * Unchanged from original.
* @return the `String` representation * @return the `String` representation
*/ */

View file

@ -1,6 +1,7 @@
// Copyright (c) 2016 PSForever.net to present // Copyright (c) 2016 PSForever.net to present
package net.psforever.packet.game package net.psforever.packet.game
import net.psforever.newcodecs.newcodecs
import net.psforever.packet.{GamePacketOpcode, Marshallable, PacketHelpers, PlanetSideGamePacket} import net.psforever.packet.{GamePacketOpcode, Marshallable, PacketHelpers, PlanetSideGamePacket}
import scodec.Codec import scodec.Codec
import scodec.codecs._ import scodec.codecs._
@ -9,23 +10,15 @@ import scodec.codecs._
* Information for positioning a hotspot on the continental map.<br> * Information for positioning a hotspot on the continental map.<br>
* <br> * <br>
* The origin point is the lowest left corner of the map grid. * 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. * The coordinates of the hotspot do necessarily 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. * Instead, all maps use a 0 - 8192 coordinate overlay.
* The scale is typically set as 128 (`80000`) but can also be made smaller or even made absurdly big.<br>
* <br>
* Exploration:<br>
* 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 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 y the y-coord of the center of the hotspot
* @param scale how big the hotspot explosion icon appears * @param scale how big the hotspot explosion icon appears
*/ */
final case class HotSpotInfo(unk1 : Int, final case class HotSpotInfo(x : Float,
x : Int, y : Float,
unk2 : Int, scale : Float)
y : 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.
@ -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.<br> * To clear away only some hotspots, but retains others, a continental `List` would have to be pruned selectively for the client.<br>
* <br> * <br>
* Exploration:<br> * Exploration:<br>
* The unknown parameter has been observed with various non-zero values such as 1, 2, and 5. * What does (zone) priority entail?
* 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 continent_guid the zone (continent)
* @param unk na * @param priority na
* @param spots a List of HotSpotInfo * @param spots a List of HotSpotInfo
*/ */
final case class HotSpotUpdateMessage(continent_guid : PlanetSideGUID, final case class HotSpotUpdateMessage(continent_guid : PlanetSideGUID,
unk : Int, priority : Int,
spots : List[HotSpotInfo] = Nil) spots : List[HotSpotInfo] = Nil)
extends PlanetSideGamePacket { extends PlanetSideGamePacket {
type Packet = HotSpotUpdateMessage type Packet = HotSpotUpdateMessage
@ -56,30 +46,22 @@ final case class HotSpotUpdateMessage(continent_guid : PlanetSideGUID,
} }
object HotSpotInfo extends Marshallable[HotSpotInfo] { 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] = { implicit val codec : Codec[HotSpotInfo] = {
("unk1" | uint8L) :: ("x" | newcodecs.q_float(0.0, 8192.0, 20)) ::
("x" | uintL(12)) :: ("y" | newcodecs.q_float(0.0, 8192.0, 20)) ::
("unk2" | uint8L) :: ("scale" | newcodecs.q_float(0.0, 524288.0, 20))
("y" | uintL(12)) ::
("scale" | uintL(20))
}.as[HotSpotInfo] }.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] { object HotSpotUpdateMessage extends Marshallable[HotSpotUpdateMessage] {
implicit val codec : Codec[HotSpotUpdateMessage] = ( implicit val codec : Codec[HotSpotUpdateMessage] = (
("continent_guid" | PlanetSideGUID.codec) :: ("continent_guid" | PlanetSideGUID.codec) ::
("unk" | uint4L) :: ("priority" | uint4L) ::
("spots" | PacketHelpers.listOfNAligned(uint8L, 4, HotSpotInfo.codec)) ("spots" | PacketHelpers.listOfNAligned(longL(8), 4, HotSpotInfo.codec))
).as[HotSpotUpdateMessage] ).as[HotSpotUpdateMessage]
} }

View file

@ -921,23 +921,16 @@ class GamePacketTest extends Specification {
"decode" in { "decode" in {
HotSpotInfo.codec.decode(string.toBitVector) match { HotSpotInfo.codec.decode(string.toBitVector) match {
case Attempt.Successful(decoded) => case Attempt.Successful(decoded) =>
decoded.value.x mustEqual 2000 decoded.value.x mustEqual 4000.0f
decoded.value.y mustEqual 2700 decoded.value.y mustEqual 5400.0f
decoded.value.scale mustEqual 128 decoded.value.scale mustEqual 64.0f
case _ => case _ =>
ko ko
} }
} }
"encode (long-hand)" in { "encode" in {
val msg = HotSpotInfo(0, 2000, 0, 2700, 128) val msg = HotSpotInfo(4000.0f, 5400.0f, 64.0f)
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 val pkt = HotSpotInfo.codec.encode(msg).require.toByteVector
pkt mustEqual string pkt mustEqual string
@ -966,9 +959,9 @@ class GamePacketTest extends Specification {
continent_guid mustEqual PlanetSideGUID(5) continent_guid mustEqual PlanetSideGUID(5)
unk mustEqual 1 unk mustEqual 1
spots.size mustEqual 1 spots.size mustEqual 1
spots.head.x mustEqual 2350 spots.head.x mustEqual 4700.0f
spots.head.y mustEqual 1300 spots.head.y mustEqual 2600.0f
spots.head.scale mustEqual 128 spots.head.scale mustEqual 64.0f
case _ => case _ =>
ko ko
} }
@ -980,12 +973,12 @@ class GamePacketTest extends Specification {
continent_guid mustEqual PlanetSideGUID(5) continent_guid mustEqual PlanetSideGUID(5)
unk mustEqual 5 unk mustEqual 5
spots.size mustEqual 2 spots.size mustEqual 2
spots.head.x mustEqual 2000 spots.head.x mustEqual 4000.0f
spots.head.y mustEqual 2700 spots.head.y mustEqual 5400.0f
spots.head.scale mustEqual 128 spots.head.scale mustEqual 64.0f
spots(1).x mustEqual 2750 spots(1).x mustEqual 5500.0f
spots(1).y mustEqual 1100 spots(1).y mustEqual 2200.0f
spots(1).scale mustEqual 128 spots(1).scale mustEqual 64.0f
case _ => case _ =>
ko ko
} }
@ -998,13 +991,13 @@ class GamePacketTest extends Specification {
} }
"encode (one)" in { "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 val pkt = PacketCoding.EncodePacket(msg).require.toByteVector
pkt mustEqual stringOne pkt mustEqual stringOne
} }
"encode (two)" in { "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 val pkt = PacketCoding.EncodePacket(msg).require.toByteVector
pkt mustEqual stringTwo pkt mustEqual stringTwo
} }