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 + } +}