mirror of
https://github.com/2revoemag/PSF-BotServer.git
synced 2026-07-15 16:34:39 +00:00
Merge branch 'hotspots'
This commit is contained in:
commit
54de292506
4 changed files with 292 additions and 2 deletions
|
|
@ -506,7 +506,7 @@ object GamePacketOpcode extends Enumeration {
|
||||||
case 0x9c => noDecoder(DebugDrawMessage)
|
case 0x9c => noDecoder(DebugDrawMessage)
|
||||||
case 0x9d => noDecoder(SoulMarkMessage)
|
case 0x9d => noDecoder(SoulMarkMessage)
|
||||||
case 0x9e => noDecoder(UplinkPositionEvent)
|
case 0x9e => noDecoder(UplinkPositionEvent)
|
||||||
case 0x9f => noDecoder(HotSpotUpdateMessage)
|
case 0x9f => game.HotSpotUpdateMessage.decode
|
||||||
|
|
||||||
// OPCODES 0xa0-af
|
// OPCODES 0xa0-af
|
||||||
case 0xa0 => game.BuildingInfoUpdateMessage.decode
|
case 0xa0 => game.BuildingInfoUpdateMessage.decode
|
||||||
|
|
|
||||||
|
|
@ -3,12 +3,15 @@ package net.psforever.packet
|
||||||
|
|
||||||
import java.nio.charset.Charset
|
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.bits._
|
||||||
import scodec.codecs._
|
import scodec.codecs._
|
||||||
import scodec._
|
import scodec._
|
||||||
import shapeless._
|
import shapeless._
|
||||||
|
|
||||||
|
import scala.util.Success
|
||||||
|
|
||||||
/** The base of all packets */
|
/** The base of all packets */
|
||||||
sealed trait PlanetSidePacket extends Serializable {
|
sealed trait PlanetSidePacket extends Serializable {
|
||||||
def encode : Attempt[BitVector]
|
def encode : Attempt[BitVector]
|
||||||
|
|
@ -203,4 +206,136 @@ 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>
|
||||||
|
* <br>
|
||||||
|
* This function is copied almost verbatim from its source, but swapping the normal `ListCodec` for a new `AlignedListCodec`.
|
||||||
|
* It also changes the type of the list length `Codec` from `Int` to `Long`.
|
||||||
|
* Due to type erasure, this method can not be overloaded for both `Codec[Int]` and `Codec[Long]`.
|
||||||
|
* The compiler would resolve both internally into type `Codec[T]` and their function definitions would be identical.
|
||||||
|
* For the purposes of use, `longL(n)` will cast to an `Int` for the same acceptable values of `n` as in `uintL(n)`.
|
||||||
|
* @param countCodec the codec that represents the prefixed size of the `List`
|
||||||
|
* @param alignment the number of bits padded between the `List` size and the `List` contents
|
||||||
|
* @param valueCodec a codec that describes each of the contents of the `List`
|
||||||
|
* @tparam A the type of the `List` contents
|
||||||
|
* @see codec\package.scala, listOfN
|
||||||
|
* @return a codec that works on a List of A
|
||||||
|
*/
|
||||||
|
def listOfNAligned[A](countCodec : Codec[Long], alignment : Int, valueCodec : Codec[A]) : Codec[List[A]] = {
|
||||||
|
countCodec.
|
||||||
|
flatZip {
|
||||||
|
count =>
|
||||||
|
new AlignedListCodec(countCodec, valueCodec, alignment, Some(count))
|
||||||
|
}.
|
||||||
|
narrow[List[A]] (
|
||||||
|
{
|
||||||
|
case (cnt, xs) =>
|
||||||
|
if(xs.size == cnt)
|
||||||
|
Attempt.successful(xs)
|
||||||
|
else
|
||||||
|
Attempt.failure(Err(s"Insufficient number of elements: decoded ${xs.size} but should have decoded $cnt"))
|
||||||
|
},
|
||||||
|
{
|
||||||
|
xs =>
|
||||||
|
(xs.size, xs)
|
||||||
|
}
|
||||||
|
).
|
||||||
|
withToString(s"listOfN($countCodec, $valueCodec)")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* The greater `Codec` class that encodes and decodes a byte-aligned `List`.<br>
|
||||||
|
* <br>
|
||||||
|
* This class is copied almost verbatim from its source, with two major modifications.
|
||||||
|
* First, heavy modifications to its `encode` process account for the alignment value.
|
||||||
|
* Second, the length field is parsed as a `Codec[Long]` value and type conversion is accounted for at several points.
|
||||||
|
* @param countCodec the codec that represents the prefixed size of the `List`
|
||||||
|
* @param valueCodec a codec that describes each of the contents of the `List`
|
||||||
|
* @param alignment the number of bits padded between the `List` size and the `List` contents (on successful)
|
||||||
|
* @param limit the number of elements in the `List`
|
||||||
|
* @tparam A the type of the `List` contents
|
||||||
|
* @see ListCodec.scala
|
||||||
|
*/
|
||||||
|
private class AlignedListCodec[A](countCodec : Codec[Long], valueCodec: Codec[A], alignment : Int, limit: Option[Long] = None) extends Codec[List[A]] {
|
||||||
|
/**
|
||||||
|
* Convert a `List` of elements into a byte-aligned `BitVector`.<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.
|
||||||
|
* @param list the `List` to be encoded
|
||||||
|
* @return the `BitVector` encoding, if successful
|
||||||
|
*/
|
||||||
|
override def encode(list : List[A]) : Attempt[BitVector] = {
|
||||||
|
var solve : Attempt[BitVector] = Encoder.encodeSeq(valueCodec)(list)
|
||||||
|
if(alignment > 0) {
|
||||||
|
solve match {
|
||||||
|
case Attempt.Successful(vector) =>
|
||||||
|
val countCodecSize : Long = countCodec.sizeBound.lowerBound
|
||||||
|
solve = Attempt.successful(vector.take(countCodecSize) ++ BitVector.fill(alignment)(false) ++ vector.drop(countCodecSize))
|
||||||
|
case _ =>
|
||||||
|
solve = Attempt.failure(Err("failed to create a list"))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
solve
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Convert a byte-aligned `BitVector` into a `List` of elements.
|
||||||
|
* @param buffer the encoded bits in the `List`, preceded by the alignment bits
|
||||||
|
* @return the decoded `List`
|
||||||
|
*/
|
||||||
|
override def decode(buffer: BitVector) = {
|
||||||
|
val lim = Option( if(limit.isDefined) limit.get.asInstanceOf[Int] else 0 ) //TODO potentially unsafe size conversion
|
||||||
|
Decoder.decodeCollect[List, A](valueCodec, lim)(buffer.drop(alignment))
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* The size of the encoded `List`.
|
||||||
|
* @return the size as calculated by the size of each element for each element
|
||||||
|
*/
|
||||||
|
override def sizeBound = limit match {
|
||||||
|
case None =>
|
||||||
|
SizeBound.unknown
|
||||||
|
case Some(lim : Long) =>
|
||||||
|
valueCodec.sizeBound * lim
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Get a `String` representation of this `List`.
|
||||||
|
* Unchanged from original.
|
||||||
|
* @return the `String` representation
|
||||||
|
*/
|
||||||
|
override def toString = s"list($valueCodec)"
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -0,0 +1,67 @@
|
||||||
|
// 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._
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 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 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 y the y-coord of the center of the hotspot
|
||||||
|
* @param scale how big the hotspot explosion icon appears
|
||||||
|
*/
|
||||||
|
final case class HotSpotInfo(x : Float,
|
||||||
|
y : Float,
|
||||||
|
scale : Float)
|
||||||
|
|
||||||
|
/**
|
||||||
|
* A list of data for creating hotspots on a continental map.
|
||||||
|
* Hotspots indicate player activity, almost always some form of combat or aggressive encounter.<br>
|
||||||
|
* <br>
|
||||||
|
* 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.<br>
|
||||||
|
* <br>
|
||||||
|
* Exploration:<br>
|
||||||
|
* What does (zone) priority entail?
|
||||||
|
* @param continent_guid the zone (continent)
|
||||||
|
* @param priority na
|
||||||
|
* @param spots a List of HotSpotInfo
|
||||||
|
*/
|
||||||
|
final case class HotSpotUpdateMessage(continent_guid : PlanetSideGUID,
|
||||||
|
priority : Int,
|
||||||
|
spots : List[HotSpotInfo] = Nil)
|
||||||
|
extends PlanetSideGamePacket {
|
||||||
|
type Packet = HotSpotUpdateMessage
|
||||||
|
def opcode = GamePacketOpcode.HotSpotUpdateMessage
|
||||||
|
def encode = HotSpotUpdateMessage.encode(this)
|
||||||
|
}
|
||||||
|
|
||||||
|
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] = {
|
||||||
|
("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]
|
||||||
|
}
|
||||||
|
|
||||||
|
object HotSpotUpdateMessage extends Marshallable[HotSpotUpdateMessage] {
|
||||||
|
implicit val codec : Codec[HotSpotUpdateMessage] = (
|
||||||
|
("continent_guid" | PlanetSideGUID.codec) ::
|
||||||
|
("priority" | uint4L) ::
|
||||||
|
("spots" | PacketHelpers.listOfNAligned(longL(8), 4, HotSpotInfo.codec))
|
||||||
|
).as[HotSpotUpdateMessage]
|
||||||
|
}
|
||||||
|
|
@ -5,6 +5,7 @@ import org.specs2.mutable._
|
||||||
import net.psforever.packet._
|
import net.psforever.packet._
|
||||||
import net.psforever.packet.game._
|
import net.psforever.packet.game._
|
||||||
import net.psforever.types._
|
import net.psforever.types._
|
||||||
|
import scodec.Attempt
|
||||||
import scodec.Attempt.Successful
|
import scodec.Attempt.Successful
|
||||||
import scodec.bits._
|
import scodec.bits._
|
||||||
|
|
||||||
|
|
@ -915,6 +916,93 @@ 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 4000.0f
|
||||||
|
decoded.value.y mustEqual 5400.0f
|
||||||
|
decoded.value.scale mustEqual 64.0f
|
||||||
|
case _ =>
|
||||||
|
ko
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
"encode" in {
|
||||||
|
val msg = HotSpotInfo(4000.0f, 5400.0f, 64.0f)
|
||||||
|
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"
|
||||||
|
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 {
|
||||||
|
case HotSpotUpdateMessage(continent_guid, unk, spots) =>
|
||||||
|
continent_guid mustEqual PlanetSideGUID(5)
|
||||||
|
unk mustEqual 1
|
||||||
|
spots.size mustEqual 0
|
||||||
|
case _ =>
|
||||||
|
ko
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
"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 4700.0f
|
||||||
|
spots.head.y mustEqual 2600.0f
|
||||||
|
spots.head.scale mustEqual 64.0f
|
||||||
|
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 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
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
"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(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(4000.0f, 5400.0f, 64.0f)::HotSpotInfo(5500.0f, 2200.0f, 64.0f)::Nil)
|
||||||
|
val pkt = PacketCoding.EncodePacket(msg).require.toByteVector
|
||||||
|
pkt mustEqual stringTwo
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
"BuildingInfoUpdateMessage" should {
|
"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"
|
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"
|
||||||
|
|
||||||
|
|
|
||||||
Loading…
Add table
Add a link
Reference in a new issue