diff --git a/common/src/main/scala/net/psforever/packet/GamePacketOpcode.scala b/common/src/main/scala/net/psforever/packet/GamePacketOpcode.scala
index 6e9984e5..3fae9098 100644
--- a/common/src/main/scala/net/psforever/packet/GamePacketOpcode.scala
+++ b/common/src/main/scala/net/psforever/packet/GamePacketOpcode.scala
@@ -368,7 +368,7 @@ object GamePacketOpcode extends Enumeration {
// 0x28
case 0x28 => noDecoder(CreateShortcutMessage)
case 0x29 => noDecoder(ChangeShortcutBankMessage)
- case 0x2a => noDecoder(ObjectAttachMessage)
+ case 0x2a => game.ObjectAttachMessage.decode
case 0x2b => noDecoder(UnknownMessage43)
case 0x2c => noDecoder(PlanetsideAttributeMessage)
case 0x2d => game.RequestDestroyMessage.decode
diff --git a/common/src/main/scala/net/psforever/packet/game/ObjectAttachMessage.scala b/common/src/main/scala/net/psforever/packet/game/ObjectAttachMessage.scala
new file mode 100644
index 00000000..056900e5
--- /dev/null
+++ b/common/src/main/scala/net/psforever/packet/game/ObjectAttachMessage.scala
@@ -0,0 +1,60 @@
+// Copyright (c) 2016 PSForever.net to present
+package net.psforever.packet.game
+
+import net.psforever.packet.{GamePacketOpcode, Marshallable, PlanetSideGamePacket}
+import scodec.Codec
+import scodec.codecs._
+
+/**
+ * Change the location of an item within the game's inventory system.
+ *
+ * The data portion of this packet defines a player, an item, and a destination.
+ * After the packet is received by the client, the item will be guaranteed to be within the player's inventory in the codified location.
+ * The "inventory" in this case includes both the player's literal grid inventory and their available equipment slots.
+ * Where the item was before it was moved is not specified.
+ *
+ * This packet is a complementary packet that simulates a lazy TCP-like approach to coordinating item manipulation.
+ * In some cases, the client will (appear to) proceed with what it intended to do without waiting for the server to confirm.
+ * For example, grabbing an item from an inventory position will generate a `MoveItemMessage` that defines "the player's cursor" as a destination.
+ * The client will "attach to the player's cursor" without waiting for the `ObjectAttachMessage` from the server which echoes the destination.
+ * The change is observable.
+ * Inversely, the client is blocked from detaching the item from "the player's cursor" and putting it back into the inventory on its own.
+ * It waits until it receives an `ObjectAttachMessage` in confirmation.
+ *
+ * Destination codes:
+ * `80` is the first pistol slot
+ * `81` is the second pistol slot
+ * `82` is the first rifle slot
+ * `83` is the second rifle slot
+ * `86` is the first entry in the player's inventory
+ * `00 FA` is a special dest/extra code that "attaches the object to the player's cursor"
+ * @param player_guid the player GUID
+ * @param item_guid the item GUID
+ * @param dest a codified location within the player's inventory see above
+ * //@param extra optional; a special kind of item manipulation; the common one is `FA`
+ */
+final case class ObjectAttachMessage(player_guid : PlanetSideGUID,
+ item_guid : PlanetSideGUID,
+ dest : Int)
+ extends PlanetSideGamePacket {
+ type Packet = ObjectAttachMessage
+ def opcode = GamePacketOpcode.ObjectAttachMessage
+ def encode = ObjectAttachMessage.encode(this)
+}
+
+object ObjectAttachMessage extends Marshallable[ObjectAttachMessage] {
+// implicit val codec : Codec[ObjectAttachMessage] = (
+// ("player_guid" | PlanetSideGUID.codec) ::
+// ("item_guid" | PlanetSideGUID.codec) >>:~ { _ =>
+// ("dest" | uint8L) >>:~ ( loc =>
+// conditional(loc == 0, "extra" | uint8L)
+// )
+// }
+// ).as[ObjectAttachMessage]
+
+ implicit val codec : Codec[ObjectAttachMessage] = (
+ ("player_guid" | PlanetSideGUID.codec) ::
+ ("item_guid" | PlanetSideGUID.codec) ::
+ ("dest" | uint8L)
+ ).as[ObjectAttachMessage]
+}
diff --git a/common/src/test/scala/GamePacketTest.scala b/common/src/test/scala/GamePacketTest.scala
index 2f7e8a11..4c0cc329 100644
--- a/common/src/test/scala/GamePacketTest.scala
+++ b/common/src/test/scala/GamePacketTest.scala
@@ -299,6 +299,29 @@ class GamePacketTest extends Specification {
}
}
+ "ObjectAttachMessage" should {
+ val stringToInventory = hex"2A 9F05 D405 86"
+ val stringToCursor = hex"2A 9F05 D405 00FA"
+
+ "encode" in {
+ PacketCoding.DecodePacket(stringToInventory).require match {
+ case ObjectAttachMessage(player_guid, item_guid, index) =>
+ player_guid mustEqual PlanetSideGUID(1439)
+ item_guid mustEqual PlanetSideGUID(1492)
+ index mustEqual 134
+ case default =>
+ ko
+ }
+ }
+
+ "decode" in {
+ val msg = ObjectAttachMessage(PlanetSideGUID(1439), PlanetSideGUID(1492), 134)
+ val pkt = PacketCoding.EncodePacket(msg).require.toByteVector
+
+ pkt mustEqual stringToInventory
+ }
+ }
+
"DropItemMessage" should {
val string = hex"37 4C00"