diff --git a/common/src/main/scala/net/psforever/packet/game/BuildingInfoUpdateMessage.scala b/common/src/main/scala/net/psforever/packet/game/BuildingInfoUpdateMessage.scala index b883be03..205adf29 100644 --- a/common/src/main/scala/net/psforever/packet/game/BuildingInfoUpdateMessage.scala +++ b/common/src/main/scala/net/psforever/packet/game/BuildingInfoUpdateMessage.scala @@ -2,9 +2,13 @@ package net.psforever.packet.game import net.psforever.packet.{GamePacketOpcode, Marshallable, PacketHelpers, PlanetSideGamePacket} -import scodec.Codec +import scodec.{Attempt, Codec, Err} import scodec.codecs._ +import shapeless.{::, HNil} +/** + * An `Enumeration` `Codec` that represents that various states of a major facility's Generator. + */ object PlanetSideGeneratorState extends Enumeration { type Type = Value val Normal, @@ -16,19 +20,102 @@ object PlanetSideGeneratorState extends Enumeration { implicit val codec = PacketHelpers.createEnumerationCodec(this, uintL(2)) } +/** + * na + * @param unk1 na + * @param unk2 na + * @param unk3 na + */ final case class Additional1(unk1 : String, unk2 : Int, unk3 : Long) +/** + * na + * @param unk1 na + * @param unk2 na + */ final case class Additional2(unk1 : Int, unk2 : Long) +/** + * na + * @param unk1 na + * @param unk2 na + */ final case class Additional3(unk1 : Boolean, unk2 : Int) /** - * BuildingInfoUpdateMessage is sent for all bases, towers, and warpgates from all continents upon login. - */ + * Update the state of map asset for a client's specific building's state. + * The most common application of this packet is to synchronize map state during world login.
+ *
+ * A "building" mainly constitutes any map-viewable structure that has properties or whose ownership can be asserted. + * This packet is valid for all major facilities, field towers, warp gates, and some static environment elements. + * Additional properties, if available, can be viewed by selecting the sphere of influence of a given building. + * The combination of continent UID and building UID ensures that all buildings are uniquely-defined. + * This packet can be applied on any continent and will affect the appropriate building on any other continent. + * As the intercontinental map is always available, all map assets will publish real time updates to all players. + * Map information configured by this packet is not obscured from any players, regardless of faction. + * (Network state updates will be delayed for, and the type of compromise will not be specified to, defenders.)
+ *
+ * Aside from the map-viewable aspects, a few properties set by this packet also have game world effects. + * Additionally, though most parameters are treated as mandatory, not all buildings will be able to use those parameters. + * A parameter that is not applicable for a given asset, e.g., NTU for a field tower, will be ignored. + * A collision between some parameters can occur. + * For example, if `is_hacking` is `false`, the other hacking fields are considered invalid. + * If `is_hacking` is `true` but the hacking empire is also the owning empire, the `is_hacking` state is invalid.
+ *
+ * Lattice benefits: (stackable)
+ * ` + * 00 - None
+ * 01 - Amp Station
+ * 02 - Dropship Center
+ * 04 - Bio Laboratory
+ * 08 - Interlink Facility
+ * 16 - Technology Plant
+ * ` + *
+ * Cavern benefits: (stackable)
+ * ` + * 000 - None
+ * 004 - Speed Module
+ * 008 - Shield Module
+ * 016 - Vehicle Module
+ * 032 - Equipment Module
+ * 064 - Health Module
+ * 128 - Pain Module
+ * ` + * @param continent_guid the continent (zone) + * @param building_guid the building + * @param ntu_level if the building has a silo, the amount of NTU in that silo; + * NTU is reported in multiples of 10%; + * valid for 0 (0%) to 10 (100%) + * @param is_hacked if the building can be hacked and will take time to convert, whether the building is being hacked + * @param empire_hack if the building is being hacked, the empire that is performing the hacking + * @param hack_time_remaining if the building is being hacked, the amount of time remaining until the hack finishes/clears; + * recorded in milliseconds (ms) + * @param empire_own the empire that owns the building currently + * @param unk1 na; + * value != 0 causes the next field to be defined + * @param unk1x na + * @param generator_state if the building has a generator, the state of the generator + * @param spawn_tubes_normal if the building has spawn tubes, whether at least one of the tubes is powered and operational + * @param force_dome_active if the building is a capitol facility, whether the force dome is active + * @param lattice_benefit the benefits from other Lattice-linked bases does this building possess + * @param cavern_benefit cavern benefits; + * any non-zero value will cause the cavern module icon (yellow) to appear; + * proper module values cause the cavern module icon to render green; + * all benefits will report as due to a "Cavern Lock" + * @param unk4 na + * @param unk5 na + * @param unk6 na + * @param unk7 na; + * value != 8 causes the next field to be defined + * @param unk7x na + * @param boost_spawn_pain if the building has spawn tubes, the (boosted) strength of its enemy pain field + * @param boost_generator_pain if the building has a generator, the (boosted) strength of its enemy pain field + */ final case class BuildingInfoUpdateMessage(continent_guid : PlanetSideGUID, building_guid : PlanetSideGUID, ntu_level : Int, @@ -42,7 +129,7 @@ final case class BuildingInfoUpdateMessage(continent_guid : PlanetSideGUID, spawn_tubes_normal : Boolean, force_dome_active : Boolean, lattice_benefit : Int, - unk3 : Int, + cavern_benefit : Int, unk4 : List[Additional2], unk5 : Long, unk6 : Boolean, @@ -57,17 +144,26 @@ final case class BuildingInfoUpdateMessage(continent_guid : PlanetSideGUID, } object BuildingInfoUpdateMessage extends Marshallable[BuildingInfoUpdateMessage] { + /** + * A `Codec` for a set of additional fields. + */ private val additional1_codec : Codec[Additional1] = ( ("unk1" | PacketHelpers.encodedWideStringAligned(3)) :: ("unk2" | uint8L) :: ("unk3" | uint32L) ).as[Additional1] + /** + * A `Codec` for a set of additional fields. + */ private val additional2_codec : Codec[Additional2] = ( ("unk1" | uint4L) :: ("unk2" | uint32L) ).as[Additional2] + /** + * A `Codec` for a set of additional fields. + */ private val additional3_codec : Codec[Additional3] = ( ("unk1" | bool) :: ("unk2" | uint2L) @@ -79,15 +175,15 @@ object BuildingInfoUpdateMessage extends Marshallable[BuildingInfoUpdateMessage] ("ntu_level" | uint4L) :: ("is_hacked" | bool ) :: ("empire_hack" | PlanetSideEmpire.codec) :: - ("hack_time_remaining" | uint32L ) :: //In milliseconds + ("hack_time_remaining" | uint32L ) :: ("empire_own" | PlanetSideEmpire.codec) :: (("unk1" | uint32L) >>:~ { unk1 => conditional(unk1 != 0L, "unk1x" | additional1_codec) :: ("generator_state" | PlanetSideGeneratorState.codec) :: ("spawn_tubes_normal" | bool) :: ("force_dome_active" | bool) :: - ("lattice_benefit" | uintL(5)) :: //5 possible benefits, bitwise combination. (MSB)5:Tech 4:Inter 3:Bio 2:Drop 1:Amp(LSB) - ("unk3" | uintL(10)) :: //Module related. 0x3FF gives all modules with no timer. Unclear how this works. + ("lattice_benefit" | uintL(5)) :: + ("cavern_benefit" | uintL(10)) :: ("unk4" | listOfN(uint4L, additional2_codec)) :: ("unk5" | uint32L) :: ("unk6" | bool) :: @@ -97,5 +193,32 @@ object BuildingInfoUpdateMessage extends Marshallable[BuildingInfoUpdateMessage] ("boost_generator_pain" | bool) }) }) - ).as[BuildingInfoUpdateMessage] + ).exmap[BuildingInfoUpdateMessage] ( + { + case a :: b :: c :: d :: e :: f :: g :: h :: i :: j :: k :: l :: m :: n :: o :: p :: q :: r :: s :: t :: u :: HNil => + Attempt.successful(BuildingInfoUpdateMessage(a, b, c, d, e, f, g, h, i, j, k, l, m, n, o, p, q, r, s, t, u)) + }, + { + case BuildingInfoUpdateMessage(_, _, _, _, _, _, _, 0, Some(x), _, _, _, _, _, _, _, _, _, _, _, _) => + Attempt.failure(Err("invalid properties when value == 0")) + + case BuildingInfoUpdateMessage(_, _, _, _, _, _, _, _, _, _, _, _, _, _, _, _, _, 8, Some(x), _, _) => + Attempt.failure(Err("invalid properties when value == 8")) + + case BuildingInfoUpdateMessage(a, b, c, d, e, f, g, h, i, j, k, l, m, n, lst, p, q, r, s, t, u) => + if(h != 0 && i.isEmpty) { + Attempt.failure(Err(s"missing properties when value != 0 (actual: $h)")) //TODO can we recover by forcing value -> 0? + } + else if(r != 8 && s.isEmpty) { + Attempt.failure(Err(s"missing properties when value != 8 (actual: $r)")) //TODO can we recover by forcing value -> 8? + } + val size = lst.size + if(size > 15) { + Attempt.failure(Err(s"too many elements in list (max: 15, actual: $size)")) + } + else { + Attempt.successful(a :: b :: c :: d :: e :: f :: g :: h :: i :: j :: k :: l :: m :: n :: lst :: p :: q :: r :: s :: t :: u :: HNil) + } + } + ) }