diff --git a/common/src/main/scala/net/psforever/packet/GamePacketOpcode.scala b/common/src/main/scala/net/psforever/packet/GamePacketOpcode.scala index ac612f1c..d7bc550b 100644 --- a/common/src/main/scala/net/psforever/packet/GamePacketOpcode.scala +++ b/common/src/main/scala/net/psforever/packet/GamePacketOpcode.scala @@ -392,7 +392,7 @@ object GamePacketOpcode extends Enumeration { case 0x3c => game.GenericCollisionMsg.decode case 0x3d => game.QuantityUpdateMessage.decode case 0x3e => game.ArmorChangedMessage.decode - case 0x3f => noDecoder(ProjectileStateMessage) + case 0x3f => game.ProjectileStateMessage.decode // OPCODES 0x40-4f case 0x40 => noDecoder(MountVehicleCargoMsg) diff --git a/common/src/main/scala/net/psforever/packet/game/ProjectileStateMessage.scala b/common/src/main/scala/net/psforever/packet/game/ProjectileStateMessage.scala new file mode 100644 index 00000000..329d6742 --- /dev/null +++ b/common/src/main/scala/net/psforever/packet/game/ProjectileStateMessage.scala @@ -0,0 +1,65 @@ +// Copyright (c) 2017 PSForever +package net.psforever.packet.game + +import net.psforever.packet.{GamePacketOpcode, Marshallable, PlanetSideGamePacket} +import net.psforever.types.Vector3 +import scodec.Codec +import scodec.codecs._ + +/** + * Dispatched to deliberately render certain projectiles of a weapon on other players' clients.
+ *
+ * This packet is generated by firing specific weapons in specific fire modes. + * For example, the Phoenix (`hunterseeker`) discharged in its primary fire mode generates this packet; + * but, the Phoenix in secondary fire mode does not. + * The Striker (`striker`) discharged in its primary fire mode generates this packet; + * but, the Striker in secondary fire mode does not. + * The chosen fire mode(s) are not a straight-fire projectile but one that has special control asserted over it. + * For the Phoenix, it is user-operated. + * For the Striker, it tracks towards a target while the weapon's reticle hovers over that target.
+ *
+ * This packet will continue to be dispatched by the client for as long as the projectile being tracked is in the air. + * All projectiles have a maximum lifespan before they will lose control and either despawn and/or explode. + * This number is tracked in the packet for simplicity. + * If the projectile strikes a valid target, the count will jump to a significantly enormous value beyond its normal lifespan. + * This ensures that the projectile - locally and the shared model - will despawn. + * @param projectile_guid the projectile + * @param shot_pos the position of the projectile + * @param shot_vel the velocity of the projectile + * @param unk1 na; + * usually 0 + * @param unk2 na; + * will remain consistent for the lifespan of a given projectile in most cases + * @param unk3 na; + * will remain consistent for the lifespan of a given projectile in most cases + * @param unk4 na; + * usually false + * @param time_alive how long the projectile has been in the air; + * often expressed in multiples of 2 + */ +final case class ProjectileStateMessage(projectile_guid : PlanetSideGUID, + shot_pos : Vector3, + shot_vel : Vector3, + unk1 : Int, + unk2 : Int, + unk3 : Int, + unk4 : Boolean, + time_alive : Int) + extends PlanetSideGamePacket { + type Packet = ProjectileStateMessage + def opcode = GamePacketOpcode.ProjectileStateMessage + def encode = ProjectileStateMessage.encode(this) +} + +object ProjectileStateMessage extends Marshallable[ProjectileStateMessage] { + implicit val codec : Codec[ProjectileStateMessage] = ( + ("projectile_guid" | PlanetSideGUID.codec) :: + ("shot_pos" | Vector3.codec_pos) :: + ("shot_vel" | Vector3.codec_float) :: + ("unk1" | uint8L) :: + ("unk2" | uint8L) :: + ("unk3" | uint8L) :: + ("unk4" | bool) :: + ("time_alive" | uint16L) + ).as[ProjectileStateMessage] +} diff --git a/common/src/test/scala/game/ProjectileStateMessageTest.scala b/common/src/test/scala/game/ProjectileStateMessageTest.scala new file mode 100644 index 00000000..f62e181e --- /dev/null +++ b/common/src/test/scala/game/ProjectileStateMessageTest.scala @@ -0,0 +1,44 @@ +// Copyright (c) 2017 PSForever +package game + +import org.specs2.mutable._ +import net.psforever.packet._ +import net.psforever.packet.game._ +import net.psforever.types.Vector3 +import scodec.bits._ + +class ProjectileStateMessageTest extends Specification { + val string = hex"3f 259d c5019 30e4a 9514 c52c9541 d9ba05c2 c5973941 00 f8 ec 020000" + + "decode" in { + PacketCoding.DecodePacket(string).require match { + case ProjectileStateMessage(projectile, pos, vel, unk1, unk2, unk3, unk4, time_alive) => + projectile mustEqual PlanetSideGUID(40229) + pos.x mustEqual 4611.539f + pos.y mustEqual 5576.375f + pos.z mustEqual 82.328125f + vel.x mustEqual 18.64686f + vel.y mustEqual -33.43247f + vel.z mustEqual 11.599553f + unk1 mustEqual 0 + unk2 mustEqual 248 + unk3 mustEqual 236 + unk4 mustEqual false + time_alive mustEqual 4 + case _ => + ko + } + } + + "encode" in { + val msg = ProjectileStateMessage( + PlanetSideGUID(40229), + Vector3(4611.539f, 5576.375f, 82.328125f), + Vector3(18.64686f, -33.43247f, 11.599553f), + 0, 248, 236, false, 4 + ) + val pkt = PacketCoding.EncodePacket(msg).require.toByteVector + + pkt mustEqual string + } +} diff --git a/pslogin/src/main/scala/WorldSessionActor.scala b/pslogin/src/main/scala/WorldSessionActor.scala index 852833d5..dcaae96a 100644 --- a/pslogin/src/main/scala/WorldSessionActor.scala +++ b/pslogin/src/main/scala/WorldSessionActor.scala @@ -255,6 +255,9 @@ class WorldSessionActor extends Actor with MDCContextAware { case msg @ ChildObjectStateMessage(object_guid : PlanetSideGUID, pitch : Int, yaw : Int) => //log.info("ChildObjectState: " + msg) + case msg @ ProjectileStateMessage(projectile_guid, shot_pos, shot_vector, unk1, unk2, unk3, unk4, time_alive) => + //log.info("ProjectileState: " + msg) + case msg @ ChatMsg(messagetype, has_wide_contents, recipient, contents, note_contents) => // TODO: Prevents log spam, but should be handled correctly if (messagetype != ChatMessageType.CMT_TOGGLE_GM) {