diff --git a/common/src/main/scala/net/psforever/packet/GamePacketOpcode.scala b/common/src/main/scala/net/psforever/packet/GamePacketOpcode.scala
index 5c29f2b09..14d667f9d 100644
--- a/common/src/main/scala/net/psforever/packet/GamePacketOpcode.scala
+++ b/common/src/main/scala/net/psforever/packet/GamePacketOpcode.scala
@@ -458,7 +458,7 @@ object GamePacketOpcode extends Enumeration {
case 0x73 => game.FriendsResponse.decode
case 0x74 => noDecoder(TriggerEnvironmentalDamageMessage)
case 0x75 => game.TrainingZoneMessage.decode
- case 0x76 => noDecoder(DeployableObjectsInfoMessage)
+ case 0x76 => game.DeployableObjectsInfoMessage.decode
case 0x77 => noDecoder(SquadState)
// 0x78
case 0x78 => noDecoder(OxygenStateMessage)
diff --git a/common/src/main/scala/net/psforever/packet/game/DeployableObjectsInfoMessage.scala b/common/src/main/scala/net/psforever/packet/game/DeployableObjectsInfoMessage.scala
new file mode 100644
index 000000000..4b5809992
--- /dev/null
+++ b/common/src/main/scala/net/psforever/packet/game/DeployableObjectsInfoMessage.scala
@@ -0,0 +1,109 @@
+// Copyright (c) 2017 PSForever
+package net.psforever.packet.game
+
+import net.psforever.packet.{GamePacketOpcode, Marshallable, PacketHelpers, PlanetSideGamePacket}
+import net.psforever.types.Vector3
+import scodec.Codec
+import scodec.codecs._
+
+/**
+ * An `Enumeration` of the actions that can be performed upon a deployable item.
+ */
+object DeploymentAction extends Enumeration {
+ type Type = Value
+
+ val Dismiss,
+ Build
+ = Value
+
+ implicit val codec = PacketHelpers.createEnumerationCodec(this, uint(1)) //no bool overload
+}
+
+/**
+ * An `Enumeration` of the map element icons that can be displayed based on the type of deployable item.
+ */
+object DeployableIcon extends Enumeration {
+ type Type = Value
+
+ val Boomer,
+ HEMine,
+ MotionAlarmSensor,
+ SpitfireTurret,
+ RouterTelepad,
+ DisruptorMine,
+ ShadowTurret,
+ CerebusTurret,
+ TRAP,
+ AegisShieldGenerator,
+ FieldTurret,
+ SensorDisruptor
+ = Value
+
+ implicit val codec = PacketHelpers.createEnumerationCodec(this, uint4L)
+}
+
+/**
+ * The entry of a deployable item.
+ * @param object_guid the deployable item
+ * @param icon the map element depicting the item
+ * @param pos the position of the deployable in the world (and on the map)
+ * @param player_guid the player who is the owner
+ */
+final case class DeployableInfo(object_guid : PlanetSideGUID,
+ icon : DeployableIcon.Value,
+ pos : Vector3,
+ player_guid : PlanetSideGUID)
+
+/**
+ * Dispatched by the server to inform the client of a change in deployable items and that the map should be updated.
+ *
+ * When this packet defines a `Build` `action`, an icon of the deployable item is added to the avatar's map.
+ * The actual object referenced does not have to actually exist on the client for the map element to appear.
+ * The identity of the element is discerned from its icon rather than the actual object (if it exists).
+ * When this packet defines a `Deconstruct` `action`, the icon of the deployable item is removed from the avatar's map.
+ * (The map icon to be removed is located by searching for the matching UID.
+ * The item does not need to exist to remove its icon.)
+ *
+ * All deployables have a map-icon-menu that allows for control of and some feedback about the item.
+ * At the very least, the item can be dismissed.
+ * The type of icon indicating the type of deployable item determines the map-icon-menu.
+ * Normally, the icon of a random (but friendly) deployable is gray and the menu is unresponsive.
+ * If the `player_guid` matches the client's avatar, the icon is yellow and that marks that the avatar owns this item.
+ * The avatar is capable of accessing the item's map-icon-menu and manipulating the item from there.
+ * If the deployable item actually doesn't exist, feedback is disabled, e.g., Aegis Shield Generators lack upgrade information.
+ * @param action how the entries in the following `List` are affected
+ * @param deployables a `List` of information regarding deployable items
+ */
+final case class DeployableObjectsInfoMessage(action : DeploymentAction.Value,
+ deployables : List[DeployableInfo]
+ ) extends PlanetSideGamePacket {
+ type Packet = DeployableObjectsInfoMessage
+ def opcode = GamePacketOpcode.DeployableObjectsInfoMessage
+ def encode = DeployableObjectsInfoMessage.encode(this)
+}
+
+object DeployableObjectsInfoMessage extends Marshallable[DeployableObjectsInfoMessage] {
+ /**
+ * Overloaded constructor that accepts a single `DeployableInfo` entry (and turns it into a `List`).
+ * @param action how the following entry is affected
+ * @param info the singular entry of a deployable item
+ * @return a `DeployableObjectsInfoMessage` object
+ */
+ def apply(action : DeploymentAction.Type, info : DeployableInfo) : DeployableObjectsInfoMessage =
+ new DeployableObjectsInfoMessage(action, info :: Nil)
+
+ /**
+ * `Codec` for `DeployableInfo` data.
+ */
+ private val info_codec : Codec[DeployableInfo] = (
+ ("object_guid" | PlanetSideGUID.codec) ::
+ ("icon" | DeployableIcon.codec) ::
+ ("pos" | Vector3.codec_pos) ::
+ ("player_guid" | PlanetSideGUID.codec)
+ ).as[DeployableInfo]
+
+ implicit val codec : Codec[DeployableObjectsInfoMessage] = (
+ ("action" | DeploymentAction.codec) ::
+ ("deployables" | PacketHelpers.listOfNAligned(uint32L, 0, info_codec))
+ ).as[DeployableObjectsInfoMessage]
+}
diff --git a/common/src/test/scala/game/DeployableObjectsInfoMessageTest.scala b/common/src/test/scala/game/DeployableObjectsInfoMessageTest.scala
new file mode 100644
index 000000000..fc2f3f3c3
--- /dev/null
+++ b/common/src/test/scala/game/DeployableObjectsInfoMessageTest.scala
@@ -0,0 +1,39 @@
+// Copyright (c) 2017 PSForever
+package game
+
+import org.specs2.mutable._
+import net.psforever.packet._
+import net.psforever.packet.game.{PlanetSideGUID, DeploymentAction, DeployableIcon, DeployableInfo, DeployableObjectsInfoMessage}
+import net.psforever.types.Vector3
+import scodec.bits._
+
+class DeployableObjectsInfoMessageTest extends Specification {
+ val string = hex"76 00 80 00 00 31 85 41 CF D3 7E B3 34 00 E6 30 48" //this was a TRAP @ Ogma, Forseral
+
+ "decode" in {
+ PacketCoding.DecodePacket(string).require match {
+ case DeployableObjectsInfoMessage(action, list) =>
+ action mustEqual DeploymentAction.Dismiss
+ list.size mustEqual 1
+ //0
+ list.head.object_guid mustEqual PlanetSideGUID(2659)
+ list.head.icon mustEqual DeployableIcon.TRAP
+ list.head.pos.x mustEqual 3572.4453f
+ list.head.pos.y mustEqual 3277.9766f
+ list.head.pos.z mustEqual 114.0f
+ list.head.player_guid mustEqual PlanetSideGUID(2502)
+ case _ =>
+ ko
+ }
+ }
+
+ "encode" in {
+ val msg = DeployableObjectsInfoMessage(
+ DeploymentAction.Dismiss,
+ DeployableInfo(PlanetSideGUID(2659), DeployableIcon.TRAP, Vector3(3572.4453f, 3277.9766f, 114.0f), PlanetSideGUID(2502))
+ )
+ val pkt = PacketCoding.EncodePacket(msg).require.toByteVector
+
+ pkt mustEqual string
+ }
+}