From 4862b1b5448d50ccd45083f3414bb9fb77db81bf Mon Sep 17 00:00:00 2001 From: FateJH Date: Sat, 7 Jan 2017 20:39:32 -0500 Subject: [PATCH] imported PlayerStateMessage packet from volatile branch --- .../packet/game/PlayerStateMessage.scala | 290 +++++++++++++++--- 1 file changed, 254 insertions(+), 36 deletions(-) diff --git a/common/src/main/scala/net/psforever/packet/game/PlayerStateMessage.scala b/common/src/main/scala/net/psforever/packet/game/PlayerStateMessage.scala index 38c428546..3d32307ea 100644 --- a/common/src/main/scala/net/psforever/packet/game/PlayerStateMessage.scala +++ b/common/src/main/scala/net/psforever/packet/game/PlayerStateMessage.scala @@ -8,47 +8,73 @@ import scodec.Codec import scodec.codecs._ import shapeless.{::, HNil} +import scala.collection.mutable + /** - * na + * The server instructs clients to render a certain avatar not operated by its player to move in a certain way.
+ *
+ * The avatar model normally moves from where it currently is to `pos`. + * When `vel` is defined, `pos` is treated as where the avatar model starts its animation; + * and, from there, it moves a certain distance as according to the values. + * The repositioning always takes the same amount of time and the player model is left in running animation (in place). + * The coordinates evaluate between -256.0 and 256.0.
+ *
+ * facingYaw:
+ * `0x00` -- E
+ * `0x10` -- NE
+ * `0x20` -- N
+ * `0x30` -- NW
+ * `0x40` -- W
+ * `0x50` -- SW
+ * `0x60` -- S
+ * `0x70` -- SE
+ * `0x80` -- E
+ *
+ * facingPitch:
+ * `0x00`-`0x20` -- downwards-facing angles, with `0x00` as forwards-facing
+ * `0x21`-`0x40` -- downwards-facing
+ * `0x41`-`0x59` -- upwards-facing
+ * `0x60`-`0x80` -- upwards-facing angles, with `0x80` as forwards-facing
+ *
+ * facingYawUpper:
+ * `0x00`-`0x20` -- turning to left, with `0x00` being forward-facing
+ * `0x21`-`0x40` -- facing leftwards
+ * `0x41`-`0x59` -- facing rightwards
+ * `0x60`-`0x80` -- turning to right, with `0x80` being forward-facing + * * @param guid the avatar's guid * @param pos the position of the avatar in the world environment (in three coordinates) + * @param vel an optional velocity * @param facingYaw the angle with respect to the horizon towards which the avatar is looking; - * every `0x01` is 5.625 degrees counter clockwise from North; - * every `0x10` is 90-degrees; - * it wraps to North every `0x40` + * every `0x1` is about 2.8125 degrees; + * measurements are counter-clockwise from East * @param facingPitch the angle with respect to the sky and the ground towards which the avatar is looking; - * every '0x01' is about 5.625 degrees; - * `0x00` to `0x10` are downwards-facing angles, with `0x00` as forwards-facing; - * nothing from `0x11` to `0x29`; - * `0x30` to `0x40` are upwards-facing angles, with `0x30` starting at full-up; - * starting at `0x40` == `0x00` this system repeats + * every `0x1` is about 2.8125 degrees * @param facingYawUpper the angle of the avatar's upper body with respect to its forward-facing direction; - * `0x00` to `0x10` are the avatar turning to its left, with `0x00` being forward-facing; - * nothing from `0x11` to `0x29`; - * `0x30` to `0x40` are the avatar turning to its right, with `0x40` being forward-facing; - * starting at `0x40` == `0x00` this system repeats - * @param unk4 na - * @param more activate parsing for the following four fields - * @param unk5 na + * every `0x1` is about 2.8125 degrees + * @param unk1 na + * @param fourBools set to `false` to parse the following four fields, otherwise those values will be ignored * @param isCrouching avatar is crouching; - * must remain flagged for crouch to maintain animation; - * turn off to stand up + * must remain flagged for crouch to maintain animation; + * turn off to stand up * @param isJumping avatar is jumping; * must remain flagged for jump to maintain animation; - * turn off when landed - * @param unk8 na + * turn off to land(?) + * @param unk2 na + * @param unk3 na */ final case class PlayerStateMessage(guid : PlanetSideGUID, pos : Vector3, + vel : Option[Vector3], facingYaw : Int, facingPitch : Int, facingYawUpper : Int, - unk4 : Int, - more : Boolean, - unk5 : Boolean = false, + unk1 : Int, + fourBools : Boolean, isCrouching : Boolean = false, isJumping : Boolean = false, - unk8 : Boolean = false) + unk2 : Boolean = false, + unk3 : Boolean = false) extends PlanetSideGamePacket { type Packet = PlayerStateMessage def opcode = GamePacketOpcode.PlayerStateMessage @@ -58,13 +84,19 @@ final case class PlayerStateMessage(guid : PlanetSideGUID, object PlayerStateMessage extends Marshallable[PlayerStateMessage] { type fourBoolPattern = Boolean :: Boolean :: Boolean :: Boolean :: HNil + /** + * A `Codec` for reading out the four `Boolean` values near the end of the formal packet. + */ val booleanCodec : Codec[fourBoolPattern] = ( - bool :: - bool :: - bool :: - bool + ("isCrouching" | bool) :: + ("isJumping" | bool) :: + ("unk2" | bool) :: + ("unk3" | bool) ).as[fourBoolPattern] + /** + * A `Codec` for ignoring the four values at the end of the formal packet (all set to `false`). + */ val defaultCodec : Codec[fourBoolPattern] = ignore(0).xmap[fourBoolPattern] ( { case _ => @@ -79,13 +111,199 @@ object PlayerStateMessage extends Marshallable[PlayerStateMessage] { implicit val codec : Codec[PlayerStateMessage] = ( ("guid" | PlanetSideGUID.codec) :: ("pos" | Vector3.codec_pos) :: - ("facingYaw" | uint8L) :: - ("facingPitch" | uint8L) :: - ("facingYawUpper" | uint8L) :: - ("unk4" | uintL(10)) :: - ("more" | bool >>:~ { test => - ignore(0) :: - newcodecs.binary_choice(test, booleanCodec, defaultCodec) + (bool >>:~ { b1 => + conditional(b1, "unk1" | Vector3.codec_vel) :: + ("facingYaw" | uint8L) :: + ("facingPitch" | uint8L) :: + ("facingYawUpper" | uint8L) :: + ("unk1" | uintL(10)) :: + ("fourBools" | bool >>:~ { b2 => + ignore(0) :: + newcodecs.binary_choice(!b2, booleanCodec, defaultCodec) + }) }) - ).as[PlayerStateMessage] + ).xmap[PlayerStateMessage] ( + { + case uid :: p :: true :: Some(extra) :: f1 :: f2 :: f3 :: u :: b :: _ :: b1 :: b2 :: b3 :: b4 :: HNil => + PlayerStateMessage(uid, p, Some(extra), f1, f2, f3, u, b, b1, b2, b3, b4) + case uid :: p :: false :: None :: f1 :: f2 :: f3 :: u :: b :: _ :: b1 :: b2 :: b3 :: b4 :: HNil => + PlayerStateMessage(uid, p, None, f1, f2, f3, u, b, b1, b2, b3, b4) + }, + { + case PlayerStateMessage(uid, p, Some(extra), f1, f2, f3, u, b, b1, b2, b3, b4) => + uid :: p :: true :: Some(extra) :: f1 :: f2 :: f3 :: u :: b :: () :: b1 :: b2 :: b3 :: b4 :: HNil + case PlayerStateMessage(uid, p, None, f1, f2, f3, u, b, b1, b2, b3, b4) => + uid :: p :: false :: None :: f1 :: f2 :: f3 :: u :: b :: () :: b1 :: b2 :: b3 :: b4 :: HNil + } + ) +} + +//TODO the following logic is unimplemented +/* +There is a bool that is currently unhandled that determines if the packet is aware that this code would run. +If it passes, the first 8-bit value is the number of times the data will be iterated over. +On each pass, a 4-bit value is extracted from the packet and compared against 15. +When 15 is read, an 8-bit value is read on that same turn. +On each subsequent turn, 8-bit values will be read until the number of iterations or there is an exception. +I have no clue what any of this is supposed to do. + */ +/** + * na + * @param size a length to be applied to the next list, but not necessarily the length of that list + * @param data a list of data that comes in either a single 8-bit value, or a 4-bit value and, maybe, an 8-bit value + */ +final case class Extra1(size : Int, + data : List[Extra2]) + +/** + * na + * @param unk1 na; + * the first 8-bit value in one-value form or the first 4-bit value in two-value form; + * in two-value form, when equal to 15, the second value is read + * @param unk2 na; + * the potential second 8-bit value in two-value form + * @param more the next data in the sequence + */ +final case class Extra2(unk1 : Int, + unk2 : Option[Int], + more : Option[Extra2] = None) + +object Extra1 { + /** + * Take a chain of `Extra2` objects produced from decoding and compress it into a `List`. + * @param lst the list in which the `Extra2` data will be stored + * @param nesting the current link in the chain of `Extra2` objects + */ + private def packExtraList(lst : mutable.ListBuffer[Extra2], nesting : Option[Extra2]) : Unit = { + if(nesting.isEmpty) { //escape case + return + } + val elem : Extra2 = nesting.get + lst += Extra2(elem.unk1, elem.unk2) + packExtraList(lst, elem.more) //tail recursion + } + + /** + * Take a `List` of `Extra2` objects for encoding and expand it into a chain. + * @param iter the iterator for a `List` of `Extra2` data + * @return the head of a chain of `Extra2` objects + */ + private def unpackExtraList(iter : Iterator[Extra2]) : Option[Extra2] = { + //TODO as I don't think I can use tail recursion, how do I do this iteratively? + if(!iter.hasNext) + return None + val elem : Extra2 = iter.next + Some(Extra2(elem.unk1, elem.unk2, unpackExtraList(iter))) + } + + implicit val codec : Codec[Extra1] = ( + ("size" | uint8L) >>:~ { sz => + ignore(0) :: + //external logic: the client checks sz < dword_D33D38 before decoding beyond this point + conditional(sz != 0, "data" | Extra2.processData(sz)) + } + ).xmap[Extra1] ( + { + case a :: _ :: None :: HNil => + Extra1(a, List.empty) //it's okay if a != 0 + case a :: _ :: b :: HNil => + val list = mutable.ListBuffer[Extra2]() + packExtraList(list, b) + Extra1(a, list.toList) + }, + { + case Extra1(a, b) => + if(b.isEmpty) + a :: () :: None :: HNil + else + a :: () :: unpackExtraList(b.iterator) :: HNil + } + ) +} + +object Extra2 { + /** + * An abbreviated constructor for the one-value form. + * @param a na + * @return an `Extra2` object + */ + def apply(a : Int) : Extra2 = { + Extra2(a, None) + } + + /** + * An abbreviated constructor for the two-value form. + * @param a na + * @param b na + * @return an `Extra2` object + */ + def apply(a : Int, b : Int) : Extra2 = { + Extra2(a, Some(b)) + } + + /** + * A `Codec` for reading a single value. + */ + private val oneValueCodec : Codec[Extra2] = ( + ignore(0) :: + ("unk2" | uint8L) + ).xmap[Extra2] ( + { + case _ :: a :: HNil => + Extra2(a, None, None) + }, + { + case Extra2(a, None, _) => + () :: a :: HNil + } + ) + + /** + * A `Codec` for reading potentially two values. + */ + private val twoValueCodec : Codec[Extra2] = ( + ("unk1" | uint4L) >>:~ { unk => + ignore(0) :: + conditional(unk == 15, "unk2" | uint8L) + } + ).xmap[Extra2] ( + { + case a :: _ :: b :: HNil => + Extra2(a, b, None) + }, + { + case Extra2(a, b, _) => + a :: () :: b :: HNil + } + ) + + /** + * A recursive `Codec` that allows for swapping between different `Codec`s to account for two ways to parse the next element. + * The function calls itself to process each element in the sequence of data in the same manner until complete. + * The `Extra2` object that is recovered from the first choice of `Codec`s is merely an intermediary object. + * Due to immutability, the initial object is repackaged to append the chain of `Extra2` in an `Extra2` object. + * @param size the number of iterations of the looping process left to perform, including this one + * @param form determine whether we use `oneValueCodec` or `twoValueCodec`; + * should be set to `false` at first and set to `true` when two values are read in one pass; + * it will stay as `false` until set to `true`, whereupon it will always be `true` + * @return a `Codec` the translates a chain of `Extra2` data + */ + def processData(size : Int, form : Boolean = false) : Codec[Extra2] = ( + newcodecs.binary_choice(form, Extra2.oneValueCodec, Extra2.twoValueCodec) >>:~ { elem => + ignore(0) :: + conditional(size > 0, newcodecs.binary_choice(form || elem.unk2.isDefined, + Extra2.processData(size - 1, true), + Extra2.processData(size - 1)) + ) + } + ).xmap[Extra2] ( + { + case a :: _ :: b :: HNil => + Extra2(a.unk1, a.unk2, b) + }, + { + case Extra2(a, b, c) => + Extra2(a, b) :: () :: c :: HNil + } + ) }