diff --git a/common/src/main/scala/net/psforever/packet/GamePacketOpcode.scala b/common/src/main/scala/net/psforever/packet/GamePacketOpcode.scala index c13f9cec..ef274c09 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 => game.CreateShortcutMessage.decode case 0x29 => game.ChangeShortcutBankMessage.decode - 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..b8cf2bcc --- /dev/null +++ b/common/src/main/scala/net/psforever/packet/game/ObjectAttachMessage.scala @@ -0,0 +1,55 @@ +// Copyright (c) 2016 PSForever.net to present +package net.psforever.packet.game + +import net.psforever.packet.{GamePacketOpcode, Marshallable, PacketHelpers, 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 an `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:
+ * `0x80` - pistol slot 1
+ * `0x81` - pistol slot 2
+ * `0x82` - rifle slot 1
+ * `0x83` - rifle slot 2
+ * `0x84` - melee/knife slot
+ * `0x85` - mystery slot
+ * `0x86` - grid inventory (1,1)
+ * `0x00FA` is a special dest/extra code that "attaches the item to the player's cursor" + * @param player_guid the player + * @param item_guid the item + * @param slot a codified location within an inventory, and overlapping the player's holsters if need be; + * 8u (0 - 127 or `0x80 - 0xFF`) or + * 16u (128 - 32767 or `0x0080 - 0x7FFF`) + * @see `MoveItemMessage`, `objectcreate\ObjectClass.SLOT_BLOCKER` + */ +final case class ObjectAttachMessage(player_guid : PlanetSideGUID, + item_guid : PlanetSideGUID, + slot : 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) :: + ("slot" | PacketHelpers.encodedStringSize) + ).as[ObjectAttachMessage] +} diff --git a/common/src/test/scala/GamePacketTest.scala b/common/src/test/scala/GamePacketTest.scala index aebb8ab7..45feb507 100644 --- a/common/src/test/scala/GamePacketTest.scala +++ b/common/src/test/scala/GamePacketTest.scala @@ -317,7 +317,7 @@ class GamePacketTest extends Specification { shortcut.get.tile mustEqual "medkit" shortcut.get.effect1 mustEqual "" shortcut.get.effect2 mustEqual "" - case default => + case _ => ko } } @@ -347,7 +347,7 @@ class GamePacketTest extends Specification { unk mustEqual 0 addShortcut mustEqual false shortcut.isDefined mustEqual false - case default => + case _ => ko } } @@ -407,6 +407,47 @@ class GamePacketTest extends Specification { } } + "ObjectAttachMessage" should { + val stringToInventory = hex"2A 9F05 D405 86" + val stringToCursor = hex"2A 9F05 D405 00FA" + + "decode (inventory 1,1)" 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 6 + case default => + ko + } + } + + "decode (cursor)" in { + PacketCoding.DecodePacket(stringToCursor).require match { + case ObjectAttachMessage(player_guid, item_guid, index) => + player_guid mustEqual PlanetSideGUID(1439) + item_guid mustEqual PlanetSideGUID(1492) + index mustEqual 250 + case default => + ko + } + } + + "encode (inventory 1,1)" in { + val msg = ObjectAttachMessage(PlanetSideGUID(1439), PlanetSideGUID(1492), 6) + val pkt = PacketCoding.EncodePacket(msg).require.toByteVector + + pkt mustEqual stringToInventory + } + + "encode (cursor)" in { + val msg = ObjectAttachMessage(PlanetSideGUID(1439), PlanetSideGUID(1492), 250) + val pkt = PacketCoding.EncodePacket(msg).require.toByteVector + + pkt mustEqual stringToCursor + } + } + "ChangeShortcutBankMessage" should { val string = hex"29 4B00 20" @@ -997,6 +1038,8 @@ class GamePacketTest extends Specification { list(3).online mustEqual false list(4).name mustEqual "KurtHectic-G" list(4).online mustEqual false + case default => + ko } }