diff --git a/common/src/main/scala/net/psforever/packet/GamePacketOpcode.scala b/common/src/main/scala/net/psforever/packet/GamePacketOpcode.scala index 6e9984e5d..506c22312 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..49c8c57c0 --- /dev/null +++ b/common/src/main/scala/net/psforever/packet/game/PlayerStateShiftMessage.scala @@ -0,0 +1,119 @@ +// 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.Codec +import scodec.codecs._ +import shapeless.{::, HNil} + +/** + * na + * @param pos the position to move the character to in the world environment (in three coordinates) + * @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 (in three coordinates) + */ +final case class PlayerState(pos : Vector3, + viewYawLim : Int, + vel : Option[Vector3]) + +/** + * Force the client's character to adhere to the influence of specific external stimulus. + * @param unk1 na + * @param state the state to influence the character with respect to his environment in the current zone + + */ +final case class PlayerStateShiftMessage(unk1 : Int, + state : Option[PlayerState], + unk2 : Boolean) + extends PlanetSideGamePacket { + type Packet = TimeOfDayMessage + def opcode = GamePacketOpcode.PlayerStateShiftMessage + def encode = PlayerStateShiftMessage.encode(this) +} + +object PlayerState extends Marshallable[PlayerState] { + /** + * An abbreviated constructor for creating `PlayerState`, assuming velocity is not applied. + * @param pos the position of the character in the world environment (in three coordinates) + * @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 (in three coordinates) + * @return a `PlayerState` object + */ + def apply(pos : Vector3, viewYawLim : Int, vel : Vector3) : PlayerState = + PlayerState(pos, viewYawLim, Some(vel)) + + /** + * An abbreviated constructor for creating `PlayerState`, removing the optional condition of all parameters. + * @param pos the position of the character in the world environment (in three coordinates) + * @param viewYawLim an angle with respect to the horizon towards which the avatar is looking (to some respect) + * @return a `PlayerState` object + */ + def apply(pos : Vector3, viewYawLim : Int) : PlayerState = + PlayerState(pos, viewYawLim, None) + + implicit val codec : Codec[PlayerState] = ( + ("pos" | Vector3.codec_pos) :: + ("unk2" | uint8L) :: + (bool >>:~ { test => + ignore(0) :: + conditional(test, "pos" | Vector3.codec_vel) + }) + ).xmap[PlayerState] ( + { + case a :: b :: false :: _ :: None :: HNil => + PlayerState(a, b, None) + case a :: b :: true :: _ :: Some(vel) :: HNil => + PlayerState(a, b, Some(vel)) + }, + { + case PlayerState(a, b, None) => + a :: b :: false :: () :: None :: HNil + case PlayerState(a, b, Some(vel)) => + a :: b :: true :: () :: Some(vel) :: HNil + } + ).as[PlayerState] +} + +object PlayerStateShiftMessage extends Marshallable[PlayerStateShiftMessage] { + private type pattern = Int :: Option[PlayerState] :: Boolean :: HNil + + /** + * An abbreviated constructor for creating `PlayerStateShiftMessage`, removing the optional condition of `state`. + * @param unk1 na + * @param state the state to which to influence the character with respect to his environment in the current zone + * @param unk2 na + * @return a `PlayerStateShiftMessage` packet + */ + def apply(unk1 : Int, state : PlayerState, unk2 : Boolean) : PlayerStateShiftMessage = + PlayerStateShiftMessage(unk1, Some(state), unk2) + + /** + * An abbreviated constructor for creating `PlayerStateShiftMessage`, assuming the parameter `state` is not defined. + * @param unk1 na + * @param unk2 na + * @return a `PlayerStateShiftMessage` packet + */ + def apply(unk1 : Int, unk2 : Boolean) : PlayerStateShiftMessage = + PlayerStateShiftMessage(unk1, None, unk2) + + implicit val codec : Codec[PlayerStateShiftMessage] = ( + bool >>:~ { test1 => + ("unk1" | uintL(3)) :: + conditional(test1, "pos" | PlayerState.codec) :: + ("unk2" | bool) + }).xmap[PlayerStateShiftMessage] ( + { + case false :: a :: None :: b :: HNil => + PlayerStateShiftMessage(a, None, b) + case true :: a :: Some(pos) :: b :: HNil => + PlayerStateShiftMessage(a, Some(pos), b) + }, + { + case PlayerStateShiftMessage(a, None, b) => + false :: a :: None :: b :: HNil + case PlayerStateShiftMessage(a, Some(pos), b) => + true :: a :: Some(pos) :: b :: HNil + } + ).as[PlayerStateShiftMessage] +} diff --git a/common/src/test/scala/GamePacketTest.scala b/common/src/test/scala/GamePacketTest.scala index 2f7e8a11d..7ddeecff7 100644 --- a/common/src/test/scala/GamePacketTest.scala +++ b/common/src/test/scala/GamePacketTest.scala @@ -527,6 +527,83 @@ 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(unk1, state, unk2) => + unk1 mustEqual 6 + state.isDefined mustEqual false + unk2 mustEqual true + case _ => + ko + } + } + + "decode (pos)" in { + PacketCoding.DecodePacket(string_pos).require match { + case PlayerStateShiftMessage(unk1, state, unk2) => + unk1 mustEqual 1 + state.isDefined mustEqual true + 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 + unk2 mustEqual false + case _ => + ko + } + } + + "decode (pos and vel)" in { + PacketCoding.DecodePacket(string_posAndVel).require match { + case PlayerStateShiftMessage(unk1, state, unk2) => + unk1 mustEqual 2 + state.isDefined mustEqual true + 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 + unk2 mustEqual false + case _ => + ko + } + } + + "encode (short)" in { + val msg = PlayerStateShiftMessage(6, true) + val pkt = PacketCoding.EncodePacket(msg).require.toByteVector + + pkt mustEqual string_short + } + + "encode (pos)" in { + val msg = PlayerStateShiftMessage(1, + PlayerState(Vector3(4624.703f, 5922.1484f, 46.171875f), 255), + false) + val pkt = PacketCoding.EncodePacket(msg).require.toByteVector + + pkt mustEqual string_pos + } + + "encode (pos and vel)" in { + val msg = PlayerStateShiftMessage(2, + PlayerState(Vector3(4645.75f, 5811.6016f, 50.3125f), 14, Vector3(2.8125f, -8.0f, 0.375f)), + false) + val pkt = PacketCoding.EncodePacket(msg).require.toByteVector + + pkt mustEqual string_posAndVel + } + } + "UseItemMessage" should { val string = hex"10 4B00 0000 7401 FFFFFFFF 4001000000000000000000000000058C803600800000"