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