diff --git a/common/src/main/scala/net/psforever/packet/GamePacketOpcode.scala b/common/src/main/scala/net/psforever/packet/GamePacketOpcode.scala index ed42c8c9..2dab10d6 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 00000000..b99b8203 --- /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 15e9c277..18f32d92 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"