diff --git a/common/src/main/scala/net/psforever/packet/GamePacketOpcode.scala b/common/src/main/scala/net/psforever/packet/GamePacketOpcode.scala
index ed42c8c97..2dab10d68 100644
--- a/common/src/main/scala/net/psforever/packet/GamePacketOpcode.scala
+++ b/common/src/main/scala/net/psforever/packet/GamePacketOpcode.scala
@@ -543,7 +543,7 @@ object GamePacketOpcode extends Enumeration {
case 0xbb => noDecoder(MapObjectStateBlockMessage)
case 0xbc => noDecoder(SnoopMsg)
case 0xbd => game.PlayerStateMessageUpstream.decode
- case 0xbe => noDecoder(PlayerStateShiftMessage)
+ case 0xbe => game.PlayerStateShiftMessage.decode
case 0xbf => noDecoder(ZipLineMessage)
// OPCODES 0xc0-cf
diff --git a/common/src/main/scala/net/psforever/packet/game/PlayerStateShiftMessage.scala b/common/src/main/scala/net/psforever/packet/game/PlayerStateShiftMessage.scala
new file mode 100644
index 000000000..b99b8203a
--- /dev/null
+++ b/common/src/main/scala/net/psforever/packet/game/PlayerStateShiftMessage.scala
@@ -0,0 +1,136 @@
+// Copyright (c) 2016 PSForever.net to present
+package net.psforever.packet.game
+
+import net.psforever.packet.{GamePacketOpcode, Marshallable, PlanetSideGamePacket}
+import net.psforever.types.Vector3
+import scodec.{Attempt, Codec, Err}
+import scodec.codecs._
+import shapeless.{::, HNil}
+
+/**
+ * Instructs an avatar to be stood, to look, and to move, in a certain way.
+ *
+ * The position defines a coordinate location in the avatar's current zone to which the avatar is immediately moved
+ * This movement is instantaneous and has no associated animation.
+ * If velocity is defined, the avatar is provided an "external force" that "pushes" the avatar in a given direction.
+ * This external force is not accumulative.
+ * Also, the external force is only applied once the avatar is set to the provided position.
+ *
+ * `viewYawLim` defines a "range of angles" that the avatar may look centered on the supplied angle.
+ * The avatar must be facing within 60-degrees of that direction, subjectively his left or his right.
+ * The avatar's view is immediately set to the closest 60-degree mark if it is outside of that range.
+ * The absolute angular displacement of the avatar is considered before applying this corrective behavior.
+ * After rotating any number of times:
+ * stopping in a valid part of the range is acceptable;
+ * stopping in an invalid part of the range will cause the avatar to align to the __earliest__ still-valid 60-degree mark.
+ * For that reason, even if the avatar's final angle is closest to the "left mark," it may re-align to the "right mark."
+ * This also resets the avatar's angular displacement.
+ * @param unk na
+ * @param pos the position to move the character to in the world environment
+ * @param viewYawLim an angle with respect to the horizon towards which the avatar is looking (to some respect)
+ * @param vel if defined, the velocity to apply to to the character at the given position
+ */
+final case class ShiftState(unk : Int,
+ pos : Vector3,
+ viewYawLim : Int,
+ vel : Option[Vector3])
+
+/**
+ * Push specific motion-based stimuli on a specific character.
+ *
+ * `PlayerStateMessageUpstream` involves data transmitted from a client to the server regarding its avatar.
+ * `PlayerStateMessage` involves data transmitted from the server to the clients regarding characters other than that client's avatar.
+ * `PlayerStateShiftMessage` involves data transmitted from the server to a client about that client's avatar.
+ * It temporarily asserts itself before normal player movement and asserts specific placement and motion.
+ * An application of this packet is being `/warp`ed within a zone via a non-triggering agent (like a teleporter).
+ * Another, more common, application of this packet is being thrown about when the target of an attempted roadkill.
+ * @param state if defined, the behaviors to influence the character
+ * @param unk na
+ */
+final case class PlayerStateShiftMessage(state : Option[ShiftState],
+ unk : Option[Int] = None)
+ extends PlanetSideGamePacket {
+ type Packet = TimeOfDayMessage
+ def opcode = GamePacketOpcode.PlayerStateShiftMessage
+ def encode = PlayerStateShiftMessage.encode(this)
+}
+
+object ShiftState extends Marshallable[ShiftState] {
+ /**
+ * An abbreviated constructor for creating `ShiftState`, assuming velocity is not applied.
+ * @param unk na
+ * @param pos the position of the character in the world environment
+ * @param viewYawLim an angle with respect to the horizon towards which the avatar is looking (to some respect)
+ * @param vel the velocity to apply to to the character at the given position
+ * @return a `ShiftState` object
+ */
+ def apply(unk : Int, pos : Vector3, viewYawLim : Int, vel : Vector3) : ShiftState =
+ ShiftState(unk, pos, viewYawLim, Some(vel))
+
+ /**
+ * An abbreviated constructor for creating `ShiftState`, removing the optional condition of all parameters.
+ * @param unk na
+ * @param pos the position of the character in the world environment
+ * @param viewYawLim an angle with respect to the horizon towards which the avatar is looking (to some respect)
+ * @return a `ShiftState` object
+ */
+ def apply(unk : Int, pos : Vector3, viewYawLim : Int) : ShiftState =
+ ShiftState(unk, pos, viewYawLim, None)
+
+ implicit val codec : Codec[ShiftState] = (
+ ("unk1" | uintL(3)) ::
+ ("pos" | Vector3.codec_pos) ::
+ ("viewYawLim" | uint8L) ::
+ optional(bool, "pos" | Vector3.codec_vel)
+ ).xmap[ShiftState] (
+ {
+ case a :: b :: c :: d :: HNil =>
+ ShiftState(a, b, c, d)
+ },
+ {
+ case ShiftState(a, b, c, d) =>
+ a :: b :: c :: d :: HNil
+ }
+ ).as[ShiftState]
+}
+
+object PlayerStateShiftMessage extends Marshallable[PlayerStateShiftMessage] {
+ /**
+ * An abbreviated constructor for creating `PlayerStateShiftMessage`, removing the optional condition of `state`.
+ * @param state the behaviors to influence the character
+ * @param unk na
+ * @return a `PlayerStateShiftMessage` packet
+ */
+ def apply(state : ShiftState, unk : Int) : PlayerStateShiftMessage =
+ PlayerStateShiftMessage(Some(state), Some(unk))
+
+ /**
+ * An abbreviated constructor for creating `PlayerStateShiftMessage`, removing the optional condition of `unk2`.
+ * @param state the behaviors to influence the character
+ * @return a `PlayerStateShiftMessage` packet
+ */
+ def apply(state : ShiftState) : PlayerStateShiftMessage =
+ PlayerStateShiftMessage(Some(state), None)
+
+ /**
+ * An abbreviated constructor for creating `PlayerStateShiftMessage`, assuming the parameters `unk1` and `state` are not defined.
+ * @param unk na
+ * @return a `PlayerStateShiftMessage` packet
+ */
+ def apply(unk : Int) : PlayerStateShiftMessage =
+ PlayerStateShiftMessage(None, Some(unk))
+
+ implicit val codec : Codec[PlayerStateShiftMessage] = (
+ optional(bool, "state" | ShiftState.codec) ::
+ optional(bool, "unk" | uintL(3))
+ ).xmap[PlayerStateShiftMessage] (
+ {
+ case a :: b :: HNil =>
+ PlayerStateShiftMessage(a, b)
+ },
+ {
+ case PlayerStateShiftMessage(a, b) =>
+ a :: b :: HNil
+ }
+ ).as[PlayerStateShiftMessage]
+}
diff --git a/common/src/test/scala/GamePacketTest.scala b/common/src/test/scala/GamePacketTest.scala
index 15e9c277b..18f32d92e 100644
--- a/common/src/test/scala/GamePacketTest.scala
+++ b/common/src/test/scala/GamePacketTest.scala
@@ -693,6 +693,79 @@ class GamePacketTest extends Specification {
}
}
+ "PlayerStateShiftMessage" should {
+ val string_short = hex"BE 68"
+ val string_pos = hex"BE 95 A0 89 13 91 B8 B0 BF F0"
+ val string_posAndVel = hex"BE AE 01 29 CD 59 B9 40 C0 EA D4 00 0F 86 40"
+
+ "decode (short)" in {
+ PacketCoding.DecodePacket(string_short).require match {
+ case PlayerStateShiftMessage(state, unk) =>
+ state.isDefined mustEqual false
+ unk.isDefined mustEqual true
+ unk.get mustEqual 5
+ case _ =>
+ ko
+ }
+ }
+
+ "decode (pos)" in {
+ PacketCoding.DecodePacket(string_pos).require match {
+ case PlayerStateShiftMessage(state, unk) =>
+ state.isDefined mustEqual true
+ state.get.unk mustEqual 1
+ state.get.pos.x mustEqual 4624.703f
+ state.get.pos.y mustEqual 5922.1484f
+ state.get.pos.z mustEqual 46.171875f
+ state.get.viewYawLim mustEqual 255
+ state.get.vel.isDefined mustEqual false
+ unk.isDefined mustEqual false
+ case _ =>
+ ko
+ }
+ }
+
+ "decode (pos and vel)" in {
+ PacketCoding.DecodePacket(string_posAndVel).require match {
+ case PlayerStateShiftMessage(state, unk) =>
+ state.isDefined mustEqual true
+ state.get.unk mustEqual 2
+ state.get.pos.x mustEqual 4645.75f
+ state.get.pos.y mustEqual 5811.6016f
+ state.get.pos.z mustEqual 50.3125f
+ state.get.viewYawLim mustEqual 14
+ state.get.vel.isDefined mustEqual true
+ state.get.vel.get.x mustEqual 2.8125f
+ state.get.vel.get.y mustEqual -8.0f
+ state.get.vel.get.z mustEqual 0.375f
+ unk.isDefined mustEqual false
+ case _ =>
+ ko
+ }
+ }
+
+ "encode (short)" in {
+ val msg = PlayerStateShiftMessage(5)
+ val pkt = PacketCoding.EncodePacket(msg).require.toByteVector
+
+ pkt mustEqual string_short
+ }
+
+ "encode (pos)" in {
+ val msg = PlayerStateShiftMessage(ShiftState(1, Vector3(4624.703f, 5922.1484f, 46.171875f), 255))
+ val pkt = PacketCoding.EncodePacket(msg).require.toByteVector
+
+ pkt mustEqual string_pos
+ }
+
+ "encode (pos and vel)" in {
+ val msg = PlayerStateShiftMessage(ShiftState(2, Vector3(4645.75f, 5811.6016f, 50.3125f), 14, Vector3(2.8125f, -8.0f, 0.375f)))
+ val pkt = PacketCoding.EncodePacket(msg).require.toByteVector
+
+ pkt mustEqual string_posAndVel
+ }
+ }
+
"UseItemMessage" should {
val string = hex"10 4B00 0000 7401 FFFFFFFF 4001000000000000000000000000058C803600800000"