diff --git a/common/src/main/scala/net/psforever/packet/GamePacketOpcode.scala b/common/src/main/scala/net/psforever/packet/GamePacketOpcode.scala index 27ef110d..42f24ac6 100644 --- a/common/src/main/scala/net/psforever/packet/GamePacketOpcode.scala +++ b/common/src/main/scala/net/psforever/packet/GamePacketOpcode.scala @@ -344,7 +344,7 @@ object GamePacketOpcode extends Enumeration { case 0x13 => game.CharacterNoRecordMessage.decode case 0x14 => game.CharacterInfoMessage.decode case 0x15 => noDecoder(UnknownMessage21) - case 0x16 => noDecoder(BindPlayerMessage) + case 0x16 => game.BindPlayerMessage.decode case 0x17 => noDecoder(ObjectCreateMessage_Duplicate) // 0x18 case 0x18 => game.ObjectCreateMessage.decode diff --git a/common/src/main/scala/net/psforever/packet/game/BindPlayerMessage.scala b/common/src/main/scala/net/psforever/packet/game/BindPlayerMessage.scala new file mode 100644 index 00000000..e02e4844 --- /dev/null +++ b/common/src/main/scala/net/psforever/packet/game/BindPlayerMessage.scala @@ -0,0 +1,88 @@ +// Copyright (c) 2016 PSForever.net to present +package net.psforever.packet.game + +import net.psforever.packet.{GamePacketOpcode, Marshallable, PacketHelpers, PlanetSideGamePacket} +import net.psforever.types.Vector3 +import scodec.Codec +import scodec.codecs._ + +/** + * A packet dispatched to maintain a manually-set respawn location.
+ *
+ * The packet establishes the player's ability to spawn in an arbitrary location that is not a normal local option. + * This process is called "binding." + * In addition to player establishing the binding, the packet updates as conditions of the respawn location changes.
+ *
+ * If `logging` is turned on, packets will display different messages depending on context. + * As long as the event is marked to be logged, when the packet is received, a message is displayed in the events window. + * If the logged action is applicable, the matrixing sound effect will be played too. + * Not displaying events is occasionally warranted for aesthetics. + * The game will always note if this is your first time binding.
+ *
+ * One common occurrence of this packet is during zone transport. + * Specifically, a packet is dispatched after unloading the current zone but before beginning loading in the new zone. + * It is preceded by all of the `ObjectDeleteMessage` packets and itself precedes the `LoadMapMessage` packet.
+ *
+ * Actions:
+ * `1` - bound to respawn point
+ * `2` - general unbound / unbinding from respawn point
+ * `3` - respawn point lost
+ * `4` - bound spawn point became available
+ * `5` - bound spawn point became unavailable (different from 3)
+ *
+ * Bind Descriptors:
+ * `@amp_station`
+ * `@ams`
+ * `@comm_station` (interlink facility?)
+ * `@comm_station_dsp` (dropship center?)
+ * `@cryo_facility` (biolab?)
+ * `@tech_plant`
+ *
+ * Exploration:
+ * Find other bind descriptors. + * @param action the purpose of the packet + * @param bindDesc a description of the respawn binding point + * @param unk1 na; usually set `true` if there is more data in the packet ... + * @param logging true, to report on bind point change visible in the events window; + * false, to render spawn change silent; + * a first time event notification will always show + * @param unk2 na; if a value, it is usually 40 (hex`28`) + * @param unk3 na + * @param unk4 na + * @param pos a position associated with the binding + */ +final case class BindPlayerMessage(action : Int, + bindDesc : String, + unk1 : Boolean, + logging : Boolean, + unk2 : Int, + unk3 : Long, + unk4 : Long, + pos : Vector3) + extends PlanetSideGamePacket { + type Packet = BindPlayerMessage + def opcode = GamePacketOpcode.BindPlayerMessage + def encode = BindPlayerMessage.encode(this) +} + +object BindPlayerMessage extends Marshallable[BindPlayerMessage] { + /** + * A common variant of this packet. + * `16028004000000000000000000000000000000` + */ + val STANDARD = BindPlayerMessage(2, "", false, false, 2, 0, 0, Vector3(0, 0, 0)) + + //TODO: there are two ignore(1) in this packet; are they in a good position? + implicit val codec : Codec[BindPlayerMessage] = ( + ("action" | uint8L) :: + ("bindDesc" | PacketHelpers.encodedString) :: + ("unk1" | bool) :: + ("logging" | bool) :: + ignore(1) :: + ("unk2" | uint4L) :: + ignore(1) :: + ("unk3" | uint32L) :: + ("unk4" | uint32L) :: + ("pos" | Vector3.codec_pos) + ).as[BindPlayerMessage] +} diff --git a/common/src/test/scala/game/BindPlayerMessageTest.scala b/common/src/test/scala/game/BindPlayerMessageTest.scala new file mode 100644 index 00000000..558e7ec2 --- /dev/null +++ b/common/src/test/scala/game/BindPlayerMessageTest.scala @@ -0,0 +1,72 @@ +// Copyright (c) 2016 PSForever.net to present +package game + +import org.specs2.mutable._ +import net.psforever.packet._ +import net.psforever.packet.game._ +import net.psforever.types.Vector3 +import scodec.bits._ + +class BindPlayerMessageTest extends Specification { + val string_ams = hex"16 05 8440616D73 08 28000000 00000000 00000 00000 0000" + val string_tech = hex"16 01 8b40746563685f706c616e74 d4 28000000 38000000 00064 012b1 a044" + + "decode (ams)" in { + PacketCoding.DecodePacket(string_ams).require match { + case BindPlayerMessage(action, bindDesc, unk1, logging, unk2, unk3, unk4, pos) => + action mustEqual 5 + bindDesc.length mustEqual 4 + bindDesc mustEqual "@ams" + unk1 mustEqual false + logging mustEqual false + unk2 mustEqual 4 + unk3 mustEqual 40 + unk4 mustEqual 0 + pos.x mustEqual 0f + pos.y mustEqual 0f + pos.z mustEqual 0f + case _ => + ko + } + } + + "decode (tech)" in { + PacketCoding.DecodePacket(string_tech).require match { + case BindPlayerMessage(action, bindDesc, unk1, logging, unk2, unk3, unk4, pos) => + action mustEqual 1 + bindDesc.length mustEqual 11 + bindDesc mustEqual "@tech_plant" + unk1 mustEqual true + logging mustEqual true + unk2 mustEqual 10 + unk3 mustEqual 40 + unk4 mustEqual 56 + pos.x mustEqual 2060.0f + pos.y mustEqual 598.0078f + pos.z mustEqual 274.5f + case _ => + ko + } + } + + "encode (ams)" in { + val msg = BindPlayerMessage(5, "@ams", false, false, 4, 40, 0, Vector3(0, 0, 0)) + val pkt = PacketCoding.EncodePacket(msg).require.toByteVector + + pkt mustEqual string_ams + } + + "encode (tech)" in { + val msg = BindPlayerMessage(1, "@tech_plant", true, true, 10, 40, 56, Vector3(2060.0f, 598.0078f, 274.5f)) + val pkt = PacketCoding.EncodePacket(msg).require.toByteVector + + pkt mustEqual string_tech + } + + "standard" in { + val msg = BindPlayerMessage.STANDARD + val pkt = PacketCoding.EncodePacket(msg).require.toByteVector + + pkt mustEqual hex"16028004000000000000000000000000000000" + } +} diff --git a/pslogin/src/main/scala/WorldSessionActor.scala b/pslogin/src/main/scala/WorldSessionActor.scala index 8126ad8c..1af9aa6f 100644 --- a/pslogin/src/main/scala/WorldSessionActor.scala +++ b/pslogin/src/main/scala/WorldSessionActor.scala @@ -373,6 +373,9 @@ class WorldSessionActor extends Actor with MDCContextAware { case msg @ BugReportMessage(version_major,version_minor,version_date,bug_type,repeatable,location,zone,pos,summary,desc) => log.info("BugReportMessage: " + msg) + case msg @ BindPlayerMessage(action, bindDesc, unk1, logging, unk2, unk3, unk4, pos) => + log.info("BindPlayerMessage: " + msg) + case default => log.error(s"Unhandled GamePacket ${pkt}") }