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)
*/
/**
* 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>
* <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 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`.<br>
* The greater `Codec` class that encodes and decodes a byte-aligned `List`.<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 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`.<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.
* 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`.<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
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`.<br>
* <br>
* 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
*/

View file

@ -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.<br>
* <br>
* 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.<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?
* 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.<br>
* <br>
* Exploration:<br>
* 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]
}

View file

@ -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
}