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 {