diff --git a/common/src/main/scala/net/psforever/packet/GamePacketOpcode.scala b/common/src/main/scala/net/psforever/packet/GamePacketOpcode.scala
index 1242feb4..cad22888 100644
--- a/common/src/main/scala/net/psforever/packet/GamePacketOpcode.scala
+++ b/common/src/main/scala/net/psforever/packet/GamePacketOpcode.scala
@@ -414,7 +414,7 @@ object GamePacketOpcode extends Enumeration {
case 0x4f => game.LashMessage.decode
// OPCODES 0x50-5f
- case 0x50 => noDecoder(TargetingInfoMessage)
+ case 0x50 => game.TargetingInfoMessage.decode
case 0x51 => noDecoder(TriggerEffectMessage)
case 0x52 => game.WeaponDryFireMessage.decode
case 0x53 => noDecoder(DroppodLaunchRequestMessage)
diff --git a/common/src/main/scala/net/psforever/packet/game/TargetingInfoMessage.scala b/common/src/main/scala/net/psforever/packet/game/TargetingInfoMessage.scala
new file mode 100644
index 00000000..b72af1f4
--- /dev/null
+++ b/common/src/main/scala/net/psforever/packet/game/TargetingInfoMessage.scala
@@ -0,0 +1,129 @@
+// Copyright (c) 2017 PSForever
+package net.psforever.packet.game
+
+import net.psforever.packet.{GamePacketOpcode, Marshallable, PlanetSideGamePacket}
+import scodec.Codec
+import scodec.codecs._
+import shapeless.{::, HNil}
+
+/**
+ * An entry regarding a target's health and, if applicable, any secondary defensive option they possess, hitherto, "armor."
+ * @param target_guid the target
+ * @param health the amount of health the target has, as a percentage of a filled bar scaled between 0f and 1f inclusive
+ * @param armor the amount of armor the target has, as a percentage of a filled bar scaled between 0f and 1f inclusive;
+ * defaults to 0f
+ */
+final case class TargetInfo(target_guid : PlanetSideGUID,
+ health : Float,
+ armor : Float = 0f)
+
+/**
+ * Dispatched by the server to update status information regarding the listed targets.
+ *
+ * This packet is often in response to a client-sent `TargetingImplantRequest` packet, when related to the implant's operation.
+ * It can also arrive independent of a formal request and will operate even without the implant.
+ * The enumerated targets report their status as two "progress bars" that can be empty (0f) or filled (1f).
+ * When this packet is received, the client will actually update the current fields associated with those values for the target.
+ * For example, for `0x17` player characters, the values are assigned to their health points and armor points respectively.
+ * Allied player characters will have their "progress bar" visuals updated immediately;
+ * the implant is still necessary to view enemy target progress bars, if they will be visible.
+ *
+ * This function can be used to update fields properly.
+ * The value between 0 and 255 (0f to 1f) can be inserted directly into `ObjectCreateMessage` creations as it matches the scale.
+ * The target will be killed or destroyed as expected when health is set to zero.
+ * @param target_list a list of targets
+ */
+final case class TargetingInfoMessage(target_list : List[TargetInfo])
+ extends PlanetSideGamePacket {
+ type Packet = TargetingInfoMessage
+ def opcode = GamePacketOpcode.TargetingInfoMessage
+ def encode = TargetingInfoMessage.encode(this)
+}
+
+object TargetInfo {
+ /**
+ * Overloaded constructor that takes `Integer` values rather than `Float` values.
+ * @param target_guid the target
+ * @param health the amount of health the target has
+ * @param armor the amount of armor the target has
+ * @return a `TargetInfo` object
+ */
+ def apply(target_guid : PlanetSideGUID, health : Int, armor : Int) : TargetInfo = {
+ val health2 : Float = TargetingInfoMessage.rangedFloat(health)
+ val armor2 : Float = TargetingInfoMessage.rangedFloat(armor)
+ TargetInfo(target_guid, health2, armor2)
+ }
+
+ /**
+ * Overloaded constructor that takes `Integer` values rather than `Float` values and only expects the first field.
+ * @param target_guid the target
+ * @param health the amount of health the target has
+ * @return a `TargetInfo` object
+ */
+ def apply(target_guid : PlanetSideGUID, health : Int) : TargetInfo = {
+ val health2 : Float = TargetingInfoMessage.rangedFloat(health)
+ TargetInfo(target_guid, health2)
+ }
+}
+
+object TargetingInfoMessage extends Marshallable[TargetingInfoMessage] {
+ private final val unit : Double = 0.0039215689 //common constant for 1/255
+
+ /**
+ * Transform an unsigned `Integer` number into a scaled `Float`.
+ * @param n an unsigned `Integer` number inclusive 0 and below 256
+ * @return a scaled `Float` number inclusive to 0f to 1f
+ */
+ def rangedFloat(n : Int) : Float = {
+ (
+ (if(n <= 0) {
+ 0
+ }
+ else if(n >= 255) {
+ 255
+ }
+ else {
+ n
+ }).toDouble * unit
+ ).toFloat
+ }
+ /**
+ * Transform a scaled `Float` number into an unsigned `Integer`.
+ * @param n `Float` number inclusive to 0f to 1f
+ * @return a scaled unsigned `Integer` number inclusive 0 and below 256
+ */
+ def rangedInt(n : Float) : Int = {
+ (
+ (if(n <= 0f) {
+ 0f
+ }
+ else if(n >= 1.0f) {
+ 1.0f
+ }
+ else {
+ n
+ }).toDouble * 255
+ ).toInt
+ }
+
+ private val info_codec : Codec[TargetInfo] = (
+ ("target_guid" | PlanetSideGUID.codec) ::
+ ("unk1" | uint8L) ::
+ ("unk2" | uint8L)
+ ).xmap[TargetInfo] (
+ {
+ case a :: b :: c :: HNil =>
+ val b2 : Float = rangedFloat(b)
+ val c2 : Float = rangedFloat(c)
+ TargetInfo(a, b2, c2)
+ },
+ {
+ case TargetInfo(a, b, c) =>
+ val b2 : Int = rangedInt(b)
+ val c2 : Int = rangedInt(c)
+ a :: b2 :: c2 :: HNil
+ }
+ )
+
+ implicit val codec : Codec[TargetingInfoMessage] = ("target_list" | listOfN(uint8L, info_codec)).as[TargetingInfoMessage]
+}
diff --git a/common/src/test/scala/game/TargetingInfoMessageTest.scala b/common/src/test/scala/game/TargetingInfoMessageTest.scala
new file mode 100644
index 00000000..ed62f066
--- /dev/null
+++ b/common/src/test/scala/game/TargetingInfoMessageTest.scala
@@ -0,0 +1,54 @@
+// Copyright (c) 2017 PSForever
+package game
+
+import org.specs2.mutable._
+import net.psforever.packet._
+import net.psforever.packet.game._
+import scodec.bits._
+
+class TargetingInfoMessageTest extends Specification {
+ val string = hex"50 05 3D10C200 570EFF3C 2406EC00 2B068C00 2A069400"
+
+ "decode" in {
+ PacketCoding.DecodePacket(string).require match {
+ case TargetingInfoMessage(target_list) =>
+ target_list.size mustEqual 5
+ //0
+ target_list.head.target_guid mustEqual PlanetSideGUID(4157)
+ target_list.head.health mustEqual 0.7607844f
+ target_list.head.armor mustEqual 0f
+ //1
+ target_list(1).target_guid mustEqual PlanetSideGUID(3671)
+ target_list(1).health mustEqual 1.0000001f
+ target_list(1).armor mustEqual 0.23529413f
+ //2
+ target_list(2).target_guid mustEqual PlanetSideGUID(1572)
+ target_list(2).health mustEqual 0.92549026f
+ target_list(2).armor mustEqual 0f
+ //3
+ target_list(3).target_guid mustEqual PlanetSideGUID(1579)
+ target_list(3).health mustEqual 0.54901963f
+ target_list(3).armor mustEqual 0f
+ //4
+ target_list(4).target_guid mustEqual PlanetSideGUID(1578)
+ target_list(4).health mustEqual 0.5803922f
+ target_list(4).armor mustEqual 0f
+ case _ =>
+ ko
+ }
+ }
+
+ "encode" in {
+ val msg = TargetingInfoMessage(
+ TargetInfo(PlanetSideGUID(4157), 0.7607844f) ::
+ TargetInfo(PlanetSideGUID(3671), 1.0000001f, 0.23529413f) ::
+ TargetInfo(PlanetSideGUID(1572), 0.92549026f) ::
+ TargetInfo(PlanetSideGUID(1579), 0.54901963f) ::
+ TargetInfo(PlanetSideGUID(1578), 0.5803922f) ::
+ Nil
+ )
+ val pkt = PacketCoding.EncodePacket(msg).require.toByteVector
+
+ pkt mustEqual string
+ }
+}