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}")
}