From 98c924a40aee84ee71e3bbff344d1fa7430cecb8 Mon Sep 17 00:00:00 2001 From: FateJH Date: Sat, 10 Dec 2016 21:33:39 -0500 Subject: [PATCH 1/6] initial packet work --- .../psforever/packet/GamePacketOpcode.scala | 2 +- .../packet/game/PlayerStateMessage.scala | 61 +++++++++++++++++++ 2 files changed, 62 insertions(+), 1 deletion(-) create mode 100644 common/src/main/scala/net/psforever/packet/game/PlayerStateMessage.scala diff --git a/common/src/main/scala/net/psforever/packet/GamePacketOpcode.scala b/common/src/main/scala/net/psforever/packet/GamePacketOpcode.scala index 6e9984e5..af57d03a 100644 --- a/common/src/main/scala/net/psforever/packet/GamePacketOpcode.scala +++ b/common/src/main/scala/net/psforever/packet/GamePacketOpcode.scala @@ -328,7 +328,7 @@ object GamePacketOpcode extends Enumeration { case 0x06 => noDecoder(UnknownMessage6) case 0x07 => noDecoder(UnknownMessage7) // 0x08 - case 0x08 => noDecoder(PlayerStateMessage) + case 0x08 => game.PlayerStateMessage.decode case 0x09 => game.HitMessage.decode case 0x0a => noDecoder(HitHint) case 0x0b => noDecoder(DamageMessage) diff --git a/common/src/main/scala/net/psforever/packet/game/PlayerStateMessage.scala b/common/src/main/scala/net/psforever/packet/game/PlayerStateMessage.scala new file mode 100644 index 00000000..318d0cf0 --- /dev/null +++ b/common/src/main/scala/net/psforever/packet/game/PlayerStateMessage.scala @@ -0,0 +1,61 @@ +// Copyright (c) 2016 PSForever.net to present +package net.psforever.packet.game + +import net.psforever.newcodecs.newcodecs +import net.psforever.packet.{GamePacketOpcode, Marshallable, PlanetSideGamePacket} +import net.psforever.types.Vector3 +import scodec.Codec +import scodec.codecs._ +import shapeless.{::, HNil} + +final case class PlayerStateMessage(guid : PlanetSideGUID, + pos : Vector3, + unk1 : Int, + unk2 : Int, + unk3 : Int, + unk4 : Int, + y : Boolean, + unk5 : Boolean = false, + unk6 : Boolean = false, + unk7 : Boolean = false, + unk8 : Boolean = false) + extends PlanetSideGamePacket { + type Packet = TimeOfDayMessage + def opcode = GamePacketOpcode.PlayerStateMessage + def encode = PlayerStateMessage.encode(this) +} + +object PlayerStateMessage extends Marshallable[PlayerStateMessage] { + type fourBoolPattern = Boolean :: Boolean :: Boolean :: Boolean :: HNil + + val booleanCodec : Codec[fourBoolPattern] = ( + bool :: + bool :: + bool :: + bool + ).as[fourBoolPattern] + + val defaultCodec : Codec[fourBoolPattern] = ignore(0).xmap[fourBoolPattern] ( + { + case _ => + false :: false :: false :: false :: HNil + }, + { + case _ => + () + } + ).as[fourBoolPattern] + + implicit val codec : Codec[PlayerStateMessage] = ( + ("guid" | PlanetSideGUID.codec) :: + ("pos" | Vector3.codec_pos) :: + ("unk1" | uint8L) :: + ("unk2" | uint8L) :: + ("unk3" | uint8L) :: + ("unk4" | uintL(10)) :: + ("y" | bool >>:~ { test => + ignore(0) :: + newcodecs.binary_choice(test, booleanCodec, defaultCodec) + }) + ).as[PlayerStateMessage] +} From 6af7b79c55cdc2d1164ee413a76af277aca6e6e2 Mon Sep 17 00:00:00 2001 From: FateJH Date: Sat, 24 Dec 2016 00:57:21 -0500 Subject: [PATCH 2/6] re-importing modified PlayerStateMessage packet from volatile branch --- .../packet/game/PlayerStateMessage.scala | 56 ++++++++++++++----- 1 file changed, 43 insertions(+), 13 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 318d0cf0..171b7c6a 100644 --- a/common/src/main/scala/net/psforever/packet/game/PlayerStateMessage.scala +++ b/common/src/main/scala/net/psforever/packet/game/PlayerStateMessage.scala @@ -8,16 +8,46 @@ import scodec.Codec import scodec.codecs._ import shapeless.{::, HNil} +/** + * na + * @param guid the avatar's guid + * @param pos the position of the avatar in the world environment (in three coordinates) + * @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` + * @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 + * @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 + * @param isCrouching avatar is crouching; + * 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 + */ final case class PlayerStateMessage(guid : PlanetSideGUID, pos : Vector3, - unk1 : Int, - unk2 : Int, - unk3 : Int, + facingYaw : Int, + facingPitch : Int, + facingYawUpper : Int, unk4 : Int, - y : Boolean, + more : Boolean, unk5 : Boolean = false, - unk6 : Boolean = false, - unk7 : Boolean = false, + isCrouching : Boolean = false, + isJumping : Boolean = false, unk8 : Boolean = false) extends PlanetSideGamePacket { type Packet = TimeOfDayMessage @@ -30,9 +60,9 @@ object PlayerStateMessage extends Marshallable[PlayerStateMessage] { val booleanCodec : Codec[fourBoolPattern] = ( bool :: - bool :: - bool :: - bool + bool :: + bool :: + bool ).as[fourBoolPattern] val defaultCodec : Codec[fourBoolPattern] = ignore(0).xmap[fourBoolPattern] ( @@ -49,11 +79,11 @@ object PlayerStateMessage extends Marshallable[PlayerStateMessage] { implicit val codec : Codec[PlayerStateMessage] = ( ("guid" | PlanetSideGUID.codec) :: ("pos" | Vector3.codec_pos) :: - ("unk1" | uint8L) :: - ("unk2" | uint8L) :: - ("unk3" | uint8L) :: + ("facingYaw" | uint8L) :: + ("facingPitch" | uint8L) :: + ("facingYawUpper" | uint8L) :: ("unk4" | uintL(10)) :: - ("y" | bool >>:~ { test => + ("more" | bool >>:~ { test => ignore(0) :: newcodecs.binary_choice(test, booleanCodec, defaultCodec) }) From c873d4240edbb8d9694784683476910b7714430e Mon Sep 17 00:00:00 2001 From: FateJH Date: Wed, 28 Dec 2016 00:01:48 -0500 Subject: [PATCH 3/6] packet type is now correct --- .../scala/net/psforever/packet/game/PlayerStateMessage.scala | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) 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 171b7c6a..38c42854 100644 --- a/common/src/main/scala/net/psforever/packet/game/PlayerStateMessage.scala +++ b/common/src/main/scala/net/psforever/packet/game/PlayerStateMessage.scala @@ -50,7 +50,7 @@ final case class PlayerStateMessage(guid : PlanetSideGUID, isJumping : Boolean = false, unk8 : Boolean = false) extends PlanetSideGamePacket { - type Packet = TimeOfDayMessage + type Packet = PlayerStateMessage def opcode = GamePacketOpcode.PlayerStateMessage def encode = PlayerStateMessage.encode(this) } From 4862b1b5448d50ccd45083f3414bb9fb77db81bf Mon Sep 17 00:00:00 2001 From: FateJH Date: Sat, 7 Jan 2017 20:39:32 -0500 Subject: [PATCH 4/6] 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 38c42854..3d32307e 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 + } + ) } From 151e2d0ad8ce8917514d74586ffc454da337f749 Mon Sep 17 00:00:00 2001 From: FateJH Date: Sat, 7 Jan 2017 22:22:04 -0500 Subject: [PATCH 5/6] working tests for packet --- .../packet/game/PlayerStateMessage.scala | 62 +++++----- common/src/test/scala/GamePacketTest.scala | 108 ++++++++++++++++++ 2 files changed, 142 insertions(+), 28 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 3d32307e..bf2e7e87 100644 --- a/common/src/main/scala/net/psforever/packet/game/PlayerStateMessage.scala +++ b/common/src/main/scala/net/psforever/packet/game/PlayerStateMessage.scala @@ -11,13 +11,27 @@ import shapeless.{::, HNil} import scala.collection.mutable /** - * The server instructs clients to render a certain avatar not operated by its player to move in a certain way.
+ * The server instructs some clients to render a player (usually not that client's avatar) 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.
+ * This packet instructs the basic aspects of how the player character is positioned and how the player character moves. + * Each client keeps track of where a character "currently" is according to that client. + * `pos` reflects an update in regards to where the character should be moved. + * Data between this "currently" and "new" are interpolated over a fixed time interval. + * Position and velocity data is standard to normal PlanetSide ranges. + * All angles follow the convention that every `0x1` is about 2.8125 degrees; so, `0x10` is 45.0 degrees.
+ *
+ * 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. + * In that case, it sppears to teleport to `pos` to carry out the interpolated movement according to `vel`. + * After the move, it remains at essentially `pos + vel * t`. + * The repositioning always takes the same amount of time. + * The player model is left in a walking/running animation (in place) until directed otherwise.
+ *
+ * If the model must interact with the environment during a velocity-driven move, it copes with local physics. + * A demonstration of this is what happens when one player "runs past"/"into" another player running up stairs. + * The climbing player is frequently reported by the other to appear to bounce over that player's head. + * If the other player is off the ground, passing too near to the observer can cause a rubber band effect on trajectory. + * This effect is entirely client-side to the observer and affects the moving player in no way.
*
* facingYaw:
* `0x00` -- E
@@ -46,22 +60,18 @@ import scala.collection.mutable * @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 `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 `0x1` is about 2.8125 degrees - * @param facingYawUpper the angle of the avatar's upper body with respect to its forward-facing direction; - * every `0x1` is about 2.8125 degrees + * @param facingPitch the angle with respect to the sky and the ground towards which the avatar is looking + * @param facingYawUpper the angle of the avatar's upper body with respect to its forward-facing direction * @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 to maintain crouch * @param isJumping avatar is jumping; * must remain flagged for jump to maintain animation; * turn off to land(?) * @param unk2 na - * @param unk3 na + * @param isCloaked avatar is cloaked by virtue of an Infiltration Suit; + * must remain flagged to stay cloaked */ final case class PlayerStateMessage(guid : PlanetSideGUID, pos : Vector3, @@ -70,11 +80,10 @@ final case class PlayerStateMessage(guid : PlanetSideGUID, facingPitch : Int, facingYawUpper : Int, unk1 : Int, - fourBools : Boolean, isCrouching : Boolean = false, isJumping : Boolean = false, unk2 : Boolean = false, - unk3 : Boolean = false) + isCloaked : Boolean = false) extends PlanetSideGamePacket { type Packet = PlayerStateMessage def opcode = GamePacketOpcode.PlayerStateMessage @@ -91,7 +100,7 @@ object PlayerStateMessage extends Marshallable[PlayerStateMessage] { ("isCrouching" | bool) :: ("isJumping" | bool) :: ("unk2" | bool) :: - ("unk3" | bool) + ("isCloaked" | bool) ).as[fourBoolPattern] /** @@ -124,28 +133,25 @@ object PlayerStateMessage extends Marshallable[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 uid :: pos :: _ :: vel :: f1 :: f2 :: f3 :: u :: _ :: _ :: b1 :: b2 :: b3 :: b4 :: HNil => + PlayerStateMessage(uid, pos, vel, f1, f2, f3, u, 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 + case PlayerStateMessage(uid, pos, vel, f1, f2, f3, u, b1, b2, b3, b4) => + val b : Boolean = !(b1 || b2 || b3 || b4) + uid :: pos :: vel.isDefined :: vel :: 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. +There is a boolean 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. +Until I find a packet that responds somehow, I have no clue what any of this is supposed to do. */ /** * na diff --git a/common/src/test/scala/GamePacketTest.scala b/common/src/test/scala/GamePacketTest.scala index 2f7e8a11..9a2ca0c7 100644 --- a/common/src/test/scala/GamePacketTest.scala +++ b/common/src/test/scala/GamePacketTest.scala @@ -118,6 +118,114 @@ class GamePacketTest extends Specification { } } + "PlayerStateMessage" should { + val string_short = hex"08 A006 DFD17 B5AEB 380B 0F80002990" + val string_mod = hex"08 A006 DFD17 B5AEB 380B 0F80002985" //slightly modified from above to demonstrate active booleans + val string_vel = hex"08 A006 4DD47 CDB1B 0C0B A8C1A5000403008014A4" + + "decode (short)" in { + PacketCoding.DecodePacket(string_short).require match { + case PlayerStateMessage(guid, pos, vel, facingYaw, facingPitch, facingUpper, unk1, crouching, jumping, unk2, unk3) => + guid mustEqual PlanetSideGUID(1696) + pos.x mustEqual 4003.7422f + pos.y mustEqual 5981.414f + pos.z mustEqual 44.875f + vel.isDefined mustEqual false + facingYaw mustEqual 31 + facingPitch mustEqual 0 + facingUpper mustEqual 0 + unk1 mustEqual 83 + crouching mustEqual false + jumping mustEqual false + unk2 mustEqual false + unk3 mustEqual false + case default => + ko + } + } + + "decode (mod)" in { + PacketCoding.DecodePacket(string_mod).require match { + case PlayerStateMessage(guid, pos, vel, facingYaw, facingPitch, facingUpper, unk1, crouching, jumping, unk2, unk3) => + guid mustEqual PlanetSideGUID(1696) + pos.x mustEqual 4003.7422f + pos.y mustEqual 5981.414f + pos.z mustEqual 44.875f + vel.isDefined mustEqual false + facingYaw mustEqual 31 + facingPitch mustEqual 0 + facingUpper mustEqual 0 + unk1 mustEqual 83 + crouching mustEqual false + jumping mustEqual true + unk2 mustEqual false + unk3 mustEqual true + case default => + ko + } + } + + "decode (vel)" in { + PacketCoding.DecodePacket(string_vel).require match { + case PlayerStateMessage(guid, pos, vel, facingYaw, facingPitch, facingUpper, unk1, crouching, jumping, unk2, unk3) => + guid mustEqual PlanetSideGUID(1696) + pos.x mustEqual 4008.6016f + pos.y mustEqual 5987.6016f + pos.z mustEqual 44.1875f + vel.isDefined mustEqual true + vel.get.x mustEqual 2.53125f + vel.get.y mustEqual 6.5625f + vel.get.z mustEqual 0.0f + facingYaw mustEqual 24 + facingPitch mustEqual 4 + facingUpper mustEqual 0 + unk1 mustEqual 165 + crouching mustEqual false + jumping mustEqual false + unk2 mustEqual false + unk3 mustEqual false + case default => + ko + } + } + + "encode (short)" in { + val msg = PlayerStateMessage( + PlanetSideGUID(1696), + Vector3(4003.7422f, 5981.414f, 44.875f), + None, + 31, 0, 0, 83, + false, false, false, false) + val pkt = PacketCoding.EncodePacket(msg).require.toByteVector + + pkt mustEqual string_short + } + + "encode (mod)" in { + val msg = PlayerStateMessage( + PlanetSideGUID(1696), + Vector3(4003.7422f, 5981.414f, 44.875f), + None, + 31, 0, 0, 83, + false, true, false, true) + val pkt = PacketCoding.EncodePacket(msg).require.toByteVector + + pkt mustEqual string_mod + } + + "encode (vel)" in { + val msg = PlayerStateMessage( + PlanetSideGUID(1696), + Vector3(4008.6016f, 5987.6016f, 44.1875f), + Some(Vector3(2.53125f, 6.5625f, 0f)), + 24, 4, 0, 165, + false, false, false, false) + val pkt = PacketCoding.EncodePacket(msg).require.toByteVector + + pkt mustEqual string_vel + } + } + "ActionResultMessage" should { "decode" in { PacketCoding.DecodePacket(hex"1f 80").require match { From e10b52c00478c0a7f49185c9acd90983bd5b67f8 Mon Sep 17 00:00:00 2001 From: FateJH Date: Thu, 12 Jan 2017 08:05:43 -0500 Subject: [PATCH 6/6] streamlining parts of packet; separating Extra2 logic into two functions --- .../packet/game/PlayerStateMessage.scala | 140 ++++++++++-------- 1 file changed, 77 insertions(+), 63 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 bf2e7e87..d5053bba 100644 --- a/common/src/main/scala/net/psforever/packet/game/PlayerStateMessage.scala +++ b/common/src/main/scala/net/psforever/packet/game/PlayerStateMessage.scala @@ -60,18 +60,16 @@ import scala.collection.mutable * @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; + * the model's whole body is facing this direction; * measurements are counter-clockwise from East * @param facingPitch the angle with respect to the sky and the ground towards which the avatar is looking * @param facingYawUpper the angle of the avatar's upper body with respect to its forward-facing direction * @param unk1 na - * @param isCrouching avatar is crouching; - * must remain flagged to maintain crouch - * @param isJumping avatar is jumping; - * must remain flagged for jump to maintain animation; - * turn off to land(?) + * @param is_crouching avatar is crouching + * @param is_jumping avatar is jumping; + * must remain flagged for jump to maintain animation * @param unk2 na - * @param isCloaked avatar is cloaked by virtue of an Infiltration Suit; - * must remain flagged to stay cloaked + * @param is_cloaked avatar is cloaked by virtue of an Infiltration Suit */ final case class PlayerStateMessage(guid : PlanetSideGUID, pos : Vector3, @@ -80,10 +78,10 @@ final case class PlayerStateMessage(guid : PlanetSideGUID, facingPitch : Int, facingYawUpper : Int, unk1 : Int, - isCrouching : Boolean = false, - isJumping : Boolean = false, + is_crouching : Boolean = false, + is_jumping : Boolean = false, unk2 : Boolean = false, - isCloaked : Boolean = false) + is_cloaked : Boolean = false) extends PlanetSideGamePacket { type Packet = PlayerStateMessage def opcode = GamePacketOpcode.PlayerStateMessage @@ -97,49 +95,46 @@ object PlayerStateMessage extends Marshallable[PlayerStateMessage] { * A `Codec` for reading out the four `Boolean` values near the end of the formal packet. */ val booleanCodec : Codec[fourBoolPattern] = ( - ("isCrouching" | bool) :: - ("isJumping" | bool) :: + ("is_crouching" | bool) :: + ("is_jumping" | bool) :: ("unk2" | bool) :: - ("isCloaked" | bool) + ("is_cloaked" | 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] ( + val defaultCodec : Codec[fourBoolPattern] = ignore(0).hlist.xmap[fourBoolPattern] ( { - case _ => + case _ :: HNil => false :: false :: false :: false :: HNil }, { - case _ => - () + case _ :: _ :: _ :: _ :: HNil => + () :: HNil } ).as[fourBoolPattern] implicit val codec : Codec[PlayerStateMessage] = ( ("guid" | PlanetSideGUID.codec) :: ("pos" | Vector3.codec_pos) :: - (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) - }) + optional(bool, "unk1" | Vector3.codec_vel) :: + ("facingYaw" | uint8L) :: + ("facingPitch" | uint8L) :: + ("facingYawUpper" | uint8L) :: + ("unk1" | uintL(10)) :: + (bool >>:~ { fourBools => + newcodecs.binary_choice(!fourBools, booleanCodec, defaultCodec) }) ).xmap[PlayerStateMessage] ( { - case uid :: pos :: _ :: vel :: f1 :: f2 :: f3 :: u :: _ :: _ :: b1 :: b2 :: b3 :: b4 :: HNil => + case uid :: pos :: vel :: f1 :: f2 :: f3 :: u :: _ :: b1 :: b2 :: b3 :: b4 :: HNil => PlayerStateMessage(uid, pos, vel, f1, f2, f3, u, b1, b2, b3, b4) }, { case PlayerStateMessage(uid, pos, vel, f1, f2, f3, u, b1, b2, b3, b4) => val b : Boolean = !(b1 || b2 || b3 || b4) - uid :: pos :: vel.isDefined :: vel :: f1 :: f2 :: f3 :: u :: b :: () :: b1 :: b2 :: b3 :: b4 :: HNil + uid :: pos :: vel :: f1 :: f2 :: f3 :: u :: b :: b1 :: b2 :: b3 :: b4 :: HNil } ) } @@ -150,13 +145,14 @@ There is a boolean that is currently unhandled(?) that determines if the packet 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. +On each subsequent turn, 8-bit values will be read until the number of iterations or until there is an exception. Until I find a packet that responds somehow, 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 + * (if I could prove that size == list.size always then I could eliminate superfluous logic from `Extra1`) + * @param data a list of data that comes as either an 8-bit value, or as a 4-bit value and, maybe, an 8-bit value */ final case class Extra1(size : Int, data : List[Extra2]) @@ -204,25 +200,23 @@ object Extra1 { 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)) + conditional(sz != 0, "data" | Extra2.processData(sz)).hlist } ).xmap[Extra1] ( { - case a :: _ :: None :: HNil => + case a :: None :: HNil => Extra1(a, List.empty) //it's okay if a != 0 - case a :: _ :: b :: HNil => + case a :: b :: HNil => val list = mutable.ListBuffer[Extra2]() packExtraList(list, b) Extra1(a, list.toList) }, { + case Extra1(a, Nil) => + a :: None :: HNil case Extra1(a, b) => - if(b.isEmpty) - a :: () :: None :: HNil - else - a :: () :: unpackExtraList(b.iterator) :: HNil + a :: unpackExtraList(b.iterator) :: HNil } ) } @@ -250,17 +244,14 @@ object Extra2 { /** * A `Codec` for reading a single value. */ - private val oneValueCodec : Codec[Extra2] = ( - ignore(0) :: - ("unk2" | uint8L) - ).xmap[Extra2] ( + private val oneValueCodec : Codec[Extra2] = ("unk2" | uint8L).hlist.xmap[Extra2] ( { - case _ :: a :: HNil => + case a :: HNil => Extra2(a, None, None) }, { case Extra2(a, None, _) => - () :: a :: HNil + a :: HNil } ) @@ -269,47 +260,70 @@ object Extra2 { */ private val twoValueCodec : Codec[Extra2] = ( ("unk1" | uint4L) >>:~ { unk => - ignore(0) :: - conditional(unk == 15, "unk2" | uint8L) + conditional(unk == 15, "unk2" | uint8L).hlist } ).xmap[Extra2] ( { - case a :: _ :: b :: HNil => + case a :: b :: HNil => Extra2(a, b, None) }, { case Extra2(a, b, _) => - a :: () :: b :: HNil + a :: b :: HNil } ) /** - * A recursive `Codec` that allows for swapping between different `Codec`s to account for two ways to parse the next element. + * Half of a recursive `Codec` that allows for swapping between different `Codec`s in between `List` elements.
+ *
* 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. + * Eventually, `processData` will parse a 4-bit value of 15 and will pass control over to `processDataSingle`. * @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 + * @return a `Codec` translating a chain of `Extra2` data + * @see Extra2.processDataSingle */ - 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)) - ) + def processData(size : Int) : Codec[Extra2] = ( + //TODO: without tail recursion, this might cause a stack overflow + twoValueCodec >>:~ { elem => + conditional(size > 0, newcodecs.binary_choice(elem.unk2.isDefined, + processDataSingle(size - 1), + processData(size - 1)) + ).hlist } ).xmap[Extra2] ( { - case a :: _ :: b :: HNil => + case a :: b :: HNil => Extra2(a.unk1, a.unk2, b) }, { case Extra2(a, b, c) => - Extra2(a, b) :: () :: c :: HNil + Extra2(a, b) :: c :: HNil + } + ) + + /** + * Latter half of a recursive `Codec` that allows for swapping between different `Codec`s in between `List` elements. + * This `Codec` no longer performs swapping and merely runs out the data.
+ *
+ * @param size the number of iterations of the looping process left to perform, including this one + * @return a `Codec` translating a chain of `Extra2` data + * @see Extra2.processData + */ + private def processDataSingle(size : Int) : Codec[Extra2] = ( + //TODO: without tail recursion, this might cause a stack overflow + oneValueCodec >>:~ { elem => + conditional(size > 0, processDataSingle(size - 1)).hlist + } + ).xmap[Extra2] ( + { + case a :: b :: HNil => + Extra2(a.unk1, a.unk2, b) + }, + { + case Extra2(a, b, c) => + Extra2(a, b) :: c :: HNil } ) }