diff --git a/common/src/main/scala/net/psforever/packet/GamePacketOpcode.scala b/common/src/main/scala/net/psforever/packet/GamePacketOpcode.scala
index cad228880..dcde70310 100644
--- a/common/src/main/scala/net/psforever/packet/GamePacketOpcode.scala
+++ b/common/src/main/scala/net/psforever/packet/GamePacketOpcode.scala
@@ -363,7 +363,7 @@ object GamePacketOpcode extends Enumeration {
case 0x23 => noDecoder(ActionCancelAcknowledgeMessage)
case 0x24 => game.SetEmpireMessage.decode
case 0x25 => game.EmoteMsg.decode
- case 0x26 => noDecoder(UnuseItemMessage)
+ case 0x26 => game.UnuseItemMessage.decode
case 0x27 => game.ObjectDetachMessage.decode
// 0x28
case 0x28 => game.CreateShortcutMessage.decode
@@ -392,7 +392,7 @@ object GamePacketOpcode extends Enumeration {
case 0x3c => game.GenericCollisionMsg.decode
case 0x3d => game.QuantityUpdateMessage.decode
case 0x3e => game.ArmorChangedMessage.decode
- case 0x3f => noDecoder(ProjectileStateMessage)
+ case 0x3f => game.ProjectileStateMessage.decode
// OPCODES 0x40-4f
case 0x40 => noDecoder(MountVehicleCargoMsg)
@@ -415,7 +415,7 @@ object GamePacketOpcode extends Enumeration {
// OPCODES 0x50-5f
case 0x50 => game.TargetingInfoMessage.decode
- case 0x51 => noDecoder(TriggerEffectMessage)
+ case 0x51 => game.TriggerEffectMessage.decode
case 0x52 => game.WeaponDryFireMessage.decode
case 0x53 => noDecoder(DroppodLaunchRequestMessage)
case 0x54 => noDecoder(HackMessage)
@@ -425,7 +425,7 @@ object GamePacketOpcode extends Enumeration {
// 0x58
case 0x58 => game.AvatarImplantMessage.decode
case 0x59 => noDecoder(UnknownMessage89)
- case 0x5a => noDecoder(DelayedPathMountMsg)
+ case 0x5a => game.DelayedPathMountMsg.decode
case 0x5b => noDecoder(OrbitalShuttleTimeMsg)
case 0x5c => noDecoder(AIDamage)
case 0x5d => game.DeployObjectMessage.decode
@@ -461,7 +461,7 @@ object GamePacketOpcode extends Enumeration {
case 0x76 => game.DeployableObjectsInfoMessage.decode
case 0x77 => noDecoder(SquadState)
// 0x78
- case 0x78 => noDecoder(OxygenStateMessage)
+ case 0x78 => game.OxygenStateMessage.decode
case 0x79 => noDecoder(TradeMessage)
case 0x7a => noDecoder(UnknownMessage122)
case 0x7b => noDecoder(DamageFeedbackMessage)
@@ -510,7 +510,7 @@ object GamePacketOpcode extends Enumeration {
// OPCODES 0xa0-af
case 0xa0 => game.BuildingInfoUpdateMessage.decode
- case 0xa1 => noDecoder(FireHintMessage)
+ case 0xa1 => game.FireHintMessage.decode
case 0xa2 => noDecoder(UplinkRequest)
case 0xa3 => noDecoder(UplinkResponse)
case 0xa4 => game.WarpgateRequest.decode
@@ -533,7 +533,7 @@ object GamePacketOpcode extends Enumeration {
case 0xb2 => game.VoiceHostInfo.decode
case 0xb3 => game.BattleplanMessage.decode
case 0xb4 => game.BattleExperienceMessage.decode
- case 0xb5 => noDecoder(TargetingImplantRequest)
+ case 0xb5 => game.TargetingImplantRequest.decode
case 0xb6 => game.ZonePopulationUpdateMessage.decode
case 0xb7 => game.DisconnectMessage.decode
// 0xb8
@@ -567,7 +567,7 @@ object GamePacketOpcode extends Enumeration {
// OPCODES 0xd0-df
case 0xd0 => noDecoder(UnknownMessage208)
- case 0xd1 => noDecoder(DisplayedAwardMessage)
+ case 0xd1 => game.DisplayedAwardMessage.decode
case 0xd2 => noDecoder(RespawnAMSInfoMessage)
case 0xd3 => noDecoder(ComponentDamageMessage)
case 0xd4 => noDecoder(GenericObjectActionAtPositionMessage)
diff --git a/common/src/main/scala/net/psforever/packet/game/DelayedPathMountMsg.scala b/common/src/main/scala/net/psforever/packet/game/DelayedPathMountMsg.scala
new file mode 100644
index 000000000..4c713e096
--- /dev/null
+++ b/common/src/main/scala/net/psforever/packet/game/DelayedPathMountMsg.scala
@@ -0,0 +1,32 @@
+// 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._
+
+/**
+ * na
+ * @param player_guid na
+ * @param vehicle_guid vehicle ?, turret ? Found a HART GUID for now. Need more search.
+ * @param u1 na - maybe a delay ?
+ * @param u2 na
+ */
+final case class DelayedPathMountMsg(player_guid : PlanetSideGUID,
+ vehicle_guid : PlanetSideGUID,
+ u1 : Int,
+ u2 : Boolean)
+ extends PlanetSideGamePacket {
+ type Packet = DelayedPathMountMsg
+ def opcode = GamePacketOpcode.DelayedPathMountMsg
+ def encode = DelayedPathMountMsg.encode(this)
+}
+
+object DelayedPathMountMsg extends Marshallable[DelayedPathMountMsg] {
+ implicit val codec : Codec[DelayedPathMountMsg] = (
+ ("player_guid" | PlanetSideGUID.codec) ::
+ ("vehicle_guid" | PlanetSideGUID.codec) ::
+ ("u1" | uint8L) ::
+ ("u2" | bool)
+ ).as[DelayedPathMountMsg]
+}
diff --git a/common/src/main/scala/net/psforever/packet/game/DisplayedAwardMessage.scala b/common/src/main/scala/net/psforever/packet/game/DisplayedAwardMessage.scala
new file mode 100644
index 000000000..2f7024adb
--- /dev/null
+++ b/common/src/main/scala/net/psforever/packet/game/DisplayedAwardMessage.scala
@@ -0,0 +1,60 @@
+// Copyright (c) 2017 PSForever
+package net.psforever.packet.game
+
+import net.psforever.types.MeritCommendation
+import net.psforever.packet.{GamePacketOpcode, Marshallable, PacketHelpers, PlanetSideGamePacket}
+import scodec.Codec
+import scodec.codecs._
+
+/**
+ * An `Enumeration` of the slots for award ribbons on a player's `RibbonBars`.
+ */
+object RibbonBarsSlot extends Enumeration {
+ type Type = Value
+
+ val Top,
+ Middle,
+ Bottom,
+ TermOfService //technically,the slot above "Top"
+ = Value
+
+ implicit val codec = PacketHelpers.createEnumerationCodec(this, uint4L)
+}
+
+/**
+ * Dispatched to configure a player's merit commendation ribbons.
+ *
+ * Normally, this packet is dispatched by the client when managing merit commendations through the "Character Info/Achievements" tab.
+ * On Gemini Live, this packet was also always dispatched once by the server during character login.
+ * It set the term of service ribbon explicitly.
+ * Generally, this was unnecessary, as the encoded character data maintains information about displayed ribbons.
+ * This behavior was probably a routine that ensured that correct yearly progression was tracked if the player earned it while offline.
+ * It never set any of the other ribbon slot positions during login.
+ *
+ * A specific ribbon may only be set once to one slot.
+ * The last set slot is considered the valid position to which that ribbon will be placed/moved.
+ * @param player_guid the player
+ * @param ribbon the award to be displayed;
+ * defaults to `MeritCommendation.None`;
+ * use `MeritCommendation.None` when indicating "no ribbon"
+ * @param bar any of the four positions where the award ribbon is to be displayed;
+ * defaults to `TermOfService`
+ * @see `RibbonBars`
+ * @see `MeritCommendation`
+ */
+final case class DisplayedAwardMessage(player_guid : PlanetSideGUID,
+ ribbon : MeritCommendation.Value = MeritCommendation.None,
+ bar : RibbonBarsSlot.Value = RibbonBarsSlot.TermOfService)
+ extends PlanetSideGamePacket {
+ type Packet = DisplayedAwardMessage
+ def opcode = GamePacketOpcode.DisplayedAwardMessage
+ def encode = DisplayedAwardMessage.encode(this)
+}
+
+object DisplayedAwardMessage extends Marshallable[DisplayedAwardMessage] {
+ implicit val codec : Codec[DisplayedAwardMessage] = (
+ ("player_guid" | PlanetSideGUID.codec) ::
+ ("ribbon" | MeritCommendation.codec) ::
+ ("bar" | RibbonBarsSlot.codec)
+ ).as[DisplayedAwardMessage]
+}
diff --git a/common/src/main/scala/net/psforever/packet/game/FireHintMessage.scala b/common/src/main/scala/net/psforever/packet/game/FireHintMessage.scala
new file mode 100644
index 000000000..53bbd519c
--- /dev/null
+++ b/common/src/main/scala/net/psforever/packet/game/FireHintMessage.scala
@@ -0,0 +1,44 @@
+// 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._
+
+/**
+ * not sure for u1 / u2 / u3, maybe need a real brain ...
+ * @param weapon_guid na
+ * @param pos na; pos of what ?!
+ * @param u1 na
+ * @param u2 na
+ * @param u3 na
+ * @param u4 na
+ * @param u5 na; vel of what ?!
+ */
+
+final case class FireHintMessage(weapon_guid : PlanetSideGUID,
+ pos : Vector3,
+ u1 : Int,
+ u2 : Int,
+ u3 : Int,
+ u4 : Int,
+ u5 : Option[Vector3] = None)
+ extends PlanetSideGamePacket {
+ type Packet = FireHintMessage
+ def opcode = GamePacketOpcode.FireHintMessage
+ def encode = FireHintMessage.encode(this)
+}
+
+object FireHintMessage extends Marshallable[FireHintMessage] {
+
+ implicit val codec : Codec[FireHintMessage] = (
+ ("weapon_guid" | PlanetSideGUID.codec) ::
+ ("pos" | Vector3.codec_pos) ::
+ ("u1" | uint16L) ::
+ ("u2" | uint16L) ::
+ ("u3" | uint16L) ::
+ ("u4" | uintL(3)) ::
+ optional(bool, "u5" | Vector3.codec_vel)
+ ).as[FireHintMessage]
+}
diff --git a/common/src/main/scala/net/psforever/packet/game/ObjectCreateDetailedMessage.scala b/common/src/main/scala/net/psforever/packet/game/ObjectCreateDetailedMessage.scala
index 2f2ab416e..4040714fe 100644
--- a/common/src/main/scala/net/psforever/packet/game/ObjectCreateDetailedMessage.scala
+++ b/common/src/main/scala/net/psforever/packet/game/ObjectCreateDetailedMessage.scala
@@ -55,8 +55,10 @@ object ObjectCreateDetailedMessage extends Marshallable[ObjectCreateDetailedMess
* @param data the data used to construct this type of object
* @return an ObjectCreateMessage
*/
- def apply(objectClass : Int, guid : PlanetSideGUID, parentInfo : ObjectCreateMessageParent, data : ConstructorData) : ObjectCreateDetailedMessage =
- ObjectCreateDetailedMessage(0L, objectClass, guid, Some(parentInfo), Some(data))
+ def apply(objectClass : Int, guid : PlanetSideGUID, parentInfo : ObjectCreateMessageParent, data : ConstructorData) : ObjectCreateDetailedMessage = {
+ val parentInfoOpt : Option[ObjectCreateMessageParent] = Some(parentInfo)
+ ObjectCreateDetailedMessage(ObjectCreateBase.streamLen(parentInfoOpt, data), objectClass, guid, parentInfoOpt, Some(data))
+ }
/**
* An abbreviated constructor for creating `ObjectCreateMessages`, ignoring `parentInfo`.
@@ -65,8 +67,9 @@ object ObjectCreateDetailedMessage extends Marshallable[ObjectCreateDetailedMess
* @param data the data used to construct this type of object
* @return an ObjectCreateMessage
*/
- def apply(objectClass : Int, guid : PlanetSideGUID, data : ConstructorData) : ObjectCreateDetailedMessage =
- ObjectCreateDetailedMessage(0L, objectClass, guid, None, Some(data))
+ def apply(objectClass : Int, guid : PlanetSideGUID, data : ConstructorData) : ObjectCreateDetailedMessage = {
+ ObjectCreateDetailedMessage(ObjectCreateBase.streamLen(None, data), objectClass, guid, None, Some(data))
+ }
/**
* Take the important information of a game piece and transform it into bit data.
diff --git a/common/src/main/scala/net/psforever/packet/game/OxygenStateMessage.scala b/common/src/main/scala/net/psforever/packet/game/OxygenStateMessage.scala
new file mode 100644
index 000000000..54da0ab08
--- /dev/null
+++ b/common/src/main/scala/net/psforever/packet/game/OxygenStateMessage.scala
@@ -0,0 +1,111 @@
+// Copyright (c) 2017 PSForever
+package net.psforever.packet.game
+
+import net.psforever.newcodecs.newcodecs
+import net.psforever.packet.{GamePacketOpcode, Marshallable, PlanetSideGamePacket}
+import scodec.{Attempt, Codec}
+import scodec.codecs._
+import shapeless.{::, HNil}
+
+/**
+ * Alert the condition of a vehicle the player is using when going too far underwater.
+ * The player must be mounted in/on this vehicle at start time for this countdown to display.
+ * @param vehicle_guid the player's mounted vehicle
+ * @param progress the remaining countdown;
+ * for vehicle waterlog condition, the progress per second rate is very high
+ * @param active show a new countdown if `true` (resets any active countdown);
+ * clear any active countdowns if `false`;
+ * defaults to `true`
+ */
+final case class WaterloggedVehicleState(vehicle_guid : PlanetSideGUID,
+ progress : Float,
+ active : Boolean = true)
+
+/**
+ * Dispatched by the server to cause the player to slowly drown.
+ * If the player is mounted in a vehicle at the time, alert the player that the vehicle may be disabled.
+ *
+ * When a player walks too far underwater, a borderless red progress bar with a countdown from 100 (98) is displayed across the screen.
+ * The countdown proceeds to zero at a fixed rate and is timed with the depleting progress bar.
+ * When it reaches zero, the player will be killed.
+ * If the player is in a vehicle after a certain depth, a blue bar and countdown pair will superimpose the red indicators.
+ * It depletes much more rapidly than the red indicators.
+ * When it reaches zero, the vehicle will become disabled.
+ * All players in the vehicle's seats will be kicked and they will not be allowed back in.
+ *
+ * Normally, the countdowns should be set to begin at 100 (100.0).
+ * This is the earliest the drowning GUI will appear for either blue or red indicators.
+ * Passing greater intervals - up to 204.8 - will start the countdown silently but the GUI will be hidden until 100.0.
+ * (The progress indicators will actually appear to start counting from 98.)
+ * Managing the secondary vehicle countdown independent of the primary player countdown requires updating with the correct levels.
+ * The countdown can be cancelled by instructing it to be `active = false`.
+ *
+ * Except for updating the indicators, all other functionality of "drowning" is automated by the server.
+ * @param player_guid the player
+ * @param progress the remaining countdown;
+ * for character oxygen, the progress per second rate is about 1
+ * @param active show a new countdown if `true` (resets any active countdown);
+ * clear any active countdowns if `false`
+ * @param vehicle_state optional state of the vehicle the player is driving
+ */
+final case class OxygenStateMessage(player_guid : PlanetSideGUID,
+ progress : Float,
+ active : Boolean,
+ vehicle_state : Option[WaterloggedVehicleState] = None)
+ extends PlanetSideGamePacket {
+ type Packet = OxygenStateMessage
+ def opcode = GamePacketOpcode.OxygenStateMessage
+ def encode = OxygenStateMessage.encode(this)
+}
+
+object OxygenStateMessage extends Marshallable[OxygenStateMessage] {
+ /**
+ * Overloaded constructor that removes the optional state of the `WaterloggedVehicleState` parameter.
+ * @param player_guid the player
+ * @param progress the remaining countdown
+ * @param active show or clear the countdown
+ * @param vehicle_state state of the vehicle the player is driving
+ * @return
+ */
+ def apply(player_guid : PlanetSideGUID, progress : Float, active : Boolean, vehicle_state : WaterloggedVehicleState) : OxygenStateMessage =
+ OxygenStateMessage(player_guid, progress, active, Some(vehicle_state))
+
+ /**
+ * A simple pattern that expands the datatypes of the packet's basic `Codec`.
+ */
+ private type basePattern = PlanetSideGUID :: Float :: Boolean :: HNil
+
+ /**
+ * A `Codec` for the repeated processing of three values.
+ * This `Codec` is the basis for the packet's data.
+ */
+ private val base_codec : Codec[basePattern] =
+ PlanetSideGUID.codec ::
+ newcodecs.q_float(0.0f, 204.8f, 11) :: //hackish: 2^11 == 2047, so it should be 204.7; but, 204.8 allows decode == encode
+ bool
+
+ implicit val codec : Codec[OxygenStateMessage] = (
+ base_codec.exmap[basePattern] (
+ {
+ case guid :: time :: active :: HNil =>
+ Attempt.successful(guid :: time :: active :: HNil)
+ },
+ {
+ case guid :: time :: active :: HNil =>
+ Attempt.successful(guid :: time :: active :: HNil)
+ }
+ ) :+
+ optional(bool,
+ "vehicle_state" | base_codec.exmap[WaterloggedVehicleState] (
+ {
+ case guid :: time :: active :: HNil =>
+ Attempt.successful(WaterloggedVehicleState(guid, time, active))
+ },
+ {
+ case WaterloggedVehicleState(guid, time, active) =>
+ Attempt.successful(guid :: time :: active :: HNil)
+ }
+ ).as[WaterloggedVehicleState]
+ )
+ ).as[OxygenStateMessage]
+}
diff --git a/common/src/main/scala/net/psforever/packet/game/ProjectileStateMessage.scala b/common/src/main/scala/net/psforever/packet/game/ProjectileStateMessage.scala
new file mode 100644
index 000000000..329d67429
--- /dev/null
+++ b/common/src/main/scala/net/psforever/packet/game/ProjectileStateMessage.scala
@@ -0,0 +1,65 @@
+// Copyright (c) 2017 PSForever
+package net.psforever.packet.game
+
+import net.psforever.packet.{GamePacketOpcode, Marshallable, PlanetSideGamePacket}
+import net.psforever.types.Vector3
+import scodec.Codec
+import scodec.codecs._
+
+/**
+ * Dispatched to deliberately render certain projectiles of a weapon on other players' clients.
+ *
+ * This packet is generated by firing specific weapons in specific fire modes.
+ * For example, the Phoenix (`hunterseeker`) discharged in its primary fire mode generates this packet;
+ * but, the Phoenix in secondary fire mode does not.
+ * The Striker (`striker`) discharged in its primary fire mode generates this packet;
+ * but, the Striker in secondary fire mode does not.
+ * The chosen fire mode(s) are not a straight-fire projectile but one that has special control asserted over it.
+ * For the Phoenix, it is user-operated.
+ * For the Striker, it tracks towards a target while the weapon's reticle hovers over that target.
+ *
+ * This packet will continue to be dispatched by the client for as long as the projectile being tracked is in the air.
+ * All projectiles have a maximum lifespan before they will lose control and either despawn and/or explode.
+ * This number is tracked in the packet for simplicity.
+ * If the projectile strikes a valid target, the count will jump to a significantly enormous value beyond its normal lifespan.
+ * This ensures that the projectile - locally and the shared model - will despawn.
+ * @param projectile_guid the projectile
+ * @param shot_pos the position of the projectile
+ * @param shot_vel the velocity of the projectile
+ * @param unk1 na;
+ * usually 0
+ * @param unk2 na;
+ * will remain consistent for the lifespan of a given projectile in most cases
+ * @param unk3 na;
+ * will remain consistent for the lifespan of a given projectile in most cases
+ * @param unk4 na;
+ * usually false
+ * @param time_alive how long the projectile has been in the air;
+ * often expressed in multiples of 2
+ */
+final case class ProjectileStateMessage(projectile_guid : PlanetSideGUID,
+ shot_pos : Vector3,
+ shot_vel : Vector3,
+ unk1 : Int,
+ unk2 : Int,
+ unk3 : Int,
+ unk4 : Boolean,
+ time_alive : Int)
+ extends PlanetSideGamePacket {
+ type Packet = ProjectileStateMessage
+ def opcode = GamePacketOpcode.ProjectileStateMessage
+ def encode = ProjectileStateMessage.encode(this)
+}
+
+object ProjectileStateMessage extends Marshallable[ProjectileStateMessage] {
+ implicit val codec : Codec[ProjectileStateMessage] = (
+ ("projectile_guid" | PlanetSideGUID.codec) ::
+ ("shot_pos" | Vector3.codec_pos) ::
+ ("shot_vel" | Vector3.codec_float) ::
+ ("unk1" | uint8L) ::
+ ("unk2" | uint8L) ::
+ ("unk3" | uint8L) ::
+ ("unk4" | bool) ::
+ ("time_alive" | uint16L)
+ ).as[ProjectileStateMessage]
+}
diff --git a/common/src/main/scala/net/psforever/packet/game/TargetingImplantRequest.scala b/common/src/main/scala/net/psforever/packet/game/TargetingImplantRequest.scala
new file mode 100644
index 000000000..63cceeb65
--- /dev/null
+++ b/common/src/main/scala/net/psforever/packet/game/TargetingImplantRequest.scala
@@ -0,0 +1,36 @@
+// Copyright (c) 2017 PSForever
+package net.psforever.packet.game
+
+import net.psforever.packet.{GamePacketOpcode, Marshallable, PlanetSideGamePacket}
+import scodec.Codec
+import scodec.codecs._
+
+/**
+ * An entry regarding a specific target.
+ * @param target_guid the target
+ * @param unk na
+ */
+final case class TargetRequest(target_guid : PlanetSideGUID,
+ unk : Boolean)
+
+/**
+ * Dispatched by the client when the advanced targeting implant activates to collect status information from the server.
+ *
+ * This packet is answered by a `TargetingInfoMessage` with `List` entries of thed corresponding UIDs.
+ * @param target_list a `List` of targets
+ */
+final case class TargetingImplantRequest(target_list : List[TargetRequest])
+ extends PlanetSideGamePacket {
+ type Packet = TargetingImplantRequest
+ def opcode = GamePacketOpcode.TargetingImplantRequest
+ def encode = TargetingImplantRequest.encode(this)
+}
+
+object TargetingImplantRequest extends Marshallable[TargetingImplantRequest] {
+ private val request_codec : Codec[TargetRequest] = (
+ ("target_guid" | PlanetSideGUID.codec) ::
+ ("unk" | bool)
+ ).as[TargetRequest]
+
+ implicit val codec : Codec[TargetingImplantRequest] = ("target_list" | listOfN(intL(6), request_codec)).as[TargetingImplantRequest]
+}
diff --git a/common/src/main/scala/net/psforever/packet/game/TriggerEffectMessage.scala b/common/src/main/scala/net/psforever/packet/game/TriggerEffectMessage.scala
new file mode 100644
index 000000000..a8c2cc9be
--- /dev/null
+++ b/common/src/main/scala/net/psforever/packet/game/TriggerEffectMessage.scala
@@ -0,0 +1,83 @@
+// 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._
+
+/**
+ * na
+ * @param unk1 na;
+ * `true` to apply the effect usually
+ * @param unk2 na
+ */
+final case class TriggeredEffect(unk1 : Boolean,
+ unk2 : Long)
+
+/**
+ * Activate an effect that is not directly associated with an existing game object.
+ * Without a game object from which to inherit position and orientation, those explicit parameters must be provided.
+ * @param pos the position in the game world
+ * @param roll the amount of roll that affects orientation
+ * @param pitch the amount of pitch that affects orientation
+ * @param yaw the amount of yaw that affects orientation
+ */
+final case class TriggeredEffectLocation(pos : Vector3,
+ roll : Int,
+ pitch : Int,
+ yaw : Int)
+
+/**
+ * Dispatched by the server to cause a client to display a special graphical effect.
+ *
+ * The effect being triggered can be based around a specific game object or replayed freely, absent of an anchoring object.
+ * If object-based then the kinds of effects that can be activated are specific to the object.
+ * If unbound, then a wider range of effects can be displayed.
+ * Regardless, one category will rarely ever be activated under the same valid conditions of the other category.
+ * For example, the effect "on" will only work on objects that accept "on" normally, like a deployed `motionalarmsensor`.
+ * The effect "spawn_object_effect" can be applied anywhere in the environment;
+ * but, it can not be activated in conjunction with an existing object.
+ * @param obj an object that accepts the effect
+ * @param effect the name of the effect
+ * @param unk na;
+ * when activating an effect on an existing object
+ * @param location an optional position where the effect will be displayed;
+ * when activating an effect independently
+ */
+final case class TriggerEffectMessage(obj : PlanetSideGUID,
+ effect : String,
+ unk : Option[TriggeredEffect] = None,
+ location : Option[TriggeredEffectLocation] = None
+ ) extends PlanetSideGamePacket {
+ type Packet = TriggerEffectMessage
+ def opcode = GamePacketOpcode.TriggerEffectMessage
+ def encode = TriggerEffectMessage.encode(this)
+}
+
+object TriggerEffectMessage extends Marshallable[TriggerEffectMessage] {
+ /**
+ * A `Codec` for `TriggeredEffect` data.
+ */
+ private val effect_codec : Codec[TriggeredEffect] = (
+ ("unk1" | bool) ::
+ ("unk2" | uint32L)
+ ).as[TriggeredEffect]
+
+ /**
+ * A `Codec` for `TriggeredEffectLocation` data.
+ */
+ private val effect_location_codec : Codec[TriggeredEffectLocation] = (
+ ("pos" | Vector3.codec_pos) ::
+ ("roll" | uint8L) ::
+ ("pitch" | uint8L) ::
+ ("yaw" | uint8L)
+ ).as[TriggeredEffectLocation]
+
+ implicit val codec : Codec[TriggerEffectMessage] = (
+ ("obj" | PlanetSideGUID.codec) >>:~ { obj =>
+ ("effect" | PacketHelpers.encodedString) ::
+ optional(bool, "unk" | effect_codec) ::
+ conditional(obj.guid == 0, "location" | effect_location_codec)
+ }).as[TriggerEffectMessage]
+}
diff --git a/common/src/main/scala/net/psforever/packet/game/UnuseItemMessage.scala b/common/src/main/scala/net/psforever/packet/game/UnuseItemMessage.scala
new file mode 100644
index 000000000..63433ded8
--- /dev/null
+++ b/common/src/main/scala/net/psforever/packet/game/UnuseItemMessage.scala
@@ -0,0 +1,30 @@
+// Copyright (c) 2017 PSForever
+package net.psforever.packet.game
+
+import net.psforever.packet.{GamePacketOpcode, Marshallable, PlanetSideGamePacket}
+import scodec.Codec
+import scodec.codecs._
+
+/**
+ * Dispatched by the client when its player is done using something.
+ *
+ * The common example is sifting through backpacks, an activity that only one player is allowed to do at a time.
+ * When a backpack is accessed by one player, other players are blocked.
+ * When the first player is done accessing the backpack, this packet informs the server so other players may be allowed access.
+ * @param player_guid the player
+ * @param item_guid the item
+ */
+final case class UnuseItemMessage(player_guid : PlanetSideGUID,
+ item_guid : PlanetSideGUID)
+ extends PlanetSideGamePacket {
+ type Packet = UnuseItemMessage
+ def opcode = GamePacketOpcode.UnuseItemMessage
+ def encode = UnuseItemMessage.encode(this)
+}
+
+object UnuseItemMessage extends Marshallable[UnuseItemMessage] {
+ implicit val codec : Codec[UnuseItemMessage] = (
+ ("player_guid" | PlanetSideGUID.codec) ::
+ ("item_guid" | PlanetSideGUID.codec)
+ ).as[UnuseItemMessage]
+}
diff --git a/common/src/main/scala/net/psforever/packet/game/objectcreate/RibbonBars.scala b/common/src/main/scala/net/psforever/packet/game/objectcreate/RibbonBars.scala
index 305d9a488..83949ee51 100644
--- a/common/src/main/scala/net/psforever/packet/game/objectcreate/RibbonBars.scala
+++ b/common/src/main/scala/net/psforever/packet/game/objectcreate/RibbonBars.scala
@@ -2,35 +2,33 @@
package net.psforever.packet.game.objectcreate
import net.psforever.packet.Marshallable
+import net.psforever.types.MeritCommendation
import scodec.Codec
import scodec.codecs._
/**
* Enumerate the player-displayed merit commendation awards granted for excellence (or tenacity) in combat.
- * These are the medals players wish to brandish on their left pauldron.
- *
- * All merit commendation ribbons are represented by a 32-bit signature.
- * The default "no-ribbon" value is `0xFFFFFFFF`, although some illegal values will also work.
- * The term of service ribbon can not be modified by the user and will apply itself to its slot automatically when valid.
+ * These are the medals players wish to brandish on their left pauldron.
* @param upper the "top" configurable merit ribbon
* @param middle the central configurable merit ribbon
* @param lower the lower configurable merit ribbon
* @param tos the top-most term of service merit ribbon
+ * @see `MeritCommendation`
+ * @see `DisplayedAwardMessage`
*/
-final case class RibbonBars(upper : Long = RibbonBars.noRibbon,
- middle : Long = RibbonBars.noRibbon,
- lower : Long = RibbonBars.noRibbon,
- tos : Long = RibbonBars.noRibbon) extends StreamBitSize {
+final case class RibbonBars(upper : MeritCommendation.Value = MeritCommendation.None,
+ middle : MeritCommendation.Value = MeritCommendation.None,
+ lower : MeritCommendation.Value = MeritCommendation.None,
+ tos : MeritCommendation.Value = MeritCommendation.None
+ ) extends StreamBitSize {
override def bitsize : Long = 128L
}
object RibbonBars extends Marshallable[RibbonBars] {
- val noRibbon : Long = 0xFFFFFFFFL
-
implicit val codec : Codec[RibbonBars] = (
- ("upper" | uint32L) ::
- ("middle" | uint32L) ::
- ("lower" | uint32L) ::
- ("tos" | uint32L)
+ ("upper" | MeritCommendation.codec) ::
+ ("middle" | MeritCommendation.codec) ::
+ ("lower" | MeritCommendation.codec) ::
+ ("tos" | MeritCommendation.codec)
).as[RibbonBars]
}
diff --git a/common/src/main/scala/net/psforever/types/MeritCommendation.scala b/common/src/main/scala/net/psforever/types/MeritCommendation.scala
new file mode 100644
index 000000000..7dd056de4
--- /dev/null
+++ b/common/src/main/scala/net/psforever/types/MeritCommendation.scala
@@ -0,0 +1,522 @@
+// Copyright (c) 2017 PSForever
+package net.psforever.types
+
+import scodec.{Attempt, Err}
+import scodec.codecs._
+
+/**
+ * An `Enumeration` of all merit commendation award categories organized into associated ribbons.
+ * By astonishing coincidence, with exception of the first ten special awards, the rest of list is in alphabetical order.
+ */
+object MeritCommendation extends Enumeration {
+ type Type = Value
+
+ val //0
+ FanFaire2005Commander,
+ FanFaire2005Soldier,
+ FanFaire2006Atlanta,
+ HalloweenMassacre2006NC,
+ HalloweenMassacre2006TR,
+ HalloweenMassacre2006VS,
+ FanFaire2007,
+ FanFaire2008,
+ FanFaire2009,
+ AdvancedMedic1,
+ //10
+ AdvancedMedic2,
+ AdvancedMedic3,
+ AdvancedMedic4,
+ AdvancedMedic5,
+ AdvancedMedic6,
+ AdvancedMedic7,
+ AdvancedMedicAssists1,
+ AdvancedMedicAssists2,
+ AdvancedMedicAssists3,
+ AdvancedMedicAssists4,
+ //20
+ AdvancedMedicAssists5,
+ AdvancedMedicAssists6,
+ AdvancedMedicAssists7,
+ AirDefender1,
+ AirDefender2,
+ AirDefender3,
+ AirDefender4,
+ AirDefender5,
+ AirDefender6,
+ AirDefender7,
+ //30
+ AMSSupport1,
+ AMSSupport2,
+ AMSSupport3,
+ AMSSupport4,
+ AMSSupport5,
+ AMSSupport6,
+ AMSSupport7,
+ AntiVehicular1,
+ AntiVehicular2,
+ AntiVehicular3,
+ //40
+ AntiVehicular4,
+ AntiVehicular5,
+ AntiVehicular6,
+ AntiVehicular7,
+ Avenger1,
+ Avenger2,
+ Avenger3,
+ Avenger4,
+ Avenger5,
+ Avenger6,
+ //50
+ Avenger7,
+ AwardColors, //what?
+ BendingMovieActor,
+ BFRAdvanced,
+ BFRAdvanced2,
+ BFRAdvanced3,
+ BFRAdvanced4,
+ BFRAdvanced5,
+ BFRBuster1,
+ BFRBuster2,
+ //60
+ BFRBuster3,
+ BFRBuster4,
+ BFRBuster5,
+ BFRBuster6,
+ BFRBuster7,
+ BlackOpsHunter1,
+ BlackOpsHunter2,
+ BlackOpsHunter3,
+ BlackOpsHunter4,
+ BlackOpsHunter5,
+ //70
+ BlackOpsParticipant,
+ BlackOpsVictory,
+ Bombadier1,
+ Bombadier2,
+ Bombadier3,
+ Bombadier4,
+ Bombadier5,
+ Bombadier6,
+ Bombadier7,
+ BomberAce1,
+ //80
+ BomberAce2,
+ BomberAce3,
+ BomberAce4,
+ BomberAce5,
+ BomberAce6,
+ BomberAce7,
+ Boomer1,
+ Boomer2,
+ Boomer3,
+ Boomer4,
+ //90
+ Boomer5,
+ Boomer6,
+ Boomer7,
+ CalvaryDriver1,
+ CalvaryDriver2,
+ CalvaryDriver3,
+ CalvaryDriver4,
+ CalvaryDriver5,
+ CalvaryDriver6,
+ CalvaryDriver7,
+ //100
+ CalvaryPilot,
+ CalvaryPilot2,
+ CalvaryPilot3,
+ CalvaryPilot4,
+ CalvaryPilot5,
+ CalvaryPilot6,
+ CalvaryPilot7,
+ CMTopOutfit,
+ CombatMedic,
+ CombatMedic2,
+ //110
+ CombatMedic3,
+ CombatMedic4,
+ CombatMedic5,
+ CombatMedic6,
+ CombatMedic7,
+ CombatRepair1,
+ CombatRepair2,
+ CombatRepair3,
+ CombatRepair4,
+ CombatRepair5,
+ //120
+ CombatRepair6,
+ CombatRepair7,
+ ContestFirstBR40,
+ ContestMovieMaker,
+ ContestMovieMakerOutfit,
+ ContestPlayerOfTheMonth,
+ ContestPlayerOfTheYear,
+ CSAppreciation,
+ DefenseNC1,
+ DefenseNC2,
+ //130
+ DefenseNC3,
+ DefenseNC4,
+ DefenseNC5,
+ DefenseNC6,
+ DefenseNC7,
+ DefenseTR1,
+ DefenseTR2,
+ DefenseTR3,
+ DefenseTR4,
+ DefenseTR5,
+ //40
+ DefenseTR6,
+ DefenseTR7,
+ DefenseVS1,
+ DefenseVS2,
+ DefenseVS3,
+ DefenseVS4,
+ DefenseVS5,
+ DefenseVS6,
+ DefenseVS7,
+ DevilDogsMovie,
+ //150
+ DogFighter1,
+ DogFighter2,
+ DogFighter3,
+ DogFighter4,
+ DogFighter5,
+ DogFighter6,
+ DogFighter7,
+ DriverGunner1,
+ DriverGunner2,
+ DriverGunner3,
+ //160
+ DriverGunner4,
+ DriverGunner5,
+ DriverGunner6,
+ DriverGunner7,
+ EliteAssault0,
+ EliteAssault1,
+ EliteAssault2,
+ EliteAssault3,
+ EliteAssault4,
+ EliteAssault5,
+ //170
+ EliteAssault6,
+ EliteAssault7,
+ EmeraldVeteran,
+ Engineer1,
+ Engineer2,
+ Engineer3,
+ Engineer4,
+ Engineer5,
+ Engineer6,
+ EquipmentSupport1,
+ //180
+ EquipmentSupport2,
+ EquipmentSupport3,
+ EquipmentSupport4,
+ EquipmentSupport5,
+ EquipmentSupport6,
+ EquipmentSupport7,
+ EventNCCommander,
+ EventNCElite,
+ EventNCSoldier,
+ EventTRCommander,
+ //190
+ EventTRElite,
+ EventTRSoldier,
+ EventVSCommander,
+ EventVSElite,
+ EventVSSoldier,
+ Explorer1,
+ FiveYearNC,
+ FiveYearTR,
+ FiveYearVS,
+ FourYearNC,
+ //200
+ FourYearTR,
+ FourYearVS,
+ GalaxySupport1,
+ GalaxySupport2,
+ GalaxySupport3,
+ GalaxySupport4,
+ GalaxySupport5,
+ GalaxySupport6,
+ GalaxySupport7,
+ Grenade1,
+ //210
+ Grenade2,
+ Grenade3,
+ Grenade4,
+ Grenade5,
+ Grenade6,
+ Grenade7,
+ GroundGunner1,
+ GroundGunner2,
+ GroundGunner3,
+ GroundGunner4,
+ //220
+ GroundGunner5,
+ GroundGunner6,
+ GroundGunner7,
+ HackingSupport1,
+ HackingSupport2,
+ HackingSupport3,
+ HackingSupport4,
+ HackingSupport5,
+ HackingSupport6,
+ HackingSupport7,
+ //230
+ HeavyAssault1,
+ HeavyAssault2,
+ HeavyAssault3,
+ HeavyAssault4,
+ HeavyAssault5,
+ HeavyAssault6,
+ HeavyAssault7,
+ HeavyInfantry,
+ HeavyInfantry2,
+ HeavyInfantry3,
+ //240
+ HeavyInfantry4,
+ InfantryExpert1,
+ InfantryExpert2,
+ InfantryExpert3,
+ Jacking,
+ Jacking2,
+ Jacking3,
+ Jacking4,
+ Jacking5,
+ Jacking6,
+ //250
+ Jacking7,
+ KnifeCombat1,
+ KnifeCombat2,
+ KnifeCombat3,
+ KnifeCombat4,
+ KnifeCombat5,
+ KnifeCombat6,
+ KnifeCombat7,
+ LightInfantry,
+ LockerCracker1,
+ //260
+ LockerCracker2,
+ LockerCracker3,
+ LockerCracker4,
+ LockerCracker5,
+ LockerCracker6,
+ LockerCracker7,
+ LodestarSupport1,
+ LodestarSupport2,
+ LodestarSupport3,
+ LodestarSupport4,
+ //270
+ LodestarSupport5,
+ LodestarSupport6,
+ LodestarSupport7,
+ Loser,
+ Loser2,
+ Loser3,
+ Loser4,
+ MarkovVeteran,
+ Max1,
+ Max2,
+ //280
+ Max3,
+ Max4,
+ Max5,
+ Max6,
+ MaxBuster1,
+ MaxBuster2,
+ MaxBuster3,
+ MaxBuster4,
+ MaxBuster5,
+ MaxBuster6,
+ //290
+ MediumAssault1,
+ MediumAssault2,
+ MediumAssault3,
+ MediumAssault4,
+ MediumAssault5,
+ MediumAssault6,
+ MediumAssault7,
+ OneYearNC,
+ OneYearTR,
+ OneYearVS,
+ //300
+ Orion1,
+ Orion2,
+ Orion3,
+ Orion4,
+ Orion5,
+ Orion6,
+ Orion7,
+ Osprey1,
+ Osprey2,
+ Osprey3,
+ //310
+ Osprey4,
+ Osprey5,
+ Osprey6,
+ Osprey7,
+ Phalanx1,
+ Phalanx2,
+ Phalanx3,
+ Phalanx4,
+ Phalanx5,
+ Phalanx6,
+ //320
+ Phalanx7,
+ PSUMaAttendee,
+ PSUMbAttendee,
+ QAAppreciation,
+ ReinforcementHackSpecialist,
+ ReinforcementInfantrySpecialist,
+ ReinforcementSpecialist,
+ ReinforcementVehicleSpecialist,
+ RouterSupport1,
+ RouterSupport2,
+ //330
+ RouterSupport3,
+ RouterSupport4,
+ RouterSupport5,
+ RouterSupport6,
+ RouterSupport7,
+ RouterTelepadDeploy1,
+ RouterTelepadDeploy2,
+ RouterTelepadDeploy3,
+ RouterTelepadDeploy4,
+ RouterTelepadDeploy5,
+ //340
+ RouterTelepadDeploy6,
+ RouterTelepadDeploy7,
+ ScavengerNC1,
+ ScavengerNC2,
+ ScavengerNC3,
+ ScavengerNC4,
+ ScavengerNC5,
+ ScavengerNC6,
+ ScavengerTR1,
+ ScavengerTR2,
+ //350
+ ScavengerTR3,
+ ScavengerTR4,
+ ScavengerTR5,
+ ScavengerTR6,
+ ScavengerVS1,
+ ScavengerVS2,
+ ScavengerVS3,
+ ScavengerVS4,
+ ScavengerVS5,
+ ScavengerVS6,
+ //360
+ SixYearNC,
+ SixYearTR,
+ SixYearVS,
+ Sniper1,
+ Sniper2,
+ Sniper3,
+ Sniper4,
+ Sniper5,
+ Sniper6,
+ Sniper7,
+ //370
+ SpecialAssault1,
+ SpecialAssault2,
+ SpecialAssault3,
+ SpecialAssault4,
+ SpecialAssault5,
+ SpecialAssault6,
+ SpecialAssault7,
+ StandardAssault1,
+ StandardAssault2,
+ StandardAssault3,
+ //380
+ StandardAssault4,
+ StandardAssault5,
+ StandardAssault6,
+ StandardAssault7,
+ StracticsHistorian,
+ Supply1,
+ Supply2,
+ Supply3,
+ Supply4,
+ Supply5,
+ //390
+ Supply6,
+ Supply7,
+ TankBuster1,
+ TankBuster2,
+ TankBuster3,
+ TankBuster4,
+ TankBuster5,
+ TankBuster6,
+ TankBuster7,
+ ThreeYearNC,
+ //400
+ ThreeYearTR,
+ ThreeYearVS,
+ TinyRoboticSupport1,
+ TinyRoboticSupport2,
+ TinyRoboticSupport3,
+ TinyRoboticSupport4,
+ TinyRoboticSupport5,
+ TinyRoboticSupport6,
+ TinyRoboticSupport7,
+ Transport1,
+ //410
+ Transport2,
+ Transport3,
+ Transport4,
+ Transport5,
+ Transport6,
+ Transport7,
+ TransportationCitation1,
+ TransportationCitation2,
+ TransportationCitation3,
+ TransportationCitation4,
+ //420
+ TransportationCitation5,
+ TwoYearNC,
+ TwoYearTR,
+ TwoYearVS,
+ ValentineFemale,
+ ValentineMale,
+ WernerVeteran,
+ XmasGingerman,
+ XmasSnowman,
+ XmasSpirit
+ = Value
+
+ /*
+ The value None requires special consideration.
+ - A Long number is required for this Enumeration codec.
+ - Enumerations are designed to only handle Int numbers.
+ - A Codec only handles unsigned numbers.
+ - The value of MeritCommendation.None is intended to be 0xFFFFFFFF, which (a) is 4294967295 as a Long, but (b) is -1 as an Integer.
+ - Due to (a), an Enumeration can not be used to represent that number.
+ - Due to (b), a Codec can not be used to convert to that number.
+ */
+ val None = Value(-1)
+
+ /**
+ * Carefully and explicitly convert between `Codec[Long] -> Long -> Int -> MeritCommendation.Value`.
+ */
+ implicit val codec = uint32L.exmap[MeritCommendation.Value] (
+ {
+ case 0xFFFFFFFFL =>
+ Attempt.successful(MeritCommendation.None)
+ case n =>
+ if(n > Int.MaxValue) {
+ Attempt.failure(Err(s"value $n is too high, above maximum integer value ${Int.MaxValue}"))
+ }
+ else {
+ Attempt.successful(MeritCommendation(n.toInt))
+ }
+ },
+ {
+ case MeritCommendation.None =>
+ Attempt.successful(0xFFFFFFFFL)
+ case enum =>
+ Attempt.successful(enum.id.toLong)
+ }
+ )
+}
diff --git a/common/src/test/scala/game/DelayedPathMountMsgTest.scala b/common/src/test/scala/game/DelayedPathMountMsgTest.scala
new file mode 100644
index 000000000..14bc82bba
--- /dev/null
+++ b/common/src/test/scala/game/DelayedPathMountMsgTest.scala
@@ -0,0 +1,30 @@
+// Copyright (c) 2017 PSForever
+package game
+
+import org.specs2.mutable._
+import net.psforever.packet._
+import net.psforever.packet.game._
+import scodec.bits._
+
+class DelayedPathMountMsgTest extends Specification {
+ val string = hex"5a f50583044680"
+
+ "decode" in {
+ PacketCoding.DecodePacket(string).require match {
+ case DelayedPathMountMsg(player_guid, vehicle_guid,u3,u4) =>
+ player_guid mustEqual PlanetSideGUID(1525)
+ vehicle_guid mustEqual PlanetSideGUID(1155)
+ u3 mustEqual 70
+ u4 mustEqual true
+ case _ =>
+ ko
+ }
+ }
+
+ "encode" in {
+ val msg = DelayedPathMountMsg(PlanetSideGUID(1525), PlanetSideGUID(1155),70,true)
+ val pkt = PacketCoding.EncodePacket(msg).require.toByteVector
+
+ pkt mustEqual string
+ }
+}
diff --git a/common/src/test/scala/game/DisplayedAwardMessageTest.scala b/common/src/test/scala/game/DisplayedAwardMessageTest.scala
new file mode 100644
index 000000000..2c8a58517
--- /dev/null
+++ b/common/src/test/scala/game/DisplayedAwardMessageTest.scala
@@ -0,0 +1,30 @@
+// Copyright (c) 2017 PSForever
+package game
+
+import net.psforever.types.MeritCommendation
+import org.specs2.mutable._
+import net.psforever.packet._
+import net.psforever.packet.game._
+import scodec.bits._
+
+class DisplayedAwardMessageTest extends Specification {
+ val string = hex"D1 9F06 A6010000 3 0"
+
+ "decode" in {
+ PacketCoding.DecodePacket(string).require match {
+ case DisplayedAwardMessage(player_guid, ribbon, bar) =>
+ player_guid mustEqual PlanetSideGUID(1695)
+ ribbon mustEqual MeritCommendation.TwoYearTR
+ bar mustEqual RibbonBarsSlot.TermOfService
+ case _ =>
+ ko
+ }
+ }
+
+ "encode" in {
+ val msg = DisplayedAwardMessage(PlanetSideGUID(1695), MeritCommendation.TwoYearTR, RibbonBarsSlot.TermOfService)
+ val pkt = PacketCoding.EncodePacket(msg).require.toByteVector
+
+ pkt mustEqual string
+ }
+}
diff --git a/common/src/test/scala/game/FireHintMessageTest.scala b/common/src/test/scala/game/FireHintMessageTest.scala
new file mode 100644
index 000000000..9893694c1
--- /dev/null
+++ b/common/src/test/scala/game/FireHintMessageTest.scala
@@ -0,0 +1,55 @@
+// Copyright (c) 2017 PSForever
+package game
+
+import org.specs2.mutable._
+import net.psforever.packet._
+import net.psforever.packet.game._
+import net.psforever.types.Vector3
+import scodec.bits._
+
+class FireHintMessageTest extends Specification {
+ val string = hex"a1 0117 23cd63f1d7480d 000077ff9d1d00"
+ val string2 = hex"a1 080e 65af5705074411 0000cffee0fc7b08899f5580"
+
+ "decode" in {
+ PacketCoding.DecodePacket(string).require match {
+ case FireHintMessage(weapon_guid, pos, u1, u2, u3, u4, u5) =>
+ weapon_guid mustEqual PlanetSideGUID(5889)
+ pos mustEqual Vector3(3482.2734f,3642.4922f,53.125f)
+ u1 mustEqual 0
+ u2 mustEqual 65399
+ u3 mustEqual 7581
+ u4 mustEqual 0
+ u5 mustEqual None
+ case _ =>
+ ko
+ }
+ }
+ "decode string2" in {
+ PacketCoding.DecodePacket(string2).require match {
+ case FireHintMessage(weapon_guid, pos, u1, u2, u3, u4, u5) =>
+ weapon_guid mustEqual PlanetSideGUID(3592)
+ pos mustEqual Vector3(2910.789f,3744.875f,69.0625f)
+ u1 mustEqual 0
+ u2 mustEqual 65231
+ u3 mustEqual 64736
+ u4 mustEqual 3
+ u5 mustEqual Some(Vector3(21.5f,-6.8125f,2.65625f))
+ case _ =>
+ ko
+ }
+ }
+
+ "encode" in {
+ val msg = FireHintMessage(PlanetSideGUID(5889), Vector3(3482.2734f,3642.4922f,53.125f), 0, 65399, 7581, 0)
+ val pkt = PacketCoding.EncodePacket(msg).require.toByteVector
+
+ pkt mustEqual string
+ }
+ "encode string2" in {
+ val msg = FireHintMessage(PlanetSideGUID(3592), Vector3(2910.789f,3744.875f,69.0625f), 0, 65231, 64736, 3, Some(Vector3(21.5f,-6.8125f,2.65625f)))
+ val pkt = PacketCoding.EncodePacket(msg).require.toByteVector
+
+ pkt mustEqual string2
+ }
+}
diff --git a/common/src/test/scala/game/ObjectCreateDetailedMessageTest.scala b/common/src/test/scala/game/ObjectCreateDetailedMessageTest.scala
index 4bcebf31d..c27f70641 100644
--- a/common/src/test/scala/game/ObjectCreateDetailedMessageTest.scala
+++ b/common/src/test/scala/game/ObjectCreateDetailedMessageTest.scala
@@ -201,10 +201,10 @@ class ObjectCreateDetailedMessageTest extends Specification {
char.appearance.is_cloaking mustEqual false
char.appearance.charging_pose mustEqual false
char.appearance.on_zipline mustEqual false
- char.appearance.ribbons.upper mustEqual 0xFFFFFFFFL //none
- char.appearance.ribbons.middle mustEqual 0xFFFFFFFFL //none
- char.appearance.ribbons.lower mustEqual 0xFFFFFFFFL //none
- char.appearance.ribbons.tos mustEqual 0xFFFFFFFFL //none
+ char.appearance.ribbons.upper mustEqual MeritCommendation.None
+ char.appearance.ribbons.middle mustEqual MeritCommendation.None
+ char.appearance.ribbons.lower mustEqual MeritCommendation.None
+ char.appearance.ribbons.tos mustEqual MeritCommendation.None
char.healthMax mustEqual 100
char.health mustEqual 100
char.armor mustEqual 50 //standard exosuit value
diff --git a/common/src/test/scala/game/ObjectCreateMessageTest.scala b/common/src/test/scala/game/ObjectCreateMessageTest.scala
index b60bb8115..5b6196870 100644
--- a/common/src/test/scala/game/ObjectCreateMessageTest.scala
+++ b/common/src/test/scala/game/ObjectCreateMessageTest.scala
@@ -4,7 +4,7 @@ package game
import net.psforever.packet._
import net.psforever.packet.game._
import net.psforever.packet.game.objectcreate._
-import net.psforever.types.{CharacterGender, ExoSuitType, GrenadeState, PlanetSideEmpire, Vector3}
+import net.psforever.types._
import org.specs2.mutable._
import scodec.bits._
@@ -684,10 +684,10 @@ class ObjectCreateMessageTest extends Specification {
pc.appearance.is_cloaking mustEqual false
pc.appearance.charging_pose mustEqual false
pc.appearance.on_zipline mustEqual false
- pc.appearance.ribbons.upper mustEqual 276L
- pc.appearance.ribbons.middle mustEqual 239L
- pc.appearance.ribbons.lower mustEqual 397L
- pc.appearance.ribbons.tos mustEqual 360L
+ pc.appearance.ribbons.upper mustEqual MeritCommendation.Loser4
+ pc.appearance.ribbons.middle mustEqual MeritCommendation.HeavyInfantry3
+ pc.appearance.ribbons.lower mustEqual MeritCommendation.TankBuster6
+ pc.appearance.ribbons.tos mustEqual MeritCommendation.SixYearNC
pc.health mustEqual 255
pc.armor mustEqual 253
pc.uniform_upgrade mustEqual UniformStyle.ThirdUpgrade
@@ -780,10 +780,10 @@ class ObjectCreateMessageTest extends Specification {
pc.appearance.is_cloaking mustEqual false
pc.appearance.charging_pose mustEqual false
pc.appearance.on_zipline mustEqual false
- pc.appearance.ribbons.upper mustEqual 244L
- pc.appearance.ribbons.middle mustEqual 353L
- pc.appearance.ribbons.lower mustEqual 33L
- pc.appearance.ribbons.tos mustEqual 361L
+ pc.appearance.ribbons.upper mustEqual MeritCommendation.Jacking
+ pc.appearance.ribbons.middle mustEqual MeritCommendation.ScavengerTR6
+ pc.appearance.ribbons.lower mustEqual MeritCommendation.AMSSupport4
+ pc.appearance.ribbons.tos mustEqual MeritCommendation.SixYearTR
pc.health mustEqual 0
pc.armor mustEqual 0
pc.uniform_upgrade mustEqual UniformStyle.ThirdUpgrade
@@ -1107,7 +1107,12 @@ class ObjectCreateMessageTest extends Specification {
false,
GrenadeState.None,
false, false, false,
- RibbonBars(276L, 239L, 397L, 360L)
+ RibbonBars(
+ MeritCommendation.Loser4,
+ MeritCommendation.HeavyInfantry3,
+ MeritCommendation.TankBuster6,
+ MeritCommendation.SixYearNC
+ )
),
255, 253,
UniformStyle.ThirdUpgrade,
@@ -1159,7 +1164,12 @@ class ObjectCreateMessageTest extends Specification {
false,
GrenadeState.None,
false, false, false,
- RibbonBars(244L, 353L, 33L, 361L)
+ RibbonBars(
+ MeritCommendation.Jacking,
+ MeritCommendation.ScavengerTR6,
+ MeritCommendation.AMSSupport4,
+ MeritCommendation.SixYearTR
+ )
),
0, 0,
UniformStyle.ThirdUpgrade,
diff --git a/common/src/test/scala/game/OxygenStateMessageTest.scala b/common/src/test/scala/game/OxygenStateMessageTest.scala
new file mode 100644
index 000000000..034be18ef
--- /dev/null
+++ b/common/src/test/scala/game/OxygenStateMessageTest.scala
@@ -0,0 +1,53 @@
+// Copyright (c) 2017 PSForever
+package game
+
+import org.specs2.mutable._
+import net.psforever.packet._
+import net.psforever.packet.game._
+import scodec.bits._
+
+class OxygenStateMessageTest extends Specification {
+ val string_self = hex"78 4b00f430"
+ val string_vehicle = hex"78 4b00f4385037a180"
+
+ "decode (self)" in {
+ PacketCoding.DecodePacket(string_self).require match {
+ case OxygenStateMessage(guid, progress, active, veh_state) =>
+ guid mustEqual PlanetSideGUID(75)
+ progress mustEqual 50.0
+ active mustEqual true
+ veh_state.isDefined mustEqual false
+ case _ =>
+ ko
+ }
+ }
+
+ "decode (vehicle)" in {
+ PacketCoding.DecodePacket(string_vehicle).require match {
+ case OxygenStateMessage(guid, progress, active, veh_state) =>
+ guid mustEqual PlanetSideGUID(75)
+ progress mustEqual 50.0f
+ active mustEqual true
+ veh_state.isDefined mustEqual true
+ veh_state.get.vehicle_guid mustEqual PlanetSideGUID(1546)
+ veh_state.get.progress mustEqual 50.0f
+ veh_state.get.active mustEqual true
+ case _ =>
+ ko
+ }
+ }
+
+ "encode (self)" in {
+ val msg = OxygenStateMessage(PlanetSideGUID(75), 50.0f, true)
+ val pkt = PacketCoding.EncodePacket(msg).require.toByteVector
+
+ pkt mustEqual string_self
+ }
+
+ "encode (vehicle)" in {
+ val msg = OxygenStateMessage(PlanetSideGUID(75), 50.0f, true, WaterloggedVehicleState(PlanetSideGUID(1546), 50.0f, true))
+ val pkt = PacketCoding.EncodePacket(msg).require.toByteVector
+
+ pkt mustEqual string_vehicle
+ }
+}
diff --git a/common/src/test/scala/game/ProjectileStateMessageTest.scala b/common/src/test/scala/game/ProjectileStateMessageTest.scala
new file mode 100644
index 000000000..f62e181e1
--- /dev/null
+++ b/common/src/test/scala/game/ProjectileStateMessageTest.scala
@@ -0,0 +1,44 @@
+// Copyright (c) 2017 PSForever
+package game
+
+import org.specs2.mutable._
+import net.psforever.packet._
+import net.psforever.packet.game._
+import net.psforever.types.Vector3
+import scodec.bits._
+
+class ProjectileStateMessageTest extends Specification {
+ val string = hex"3f 259d c5019 30e4a 9514 c52c9541 d9ba05c2 c5973941 00 f8 ec 020000"
+
+ "decode" in {
+ PacketCoding.DecodePacket(string).require match {
+ case ProjectileStateMessage(projectile, pos, vel, unk1, unk2, unk3, unk4, time_alive) =>
+ projectile mustEqual PlanetSideGUID(40229)
+ pos.x mustEqual 4611.539f
+ pos.y mustEqual 5576.375f
+ pos.z mustEqual 82.328125f
+ vel.x mustEqual 18.64686f
+ vel.y mustEqual -33.43247f
+ vel.z mustEqual 11.599553f
+ unk1 mustEqual 0
+ unk2 mustEqual 248
+ unk3 mustEqual 236
+ unk4 mustEqual false
+ time_alive mustEqual 4
+ case _ =>
+ ko
+ }
+ }
+
+ "encode" in {
+ val msg = ProjectileStateMessage(
+ PlanetSideGUID(40229),
+ Vector3(4611.539f, 5576.375f, 82.328125f),
+ Vector3(18.64686f, -33.43247f, 11.599553f),
+ 0, 248, 236, false, 4
+ )
+ val pkt = PacketCoding.EncodePacket(msg).require.toByteVector
+
+ pkt mustEqual string
+ }
+}
diff --git a/common/src/test/scala/game/TargetingImplantRequestTest.scala b/common/src/test/scala/game/TargetingImplantRequestTest.scala
new file mode 100644
index 000000000..0a2112c15
--- /dev/null
+++ b/common/src/test/scala/game/TargetingImplantRequestTest.scala
@@ -0,0 +1,116 @@
+// Copyright (c) 2017 PSForever
+package game
+
+import org.specs2.mutable._
+import net.psforever.packet._
+import net.psforever.packet.game._
+import scodec.bits._
+
+class TargetingImplantRequestTest extends Specification {
+ val string_single = hex"b5 061016"
+ val string_long = hex"b5 41edeb12d4409f0144053f8010541ba91d03df376831b1e26000611041e1107c0209c0"//0510085013d9ffb6720d5b132900003770?
+
+ "decode (single)" in {
+ PacketCoding.DecodePacket(string_single).require match {
+ case TargetingImplantRequest(target_list) =>
+ target_list.length mustEqual 1
+ //0
+ target_list.head.target_guid mustEqual PlanetSideGUID(1412)
+ target_list.head.unk mustEqual true
+ case _ =>
+ ko
+ }
+ }
+
+ "decode (long)" in {
+ PacketCoding.DecodePacket(string_long).require match {
+ case TargetingImplantRequest(target_list) =>
+ target_list.length mustEqual 16
+ //0
+ target_list.head.target_guid mustEqual PlanetSideGUID(31355)
+ target_list.head.unk mustEqual true
+ //1
+ target_list(1).target_guid mustEqual PlanetSideGUID(27273)
+ target_list(1).unk mustEqual false
+ //2
+ target_list(2).target_guid mustEqual PlanetSideGUID(40768)
+ target_list(2).unk mustEqual false
+ //3
+ target_list(3).target_guid mustEqual PlanetSideGUID(34818)
+ target_list(3).unk mustEqual false
+ //4
+ target_list(4).target_guid mustEqual PlanetSideGUID(65044)
+ target_list(4).unk mustEqual false
+ //5
+ target_list(5).target_guid mustEqual PlanetSideGUID(33280)
+ target_list(5).unk mustEqual true
+ //6
+ target_list(6).target_guid mustEqual PlanetSideGUID(47681)
+ target_list(6).unk mustEqual true
+ //7
+ target_list(7).target_guid mustEqual PlanetSideGUID(40995)
+ target_list(7).unk mustEqual false
+ //8
+ target_list(8).target_guid mustEqual PlanetSideGUID(52727)
+ target_list(8).unk mustEqual true
+ //9
+ target_list(9).target_guid mustEqual PlanetSideGUID(6324)
+ target_list(9).unk mustEqual true
+ //10
+ target_list(10).target_guid mustEqual PlanetSideGUID(58033)
+ target_list(10).unk mustEqual false
+ //11
+ target_list(11).target_guid mustEqual PlanetSideGUID(192)
+ target_list(11).unk mustEqual true
+ //12
+ target_list(12).target_guid mustEqual PlanetSideGUID(16772)
+ target_list(12).unk mustEqual false
+ //13
+ target_list(13).target_guid mustEqual PlanetSideGUID(2063)
+ target_list(13).unk mustEqual true
+ //14
+ target_list(14).target_guid mustEqual PlanetSideGUID(49159)
+ target_list(14).unk mustEqual false
+ //15
+ target_list(15).target_guid mustEqual PlanetSideGUID(14401)
+ target_list(15).unk mustEqual false
+ case _ =>
+ ko
+ }
+ }
+
+ "encode (single)" in {
+ val msg = TargetingImplantRequest(
+ TargetRequest(PlanetSideGUID(1412), true) ::
+ Nil
+ )
+ val pkt = PacketCoding.EncodePacket(msg).require.toByteVector
+
+ pkt mustEqual string_single
+ }
+
+ "encode (long)" in {
+ val msg = TargetingImplantRequest(
+ TargetRequest(PlanetSideGUID(31355), true) ::
+ TargetRequest(PlanetSideGUID(27273), false) ::
+ TargetRequest(PlanetSideGUID(40768), false) ::
+ TargetRequest(PlanetSideGUID(34818), false) ::
+ TargetRequest(PlanetSideGUID(65044), false) ::
+ TargetRequest(PlanetSideGUID(33280), true) ::
+ TargetRequest(PlanetSideGUID(47681), true) ::
+ TargetRequest(PlanetSideGUID(40995), false) ::
+ TargetRequest(PlanetSideGUID(52727), true) ::
+ TargetRequest(PlanetSideGUID(6324), true) ::
+ TargetRequest(PlanetSideGUID(58033), false) ::
+ TargetRequest(PlanetSideGUID(192), true) ::
+ TargetRequest(PlanetSideGUID(16772), false) ::
+ TargetRequest(PlanetSideGUID(2063), true) ::
+ TargetRequest(PlanetSideGUID(49159), false) ::
+ TargetRequest(PlanetSideGUID(14401), false) ::
+ Nil
+ )
+ val pkt = PacketCoding.EncodePacket(msg).require.toByteVector
+
+ pkt mustEqual string_long
+ }
+}
diff --git a/common/src/test/scala/game/TriggerEffectMessageTest.scala b/common/src/test/scala/game/TriggerEffectMessageTest.scala
new file mode 100644
index 000000000..9e3e6faa5
--- /dev/null
+++ b/common/src/test/scala/game/TriggerEffectMessageTest.scala
@@ -0,0 +1,73 @@
+// Copyright (c) 2017 PSForever
+package game
+
+import org.specs2.mutable._
+import net.psforever.packet._
+import net.psforever.packet.game._
+import net.psforever.types.Vector3
+import scodec.bits._
+
+class TriggerEffectMessageTest extends Specification {
+ val string_motionalarmsensor = hex"51 970B 82 6F6E FA00C00000"
+ val string_boomer = hex"51 0000 93 737061776E5F6F626A6563745F656666656374 417BB2CB3B4F8E00000000"
+
+ "decode (motion alarm sensor)" in {
+ PacketCoding.DecodePacket(string_motionalarmsensor).require match {
+ case TriggerEffectMessage(guid, effect, unk, location) =>
+ guid mustEqual PlanetSideGUID(2967)
+ effect mustEqual "on"
+ unk.isDefined mustEqual true
+ unk.get.unk1 mustEqual true
+ unk.get.unk2 mustEqual 1000L
+ location.isDefined mustEqual false
+ case _ =>
+ ko
+ }
+ }
+
+ "decode (boomer)" in {
+ PacketCoding.DecodePacket(string_boomer).require match {
+ case TriggerEffectMessage(guid, effect, unk, location) =>
+ guid mustEqual PlanetSideGUID(0)
+ effect mustEqual "spawn_object_effect"
+ unk.isDefined mustEqual false
+ location.isDefined mustEqual true
+ location.get.pos.x mustEqual 3567.0156f
+ location.get.pos.y mustEqual 3278.6953f
+ location.get.pos.z mustEqual 114.484375f
+ location.get.roll mustEqual 0
+ location.get.pitch mustEqual 0
+ location.get.yaw mustEqual 0
+ case _ =>
+ ko
+ }
+ }
+
+ "encode (motion alarm sensor)" in {
+ val msg = TriggerEffectMessage(
+ PlanetSideGUID(2967),
+ "on",
+ Some(TriggeredEffect(true, 1000L)),
+ None
+ )
+ val pkt = PacketCoding.EncodePacket(msg).require.toByteVector
+
+ pkt mustEqual string_motionalarmsensor
+ }
+
+ "encode (boomer)" in {
+ val msg = TriggerEffectMessage(
+ PlanetSideGUID(0),
+ "spawn_object_effect",
+ None,
+ Some(TriggeredEffectLocation(
+ Vector3(3567.0156f, 3278.6953f, 114.484375f),
+ 0, 0, 0
+ ))
+ )
+ val pkt = PacketCoding.EncodePacket(msg).require.toByteVector
+
+ pkt mustEqual string_boomer
+ }
+}
+
diff --git a/common/src/test/scala/game/UnuseItemMessageTest.scala b/common/src/test/scala/game/UnuseItemMessageTest.scala
new file mode 100644
index 000000000..c4b08213d
--- /dev/null
+++ b/common/src/test/scala/game/UnuseItemMessageTest.scala
@@ -0,0 +1,28 @@
+// Copyright (c) 2017 PSForever
+package game
+
+import org.specs2.mutable._
+import net.psforever.packet._
+import net.psforever.packet.game._
+import scodec.bits._
+
+class UnuseItemMessageTest extends Specification {
+ val string = hex"26 4B00 340D"
+
+ "decode" in {
+ PacketCoding.DecodePacket(string).require match {
+ case UnuseItemMessage(player, item) =>
+ player mustEqual PlanetSideGUID(75)
+ item mustEqual PlanetSideGUID(3380)
+ case _ =>
+ ko
+ }
+ }
+
+ "encode" in {
+ val msg = UnuseItemMessage(PlanetSideGUID(75), PlanetSideGUID(3380))
+ val pkt = PacketCoding.EncodePacket(msg).require.toByteVector
+
+ pkt mustEqual string
+ }
+}
diff --git a/pslogin/src/main/scala/WorldSessionActor.scala b/pslogin/src/main/scala/WorldSessionActor.scala
index 33430253d..3347b1db6 100644
--- a/pslogin/src/main/scala/WorldSessionActor.scala
+++ b/pslogin/src/main/scala/WorldSessionActor.scala
@@ -255,6 +255,9 @@ class WorldSessionActor extends Actor with MDCContextAware {
case msg @ ChildObjectStateMessage(object_guid : PlanetSideGUID, pitch : Int, yaw : Int) =>
//log.info("ChildObjectState: " + msg)
+ case msg @ ProjectileStateMessage(projectile_guid, shot_pos, shot_vector, unk1, unk2, unk3, unk4, time_alive) =>
+ //log.info("ProjectileState: " + msg)
+
case msg @ ChatMsg(messagetype, has_wide_contents, recipient, contents, note_contents) =>
// TODO: Prevents log spam, but should be handled correctly
if (messagetype != ChatMessageType.CMT_TOGGLE_GM) {
@@ -363,6 +366,9 @@ class WorldSessionActor extends Actor with MDCContextAware {
// TODO: This should only actually be sent to doors upon opening; may break non-door items upon use
sendResponse(PacketCoding.CreateGamePacket(0, GenericObjectStateMsg(object_guid, 16)))
}
+
+ case msg @ UnuseItemMessage(player, item) =>
+ log.info("UnuseItem: " + msg)
case msg @ DeployObjectMessage(guid, unk1, pos, roll, pitch, yaw, unk2) =>
log.info("DeployObject: " + msg)
@@ -440,6 +446,9 @@ class WorldSessionActor extends Actor with MDCContextAware {
case msg @ WeaponDryFireMessage(weapon) =>
log.info("WeaponDryFireMessage: "+msg)
+ case msg @ TargetingImplantRequest(list) =>
+ log.info("TargetingImplantRequest: "+msg)
+
case default => log.error(s"Unhandled GamePacket ${pkt}")
}