diff --git a/common/src/main/scala/net/psforever/objects/AmmoBox.scala b/common/src/main/scala/net/psforever/objects/AmmoBox.scala
new file mode 100644
index 000000000..f95cebb74
--- /dev/null
+++ b/common/src/main/scala/net/psforever/objects/AmmoBox.scala
@@ -0,0 +1,57 @@
+// Copyright (c) 2017 PSForever
+package net.psforever.objects
+
+import net.psforever.objects.definition.AmmoBoxDefinition
+import net.psforever.objects.equipment.{Ammo, Equipment}
+
+class AmmoBox(private val ammoDef : AmmoBoxDefinition,
+ cap : Option[Int] = None
+ ) extends Equipment {
+ private var capacity = if(cap.isDefined) { AmmoBox.limitCapacity(cap.get, 1) } else { FullCapacity }
+
+ def AmmoType : Ammo.Value = ammoDef.AmmoType
+
+ def Capacity : Int = capacity
+
+ def Capacity_=(toCapacity : Int) : Int = {
+ capacity = AmmoBox.limitCapacity(toCapacity)
+ Capacity
+ }
+
+ def FullCapacity : Int = ammoDef.Capacity
+
+ def Definition : AmmoBoxDefinition = ammoDef
+
+ override def toString : String = {
+ AmmoBox.toString(this)
+ }
+}
+
+object AmmoBox {
+ def apply(ammoDef : AmmoBoxDefinition) : AmmoBox = {
+ new AmmoBox(ammoDef)
+ }
+
+ def apply(ammoDef : AmmoBoxDefinition, capacity : Int) : AmmoBox = {
+ new AmmoBox(ammoDef, Some(capacity))
+ }
+
+ import net.psforever.packet.game.PlanetSideGUID
+ def apply(guid : PlanetSideGUID, ammoDef : AmmoBoxDefinition) : AmmoBox = {
+ val obj = new AmmoBox(ammoDef)
+ obj.GUID = guid
+ obj
+ }
+
+ def apply(guid : PlanetSideGUID, ammoDef : AmmoBoxDefinition, capacity : Int) : AmmoBox = {
+ val obj = new AmmoBox(ammoDef, Some(capacity))
+ obj.GUID = guid
+ obj
+ }
+
+ def limitCapacity(count : Int, min : Int = 0) : Int = math.min(math.max(min, count), 65535)
+
+ def toString(obj : AmmoBox) : String = {
+ s"box of ${obj.AmmoType} ammo (${obj.Capacity})"
+ }
+}
diff --git a/common/src/main/scala/net/psforever/objects/Avatars.scala b/common/src/main/scala/net/psforever/objects/Avatars.scala
new file mode 100644
index 000000000..cb9d49d09
--- /dev/null
+++ b/common/src/main/scala/net/psforever/objects/Avatars.scala
@@ -0,0 +1,19 @@
+// Copyright (c) 2017 PSForever
+package net.psforever.objects
+
+/**
+ * An `Enumeration` of all the avatar types in the game, paired with their object id as the `Value`.
+ * #121 is the most important.
+ */
+object Avatars extends Enumeration {
+ final val avatar = Value(121)
+ final val avatar_bot = Value(122)
+ final val avatar_bot_agile = Value(123)
+ final val avatar_bot_agile_no_weapon = Value(124)
+ final val avatar_bot_max = Value(125)
+ final val avatar_bot_max_no_weapon = Value(126)
+ final val avatar_bot_reinforced = Value(127)
+ final val avatar_bot_reinforced_no_weapon = Value(128)
+ final val avatar_bot_standard = Value(129)
+ final val avatar_bot_standard_no_weapon = Value(130)
+}
diff --git a/common/src/main/scala/net/psforever/objects/ConstructionItem.scala b/common/src/main/scala/net/psforever/objects/ConstructionItem.scala
new file mode 100644
index 000000000..6a6229551
--- /dev/null
+++ b/common/src/main/scala/net/psforever/objects/ConstructionItem.scala
@@ -0,0 +1,38 @@
+// Copyright (c) 2017 PSForever
+package net.psforever.objects
+
+import net.psforever.objects.definition.ConstructionItemDefinition
+import net.psforever.objects.equipment.{CItem, Equipment, FireModeSwitch}
+
+class ConstructionItem(private val cItemDef : ConstructionItemDefinition) extends Equipment with FireModeSwitch[CItem.DeployedItem.Value] {
+ private var fireModeIndex : Int = 0
+
+ def FireModeIndex : Int = fireModeIndex
+
+ def FireModeIndex_=(index : Int) : Int = {
+ fireModeIndex = index % cItemDef.Modes.length
+ FireModeIndex
+ }
+
+ def FireMode : CItem.DeployedItem.Value = cItemDef.Modes(fireModeIndex)
+
+ def NextFireMode : CItem.DeployedItem.Value = {
+ FireModeIndex = FireModeIndex + 1
+ FireMode
+ }
+
+ def Definition : ConstructionItemDefinition = cItemDef
+}
+
+object ConstructionItem {
+ def apply(cItemDef : ConstructionItemDefinition) : ConstructionItem = {
+ new ConstructionItem(cItemDef)
+ }
+
+ import net.psforever.packet.game.PlanetSideGUID
+ def apply(guid : PlanetSideGUID, cItemDef : ConstructionItemDefinition) : ConstructionItem = {
+ val obj = new ConstructionItem(cItemDef)
+ obj.GUID = guid
+ obj
+ }
+}
diff --git a/common/src/main/scala/net/psforever/objects/EquipmentSlot.scala b/common/src/main/scala/net/psforever/objects/EquipmentSlot.scala
new file mode 100644
index 000000000..7cb698b1b
--- /dev/null
+++ b/common/src/main/scala/net/psforever/objects/EquipmentSlot.scala
@@ -0,0 +1,53 @@
+// Copyright (c) 2017 PSForever
+package net.psforever.objects
+
+import net.psforever.objects.equipment.{Equipment, EquipmentSize}
+
+/**
+ * A size-checked unit of storage (or mounting) for `Equipment`.
+ * Unlike inventory space, anything placed in "slot" space is expected to be visible to the game world in some fashion.
+ */
+class EquipmentSlot {
+ private var size : EquipmentSize.Value = EquipmentSize.Blocked
+ private var tool : Option[Equipment] = None
+ //TODO eventually move this object from storing the item directly to just storing its GUID?
+
+ def Size : EquipmentSize.Value = size
+
+ def Size_=(assignSize : EquipmentSize.Value) : EquipmentSize.Value = {
+ if(tool.isEmpty) {
+ size = assignSize
+ }
+ Size
+ }
+
+ def Equipment : Option[Equipment] = tool
+
+ def Equipment_=(assignEquipment : Equipment) : Option[Equipment] = {
+ Equipment = Some(assignEquipment)
+ }
+
+ def Equipment_=(assignEquipment : Option[Equipment]) : Option[Equipment] = {
+ if(assignEquipment.isDefined) { //if new equipment is defined, don't put it in the slot if the slot is being used
+ if(tool.isEmpty && EquipmentSize.isEqual(size, assignEquipment.get.Size)) {
+ tool = assignEquipment
+ }
+ }
+ else {
+ tool = None
+ }
+ Equipment
+ }
+}
+
+object EquipmentSlot {
+ def apply() : EquipmentSlot = {
+ new EquipmentSlot()
+ }
+
+ def apply(size : EquipmentSize.Value) : EquipmentSlot = {
+ val slot = new EquipmentSlot()
+ slot.Size = size
+ slot
+ }
+}
diff --git a/common/src/main/scala/net/psforever/objects/ExoSuitDefinition.scala b/common/src/main/scala/net/psforever/objects/ExoSuitDefinition.scala
new file mode 100644
index 000000000..0ef2ba55f
--- /dev/null
+++ b/common/src/main/scala/net/psforever/objects/ExoSuitDefinition.scala
@@ -0,0 +1,128 @@
+// Copyright (c) 2017 PSForever
+package net.psforever.objects
+
+import net.psforever.objects.equipment.EquipmentSize
+import net.psforever.objects.inventory.InventoryTile
+import net.psforever.types.ExoSuitType
+
+/**
+ * A definition for producing the personal armor the player wears.
+ * Players are influenced by the exo-suit they wear in a variety of ways, with speed and available equipment slots being major differences.
+ * @param suitType the `Enumeration` corresponding to this exo-suit
+ */
+class ExoSuitDefinition(private val suitType : ExoSuitType.Value) {
+ private var permission : Int = 0 //TODO certification type?
+ private var maxArmor : Int = 0
+ private val holsters : Array[EquipmentSize.Value] = Array.fill[EquipmentSize.Value](5)(EquipmentSize.Blocked)
+ private var inventoryScale : InventoryTile = InventoryTile.Tile11 //override with custom InventoryTile
+ private var inventoryOffset : Int = 0
+
+ def SuitType : ExoSuitType.Value = suitType
+
+ def MaxArmor : Int = maxArmor
+
+ def MaxArmor_=(armor : Int) : Int = {
+ maxArmor = math.min(math.max(0, armor), 65535)
+ MaxArmor
+ }
+
+ def InventoryScale : InventoryTile = inventoryScale
+
+ def InventoryScale_=(scale : InventoryTile) : InventoryTile = {
+ inventoryScale = scale
+ InventoryScale
+ }
+
+ def InventoryOffset : Int = inventoryOffset
+
+ def InventoryOffset_=(offset : Int) : Int = {
+ inventoryOffset = offset
+ InventoryOffset
+ }
+
+ def Holsters : Array[EquipmentSize.Value] = holsters
+
+ def Holster(slot : Int) : EquipmentSize.Value = {
+ if(slot >= 0 && slot < 5) {
+ holsters(slot)
+ }
+ else {
+ EquipmentSize.Blocked
+ }
+ }
+
+ def Holster(slot : Int, value : EquipmentSize.Value) : EquipmentSize.Value = {
+ if(slot >= 0 && slot < 5) {
+ holsters(slot) = value
+ holsters(slot)
+ }
+ else {
+ EquipmentSize.Blocked
+ }
+ }
+}
+
+object ExoSuitDefinition {
+ final val Standard = ExoSuitDefinition(ExoSuitType.Standard)
+ Standard.MaxArmor = 50
+ Standard.InventoryScale = new InventoryTile(9,6)
+ Standard.InventoryOffset = 6
+ Standard.Holster(0, EquipmentSize.Pistol)
+ Standard.Holster(2, EquipmentSize.Rifle)
+ Standard.Holster(4, EquipmentSize.Melee)
+
+ final val Agile = ExoSuitDefinition(ExoSuitType.Agile)
+ Agile.MaxArmor = 100
+ Agile.InventoryScale = new InventoryTile(9,9)
+ Agile.InventoryOffset = 6
+ Agile.Holster(0, EquipmentSize.Pistol)
+ Agile.Holster(1, EquipmentSize.Pistol)
+ Agile.Holster(2, EquipmentSize.Rifle)
+ Agile.Holster(4, EquipmentSize.Melee)
+
+ final val Reinforced = ExoSuitDefinition(ExoSuitType.Reinforced)
+ Reinforced.permission = 1
+ Reinforced.MaxArmor = 200
+ Reinforced.InventoryScale = new InventoryTile(12,9)
+ Reinforced.InventoryOffset = 6
+ Reinforced.Holster(0, EquipmentSize.Pistol)
+ Reinforced.Holster(1, EquipmentSize.Pistol)
+ Reinforced.Holster(2, EquipmentSize.Rifle)
+ Reinforced.Holster(3, EquipmentSize.Rifle)
+ Reinforced.Holster(4, EquipmentSize.Melee)
+
+ final val Infiltration = ExoSuitDefinition(ExoSuitType.Standard)
+ Infiltration.permission = 1
+ Infiltration.MaxArmor = 0
+ Infiltration.InventoryScale = new InventoryTile(6,6)
+ Infiltration.InventoryOffset = 6
+ Infiltration.Holster(0, EquipmentSize.Pistol)
+ Infiltration.Holster(4, EquipmentSize.Melee)
+
+ final val MAX = ExoSuitDefinition(ExoSuitType.MAX)
+ MAX.permission = 1
+ MAX.MaxArmor = 650
+ MAX.InventoryScale = new InventoryTile(16,12)
+ MAX.InventoryOffset = 6
+ MAX.Holster(0, EquipmentSize.Max)
+ MAX.Holster(4, EquipmentSize.Melee)
+
+ def apply(suitType : ExoSuitType.Value) : ExoSuitDefinition = {
+ new ExoSuitDefinition(suitType)
+ }
+
+ /**
+ * A function to retrieve the correct defintion of an exo-suit from the type of exo-suit.
+ * @param suit the `Enumeration` corresponding to this exo-suit
+ * @return the exo-suit definition
+ */
+ def Select(suit : ExoSuitType.Value) : ExoSuitDefinition = {
+ suit match {
+ case ExoSuitType.Agile => ExoSuitDefinition.Agile
+ case ExoSuitType.Infiltration => ExoSuitDefinition.Infiltration
+ case ExoSuitType.MAX => ExoSuitDefinition.MAX
+ case ExoSuitType.Reinforced => ExoSuitDefinition.Reinforced
+ case _ => ExoSuitDefinition.Standard
+ }
+ }
+}
diff --git a/common/src/main/scala/net/psforever/objects/GlobalDefinitions.scala b/common/src/main/scala/net/psforever/objects/GlobalDefinitions.scala
new file mode 100644
index 000000000..4f3df693c
--- /dev/null
+++ b/common/src/main/scala/net/psforever/objects/GlobalDefinitions.scala
@@ -0,0 +1,1163 @@
+// Copyright (c) 2017 PSForever
+package net.psforever.objects
+
+import net.psforever.objects.definition._
+import net.psforever.objects.definition.converter.{CommandDetonaterConverter, REKConverter}
+import net.psforever.objects.equipment.CItem.DeployedItem
+import net.psforever.objects.equipment._
+import net.psforever.objects.inventory.InventoryTile
+import net.psforever.packet.game.objectcreate.ObjectClass
+import net.psforever.types.PlanetSideEmpire
+
+object GlobalDefinitions {
+ /**
+ * Given a faction, provide the standard assault melee weapon.
+ * @param faction the faction
+ * @return the `ToolDefinition` for the melee weapon
+ */
+ def StandardMelee(faction : PlanetSideEmpire.Value) : ToolDefinition = {
+ faction match {
+ case PlanetSideEmpire.TR => chainblade
+ case PlanetSideEmpire.NC => magcutter
+ case PlanetSideEmpire.VS => forceblade
+ case PlanetSideEmpire.NEUTRAL => chainblade //do NOT hand out the katana
+ }
+ }
+
+ /**
+ * Given a faction, provide the satndard assault pistol.
+ * @param faction the faction
+ * @return the `ToolDefinition` for the pistol
+ */
+ def StandardPistol(faction : PlanetSideEmpire.Value) : ToolDefinition = {
+ faction match {
+ case PlanetSideEmpire.TR => repeater
+ case PlanetSideEmpire.NC => isp
+ case PlanetSideEmpire.VS => beamer
+ case PlanetSideEmpire.NEUTRAL => ilc9
+ }
+ }
+
+ /**
+ * For a given faction, provide the ammunition for the standard assault pistol.
+ * The ammunition value here must work with the result of obtaining the pistol using the faction.
+ * @param faction the faction
+ * @return thr `AmmoBoxDefinition` for the pistol's ammo
+ * @see `GlobalDefinitions.StandardPistol`
+ */
+ def StandardPistolAmmo(faction : PlanetSideEmpire.Value) : AmmoBoxDefinition = {
+ faction match {
+ case PlanetSideEmpire.TR => bullet_9mm
+ case PlanetSideEmpire.NC => shotgun_shell
+ case PlanetSideEmpire.VS => energy_cell
+ case PlanetSideEmpire.NEUTRAL => bullet_9mm
+ }
+ }
+
+ /**
+ * For a given faction, provide the medium assault pistol.
+ * The medium assault pistols all use the same ammunition so there is no point for a separate selection function.
+ * @param faction the faction
+ * @return the `ToolDefinition` for the pistol
+ */
+ def MediumPistol(faction : PlanetSideEmpire.Value) : ToolDefinition = {
+ faction match {
+ case PlanetSideEmpire.TR => anniversary_guna
+ case PlanetSideEmpire.NC => anniversary_gun
+ case PlanetSideEmpire.VS => anniversary_gunb
+ case PlanetSideEmpire.NEUTRAL => ilc9 //do not hand out the spiker
+ }
+ }
+
+ /**
+ * For a given faction, provide the medium assault rifle.
+ * For `Neutral` or `Black Ops`, just return a Suppressor.
+ * @param faction the faction
+ * @return the `ToolDefinition` for the rifle
+ */
+ def MediumRifle(faction : PlanetSideEmpire.Value) : ToolDefinition = {
+ faction match {
+ case PlanetSideEmpire.TR => cycler
+ case PlanetSideEmpire.NC => gauss
+ case PlanetSideEmpire.VS => pulsar
+ case PlanetSideEmpire.NEUTRAL => suppressor //the Punisher would be messy to have to code for
+ }
+ }
+
+ /**
+ * For a given faction, provide the ammunition for the medium assault rifle.
+ * The ammunition value here must work with the result of obtaining the rifle using the faction.
+ * @param faction the faction
+ * @return thr `AmmoBoxDefinition` for the rifle's ammo
+ * @see `GlobalDefinitions.MediumRifle`
+ */
+ def MediumRifleAmmo(faction : PlanetSideEmpire.Value) : AmmoBoxDefinition = {
+ faction match {
+ case PlanetSideEmpire.TR => bullet_9mm
+ case PlanetSideEmpire.NC => bullet_9mm
+ case PlanetSideEmpire.VS => energy_cell
+ case PlanetSideEmpire.NEUTRAL => bullet_9mm
+ }
+ }
+
+ /**
+ * For a given faction, provide the heavy assault rifle.
+ * For `Neutral` or `Black Ops`, just return a Suppressor.
+ * @param faction the faction
+ * @return the `ToolDefinition` for the rifle
+ */
+ def HeavyRifle(faction : PlanetSideEmpire.Value) : ToolDefinition = {
+ faction match {
+ case PlanetSideEmpire.TR => mini_chaingun
+ case PlanetSideEmpire.NC => r_shotgun
+ case PlanetSideEmpire.VS => lasher
+ case PlanetSideEmpire.NEUTRAL => suppressor //do not hand out the maelstrom
+ }
+ }
+
+ /**
+ * For a given faction, provide the ammunition for the heavy assault rifle.
+ * The ammunition value here must work with the result of obtaining the rifle using the faction.
+ * @param faction the faction
+ * @return thr `AmmoBoxDefinition` for the rifle's ammo
+ * @see `GlobalDefinitions.HeavyRifle`
+ */
+ def HeavyRifleAmmo(faction : PlanetSideEmpire.Value) : AmmoBoxDefinition = {
+ faction match {
+ case PlanetSideEmpire.TR => bullet_9mm
+ case PlanetSideEmpire.NC => shotgun_shell
+ case PlanetSideEmpire.VS => energy_cell
+ case PlanetSideEmpire.NEUTRAL => bullet_9mm
+ }
+ }
+
+ /**
+ * For a given faction, provide the anti-vehicular launcher.
+ * @param faction the faction
+ * @return the `ToolDefinition` for the launcher
+ */
+ def AntiVehicular(faction : PlanetSideEmpire.Value) : ToolDefinition = {
+ faction match {
+ case PlanetSideEmpire.TR => striker
+ case PlanetSideEmpire.NC => hunterseeker
+ case PlanetSideEmpire.VS => lancer
+ case PlanetSideEmpire.NEUTRAL => phoenix
+ }
+ }
+
+ /**
+ * For a given faction, provide the ammunition for the anti-vehicular launcher.
+ * The ammunition value here must work with the result of obtaining the anti-vehicular launcher using the faction.
+ * @param faction the faction
+ * @return thr `AmmoBoxDefinition` for the launcher's ammo
+ * @see `GlobalDefinitions.AntiVehicular`
+ */
+ def AntiVehicularAmmo(faction : PlanetSideEmpire.Value) : AmmoBoxDefinition = {
+ faction match {
+ case PlanetSideEmpire.TR => striker_missile_ammo
+ case PlanetSideEmpire.NC => hunter_seeker_missile
+ case PlanetSideEmpire.VS => lancer_cartridge
+ case PlanetSideEmpire.NEUTRAL => phoenix_missile //careful - does not exist as an AmmoBox normally
+ }
+ }
+
+ /**
+ * Using the definition for a piece of `Equipment` determine with which faction it aligns if it is a weapon.
+ * Only checks `Tool` objects.
+ * Useful for determining if some item has to be dropped during an activity like `InfantryLoadout` switching.
+ * @param edef the `EquipmentDefinition` of the item
+ * @return the faction alignment, or `Neutral`
+ */
+ def isFactionWeapon(edef : EquipmentDefinition) : PlanetSideEmpire.Value = {
+ edef match {
+ case `chainblade` | `repeater` | `anniversary_guna` | `cycler` | `mini_chaingun` | `striker` =>
+ PlanetSideEmpire.TR
+ case `magcutter` | `isp` | `anniversary_gun` | `gauss` | `r_shotgun` | `hunterseeker` =>
+ PlanetSideEmpire.NC
+ case `forceblade` | `beamer` | `anniversary_gunb` | `pulsar` | `lasher` | `lancer` =>
+ PlanetSideEmpire.VS
+ case _ =>
+ PlanetSideEmpire.NEUTRAL
+ }
+ }
+
+ /**
+ * Using the definition for a piece of `Equipment` determine with which faction it aligns.
+ * Checks both `Tool` objects and unique `AmmoBox` objects.
+ * @param edef the `EquipmentDefinition` of the item
+ * @return the faction alignment, or `Neutral`
+ */
+ def isFactionEquipment(edef : EquipmentDefinition) : PlanetSideEmpire.Value = {
+ edef match {
+ case `chainblade` | `repeater` | `anniversary_guna` | `cycler` | `mini_chaingun` | `striker` | `striker_missile_ammo` =>
+ PlanetSideEmpire.TR
+ case `magcutter` | `isp` | `anniversary_gun` | `gauss` | `r_shotgun` | `hunterseeker` | `hunter_seeker_missile` =>
+ PlanetSideEmpire.NC
+ case `forceblade` | `beamer` | `anniversary_gunb` | `pulsar` | `lasher` | `lancer` | `energy_cell` | `lancer_cartridge` =>
+ PlanetSideEmpire.VS
+ case _ =>
+ PlanetSideEmpire.NEUTRAL
+ }
+ }
+
+ /**
+ * Using the definition for a piece of `Equipment` determine whether it is a "cavern weapon."
+ * Useful for determining if some item has to be dropped during an activity like `InfantryLoadout` switching.
+ * @param edef the `EquipmentDefinition` of the item
+ * @return `true`, if it is; otherwise, `false`
+ */
+ def isCavernWeapon(edef : EquipmentDefinition) : Boolean = {
+ edef match {
+ case `spiker` | `maelstrom` | `radiator` => true
+ case _ => false
+ }
+ }
+
+ /**
+ * Using the definition for a piece of `Equipment` determine whether it is "cavern equipment."
+ * @param edef the `EquipmentDefinition` of the item
+ * @return `true`, if it is; otherwise, `false`
+ */
+ def isCavernEquipment(edef : EquipmentDefinition) : Boolean = {
+ edef match {
+ case `spiker` | `maelstrom` | `radiator` | `ancient_ammo_combo` | `maelstrom_ammo` => true
+ case _ => false
+ }
+ }
+
+ /**
+ * Using the definition for a piece of `Equipment` determine whether it is "special."
+ * "Special equipment" is any non-standard `Equipment` that, while it can be obtained from a `Terminal`, has artificial prerequisites.
+ * For example, the Kits are unlocked as rewards for holiday events and require possessing a specific `MeritCommendation`.
+ * @param edef the `EquipmentDefinition` of the item
+ * @return `true`, if it is; otherwise, `false`
+ */
+ def isSpecialEquipment(edef : EquipmentDefinition) : Boolean = {
+ edef match {
+ case `super_medkit` | `super_armorkit` | `super_staminakit` | `katana` =>
+ true
+ case _ =>
+ false
+ }
+ }
+
+ val
+ medkit = KitDefinition(Kits.medkit)
+
+ val
+ super_medkit = KitDefinition(Kits.super_medkit)
+
+ val
+ super_armorkit = KitDefinition(Kits.super_armorkit)
+
+ val
+ super_staminakit = KitDefinition(Kits.super_staminakit) //super stimpak
+
+ val
+ melee_ammo = AmmoBoxDefinition(Ammo.melee_ammo)
+
+ val
+ frag_grenade_ammo = AmmoBoxDefinition(Ammo.frag_grenade_ammo)
+
+ val
+ plasma_grenade_ammo = AmmoBoxDefinition(Ammo.plasma_grenade_ammo)
+
+ val
+ jammer_grenade_ammo = AmmoBoxDefinition(Ammo.jammer_grenade_ammo)
+
+ val
+ bullet_9mm = AmmoBoxDefinition(Ammo.bullet_9mm)
+ bullet_9mm.Capacity = 50
+ bullet_9mm.Tile = InventoryTile.Tile33
+
+ val
+ bullet_9mm_AP = AmmoBoxDefinition(Ammo.bullet_9mm_AP)
+ bullet_9mm_AP.Capacity = 50
+ bullet_9mm_AP.Tile = InventoryTile.Tile33
+
+ val
+ shotgun_shell = AmmoBoxDefinition(Ammo.shotgun_shell)
+ shotgun_shell.Capacity = 32
+ shotgun_shell.Tile = InventoryTile.Tile33
+
+ val
+ shotgun_shell_AP = AmmoBoxDefinition(Ammo.shotgun_shell_AP)
+ shotgun_shell_AP.Capacity = 32
+ shotgun_shell_AP.Tile = InventoryTile.Tile33
+
+ val
+ energy_cell = AmmoBoxDefinition(Ammo.energy_cell)
+ energy_cell.Capacity = 50
+ energy_cell.Tile = InventoryTile.Tile33
+
+ val
+ anniversary_ammo = AmmoBoxDefinition(Ammo.anniversary_ammo) //10mm multi-phase
+ anniversary_ammo.Capacity = 30
+ anniversary_ammo.Tile = InventoryTile.Tile33
+
+ val
+ ancient_ammo_combo = AmmoBoxDefinition(Ammo.ancient_ammo_combo)
+ ancient_ammo_combo.Capacity = 30
+ ancient_ammo_combo.Tile = InventoryTile.Tile33
+
+ val
+ maelstrom_ammo = AmmoBoxDefinition(Ammo.maelstrom_ammo)
+ maelstrom_ammo.Capacity = 50
+ maelstrom_ammo.Tile = InventoryTile.Tile33
+
+ val
+ phoenix_missile = AmmoBoxDefinition(Ammo.phoenix_missile) //decimator missile
+
+ val
+ striker_missile_ammo = AmmoBoxDefinition(Ammo.striker_missile_ammo)
+ striker_missile_ammo.Capacity = 15
+ striker_missile_ammo.Tile = InventoryTile.Tile44
+
+ val
+ hunter_seeker_missile = AmmoBoxDefinition(Ammo.hunter_seeker_missile) //phoenix missile
+ hunter_seeker_missile.Capacity = 9
+ hunter_seeker_missile.Tile = InventoryTile.Tile44
+
+ val
+ lancer_cartridge = AmmoBoxDefinition(Ammo.lancer_cartridge)
+ lancer_cartridge.Capacity = 18
+ lancer_cartridge.Tile = InventoryTile.Tile44
+
+ val
+ rocket = AmmoBoxDefinition(Ammo.rocket)
+ rocket.Capacity = 15
+ rocket.Tile = InventoryTile.Tile33
+
+ val
+ frag_cartridge = AmmoBoxDefinition(Ammo.frag_cartridge)
+ frag_cartridge.Capacity = 12
+ frag_cartridge.Tile = InventoryTile.Tile33
+
+ val
+ plasma_cartridge = AmmoBoxDefinition(Ammo.plasma_cartridge)
+ plasma_cartridge.Capacity = 12
+ plasma_cartridge.Tile = InventoryTile.Tile33
+
+ val
+ jammer_cartridge = AmmoBoxDefinition(Ammo.jammer_cartridge)
+ jammer_cartridge.Capacity = 12
+ jammer_cartridge.Tile = InventoryTile.Tile33
+
+ val
+ bolt = AmmoBoxDefinition(Ammo.bolt)
+ bolt.Capacity = 10
+ bolt.Tile = InventoryTile.Tile33
+
+ val
+ oicw_ammo = AmmoBoxDefinition(Ammo.oicw_ammo) //scorpion missile
+ oicw_ammo.Capacity = 10
+ oicw_ammo.Tile = InventoryTile.Tile44
+
+ val
+ flamethrower_ammo = AmmoBoxDefinition(Ammo.flamethrower_ammo)
+ flamethrower_ammo.Capacity = 100
+ flamethrower_ammo.Tile = InventoryTile.Tile44
+
+ val
+ health_canister = AmmoBoxDefinition(Ammo.health_canister)
+ health_canister.Capacity = 100
+ health_canister.Tile = InventoryTile.Tile33
+
+ val
+ armor_canister = AmmoBoxDefinition(Ammo.armor_canister)
+ armor_canister.Capacity = 100
+ armor_canister.Tile = InventoryTile.Tile33
+
+ val
+ upgrade_canister = AmmoBoxDefinition(Ammo.upgrade_canister)
+ upgrade_canister.Capacity = 100
+ upgrade_canister.Tile = InventoryTile.Tile33
+
+ val
+ trek_ammo = AmmoBoxDefinition(Ammo.trek_ammo)
+//
+ val
+ bullet_35mm = AmmoBoxDefinition(Ammo.bullet_35mm) //liberator nosegun
+ bullet_35mm.Capacity = 100
+ bullet_35mm.Tile = InventoryTile.Tile44
+
+ val
+ aphelion_laser_ammo = AmmoBoxDefinition(Ammo.aphelion_laser_ammo)
+ aphelion_laser_ammo.Capacity = 165
+ aphelion_laser_ammo.Tile = InventoryTile.Tile44
+
+ val
+ aphelion_immolation_cannon_ammo = AmmoBoxDefinition(Ammo.aphelion_immolation_cannon_ammo)
+ aphelion_immolation_cannon_ammo.Capacity = 100
+ aphelion_immolation_cannon_ammo.Tile = InventoryTile.Tile55
+
+ val
+ aphelion_plasma_rocket_ammo = AmmoBoxDefinition(Ammo.aphelion_plasma_rocket_ammo)
+ aphelion_plasma_rocket_ammo.Capacity = 195
+ aphelion_plasma_rocket_ammo.Tile = InventoryTile.Tile55
+
+ val
+ aphelion_ppa_ammo = AmmoBoxDefinition(Ammo.aphelion_ppa_ammo)
+ aphelion_ppa_ammo.Capacity = 110
+ aphelion_ppa_ammo.Tile = InventoryTile.Tile44
+
+ val
+ aphelion_starfire_ammo = AmmoBoxDefinition(Ammo.aphelion_starfire_ammo)
+ aphelion_starfire_ammo.Capacity = 132
+ aphelion_starfire_ammo.Tile = InventoryTile.Tile44
+
+ val
+ skyguard_flak_cannon_ammo = AmmoBoxDefinition(Ammo.skyguard_flak_cannon_ammo)
+ skyguard_flak_cannon_ammo.Capacity = 200
+ skyguard_flak_cannon_ammo.Tile = InventoryTile.Tile44
+
+ val
+ flux_cannon_thresher_battery = AmmoBoxDefinition(Ammo.flux_cannon_thresher_battery)
+ flux_cannon_thresher_battery.Capacity = 150
+ flux_cannon_thresher_battery.Tile = InventoryTile.Tile44
+
+ val
+ fluxpod_ammo = AmmoBoxDefinition(Ammo.fluxpod_ammo)
+ fluxpod_ammo.Capacity = 80
+ fluxpod_ammo.Tile = InventoryTile.Tile44
+
+ val
+ hellfire_ammo = AmmoBoxDefinition(Ammo.hellfire_ammo)
+ hellfire_ammo.Capacity = 24
+ hellfire_ammo.Tile = InventoryTile.Tile44
+
+ val
+ liberator_bomb = AmmoBoxDefinition(Ammo.liberator_bomb)
+ liberator_bomb.Capacity = 20
+ liberator_bomb.Tile = InventoryTile.Tile44
+
+ val
+ bullet_25mm = AmmoBoxDefinition(Ammo.bullet_25mm) //liberator tailgun
+ bullet_25mm.Capacity = 150
+ bullet_25mm.Tile = InventoryTile.Tile44
+
+ val
+ bullet_75mm = AmmoBoxDefinition(Ammo.bullet_75mm) //lightning shell
+ bullet_75mm.Capacity = 100
+ bullet_75mm.Tile = InventoryTile.Tile44
+
+ val
+ heavy_grenade_mortar = AmmoBoxDefinition(Ammo.heavy_grenade_mortar) //marauder and gal gunship
+ heavy_grenade_mortar.Capacity = 100
+ heavy_grenade_mortar.Tile = InventoryTile.Tile44
+
+ val
+ pulse_battery = AmmoBoxDefinition(Ammo.pulse_battery)
+ pulse_battery.Capacity = 100
+ pulse_battery.Tile = InventoryTile.Tile44
+
+ val
+ heavy_rail_beam_battery = AmmoBoxDefinition(Ammo.heavy_rail_beam_battery)
+ heavy_rail_beam_battery.Capacity = 100
+ heavy_rail_beam_battery.Tile = InventoryTile.Tile44
+
+ val
+ reaver_rocket = AmmoBoxDefinition(Ammo.reaver_rocket)
+ reaver_rocket.Capacity = 12
+ reaver_rocket.Tile = InventoryTile.Tile44
+
+ val
+ bullet_20mm = AmmoBoxDefinition(Ammo.bullet_20mm) //reaver nosegun
+ bullet_20mm.Capacity = 200
+ bullet_20mm.Tile = InventoryTile.Tile44
+
+ val
+ bullet_12mm = AmmoBoxDefinition(Ammo.bullet_12mm) //common
+ bullet_12mm.Capacity = 200
+ bullet_12mm.Tile = InventoryTile.Tile44
+
+ val
+ wasp_rocket_ammo = AmmoBoxDefinition(Ammo.wasp_rocket_ammo)
+ wasp_rocket_ammo.Capacity = 6
+ wasp_rocket_ammo.Tile = InventoryTile.Tile44
+
+ val
+ wasp_gun_ammo = AmmoBoxDefinition(Ammo.wasp_gun_ammo) //wasp nosegun
+ wasp_gun_ammo.Capacity = 150
+ wasp_gun_ammo.Tile = InventoryTile.Tile44
+
+ val
+ bullet_15mm = AmmoBoxDefinition(Ammo.bullet_15mm)
+ bullet_15mm.Capacity = 360
+ bullet_15mm.Tile = InventoryTile.Tile44
+
+ val
+ colossus_100mm_cannon_ammo = AmmoBoxDefinition(Ammo.colossus_100mm_cannon_ammo)
+ colossus_100mm_cannon_ammo.Capacity = 90
+ colossus_100mm_cannon_ammo.Tile = InventoryTile.Tile55
+
+ val
+ colossus_burster_ammo = AmmoBoxDefinition(Ammo.colossus_burster_ammo)
+ colossus_burster_ammo.Capacity = 235
+ colossus_burster_ammo.Tile = InventoryTile.Tile44
+
+ val
+ colossus_cluster_bomb_ammo = AmmoBoxDefinition(Ammo.colossus_cluster_bomb_ammo) //colossus mortar launcher shells
+ colossus_cluster_bomb_ammo.Capacity = 150
+ colossus_cluster_bomb_ammo.Tile = InventoryTile.Tile55
+
+ val
+ colossus_chaingun_ammo = AmmoBoxDefinition(Ammo.colossus_chaingun_ammo)
+ colossus_chaingun_ammo.Capacity = 600
+ colossus_chaingun_ammo.Tile = InventoryTile.Tile44
+
+ val
+ colossus_tank_cannon_ammo = AmmoBoxDefinition(Ammo.colossus_tank_cannon_ammo)
+ colossus_tank_cannon_ammo.Capacity = 110
+ colossus_tank_cannon_ammo.Tile = InventoryTile.Tile44
+
+ val
+ bullet_105mm = AmmoBoxDefinition(Ammo.bullet_105mm) //prowler 100mm cannon shell
+ bullet_105mm.Capacity = 100
+ bullet_105mm.Tile = InventoryTile.Tile44
+
+ val
+ gauss_cannon_ammo = AmmoBoxDefinition(Ammo.gauss_cannon_ammo)
+ gauss_cannon_ammo.Capacity = 15
+ gauss_cannon_ammo.Tile = InventoryTile.Tile44
+
+ val
+ peregrine_dual_machine_gun_ammo = AmmoBoxDefinition(Ammo.peregrine_dual_machine_gun_ammo)
+ peregrine_dual_machine_gun_ammo.Capacity = 240
+ peregrine_dual_machine_gun_ammo.Tile = InventoryTile.Tile44
+
+ val
+ peregrine_mechhammer_ammo = AmmoBoxDefinition(Ammo.peregrine_mechhammer_ammo)
+ peregrine_mechhammer_ammo.Capacity = 30
+ peregrine_mechhammer_ammo.Tile = InventoryTile.Tile44
+
+ val
+ peregrine_particle_cannon_ammo = AmmoBoxDefinition(Ammo.peregrine_particle_cannon_ammo)
+ peregrine_particle_cannon_ammo.Capacity = 40
+ peregrine_particle_cannon_ammo.Tile = InventoryTile.Tile55
+
+ val
+ peregrine_rocket_pod_ammo = AmmoBoxDefinition(Ammo.peregrine_rocket_pod_ammo)
+ peregrine_rocket_pod_ammo.Capacity = 275
+ peregrine_rocket_pod_ammo.Tile = InventoryTile.Tile55
+
+ val
+ peregrine_sparrow_ammo = AmmoBoxDefinition(Ammo.peregrine_sparrow_ammo)
+ peregrine_sparrow_ammo.Capacity = 150
+ peregrine_sparrow_ammo.Tile = InventoryTile.Tile44
+
+ val
+ bullet_150mm = AmmoBoxDefinition(Ammo.bullet_150mm)
+ bullet_150mm.Capacity = 50
+ bullet_150mm.Tile = InventoryTile.Tile44
+
+ val
+ chainblade = ToolDefinition(ObjectClass.chainblade)
+ chainblade.Size = EquipmentSize.Melee
+ chainblade.AmmoTypes += Ammo.melee_ammo
+ chainblade.FireModes += new FireModeDefinition
+ chainblade.FireModes.head.AmmoTypeIndices += 0
+ chainblade.FireModes.head.AmmoSlotIndex = 0
+ chainblade.FireModes.head.Magazine = 1
+ chainblade.FireModes += new FireModeDefinition
+ chainblade.FireModes(1).AmmoTypeIndices += 0
+ chainblade.FireModes(1).AmmoSlotIndex = 0
+ chainblade.FireModes(1).Magazine = 1
+
+ val
+ magcutter = ToolDefinition(ObjectClass.magcutter)
+ magcutter.Size = EquipmentSize.Melee
+ magcutter.AmmoTypes += Ammo.melee_ammo
+ magcutter.FireModes += new FireModeDefinition
+ magcutter.FireModes.head.AmmoTypeIndices += 0
+ magcutter.FireModes.head.AmmoSlotIndex = 0
+ magcutter.FireModes.head.Magazine = 1
+ magcutter.FireModes += new FireModeDefinition
+ magcutter.FireModes(1).AmmoTypeIndices += 0
+ magcutter.FireModes(1).AmmoSlotIndex = 0
+ magcutter.FireModes(1).Magazine = 1
+
+ val
+ forceblade = ToolDefinition(ObjectClass.forceblade)
+ forceblade.Size = EquipmentSize.Melee
+ forceblade.AmmoTypes += Ammo.melee_ammo
+ forceblade.FireModes += new FireModeDefinition
+ forceblade.FireModes.head.AmmoTypeIndices += 0
+ forceblade.FireModes.head.AmmoSlotIndex = 0
+ forceblade.FireModes.head.Magazine = 1
+ forceblade.FireModes.head.Chamber = 0
+ forceblade.FireModes += new FireModeDefinition
+ forceblade.FireModes(1).AmmoTypeIndices += 0
+ forceblade.FireModes(1).AmmoSlotIndex = 0
+ forceblade.FireModes(1).Magazine = 1
+ forceblade.FireModes(1).Chamber = 0
+
+ val
+ katana = ToolDefinition(ObjectClass.katana)
+ katana.Size = EquipmentSize.Melee
+ katana.AmmoTypes += Ammo.melee_ammo
+ katana.FireModes += new FireModeDefinition
+ katana.FireModes.head.AmmoTypeIndices += 0
+ katana.FireModes.head.AmmoSlotIndex = 0
+ katana.FireModes.head.Magazine = 1
+ katana.FireModes.head.Chamber = 0
+ katana.FireModes += new FireModeDefinition
+ katana.FireModes(1).AmmoTypeIndices += 0
+ katana.FireModes(1).AmmoSlotIndex = 0
+ katana.FireModes(1).Magazine = 1
+ katana.FireModes(1).Chamber = 0
+
+ val
+ frag_grenade = ToolDefinition(ObjectClass.frag_grenade)
+ frag_grenade.Size = EquipmentSize.Pistol
+ frag_grenade.AmmoTypes += Ammo.frag_grenade_ammo
+ frag_grenade.FireModes += new FireModeDefinition
+ frag_grenade.FireModes.head.AmmoTypeIndices += 0
+ frag_grenade.FireModes.head.AmmoSlotIndex = 0
+ frag_grenade.FireModes.head.Magazine = 3
+ frag_grenade.FireModes += new FireModeDefinition
+ frag_grenade.FireModes(1).AmmoTypeIndices += 0
+ frag_grenade.FireModes(1).AmmoSlotIndex = 0
+ frag_grenade.FireModes(1).Magazine = 3
+ frag_grenade.Tile = InventoryTile.Tile22
+
+ val
+ plasma_grenade = ToolDefinition(ObjectClass.plasma_grenade)
+ plasma_grenade.Size = EquipmentSize.Pistol
+ plasma_grenade.AmmoTypes += Ammo.plasma_grenade_ammo
+ plasma_grenade.FireModes += new FireModeDefinition
+ plasma_grenade.FireModes.head.AmmoTypeIndices += 0
+ plasma_grenade.FireModes.head.AmmoSlotIndex = 0
+ plasma_grenade.FireModes.head.Magazine = 3
+ plasma_grenade.FireModes += new FireModeDefinition
+ plasma_grenade.FireModes(1).AmmoTypeIndices += 0
+ plasma_grenade.FireModes(1).AmmoSlotIndex = 0
+ plasma_grenade.FireModes(1).Magazine = 3
+ plasma_grenade.Tile = InventoryTile.Tile22
+
+ val
+ jammer_grenade = ToolDefinition(ObjectClass.jammer_grenade)
+ jammer_grenade.Size = EquipmentSize.Pistol
+ jammer_grenade.AmmoTypes += Ammo.jammer_grenade_ammo
+ jammer_grenade.FireModes += new FireModeDefinition
+ jammer_grenade.FireModes.head.AmmoTypeIndices += 0
+ jammer_grenade.FireModes.head.AmmoSlotIndex = 0
+ jammer_grenade.FireModes.head.Magazine = 3
+ jammer_grenade.FireModes += new FireModeDefinition
+ jammer_grenade.FireModes(1).AmmoTypeIndices += 0
+ jammer_grenade.FireModes(1).AmmoSlotIndex = 0
+ jammer_grenade.FireModes(1).Magazine = 3
+ jammer_grenade.Tile = InventoryTile.Tile22
+
+ val
+ repeater = ToolDefinition(ObjectClass.repeater)
+ repeater.Size = EquipmentSize.Pistol
+ repeater.AmmoTypes += Ammo.bullet_9mm
+ repeater.AmmoTypes += Ammo.bullet_9mm_AP
+ repeater.FireModes += new FireModeDefinition
+ repeater.FireModes.head.AmmoTypeIndices += 0
+ repeater.FireModes.head.AmmoTypeIndices += 1
+ repeater.FireModes.head.AmmoSlotIndex = 0
+ repeater.FireModes.head.Magazine = 20
+ repeater.Tile = InventoryTile.Tile33
+
+ val
+ isp = ToolDefinition(ObjectClass.isp) //mag-scatter
+ isp.Size = EquipmentSize.Pistol
+ isp.AmmoTypes += Ammo.shotgun_shell
+ isp.AmmoTypes += Ammo.shotgun_shell_AP
+ isp.FireModes += new FireModeDefinition
+ isp.FireModes.head.AmmoTypeIndices += 0
+ isp.FireModes.head.AmmoTypeIndices += 1
+ isp.FireModes.head.AmmoSlotIndex = 0
+ isp.FireModes.head.Magazine = 8
+ isp.Tile = InventoryTile.Tile33
+
+ val
+ beamer = ToolDefinition(ObjectClass.beamer)
+ beamer.Size = EquipmentSize.Pistol
+ beamer.AmmoTypes += Ammo.energy_cell
+ beamer.FireModes += new FireModeDefinition
+ beamer.FireModes.head.AmmoTypeIndices += 0
+ beamer.FireModes.head.AmmoSlotIndex = 0
+ beamer.FireModes.head.Magazine = 16
+ beamer.FireModes += new FireModeDefinition
+ beamer.FireModes(1).AmmoTypeIndices += 0
+ beamer.FireModes(1).AmmoSlotIndex = 0
+ beamer.FireModes(1).Magazine = 16
+ beamer.Tile = InventoryTile.Tile33
+
+ val
+ ilc9 = ToolDefinition(ObjectClass.ilc9) //amp
+ ilc9.Size = EquipmentSize.Pistol
+ ilc9.AmmoTypes += Ammo.bullet_9mm
+ ilc9.AmmoTypes += Ammo.bullet_9mm_AP
+ ilc9.FireModes += new FireModeDefinition
+ ilc9.FireModes.head.AmmoTypeIndices += 0
+ ilc9.FireModes.head.AmmoTypeIndices += 1
+ ilc9.FireModes.head.AmmoSlotIndex = 0
+ ilc9.FireModes.head.Magazine = 30
+ ilc9.Tile = InventoryTile.Tile33
+
+ val
+ suppressor = ToolDefinition(ObjectClass.suppressor)
+ suppressor.Size = EquipmentSize.Rifle
+ suppressor.AmmoTypes += Ammo.bullet_9mm
+ suppressor.AmmoTypes += Ammo.bullet_9mm_AP
+ suppressor.FireModes += new FireModeDefinition
+ suppressor.FireModes.head.AmmoTypeIndices += 0
+ suppressor.FireModes.head.AmmoTypeIndices += 1
+ suppressor.FireModes.head.AmmoSlotIndex = 0
+ suppressor.FireModes.head.Magazine = 25
+ suppressor.Tile = InventoryTile.Tile63
+
+ val
+ punisher = ToolDefinition(ObjectClass.punisher)
+ punisher.Size = EquipmentSize.Rifle
+ punisher.AmmoTypes += Ammo.bullet_9mm
+ punisher.AmmoTypes += Ammo.bullet_9mm_AP
+ punisher.AmmoTypes += Ammo.rocket
+ punisher.AmmoTypes += Ammo.frag_cartridge
+ punisher.AmmoTypes += Ammo.jammer_cartridge
+ punisher.AmmoTypes += Ammo.plasma_cartridge
+ punisher.FireModes += new FireModeDefinition
+ punisher.FireModes.head.AmmoTypeIndices += 0
+ punisher.FireModes.head.AmmoTypeIndices += 1
+ punisher.FireModes.head.AmmoSlotIndex = 0
+ punisher.FireModes.head.Magazine = 30
+ punisher.FireModes += new FireModeDefinition
+ punisher.FireModes(1).AmmoTypeIndices += 2
+ punisher.FireModes(1).AmmoTypeIndices += 3
+ punisher.FireModes(1).AmmoTypeIndices += 4
+ punisher.FireModes(1).AmmoTypeIndices += 5
+ punisher.FireModes(1).AmmoSlotIndex = 1
+ punisher.FireModes(1).Magazine = 1
+ punisher.Tile = InventoryTile.Tile63
+
+ val
+ flechette = ToolDefinition(ObjectClass.flechette) //sweeper
+ flechette.Size = EquipmentSize.Rifle
+ flechette.AmmoTypes += Ammo.shotgun_shell
+ flechette.AmmoTypes += Ammo.shotgun_shell_AP
+ flechette.FireModes += new FireModeDefinition
+ flechette.FireModes.head.AmmoTypeIndices += 0
+ flechette.FireModes.head.AmmoTypeIndices += 1
+ flechette.FireModes.head.AmmoSlotIndex = 0
+ flechette.FireModes.head.Magazine = 12 //12 shells * 8 pellets = 96
+ flechette.Tile = InventoryTile.Tile63
+
+ val
+ cycler = ToolDefinition(ObjectClass.cycler)
+ cycler.Size = EquipmentSize.Rifle
+ cycler.AmmoTypes += Ammo.bullet_9mm
+ cycler.AmmoTypes += Ammo.bullet_9mm_AP
+ cycler.FireModes += new FireModeDefinition
+ cycler.FireModes.head.AmmoTypeIndices += 0
+ cycler.FireModes.head.AmmoTypeIndices += 1
+ cycler.FireModes.head.AmmoSlotIndex = 0
+ cycler.FireModes.head.Magazine = 50
+ cycler.Tile = InventoryTile.Tile63
+
+ val
+ gauss = ToolDefinition(ObjectClass.gauss)
+ gauss.Size = EquipmentSize.Rifle
+ gauss.AmmoTypes += Ammo.bullet_9mm
+ gauss.AmmoTypes += Ammo.bullet_9mm_AP
+ gauss.FireModes += new FireModeDefinition
+ gauss.FireModes.head.AmmoTypeIndices += 0
+ gauss.FireModes.head.AmmoTypeIndices += 1
+ gauss.FireModes.head.AmmoSlotIndex = 0
+ gauss.FireModes.head.Magazine = 30
+ gauss.Tile = InventoryTile.Tile63
+
+ val
+ pulsar = ToolDefinition(ObjectClass.pulsar)
+ pulsar.Size = EquipmentSize.Rifle
+ pulsar.AmmoTypes += Ammo.energy_cell
+ pulsar.FireModes += new FireModeDefinition
+ pulsar.FireModes.head.AmmoTypeIndices += 0
+ pulsar.FireModes.head.AmmoSlotIndex = 0
+ pulsar.FireModes.head.Magazine = 40
+ pulsar.FireModes += new FireModeDefinition
+ pulsar.FireModes(1).AmmoTypeIndices += 0
+ pulsar.FireModes(1).AmmoSlotIndex = 0
+ pulsar.FireModes(1).Magazine = 40
+ pulsar.Tile = InventoryTile.Tile63
+
+ val
+ anniversary_guna = ToolDefinition(ObjectClass.anniversary_guna) //tr stinger
+ anniversary_guna.Size = EquipmentSize.Pistol
+ anniversary_guna.AmmoTypes += Ammo.anniversary_ammo
+ anniversary_guna.FireModes += new FireModeDefinition
+ anniversary_guna.FireModes.head.AmmoTypeIndices += 0
+ anniversary_guna.FireModes.head.AmmoSlotIndex = 0
+ anniversary_guna.FireModes.head.Magazine = 6
+ anniversary_guna.FireModes += new FireModeDefinition
+ anniversary_guna.FireModes(1).AmmoTypeIndices += 0
+ anniversary_guna.FireModes(1).AmmoSlotIndex = 0
+ anniversary_guna.FireModes(1).Magazine = 6
+ anniversary_guna.FireModes(1).Chamber = 6
+ anniversary_guna.Tile = InventoryTile.Tile33
+
+ val
+ anniversary_gun = ToolDefinition(ObjectClass.anniversary_gun) //nc spear
+ anniversary_gun.Size = EquipmentSize.Pistol
+ anniversary_gun.AmmoTypes += Ammo.anniversary_ammo
+ anniversary_gun.FireModes += new FireModeDefinition
+ anniversary_gun.FireModes.head.AmmoTypeIndices += 0
+ anniversary_gun.FireModes.head.AmmoSlotIndex = 0
+ anniversary_gun.FireModes.head.Magazine = 6
+ anniversary_gun.FireModes += new FireModeDefinition
+ anniversary_gun.FireModes(1).AmmoTypeIndices += 0
+ anniversary_gun.FireModes(1).AmmoSlotIndex = 0
+ anniversary_gun.FireModes(1).Magazine = 6
+ anniversary_gun.FireModes(1).Chamber = 6
+ anniversary_gun.Tile = InventoryTile.Tile33
+
+ val
+ anniversary_gunb = ToolDefinition(ObjectClass.anniversary_gunb) //vs eraser
+ anniversary_gunb.Size = EquipmentSize.Pistol
+ anniversary_gunb.AmmoTypes += Ammo.anniversary_ammo
+ anniversary_gunb.FireModes += new FireModeDefinition
+ anniversary_gunb.FireModes.head.AmmoTypeIndices += 0
+ anniversary_gunb.FireModes.head.AmmoSlotIndex = 0
+ anniversary_gunb.FireModes.head.Magazine = 6
+ anniversary_gunb.FireModes += new FireModeDefinition
+ anniversary_gunb.FireModes(1).AmmoTypeIndices += 0
+ anniversary_gunb.FireModes(1).AmmoSlotIndex = 0
+ anniversary_gunb.FireModes(1).Magazine = 6
+ anniversary_gunb.FireModes(1).Chamber = 6
+ anniversary_gunb.Tile = InventoryTile.Tile33
+
+ val
+ spiker = ToolDefinition(ObjectClass.spiker)
+ spiker.Size = EquipmentSize.Pistol
+ spiker.AmmoTypes += Ammo.ancient_ammo_combo
+ spiker.FireModes += new FireModeDefinition
+ spiker.FireModes.head.AmmoTypeIndices += 0
+ spiker.FireModes.head.AmmoSlotIndex = 0
+ spiker.FireModes.head.Magazine = 25
+ spiker.Tile = InventoryTile.Tile33
+
+ val
+ mini_chaingun = ToolDefinition(ObjectClass.mini_chaingun)
+ mini_chaingun.Size = EquipmentSize.Rifle
+ mini_chaingun.AmmoTypes += Ammo.bullet_9mm
+ mini_chaingun.AmmoTypes += Ammo.bullet_9mm_AP
+ mini_chaingun.FireModes += new FireModeDefinition
+ mini_chaingun.FireModes.head.AmmoTypeIndices += 0
+ mini_chaingun.FireModes.head.AmmoTypeIndices += 1
+ mini_chaingun.FireModes.head.AmmoSlotIndex = 0
+ mini_chaingun.FireModes.head.Magazine = 100
+ mini_chaingun.Tile = InventoryTile.Tile93
+
+ val
+ r_shotgun = ToolDefinition(ObjectClass.r_shotgun) //jackhammer
+ r_shotgun.Size = EquipmentSize.Rifle
+ r_shotgun.AmmoTypes += Ammo.shotgun_shell
+ r_shotgun.AmmoTypes += Ammo.shotgun_shell_AP
+ r_shotgun.FireModes += new FireModeDefinition
+ r_shotgun.FireModes.head.AmmoTypeIndices += 0
+ r_shotgun.FireModes.head.AmmoTypeIndices += 1
+ r_shotgun.FireModes.head.AmmoSlotIndex = 0
+ r_shotgun.FireModes.head.Magazine = 16 //16 shells * 8 pellets = 128
+ r_shotgun.FireModes += new FireModeDefinition
+ r_shotgun.FireModes(1).AmmoTypeIndices += 0
+ r_shotgun.FireModes(1).AmmoTypeIndices += 1
+ r_shotgun.FireModes(1).AmmoSlotIndex = 0
+ r_shotgun.FireModes(1).Magazine = 16 //16 shells * 8 pellets = 128
+ r_shotgun.FireModes(1).Chamber = 3
+ r_shotgun.Tile = InventoryTile.Tile93
+
+ val
+ lasher = ToolDefinition(ObjectClass.lasher)
+ lasher.Size = EquipmentSize.Rifle
+ lasher.AmmoTypes += Ammo.energy_cell
+ lasher.FireModes += new FireModeDefinition
+ lasher.FireModes.head.AmmoTypeIndices += 0
+ lasher.FireModes.head.AmmoSlotIndex = 0
+ lasher.FireModes.head.Magazine = 35
+ lasher.FireModes += new FireModeDefinition
+ lasher.FireModes(1).AmmoTypeIndices += 0
+ lasher.FireModes(1).AmmoSlotIndex = 0
+ lasher.FireModes(1).Magazine = 35
+ lasher.Tile = InventoryTile.Tile93
+
+ val
+ maelstrom = ToolDefinition(ObjectClass.maelstrom)
+ maelstrom.Size = EquipmentSize.Rifle
+ maelstrom.AmmoTypes += Ammo.maelstrom_ammo
+ maelstrom.FireModes += new FireModeDefinition
+ maelstrom.FireModes.head.AmmoTypeIndices += 0
+ maelstrom.FireModes.head.AmmoSlotIndex = 0
+ maelstrom.FireModes.head.Magazine = 150
+ maelstrom.FireModes += new FireModeDefinition
+ maelstrom.FireModes(1).AmmoTypeIndices += 0
+ maelstrom.FireModes(1).AmmoSlotIndex = 0
+ maelstrom.FireModes(1).Magazine = 150
+ maelstrom.FireModes += new FireModeDefinition
+ maelstrom.FireModes(2).AmmoTypeIndices += 0
+ maelstrom.FireModes(2).AmmoSlotIndex = 0
+ maelstrom.FireModes(2).Magazine = 150
+ maelstrom.Tile = InventoryTile.Tile93
+
+ val
+ phoenix = ToolDefinition(ObjectClass.phoenix) //decimator
+ phoenix.Size = EquipmentSize.Rifle
+ phoenix.AmmoTypes += Ammo.phoenix_missile
+ phoenix.FireModes += new FireModeDefinition
+ phoenix.FireModes.head.AmmoTypeIndices += 0
+ phoenix.FireModes.head.AmmoSlotIndex = 0
+ phoenix.FireModes.head.Magazine = 3
+ phoenix.FireModes += new FireModeDefinition
+ phoenix.FireModes(1).AmmoTypeIndices += 0
+ phoenix.FireModes(1).AmmoSlotIndex = 0
+ phoenix.FireModes(1).Magazine = 3
+ phoenix.Tile = InventoryTile.Tile93
+
+ val
+ striker = ToolDefinition(ObjectClass.striker)
+ striker.Size = EquipmentSize.Rifle
+ striker.AmmoTypes += Ammo.striker_missile_ammo
+ striker.FireModes += new FireModeDefinition
+ striker.FireModes.head.AmmoTypeIndices += 0
+ striker.FireModes.head.AmmoSlotIndex = 0
+ striker.FireModes.head.Magazine = 5
+ striker.FireModes += new FireModeDefinition
+ striker.FireModes(1).AmmoTypeIndices += 0
+ striker.FireModes(1).AmmoSlotIndex = 0
+ striker.FireModes(1).Magazine = 5
+ striker.Tile = InventoryTile.Tile93
+
+ val
+ hunterseeker = ToolDefinition(ObjectClass.hunterseeker) //phoenix
+ hunterseeker.Size = EquipmentSize.Rifle
+ hunterseeker.AmmoTypes += Ammo.hunter_seeker_missile
+ hunterseeker.FireModes += new FireModeDefinition
+ hunterseeker.FireModes.head.AmmoTypeIndices += 0
+ hunterseeker.FireModes.head.AmmoSlotIndex = 0
+ hunterseeker.FireModes.head.Magazine = 1
+ hunterseeker.FireModes += new FireModeDefinition
+ hunterseeker.FireModes(1).AmmoTypeIndices += 0
+ hunterseeker.FireModes(1).AmmoSlotIndex = 0
+ hunterseeker.FireModes(1).Magazine = 1
+ hunterseeker.Tile = InventoryTile.Tile93
+
+ val
+ lancer = ToolDefinition(ObjectClass.lancer)
+ lancer.Size = EquipmentSize.Rifle
+ lancer.AmmoTypes += Ammo.lancer_cartridge
+ lancer.FireModes += new FireModeDefinition
+ lancer.FireModes.head.AmmoTypeIndices += 0
+ lancer.FireModes.head.AmmoSlotIndex = 0
+ lancer.FireModes.head.Magazine = 6
+ lancer.Tile = InventoryTile.Tile93
+
+ val
+ rocklet = ToolDefinition(ObjectClass.rocklet)
+ rocklet.Size = EquipmentSize.Rifle
+ rocklet.AmmoTypes += Ammo.rocket
+ rocklet.AmmoTypes += Ammo.frag_cartridge
+ rocklet.FireModes += new FireModeDefinition
+ rocklet.FireModes.head.AmmoTypeIndices += 0
+ rocklet.FireModes.head.AmmoTypeIndices += 1
+ rocklet.FireModes.head.AmmoSlotIndex = 0
+ rocklet.FireModes.head.Magazine = 6
+ rocklet.FireModes += new FireModeDefinition
+ rocklet.FireModes(1).AmmoTypeIndices += 0
+ rocklet.FireModes(1).AmmoTypeIndices += 1
+ rocklet.FireModes(1).AmmoSlotIndex = 0
+ rocklet.FireModes(1).Magazine = 6
+ rocklet.FireModes(1).Chamber = 6
+ rocklet.Tile = InventoryTile.Tile63
+
+ val
+ thumper = ToolDefinition(ObjectClass.thumper)
+ thumper.Size = EquipmentSize.Rifle
+ thumper.AmmoTypes += Ammo.frag_cartridge
+ thumper.AmmoTypes += Ammo.plasma_cartridge
+ thumper.AmmoTypes += Ammo.jammer_cartridge
+ thumper.FireModes += new FireModeDefinition
+ thumper.FireModes.head.AmmoTypeIndices += 0
+ thumper.FireModes.head.AmmoTypeIndices += 1
+ thumper.FireModes.head.AmmoTypeIndices += 2
+ thumper.FireModes.head.AmmoSlotIndex = 0
+ thumper.FireModes.head.Magazine = 6
+ thumper.FireModes += new FireModeDefinition
+ thumper.FireModes(1).AmmoTypeIndices += 0
+ thumper.FireModes(1).AmmoTypeIndices += 1
+ thumper.FireModes(1).AmmoTypeIndices += 2
+ thumper.FireModes(1).AmmoSlotIndex = 0
+ thumper.FireModes(1).Magazine = 6
+ thumper.Tile = InventoryTile.Tile63
+
+ val
+ radiator = ToolDefinition(ObjectClass.radiator)
+ radiator.Size = EquipmentSize.Rifle
+ radiator.AmmoTypes += Ammo.ancient_ammo_combo
+ radiator.FireModes += new FireModeDefinition
+ radiator.FireModes.head.AmmoTypeIndices += 0
+ radiator.FireModes.head.AmmoSlotIndex = 0
+ radiator.FireModes.head.Magazine = 25
+ radiator.FireModes += new FireModeDefinition
+ radiator.FireModes(1).AmmoTypeIndices += 0
+ radiator.FireModes(1).AmmoSlotIndex = 0
+ radiator.FireModes(1).Magazine = 25
+ radiator.Tile = InventoryTile.Tile63
+
+ val
+ heavy_sniper = ToolDefinition(ObjectClass.heavy_sniper) //hsr
+ heavy_sniper.Size = EquipmentSize.Rifle
+ heavy_sniper.AmmoTypes += Ammo.bolt
+ heavy_sniper.FireModes += new FireModeDefinition
+ heavy_sniper.FireModes.head.AmmoTypeIndices += 0
+ heavy_sniper.FireModes.head.AmmoSlotIndex = 0
+ heavy_sniper.FireModes.head.Magazine = 10
+ heavy_sniper.Tile = InventoryTile.Tile93
+
+ val
+ bolt_driver = ToolDefinition(ObjectClass.bolt_driver)
+ bolt_driver.Size = EquipmentSize.Rifle
+ bolt_driver.AmmoTypes += Ammo.bolt
+ bolt_driver.FireModes += new FireModeDefinition
+ bolt_driver.FireModes.head.AmmoTypeIndices += 0
+ bolt_driver.FireModes.head.AmmoSlotIndex = 0
+ bolt_driver.FireModes.head.Magazine = 1
+ bolt_driver.Tile = InventoryTile.Tile93
+
+ val
+ oicw = ToolDefinition(ObjectClass.oicw) //scorpion
+ oicw.Size = EquipmentSize.Rifle
+ oicw.AmmoTypes += Ammo.oicw_ammo
+ oicw.FireModes += new FireModeDefinition
+ oicw.FireModes.head.AmmoTypeIndices += 0
+ oicw.FireModes.head.AmmoSlotIndex = 0
+ oicw.FireModes.head.Magazine = 1
+ oicw.FireModes += new FireModeDefinition
+ oicw.FireModes(1).AmmoTypeIndices += 0
+ oicw.FireModes(1).AmmoSlotIndex = 0
+ oicw.FireModes(1).Magazine = 1
+ oicw.Tile = InventoryTile.Tile93
+
+ val
+ flamethrower = ToolDefinition(ObjectClass.flamethrower)
+ flamethrower.Size = EquipmentSize.Rifle
+ flamethrower.AmmoTypes += Ammo.flamethrower_ammo
+ flamethrower.FireModes += new FireModeDefinition
+ flamethrower.FireModes.head.AmmoTypeIndices += 0
+ flamethrower.FireModes.head.AmmoSlotIndex = 0
+ flamethrower.FireModes.head.Magazine = 100
+ flamethrower.FireModes.head.Chamber = 5
+ flamethrower.FireModes += new FireModeDefinition
+ flamethrower.FireModes(1).AmmoTypeIndices += 0
+ flamethrower.FireModes(1).AmmoSlotIndex = 0
+ flamethrower.FireModes(1).Magazine = 100
+ flamethrower.FireModes(1).Chamber = 50
+ flamethrower.Tile = InventoryTile.Tile63
+
+ val
+ medicalapplicator = ToolDefinition(ObjectClass.medicalapplicator)
+ medicalapplicator.Size = EquipmentSize.Pistol
+ medicalapplicator.AmmoTypes += Ammo.health_canister
+ medicalapplicator.FireModes += new FireModeDefinition
+ medicalapplicator.FireModes.head.AmmoTypeIndices += 0
+ medicalapplicator.FireModes.head.AmmoSlotIndex = 0
+ medicalapplicator.FireModes.head.Magazine = 100
+ medicalapplicator.FireModes += new FireModeDefinition
+ medicalapplicator.FireModes(1).AmmoTypeIndices += 0
+ medicalapplicator.FireModes(1).AmmoSlotIndex = 0
+ medicalapplicator.FireModes(1).Magazine = 100
+ medicalapplicator.Tile = InventoryTile.Tile33
+
+ val
+ nano_dispenser = ToolDefinition(ObjectClass.nano_dispenser)
+ nano_dispenser.Size = EquipmentSize.Rifle
+ nano_dispenser.AmmoTypes += Ammo.armor_canister
+ nano_dispenser.AmmoTypes += Ammo.upgrade_canister
+ nano_dispenser.FireModes += new FireModeDefinition
+ nano_dispenser.FireModes.head.AmmoTypeIndices += 0
+ nano_dispenser.FireModes.head.AmmoTypeIndices += 1
+ nano_dispenser.FireModes.head.AmmoSlotIndex = 0
+ nano_dispenser.FireModes.head.Magazine = 100
+ nano_dispenser.Tile = InventoryTile.Tile63
+
+ val
+ bank = ToolDefinition(ObjectClass.bank)
+ bank.Size = EquipmentSize.Pistol
+ bank.AmmoTypes += Ammo.armor_canister
+ bank.FireModes += new FireModeDefinition
+ bank.FireModes.head.AmmoTypeIndices += 0
+ bank.FireModes.head.AmmoSlotIndex = 0
+ bank.FireModes.head.Magazine = 100
+ bank.FireModes += new FireModeDefinition
+ bank.FireModes(1).AmmoTypeIndices += 0
+ bank.FireModes(1).AmmoSlotIndex = 0
+ bank.FireModes(1).Magazine = 100
+ bank.Tile = InventoryTile.Tile33
+
+ val
+ remote_electronics_kit = SimpleItemDefinition(SItem.remote_electronics_kit)
+ remote_electronics_kit.Packet = new REKConverter
+ remote_electronics_kit.Tile = InventoryTile.Tile33
+
+ val
+ trek = ToolDefinition(ObjectClass.trek)
+ trek.Size = EquipmentSize.Pistol
+ trek.AmmoTypes += Ammo.trek_ammo
+ trek.FireModes += new FireModeDefinition
+ trek.FireModes.head.AmmoTypeIndices += 0
+ trek.FireModes.head.AmmoSlotIndex = 0
+ trek.FireModes.head.Magazine = 4
+ trek.FireModes += new FireModeDefinition
+ trek.FireModes(1).AmmoTypeIndices += 0
+ trek.FireModes(1).AmmoSlotIndex = 0
+ trek.FireModes(1).Magazine = 0
+ trek.Tile = InventoryTile.Tile33
+
+ val
+ flail_targeting_laser = SimpleItemDefinition(SItem.flail_targeting_laser)
+ flail_targeting_laser.Packet = new CommandDetonaterConverter
+
+ val
+ command_detonater = SimpleItemDefinition(SItem.command_detonater)
+ command_detonater.Packet = new CommandDetonaterConverter
+
+ val
+ ace = ConstructionItemDefinition(CItem.Unit.ace)
+ ace.Modes += DeployedItem.boomer
+ ace.Modes += DeployedItem.he_mine
+ ace.Modes += DeployedItem.jammer_mine
+ ace.Modes += DeployedItem.spitfire_turret
+ ace.Modes += DeployedItem.spitfire_cloaked
+ ace.Modes += DeployedItem.spitfire_aa
+ ace.Modes += DeployedItem.motionalarmsensor
+ ace.Modes += DeployedItem.sensor_shield
+ ace.Tile = InventoryTile.Tile33
+
+ val
+ advanced_ace = ConstructionItemDefinition(CItem.Unit.advanced_ace)
+ advanced_ace.Modes += DeployedItem.tank_traps
+ advanced_ace.Modes += DeployedItem.portable_manned_turret
+ advanced_ace.Modes += DeployedItem.deployable_shield_generator
+ advanced_ace.Tile = InventoryTile.Tile63
+
+ val
+ fury_weapon_systema = ToolDefinition(ObjectClass.fury_weapon_systema)
+ fury_weapon_systema.Size = EquipmentSize.VehicleWeapon
+ fury_weapon_systema.AmmoTypes += Ammo.hellfire_ammo
+ fury_weapon_systema.FireModes += new FireModeDefinition
+ fury_weapon_systema.FireModes.head.AmmoTypeIndices += 0
+ fury_weapon_systema.FireModes.head.AmmoSlotIndex = 0
+ fury_weapon_systema.FireModes.head.Magazine = 2
+
+ val
+ fury = VehicleDefinition(ObjectClass.fury)
+ fury.Seats += 0 -> new SeatDefinition()
+ fury.Seats(0).Bailable = true
+ fury.Seats(0).ControlledWeapon = Some(1)
+ fury.MountPoints += 0 -> 0
+ fury.MountPoints += 2 -> 0
+ fury.Weapons += 1 -> fury_weapon_systema
+ fury.TrunkSize = InventoryTile(11, 11)
+ fury.TrunkOffset = 30
+}
diff --git a/common/src/main/scala/net/psforever/objects/Implant.scala b/common/src/main/scala/net/psforever/objects/Implant.scala
new file mode 100644
index 000000000..2d7483f71
--- /dev/null
+++ b/common/src/main/scala/net/psforever/objects/Implant.scala
@@ -0,0 +1,86 @@
+// Copyright (c) 2017 PSForever
+package net.psforever.objects
+
+import net.psforever.objects.definition.{ImplantDefinition, Stance}
+import net.psforever.types.{ExoSuitType, ImplantType}
+
+/**
+ * A type of installable player utility that grants a perk, usually in exchange for stamina (energy).
+ *
+ * An implant starts with a never-to-initialized timer value of -1 and will not report as `Ready` until the timer is 0.
+ * The `Timer`, however, will report to the user a time of 0 since negative time does not make sense.
+ * Although the `Timer` can be manually set, using `Reset` is the better way to default the initialization timer to the correct amount.
+ * An external script will be necessary to operate the actual initialization countdown.
+ * An implant must be `Ready` before it can be `Active`.
+ * The `Timer` must be set (or reset) (or countdown) to 0 to be `Ready` and then it must be activated.
+ * @param implantDef the `ObjectDefinition` that constructs this item and maintains some of its immutable fields
+ */
+class Implant(implantDef : ImplantDefinition) {
+ private var active : Boolean = false
+ private var initTimer : Long = -1L
+
+ def Name : String = implantDef.Name
+
+ def Ready : Boolean = initTimer == 0L
+
+ def Active : Boolean = active
+
+ def Active_=(isActive : Boolean) : Boolean = {
+ active = Ready && isActive
+ Active
+ }
+
+ def Timer : Long = math.max(0, initTimer)
+
+ def Timer_=(time : Long) : Long = {
+ initTimer = math.max(0, time)
+ Timer
+ }
+
+ def MaxTimer : Long = implantDef.Initialization
+
+ def ActivationCharge : Int = Definition.ActivationCharge
+
+ /**
+ * Calculate the stamina consumption of the implant for any given moment of being active after its activation.
+ * As implant energy use can be influenced by both exo-suit worn and general stance held, both are considered.
+ * @param suit the exo-suit being worn
+ * @param stance the player's stance
+ * @return the amount of stamina (energy) that is consumed
+ */
+ def Charge(suit : ExoSuitType.Value, stance : Stance.Value) : Int = {
+ if(active) {
+ implantDef.DurationChargeBase + implantDef.DurationChargeByExoSuit(suit) + implantDef.DurationChargeByStance(stance)
+ }
+ else {
+ 0
+ }
+ }
+
+ /**
+ * Place an implant back in its initializing state.
+ */
+ def Reset() : Unit = {
+ Active = false
+ Timer = MaxTimer
+ }
+
+ /**
+ * Place an implant back in its pre-initialization state.
+ * The implant is inactive and can not proceed to a `Ready` condition naturally from this state.
+ */
+ def Jammed() : Unit = {
+ Active = false
+ Timer = -1
+ }
+
+ def Definition : ImplantDefinition = implantDef
+}
+
+object Implant {
+ def default : Implant = new Implant(ImplantDefinition(ImplantType.RangeMagnifier))
+
+ def apply(implantDef : ImplantDefinition) : Implant = {
+ new Implant(implantDef)
+ }
+}
diff --git a/common/src/main/scala/net/psforever/objects/ImplantSlot.scala b/common/src/main/scala/net/psforever/objects/ImplantSlot.scala
new file mode 100644
index 000000000..00951f503
--- /dev/null
+++ b/common/src/main/scala/net/psforever/objects/ImplantSlot.scala
@@ -0,0 +1,62 @@
+// Copyright (c) 2017 PSForever
+package net.psforever.objects
+
+import net.psforever.objects.definition.ImplantDefinition
+import net.psforever.types.ImplantType
+
+/**
+ * A slot "on the player" into which an implant is installed.
+ *
+ * In total, players have three implant slots.
+ * At battle rank one (BR1), however, all of those slots are locked.
+ * The player earns implants at BR16, BR12, and BR18.
+ * A locked implant slot can not be used.
+ * (The code uses "not yet unlocked" logic.)
+ * When unlocked, an implant may be installed into that slot.
+ *
+ * The default implant that the underlying slot utilizes is the "Range Magnifier."
+ * Until the `Installed` condition is some value other than `None`, however, the implant in the slot will not work.
+ */
+class ImplantSlot {
+ /** is this slot available for holding an implant */
+ private var unlocked : Boolean = false
+ /** what implant is currently installed in this slot; None if there is no implant currently installed */
+ private var installed : Option[ImplantType.Value] = None
+ /** the entry for that specific implant used by the a player; always occupied by some type of implant */
+ private var implant : Implant = ImplantSlot.default
+
+ def Unlocked : Boolean = unlocked
+
+ def Unlocked_=(lock : Boolean) : Boolean = {
+ unlocked = lock
+ Unlocked
+ }
+
+ def Installed : Option[ImplantType.Value] = installed
+
+ def Implant : Option[Implant] = if(Installed.isDefined) { Some(implant) } else { None }
+
+ def Implant_=(anImplant : Option[Implant]) : Option[Implant] = {
+ anImplant match {
+ case Some(module) =>
+ Implant = module
+ case None =>
+ installed = None
+ }
+ Implant
+ }
+
+ def Implant_=(anImplant : Implant) : Option[Implant] = {
+ implant = anImplant
+ installed = Some(anImplant.Definition.Type)
+ Implant
+ }
+}
+
+object ImplantSlot {
+ private val default = new Implant(ImplantDefinition(ImplantType.RangeMagnifier))
+
+ def apply() : ImplantSlot = {
+ new ImplantSlot()
+ }
+}
diff --git a/common/src/main/scala/net/psforever/objects/InfantryLoadout.scala b/common/src/main/scala/net/psforever/objects/InfantryLoadout.scala
new file mode 100644
index 000000000..ac1c27df7
--- /dev/null
+++ b/common/src/main/scala/net/psforever/objects/InfantryLoadout.scala
@@ -0,0 +1,265 @@
+// Copyright (c) 2017 PSForever
+package net.psforever.objects
+
+import net.psforever.objects.definition._
+import net.psforever.objects.equipment.Equipment
+import net.psforever.objects.inventory.InventoryItem
+import net.psforever.types.ExoSuitType
+
+import scala.annotation.tailrec
+
+/**
+ * From a `Player` their current exo-suit and their `Equipment`, retain a set of instructions to reconstruct this arrangement.
+ *
+ * `InfantryLoadout` objects are composed of the following information, as if a blueprint:
+ * - the avatar's current exo-suit
+ * - the type of specialization, called a "subtype" (mechanized assault exo-suits only)
+ * - the contents of the avatar's occupied holster slots
+ * - the contents of the avatar's occupied inventory
+ * `Equipment` contents of the holsters and of the formal inventory region will be condensed into a simplified form.
+ * These are also "blueprints."
+ * At its most basic, this simplification will merely comprise the former object's `EquipmentDefinition`.
+ * For items that are already simple - `Kit` objects and `SimpleItem` objects - this form will not be too far removed.
+ * For more complicated affairs like `Tool` objects and `AmmoBox` objects, only essential information will be retained.
+ *
+ * The deconstructed blueprint can be applied to any avatar.
+ * They are, however, typically tied to unique users and unique characters.
+ * For reasons of certifications, however, permissions on that avatar may affect what `Equipment` can be distributed.
+ * Even a whole blueprint can be denied if the user lacks the necessary exo-suit certification.
+ * A completely new piece of `Equipment` is constructed when the `Loadout` is regurgitated.
+ *
+ * The fifth tab on an `order_terminal` window is for "Favorite" blueprints for `InfantryLoadout` entries.
+ * The ten-long list is initialized with `FavoritesMessage` packets.
+ * Specific entries are loaded or removed using `FavoritesRequest` packets.
+ * @param player the player
+ * @param label the name by which this inventory will be known when displayed in a Favorites list
+ */
+class InfantryLoadout(player : Player, private val label : String) {
+ /** the exo-suit */
+ private val exosuit : ExoSuitType.Value = player.ExoSuit
+ /** the MAX specialization, to differentiate the three types of MAXes who all use the same exo-suit name */
+ private val subtype =
+ if(exosuit == ExoSuitType.MAX) {
+ import net.psforever.packet.game.objectcreate.ObjectClass
+ player.Holsters().head.Equipment.get.Definition.ObjectId match {
+ case ObjectClass.trhev_dualcycler | ObjectClass.nchev_scattercannon | ObjectClass.vshev_quasar =>
+ 1
+ case ObjectClass.trhev_pounder | ObjectClass.nchev_falcon | ObjectClass.vshev_comet =>
+ 2
+ case ObjectClass.trhev_burster | ObjectClass.nchev_sparrow | ObjectClass.vshev_starfire =>
+ 3
+ case _ =>
+ 0
+ }
+ }
+ else {
+ 0
+ }
+ /** simplified representation of the holster `Equipment` */
+ private val holsters : List[InfantryLoadout.SimplifiedEntry] =
+ InfantryLoadout.packageSimplifications(player.Holsters())
+ /** simplified representation of the inventory `Equipment` */
+ private val inventory : List[InfantryLoadout.SimplifiedEntry] =
+ InfantryLoadout.packageSimplifications(player.Inventory.Items.values.toList)
+
+ /**
+ * The label by which this `InfantryLoadout` is called.
+ * @return the label
+ */
+ def Label : String = label
+
+ /**
+ * The exo-suit in which the avatar will be dressed.
+ * Might be restricted and, thus, restrict the rest of the `Equipment` from being constructed and given.
+ * @return the exo-suit
+ */
+ def ExoSuit : ExoSuitType.Value = exosuit
+
+ /**
+ * The mechanized assault exo-suit specialization number that indicates whether the MAX performs:
+ * anti-infantry (1),
+ * anti-vehicular (2),
+ * or anti-air work (3).
+ * The major distinction is the type of arm weapons that MAX is equipped.
+ * When the blueprint doesn't call for a MAX, the number will be 0.
+ * @return the specialization number
+ */
+ def Subtype : Int = subtype
+
+ /**
+ * The `Equipment` in the `Player`'s holster slots when this `InfantryLoadout` is created.
+ * @return a `List` of the holster item blueprints
+ */
+ def Holsters : List[InfantryLoadout.SimplifiedEntry] = holsters
+
+ /**
+ * The `Equipment` in the `Player`'s inventory region when this `InfantryLoadout` is created.
+ * @return a `List` of the inventory item blueprints
+ */
+ def Inventory : List[InfantryLoadout.SimplifiedEntry] = inventory
+}
+
+object InfantryLoadout {
+ /**
+ * A basic `Trait` connecting all of the `Equipment` blueprints.
+ */
+ sealed trait Simplification
+
+ /**
+ * An entry in the `InfantryLoadout`, wrapping around a slot index and what is in the slot index.
+ * @param item the `Equipment`
+ * @param index the slot number where the `Equipment` is to be stowed
+ * @see `InventoryItem`
+ */
+ final case class SimplifiedEntry(item: Simplification, index: Int)
+
+ /**
+ * The simplified form of an `AmmoBox`.
+ * @param adef the `AmmoBoxDefinition` that describes this future object
+ * @param capacity the amount of ammunition, if any, to initialize;
+ * if `None`, then the previous `AmmoBoxDefinition` will be referenced for the amount later
+ */
+ final case class ShorthandAmmoBox(adef : AmmoBoxDefinition, capacity : Int) extends Simplification
+ /**
+ * The simplified form of a `Tool`.
+ * @param tdef the `ToolDefinition` that describes this future object
+ * @param ammo the blueprints to construct the correct number of ammunition slots in the `Tool`
+ */
+ final case class ShorthandTool(tdef : ToolDefinition, ammo : List[ShorthandAmmotSlot]) extends Simplification
+ /**
+ * The simplified form of a `Tool` `FireMode`
+ * @param ammoIndex the index that points to the type of ammunition this slot currently uses
+ * @param ammo a `ShorthandAmmoBox` object to load into that slot
+ */
+ final case class ShorthandAmmotSlot(ammoIndex : Int, ammo : ShorthandAmmoBox)
+ /**
+ * The simplified form of a `ConstructionItem`.
+ * @param cdef the `ConstructionItemDefinition` that describes this future object
+ */
+ final case class ShorthandConstructionItem(cdef : ConstructionItemDefinition) extends Simplification
+ /**
+ * The simplified form of a `SimpleItem`.
+ * @param sdef the `SimpleItemDefinition` that describes this future object
+ */
+ final case class ShorthandSimpleItem(sdef : SimpleItemDefinition) extends Simplification
+ /**
+ * The simplified form of a `Kit`.
+ * @param kdef the `KitDefinition` that describes this future object
+ */
+ final case class ShorthandKit(kdef : KitDefinition) extends Simplification
+
+ /**
+ * Overloaded entry point for constructing simplified blueprints from holster slot equipment.
+ * @param equipment the holster slots
+ * @return a `List` of simplified `Equipment`
+ */
+ private def packageSimplifications(equipment : Array[EquipmentSlot]) : List[SimplifiedEntry] = {
+ recursiveHolsterSimplifications(equipment.iterator)
+ }
+
+ /**
+ * Overloaded entry point for constructing simplified blueprints from inventory region equipment.
+ * @param equipment the enumerated contents of the inventory
+ * @return a `List` of simplified `Equipment`
+ */
+ private def packageSimplifications(equipment : List[InventoryItem]) : List[SimplifiedEntry] = {
+ recursiveInventorySimplifications(equipment.iterator)
+ }
+
+ /**
+ * Traverse a `Player`'s holsters and transform occupied slots into simplified blueprints for the contents of that slot.
+ * The holsters are fixed positions and can be unoccupied.
+ * Only occupied holsters are transformed into blueprints.
+ * The `index` field is necessary as the `Iterator` for the holsters lacks self-knowledge about slot position.
+ * @param iter an `Iterator`
+ * @param index the starting index;
+ * defaults to 0 and increments automatically
+ * @param list an updating `List` of simplified `Equipment` blueprints;
+ * empty, by default
+ * @return a `List` of simplified `Equipment` blueprints
+ */
+ @tailrec private def recursiveHolsterSimplifications(iter : Iterator[EquipmentSlot], index : Int = 0, list : List[SimplifiedEntry] = Nil) : List[SimplifiedEntry] = {
+ if(!iter.hasNext) {
+ list
+ }
+ else {
+ val entry = iter.next
+ entry.Equipment match {
+ case Some(obj) =>
+ recursiveHolsterSimplifications(iter, index + 1, list :+ SimplifiedEntry(buildSimplification(obj), index))
+ case None =>
+ recursiveHolsterSimplifications(iter, index + 1, list)
+ }
+ }
+ }
+
+ /**
+ * Traverse a `Player`'s inventory and transform `Equipment` into simplified blueprints.
+ * @param iter an `Iterator`
+ * @param list an updating `List` of simplified `Equipment` blueprints;
+ * empty, by default
+ * @return a `List` of simplified `Equipment` blueprints
+ */
+ @tailrec private def recursiveInventorySimplifications(iter : Iterator[InventoryItem], list : List[SimplifiedEntry] = Nil) : List[SimplifiedEntry] = {
+ if(!iter.hasNext) {
+ list
+ }
+ else {
+ val entry = iter.next
+ recursiveInventorySimplifications(iter, list :+ SimplifiedEntry(buildSimplification(entry.obj), entry.start))
+ }
+ }
+
+ /**
+ * Ammunition slots are internal connection points where `AmmoBox` units and their characteristics represent a `Tool`'s magazine.
+ * Their simplification process has a layer of complexity that ensures that the content of the slot matches the type of content that should be in the slot.
+ * If it does not, it extracts information about the slot from the `EquipmentDefinition` and sets the blueprints to that.
+ * @param iter an `Iterator`
+ * @param list an updating `List` of simplified ammo slot blueprints;
+ * empty, by default
+ * @return a `List` of simplified ammo slot blueprints
+ * @see `Tool.FireModeSlot`
+ */
+ @tailrec private def recursiveFireModeSimplications(iter : Iterator[Tool.FireModeSlot], list : List[ShorthandAmmotSlot] = Nil) : List[ShorthandAmmotSlot] = {
+ if(!iter.hasNext) {
+ list
+ }
+ else {
+ val entry = iter.next
+ val fmodeSimp = if(entry.Box.AmmoType == entry.AmmoType) {
+ ShorthandAmmotSlot(
+ entry.AmmoTypeIndex,
+ ShorthandAmmoBox(entry.Box.Definition, entry.Box.Capacity)
+ )
+ }
+ else {
+ ShorthandAmmotSlot(
+ entry.AmmoTypeIndex,
+ ShorthandAmmoBox(AmmoBoxDefinition(entry.Tool.Definition.AmmoTypes(entry.Definition.AmmoTypeIndices.head).id), 1)
+ )
+ }
+ recursiveFireModeSimplications(iter, list :+ fmodeSimp)
+ }
+ }
+
+ /**
+ * Accept a piece of `Equipment` and transform it into a simplified blueprint.
+ * @param obj the `Equipment`
+ * @return the simplified blueprint
+ */
+ private def buildSimplification(obj : Equipment) : Simplification = {
+ obj match {
+ case obj : Tool =>
+ val flist = recursiveFireModeSimplications(obj.AmmoSlots.iterator)
+ ShorthandTool(obj.Definition, flist)
+ case obj : AmmoBox =>
+ ShorthandAmmoBox(obj.Definition, obj.Capacity)
+ case obj : ConstructionItem =>
+ ShorthandConstructionItem(obj.Definition)
+ case obj : SimpleItem =>
+ ShorthandSimpleItem(obj.Definition)
+ case obj : Kit =>
+ ShorthandKit(obj.Definition)
+ }
+ }
+}
diff --git a/common/src/main/scala/net/psforever/objects/Kit.scala b/common/src/main/scala/net/psforever/objects/Kit.scala
new file mode 100644
index 000000000..e8c89d83d
--- /dev/null
+++ b/common/src/main/scala/net/psforever/objects/Kit.scala
@@ -0,0 +1,26 @@
+// Copyright (c) 2017 PSForever
+package net.psforever.objects
+
+import net.psforever.objects.definition.KitDefinition
+import net.psforever.objects.equipment.Equipment
+
+/**
+ * A one-time-use recovery item that can be applied by the player while held within their inventory.
+ * @param kitDef the `ObjectDefinition` that constructs this item and maintains some of its immutable fields
+ */
+class Kit(private val kitDef : KitDefinition) extends Equipment {
+ def Definition : KitDefinition = kitDef
+}
+
+object Kit {
+ def apply(kitDef : KitDefinition) : Kit = {
+ new Kit(kitDef)
+ }
+
+ import net.psforever.packet.game.PlanetSideGUID
+ def apply(guid : PlanetSideGUID, kitDef : KitDefinition) : Kit = {
+ val obj = new Kit(kitDef)
+ obj.GUID = guid
+ obj
+ }
+}
diff --git a/common/src/main/scala/net/psforever/objects/LivePlayerList.scala b/common/src/main/scala/net/psforever/objects/LivePlayerList.scala
new file mode 100644
index 000000000..c0ad0e8f3
--- /dev/null
+++ b/common/src/main/scala/net/psforever/objects/LivePlayerList.scala
@@ -0,0 +1,184 @@
+// Copyright (c) 2017 PSForever
+package net.psforever.objects
+
+import net.psforever.packet.game.PlanetSideGUID
+
+import scala.collection.concurrent.{Map, TrieMap}
+
+/**
+ * See the companion object for class and method documentation.
+ * `LivePlayerList` is a singleton and this private class lacks exposure.
+ */
+private class LivePlayerList {
+ /** key - the session id; value - a `Player` object */
+ private val sessionMap : Map[Long, Player] = new TrieMap[Long, Player]
+ /** key - the global unique identifier; value - the session id */
+ private val playerMap : Map[Int, Long] = new TrieMap[Int, Long]
+
+ def WorldPopulation(predicate : ((_, Player)) => Boolean) : List[Player] = {
+ sessionMap.filter(predicate).map({ case(_, char) => char }).toList
+ }
+
+ def Add(sessionId : Long, player : Player) : Boolean = {
+ sessionMap.values.find(char => char.equals(player)) match {
+ case None =>
+ sessionMap.putIfAbsent(sessionId, player).isEmpty
+ true
+ case Some(_) =>
+ false
+ }
+ }
+
+ def Remove(sessionId : Long) : Option[Player] = {
+ sessionMap.remove(sessionId) match {
+ case Some(char) =>
+ playerMap.find({ case(_, sess) => sess == sessionId }) match {
+ case Some((guid, _)) =>
+ playerMap.remove(guid)
+ case None => ;
+ }
+ Some(char)
+ case None =>
+ None
+ }
+ }
+
+ def Get(guid : PlanetSideGUID) : Option[Player] = {
+ Get(guid.guid)
+ }
+
+ def Get(guid : Int) : Option[Player] = {
+ playerMap.get(guid) match {
+ case Some(sess) =>
+ sessionMap.get(sess)
+ case _ =>
+ None
+ }
+ }
+
+ def Assign(sessionId : Long, guid : PlanetSideGUID) : Boolean = Assign(sessionId, guid.guid)
+
+ def Assign(sessionId : Long, guid : Int) : Boolean = {
+ sessionMap.find({ case(sess, _) => sess == sessionId}) match {
+ case Some((_, char)) =>
+ if(char.GUID.guid == guid) {
+ playerMap.find({ case(_, sess) => sess == sessionId }) match {
+ case Some((id, _)) =>
+ playerMap.remove(id)
+ case None => ;
+ }
+ playerMap.put(guid, sessionId)
+ true
+ }
+ else {
+ false
+ }
+
+ case None =>
+ false
+ }
+ }
+
+ def Shutdown : List[Player] = {
+ val list = sessionMap.values.toList
+ sessionMap.clear
+ playerMap.clear
+ list
+ }
+}
+
+/**
+ * A class for storing `Player` mappings for users that are currently online.
+ * The mapping system is tightly coupled between the `Player` class and to an instance of `WorldSessionActor`.
+ * A loose coupling between the current globally unique identifier (GUID) and the user is also present.
+ *
+ * Use:
+ * 1) When a users logs in during `WorldSessionActor`, associate that user's session id and the character.
+ * `LivePlayerList.Add(session, player)`
+ * 2) When that user's chosen character is declared his avatar using `SetCurrentAvatarMessage`,
+ * also associate the user's session with their current GUID.
+ * `LivePlayerList.Assign(session, guid)`
+ * 3) Repeat the previous step for as many times the user's GUID changes, especially during the aforementioned condition.
+ * 4a) In between the previous two steps, a user's character may be referenced by their current GUID.
+ * `LivePlayerList.Get(guid)`
+ * 4b) Also in between those same previous steps, a range of characters may be queried based on provided statistics.
+ * `LivePlayerList.WorldPopulation(...)`
+ * 5) When the user leaves the game, his character's entries are removed from the mappings.
+ * `LivePlayerList.Remove(session)`
+ */
+object LivePlayerList {
+ /** As `LivePlayerList` is a singleton, an object of `LivePlayerList` is automatically instantiated. */
+ private val Instance : LivePlayerList = new LivePlayerList
+
+ /**
+ * Given some criteria, examine the mapping of user characters and find the ones that fulfill the requirements.
+ *
+ * Note the signature carefully.
+ * A two-element tuple is checked, but only the second element of that tuple - a character - is eligible for being queried.
+ * The first element is ignored.
+ * Even a predicate as simple as `{ case ((x : Long, _)) => x > 0 }` will not work for that reason.
+ * @param predicate the conditions for filtering the live `Player`s
+ * @return a list of users's `Player`s that fit the criteria
+ */
+ def WorldPopulation(predicate : ((_, Player)) => Boolean) : List[Player] = Instance.WorldPopulation(predicate)
+
+ /**
+ * Create a mapped entry between the user's session and a user's character.
+ * Neither the player nor the session may exist in the current mappings if this is to work.
+ * @param sessionId the session
+ * @param player the character
+ * @return `true`, if the session was association was made; `false`, otherwise
+ */
+ def Add(sessionId : Long, player : Player) : Boolean = Instance.Add(sessionId, player)
+
+ /**
+ * Remove all entries related to the given session identifier from the mappings.
+ * The player no longer counts as "online."
+ * This function cleans up __all__ associations - those created by `Add`, and those created by `Assign`.
+ * @param sessionId the session
+ * @return any character that was afffected by the mapping removal
+ */
+ def Remove(sessionId : Long) : Option[Player] = Instance.Remove(sessionId)
+
+ /**
+ * Get a user's character from the mappings.
+ * @param guid the current GUID of the character
+ * @return the character, if it can be found using the GUID
+ */
+ def Get(guid : PlanetSideGUID) : Option[Player] = Instance.Get(guid)
+
+ /**
+ * Get a user's character from the mappings.
+ * @param guid the current GUID of the character
+ * @return the character, if it can be found using the GUID
+ */
+ def Get(guid : Int) : Option[Player] = Instance.Get(guid)
+
+ /**
+ * Given a session that maps to a user's character, create a mapping between the character's current GUID and the session.
+ * If the user already has a GUID in the mappings, remove it and assert the new one.
+ * @param sessionId the session
+ * @param guid the GUID to associate with the character;
+ * technically, it has already been assigned and should be findable using `{character}.GUID.guid`
+ * @return `true`, if the mapping was created;
+ * `false`, if the session can not be found or if the character's GUID doesn't match the one provided
+ */
+ def Assign(sessionId : Long, guid : PlanetSideGUID) : Boolean = Instance.Assign(sessionId, guid)
+
+ /**
+ * Given a session that maps to a user's character, create a mapping between the character's current GUID and the session.
+ * If the user already has a GUID in the mappings, remove it and assert the new one.
+ * @param sessionId the session
+ * @param guid the GUID to associate with the character;
+ * technically, it has already been assigned and should be findable using `{character}.GUID.guid`
+ * @return `true`, if the mapping was created;
+ * `false`, if the session can not be found or if the character's GUID doesn't match the one provided
+ */
+ def Assign(sessionId : Long, guid : Int) : Boolean = Instance.Assign(sessionId, guid)
+
+ /**
+ * Hastily remove all mappings and ids.
+ * @return an unsorted list of the characters that were still online
+ */
+ def Shutdown : List[Player] = Instance.Shutdown
+}
diff --git a/common/src/main/scala/net/psforever/objects/LockerContainer.scala b/common/src/main/scala/net/psforever/objects/LockerContainer.scala
new file mode 100644
index 000000000..0e5357839
--- /dev/null
+++ b/common/src/main/scala/net/psforever/objects/LockerContainer.scala
@@ -0,0 +1,34 @@
+// Copyright (c) 2017 PSForever
+package net.psforever.objects
+
+import net.psforever.objects.definition.EquipmentDefinition
+import net.psforever.objects.definition.converter.LockerContainerConverter
+import net.psforever.objects.equipment.{Equipment, EquipmentSize}
+import net.psforever.objects.inventory.GridInventory
+
+class LockerContainer extends Equipment {
+ private val inventory = GridInventory() //?
+
+ def Inventory : GridInventory = inventory
+
+ def Fit(obj : Equipment) : Option[Int] = inventory.Fit(obj.Definition.Tile)
+
+ def Definition : EquipmentDefinition = new EquipmentDefinition(456) {
+ Name = "locker container"
+ Size = EquipmentSize.Inventory
+ Packet = new LockerContainerConverter()
+ }
+}
+
+object LockerContainer {
+ def apply() : LockerContainer = {
+ new LockerContainer()
+ }
+
+ import net.psforever.packet.game.PlanetSideGUID
+ def apply(guid : PlanetSideGUID) : LockerContainer = {
+ val obj = new LockerContainer()
+ obj.GUID = guid
+ obj
+ }
+}
diff --git a/common/src/main/scala/net/psforever/objects/OffhandEquipmentSlot.scala b/common/src/main/scala/net/psforever/objects/OffhandEquipmentSlot.scala
new file mode 100644
index 000000000..6ac7ebb0a
--- /dev/null
+++ b/common/src/main/scala/net/psforever/objects/OffhandEquipmentSlot.scala
@@ -0,0 +1,20 @@
+// Copyright (c) 2017 PSForever
+package net.psforever.objects
+
+import net.psforever.objects.equipment.EquipmentSize
+
+/**
+ * A size-checked unit of storage (or mounting) for `Equipment`.
+ * Unlike conventional `EquipmentSlot` space, this size of allowable `Equipment` is fixed.
+ * @param size the permanent size of the `Equipment` allowed in this slot
+ */
+class OffhandEquipmentSlot(size : EquipmentSize.Value) extends EquipmentSlot {
+ super.Size_=(size)
+
+ /**
+ * Not allowed to change the slot size manually.
+ * @param assignSize the changed in capacity for this slot
+ * @return the capacity for this slot
+ */
+ override def Size_=(assignSize : EquipmentSize.Value) : EquipmentSize.Value = Size
+}
\ No newline at end of file
diff --git a/common/src/main/scala/net/psforever/objects/PlanetSideGameObject.scala b/common/src/main/scala/net/psforever/objects/PlanetSideGameObject.scala
new file mode 100644
index 000000000..4e781c092
--- /dev/null
+++ b/common/src/main/scala/net/psforever/objects/PlanetSideGameObject.scala
@@ -0,0 +1,47 @@
+// Copyright (c) 2017 PSForever
+package net.psforever.objects
+
+import net.psforever.objects.definition.ObjectDefinition
+import net.psforever.objects.entity.{IdentifiableEntity, SimpleWorldEntity, WorldEntity}
+import net.psforever.types.Vector3
+
+/**
+ * A basic class that indicates an entity that exists somewhere in the world and has a globally unique identifier.
+ */
+abstract class PlanetSideGameObject extends IdentifiableEntity with WorldEntity {
+ private var entity : WorldEntity = new SimpleWorldEntity()
+
+ def Entity : WorldEntity = entity
+
+ def Entity_=(newEntity : WorldEntity) : Unit = {
+ entity = newEntity
+ }
+
+ def Position : Vector3 = Entity.Position
+
+ def Position_=(vec : Vector3) : Vector3 = {
+ Entity.Position = vec
+ }
+
+ def Orientation : Vector3 = Entity.Orientation
+
+ def Orientation_=(vec : Vector3) : Vector3 = {
+ Entity.Orientation = vec
+ }
+
+ def Velocity : Option[Vector3] = Entity.Velocity
+
+ def Velocity_=(vec : Option[Vector3]) : Option[Vector3] = {
+ Entity.Velocity = vec
+ }
+
+ def Definition : ObjectDefinition
+}
+
+object PlanetSideGameObject {
+ def toString(obj : PlanetSideGameObject) : String = {
+ val guid : String = try { obj.GUID.guid.toString } catch { case _ : Exception => "NOGUID" }
+ val P = obj.Position
+ s"[$guid](x,y,z=${P.x%.3f},${P.y%.3f},${P.z%.3f})"
+ }
+}
diff --git a/common/src/main/scala/net/psforever/objects/Player.scala b/common/src/main/scala/net/psforever/objects/Player.scala
new file mode 100644
index 000000000..85e830e75
--- /dev/null
+++ b/common/src/main/scala/net/psforever/objects/Player.scala
@@ -0,0 +1,590 @@
+// Copyright (c) 2017 PSForever
+package net.psforever.objects
+
+import net.psforever.objects.definition.AvatarDefinition
+import net.psforever.objects.equipment.{Equipment, EquipmentSize}
+import net.psforever.objects.inventory.{GridInventory, InventoryItem}
+import net.psforever.packet.game.PlanetSideGUID
+import net.psforever.types._
+
+import scala.annotation.tailrec
+
+class Player(private val name : String,
+ private val faction : PlanetSideEmpire.Value,
+ private val sex : CharacterGender.Value,
+ private val voice : Int,
+ private val head : Int
+ ) extends PlanetSideGameObject {
+ private var alive : Boolean = false
+ private var backpack : Boolean = false
+ private var health : Int = 0
+ private var stamina : Int = 0
+ private var armor : Int = 0
+ private var maxHealth : Int = 100 //TODO affected by empire benefits, territory benefits, and bops
+ private var maxStamina : Int = 100 //does anything affect this?
+
+ private var exosuit : ExoSuitType.Value = ExoSuitType.Standard
+ private val freeHand : EquipmentSlot = new OffhandEquipmentSlot(EquipmentSize.Any)
+ private val holsters : Array[EquipmentSlot] = Array.fill[EquipmentSlot](5)(new EquipmentSlot)
+ private val fifthSlot : EquipmentSlot = new OffhandEquipmentSlot(EquipmentSize.Inventory)
+ private val inventory : GridInventory = GridInventory()
+ private var drawnSlot : Int = Player.HandsDownSlot
+ private var lastDrawnSlot : Int = 0
+
+ private val loadouts : Array[Option[InfantryLoadout]] = Array.fill[Option[InfantryLoadout]](10)(None)
+
+ private val implants : Array[ImplantSlot] = Array.fill[ImplantSlot](3)(new ImplantSlot)
+
+// private var tosRibbon : MeritCommendation.Value = MeritCommendation.None
+// private var upperRibbon : MeritCommendation.Value = MeritCommendation.None
+// private var middleRibbon : MeritCommendation.Value = MeritCommendation.None
+// private var lowerRibbon : MeritCommendation.Value = MeritCommendation.None
+
+ private var facingYawUpper : Float = 0f
+ private var crouching : Boolean = false
+ private var jumping : Boolean = false
+ private var cloaked : Boolean = false
+ private var backpackAccess : Option[PlanetSideGUID] = None
+
+ private var sessionId : Long = 0
+ private var admin : Boolean = false
+ private var spectator : Boolean = false
+
+ private var vehicleSeated : Option[PlanetSideGUID] = None
+ private var vehicleOwned : Option[PlanetSideGUID] = None
+
+ private var continent : String = "home2" //actually, the zoneId
+ private var playerDef : AvatarDefinition = Player.definition
+
+ //SouNourS things
+ /** Last medkituse. */
+ var lastMedkit : Long = 0
+ var death_by : Int = 0
+ var doors : Array[Int] = Array.ofDim(120)
+ var doorsTime : Array[Long] = Array.ofDim(120)
+ var lastSeenStreamMessage : Array[Long] = Array.fill[Long](65535)(0L)
+ var lastShotSeq_time : Int = -1
+ /** The player is shooting. */
+ var shooting : Boolean = false
+ /** From PlanetsideAttributeMessage */
+ var PlanetsideAttribute : Array[Long] = Array.ofDim(120)
+
+ Player.SuitSetup(this, ExoSuit)
+
+ def Name : String = name
+
+ def Faction : PlanetSideEmpire.Value = faction
+
+ def Sex : CharacterGender.Value = sex
+
+ def Voice : Int = voice
+
+ def Head : Int = head
+
+ def isAlive : Boolean = alive
+
+ def isBackpack : Boolean = backpack
+
+ def Spawn : Boolean = {
+ if(!isAlive && !isBackpack) {
+ alive = true
+ Health = MaxHealth
+ Stamina = MaxStamina
+ Armor = MaxArmor
+ ResetAllImplants()
+ }
+ isAlive
+ }
+
+ def Die : Boolean = {
+ alive = false
+ Health = 0
+ Stamina = 0
+ false
+ }
+
+ def Release : Boolean = {
+ if(!isAlive) {
+ backpack = true
+ true
+ }
+ else {
+ false
+ }
+ }
+
+ def Health : Int = health
+
+ def Health_=(assignHealth : Int) : Int = {
+ health = if(isAlive) { math.min(math.max(0, assignHealth), MaxHealth) } else { 0 }
+ Health
+ }
+
+ def MaxHealth : Int = maxHealth
+
+ def MaxHealth_=(max : Int) : Int = {
+ maxHealth = math.min(math.max(0, max), 65535)
+ MaxHealth
+ }
+
+ def Stamina : Int = stamina
+
+ def Stamina_=(assignEnergy : Int) : Int = {
+ stamina = if(isAlive) { math.min(math.max(0, assignEnergy), MaxStamina) } else { 0 }
+ Stamina
+ }
+
+ def MaxStamina : Int = maxStamina
+
+ def MaxStamina_=(max : Int) : Int = {
+ maxStamina = math.min(math.max(0, max), 65535)
+ MaxStamina
+ }
+
+ def Armor : Int = armor
+
+ def Armor_=(assignArmor : Int) : Int = {
+ armor = if(isAlive) { math.min(math.max(0, assignArmor), MaxArmor) } else { 0 }
+ Armor
+ }
+
+ def MaxArmor : Int = ExoSuitDefinition.Select(exosuit).MaxArmor
+
+ def Slot(slot : Int) : EquipmentSlot = {
+ if(inventory.Offset <= slot && slot <= inventory.LastIndex) {
+ inventory.Slot(slot)
+ }
+ else if(slot > -1 && slot < 5) {
+ holsters(slot)
+ }
+ else if(slot == 5) {
+ fifthSlot
+ }
+ else if(slot == Player.FreeHandSlot) {
+ freeHand
+ }
+ else {
+ new OffhandEquipmentSlot(EquipmentSize.Blocked)
+ }
+ }
+
+ def Holsters() : Array[EquipmentSlot] = holsters
+
+ def Inventory : GridInventory = inventory
+
+ def Fit(obj : Equipment) : Option[Int] = {
+ recursiveHolsterFit(holsters.iterator, obj.Size) match {
+ case Some(index) =>
+ Some(index)
+ case None =>
+ inventory.Fit(obj.Definition.Tile) match {
+ case Some(index) =>
+ Some(index)
+ case None =>
+ if(freeHand.Equipment.isDefined) { None } else { Some(Player.FreeHandSlot) }
+ }
+ }
+ }
+
+ @tailrec private def recursiveHolsterFit(iter : Iterator[EquipmentSlot], objSize : EquipmentSize.Value, index : Int = 0) : Option[Int] = {
+ if(!iter.hasNext) {
+ None
+ }
+ else {
+ val slot = iter.next
+ if(slot.Equipment.isEmpty && slot.Size.equals(objSize)) {
+ Some(index)
+ }
+ else {
+ recursiveHolsterFit(iter, objSize, index + 1)
+ }
+ }
+ }
+
+ def Equip(slot : Int, obj : Equipment) : Boolean = {
+ if(-1 < slot && slot < 5) {
+ holsters(slot).Equipment = obj
+ true
+ }
+ else if(slot == Player.FreeHandSlot) {
+ freeHand.Equipment = obj
+ true
+ }
+ else {
+ inventory += slot -> obj
+ }
+ }
+
+ def FreeHand = freeHand
+
+ def FreeHand_=(item : Option[Equipment]) : Option[Equipment] = {
+ if(freeHand.Equipment.isEmpty || item.isEmpty) {
+ freeHand.Equipment = item
+ }
+ FreeHand.Equipment
+ }
+
+ def SaveLoadout(label : String, line : Int) : Unit = {
+ loadouts(line) = Some(new InfantryLoadout(this, label))
+ }
+
+ def LoadLoadout(line : Int) : Option[InfantryLoadout] = loadouts(line)
+
+ def DeleteLoadout(line : Int) : Unit = {
+ loadouts(line) = None
+ }
+
+ def Find(obj : Equipment) : Option[Int] = Find(obj.GUID)
+
+ def Find(guid : PlanetSideGUID) : Option[Int] = {
+ findInHolsters(holsters.iterator, guid) match {
+ case Some(index) =>
+ Some(index)
+ case None =>
+ findInInventory(inventory.Items.values.iterator, guid) match {
+ case Some(index) =>
+ Some(index)
+ case None =>
+ if(freeHand.Equipment.isDefined && freeHand.Equipment.get.GUID == guid) {
+ Some(Player.FreeHandSlot)
+ }
+ else {
+ None
+ }
+ }
+ }
+ }
+
+ @tailrec private def findInHolsters(iter : Iterator[EquipmentSlot], guid : PlanetSideGUID, index : Int = 0) : Option[Int] = {
+ if(!iter.hasNext) {
+ None
+ }
+ else {
+ val slot = iter.next
+ if(slot.Equipment.isDefined && slot.Equipment.get.GUID == guid) {
+ Some(index)
+ }
+ else {
+ findInHolsters(iter, guid, index + 1)
+ }
+ }
+ }
+
+ @tailrec private def findInInventory(iter : Iterator[InventoryItem], guid : PlanetSideGUID) : Option[Int] = {
+ if(!iter.hasNext) {
+ None
+ }
+ else {
+ val item = iter.next
+ if(item.obj.GUID == guid) {
+ Some(item.start)
+ }
+ else {
+ findInInventory(iter, guid)
+ }
+ }
+ }
+
+ def DrawnSlot : Int = drawnSlot
+
+ def DrawnSlot_=(slot : Int = Player.HandsDownSlot) : Int = {
+ if(slot != drawnSlot) {
+ val origDrawnSlot : Int = drawnSlot
+ if(slot == Player.HandsDownSlot) {
+ drawnSlot = slot
+ }
+ else if(-1 < slot && slot < 5 && holsters(slot).Equipment.isDefined) {
+ drawnSlot = slot
+ }
+ lastDrawnSlot = if(-1 < origDrawnSlot && origDrawnSlot < 5) { origDrawnSlot } else { lastDrawnSlot }
+ }
+ DrawnSlot
+ }
+
+ def LastDrawnSlot : Int = lastDrawnSlot
+
+ def ExoSuit : ExoSuitType.Value = exosuit
+
+ def ExoSuit_=(suit : ExoSuitType.Value) : Unit = {
+ exosuit = suit
+ }
+
+ def Implants : Array[ImplantSlot] = implants
+
+ def Implant(slot : Int) : Option[ImplantType.Value] = {
+ if(-1 < slot && slot < implants.length) { implants(slot).Installed } else { None }
+ }
+
+ def Implant(implantType : ImplantType.Value) : Option[Implant] = {
+ implants.find(_.Installed.contains(implantType)) match {
+ case Some(slot) =>
+ slot.Implant
+ case None =>
+ None
+ }
+ }
+
+ def InstallImplant(implant : Implant) : Boolean = {
+ getAvailableImplantSlot(implants.iterator, implant.Definition.Type) match {
+ case Some(slot) =>
+ slot.Implant = implant
+ slot.Implant.get.Reset()
+ true
+ case None =>
+ false
+ }
+ }
+
+ @tailrec private def getAvailableImplantSlot(iter : Iterator[ImplantSlot], implantType : ImplantType.Value) : Option[ImplantSlot] = {
+ if(!iter.hasNext) {
+ None
+ }
+ else {
+ val slot = iter.next
+ if(!slot.Unlocked || slot.Installed.contains(implantType)) {
+ None
+ }
+ else if(slot.Installed.isEmpty) {
+ Some(slot)
+ }
+ else {
+ getAvailableImplantSlot(iter, implantType)
+ }
+ }
+ }
+
+ def UninstallImplant(implantType : ImplantType.Value) : Boolean = {
+ implants.find({slot => slot.Installed.contains(implantType)}) match {
+ case Some(slot) =>
+ slot.Implant = None
+ true
+ case None =>
+ false
+ }
+ }
+
+ def ResetAllImplants() : Unit = {
+ implants.foreach(slot => {
+ slot.Implant match {
+ case Some(implant) =>
+ implant.Reset()
+ case None => ;
+ }
+ })
+ }
+
+ def FacingYawUpper : Float = facingYawUpper
+
+ def FacingYawUpper_=(facing : Float) : Float = {
+ facingYawUpper = facing
+ FacingYawUpper
+ }
+
+ def Crouching : Boolean = crouching
+
+ def Crouching_=(crouched : Boolean) : Boolean = {
+ crouching = crouched
+ Crouching
+ }
+
+ def Jumping : Boolean = jumping
+
+ def Jumping_=(jumped : Boolean) : Boolean = {
+ jumping = jumped
+ Jumping
+ }
+
+ def Cloaked : Boolean = jumping
+
+ def Cloaked_=(isCloaked : Boolean) : Boolean = {
+ cloaked = isCloaked
+ Cloaked
+ }
+
+ def AccessingBackpack : Option[PlanetSideGUID] = backpackAccess
+
+ def AccessingBackpack_=(guid : PlanetSideGUID) : Option[PlanetSideGUID] = {
+ AccessingBackpack = Some(guid)
+ }
+
+ /**
+ * Change which player has access to the backpack of this player.
+ * A player may only access to the backpack of a dead released player, and only if no one else has access at the moment.
+ * @param guid the player who wishes to access the backpack
+ * @return the player who is currently allowed to access the backpack
+ */
+ def AccessingBackpack_=(guid : Option[PlanetSideGUID]) : Option[PlanetSideGUID] = {
+ guid match {
+ case None =>
+ backpackAccess = None
+ case Some(player) =>
+ if(isBackpack && backpackAccess.isEmpty) {
+ backpackAccess = Some(player)
+ }
+ }
+ AccessingBackpack
+ }
+
+ /**
+ * Can the other `player` access the contents of this `Player`'s backpack?
+ * @param player a player attempting to access this backpack
+ * @return `true`, if the `player` is permitted access; `false`, otherwise
+ */
+ def CanAccessBackpack(player : Player) : Boolean = {
+ isBackpack && (backpackAccess.isEmpty || backpackAccess.contains(player.GUID))
+ }
+
+ def SessionId : Long = sessionId
+
+ def Admin : Boolean = admin
+
+ def Spectator : Boolean = spectator
+
+ def Continent : String = continent
+
+ def VehicleSeated : Option[PlanetSideGUID] = vehicleSeated
+
+ def VehicleSeated_=(vehicle : Vehicle) : Option[PlanetSideGUID] = {
+ vehicleSeated = Some(vehicle.GUID)
+ VehicleSeated
+ }
+
+ def VehicleSeated_=(guid : Option[PlanetSideGUID]) : Option[PlanetSideGUID] = {
+ vehicleSeated = guid
+ VehicleSeated
+ }
+
+ def VehicleOwned : Option[PlanetSideGUID] = vehicleOwned
+
+ def VehicleOwned_=(vehicle : Vehicle) : Option[PlanetSideGUID] = {
+ vehicleOwned = Some(vehicle.GUID)
+ VehicleOwned
+ }
+
+ def VehicleOwned_=(guid : Option[PlanetSideGUID]) : Option[PlanetSideGUID] = {
+ vehicleOwned = guid
+ VehicleOwned
+ }
+
+ def Continent_=(zoneId : String) : String = {
+ continent = zoneId
+ Continent
+ }
+
+ def Definition : AvatarDefinition = playerDef
+
+ override def toString : String = {
+ Player.toString(this)
+ }
+
+ def canEqual(other: Any): Boolean = other.isInstanceOf[Player]
+
+ override def equals(other : Any) : Boolean = other match {
+ case that: Player =>
+ (that canEqual this) &&
+ name == that.name &&
+ faction == that.faction &&
+ sex == that.sex &&
+ voice == that.voice &&
+ head == that.head
+ case _ =>
+ false
+ }
+
+ override def hashCode() : Int = {
+ val state = Seq(name, faction, sex, voice, head)
+ state.map(_.hashCode()).foldLeft(0)((a, b) => 31 * a + b)
+ }
+}
+
+object Player {
+ final private val definition : AvatarDefinition = new AvatarDefinition(121)
+ final val FreeHandSlot : Int = 250
+ final val HandsDownSlot : Int = 255
+
+ def apply(name : String, faction : PlanetSideEmpire.Value, sex : CharacterGender.Value, voice : Int, head : Int) : Player = {
+ new Player(name, faction, sex, voice, head)
+ }
+
+ def apply(guid : PlanetSideGUID, name : String, faction : PlanetSideEmpire.Value, sex : CharacterGender.Value, voice : Int, head : Int) : Player = {
+ val obj = new Player(name, faction, sex, voice, head)
+ obj.GUID = guid
+ obj
+ }
+
+ /**
+ * Change the type of `AvatarDefinition` is used to define the player.
+ * @param player the player
+ * @param avatarDef the player's new definition entry
+ * @return the changed player
+ */
+ def apply(player : Player, avatarDef : AvatarDefinition) : Player = {
+ player.playerDef = avatarDef
+ player
+ }
+
+ def apply(player : Player, sessId : Long) : Player = {
+ player.sessionId = sessId
+ player
+ }
+
+ def SuitSetup(player : Player, eSuit : ExoSuitType.Value) : Unit = {
+ val esuitDef : ExoSuitDefinition = ExoSuitDefinition.Select(eSuit)
+ //exosuit
+ player.ExoSuit = eSuit
+ //inventory
+ player.Inventory.Clear()
+ player.Inventory.Resize(esuitDef.InventoryScale.width, esuitDef.InventoryScale.height)
+ player.Inventory.Offset = esuitDef.InventoryOffset
+ //holsters
+ (0 until 5).foreach(index => { player.Slot(index).Size = esuitDef.Holster(index) })
+ }
+
+ def ChangeSessionId(player : Player, session : Long) : Long = {
+ player.sessionId = session
+ player.SessionId
+ }
+
+ def Administrate(player : Player, isAdmin : Boolean) : Player = {
+ player.admin = isAdmin
+ player
+ }
+
+ def Spectate(player : Player, isSpectator : Boolean) : Player = {
+ player.spectator = isSpectator
+ player
+ }
+
+ def Release(player : Player) : Player = {
+ if(player.Release) {
+ val obj = new Player(player.Name, player.Faction, player.Sex, player.Voice, player.Head)
+ obj.VehicleOwned = player.VehicleOwned
+ obj.Continent = player.Continent
+ //hand over loadouts
+ (0 until 10).foreach(index => {
+ obj.loadouts(index) = player.loadouts(index)
+ })
+ //hand over implants
+ (0 until 3).foreach(index => {
+ if(obj.Implants(index).Unlocked = player.Implants(index).Unlocked) {
+ obj.Implants(index).Implant = player.Implants(index).Implant
+ }
+ })
+ //hand over knife
+ obj.Slot(4).Equipment = player.Slot(4).Equipment
+ player.Slot(4).Equipment = None
+ //hand over ???
+ obj.fifthSlot.Equipment = player.fifthSlot.Equipment
+ player.fifthSlot.Equipment = None
+ obj
+ }
+ else {
+ player
+ }
+ }
+
+ def toString(obj : Player) : String = {
+ val name : String = if(obj.VehicleSeated.isDefined) { s"[${obj.name}, ${obj.VehicleSeated.get.guid}]" } else { obj.Name }
+ s"[player $name, ${obj.Faction} (${obj.Health}/${obj.MaxHealth})(${obj.Armor}/${obj.MaxArmor})]"
+ }
+}
diff --git a/common/src/main/scala/net/psforever/objects/SimpleItem.scala b/common/src/main/scala/net/psforever/objects/SimpleItem.scala
new file mode 100644
index 000000000..75158c30b
--- /dev/null
+++ b/common/src/main/scala/net/psforever/objects/SimpleItem.scala
@@ -0,0 +1,22 @@
+// Copyright (c) 2017 PSForever
+package net.psforever.objects
+
+import net.psforever.objects.definition.SimpleItemDefinition
+import net.psforever.objects.equipment.Equipment
+
+class SimpleItem(private val simpDef : SimpleItemDefinition) extends Equipment {
+ def Definition : SimpleItemDefinition = simpDef
+}
+
+object SimpleItem {
+ def apply(simpDef : SimpleItemDefinition) : SimpleItem = {
+ new SimpleItem(simpDef)
+ }
+
+ import net.psforever.packet.game.PlanetSideGUID
+ def apply(guid : PlanetSideGUID, simpDef : SimpleItemDefinition) : SimpleItem = {
+ val obj = new SimpleItem(simpDef)
+ obj.GUID = guid
+ obj
+ }
+}
diff --git a/common/src/main/scala/net/psforever/objects/Tool.scala b/common/src/main/scala/net/psforever/objects/Tool.scala
new file mode 100644
index 000000000..85b2c484c
--- /dev/null
+++ b/common/src/main/scala/net/psforever/objects/Tool.scala
@@ -0,0 +1,166 @@
+// Copyright (c) 2017 PSForever
+package net.psforever.objects
+
+import net.psforever.objects.definition.{AmmoBoxDefinition, ToolDefinition}
+import net.psforever.objects.equipment.{Ammo, Equipment, FireModeDefinition, FireModeSwitch}
+import net.psforever.packet.game.PlanetSideGUID
+
+import scala.annotation.tailrec
+
+/**
+ * A type of utility that can be wielded and loaded with certain other game elements.
+ *
+ * "Tool" is a very mechanical name while this class is intended for various weapons and support items.
+ * The primary trait of a `Tool` is that it has something that counts as an "ammunition,"
+ * depleted as the `Tool` is used, replaceable as long as one has an appropriate type of `AmmoBox` object.
+ * (The former is always called "consuming;" the latter, "reloading.")
+ *
+ * Some weapons Chainblade have ammunition but do not consume it.
+ * @param toolDef the `ObjectDefinition` that constructs this item and maintains some of its immutable fields
+ */
+class Tool(private val toolDef : ToolDefinition) extends Equipment with FireModeSwitch[FireModeDefinition] {
+ private var fireModeIndex : Int = 0
+ private val ammoSlot : List[Tool.FireModeSlot] = Tool.LoadDefinition(this)
+
+ def FireModeIndex : Int = fireModeIndex
+
+ def FireModeIndex_=(index : Int) : Int = {
+ fireModeIndex = index % toolDef.FireModes.length
+ FireModeIndex
+ }
+
+ def FireMode : FireModeDefinition = toolDef.FireModes(fireModeIndex)
+
+ def NextFireMode : FireModeDefinition = {
+ FireModeIndex = FireModeIndex + 1
+ FireMode
+ }
+
+ def AmmoTypeIndex : Int = ammoSlot(fireModeIndex).AmmoTypeIndex
+
+ def AmmoTypeIndex_=(index : Int) : Int = {
+ ammoSlot(fireModeIndex).AmmoTypeIndex = index % FireMode.AmmoTypeIndices.length
+ AmmoTypeIndex
+ }
+
+ def AmmoType : Ammo.Value = toolDef.AmmoTypes(AmmoTypeIndex)
+
+ def NextAmmoType : Ammo.Value = {
+ AmmoTypeIndex = AmmoTypeIndex + 1
+ AmmoType
+ }
+
+ def Magazine : Int = ammoSlot(fireModeIndex).Magazine
+
+ def Magazine_=(mag : Int) : Int = {
+ ammoSlot(fireModeIndex).Magazine = Math.min(Math.max(0, mag), MaxMagazine)
+ Magazine
+ }
+
+ def MaxMagazine : Int = FireMode.Magazine
+
+ def NextDischarge : Int = math.min(Magazine, FireMode.Chamber)
+
+ def AmmoSlots : List[Tool.FireModeSlot] = ammoSlot
+
+ def MaxAmmoSlot : Int = ammoSlot.length
+
+ def Definition : ToolDefinition = toolDef
+
+ override def toString : String = {
+ Tool.toString(this)
+ }
+}
+
+object Tool {
+ def apply(toolDef : ToolDefinition) : Tool = {
+ new Tool(toolDef)
+ }
+
+ def apply(guid : PlanetSideGUID, toolDef : ToolDefinition) : Tool = {
+ val obj = new Tool(toolDef)
+ obj.GUID = guid
+ obj
+ }
+
+ /**
+ * Use the `*Definition` that was provided to this object to initialize its fields and settings.
+ * @param tool the `Tool` being initialized
+ */
+ def LoadDefinition(tool : Tool) : List[FireModeSlot] = {
+ val tdef : ToolDefinition = tool.Definition
+ val maxSlot = tdef.FireModes.maxBy(fmode => fmode.AmmoSlotIndex).AmmoSlotIndex
+ buildFireModes(tool, (0 to maxSlot).iterator, tdef.FireModes.toList)
+ }
+
+ @tailrec private def buildFireModes(tool : Tool, iter : Iterator[Int], fmodes : List[FireModeDefinition], list : List[FireModeSlot] = Nil) : List[FireModeSlot] = {
+ if(!iter.hasNext) {
+ list
+ }
+ else {
+ val index = iter.next
+ fmodes.filter(fmode => fmode.AmmoSlotIndex == index) match {
+ case fmode :: _ =>
+ buildFireModes(tool, iter, fmodes, list :+ new FireModeSlot(tool, fmode))
+ case Nil =>
+ throw new IllegalArgumentException(s"tool ${tool.Definition.Name} ammo slot #$index is missing a fire mode specification; do not skip")
+ }
+ }
+ }
+
+ def toString(obj : Tool) : String = {
+ s"${obj.Definition.Name} (mode=${obj.FireModeIndex}-${obj.AmmoType})(${obj.Magazine}/${obj.MaxMagazine})"
+ }
+
+ /**
+ * A hidden class that manages the specifics of the given ammunition for the current fire mode of this tool.
+ * It operates much closer to an "ammunition feed" rather than a fire mode.
+ * The relationship to fire modes is at least one-to-one and at most one-to-many.
+ */
+ class FireModeSlot(private val tool : Tool, private val fdef : FireModeDefinition) {
+ /*
+ By way of demonstration:
+ Suppressors have one fire mode, two types of ammunition, one slot (2)
+ MA Pistols have two fire modes, one type of ammunition, one slot (1)
+ Jackhammers have two fire modes, two types of ammunition, one slot (2)
+ Punishers have two fire modes, five types of ammunition, two slots (2, 3)
+ */
+
+ /** if this fire mode has multiple types of ammunition */
+ private var ammoTypeIndex : Int = fdef.AmmoTypeIndices.head
+ /** a reference to the actual `AmmoBox` of this slot; will not synch up with `AmmoType` immediately */
+ private var box : AmmoBox = AmmoBox(AmmoBoxDefinition(AmmoType)) //defaults to box of one round of the default type for this slot
+
+ def AmmoTypeIndex : Int = ammoTypeIndex
+
+ def AmmoTypeIndex_=(index : Int) : Int = {
+ ammoTypeIndex = index
+ AmmoTypeIndex
+ }
+
+ def AmmoType : Ammo.Value = tool.Definition.AmmoTypes(ammoTypeIndex)
+
+ def Magazine : Int = box.Capacity
+
+ def Magazine_=(mag : Int) : Int = {
+ box.Capacity = mag
+ Magazine
+ }
+
+ def Box : AmmoBox = box
+
+ def Box_=(toBox : AmmoBox) : Option[AmmoBox] = {
+ if(toBox.AmmoType == AmmoType) {
+ box = toBox
+ Some(Box)
+ }
+ else {
+ None
+ }
+ }
+
+ def Tool : Tool = tool
+
+ def Definition : FireModeDefinition = fdef
+ }
+}
diff --git a/common/src/main/scala/net/psforever/objects/Vehicle.scala b/common/src/main/scala/net/psforever/objects/Vehicle.scala
new file mode 100644
index 000000000..206fc78ff
--- /dev/null
+++ b/common/src/main/scala/net/psforever/objects/Vehicle.scala
@@ -0,0 +1,375 @@
+// Copyright (c) 2017 PSForever
+package net.psforever.objects
+
+import net.psforever.objects.definition.VehicleDefinition
+import net.psforever.objects.equipment.{Equipment, EquipmentSize}
+import net.psforever.objects.inventory.GridInventory
+import net.psforever.objects.vehicles.{Seat, Utility, VehicleLockState}
+import net.psforever.packet.game.PlanetSideGUID
+import net.psforever.packet.game.objectcreate.DriveState
+import net.psforever.types.PlanetSideEmpire
+
+import scala.collection.mutable
+
+/**
+ * The server-side support object that represents a vehicle.
+ *
+ * All infantry seating, all mounted weapons, and the trunk space are considered part of the same index hierarchy.
+ * Generally, all seating is declared first - the driver and passengers and and gunners.
+ * Following that are the mounted weapons and other utilities.
+ * Trunk space starts being indexed afterwards.
+ * The first seat is always the op;erator (driver/pilot).
+ * "Passengers" are seats that are not the operator and are not in control of a mounted weapon.
+ * "Gunners" are seats that are not the operator and ARE in control of a mounted weapon.
+ * (The operator can be in control of a weapon - that is the whole point of a turret.)
+ *
+ * Having said all that, to keep it simple, infantry seating, mounted weapons, and utilities are stored in separate `Map`s.
+ * @param vehicleDef the vehicle's definition entry'
+ * stores and unloads pertinent information about the `Vehicle`'s configuration;
+ * used in the initialization process (`loadVehicleDefinition`)
+ */
+class Vehicle(private val vehicleDef : VehicleDefinition) extends PlanetSideGameObject {
+ private var faction : PlanetSideEmpire.Value = PlanetSideEmpire.TR
+ private var owner : Option[PlanetSideGUID] = None
+ private var health : Int = 1
+ private var shields : Int = 0
+ private var deployed : DriveState.Value = DriveState.Mobile
+ private var decal : Int = 0
+ private var trunkLockState : VehicleLockState.Value = VehicleLockState.Locked
+ private var trunkAccess : Option[PlanetSideGUID] = None
+
+ private val seats : mutable.HashMap[Int, Seat] = mutable.HashMap()
+ private val weapons : mutable.HashMap[Int, EquipmentSlot] = mutable.HashMap()
+ private val utilities : mutable.ArrayBuffer[Utility] = mutable.ArrayBuffer()
+ private val trunk : GridInventory = GridInventory()
+
+ //init
+ LoadDefinition()
+
+ /**
+ * Override this method to perform any special setup that is not standardized to `*Definition`.
+ * @see `Vehicle.LoadDefinition`
+ */
+ protected def LoadDefinition() : Unit = {
+ Vehicle.LoadDefinition(this)
+ }
+
+ def Faction : PlanetSideEmpire.Value = {
+ this.faction
+ }
+
+ def Faction_=(faction : PlanetSideEmpire.Value) : PlanetSideEmpire.Value = {
+ this.faction = faction
+ faction
+ }
+
+ def Owner : Option[PlanetSideGUID] = {
+ this.owner
+ }
+
+ def Owner_=(owner : Option[PlanetSideGUID]) : Option[PlanetSideGUID] = {
+ this.owner = owner
+ owner
+ }
+
+ def Health : Int = {
+ this.health
+ }
+
+ def Health_=(health : Int) : Int = {
+ this.health = health
+ health
+ }
+
+ def MaxHealth : Int = {
+ this.vehicleDef.MaxHealth
+ }
+
+ def Shields : Int = {
+ this.shields
+ }
+
+ def Shields_=(strength : Int) : Int = {
+ this.shields = strength
+ strength
+ }
+
+ def MaxShields : Int = {
+ vehicleDef.MaxShields
+ }
+
+ def Configuration : DriveState.Value = {
+ this.deployed
+ }
+
+ def Configuration_=(deploy : DriveState.Value) : DriveState.Value = {
+ if(vehicleDef.Deployment) {
+ this.deployed = deploy
+ }
+ Configuration
+ }
+
+ def Decal : Int = {
+ this.decal
+ }
+
+ def Decal_=(decal : Int) : Int = {
+ this.decal = decal
+ decal
+ }
+
+ /**
+ * Given the index of an entry mounting point, return the infantry-accessible `Seat` associated with it.
+ * @param mountPoint an index representing the seat position / mounting point
+ * @return a seat number, or `None`
+ */
+ def GetSeatFromMountPoint(mountPoint : Int) : Option[Int] = {
+ vehicleDef.MountPoints.get(mountPoint)
+ }
+
+ /**
+ * Get the seat at the index.
+ * The specified "seat" can only accommodate a player as opposed to weapon mounts which share the same indexing system.
+ * @param seatNumber an index representing the seat position / mounting point
+ * @return a `Seat`, or `None`
+ */
+ def Seat(seatNumber : Int) : Option[Seat] = {
+ if(seatNumber >= 0 && seatNumber < this.seats.size) {
+ this.seats.get(seatNumber)
+ }
+ else {
+ None
+ }
+ }
+
+ def Seats : List[Seat] = {
+ seats.values.toList
+ }
+
+ def Weapons : mutable.HashMap[Int, EquipmentSlot] = weapons
+
+ /**
+ * Get the weapon at the index.
+ * @param wepNumber an index representing the seat position / mounting point
+ * @return a weapon, or `None`
+ */
+ def ControlledWeapon(wepNumber : Int) : Option[Equipment] = {
+ val slot = this.weapons.get(wepNumber)
+ if(slot.isDefined) {
+ slot.get.Equipment
+ }
+ else {
+ None
+ }
+ }
+
+ /**
+ * Given a player who may be a passenger, retrieve an index where this player is seated.
+ * @param player the player
+ * @return a seat by index, or `None` if the `player` is not actually seated in this `Vehicle`
+ */
+ def PassengerInSeat(player : Player) : Option[Int] = {
+ var outSeat : Option[Int] = None
+ val GUID = player.GUID
+ for((seatNumber, seat) <- this.seats) {
+ val occupant : Option[PlanetSideGUID] = seat.Occupant
+ if(occupant.isDefined && occupant.get == GUID) {
+ outSeat = Some(seatNumber)
+ }
+ }
+ outSeat
+ }
+
+ /**
+ * Given a valid seat number, retrieve an index where a weapon controlled from this seat is attached.
+ * @param seatNumber the seat number
+ * @return a mounted weapon by index, or `None` if either the seat doesn't exist or there is no controlled weapon
+ */
+ def WeaponControlledFromSeat(seatNumber : Int) : Option[Tool] = {
+ Seat(seatNumber) match {
+ case Some(seat) =>
+ wepFromSeat(seat)
+ case None =>
+ None
+ }
+ }
+
+ private def wepFromSeat(seat : Seat) : Option[Tool] = {
+ seat.ControlledWeapon match {
+ case Some(index) =>
+ wepFromSeat(index)
+ case None =>
+ None
+ }
+ }
+
+ private def wepFromSeat(wepIndex : Int) : Option[Tool] = {
+ weapons.get(wepIndex) match {
+ case Some(wep) =>
+ wep.Equipment.asInstanceOf[Option[Tool]]
+ case None =>
+ None
+ }
+ }
+
+ def Utilities : mutable.ArrayBuffer[Utility] = utilities
+
+ /**
+ * Get a referenece ot a certain `Utility` attached to this `Vehicle`.
+ * @param utilNumber the attachment number of the `Utility`
+ * @return the `Utility` or `None` (if invalid)
+ */
+ def Utility(utilNumber : Int) : Option[Utility] = {
+ if(utilNumber >= 0 && utilNumber < this.utilities.size) {
+ Some(this.utilities(utilNumber))
+ }
+ else {
+ None
+ }
+ }
+
+ /**
+ * A reference to the `Vehicle` `Trunk` space.
+ * @return this `Vehicle` `Trunk`
+ */
+ def Trunk : GridInventory = {
+ this.trunk
+ }
+
+ def AccessingTrunk : Option[PlanetSideGUID] = trunkAccess
+
+ def AccessingTrunk_=(guid : PlanetSideGUID) : Option[PlanetSideGUID] = {
+ AccessingTrunk = Some(guid)
+ }
+
+ /**
+ * Change which player has access to the trunk of this vehicle.
+ * A player may only gain access to the trunk if no one else has access to the trunk at the moment.
+ * @param guid the player who wishes to access the trunk
+ * @return the player who is currently allowed to access the trunk
+ */
+ def AccessingTrunk_=(guid : Option[PlanetSideGUID]) : Option[PlanetSideGUID] = {
+ guid match {
+ case None =>
+ trunkAccess = None
+ case Some(player) =>
+ if(trunkAccess.isEmpty) {
+ trunkAccess = Some(player)
+ }
+ }
+ AccessingTrunk
+ }
+
+ /**
+ * Can this `player` access the contents of this `Vehicle`'s `Trunk` given its current access permissions?
+ * @param player a player attempting to access this `Trunk`
+ * @return `true`, if the `player` is permitted access; `false`, otherwise
+ */
+ def CanAccessTrunk(player : Player) : Boolean = {
+ if(trunkAccess.isEmpty || trunkAccess.contains(player.GUID)) {
+ trunkLockState match {
+ case VehicleLockState.Locked => //only the owner
+ owner.isEmpty || (owner.isDefined && player.GUID == owner.get)
+ case VehicleLockState.Group => //anyone in the owner's squad or platoon
+ faction == player.Faction //TODO this is not correct
+ case VehicleLockState.Empire => //anyone of the owner's faction
+ faction == player.Faction
+ }
+ }
+ else {
+ false
+ }
+ }
+
+ /**
+ * Check access to the `Trunk`.
+ * @return the current access value for the `Vehicle` `Trunk`
+ */
+ def TrunkLockState : VehicleLockState.Value = {
+ this.trunkLockState
+ }
+
+ /**
+ * Change the access value for the trunk.
+ * @param lockState the new access value for the `Vehicle` `Trunk`
+ * @return the current access value for the `Vehicle` `Trunk` after the change
+ */
+ def TrunkLockState_=(lockState : VehicleLockState.Value) : VehicleLockState.Value = {
+ this.trunkLockState = lockState
+ lockState
+ }
+
+ /**
+ * This is the definition entry that is used to store and unload pertinent information about the `Vehicle`.
+ * @return the vehicle's definition entry
+ */
+ def Definition : VehicleDefinition = vehicleDef
+
+ /**
+ * Override the string representation to provide additional information.
+ * @return the string output
+ */
+ override def toString : String = {
+ Vehicle.toString(this)
+ }
+}
+
+object Vehicle {
+ /**
+ * Overloaded constructor.
+ * @param vehicleDef the vehicle's definition entry
+ * @return a `Vwehicle` object
+ */
+ def apply(vehicleDef : VehicleDefinition) : Vehicle = {
+ new Vehicle(vehicleDef)
+ }
+ /**
+ * Overloaded constructor.
+ * @param vehicleDef the vehicle's definition entry
+ * @return a `Vwehicle` object
+ */
+ def apply(guid : PlanetSideGUID, vehicleDef : VehicleDefinition) : Vehicle = {
+ val obj = new Vehicle(vehicleDef)
+ obj.GUID = guid
+ obj
+ }
+
+ /**
+ * Use the `*Definition` that was provided to this object to initialize its fields and settings.
+ * @param vehicle the `Vehicle` being initialized
+ * @see `{object}.LoadDefinition`
+ */
+ def LoadDefinition(vehicle : Vehicle) : Vehicle = {
+ val vdef : VehicleDefinition = vehicle.Definition
+ //general stuff
+ vehicle.Health = vdef.MaxHealth
+ //create weapons
+ for((num, definition) <- vdef.Weapons) {
+ val slot = EquipmentSlot(EquipmentSize.VehicleWeapon)
+ slot.Equipment = Tool(definition)
+ vehicle.weapons += num -> slot
+ vehicle
+ }
+ //create seats
+ for((num, seatDef) <- vdef.Seats) {
+ vehicle.seats += num -> Seat(seatDef, vehicle)
+ }
+ for(i <- vdef.Utilities) {
+ //TODO utilies must be loaded and wired on a case-by-case basis?
+ vehicle.Utilities += Utility.Select(i, vehicle)
+ }
+ //trunk
+ vehicle.trunk.Resize(vdef.TrunkSize.width, vdef.TrunkSize.height)
+ vehicle.trunk.Offset = vdef.TrunkOffset
+ vehicle
+ }
+
+ /**
+ * Provide a fixed string representation.
+ * @return the string output
+ */
+ def toString(obj : Vehicle) : String = {
+ val occupancy = obj.Seats.count(seat => seat.isOccupied)
+ s"${obj.Definition.Name}, owned by ${obj.Owner}: (${obj.Health}/${obj.MaxHealth})(${obj.Shields}/${obj.MaxShields}) ($occupancy)"
+ }
+}
diff --git a/common/src/main/scala/net/psforever/objects/definition/AmmoBoxDefinition.scala b/common/src/main/scala/net/psforever/objects/definition/AmmoBoxDefinition.scala
new file mode 100644
index 000000000..9598245fb
--- /dev/null
+++ b/common/src/main/scala/net/psforever/objects/definition/AmmoBoxDefinition.scala
@@ -0,0 +1,33 @@
+// Copyright (c) 2017 PSForever
+package net.psforever.objects.definition
+
+import net.psforever.objects.definition.converter.AmmoBoxConverter
+import net.psforever.objects.equipment.Ammo
+
+class AmmoBoxDefinition(objectId : Int) extends EquipmentDefinition(objectId) {
+ import net.psforever.objects.equipment.EquipmentSize
+ private val ammoType : Ammo.Value = Ammo(objectId) //let throw NoSuchElementException
+ private var capacity : Int = 1
+ Name = "ammo box"
+ Size = EquipmentSize.Inventory
+ Packet = new AmmoBoxConverter()
+
+ def AmmoType : Ammo.Value = ammoType
+
+ def Capacity : Int = capacity
+
+ def Capacity_=(capacity : Int) : Int = {
+ this.capacity = capacity
+ Capacity
+ }
+}
+
+object AmmoBoxDefinition {
+ def apply(objectId: Int) : AmmoBoxDefinition = {
+ new AmmoBoxDefinition(objectId)
+ }
+
+ def apply(ammoType : Ammo.Value) : AmmoBoxDefinition = {
+ new AmmoBoxDefinition(ammoType.id)
+ }
+}
diff --git a/common/src/main/scala/net/psforever/objects/definition/AvatarDefinition.scala b/common/src/main/scala/net/psforever/objects/definition/AvatarDefinition.scala
new file mode 100644
index 000000000..0214b3087
--- /dev/null
+++ b/common/src/main/scala/net/psforever/objects/definition/AvatarDefinition.scala
@@ -0,0 +1,24 @@
+// Copyright (c) 2017 PSForever
+package net.psforever.objects.definition
+
+import net.psforever.objects.definition.converter.AvatarConverter
+import net.psforever.objects.Avatars
+
+/**
+ * The definition for game objects that look like other people, and also for players.
+ * @param objectId the object's identifier number
+ */
+class AvatarDefinition(objectId : Int) extends ObjectDefinition(objectId) {
+ Avatars(objectId) //let throw NoSuchElementException
+ Packet = new AvatarConverter()
+}
+
+object AvatarDefinition {
+ def apply(objectId: Int) : AvatarDefinition = {
+ new AvatarDefinition(objectId)
+ }
+
+ def apply(avatar : Avatars.Value) : AvatarDefinition = {
+ new AvatarDefinition(avatar.id)
+ }
+}
diff --git a/common/src/main/scala/net/psforever/objects/definition/BasicDefinition.scala b/common/src/main/scala/net/psforever/objects/definition/BasicDefinition.scala
new file mode 100644
index 000000000..f7a7b61bc
--- /dev/null
+++ b/common/src/main/scala/net/psforever/objects/definition/BasicDefinition.scala
@@ -0,0 +1,13 @@
+// Copyright (c) 2017 PSForever
+package net.psforever.objects.definition
+
+abstract class BasicDefinition {
+ private var name : String = "definition"
+
+ def Name : String = name
+
+ def Name_=(name : String) : String = {
+ this.name = name
+ Name
+ }
+}
diff --git a/common/src/main/scala/net/psforever/objects/definition/ConstructionItemDefinition.scala b/common/src/main/scala/net/psforever/objects/definition/ConstructionItemDefinition.scala
new file mode 100644
index 000000000..4a3eb9bfb
--- /dev/null
+++ b/common/src/main/scala/net/psforever/objects/definition/ConstructionItemDefinition.scala
@@ -0,0 +1,23 @@
+// Copyright (c) 2017 PSForever
+package net.psforever.objects.definition
+
+import net.psforever.objects.equipment.CItem
+
+import scala.collection.mutable.ListBuffer
+
+class ConstructionItemDefinition(objectId : Int) extends EquipmentDefinition(objectId) {
+ CItem.Unit(objectId) //let throw NoSuchElementException
+ private val modes : ListBuffer[CItem.DeployedItem.Value] = ListBuffer()
+
+ def Modes : ListBuffer[CItem.DeployedItem.Value] = modes
+}
+
+object ConstructionItemDefinition {
+ def apply(objectId : Int) : ConstructionItemDefinition = {
+ new ConstructionItemDefinition(objectId)
+ }
+
+ def apply(cItem : CItem.Unit.Value) : ConstructionItemDefinition = {
+ new ConstructionItemDefinition(cItem.id)
+ }
+}
diff --git a/common/src/main/scala/net/psforever/objects/definition/EquipmentDefinition.scala b/common/src/main/scala/net/psforever/objects/definition/EquipmentDefinition.scala
new file mode 100644
index 000000000..d74e5fdb3
--- /dev/null
+++ b/common/src/main/scala/net/psforever/objects/definition/EquipmentDefinition.scala
@@ -0,0 +1,39 @@
+// Copyright (c) 2017 PSForever
+package net.psforever.objects.definition
+
+import net.psforever.objects.equipment.EquipmentSize
+import net.psforever.objects.inventory.InventoryTile
+
+/**
+ * The definition for any piece of `Equipment`.
+ * @param objectId the object's identifier number
+ */
+abstract class EquipmentDefinition(objectId : Int) extends ObjectDefinition(objectId) {
+ /** the size of the item when placed in an EquipmentSlot / holster / mounting */
+ private var size : EquipmentSize.Value = EquipmentSize.Blocked
+ /** the size of the item when placed in the grid inventory space */
+ private var tile : InventoryTile = InventoryTile.Tile11
+ /** a correction for the z-coordinate for some dropped items to avoid sinking into the ground */
+ private var dropOffset : Float = 0f
+
+ def Size : EquipmentSize.Value = size
+
+ def Size_=(newSize : EquipmentSize.Value) : EquipmentSize.Value = {
+ size = newSize
+ Size
+ }
+
+ def Tile : InventoryTile = tile
+
+ def Tile_=(newTile : InventoryTile) : InventoryTile = {
+ tile = newTile
+ Tile
+ }
+
+ def DropOffset : Float = dropOffset
+
+ def DropOffset(offset : Float) : Float = {
+ dropOffset = offset
+ DropOffset
+ }
+}
diff --git a/common/src/main/scala/net/psforever/objects/definition/ImplantDefinition.scala b/common/src/main/scala/net/psforever/objects/definition/ImplantDefinition.scala
new file mode 100644
index 000000000..1825b58e8
--- /dev/null
+++ b/common/src/main/scala/net/psforever/objects/definition/ImplantDefinition.scala
@@ -0,0 +1,93 @@
+// Copyright (c) 2017 PSForever
+package net.psforever.objects.definition
+
+import net.psforever.types.{ExoSuitType, ImplantType}
+
+import scala.collection.mutable
+
+/**
+ * An `Enumeration` of a variety of poses or generalized movement.
+ */
+object Stance extends Enumeration {
+ val Crouching,
+ Standing,
+ Walking, //not used, but should still be defined
+ Running = Value
+}
+
+/**
+ * The definition for an installable player utility that grants a perk, usually in exchange for stamina (energy).
+ *
+ * Most of the definition deals with the costs of activation and operation.
+ * When activated by the user, an `activationCharge` may be deducted form that user's stamina reserves.
+ * This does not necessarily have to be a non-zero value.
+ * Passive implants are always active and thus have no cost.
+ * After being activated, a non-passive implant consumes a specific amount of stamina each second.
+ * This cost is modified by how the user is standing and what type of exo-suit they are wearing.
+ * The `durationChargeBase` is the lowest cost for an implant.
+ * Modifiers for exo-suit type and stance type are then added onto this base cost.
+ * For example: wearing `Reinforced` costs 2 stamina but costs only 1 stamina in all other cases.
+ * Assuming that is the only cost, the definition would have a base charge of 1 and a `Reinforced` modifier of 1.
+ * @param implantType the type of implant that is defined
+ * @see `ImplantType`
+ */
+class ImplantDefinition(private val implantType : Int) extends BasicDefinition {
+ ImplantType(implantType)
+ /** how long it takes the implant to spin-up; is milliseconds */
+ private var initialization : Long = 0L
+ /** a passive certification is activated as soon as it is ready (or other condition) */
+ private var passive : Boolean = false
+ /** how much turning on the implant costs */
+ private var activationCharge : Int = 0
+ /** how much energy does this implant cost to remain active per second*/
+ private var durationChargeBase : Int = 0
+ /** how much more energy does the implant cost for this exo-suit */
+ private val durationChargeByExoSuit = mutable.HashMap[ExoSuitType.Value, Int]().withDefaultValue(0)
+ /** how much more energy does the implant cost for this stance */
+ private val durationChargeByStance = mutable.HashMap[Stance.Value, Int]().withDefaultValue(0)
+ Name = "implant"
+
+ def Initialization : Long = initialization
+
+ def Initialization_=(time : Long) : Long = {
+ initialization = math.max(0, time)
+ Initialization
+ }
+
+ def Passive : Boolean = passive
+
+ def Passive_=(isPassive : Boolean) : Boolean = {
+ passive = isPassive
+ Passive
+ }
+
+ def ActivationCharge : Int = activationCharge
+
+ def ActivationCharge_=(charge : Int) : Int = {
+ activationCharge = math.max(0, charge)
+ ActivationCharge
+ }
+
+ def DurationChargeBase : Int = durationChargeBase
+
+ def DurationChargeBase_=(charge : Int) : Int = {
+ durationChargeBase = math.max(0, charge)
+ DurationChargeBase
+ }
+
+ def DurationChargeByExoSuit : mutable.Map[ExoSuitType.Value, Int] = durationChargeByExoSuit
+
+ def DurationChargeByStance : mutable.Map[Stance.Value, Int] = durationChargeByStance
+
+ def Type : ImplantType.Value = ImplantType(implantType)
+}
+
+object ImplantDefinition {
+ def apply(implantType : Int) : ImplantDefinition = {
+ new ImplantDefinition(implantType)
+ }
+
+ def apply(implantType : ImplantType.Value) : ImplantDefinition = {
+ new ImplantDefinition(implantType.id)
+ }
+}
diff --git a/common/src/main/scala/net/psforever/objects/definition/KitDefinition.scala b/common/src/main/scala/net/psforever/objects/definition/KitDefinition.scala
new file mode 100644
index 000000000..f9d7ceae1
--- /dev/null
+++ b/common/src/main/scala/net/psforever/objects/definition/KitDefinition.scala
@@ -0,0 +1,29 @@
+// Copyright (c) 2017 PSForever
+package net.psforever.objects.definition
+
+import net.psforever.objects.definition.converter.KitConverter
+import net.psforever.objects.equipment.Kits
+
+/**
+ * The definition for a personal one-time-use recovery item.
+ * @param objectId the object's identifier number
+ */
+class KitDefinition(objectId : Int) extends EquipmentDefinition(objectId) {
+ import net.psforever.objects.equipment.EquipmentSize
+ import net.psforever.objects.inventory.InventoryTile
+ Kits(objectId) //let throw NoSuchElementException
+ Size = EquipmentSize.Inventory
+ Tile = InventoryTile.Tile42
+ Name = "kit"
+ Packet = new KitConverter()
+}
+
+object KitDefinition {
+ def apply(objectId: Int) : KitDefinition = {
+ new KitDefinition(objectId)
+ }
+
+ def apply(kit : Kits.Value) : KitDefinition = {
+ new KitDefinition(kit.id)
+ }
+}
diff --git a/common/src/main/scala/net/psforever/objects/definition/ObjectDefinition.scala b/common/src/main/scala/net/psforever/objects/definition/ObjectDefinition.scala
new file mode 100644
index 000000000..d1c0decca
--- /dev/null
+++ b/common/src/main/scala/net/psforever/objects/definition/ObjectDefinition.scala
@@ -0,0 +1,42 @@
+// Copyright (c) 2017 PSForever
+package net.psforever.objects.definition
+
+import net.psforever.objects.PlanetSideGameObject
+import net.psforever.objects.definition.converter.{ObjectCreateConverter, PacketConverter}
+
+/**
+ * Associate an object's canned in-game representation with its basic game identification unit.
+ * The extension of this `class` would identify the common data necessary to construct such a given game object.
+ *
+ * The converter transforms a game object that is created by this `ObjectDefinition` into packet data through method-calls.
+ * The field for this converter is a `PacketConverter`, the superclass for `ObjectCreateConverter`;
+ * the type of the mutator's parameter is `ObjectCreateConverter` of a wildcard `tparam`;
+ * and, the accessor return type is `ObjectCreateConverter[PlanetSideGameObject]`, a minimum-true statement.
+ * The actual type of the converter at a given point, casted or otherwise, is mostly meaningless.
+ * Casting the external object does not mutate any of the types used by the methods within that object.
+ * So long as it is an `ObjectCreatePacket`, those methods can be called correctly for a game object of the desired type.
+ * @param objectId the object's identifier number
+ */
+abstract class ObjectDefinition(private val objectId : Int) extends BasicDefinition {
+ /** a data converter for this type of object */
+ protected var packet : PacketConverter = new ObjectCreateConverter[PlanetSideGameObject]() { }
+ Name = "object definition"
+
+ /**
+ * Get the conversion object.
+ * @return
+ */
+ final def Packet : ObjectCreateConverter[PlanetSideGameObject] = packet.asInstanceOf[ObjectCreateConverter[PlanetSideGameObject]]
+
+ /**
+ * Assign this definition a conversion object.
+ * @param pkt the new converter
+ * @return the current converter, after assignment
+ */
+ final def Packet_=(pkt : ObjectCreateConverter[_]) : PacketConverter = {
+ packet = pkt
+ Packet
+ }
+
+ def ObjectId : Int = objectId
+}
diff --git a/common/src/main/scala/net/psforever/objects/definition/SeatDefinition.scala b/common/src/main/scala/net/psforever/objects/definition/SeatDefinition.scala
new file mode 100644
index 000000000..5535274e2
--- /dev/null
+++ b/common/src/main/scala/net/psforever/objects/definition/SeatDefinition.scala
@@ -0,0 +1,44 @@
+// Copyright (c) 2017 PSForever
+package net.psforever.objects.definition
+
+import net.psforever.objects.vehicles.SeatArmorRestriction
+
+/**
+ * The definition for a seat.
+ */
+class SeatDefinition extends BasicDefinition {
+ /** a restriction on the type of exo-suit a person can wear */
+ private var armorRestriction : SeatArmorRestriction.Value = SeatArmorRestriction.NoMax
+ /** the user can escape while the vehicle is moving */
+ private var bailable : Boolean = false
+ /** any controlled weapon */
+ private var weaponMount : Option[Int] = None
+ Name = "seat"
+
+ def ArmorRestriction : SeatArmorRestriction.Value = {
+ this.armorRestriction
+ }
+
+ def ArmorRestriction_=(restriction : SeatArmorRestriction.Value) : SeatArmorRestriction.Value = {
+ this.armorRestriction = restriction
+ restriction
+ }
+
+ def Bailable : Boolean = {
+ this.bailable
+ }
+
+ def Bailable_=(canBail : Boolean) : Boolean = {
+ this.bailable = canBail
+ canBail
+ }
+
+ def ControlledWeapon : Option[Int] = {
+ this.weaponMount
+ }
+
+ def ControlledWeapon_=(seat : Option[Int]) : Option[Int] = {
+ this.weaponMount = seat
+ ControlledWeapon
+ }
+}
diff --git a/common/src/main/scala/net/psforever/objects/definition/SimpleItemDefinition.scala b/common/src/main/scala/net/psforever/objects/definition/SimpleItemDefinition.scala
new file mode 100644
index 000000000..b122a0d7a
--- /dev/null
+++ b/common/src/main/scala/net/psforever/objects/definition/SimpleItemDefinition.scala
@@ -0,0 +1,21 @@
+// Copyright (c) 2017 PSForever
+package net.psforever.objects.definition
+
+import net.psforever.objects.equipment.SItem
+
+class SimpleItemDefinition(objectId : Int) extends EquipmentDefinition(objectId) {
+ import net.psforever.objects.equipment.EquipmentSize
+ SItem(objectId) //let throw NoSuchElementException
+ Name = "tool"
+ Size = EquipmentSize.Pistol //all items
+}
+
+object SimpleItemDefinition {
+ def apply(objectId : Int) : SimpleItemDefinition = {
+ new SimpleItemDefinition(objectId)
+ }
+
+ def apply(simpItem : SItem.Value) : SimpleItemDefinition = {
+ new SimpleItemDefinition(simpItem.id)
+ }
+}
diff --git a/common/src/main/scala/net/psforever/objects/definition/ToolDefinition.scala b/common/src/main/scala/net/psforever/objects/definition/ToolDefinition.scala
new file mode 100644
index 000000000..2dbb7303e
--- /dev/null
+++ b/common/src/main/scala/net/psforever/objects/definition/ToolDefinition.scala
@@ -0,0 +1,24 @@
+// Copyright (c) 2017 PSForever
+package net.psforever.objects.definition
+
+import net.psforever.objects.definition.converter.ToolConverter
+import net.psforever.objects.equipment.{Ammo, FireModeDefinition}
+
+import scala.collection.mutable
+
+class ToolDefinition(objectId : Int) extends EquipmentDefinition(objectId) {
+ private val ammoTypes : mutable.ListBuffer[Ammo.Value] = new mutable.ListBuffer[Ammo.Value]
+ private val fireModes : mutable.ListBuffer[FireModeDefinition] = new mutable.ListBuffer[FireModeDefinition]
+ Name = "tool"
+ Packet = new ToolConverter()
+
+ def AmmoTypes : mutable.ListBuffer[Ammo.Value] = ammoTypes
+
+ def FireModes : mutable.ListBuffer[FireModeDefinition] = fireModes
+}
+
+object ToolDefinition {
+ def apply(objectId : Int) : ToolDefinition = {
+ new ToolDefinition(objectId)
+ }
+}
diff --git a/common/src/main/scala/net/psforever/objects/definition/VehicleDefinition.scala b/common/src/main/scala/net/psforever/objects/definition/VehicleDefinition.scala
new file mode 100644
index 000000000..d7c67fe6c
--- /dev/null
+++ b/common/src/main/scala/net/psforever/objects/definition/VehicleDefinition.scala
@@ -0,0 +1,77 @@
+// Copyright (c) 2017 PSForever
+package net.psforever.objects.definition
+
+import net.psforever.objects.definition.converter.VehicleConverter
+import net.psforever.objects.inventory.InventoryTile
+
+import scala.collection.mutable
+
+/**
+ * An object definition system used to construct and retain the parameters of various vehicles.
+ * @param objectId the object id the is associated with this sort of `Vehicle`
+ */
+class VehicleDefinition(objectId : Int) extends ObjectDefinition(objectId) {
+ private var maxHealth : Int = 100
+ private var maxShields : Int = 0
+ /* key - seat index, value - seat object */
+ private val seats : mutable.HashMap[Int, SeatDefinition] = mutable.HashMap[Int, SeatDefinition]()
+ /* key - entry point index, value - seat index */
+ private val mountPoints : mutable.HashMap[Int, Int] = mutable.HashMap()
+ /* key - seat index (where this weapon attaches during object construction), value - the weapon on an EquipmentSlot */
+ private val weapons : mutable.HashMap[Int, ToolDefinition] = mutable.HashMap[Int, ToolDefinition]()
+ private var deployment : Boolean = false
+ private val utilities : mutable.ArrayBuffer[Int] = mutable.ArrayBuffer[Int]()
+ private var trunkSize : InventoryTile = InventoryTile.None
+ private var trunkOffset: Int = 0
+ Name = "vehicle"
+ Packet = new VehicleConverter
+
+ def MaxHealth : Int = maxHealth
+
+ def MaxHealth_=(health : Int) : Int = {
+ maxHealth = health
+ MaxHealth
+ }
+
+ def MaxShields : Int = maxShields
+
+ def MaxShields_=(shields : Int) : Int = {
+ maxShields = shields
+ MaxShields
+ }
+
+ def Seats : mutable.HashMap[Int, SeatDefinition] = seats
+
+ def MountPoints : mutable.HashMap[Int, Int] = mountPoints
+
+ def Weapons : mutable.HashMap[Int, ToolDefinition] = weapons
+
+ def Deployment : Boolean = deployment
+
+ def Deployment_=(deployable : Boolean) : Boolean = {
+ deployment = deployable
+ Deployment
+ }
+
+ def Utilities : mutable.ArrayBuffer[Int] = utilities
+
+ def TrunkSize : InventoryTile = trunkSize
+
+ def TrunkSize_=(tile : InventoryTile) : InventoryTile = {
+ trunkSize = tile
+ TrunkSize
+ }
+
+ def TrunkOffset : Int = trunkOffset
+
+ def TrunkOffset_=(offset : Int) : Int = {
+ trunkOffset = offset
+ TrunkOffset
+ }
+}
+
+object VehicleDefinition {
+ def apply(objectId: Int) : VehicleDefinition = {
+ new VehicleDefinition(objectId)
+ }
+}
diff --git a/common/src/main/scala/net/psforever/objects/definition/converter/ACEConverter.scala b/common/src/main/scala/net/psforever/objects/definition/converter/ACEConverter.scala
new file mode 100644
index 000000000..d45acda1f
--- /dev/null
+++ b/common/src/main/scala/net/psforever/objects/definition/converter/ACEConverter.scala
@@ -0,0 +1,17 @@
+// Copyright (c) 2017 PSForever
+package net.psforever.objects.definition.converter
+
+import net.psforever.objects.ConstructionItem
+import net.psforever.packet.game.objectcreate.{ACEData, DetailedACEData}
+
+import scala.util.{Success, Try}
+
+class ACEConverter extends ObjectCreateConverter[ConstructionItem]() {
+ override def ConstructorData(obj : ConstructionItem) : Try[ACEData] = {
+ Success(ACEData(0,0))
+ }
+
+ override def DetailedConstructorData(obj : ConstructionItem) : Try[DetailedACEData] = {
+ Success(DetailedACEData(0))
+ }
+}
diff --git a/common/src/main/scala/net/psforever/objects/definition/converter/AMSConverter.scala b/common/src/main/scala/net/psforever/objects/definition/converter/AMSConverter.scala
new file mode 100644
index 000000000..ee4cfc8e4
--- /dev/null
+++ b/common/src/main/scala/net/psforever/objects/definition/converter/AMSConverter.scala
@@ -0,0 +1,44 @@
+// Copyright (c) 2017 PSForever
+package net.psforever.objects.definition.converter
+
+import net.psforever.objects.Vehicle
+import net.psforever.packet.game.PlanetSideGUID
+import net.psforever.packet.game.objectcreate.{AMSData, CommonFieldData, ObjectClass, PlacementData}
+
+import scala.util.{Success, Try}
+
+class AMSConverter extends ObjectCreateConverter[Vehicle] {
+ /* Vehicles do not have a conversion for `0x18` packet data. */
+
+ override def ConstructorData(obj : Vehicle) : Try[AMSData] = {
+ Success(
+ AMSData(
+ CommonFieldData(
+ PlacementData(obj.Position, obj.Orientation, obj.Velocity),
+ obj.Faction,
+ 0,
+ if(obj.Owner.isDefined) { obj.Owner.get } else { PlanetSideGUID(0) } //this is the owner field, right?
+ ),
+ 0,
+ obj.Health,
+ 0,
+ obj.Configuration,
+ 0,
+ ReferenceUtility(obj, ObjectClass.matrix_terminalc),
+ ReferenceUtility(obj, ObjectClass.ams_respawn_tube),
+ ReferenceUtility(obj, ObjectClass.order_terminala),
+ ReferenceUtility(obj, ObjectClass.order_terminalb)
+ )
+ )
+ }
+
+ /**
+ * For an object with a list of utilities, find a specific kind of utility.
+ * @param obj the game object
+ * @param objectId the utility being sought
+ * @return the global unique identifier of the utility
+ */
+ private def ReferenceUtility(obj : Vehicle, objectId : Int) : PlanetSideGUID = {
+ obj.Utilities.find(util => util.objectId == objectId).head.GUID
+ }
+}
diff --git a/common/src/main/scala/net/psforever/objects/definition/converter/ANTConverter.scala b/common/src/main/scala/net/psforever/objects/definition/converter/ANTConverter.scala
new file mode 100644
index 000000000..19c8c729c
--- /dev/null
+++ b/common/src/main/scala/net/psforever/objects/definition/converter/ANTConverter.scala
@@ -0,0 +1,29 @@
+// Copyright (c) 2017 PSForever
+package net.psforever.objects.definition.converter
+
+import net.psforever.objects.Vehicle
+import net.psforever.packet.game.PlanetSideGUID
+import net.psforever.packet.game.objectcreate.{ANTData, CommonFieldData, PlacementData}
+
+import scala.util.{Success, Try}
+
+class ANTConverter extends ObjectCreateConverter[Vehicle] {
+ /* Vehicles do not have a conversion for `0x18` packet data. */
+
+ override def ConstructorData(obj : Vehicle) : Try[ANTData] = {
+ Success(
+ ANTData(
+ CommonFieldData(
+ PlacementData(obj.Position, obj.Orientation,obj.Velocity),
+ obj.Faction,
+ 0,
+ if(obj.Owner.isDefined) { obj.Owner.get } else { PlanetSideGUID(0) } //this is the owner field, right?
+ ),
+ 0,
+ obj.Health,
+ 0,
+ obj.Configuration
+ )
+ )
+ }
+}
diff --git a/common/src/main/scala/net/psforever/objects/definition/converter/AmmoBoxConverter.scala b/common/src/main/scala/net/psforever/objects/definition/converter/AmmoBoxConverter.scala
new file mode 100644
index 000000000..890a9963d
--- /dev/null
+++ b/common/src/main/scala/net/psforever/objects/definition/converter/AmmoBoxConverter.scala
@@ -0,0 +1,17 @@
+// Copyright (c) 2017 PSForever
+package net.psforever.objects.definition.converter
+
+import net.psforever.objects.AmmoBox
+import net.psforever.packet.game.objectcreate.{AmmoBoxData, DetailedAmmoBoxData}
+
+import scala.util.{Success, Try}
+
+class AmmoBoxConverter extends ObjectCreateConverter[AmmoBox] {
+ override def ConstructorData(obj : AmmoBox) : Try[AmmoBoxData] = {
+ Success(AmmoBoxData())
+ }
+
+ override def DetailedConstructorData(obj : AmmoBox) : Try[DetailedAmmoBoxData] = {
+ Success(DetailedAmmoBoxData(8, obj.Capacity))
+ }
+}
diff --git a/common/src/main/scala/net/psforever/objects/definition/converter/AvatarConverter.scala b/common/src/main/scala/net/psforever/objects/definition/converter/AvatarConverter.scala
new file mode 100644
index 000000000..e94547b1c
--- /dev/null
+++ b/common/src/main/scala/net/psforever/objects/definition/converter/AvatarConverter.scala
@@ -0,0 +1,171 @@
+// Copyright (c) 2017 PSForever
+package net.psforever.objects.definition.converter
+
+import net.psforever.objects.{EquipmentSlot, Player}
+import net.psforever.objects.equipment.Equipment
+import net.psforever.packet.game.PlanetSideGUID
+import net.psforever.packet.game.objectcreate.{BasicCharacterData, CharacterAppearanceData, CharacterData, DetailedCharacterData, DrawnSlot, InternalSlot, InventoryData, PlacementData, RibbonBars, UniformStyle}
+import net.psforever.types.GrenadeState
+
+import scala.annotation.tailrec
+import scala.util.{Success, Try}
+
+class AvatarConverter extends ObjectCreateConverter[Player]() {
+ override def ConstructorData(obj : Player) : Try[CharacterData] = {
+ Success(
+ CharacterData(
+ MakeAppearanceData(obj),
+ obj.Health / obj.MaxHealth * 255, //TODO not precise
+ obj.Armor / obj.MaxArmor * 255, //TODO not precise
+ UniformStyle.Normal,
+ 0,
+ None, //TODO cosmetics
+ None, //TODO implant effects
+ InventoryData(MakeHolsters(obj, BuildEquipment).sortBy(_.parentSlot)),
+ GetDrawnSlot(obj)
+ )
+ )
+ //TODO tidy this mess up
+ }
+
+ override def DetailedConstructorData(obj : Player) : Try[DetailedCharacterData] = {
+ Success(
+ DetailedCharacterData(
+ MakeAppearanceData(obj),
+ obj.MaxHealth,
+ obj.Health,
+ obj.Armor,
+ 1, 7, 7,
+ obj.MaxStamina,
+ obj.Stamina,
+ 28, 4, 44, 84, 104, 1900,
+ List.empty[String], //TODO fte list
+ List.empty[String], //TODO tutorial list
+ InventoryData((MakeHolsters(obj, BuildDetailedEquipment) ++ MakeFifthSlot(obj) ++ MakeInventory(obj)).sortBy(_.parentSlot)),
+ GetDrawnSlot(obj)
+ )
+ )
+ //TODO tidy this mess up
+ }
+
+ /**
+ * Compose some data from a `Player` into a representation common to both `CharacterData` and `DetailedCharacterData`.
+ * @param obj the `Player` game object
+ * @return the resulting `CharacterAppearanceData`
+ */
+ private def MakeAppearanceData(obj : Player) : CharacterAppearanceData = {
+ CharacterAppearanceData(
+ PlacementData(obj.Position, obj.Orientation, obj.Velocity),
+ BasicCharacterData(obj.Name, obj.Faction, obj.Sex, obj.Voice, obj.Head),
+ 0,
+ false,
+ false,
+ obj.ExoSuit,
+ "",
+ 0,
+ obj.isBackpack,
+ obj.Orientation.y.toInt,
+ obj.FacingYawUpper.toInt,
+ true,
+ GrenadeState.None,
+ false,
+ false,
+ false,
+ RibbonBars()
+ )
+ }
+
+ /**
+ * Given a player with an inventory, convert the contents of that inventory into converted-decoded packet data.
+ * The inventory is not represented in a `0x17` `Player`, so the conversion is only valid for `0x18` avatars.
+ * It will always be "`Detailed`".
+ * @param obj the `Player` game object
+ * @return a list of all items that were in the inventory in decoded packet form
+ */
+ private def MakeInventory(obj : Player) : List[InternalSlot] = {
+ obj.Inventory.Items
+ .map({
+ case(_, item) =>
+ val equip : Equipment = item.obj
+ InternalSlot(equip.Definition.ObjectId, equip.GUID, item.start, equip.Definition.Packet.DetailedConstructorData(equip).get)
+ }).toList
+ }
+ /**
+ * Given a player with equipment holsters, convert the contents of those holsters into converted-decoded packet data.
+ * The decoded packet form is determined by the function in the parameters as both `0x17` and `0x18` conversions are available,
+ * with exception to the contents of the fifth slot.
+ * The fifth slot is only represented if the `Player` is an `0x18` type.
+ * @param obj the `Player` game object
+ * @param builder the function used to transform to the decoded packet form
+ * @return a list of all items that were in the holsters in decoded packet form
+ */
+ private def MakeHolsters(obj : Player, builder : ((Int, Equipment) => InternalSlot)) : List[InternalSlot] = {
+ recursiveMakeHolsters(obj.Holsters().iterator, builder)
+ }
+
+ /**
+ * Given a player with equipment holsters, convert any content of the fifth holster slot into converted-decoded packet data.
+ * The fifth holster is a curious divider between the standard holsters and the formal inventory.
+ * This fifth slot is only ever represented if the `Player` is an `0x18` type.
+ * @param obj the `Player` game object
+ * @return a list of any item that was in the fifth holster in decoded packet form
+ */
+ private def MakeFifthSlot(obj : Player) : List[InternalSlot] = {
+ obj.Slot(5).Equipment match {
+ case Some(equip) =>
+ BuildDetailedEquipment(5, equip) :: Nil
+ case _ =>
+ Nil
+ }
+ }
+
+ /**
+ * A builder method for turning an object into `0x17` decoded packet form.
+ * @param index the position of the object
+ * @param equip the game object
+ * @return the game object in decoded packet form
+ */
+ private def BuildEquipment(index : Int, equip : Equipment) : InternalSlot = {
+ InternalSlot(equip.Definition.ObjectId, equip.GUID, index, equip.Definition.Packet.ConstructorData(equip).get)
+ }
+
+ /**
+ * A builder method for turning an object into `0x18` decoded packet form.
+ * @param index the position of the object
+ * @param equip the game object
+ * @return the game object in decoded packet form
+ */
+ private def BuildDetailedEquipment(index : Int, equip : Equipment) : InternalSlot = {
+ InternalSlot(equip.Definition.ObjectId, equip.GUID, index, equip.Definition.Packet.DetailedConstructorData(equip).get)
+ }
+
+ @tailrec private def recursiveMakeHolsters(iter : Iterator[EquipmentSlot], builder : ((Int, Equipment) => InternalSlot), list : List[InternalSlot] = Nil, index : Int = 0) : List[InternalSlot] = {
+ if(!iter.hasNext) {
+ list
+ }
+ else {
+ val slot : EquipmentSlot = iter.next
+ if(slot.Equipment.isDefined) {
+ val equip : Equipment = slot.Equipment.get
+ recursiveMakeHolsters(
+ iter,
+ builder,
+ list :+ builder(index, equip),
+ index + 1
+ )
+ }
+ else {
+ recursiveMakeHolsters(iter, builder, list, index + 1)
+ }
+ }
+ }
+
+ /**
+ * Resolve which holster the player has drawn, if any.
+ * @param obj the `Player` game object
+ * @return the holster's Enumeration value
+ */
+ private def GetDrawnSlot(obj : Player) : DrawnSlot.Value = {
+ try { DrawnSlot(obj.DrawnSlot) } catch { case _ : Exception => DrawnSlot.None }
+ }
+}
diff --git a/common/src/main/scala/net/psforever/objects/definition/converter/BoomerTriggerConverter.scala b/common/src/main/scala/net/psforever/objects/definition/converter/BoomerTriggerConverter.scala
new file mode 100644
index 000000000..6f817afe6
--- /dev/null
+++ b/common/src/main/scala/net/psforever/objects/definition/converter/BoomerTriggerConverter.scala
@@ -0,0 +1,17 @@
+// Copyright (c) 2017 PSForever
+package net.psforever.objects.definition.converter
+
+import net.psforever.objects.SimpleItem
+import net.psforever.packet.game.objectcreate.{BoomerTriggerData, DetailedBoomerTriggerData}
+
+import scala.util.{Success, Try}
+
+class BoomerTriggerConverter extends ObjectCreateConverter[SimpleItem]() {
+ override def ConstructorData(obj : SimpleItem) : Try[BoomerTriggerData] = {
+ Success(BoomerTriggerData())
+ }
+
+ override def DetailedConstructorData(obj : SimpleItem) : Try[DetailedBoomerTriggerData] = {
+ Success(DetailedBoomerTriggerData())
+ }
+}
diff --git a/common/src/main/scala/net/psforever/objects/definition/converter/CommandDetonaterConverter.scala b/common/src/main/scala/net/psforever/objects/definition/converter/CommandDetonaterConverter.scala
new file mode 100644
index 000000000..014d5bd82
--- /dev/null
+++ b/common/src/main/scala/net/psforever/objects/definition/converter/CommandDetonaterConverter.scala
@@ -0,0 +1,17 @@
+// Copyright (c) 2017 PSForever
+package net.psforever.objects.definition.converter
+
+import net.psforever.objects.SimpleItem
+import net.psforever.packet.game.objectcreate.{CommandDetonaterData, DetailedCommandDetonaterData}
+
+import scala.util.{Success, Try}
+
+class CommandDetonaterConverter extends ObjectCreateConverter[SimpleItem]() {
+ override def ConstructorData(obj : SimpleItem) : Try[CommandDetonaterData] = {
+ Success(CommandDetonaterData())
+ }
+
+ override def DetailedConstructorData(obj : SimpleItem) : Try[DetailedCommandDetonaterData] = {
+ Success(DetailedCommandDetonaterData())
+ }
+}
diff --git a/common/src/main/scala/net/psforever/objects/definition/converter/KitConverter.scala b/common/src/main/scala/net/psforever/objects/definition/converter/KitConverter.scala
new file mode 100644
index 000000000..1b0e137df
--- /dev/null
+++ b/common/src/main/scala/net/psforever/objects/definition/converter/KitConverter.scala
@@ -0,0 +1,17 @@
+// Copyright (c) 2017 PSForever
+package net.psforever.objects.definition.converter
+
+import net.psforever.objects.Kit
+import net.psforever.packet.game.objectcreate.{AmmoBoxData, DetailedAmmoBoxData}
+
+import scala.util.{Success, Try}
+
+class KitConverter extends ObjectCreateConverter[Kit]() {
+ override def ConstructorData(obj : Kit) : Try[AmmoBoxData] = {
+ Success(AmmoBoxData())
+ }
+
+ override def DetailedConstructorData(obj : Kit) : Try[DetailedAmmoBoxData] = {
+ Success(DetailedAmmoBoxData(0, 1))
+ }
+}
diff --git a/common/src/main/scala/net/psforever/objects/definition/converter/LockerContainerConverter.scala b/common/src/main/scala/net/psforever/objects/definition/converter/LockerContainerConverter.scala
new file mode 100644
index 000000000..0ef5205bf
--- /dev/null
+++ b/common/src/main/scala/net/psforever/objects/definition/converter/LockerContainerConverter.scala
@@ -0,0 +1,35 @@
+// Copyright (c) 2017 PSForever
+package net.psforever.objects.definition.converter
+
+import net.psforever.objects.LockerContainer
+import net.psforever.objects.equipment.Equipment
+import net.psforever.objects.inventory.GridInventory
+import net.psforever.packet.game.PlanetSideGUID
+import net.psforever.packet.game.objectcreate.{DetailedAmmoBoxData, InternalSlot, InventoryData, LockerContainerData}
+
+import scala.util.{Success, Try}
+
+class LockerContainerConverter extends ObjectCreateConverter[LockerContainer]() {
+ override def ConstructorData(obj : LockerContainer) : Try[LockerContainerData] = {
+ Success(LockerContainerData(InventoryData(MakeInventory(obj.Inventory))))
+ }
+
+ override def DetailedConstructorData(obj : LockerContainer) : Try[DetailedAmmoBoxData] = {
+ Success(DetailedAmmoBoxData(8, 1)) //same format as AmmoBox data
+ }
+
+ /**
+ * Transform a list of contained items into a list of contained `InternalSlot` objects.
+ * All objects will take the form of data as if found in an `0x17` packet.
+ * @param inv the inventory container
+ * @return a list of all items that were in the inventory in decoded packet form
+ */
+ private def MakeInventory(inv : GridInventory) : List[InternalSlot] = {
+ inv.Items
+ .map({
+ case(guid, item) =>
+ val equip : Equipment = item.obj
+ InternalSlot(equip.Definition.ObjectId, PlanetSideGUID(guid), item.start, equip.Definition.Packet.ConstructorData(equip).get)
+ }).toList
+ }
+}
diff --git a/common/src/main/scala/net/psforever/objects/definition/converter/PacketConverter.scala b/common/src/main/scala/net/psforever/objects/definition/converter/PacketConverter.scala
new file mode 100644
index 000000000..663b1a2a3
--- /dev/null
+++ b/common/src/main/scala/net/psforever/objects/definition/converter/PacketConverter.scala
@@ -0,0 +1,71 @@
+// Copyright (c) 2017 PSForever
+package net.psforever.objects.definition.converter
+
+import net.psforever.objects.PlanetSideGameObject
+import net.psforever.packet.game.objectcreate.ConstructorData
+
+import scala.util.{Failure, Try}
+
+/**
+ * The base trait for polymorphic assignment for `ObjectCreateConverter`.
+ */
+sealed trait PacketConverter
+
+/**
+ * A converter that accepts an object and prepares it for transformation into an `0x17` packet or an `0x18` packet.
+ * This is the decoded packet form of the game object, as if hexadecimal data from a packet was decoded.
+ * @tparam A the type of game object
+ */
+abstract class ObjectCreateConverter[A <: PlanetSideGameObject] extends PacketConverter {
+// def ObjectCreate(obj : A) : Try[ObjectCreateMessage] = {
+// Success(
+// ObjectCreateMessage(obj.Definition.ObjectId, obj.GUID,
+// DroppedItemData(
+// PlacementData(obj.Position, obj.Orientation.x.toInt, obj.Orientation.y.toInt, obj.Orientation.z.toInt, Some(obj.Velocity)),
+// ConstructorData(obj).get
+// )
+// )
+// )
+// }
+//
+// def ObjectCreate(obj : A, info : PlacementData) : Try[ObjectCreateMessage] = {
+// Success(ObjectCreateMessage(obj.Definition.ObjectId, obj.GUID, DroppedItemData(info, ConstructorData(obj).get)))
+// }
+//
+// def ObjectCreate(obj : A, info : ObjectCreateMessageParent) : Try[ObjectCreateMessage] = {
+// Success(ObjectCreateMessage(obj.Definition.ObjectId, obj.GUID, info, ConstructorData(obj).get))
+// }
+//
+// def ObjectCreateDetailed(obj : A) : Try[ObjectCreateDetailedMessage] = {
+// Success(
+// ObjectCreateDetailedMessage(obj.Definition.ObjectId, obj.GUID,
+// DroppedItemData(
+// PlacementData(obj.Position, obj.Orientation.x.toInt, obj.Orientation.y.toInt, obj.Orientation.z.toInt, Some(obj.Velocity)),
+// DetailedConstructorData(obj).get
+// )
+// )
+// )
+// }
+//
+// def ObjectCreateDetailed(obj : A, info : PlacementData) : Try[ObjectCreateDetailedMessage] = {
+// Success(ObjectCreateDetailedMessage(obj.Definition.ObjectId, obj.GUID, DroppedItemData(info, DetailedConstructorData(obj).get)))
+// }
+//
+// def ObjectCreateDetailed(obj : A, info : ObjectCreateMessageParent) : Try[ObjectCreateDetailedMessage] = {
+// Success(ObjectCreateDetailedMessage(obj.Definition.ObjectId, obj.GUID, info, DetailedConstructorData(obj).get))
+// }
+
+ /**
+ * Take a game object and transform it into its equivalent data for an `0x17` packet.
+ * @param obj the game object
+ * @return the specific `ConstructorData` that is equivalent to this object
+ */
+ def ConstructorData(obj : A) : Try[ConstructorData] = { Failure(new NoSuchMethodException(s"method not defined for object $obj")) }
+
+ /**
+ * Take a game object and transform it into its equivalent data for an `0x18` packet.
+ * @param obj the game object
+ * @return the specific `ConstructorData` that is equivalent to this object
+ */
+ def DetailedConstructorData(obj : A) : Try[ConstructorData] = { Failure(new NoSuchMethodException(s"method not defined for object $obj")) }
+}
diff --git a/common/src/main/scala/net/psforever/objects/definition/converter/REKConverter.scala b/common/src/main/scala/net/psforever/objects/definition/converter/REKConverter.scala
new file mode 100644
index 000000000..b35c789a4
--- /dev/null
+++ b/common/src/main/scala/net/psforever/objects/definition/converter/REKConverter.scala
@@ -0,0 +1,17 @@
+// Copyright (c) 2017 PSForever
+package net.psforever.objects.definition.converter
+
+import net.psforever.objects.SimpleItem
+import net.psforever.packet.game.objectcreate.{DetailedREKData, REKData}
+
+import scala.util.{Success, Try}
+
+class REKConverter extends ObjectCreateConverter[SimpleItem]() {
+ override def ConstructorData(obj : SimpleItem) : Try[REKData] = {
+ Success(REKData(8,0))
+ }
+
+ override def DetailedConstructorData(obj : SimpleItem) : Try[DetailedREKData] = {
+ Success(DetailedREKData(8))
+ }
+}
\ No newline at end of file
diff --git a/common/src/main/scala/net/psforever/objects/definition/converter/ToolConverter.scala b/common/src/main/scala/net/psforever/objects/definition/converter/ToolConverter.scala
new file mode 100644
index 000000000..eb386d6a1
--- /dev/null
+++ b/common/src/main/scala/net/psforever/objects/definition/converter/ToolConverter.scala
@@ -0,0 +1,30 @@
+// Copyright (c) 2017 PSForever
+package net.psforever.objects.definition.converter
+
+import net.psforever.objects.Tool
+import net.psforever.packet.game.objectcreate.{DetailedWeaponData, InternalSlot, WeaponData}
+
+import scala.collection.mutable.ListBuffer
+import scala.util.{Success, Try}
+
+class ToolConverter extends ObjectCreateConverter[Tool]() {
+ override def ConstructorData(obj : Tool) : Try[WeaponData] = {
+ val maxSlot : Int = obj.MaxAmmoSlot
+ val slots : ListBuffer[InternalSlot] = ListBuffer[InternalSlot]()
+ (0 until maxSlot).foreach(index => {
+ val box = obj.AmmoSlots(index).Box
+ slots += InternalSlot(box.Definition.ObjectId, box.GUID, index, box.Definition.Packet.ConstructorData(box).get)
+ })
+ Success(WeaponData(4,8, obj.FireModeIndex, slots.toList)(maxSlot))
+ }
+
+ override def DetailedConstructorData(obj : Tool) : Try[DetailedWeaponData] = {
+ val maxSlot : Int = obj.MaxAmmoSlot
+ val slots : ListBuffer[InternalSlot] = ListBuffer[InternalSlot]()
+ (0 until maxSlot).foreach(index => {
+ val box = obj.AmmoSlots(index).Box
+ slots += InternalSlot(box.Definition.ObjectId, box.GUID, index, box.Definition.Packet.DetailedConstructorData(box).get)
+ })
+ Success(DetailedWeaponData(4,8, slots.toList)(maxSlot))
+ }
+}
diff --git a/common/src/main/scala/net/psforever/objects/definition/converter/VehicleConverter.scala b/common/src/main/scala/net/psforever/objects/definition/converter/VehicleConverter.scala
new file mode 100644
index 000000000..39ea3403d
--- /dev/null
+++ b/common/src/main/scala/net/psforever/objects/definition/converter/VehicleConverter.scala
@@ -0,0 +1,62 @@
+// Copyright (c) 2017 PSForever
+package net.psforever.objects.definition.converter
+
+import net.psforever.objects.equipment.Equipment
+import net.psforever.objects.{EquipmentSlot, Vehicle}
+import net.psforever.packet.game.PlanetSideGUID
+import net.psforever.packet.game.objectcreate.MountItem.MountItem
+import net.psforever.packet.game.objectcreate.{CommonFieldData, DriveState, MountItem, PlacementData, VehicleData}
+
+import scala.annotation.tailrec
+import scala.util.{Success, Try}
+
+class VehicleConverter extends ObjectCreateConverter[Vehicle]() {
+ /* Vehicles do not have a conversion for `0x18` packet data. */
+
+ override def ConstructorData(obj : Vehicle) : Try[VehicleData] = {
+ Success(
+ VehicleData(
+ CommonFieldData(
+ PlacementData(obj.Position, obj.Orientation, obj.Velocity),
+ obj.Faction,
+ 0,
+ if(obj.Owner.isDefined) { obj.Owner.get } else { PlanetSideGUID(0) } //this is the owner field, right?
+ ),
+ 0,
+ obj.Health / obj.MaxHealth * 255, //TODO not precise
+ 0,
+ DriveState.Mobile,
+ false,
+ 0,
+ Some(MakeMountings(obj).sortBy(_.parentSlot))
+ )
+ )
+ //TODO work utilities into this mess?
+ }
+
+ /**
+ * For an object with a list of weapon mountings, convert those weapons into data as if found in an `0x17` packet.
+ * @param obj the Vehicle game object
+ * @return the converted data
+ */
+ private def MakeMountings(obj : Vehicle) : List[MountItem] = recursiveMakeMountings(obj.Weapons.iterator)
+
+ @tailrec private def recursiveMakeMountings(iter : Iterator[(Int,EquipmentSlot)], list : List[MountItem] = Nil) : List[MountItem] = {
+ if(!iter.hasNext) {
+ list
+ }
+ else {
+ val (index, slot) = iter.next
+ if(slot.Equipment.isDefined) {
+ val equip : Equipment = slot.Equipment.get
+ recursiveMakeMountings(
+ iter,
+ list :+ MountItem(equip.Definition.ObjectId, equip.GUID, index, equip.Definition.Packet.ConstructorData(equip).get)
+ )
+ }
+ else {
+ recursiveMakeMountings(iter, list)
+ }
+ }
+ }
+}
diff --git a/common/src/main/scala/net/psforever/objects/entity/Identifiable.scala b/common/src/main/scala/net/psforever/objects/entity/Identifiable.scala
new file mode 100644
index 000000000..52327008e
--- /dev/null
+++ b/common/src/main/scala/net/psforever/objects/entity/Identifiable.scala
@@ -0,0 +1,13 @@
+// Copyright (c) 2017 PSForever
+package net.psforever.objects.entity
+
+import net.psforever.packet.game.PlanetSideGUID
+
+/**
+ * Identifiable represents anything that has its own globally unique identifier (GUID).
+ */
+trait Identifiable {
+ def GUID : PlanetSideGUID
+
+ def GUID_=(guid : PlanetSideGUID) : PlanetSideGUID
+}
diff --git a/common/src/main/scala/net/psforever/objects/entity/IdentifiableEntity.scala b/common/src/main/scala/net/psforever/objects/entity/IdentifiableEntity.scala
new file mode 100644
index 000000000..de158182d
--- /dev/null
+++ b/common/src/main/scala/net/psforever/objects/entity/IdentifiableEntity.scala
@@ -0,0 +1,96 @@
+// Copyright (c) 2017 PSForever
+package net.psforever.objects.entity
+
+import net.psforever.packet.game.PlanetSideGUID
+
+/**
+ * Represent any entity that must have its own globally unique identifier (GUID) to be functional.
+ *
+ * "Testing" the object refers to the act of acquiring a reference to the GUID the object is using.
+ * This object starts with a container class that represents a unprepared GUID state and raises an `Exception` when tested.
+ * Setting a proper `PlanetSideGUID` replaces that container class with a container class that returns the GUID when tested.
+ * The object can be invalidated, restoring the previous `Exception`-raising condition.
+ * @throws `NoGUIDException` if there is no GUID to give
+ */
+abstract class IdentifiableEntity extends Identifiable {
+ private val container : GUIDContainable = GUIDContainer()
+ private var current : GUIDContainable = IdentifiableEntity.noGUIDContainer
+
+ def HasGUID : Boolean = {
+ try {
+ GUID
+ true
+ }
+ catch {
+ case _ : NoGUIDException =>
+ false
+ }
+ }
+
+ def GUID : PlanetSideGUID = current.GUID
+
+ def GUID_=(guid : PlanetSideGUID) : PlanetSideGUID = {
+ current = container
+ current.GUID = guid
+ GUID
+ }
+
+ def Invalidate() : Unit = {
+ current = IdentifiableEntity.noGUIDContainer
+ }
+}
+
+object IdentifiableEntity {
+ private val noGUIDContainer : GUIDContainable = new NoGUIDContainer
+}
+
+/**
+ * Mask the `Identifiable` `trait`.
+ */
+sealed trait GUIDContainable extends Identifiable
+
+/**
+ * Hidden container that represents an object that is not ready to be used by the game.
+ */
+private case class NoGUIDContainer() extends GUIDContainable {
+ /**
+ * Raise an `Exception` because we have no GUID to give.
+ * @throws `NoGUIDException` always
+ * @return never returns
+ */
+ def GUID : PlanetSideGUID = {
+ throw NoGUIDException("object has not initialized a global identifier")
+ }
+
+ /**
+ * Normally, this should never be called.
+ * @param toGuid the globally unique identifier
+ * @return never returns
+ */
+ def GUID_=(toGuid : PlanetSideGUID) : PlanetSideGUID = {
+ throw NoGUIDException("can not initialize a global identifier with this object")
+ }
+}
+
+/**
+ * Hidden container that represents an object that has a working GUID and is ready to be used by the game.
+ * @param guid the object's globally unique identifier;
+ * defaults to a GUID equal to 0
+ */
+private case class GUIDContainer(private var guid : PlanetSideGUID = PlanetSideGUID(0)) extends GUIDContainable {
+ /**
+ * Provide the GUID used to initialize this object.
+ * @return the GUID
+ */
+ def GUID : PlanetSideGUID = guid
+
+ /**
+ * Exchange the previous GUID for a new one, re-using this container.
+ * @param toGuid the globally unique identifier
+ * @return the GUID
+ */
+ def GUID_=(toGuid : PlanetSideGUID) : PlanetSideGUID = {
+ guid = toGuid
+ GUID
+ }
+}
diff --git a/common/src/main/scala/net/psforever/objects/entity/MobileWorldEntity.scala b/common/src/main/scala/net/psforever/objects/entity/MobileWorldEntity.scala
new file mode 100644
index 000000000..33de4798f
--- /dev/null
+++ b/common/src/main/scala/net/psforever/objects/entity/MobileWorldEntity.scala
@@ -0,0 +1,45 @@
+// Copyright (c) 2017 PSForever
+package net.psforever.objects.entity
+
+import net.psforever.types.Vector3
+
+import scala.collection.mutable
+
+class MobileWorldEntity extends WorldEntity {
+ private var coords : mutable.Stack[TimeEntry] = mutable.Stack(TimeEntry.invalid) //history of last #n positional updates
+ private var orient : mutable.Stack[TimeEntry] = mutable.Stack(TimeEntry.invalid) //history of last #n orientation updates
+ private var vel : Option[Vector3] = None
+
+ def Position : Vector3 = coords.head.entry
+
+ def Position_=(vec : Vector3) : Vector3 = {
+ coords = MobileWorldEntity.pushNewStack(coords, vec, SimpleWorldEntity.validatePositionEntry)
+ Position
+ }
+
+ def AllPositions : scala.collection.immutable.List[TimeEntry] = coords.toList
+
+ def Orientation : Vector3 = orient.head.entry
+
+ def Orientation_=(vec : Vector3) : Vector3 = {
+ orient = MobileWorldEntity.pushNewStack(orient, vec, SimpleWorldEntity.validateOrientationEntry)
+ Orientation
+ }
+
+ def AllOrientations : scala.collection.immutable.List[TimeEntry] = orient.toList
+
+ def Velocity : Option[Vector3] = vel
+
+ def Velocity_=(vec : Option[Vector3]) : Option[Vector3] = {
+ vel = vec
+ vel
+ }
+
+ override def toString : String = WorldEntity.toString(this)
+}
+
+object MobileWorldEntity {
+ def pushNewStack(lst : mutable.Stack[TimeEntry], newEntry : Vector3, validate : (Vector3) => Vector3) : mutable.Stack[TimeEntry] = {
+ lst.slice(0, 199).push(TimeEntry(validate(newEntry)))
+ }
+}
diff --git a/common/src/main/scala/net/psforever/objects/entity/NoGUIDException.scala b/common/src/main/scala/net/psforever/objects/entity/NoGUIDException.scala
new file mode 100644
index 000000000..9685b27ad
--- /dev/null
+++ b/common/src/main/scala/net/psforever/objects/entity/NoGUIDException.scala
@@ -0,0 +1,6 @@
+// Copyright (c) 2017 PSForever
+package net.psforever.objects.entity
+
+case class NoGUIDException(private val message: String = "",
+ private val cause: Throwable = None.orNull
+ ) extends RuntimeException(message, cause)
diff --git a/common/src/main/scala/net/psforever/objects/entity/SimpleWorldEntity.scala b/common/src/main/scala/net/psforever/objects/entity/SimpleWorldEntity.scala
new file mode 100644
index 000000000..704dab21e
--- /dev/null
+++ b/common/src/main/scala/net/psforever/objects/entity/SimpleWorldEntity.scala
@@ -0,0 +1,52 @@
+// Copyright (c) 2017 PSForever
+package net.psforever.objects.entity
+
+import net.psforever.types.Vector3
+
+class SimpleWorldEntity extends WorldEntity {
+ private var coords : Vector3 = Vector3(0f, 0f, 0f)
+ private var orient : Vector3 = Vector3(0f, 0f, 0f)
+ private var vel : Option[Vector3] = None
+
+ def Position : Vector3 = coords
+
+ def Position_=(vec : Vector3) : Vector3 = {
+ coords = SimpleWorldEntity.validatePositionEntry(vec)
+ Position
+ }
+
+ def Orientation : Vector3 = orient
+
+ def Orientation_=(vec : Vector3) : Vector3 = {
+ orient = SimpleWorldEntity.validateOrientationEntry(vec)
+ Orientation
+ }
+
+ def Velocity : Option[Vector3] = vel
+
+ def Velocity_=(vec : Option[Vector3]) : Option[Vector3] = {
+ vel = vec
+ Velocity
+ }
+
+ override def toString : String = WorldEntity.toString(this)
+}
+
+object SimpleWorldEntity {
+ def validatePositionEntry(vec : Vector3) : Vector3 = vec
+
+ def validateOrientationEntry(vec : Vector3) : Vector3 = {
+ val x = clampAngle(vec.x)
+ val y = clampAngle(vec.y)
+ val z = clampAngle(vec.z)
+ Vector3(x, y, z)
+ }
+
+ def clampAngle(ang : Float) : Float = {
+ var ang2 = ang % 360f
+ if(ang2 < 0f) {
+ ang2 += 360f
+ }
+ ang2
+ }
+}
diff --git a/common/src/main/scala/net/psforever/objects/entity/TimeEntry.scala b/common/src/main/scala/net/psforever/objects/entity/TimeEntry.scala
new file mode 100644
index 000000000..f6fcc417d
--- /dev/null
+++ b/common/src/main/scala/net/psforever/objects/entity/TimeEntry.scala
@@ -0,0 +1,13 @@
+// Copyright (c) 2017 PSForever
+package net.psforever.objects.entity
+
+import net.psforever.types.Vector3
+
+case class TimeEntry(entry : net.psforever.types.Vector3)(implicit time : Long = org.joda.time.DateTime.now.getMillis)
+
+object TimeEntry {
+ val invalid = TimeEntry(Vector3(0f, 0f, 0f))(0L)
+
+ def apply(x : Float, y : Float, z : Float) : TimeEntry =
+ TimeEntry(Vector3(x, y, z))
+}
diff --git a/common/src/main/scala/net/psforever/objects/entity/WorldEntity.scala b/common/src/main/scala/net/psforever/objects/entity/WorldEntity.scala
new file mode 100644
index 000000000..2d742603d
--- /dev/null
+++ b/common/src/main/scala/net/psforever/objects/entity/WorldEntity.scala
@@ -0,0 +1,26 @@
+// Copyright (c) 2017 PSForever
+package net.psforever.objects.entity
+
+import net.psforever.types.Vector3
+
+trait WorldEntity {
+ def Position : Vector3
+
+ def Position_=(vec : Vector3) : Vector3
+
+ def Orientation : Vector3
+
+ def Orientation_=(vec : Vector3) : Vector3
+
+ def Velocity : Option[Vector3]
+
+ def Velocity_=(vec : Option[Vector3]) : Option[Vector3]
+
+ def Velocity_=(vec : Vector3) : Option[Vector3] = Velocity = Some(vec)
+}
+
+object WorldEntity {
+ def toString(obj : WorldEntity) : String = {
+ s"pos=${obj.Position}, ori=${obj.Orientation}"
+ }
+}
diff --git a/common/src/main/scala/net/psforever/objects/equipment/Ammo.scala b/common/src/main/scala/net/psforever/objects/equipment/Ammo.scala
new file mode 100644
index 000000000..e7228abcb
--- /dev/null
+++ b/common/src/main/scala/net/psforever/objects/equipment/Ammo.scala
@@ -0,0 +1,93 @@
+// Copyright (c) 2017 PSForever
+package net.psforever.objects.equipment
+
+/**
+ * An `Enumeration` of all the ammunition types in the game, paired with their object id as the `Value`.
+ */
+object Ammo extends Enumeration {
+ final val bullet_105mm = Value(0)
+ final val bullet_12mm = Value(3)
+ final val bullet_150mm = Value(6)
+ final val bullet_15mm = Value(9)
+ final val bullet_20mm = Value(16)
+ final val bullet_25mm = Value(19)
+ final val bullet_35mm = Value(21)
+ final val bullet_75mm = Value(25)
+ final val bullet_9mm = Value(28)
+ final val bullet_9mm_AP = Value(29)
+ final val ancient_ammo_combo = Value(50)
+ final val ancient_ammo_vehicle = Value(51)
+ final val anniversary_ammo = Value(54)
+ final val aphelion_immolation_cannon_ammo = Value(86)
+ final val aphelion_laser_ammo = Value(89)
+ final val aphelion_plasma_rocket_ammo = Value(97)
+ final val aphelion_ppa_ammo = Value(101)
+ final val aphelion_starfire_ammo = Value(106)
+ final val armor_canister = Value(111)
+ final val armor_siphon_ammo = Value(112)
+ final val bolt = Value(145)
+ final val burster_ammo = Value(154)
+ final val colossus_100mm_cannon_ammo = Value(180)
+ final val colossus_burster_ammo = Value(186)
+ final val colossus_chaingun_ammo = Value(191)
+ final val colossus_cluster_bomb_ammo = Value(195)
+ final val colossus_tank_cannon_ammo = Value(205)
+ final val comet_ammo = Value(209)
+ final val dualcycler_ammo = Value(265)
+ final val energy_cell = Value(272)
+ final val energy_gun_ammo = Value(275)
+ final val falcon_ammo = Value(285)
+ final val firebird_missile = Value(287)
+ final val flamethrower_ammo = Value(300)
+ final val flux_cannon_thresher_battery = Value(307)
+ final val fluxpod_ammo = Value(310)
+ final val frag_cartridge = Value(327)
+ final val frag_grenade_ammo = Value(331)
+ final val gauss_cannon_ammo = Value(345)
+ final val grenade = Value(370)
+ final val health_canister = Value(389)
+ final val heavy_grenade_mortar = Value(391)
+ final val heavy_rail_beam_battery = Value(393)
+ final val hellfire_ammo = Value(399)
+ final val hunter_seeker_missile = Value(403)
+ final val jammer_cartridge = Value(413)
+ final val jammer_grenade_ammo = Value(417)
+ final val lancer_cartridge = Value(426)
+ final val liberator_bomb = Value(434)
+ final val maelstrom_ammo = Value(463)
+ final val melee_ammo = Value(540)
+ final val mine = Value(550)
+ final val mine_sweeper_ammo = Value(553)
+ final val ntu_siphon_ammo = Value(595)
+ final val oicw_ammo = Value(600)
+ final val pellet_gun_ammo = Value(630)
+ final val peregrine_dual_machine_gun_ammo = Value(637)
+ final val peregrine_mechhammer_ammo = Value(645)
+ final val peregrine_particle_cannon_ammo = Value(653)
+ final val peregrine_rocket_pod_ammo = Value(656)
+ final val peregrine_sparrow_ammo = Value(659)
+ final val phalanx_ammo = Value(664)
+ final val phoenix_missile = Value(674)
+ final val plasma_cartridge = Value(677)
+ final val plasma_grenade_ammo = Value(681)
+ final val pounder_ammo = Value(693)
+ final val pulse_battery = Value(704)
+ final val quasar_ammo = Value(712)
+ final val reaver_rocket = Value(722)
+ final val rocket = Value(734)
+ final val scattercannon_ammo = Value(745)
+ final val shotgun_shell = Value(755)
+ final val shotgun_shell_AP = Value(756)
+ final val six_shooter_ammo = Value(762)
+ final val skyguard_flak_cannon_ammo = Value(786)
+ final val sparrow_ammo = Value(791)
+ final val spitfire_aa_ammo = Value(820)
+ final val spitfire_ammo = Value(823)
+ final val starfire_ammo = Value(830)
+ final val striker_missile_ammo = Value(839)
+ final val trek_ammo = Value(877)
+ final val upgrade_canister = Value(922)
+ final val wasp_gun_ammo = Value(998)
+ final val wasp_rocket_ammo = Value(1000)
+ final val winchester_ammo = Value(1004)
+}
diff --git a/common/src/main/scala/net/psforever/objects/equipment/CItem.scala b/common/src/main/scala/net/psforever/objects/equipment/CItem.scala
new file mode 100644
index 000000000..0582bfded
--- /dev/null
+++ b/common/src/main/scala/net/psforever/objects/equipment/CItem.scala
@@ -0,0 +1,28 @@
+// Copyright (c) 2017 PSForever
+package net.psforever.objects.equipment
+
+object CItem {
+ object Unit extends Enumeration {
+ final val ace = Value(32)
+ final val advanced_ace = Value(39) //fdu
+ final val router_telepad = Value(743)
+ }
+
+ object DeployedItem extends Enumeration {
+ final val boomer = Value(148)
+ final val deployable_shield_generator = Value(240)
+ final val he_mine = Value(388)
+ final val jammer_mine = Value(420) //disruptor mine
+ final val motionalarmsensor = Value(575)
+ final val sensor_shield = Value(752) //sensor disruptor
+ final val spitfire_aa = Value(819) //cerebus turret
+ final val spitfire_cloaked = Value(825) //shadow turret
+ final val spitfire_turret = Value(826)
+ final val tank_traps = Value(849) //trap
+ final val portable_manned_turret = Value(685)
+ final val portable_manned_turret_nc = Value(686)
+ final val portable_manned_turret_tr = Value(687)
+ final val portable_manned_turret_vs = Value(688)
+ final val router_telepad_deployable = Value(744)
+ }
+}
diff --git a/common/src/main/scala/net/psforever/objects/equipment/Equipment.scala b/common/src/main/scala/net/psforever/objects/equipment/Equipment.scala
new file mode 100644
index 000000000..6bf28bd14
--- /dev/null
+++ b/common/src/main/scala/net/psforever/objects/equipment/Equipment.scala
@@ -0,0 +1,21 @@
+// Copyright (c) 2017 PSForever
+package net.psforever.objects.equipment
+
+import net.psforever.objects.PlanetSideGameObject
+import net.psforever.objects.definition.EquipmentDefinition
+import net.psforever.objects.inventory.InventoryTile
+
+/**
+ * `Equipment` is anything that can be:
+ * placed into a slot of a certain "size";
+ * and, placed into an inventory system;
+ * and, special carried (like a lattice logic unit);
+ * and, dropped on the ground in the game world and render where it was deposited.
+ */
+abstract class Equipment extends PlanetSideGameObject {
+ def Size : EquipmentSize.Value = Definition.Size
+
+ def Tile : InventoryTile = Definition.Tile
+
+ def Definition : EquipmentDefinition
+}
diff --git a/common/src/main/scala/net/psforever/objects/equipment/EquipmentSize.scala b/common/src/main/scala/net/psforever/objects/equipment/EquipmentSize.scala
new file mode 100644
index 000000000..484d91609
--- /dev/null
+++ b/common/src/main/scala/net/psforever/objects/equipment/EquipmentSize.scala
@@ -0,0 +1,38 @@
+// Copyright (c) 2017 PSForever
+package net.psforever.objects.equipment
+
+object EquipmentSize extends Enumeration {
+ val
+ Blocked,
+ Melee, //special
+ Pistol, //2x2 and 3x3
+ Rifle, //6x3 and 9x3
+ Max, //max weapon only
+ VehicleWeapon, //vehicle-mounted weapons
+ Inventory, //reserved
+ Any
+ = Value
+
+ /**
+ * Perform custom size comparison.
+ *
+ * In almost all cases, the only time two sizes are equal is if they are the same size.
+ * If either size is `Blocked`, however, they will never be equal.
+ * If either size is `Inventory` or `Any`, however, they will always be equal.
+ * Size comparison is important for putting `Equipment` in size-fitted slots, but not for much else.
+ * @param type1 the first size
+ * @param type2 the second size
+ * @return `true`, if they are equal; `false`, otherwise
+ */
+ def isEqual(type1 : EquipmentSize.Value, type2 : EquipmentSize.Value) : Boolean = {
+ if(type1 >= Inventory || type2 >= Inventory) {
+ true
+ }
+ else if(type1 == Blocked || type2 == Blocked) {
+ false
+ }
+ else {
+ type1 == type2
+ }
+ }
+}
diff --git a/common/src/main/scala/net/psforever/objects/equipment/FireModeDefinition.scala b/common/src/main/scala/net/psforever/objects/equipment/FireModeDefinition.scala
new file mode 100644
index 000000000..2c02a54c8
--- /dev/null
+++ b/common/src/main/scala/net/psforever/objects/equipment/FireModeDefinition.scala
@@ -0,0 +1,69 @@
+// Copyright (c) 2017 PSForever
+package net.psforever.objects.equipment
+
+import scala.collection.mutable
+
+class FireModeDefinition {
+// private var ammoTypes : mutable.ListBuffer[Ammo.Value] = mutable.ListBuffer[Ammo.Value]() //ammo types valid for this fire mode
+ private val ammoTypeIndices : mutable.ListBuffer[Int] = mutable.ListBuffer[Int]() //indices pointing to all ammo types used
+ private var ammoSlotIndex : Int = 0 //ammunition slot number this fire mode utilizes
+ private var chamber : Int = 1 //how many rounds are queued to be fired at once, e.g., 3 for the Jackhammer's triple burst
+ private var magazine : Int = 1 //how many rounds are queued for each reload cycle
+ private var target : Any = _ //target designation (self? other?)
+ private var resetAmmoIndexOnSwap : Boolean = false //when changing fire modes, do not attempt to match previous mode's ammo type
+
+ //damage modifiers will follow here ...
+
+// def AmmoTypes : mutable.ListBuffer[Ammo.Value] = ammoTypes
+//
+// def AmmoTypes_=(ammo : Ammo.Value) : mutable.ListBuffer[Ammo.Value] = {
+// ammoTypes += ammo
+// }
+
+ def AmmoSlotIndex : Int = ammoSlotIndex
+
+ def AmmoSlotIndex_=(index : Int) : Int = {
+ ammoSlotIndex = index
+ AmmoSlotIndex
+ }
+
+ def AmmoTypeIndices : mutable.ListBuffer[Int] = ammoTypeIndices
+
+ def AmmoTypeIndices_=(index : Int) : mutable.ListBuffer[Int] = {
+ ammoTypeIndices += index
+ }
+
+ def Chamber : Int = chamber
+
+ def Chamber_=(inChamber : Int) : Int = {
+ chamber = inChamber
+ Chamber
+ }
+
+ def Magazine : Int = magazine
+
+ def Magazine_=(inMagazine : Int) : Int = {
+ magazine = inMagazine
+ Magazine
+ }
+
+ def Target : Any = target
+
+ def Target_+(setAsTarget : Any) : Any = {
+ target = setAsTarget
+ Target
+ }
+
+ def ResetAmmoIndexOnSwap : Boolean = resetAmmoIndexOnSwap
+
+ def ResetAmmoIndexOnSwap_=(reset : Boolean) : Boolean = {
+ resetAmmoIndexOnSwap = reset
+ ResetAmmoIndexOnSwap
+ }
+}
+
+object FireModeDefinition {
+ def apply() : FireModeDefinition = {
+ new FireModeDefinition()
+ }
+}
diff --git a/common/src/main/scala/net/psforever/objects/equipment/FireModeSwitch.scala b/common/src/main/scala/net/psforever/objects/equipment/FireModeSwitch.scala
new file mode 100644
index 000000000..6a3596cc5
--- /dev/null
+++ b/common/src/main/scala/net/psforever/objects/equipment/FireModeSwitch.scala
@@ -0,0 +1,21 @@
+// Copyright (c) 2017 PSForever
+package net.psforever.objects.equipment
+
+/**
+ * Fire mode is a non-complex method of representing variance in `Equipment` output.
+ *
+ * All weapons and some support items have fire modes, though most only have one.
+ * The number of fire modes is visually indicated by the bubbles next to the icon of the `Equipment` in a holster slot.
+ * The specifics of how a fire mode affects the output is left to implementation and execution.
+ * Contrast how `Tool`s deal with multiple types of ammunition.
+ * @tparam Mode the type parameter representing the fire mode
+ */
+trait FireModeSwitch[Mode] {
+ def FireModeIndex : Int
+
+ def FireModeIndex_=(index : Int) : Int
+
+ def FireMode : Mode
+
+ def NextFireMode : Mode
+}
\ No newline at end of file
diff --git a/common/src/main/scala/net/psforever/objects/equipment/Kits.scala b/common/src/main/scala/net/psforever/objects/equipment/Kits.scala
new file mode 100644
index 000000000..daffc3655
--- /dev/null
+++ b/common/src/main/scala/net/psforever/objects/equipment/Kits.scala
@@ -0,0 +1,12 @@
+// Copyright (c) 2017 PSForever
+package net.psforever.objects.equipment
+
+/**
+ * An `Enumeration` of the kit types in the game, paired with their object id as the `Value`.
+ */
+object Kits extends Enumeration {
+ final val medkit = Value(536)
+ final val super_armorkit = Value(842) //super repair kit
+ final val super_medkit = Value(843)
+ final val super_staminakit = Value(844) //super stimpack
+}
diff --git a/common/src/main/scala/net/psforever/objects/equipment/SItem.scala b/common/src/main/scala/net/psforever/objects/equipment/SItem.scala
new file mode 100644
index 000000000..8aea0f858
--- /dev/null
+++ b/common/src/main/scala/net/psforever/objects/equipment/SItem.scala
@@ -0,0 +1,9 @@
+// Copyright (c) 2017 PSForever
+package net.psforever.objects.equipment
+
+object SItem extends Enumeration {
+ final val boomer_trigger = Value(149)
+ final val command_detonater = Value(213) //cud
+ final val flail_targeting_laser = Value(297)
+ final val remote_electronics_kit = Value(728)
+}
diff --git a/common/src/main/scala/net/psforever/objects/guid/AvailabilityPolicy.scala b/common/src/main/scala/net/psforever/objects/guid/AvailabilityPolicy.scala
new file mode 100644
index 000000000..2775de758
--- /dev/null
+++ b/common/src/main/scala/net/psforever/objects/guid/AvailabilityPolicy.scala
@@ -0,0 +1,21 @@
+// Copyright (c) 2017 PSForever
+package net.psforever.objects.guid
+
+/**
+ * The availability of individual GUIDs is maintained by the given policy.
+ */
+object AvailabilityPolicy extends Enumeration {
+ type Type = Value
+
+ /**
+ * An `AVAILABLE` GUID is ready and waiting to be `LEASED` for use.
+ * A `LEASED` GUID has been issued and is currently being used.
+ * A `RESTRICTED` GUID can never be freed. It is allowed, however, to be assigned once as if it were `LEASED`.
+ */
+ val
+ Available,
+ Leased,
+ Restricted
+ = Value
+}
+
diff --git a/common/src/main/scala/net/psforever/objects/guid/NumberPoolHub.scala b/common/src/main/scala/net/psforever/objects/guid/NumberPoolHub.scala
new file mode 100644
index 000000000..e360df533
--- /dev/null
+++ b/common/src/main/scala/net/psforever/objects/guid/NumberPoolHub.scala
@@ -0,0 +1,474 @@
+// Copyright (c) 2017 PSForever
+package net.psforever.objects.guid
+
+import net.psforever.objects.entity.{IdentifiableEntity, NoGUIDException}
+import net.psforever.objects.guid.key.LoanedKey
+import net.psforever.objects.guid.pool.{ExclusivePool, GenericPool, NumberPool}
+import net.psforever.objects.guid.source.NumberSource
+import net.psforever.packet.game.PlanetSideGUID
+
+import scala.util.{Failure, Success, Try}
+
+/**
+ * A master object that manages `NumberPool`s when they are applied to a single `NumberSource`.
+ * It catalogs the numbers and ensures the pool contents are unique to each other.
+ *
+ * All globally unique numbers are sorted into user-defined groups called pools.
+ * Pools are intended to pre-allocate certain numbers to certain tasks.
+ * Two default pools also exist - "generic," for all numbers not formally placed into a pool, and a hidden restricted pool.
+ * The former can accept a variety of numbers on the source not known at initialization time loaded into it.
+ * The latter can only be set by the `NumberSource` and can not be affected once this object is created.
+ * @param source the number source object
+ */
+class NumberPoolHub(private val source : NumberSource) {
+ import scala.collection.mutable
+ private val hash : mutable.HashMap[String, NumberPool] = mutable.HashMap[String, NumberPool]()
+ private val bigpool : mutable.LongMap[String] = mutable.LongMap[String]()
+ hash += "generic" -> new GenericPool(bigpool, source.Size)
+ source.FinalizeRestrictions.foreach(i => bigpool += i.toLong -> "") //these numbers can never be pooled; the source can no longer restrict numbers
+
+ /**
+ * Given a globally unique identifier, rweturn any object registered to it.
+ *
+ * Use:
+ * For `val obj = new NumberPoolHub(...)` use `obj(number)`.
+ * @param number the unique number to attempt to retrieve from the `source`
+ * @return the object that is assigned to the number
+ */
+ def apply(number : PlanetSideGUID) : Option[IdentifiableEntity] = this(number.guid)
+
+ /**
+ * Given a globally unique identifier, rweturn any object registered to it.
+ *
+ * Use:
+ * For `val obj = new NumberPoolHub(...)` use `obj(number)`.
+ * @param number the unique number to attempt to retrieve from the `source`
+ * @return the object that is assigned to the number
+ */
+ def apply(number : Int) : Option[IdentifiableEntity] = source.Get(number).orElse(return None).get.Object
+
+ def Numbers : List[Int] = bigpool.keys.map(key => key.toInt).toList
+
+ /**
+ * Create a new number pool with the given label and the given numbers.
+ *
+ * Creating number pools is a task that should only be performed at whatever counts as the initialization stage.
+ * Nothing technically blocks it being done during runtime;
+ * however, stability is best served by doing it only once and while nothing else risk affecting the numbers.
+ * Unlike "live" functionality which often returns as `Success` or `Failure`, this is considered a critical operation.
+ * As thus, `Exceptions` are permitted since a fault of the pool's creation will disrupt normal operations.
+ * @param name the name of the pool
+ * @param pool the `List` of numbers that will belong to the pool
+ * @return the newly-created number pool
+ * @throws IllegalArgumentException if the pool is already defined;
+ * if the pool contains numbers the source does not
+ * if the pool contains numbers from already existing pools
+ */
+ def AddPool(name : String, pool : List[Int]) : NumberPool = {
+ if(hash.get(name).isDefined) {
+ throw new IllegalArgumentException(s"can not add pool $name - name already known to this hub?")
+ }
+ if(source.Size <= pool.max) {
+ throw new IllegalArgumentException(s"can not add pool $name - max(pool) is greater than source.size")
+ }
+ val collision = bigpool.keys.map(n => n.toInt).toSet.intersect(pool.toSet)
+ if(collision.nonEmpty) {
+ throw new IllegalArgumentException(s"can not add pool $name - it contains the following redundant numbers: ${collision.toString}")
+ }
+ pool.foreach(i => bigpool += i.toLong -> name)
+ hash += name -> new ExclusivePool(pool)
+ hash(name)
+ }
+
+ /**
+ * Remove an existing number pool with the given label from the list of number pools.
+ *
+ * Removing number pools is a task that should only be performed at whatever counts as the termination stage.
+ * All the same reasoning applies as with `AddPool` above.
+ * Although an easy operation would move all the assigned numbers in the removing pool to the "generic" pool,
+ * doing so is ill-advised both for the reasoning above and because that creates unreliability.
+ * @param name the name of the pool
+ * @return the `List` of numbers that belonged to the pool
+ * @throws IllegalArgumentException if the pool doesn't exist or is not removed (removable)
+ */
+ def RemovePool(name : String) : List[Int] = {
+ if(name.equals("generic") || name.equals("")) {
+ throw new IllegalArgumentException("can not remove pool - generic or restricted")
+ }
+ val pool = hash.get(name).orElse({
+ throw new IllegalArgumentException(s"can not remove pool - $name does not exist")
+ }).get
+ if(pool.Count > 0) {
+ throw new IllegalArgumentException(s"can not remove pool - $name is being used")
+ }
+
+ hash.remove(name)
+ pool.Numbers.foreach(number => bigpool -= number)
+ pool.Numbers
+ }
+
+ /**
+ * Get the number pool known by this name.
+ * It will not return correctly for any number that is in the "restricted" pool.
+ * @param name the name of the pool
+ * @return a reference to the number pool, or `None`
+ */
+ def GetPool(name : String) : Option[NumberPool] = if(name.equals("")) { None } else { hash.get(name) }
+
+ /**
+ * na
+ * @return na
+ */
+ def Pools : mutable.HashMap[String, NumberPool] = hash
+
+ /**
+ * Reference a specific number's pool.
+ *
+ * `WhichPool(Int)` does not require the number to be registered at the time it is used.
+ * It does not return anything for an unregistered unpooled number -
+ * a number that would be part of the "generic" nonstandard pool.
+ * It only reports "generic" if that number is registered.
+ * It will not return correctly for any number that is in the "restricted" pool.
+ * @param number a number
+ * @return the name of the number pool to which this item belongs
+ */
+ def WhichPool(number : Int) : Option[String] = {
+ val name = bigpool.get(number)
+ if(name.contains("")) { None } else { name }
+ }
+
+ /**
+ * Reference a specific number's pool.
+ *
+ * `WhichPool(IdentifiableEntity)` does require the object to be registered to be found.
+ * It checks that the object is registered, and that it is registered to the local source object.
+ * @param obj an object
+ * @return the name of the number pool to which this item belongs
+ */
+ def WhichPool(obj : IdentifiableEntity) : Option[String] = {
+ try {
+ val number : Int = obj.GUID.guid
+ val entry = source.Get(number)
+ if(entry.isDefined && entry.get.Object.contains(obj)) { WhichPool(number) } else { None }
+ }
+ catch {
+ case _ : Exception =>
+ None
+ }
+ }
+
+ /**
+ * Register an object to any available selection (of the "generic" number pool).
+ * @param obj an object being registered
+ * @return the number the was given to the object
+ */
+ def register(obj : IdentifiableEntity) : Try[Int] = register(obj, "generic")
+
+ /**
+ * Register an object to a specific number if it is available.
+ * @param obj an object being registered
+ * @param number the number whose assignment is requested
+ * @return the number the was given to the object
+ */
+ def register(obj : IdentifiableEntity, number : Int) : Try[Int] = {
+ bigpool.get(number.toLong) match {
+ case Some(name) =>
+ register_GetSpecificNumberFromPool(name, number) match {
+ case Success(key) =>
+ key.Object = obj
+ Success(obj.GUID.guid)
+ case Failure(ex) =>
+ Failure(new Exception(s"trying to register an object to a specific number but, ${ex.getMessage}"))
+ }
+ case None =>
+ import net.psforever.objects.guid.selector.SpecificSelector
+ hash("generic").Selector.asInstanceOf[SpecificSelector].SelectionIndex = number
+ register(obj, "generic")
+ }
+ }
+
+ /**
+ * Asides from using the `name` parameter to find the number pool,
+ * this method also removes the `number` from that number pool of its own accord.
+ * The "{pool}.Selector = new SpecificSelector" technique is used to safely remove the number.
+ * It will disrupt the internal order of the number pool set by its current selector and reset it to a neutral state.
+ * @param name the local pool name
+ * @param number the number whose assignment is requested
+ * @return the number the was given to the object
+ * @see `NumberPool.Selector_=(NumberSelector)`
+ */
+ private def register_GetSpecificNumberFromPool(name : String, number : Int) : Try[LoanedKey]= {
+ hash.get(name) match {
+ case Some(pool) =>
+ val slctr = pool.Selector
+ import net.psforever.objects.guid.selector.SpecificSelector
+ val specific = new SpecificSelector
+ specific.SelectionIndex = number
+ pool.Selector = specific
+ pool.Get()
+ pool.Selector = slctr
+ register_GetAvailableNumberFromSource(number)
+ case None =>
+ Failure(new Exception(s"number pool $name not defined"))
+ }
+ }
+
+ private def register_GetAvailableNumberFromSource(number : Int) : Try[LoanedKey] = {
+ source.Available(number) match {
+ case Some(key) =>
+ Success(key)
+ case None =>
+ Failure(new Exception(s"number $number is unavailable"))
+ }
+ }
+
+ /**
+ * Register an object to a specific number pool.
+ * @param obj an object being registered
+ * @param name the local pool name
+ * @return the number the was given to the object
+ */
+ def register(obj : IdentifiableEntity, name : String) : Try[Int] = {
+ try {
+ register_CheckNumberAgainstDesiredPool(obj, name, obj.GUID.guid)
+ }
+ catch {
+ case _ : Exception =>
+ register_GetPool(name) match {
+ case Success(key) =>
+ key.Object = obj
+ Success(obj.GUID.guid)
+ case Failure(ex) =>
+ Failure(new Exception(s"trying to register an object but, ${ex.getMessage}"))
+ }
+ }
+ }
+
+ private def register_CheckNumberAgainstDesiredPool(obj : IdentifiableEntity, name : String, number : Int) : Try[Int] = {
+ val directKey = source.Get(number)
+ if(directKey.isEmpty || !directKey.get.Object.contains(obj)) {
+ Failure(new Exception("object already registered, but not to this source"))
+ }
+ else if(!WhichPool(number).contains(name)) {
+ //TODO obj is not registered to the desired pool; is this okay?
+ Success(number)
+ }
+ else {
+ Success(number)
+ }
+ }
+
+ private def register_GetPool(name : String) : Try[LoanedKey] = {
+ hash.get(name) match {
+ case Some(pool) =>
+ register_GetNumberFromDesiredPool(pool)
+ case _ =>
+ Failure(new Exception(s"number pool $name not defined"))
+ }
+ }
+
+ private def register_GetNumberFromDesiredPool(pool : NumberPool) : Try[LoanedKey] = {
+ pool.Get() match {
+ case Success(number) =>
+ register_GetMonitorFromSource(number)
+ case Failure(ex) =>
+ Failure(ex)
+ }
+ }
+
+ private def register_GetMonitorFromSource(number : Int) : Try[LoanedKey] = {
+ source.Available(number) match {
+ case Some(key) =>
+ Success(key)
+ case _ =>
+ throw NoGUIDException(s"a pool gave us a number $number that is actually unavailable") //stop the show; this is terrible!
+ }
+ }
+
+ /**
+ * Register a specific number.
+ * @param number the number whose assignment is requested
+ * @return the monitor for a number
+ */
+ def register(number : Int) : Try[LoanedKey] = {
+ WhichPool(number) match {
+ case None =>
+ import net.psforever.objects.guid.selector.SpecificSelector
+ hash("generic").Selector.asInstanceOf[SpecificSelector].SelectionIndex = number
+ register_GetPool("generic")
+ case Some(name) =>
+ register_GetSpecificNumberFromPool(name, number)
+ }
+ }
+
+ /**
+ * Register a number selected automatically from the named pool.
+ * @param name the local pool name
+ * @return the monitor for a number
+ */
+ def register(name : String) : Try[LoanedKey] = register_GetPool(name)
+
+ /**
+ * na
+ * @param obj an object being registered
+ * @param number the number whose assignment is requested
+ * @return an object that has been registered
+ */
+ def latterPartRegister(obj : IdentifiableEntity, number : Int) : Try[IdentifiableEntity] = {
+ register_GetMonitorFromSource(number) match {
+ case Success(monitor) =>
+ monitor.Object = obj
+ Success(obj)
+ case Failure(ex) =>
+ Failure(ex)
+ }
+ }
+
+ /**
+ * Unregister a specific object.
+ * @param obj an object being unregistered
+ * @return the number previously associated with the object
+ */
+ def unregister(obj : IdentifiableEntity) : Try[Int] = {
+ unregister_GetPoolFromObject(obj) match {
+ case Success(pool) =>
+ val number = obj.GUID.guid
+ pool.Return(number)
+ source.Return(number)
+ obj.Invalidate()
+ Success(number)
+ case Failure(ex) =>
+ Failure(new Exception(s"can not unregister this object: ${ex.getMessage}"))
+ }
+ }
+
+ def unregister_GetPoolFromObject(obj : IdentifiableEntity) : Try[NumberPool] = {
+ WhichPool(obj) match {
+ case Some(name) =>
+ unregister_GetPool(name)
+ case None =>
+ Failure(throw new Exception("can not find a pool for this object"))
+ }
+ }
+
+ private def unregister_GetPool(name : String) : Try[NumberPool] = {
+ hash.get(name) match {
+ case Some(pool) =>
+ Success(pool)
+ case None =>
+ Failure(new Exception(s"no pool by the name of '$name'"))
+ }
+ }
+
+ /**
+ * Unregister a specific number.
+ * @param number the number previously assigned(?)
+ * @return the object, if any, previous associated with the number
+ */
+ def unregister(number : Int) : Try[Option[IdentifiableEntity]] = {
+ if(source.Test(number)) {
+ unregister_GetObjectFromSource(number)
+ }
+ else {
+ Failure(new Exception(s"can not unregister a number $number that this source does not own") )
+ }
+ }
+
+ private def unregister_GetObjectFromSource(number : Int) : Try[Option[IdentifiableEntity]] = {
+ source.Return(number) match {
+ case Some(obj) =>
+ unregister_ReturnObjectToPool(obj)
+ case None =>
+ unregister_ReturnNumberToPool(number) //nothing is wrong, but we'll check the pool
+ }
+ }
+
+ private def unregister_ReturnObjectToPool(obj : IdentifiableEntity) : Try[Option[IdentifiableEntity]] = {
+ val number = obj.GUID.guid
+ unregister_GetPoolFromNumber(number) match {
+ case Success(pool) =>
+ pool.Return(number)
+ obj.Invalidate()
+ Success(Some(obj))
+ case Failure(ex) =>
+ source.Available(number) //undo
+ Failure(new Exception(s"started unregistering, but ${ex.getMessage}"))
+ }
+ }
+
+ private def unregister_ReturnNumberToPool(number : Int) : Try[Option[IdentifiableEntity]] = {
+ unregister_GetPoolFromNumber(number) match {
+ case Success(pool) =>
+ pool.Return(number)
+ Success(None)
+ case _ => //though everything else went fine, we must still fail if this number was restricted all along
+ if(!bigpool.get(number).contains("")) {
+ Success(None)
+ }
+ else {
+ Failure(new Exception(s"can not unregister this number $number"))
+ }
+ }
+ }
+
+ private def unregister_GetPoolFromNumber(number : Int) : Try[NumberPool] = {
+ WhichPool(number) match {
+ case Some(name) =>
+ unregister_GetPool(name)
+ case None =>
+ Failure(new Exception(s"no pool using number $number"))
+ }
+ }
+
+ /**
+ * For accessing the `Return` function of the contained `NumberSource` directly.
+ * @param number the number to return.
+ * @return any object previously using this number
+ */
+ def latterPartUnregister(number : Int) : Option[IdentifiableEntity] = source.Return(number)
+
+ /**
+ * Determines if the object is registered.
+ *
+ * Three conditions are necessary to determine this condition for objects.
+ * (1) A registered object has a globally unique identifier.
+ * (2) A registered object is known to the `source` by that identifier.
+ * (3) The registered object can be found attached to that entry from the source.
+ * @param obj an object
+ * @return `true`, if the number is registered; `false`, otherwise
+ * @see `isRegistered(Int)`
+ */
+ def isRegistered(obj : IdentifiableEntity) : Boolean = {
+ try {
+ source.Get(obj.GUID.guid) match {
+ case Some(monitor) =>
+ monitor.Object.contains(obj)
+ case None =>
+ false
+ }
+ }
+ catch {
+ case _ : NoGUIDException =>
+ false
+ }
+ }
+
+ /**
+ * Determines if the number is registered.
+ *
+ * Two conditions are necessary to determine this condition for numbers.
+ * (1) A registered number is known to the `source`.
+ * (2) A register number is known as `Leased` to the `source`.
+ * @param number the number previously assigned(?)
+ * @return `true`, if the number is registered; `false`, otherwise
+ * @see `isRegistered(IdentifiableEntity)`
+ */
+ def isRegistered(number : Int) : Boolean = {
+ source.Get(number) match {
+ case Some(monitor) =>
+ monitor.Policy == AvailabilityPolicy.Leased
+ case None =>
+ false
+ }
+ }
+}
diff --git a/common/src/main/scala/net/psforever/objects/guid/Task.scala b/common/src/main/scala/net/psforever/objects/guid/Task.scala
new file mode 100644
index 000000000..f744c2af2
--- /dev/null
+++ b/common/src/main/scala/net/psforever/objects/guid/Task.scala
@@ -0,0 +1,23 @@
+// Copyright (c) 2017 PSForever
+package net.psforever.objects.guid
+
+import akka.actor.ActorRef
+
+trait Task {
+ def Execute(resolver : ActorRef) : Unit
+ def isComplete : Task.Resolution.Value = Task.Resolution.Incomplete
+ def Timeout : Long = 200L //milliseconds
+ def onSuccess() : Unit = { }
+ def onFailure(ex : Throwable) : Unit = { }
+ def onTimeout(ex : Throwable) : Unit = onFailure(ex)
+ def onAbort(ex : Throwable) : Unit = { }
+ def Cleanup() : Unit = { }
+}
+
+object Task {
+ def TimeNow : Long = { java.time.Instant.now().getEpochSecond }
+
+ object Resolution extends Enumeration {
+ val Success, Incomplete, Failure = Value
+ }
+}
diff --git a/common/src/main/scala/net/psforever/objects/guid/TaskResolver.scala b/common/src/main/scala/net/psforever/objects/guid/TaskResolver.scala
new file mode 100644
index 000000000..bea02e663
--- /dev/null
+++ b/common/src/main/scala/net/psforever/objects/guid/TaskResolver.scala
@@ -0,0 +1,393 @@
+// Copyright (c) 2017 PSForever
+package net.psforever.objects.guid
+
+import java.util.concurrent.TimeoutException
+
+import akka.actor.{Actor, ActorRef, Cancellable}
+import akka.routing.Broadcast
+
+import scala.annotation.tailrec
+import scala.collection.mutable.ListBuffer
+import scala.concurrent.duration._
+import scala.concurrent.ExecutionContext.Implicits.global
+import scala.util.{Failure, Success}
+
+class TaskResolver() extends Actor {
+ /** list of all work currently managed by this TaskResolver */
+ private val tasks : ListBuffer[TaskResolver.TaskEntry] = new ListBuffer[TaskResolver.TaskEntry]
+ /** scheduled examination of all managed work */
+ private var timeoutCleanup : Cancellable = TaskResolver.DefaultCancellable
+ //private[this] val log = org.log4s.getLogger
+
+ /**
+ * Deal with any tasks that are still enqueued with this expiring `TaskResolver`.
+ *
+ * First, eliminate all timed-out tasks.
+ * Secondly, deal with all tasks that have reported "success" but have not yet been handled.
+ * Finally, all other remaining tasks should be treated as if they had failed.
+ */
+ override def aroundPostStop() = {
+ super.aroundPostStop()
+
+ timeoutCleanup.cancel()
+ TimeoutCleanup()
+ OnSuccess()
+ val ex : Throwable = new Exception(s"a task is being stopped")
+ OnFailure(ex)
+ tasks.indices.foreach({index =>
+ val entry = tasks(index)
+ PropagateAbort(index, ex)
+ if(entry.isASubtask) {
+ entry.supertaskRef ! Failure(ex) //alert our superior task's resolver we have completed
+ }
+ })
+ }
+
+ def receive : Receive = {
+ case TaskResolver.GiveTask(aTask, Nil) =>
+ GiveTask(aTask)
+
+ case TaskResolver.GiveTask(aTask, subtasks) =>
+ QueueSubtasks(aTask, subtasks)
+
+ case TaskResolver.GiveSubtask(aTask, subtasks, resolver) =>
+ QueueSubtasks(aTask, subtasks, true, resolver)
+
+ case TaskResolver.CompletedSubtask() =>
+ ExecuteNewTasks()
+
+ case Success(_) => //ignore the contents as unreliable
+ OnSuccess()
+
+ case Failure(ex) =>
+ OnFailure(ex)
+
+ case TaskResolver.AbortTask(task, ex) =>
+ OnAbort(task, ex)
+
+ case TaskResolver.TimeoutCleanup() =>
+ TimeoutCleanup()
+
+ case _ => ;
+ }
+
+ /**
+ * Accept simple work and perform it.
+ * @param aTask the work to be completed
+ */
+ private def GiveTask(aTask : Task) : Unit = {
+ val entry : TaskResolver.TaskEntry = TaskResolver.TaskEntry(aTask)
+ tasks += entry
+ entry.Execute(self) //send this Actor; aesthetically pleasant expression
+ StartTimeoutCheck()
+ }
+
+ /**
+ * Start the periodic checks for a task that has run for too long (timed-out), unless those checks are already running.
+ */
+ private def StartTimeoutCheck() : Unit = {
+ if(timeoutCleanup.isCancelled) {
+ timeoutCleanup = context.system.scheduler.schedule(500 milliseconds, 500 milliseconds, self, TaskResolver.TimeoutCleanup())
+ }
+ }
+
+ /**
+ * Accept complicated work and divide it into a main task and tasks that must be handled before the main task.
+ * Do not start the main task until all of the aforementioned "sub-tasks" are completed.
+ *
+ * Sub-tasks can be nested many times.
+ * All immediate sub-tasks count as the primary sub-tasks for the current main task.
+ * Each pair of main task and sub-tasks, for every sub-task discovered, is passed on to another `TaskResolver` for completion.
+ * The parent of this `TaskResolver` is the router logic for all brethren `TaskResolver` `Actors`.
+ * @param task the work to be completed
+ * @param subtasks other work that needs to be completed first
+ * @param isSubTask `true`, if this task counts as internal or as a leaf in the chain of `Task` dependency;
+ * `false`, by default, if we are the top of the chain fo dependency
+ * @param resolver the `TaskResolver` that distributed this work, thus determining that this work is a sub-task;
+ * by default, no one, as the work is identified as a main task
+ */
+ private def QueueSubtasks(task : Task, subtasks : List[TaskResolver.GiveTask], isSubTask : Boolean = false, resolver : ActorRef = Actor.noSender) : Unit = {
+ val sublist : List[Task] = subtasks.map(task => task.task)
+ val entry : TaskResolver.TaskEntry = TaskResolver.TaskEntry(task, sublist, isSubTask, resolver)
+ tasks += entry
+ if(sublist.isEmpty) { //a leaf in terms of task dependency; so, not dependent on any other work
+ entry.Execute(self)
+ }
+ else {
+ subtasks.foreach({subtask =>
+ context.parent ! TaskResolver.GiveSubtask(subtask.task, subtask.subs, self) //route back to submit subtask to pool
+ })
+ }
+ StartTimeoutCheck()
+ }
+
+ /**
+ * Perform these checks when a task has reported successful completion to this TaskResolver.
+ * Since the `Success(_)` can not be associated with a specific task, every task and subtask will be checked.
+ */
+ private def OnSuccess(): Unit = {
+ //by reversing the List, we can remove TaskEntries without disrupting the order
+ TaskResolver.filterCompletion(tasks.indices.reverseIterator, tasks.toList, Task.Resolution.Success).foreach({index =>
+ val entry = tasks(index)
+ entry.task.onSuccess()
+ if(entry.isASubtask) {
+ entry.supertaskRef ! TaskResolver.CompletedSubtask() //alert our dependent task's resolver that we have completed
+ }
+ TaskCleanup(index)
+ })
+ }
+
+ /**
+ * Scan across a group of sub-tasks and determine if the associated main `Task` may execute.
+ * All of the sub-tasks must report a `Success` completion status before the main work can begin.
+ */
+ private def ExecuteNewTasks() : Unit = {
+ tasks.filter({taskEntry => taskEntry.subtasks.nonEmpty}).foreach(entry => {
+ if(TaskResolver.filterCompletionMatch(entry.subtasks.iterator, Task.Resolution.Success)) {
+ entry.Execute(self)
+ StartTimeoutCheck()
+ }
+ })
+ }
+
+ /**
+ * Perform these checks when a task has reported failure to this TaskResolver.
+ * Since the `Failure(Throwable)` can not be associated with a specific task, every task and subtask will be checked.
+ * Consequently, the specific `Throwable` that contains the error message may have nothing to do with the failed task.
+ * @param ex a `Throwable` that reports what happened to the task
+ */
+ private def OnFailure(ex : Throwable) : Unit = {
+ TaskResolver.filterCompletion(tasks.indices.reverseIterator, tasks.toList, Task.Resolution.Failure).foreach({index =>
+ val entry = tasks(index)
+ PropagateAbort(index, ex)
+ entry.task.onFailure(ex) //TODO let the error be disjoint?
+ if(entry.isASubtask) {
+ entry.supertaskRef ! Failure(ex) //alert our superior task's resolver we have completed
+ }
+ })
+ FaultSubtasks()
+ }
+
+ /**
+ * Scan across a group of sub-tasks and, if any have reported `Failure`, report to the main `Task` that it should fail as well.
+ */
+ private def FaultSubtasks() : Unit = {
+ tasks.indices.filter({index => tasks(index).subtasks.nonEmpty}).reverse.foreach(index => {
+ val entry = tasks(index)
+ if(TaskResolver.filterCompletionMatch(entry.subtasks.iterator, Task.Resolution.Failure)) {
+ val ex : Throwable = new Exception(s"a task ${entry.task} had a subtask that failed")
+ entry.task.onFailure(ex)
+ if(entry.isASubtask) {
+ entry.supertaskRef ! Failure(ex) //alert our superior task's resolver we have completed
+ }
+ TaskCleanup(index)
+ }
+ })
+ }
+
+ /**
+ * If a specific `Task` is governed by this `TaskResolver`, find its index and dispose of it and its known sub-tasks.
+ * @param task the work to be found
+ * @param ex a `Throwable` that reports what happened to the work
+ */
+ private def OnAbort(task : Task, ex : Throwable) : Unit = {
+ TaskResolver.findTaskIndex(tasks.iterator, task) match {
+ case Some(index) =>
+ PropagateAbort(index, ex)
+ case None => ;
+ }
+ }
+
+ /**
+ * If a specific `Task` is governed by this `TaskResolver`, dispose of it and its known sub-tasks.
+ * @param index the index of the discovered work
+ * @param ex a `Throwable` that reports what happened to the work
+ */
+ private def PropagateAbort(index : Int, ex : Throwable) : Unit = {
+ tasks(index).subtasks.foreach({subtask =>
+ if(subtask.isComplete == Task.Resolution.Success) {
+ subtask.onAbort(ex)
+ }
+ context.parent ! Broadcast(TaskResolver.AbortTask(subtask, ex))
+ })
+ TaskCleanup(index)
+ }
+
+ /**
+ * Find all tasks that have been running for too long and declare them as timed-out.
+ * Run periodically, as long as work is being performed.
+ */
+ private def TimeoutCleanup() : Unit = {
+ TaskResolver.filterTimeout(tasks.indices.reverseIterator, tasks.toList, Task.TimeNow).foreach({index =>
+ val ex : Throwable = new TimeoutException(s"a task ${tasks(index).task} has timed out")
+ tasks(index).task.onTimeout(ex)
+ PropagateAbort(index, ex)
+ })
+ }
+
+ /**
+ * Remove a `Task` that has reported completion.
+ * @param index an index of work in the `List` of `Task`s
+ */
+ private def TaskCleanup(index : Int) : Unit = {
+ tasks(index).task.Cleanup()
+ tasks.remove(index)
+ if(tasks.isEmpty) {
+ timeoutCleanup.cancel()
+ }
+ }
+}
+
+object TaskResolver {
+ /**
+ * Give this `TaskResolver` simple work to be performed.
+ * @param task the work to be completed
+ * @param subs other work that needs to be completed first
+ */
+ final case class GiveTask(task : Task, subs : List[GiveTask] = Nil)
+
+ /**
+ * Pass around complex work to be performed.
+ * @param task the work to be completed
+ * @param subs other work that needs to be completed first
+ * @param resolver the `TaskResolver` that will handle work that depends on the outcome of this work
+ */
+ private final case class GiveSubtask(task : Task, subs : List[GiveTask], resolver : ActorRef)
+
+ /**
+ * Run a scheduled timed-out `Task` check.
+ */
+ private final case class TimeoutCleanup()
+
+ /**
+ *
+ */
+ private final case class CompletedSubtask()
+
+ /**
+ * A `Broadcast` message designed to find and remove a particular task from this series of routed `Actors`.
+ * @param task the work to be removed
+ * @param ex an explanation why the work is being aborted
+ */
+ private final case class AbortTask(task : Task, ex : Throwable)
+
+ /**
+ * Storage unit for a specific unit of work, plus extra information.
+ * @param task the work to be completed
+ * @param subtasks other work that needs to be completed first
+ * @param isASubtask whether this work is intermediary or the last in a dependency chain
+ * @param supertaskRef the `TaskResolver` that will handle work that depends on the outcome of this work
+ */
+ private final case class TaskEntry(task : Task, subtasks : List[Task] = Nil, isASubtask : Boolean = false, supertaskRef : ActorRef = Actor.noSender) {
+ private var start : Long = 0L
+ private var isExecuting : Boolean = false
+
+ def Start : Long = start
+
+ def Executing : Boolean = isExecuting
+
+ def Execute(ref : ActorRef) : Unit = {
+ if(!isExecuting) {
+ start = Task.TimeNow
+ isExecuting = true
+ task.Execute(ref)
+ }
+ }
+ }
+
+ /**
+ * A placeholder `Cancellable` object for the time-out checking functionality.
+ */
+ private final val DefaultCancellable = new Cancellable() {
+ def cancel : Boolean = true
+ def isCancelled() : Boolean = true
+ }
+
+ /**
+ * Find the index of the targeted `Task`, if it is enqueued here.
+ * @param iter an `Iterator` of
+ * @param task a target `Task`
+ * @param index the current index in the aforementioned `List`;
+ * defaults to 0
+ * @return the index of the discovered task, or `None`
+ */
+ @tailrec private def findTaskIndex(iter : Iterator[TaskResolver.TaskEntry], task : Task, index : Int = 0) : Option[Int] = {
+ if(!iter.hasNext) {
+ None
+ }
+ else {
+ if(iter.next.task == task) {
+ Some(index)
+ }
+ else {
+ findTaskIndex(iter, task, index + 1)
+ }
+ }
+ }
+
+ /**
+ * Scan across a group of tasks to determine which ones match the target completion status.
+ * @param iter an `Iterator` of enqueued `TaskEntry` indices
+ * @param resolution the target completion status
+ * @param indexList a persistent `List` of indices
+ * @return the `List` of all valid `Task` indices
+ */
+ @tailrec private def filterCompletion(iter : Iterator[Int], tasks : List[TaskEntry], resolution : Task.Resolution.Value, indexList : List[Int] = Nil) : List[Int] = {
+ if(!iter.hasNext) {
+ indexList
+ }
+ else {
+ val index : Int = iter.next
+ if(tasks(index).task.isComplete == resolution) {
+ filterCompletion(iter, tasks, resolution, indexList :+ index)
+ }
+ else {
+ filterCompletion(iter, tasks, resolution, indexList)
+ }
+ }
+ }
+
+ /**
+ * Scan across a group of sub-tasks to determine if they all match the target completion status.
+ * @param iter an `Iterator` of enqueued sub-tasks
+ * @param resolution the target completion status
+ * @return `true`, if all tasks match the complete status;
+ * `false`, otherwise
+ */
+ @tailrec private def filterCompletionMatch(iter : Iterator[Task], resolution : Task.Resolution.Value) : Boolean = {
+ if(!iter.hasNext) {
+ true
+ }
+ else {
+ if(iter.next.isComplete == resolution) {
+ filterCompletionMatch(iter, resolution)
+ }
+ else {
+ false
+ }
+ }
+ }
+
+ /**
+ * Find the indices of all enqueued work that has timed-out.
+ * @param iter an `Iterator` of enqueued `TaskEntry` indices
+ * @param now the current time in milliseconds
+ * @param indexList a persistent `List` of indices
+ * @return the `List` of all valid `Task` indices
+ */
+ @tailrec private def filterTimeout(iter : Iterator[Int], tasks : List[TaskEntry], now : Long, indexList : List[Int] = Nil) : List[Int] = {
+ if(!iter.hasNext) {
+ indexList
+ }
+ else {
+ val index : Int = iter.next
+ val taskEntry = tasks(index)
+ if(taskEntry.Executing && taskEntry.task.isComplete == Task.Resolution.Incomplete && now - taskEntry.Start > taskEntry.task.Timeout) {
+ filterTimeout(iter, tasks, now, indexList :+ index)
+ }
+ else {
+ filterTimeout(iter, tasks, now, indexList)
+ }
+ }
+ }
+}
diff --git a/common/src/main/scala/net/psforever/objects/guid/actor/IsRegistered.scala b/common/src/main/scala/net/psforever/objects/guid/actor/IsRegistered.scala
new file mode 100644
index 000000000..b680c4698
--- /dev/null
+++ b/common/src/main/scala/net/psforever/objects/guid/actor/IsRegistered.scala
@@ -0,0 +1,31 @@
+// Copyright (c) 2017 PSForever
+package net.psforever.objects.guid.actor
+
+import net.psforever.objects.entity.IdentifiableEntity
+
+/**
+ * A message for requesting information about the registration status of an object or a number.
+ * @param obj the optional object
+ * @param number the optional number
+ */
+final case class IsRegistered(obj : Option[IdentifiableEntity], number : Option[Int])
+
+object IsRegistered {
+ /**
+ * Overloaded constructor for querying an object's status.
+ * @param obj the object
+ * @return an `IsRegistered` object
+ */
+ def apply(obj : IdentifiableEntity) : IsRegistered = {
+ new IsRegistered(Some(obj), None)
+ }
+
+ /**
+ * Overloaded constructor for querying a number's status.
+ * @param number the number
+ * @return an `IsRegistered` object
+ */
+ def apply(number : Int) : IsRegistered = {
+ new IsRegistered(None, Some(number))
+ }
+}
diff --git a/common/src/main/scala/net/psforever/objects/guid/actor/NumberPoolAccessorActor.scala b/common/src/main/scala/net/psforever/objects/guid/actor/NumberPoolAccessorActor.scala
new file mode 100644
index 000000000..ec35632c2
--- /dev/null
+++ b/common/src/main/scala/net/psforever/objects/guid/actor/NumberPoolAccessorActor.scala
@@ -0,0 +1,216 @@
+// Copyright (c) 2017 PSForever
+package net.psforever.objects.guid.actor
+
+import akka.actor.{Actor, ActorRef}
+import akka.pattern.ask
+import akka.util.Timeout
+import net.psforever.objects.entity.IdentifiableEntity
+import net.psforever.objects.guid.NumberPoolHub
+import net.psforever.objects.guid.pool.NumberPool
+
+import scala.concurrent.duration._
+import scala.util.{Failure, Success}
+
+/**
+ * An `Actor` that wraps around the `Actor` for a `NumberPool` and automates a portion of the number registration process.
+ *
+ * The `NumberPoolActor` that is created is used as the synchronized "gate" through which the number selection process occurs.
+ * This `Actor` `ask`s the internal `Actor` and then waits on that `Future` to resolve.
+ * For the registration process, once it resolves, a number for the accompanying object has been chosen.
+ * The last part involves configuring the `NumberSource` of the hub so that it knows.
+ * For the process of revoking registration, the number from the object is returned to the pool.
+ * Like during the registration process, the `NumberSource` is then also updated.
+ *
+ * The object is always registered using the underlying governed `NumberPool`.
+ * The object will not unregister if the object or its number are not recognized as members previously registered to the `NumberPool`.
+ * Whether or not an object or a specific number has been registered is always possible.
+ * The scope encompasses the whole of the associated `NumberSource` as opposed to just this `NumberPool`.
+ * @param hub the `NumberPoolHub` this `Actor` manipulates
+ * @param pool the specific `NumberPool` this `Actor` maintains
+ * @param poolActor a shared `Actor` that governs this `NumberPool`
+ */
+class NumberPoolAccessorActor(private val hub : NumberPoolHub, private val pool : NumberPool, private val poolActor : ActorRef) extends Actor {
+ //the timeout is for when we ask the poolActor
+ private implicit val timeout = Timeout(50 milliseconds)
+ private[this] val log = org.log4s.getLogger
+
+ private final case class GUIDRequest(obj : IdentifiableEntity, replyTo : ActorRef)
+ private val requestQueue : collection.mutable.LongMap[GUIDRequest] = new collection.mutable.LongMap()
+ private var index : Long = Long.MinValue
+
+ def receive : Receive = {
+ //register
+ case Register(obj, _, None, call) =>
+ try {
+ obj.GUID //stop if object has a GUID; sometimes this happens
+ log.warn(s"$obj already registered")
+ }
+ catch {
+ case _ : Exception =>
+ val id : Long = index
+ index += 1
+ requestQueue += id -> GUIDRequest(obj, call.getOrElse(sender()))
+ poolActor ! NumberPoolActor.GetAnyNumber(Some(id))
+ }
+
+ case Register(obj, _, Some(number), call) =>
+ try {
+ obj.GUID //stop if object has a GUID; sometimes this happens
+ log.warn(s"$obj already registered")
+ }
+ catch {
+ case _ : Exception =>
+ val id : Long = index
+ index += 1
+ requestQueue += id -> GUIDRequest(obj, call.getOrElse(sender()))
+ poolActor ! NumberPoolActor.GetSpecificNumber(number, Some(id))
+ }
+
+ case NumberPoolActor.GiveNumber(number, id) =>
+ id match {
+ case Some(nid : Long) =>
+ Register(nid, requestQueue.remove(nid), number)
+ case _ =>
+ pool.Return(number) //recovery?
+ log.warn(s"received a number but there is no request to process it; returning number to pool")
+ }
+
+ case NumberPoolActor.NoNumber(ex, id) =>
+ val req = id match {
+ case Some(nid : Long) =>
+ val req = requestQueue.remove(nid)
+ if(req.isDefined) { s"$req" } else { s"a corresponding request $nid was not found;" }
+ case _ =>
+ "generic request;" //should be unreachable
+ }
+ log.warn(s"a number was not drawn from the pool; $req $ex")
+
+ //unregister
+ case Unregister(obj, call) =>
+ val callback = call.getOrElse(sender())
+ try {
+ val number = obj.GUID.guid
+ if(pool.Numbers.contains(number) && hub.WhichPool(obj).isDefined) {
+ val id : Long = index
+ index += 1
+ requestQueue += id -> GUIDRequest(obj, callback)
+ poolActor ! NumberPoolActor.ReturnNumber(number, Some(id))
+ }
+ else {
+ callback ! Failure(new Exception(s"the GUID of object $obj - $number - is not a part of this number pool"))
+ }
+ }
+ catch {
+ case msg : Exception =>
+ callback ! Failure(msg)
+ }
+
+ case NumberPoolActor.ReturnNumberResult(number, None, id) =>
+ id match {
+ case Some(nid : Long) =>
+ Unregister(nid, requestQueue.remove(nid), number)
+ case _ =>
+ NumberPoolActor.GetSpecificNumber(pool, number) //recovery?
+ log.error(s"returned a number but there is no request to process it; recovering the number from pool")
+ }
+
+ case NumberPoolActor.ReturnNumberResult(number, ex, id) =>
+ val req = id match {
+ case Some(nid : Long) =>
+ val req = requestQueue.remove(nid)
+ if(req.isDefined) { s"$req" } else { s"a corresponding request $nid was not found;" }
+ case _ =>
+ "generic request;" //should be unreachable
+ }
+ log.warn(s"a number $number was not returned to the pool; $req $ex")
+
+ //common
+ case IsRegistered(Some(obj), None) =>
+ sender ! hub.isRegistered(obj)
+
+ case IsRegistered(None, Some(number)) =>
+ sender ! hub.isRegistered(number)
+
+ case NumberPoolActor.ReturnNumber(number, _) =>
+ sender ! (poolActor ? NumberPoolActor.ReturnNumber(number))
+
+ case msg =>
+ log.warn(s"unexpected message received - $msg")
+ }
+
+ /**
+ * A step of the object registration process.
+ * If there is a successful request object to be found, complete the registration request.
+ * @param id the identifier of this request
+ * @param request the request data
+ * @param number the number that was drawn from the `NumberPool`
+ */
+ private def Register(id : Long, request : Option[GUIDRequest], number : Int) : Unit = {
+ request match {
+ case Some(GUIDRequest(obj, replyTo)) =>
+ processRegisterResult(obj, number, replyTo)
+ case None =>
+ pool.Return(number) //recovery?
+ log.warn(s"received a number but the request for it is missing; returning number to pool")
+ }
+ }
+
+ /**
+ * A step of the object registration process.
+ * This step completes the registration by consulting the `NumberSource`.
+ * @param obj the object
+ * @param number the number to use
+ * @param callback an optional callback `ActorRef`
+ */
+ private def processRegisterResult(obj : IdentifiableEntity, number : Int, callback : ActorRef) : Unit = {
+ try {
+ obj.GUID
+ pool.Return(number) //recovery?
+ callback ! Success(obj)
+ }
+ catch {
+ case _ : Exception =>
+ hub.latterPartRegister(obj, number) match {
+ case Success(_) =>
+ callback ! Success(obj)
+ case Failure(ex) =>
+ pool.Return(number) //recovery?
+ callback ! Failure(ex)
+ }
+ }
+ }
+
+ /**
+ * A step of the object un-registration process.
+ * If there is a successful request object to be found, complete the registration request.
+ * @param id the identifier of this request
+ * @param request the request data
+ * @param number the number that was drawn from the `NumberPool`
+ */
+ private def Unregister(id : Long, request : Option[GUIDRequest], number : Int) : Unit = {
+ request match {
+ case Some(GUIDRequest(obj, replyTo)) =>
+ processUnregisterResult(obj, obj.GUID.guid, replyTo)
+ case None =>
+ NumberPoolActor.GetSpecificNumber(pool, number) //recovery?
+ log.error(s"returned a number but the rest of the request is missing; recovering the number from pool")
+ }
+ }
+
+ /**
+ * A step of the object un-registration process.
+ * This step completes revoking the object's registration by consulting the `NumberSource`.
+ * @param obj the object
+ * @param callback an optional callback `ActorRef`
+ */
+ private def processUnregisterResult(obj : IdentifiableEntity, number : Int, callback : ActorRef) : Unit = {
+ hub.latterPartUnregister(number) match {
+ case Some(_) =>
+ obj.Invalidate()
+ callback ! Success(obj)
+ case None =>
+ NumberPoolActor.GetSpecificNumber(pool, number) //recovery?
+ callback ! Failure(new Exception(s"failed to unregister a number; this may be a critical error"))
+ }
+ }
+}
diff --git a/common/src/main/scala/net/psforever/objects/guid/actor/NumberPoolActor.scala b/common/src/main/scala/net/psforever/objects/guid/actor/NumberPoolActor.scala
new file mode 100644
index 000000000..961360470
--- /dev/null
+++ b/common/src/main/scala/net/psforever/objects/guid/actor/NumberPoolActor.scala
@@ -0,0 +1,96 @@
+// Copyright (c) 2017 PSForever
+package net.psforever.objects.guid.actor
+
+import akka.actor.Actor
+import net.psforever.objects.guid.pool.NumberPool
+import net.psforever.objects.guid.selector.{NumberSelector, SpecificSelector}
+
+import scala.util.{Failure, Success, Try}
+
+/**
+ * An `Actor` that wraps around a `NumberPool` and regulates access to it.
+ *
+ * Wrapping around the pool like this forces a FIFO order to requests for numbers from the pool.
+ * This synchronization only lasts as long as this `Actor` is the only one for the given pool.
+ * In the distribution of globaly unique identifiers, this is extremely important.
+ * `NumberPool`s are used as the primary determination of whether a number is available at any given moment.
+ * The categorization of the pool is also important, though for a contextually-sensitive reason.
+ * @param pool the `NumberPool` being manipulated
+ */
+class NumberPoolActor(pool : NumberPool) extends Actor {
+ private[this] val log = org.log4s.getLogger
+
+ def receive : Receive = {
+ case NumberPoolActor.GetAnyNumber(id) =>
+ sender ! (pool.Get() match {
+ case Success(value) =>
+ NumberPoolActor.GiveNumber(value, id)
+ case Failure(ex) => ;
+ NumberPoolActor.NoNumber(ex, id)
+ })
+
+ case NumberPoolActor.GetSpecificNumber(number, id) =>
+ sender ! (NumberPoolActor.GetSpecificNumber(pool, number) match {
+ case Success(value) =>
+ NumberPoolActor.GiveNumber(value, id)
+ case Failure(ex) => ;
+ NumberPoolActor.NoNumber(ex, id)
+ })
+
+ case NumberPoolActor.ReturnNumber(number, id) =>
+ val result = pool.Return(number)
+ val ex : Option[Throwable] = if(!result) { Some(new Exception("number was not returned")) } else { None }
+ sender ! NumberPoolActor.ReturnNumberResult(number, ex, id)
+
+ case msg =>
+ log.info(s"received an unexpected message - ${msg.toString}")
+ }
+}
+
+object NumberPoolActor {
+ /**
+ * A message to invoke the current `NumberSelector`'s functionality.
+ * @param id a potential identifier to associate this request
+ */
+ final case class GetAnyNumber(id : Option[Any] = None)
+
+ /**
+ * A message to invoke a `SpecificSelector` to acquire the specific `number`, if it is available in this pool.
+ * @param number the pre-selected number
+ * @param id a potential identifier to associate this request
+ */
+ final case class GetSpecificNumber(number : Int, id : Option[Any] = None)
+
+ /**
+ * A message to distribute the `number` that was drawn.
+ * @param number the pre-selected number
+ * @param id a potential identifier to associate this request
+ */
+ final case class GiveNumber(number : Int, id : Option[Any] = None)
+
+ final case class NoNumber(ex : Throwable, id : Option[Any] = None)
+
+ /**
+ * A message to invoke the `Return` functionality of the current `NumberSelector`.
+ * @param number the number
+ */
+ final case class ReturnNumber(number : Int, id : Option[Any] = None)
+
+ final case class ReturnNumberResult(number : Int, ex : Option[Throwable], id : Option[Any] = None)
+
+ /**
+ * Use the `SpecificSelector` on this pool to extract a specific object from the pool, if it is included and available.
+ * @param pool the `NumberPool` to draw from
+ * @param number the number requested
+ * @return the number requested, or an error
+ */
+ def GetSpecificNumber(pool : NumberPool, number : Int) : Try[Int] = {
+ val original : NumberSelector = pool.Selector
+ val specific : SpecificSelector = new SpecificSelector
+ specific.SelectionIndex = pool.Numbers.indexOf(number)
+ pool.Selector = specific
+ val out : Try[Int] = pool.Get()
+ pool.Selector = original
+ out
+ }
+}
diff --git a/common/src/main/scala/net/psforever/objects/guid/actor/NumberPoolHubActor.scala b/common/src/main/scala/net/psforever/objects/guid/actor/NumberPoolHubActor.scala
new file mode 100644
index 000000000..d1a9af128
--- /dev/null
+++ b/common/src/main/scala/net/psforever/objects/guid/actor/NumberPoolHubActor.scala
@@ -0,0 +1,212 @@
+// Copyright (c) 2017 PSForever
+package net.psforever.objects.guid.actor
+
+import akka.pattern.ask
+import akka.util.Timeout
+import akka.actor.{Actor, ActorRef, Props}
+
+import net.psforever.objects.entity.IdentifiableEntity
+import net.psforever.objects.guid.NumberPoolHub
+import net.psforever.objects.guid.pool.NumberPool
+
+import scala.collection.mutable
+import scala.concurrent.Future
+import scala.concurrent.duration._
+import scala.util.{Failure, Success, Try}
+
+/**
+ * An incoming message for retrieving a specific `NumberPoolAccessorActor`.
+ * @param name the name of the accessor's `NumberPool`
+ */
+final case class RequestPoolActor(name : String)
+
+/**
+ * An outgoing message for giving a specific `NumberPoolAccessorActor`.
+ * @param name the name of the accessor's `NumberPool`, for reference
+ * @param actor the accessor
+ */
+final case class DeliverPoolActor(name : String, actor : ActorRef)
+
+/**
+ * An `Actor` that wraps around the management system for `NumberPools`.
+ *
+ * By just instantiating, this object builds and stores a `NumberPoolAccessorActor` for each `NumberPool` known to the `hub`.
+ * Additional `NumberPool`s created by the `hub` need to be paired with a created accessor manually.
+ * Each accessor is the primary entry point to a registration process for the specific `NumberPool` it represents.
+ * The `hub` `Actor` itself distribute any registration task it receives out to an applicable accessor of which it is aware.
+ * It will attempt to revoke registration on its own, without relying on the functionality from any accessor.
+ *
+ * In the same way that `NumberPoolHub` is a tool for keeping track of `NumberPool` objects,
+ * its `Actor` is a tool for keeping track of accessors created from `NumberPool` objects.
+ * It is very, however, for handling unspecific revoke tasks.
+ * @param hub the central `NumberPool` management object for an embedded `NumberSource` object
+ */
+class NumberPoolHubActor(private val hub : NumberPoolHub) extends Actor {
+ private val actorHash : mutable.HashMap[String, ActorRef] = mutable.HashMap[String, ActorRef]()
+ hub.Pools.foreach({ case(name, pool) => CreatePoolActor(name, pool) })
+ implicit val timeout = Timeout(50 milliseconds)
+ private[this] val log = org.log4s.getLogger
+
+ def receive : Receive = {
+ case RequestPoolActor(name) =>
+ sender ! (GetPoolActor(name) match {
+ case Success(poolActor) =>
+ DeliverPoolActor(name, poolActor)
+ case Failure(ex) =>
+ Failure(ex)
+ })
+
+ case Register(obj, name, None, callback) =>
+ HubRegister(obj, name, callback)
+
+ case Register(obj, name, Some(number), callback) =>
+ HubRegister(obj, name, number, callback)
+
+ //common
+ case IsRegistered(Some(obj), None) =>
+ sender ! hub.isRegistered(obj)
+
+ case IsRegistered(None, Some(number)) =>
+ sender ! hub.isRegistered(number)
+
+ case Unregister(obj, callback) =>
+ Unregister(obj, if(callback.isEmpty) { sender } else { callback.get })
+
+ case msg =>
+ log.warn(s"unexpected message received - ${msg.toString}")
+ }
+
+ /**
+ * From a name, find an existing `NumberPoolAccessorActor`.
+ * @param name the accessor's name
+ * @return the accessor that was requested
+ */
+ private def GetPoolActor(name : String) : Try[ActorRef] = {
+ actorHash.get(name) match {
+ case Some(actor) =>
+ Success(actor)
+ case _ =>
+ Failure(new Exception(s"number pool $name not defined"))
+ }
+ }
+
+ /**
+ * Create a new `NumberPoolAccessorActor` and add it to the local collection of accessors.
+ * @param name the accessor's name
+ * @param pool the underlying `NumberPool`
+ */
+ private def CreatePoolActor(name : String, pool : NumberPool) : Unit = {
+ actorHash.get(name) match {
+ case None =>
+ actorHash += name -> context.actorOf(Props(classOf[NumberPoolAccessorActor], hub, pool), s"${name}Actor")
+ case Some(_) =>
+ //TODO complain?
+ }
+ }
+
+ /**
+ * A step of the object registration process.
+ * Select a valid `NumberPoolAccessorActor` and pass a task onto it.
+ * @param obj an object
+ * @param name a potential accessor pool
+ * @param callback an optional callback `ActorRef`
+ */
+ private def HubRegister(obj : IdentifiableEntity, name : Option[String], callback : Option[ActorRef]) : Unit = {
+ val genericPool = actorHash("generic")
+ val pool = if(name.isDefined) { actorHash.get(name.get).orElse(Some(genericPool)).get } else { genericPool }
+ pool ! Register(obj, None, None, callback)
+ }
+
+ /**
+ * A step of the object registration process.
+ * Determine to which `NumberPool` the `number` belongs.
+ * @param obj an object
+ * @param name a potential accessor pool
+ * @param number a potential number
+ * @param callback an optional callback `ActorRef`
+ */
+ private def HubRegister(obj : IdentifiableEntity, name : Option[String], number : Int, callback : Option[ActorRef]) : Unit = {
+ hub.WhichPool(number) match {
+ case Some(poolname) =>
+ HubRegister_GetActor(obj, name, poolname, number, callback)
+ case None =>
+ self ! Register(obj, name, None, callback)
+ }
+ }
+
+ /**
+ * A step of the object registration process.
+ * Pass a task onto an accessor or, if the accessor can not be found, attempt to recover.
+ * @param obj an object
+ * @param name a potential accessor pool
+ * @param poolname the suggested accessor pool
+ * @param number a potential number
+ * @param callback an optional callback `ActorRef`
+ */
+ private def HubRegister_GetActor(obj : IdentifiableEntity, name : Option[String], poolname : String, number : Int, callback : Option[ActorRef]) : Unit = {
+ actorHash.get(poolname) match {
+ case Some(pool) =>
+ pool ! Register(obj, None, Some(number), callback)
+ case None =>
+ HubRegister_MissingActor(obj, name, poolname, number, callback)
+ }
+ }
+
+ /**
+ * A step of the object registration process.
+ * If an accessor could not be found in the last step, attempt to create the accessor.
+ * If the accessor can not be created, the `number` can not be used;
+ * fall back on the original pool (`name`).
+ * @param obj an object
+ * @param name a potential accessor pool
+ * @param poolname the suggested accessor pool
+ * @param number a potential number
+ * @param callback an optional callback `ActorRef`
+ */
+ private def HubRegister_MissingActor(obj : IdentifiableEntity, name : Option[String], poolname : String, number : Int, callback : Option[ActorRef]) : Unit = {
+ hub.GetPool(poolname) match {
+ case Some(pool) =>
+ CreatePoolActor(poolname, pool)
+ actorHash(poolname) ! Register(obj, None, Some(number), callback)
+ case None =>
+ log.error(s"matched number $number to pool $poolname, but could not find $poolname when asked")
+ self ! Register(obj, name, None, callback)
+ }
+ }
+
+ /**
+ * A step of the object un-registration process.
+ * This step locates the `NumberPool` to which this object is a member.
+ * If found, it prepares a `Future` to resolve later regarding whether the `NumberPool` accepted the number.
+ * @param obj the object
+ * @param callback a callback `ActorRef`
+ */
+ private def Unregister(obj : IdentifiableEntity, callback : ActorRef) : Unit = {
+ hub.WhichPool(obj) match {
+ case Some(name) =>
+ val objToUnregister = obj
+ val poolName = name
+ processUnregisterResult(objToUnregister, (actorHash(poolName) ? NumberPoolActor.ReturnNumber(objToUnregister.GUID.guid)).mapTo[Boolean], callback)
+ case None =>
+ callback ! UnregisterFailure(obj, new Exception("could not find pool object is member of"))
+ }
+ }
+
+ /**
+ * A step of the object un-registration process.
+ * This step completes revoking the object's registration by consulting the `NumberSource`.
+ * @param obj the object
+ * @param result whether the number was returned in the last step
+ * @param callback a callback `ActorRef`
+ */
+ private def processUnregisterResult(obj : IdentifiableEntity, result : Future[Boolean], callback : ActorRef) : Unit = {
+ import scala.concurrent.ExecutionContext.Implicits.global
+ result.foreach {
+ case true =>
+ hub.latterPartUnregister(obj.GUID.guid)
+ callback ! UnregisterSuccess(obj)
+ case false =>
+ callback ! UnregisterFailure(obj, new Exception("could not find object to remove"))
+ }
+ }
+}
diff --git a/common/src/main/scala/net/psforever/objects/guid/actor/Register.scala b/common/src/main/scala/net/psforever/objects/guid/actor/Register.scala
new file mode 100644
index 000000000..3ba4807b9
--- /dev/null
+++ b/common/src/main/scala/net/psforever/objects/guid/actor/Register.scala
@@ -0,0 +1,80 @@
+// Copyright (c) 2017 PSForever
+package net.psforever.objects.guid.actor
+
+import akka.actor.ActorRef
+import net.psforever.objects.entity.IdentifiableEntity
+
+/**
+ * A message for accepting object-number registration requests.
+ *
+ * The callback is actually an `ActorRef` to which a `RegisterSuccess` message or a `RegisterFailure` message is sent.
+ * This is as opposed to what a "callback" is normally - a function.
+ * @param obj the mandatory object
+ * @param name the optional name of the number pool to which this object is registered
+ * @param number the optional number pre-selected for registering this object
+ * @param callback the optional custom callback for the messages from the success or failure conditions
+ */
+final case class Register(obj : IdentifiableEntity, name : Option[String], number : Option[Int], callback : Option[ActorRef])
+
+object Register {
+ /**
+ * Overloaded constructor, accepting just the object.
+ * @param obj the object to be registered
+ * @return a `Register` object
+ */
+ def apply(obj : IdentifiableEntity) : Register = {
+ new Register(obj, None, None, None)
+ }
+
+ /**
+ * Overloaded constructor, accepting the object and a callback.
+ * @param obj the object to be registered
+ * @param callback the custom callback for the messages from the success or failure conditions
+ * @return a `Register` object
+ */
+ def apply(obj : IdentifiableEntity, callback : ActorRef) : Register = {
+ new Register(obj, None, None, Some(callback))
+ }
+
+ /**
+ * Overloaded constructor, accepting an object and a pre-selected number.
+ * @param obj the object to be registered
+ * @param number the pre-selected number
+ * @return a `Register` object
+ */
+ def apply(obj : IdentifiableEntity, number : Int) : Register = {
+ new Register(obj, None, Some(number), None)
+ }
+
+ /**
+ * Overloaded constructor, accepting an object, a pre-selected number, and a callback.
+ * @param obj the object to be registered
+ * @param number the pre-selected number
+ * @param callback the custom callback for the messages from the success or failure conditions
+ * @return a `Register` object
+ */
+ def apply(obj : IdentifiableEntity, number : Int, callback : ActorRef) : Register = {
+ new Register(obj, None, Some(number), Some(callback))
+ }
+
+ /**
+ * Overloaded constructor, accepting an object and a number pool.
+ * @param obj the object to be registered
+ * @param name the number pool name
+ * @return a `Register` object
+ */
+ def apply(obj : IdentifiableEntity, name : String) : Register = {
+ new Register(obj, Some(name), None, None)
+ }
+
+ /**
+ * Overloaded constructor, accepting an object, a number pool, and a callback.
+ * @param obj the object to be registered
+ * @param name the number pool name
+ * @param callback the custom callback for the messages from the success or failure conditions
+ * @return a `Register` object
+ */
+ def apply(obj : IdentifiableEntity, name : String, callback : ActorRef) : Register = {
+ new Register(obj, Some(name), None, Some(callback))
+ }
+}
diff --git a/common/src/main/scala/net/psforever/objects/guid/actor/Unregister.scala b/common/src/main/scala/net/psforever/objects/guid/actor/Unregister.scala
new file mode 100644
index 000000000..1258d998a
--- /dev/null
+++ b/common/src/main/scala/net/psforever/objects/guid/actor/Unregister.scala
@@ -0,0 +1,23 @@
+// Copyright (c) 2017 PSForever
+package net.psforever.objects.guid.actor
+
+import akka.actor.ActorRef
+import net.psforever.objects.entity.IdentifiableEntity
+
+/**
+ * A message for accepting object-number unregistration requests.
+ * When given to a number pool (`NumberPoolAccessorActor`), that `Actor` assumes itself to have the object.
+ * When given to a hub object (`NumberPoolHubActor`), it will attempt to determine which pool currently has the object.
+ *
+ * The callback is actually an `ActorRef` to which a `RegisterSuccess` message or a `RegisterFailure` message is sent.
+ * This is as opposed to what a "callback" is normally - a function.
+ * @param obj the mandatory object
+ * @param callback the optional custom callback for the messages from the success or failure conditions
+ */
+final case class Unregister(obj : IdentifiableEntity, callback : Option[ActorRef] = None)
+
+object Unregister {
+ def apply(obj : IdentifiableEntity, callback : ActorRef) : Unregister = {
+ Unregister(obj, Some(callback))
+ }
+}
diff --git a/common/src/main/scala/net/psforever/objects/guid/actor/UnregisterFailure.scala b/common/src/main/scala/net/psforever/objects/guid/actor/UnregisterFailure.scala
new file mode 100644
index 000000000..60eb0ce3c
--- /dev/null
+++ b/common/src/main/scala/net/psforever/objects/guid/actor/UnregisterFailure.scala
@@ -0,0 +1,11 @@
+// Copyright (c) 2017 PSForever
+package net.psforever.objects.guid.actor
+
+import net.psforever.objects.entity.IdentifiableEntity
+
+/**
+ * A message for when an object has failed to be unregistered for some reason.
+ * @param obj the object
+ * @param ex the reason that the registration process failed
+ */
+final case class UnregisterFailure(obj : IdentifiableEntity, ex : Throwable)
diff --git a/common/src/main/scala/net/psforever/objects/guid/actor/UnregisterSuccess.scala b/common/src/main/scala/net/psforever/objects/guid/actor/UnregisterSuccess.scala
new file mode 100644
index 000000000..603de46ad
--- /dev/null
+++ b/common/src/main/scala/net/psforever/objects/guid/actor/UnregisterSuccess.scala
@@ -0,0 +1,11 @@
+// Copyright (c) 2017 PSForever
+package net.psforever.objects.guid.actor
+
+import net.psforever.objects.entity.IdentifiableEntity
+
+/**
+ * A message for when an object has been unregistered.
+ * @param obj the object
+ */
+final case class UnregisterSuccess(obj : IdentifiableEntity)
+
diff --git a/common/src/main/scala/net/psforever/objects/guid/key/LoanedKey.scala b/common/src/main/scala/net/psforever/objects/guid/key/LoanedKey.scala
new file mode 100644
index 000000000..28901abe7
--- /dev/null
+++ b/common/src/main/scala/net/psforever/objects/guid/key/LoanedKey.scala
@@ -0,0 +1,46 @@
+// Copyright (c) 2017 PSForever
+package net.psforever.objects.guid.key
+
+import net.psforever.objects.entity.IdentifiableEntity
+import net.psforever.objects.guid.AvailabilityPolicy
+
+/**
+ * The only indirect public access a queued number monitor object (`Key`) is allowed.
+ * @param guid the GUID represented by this indirect key
+ * @param key a private reference to the original key
+ */
+class LoanedKey(private val guid : Int, private val key : Monitor) {
+ def GUID : Int = guid
+
+ def Policy : AvailabilityPolicy.Value = key.Policy
+
+ def Object : Option[IdentifiableEntity] = key.Object
+
+ /**
+ * na
+ * @param obj the object that should hold this GUID
+ * @return `true`, if the assignment worked; `false`, otherwise
+ */
+ def Object_=(obj : IdentifiableEntity) : Option[IdentifiableEntity] = Object_=(Some(obj))
+
+ /**
+ * na
+ * @param obj the object that should hold this GUID
+ * @return `true`, if the assignment worked; `false`, otherwise
+ */
+ def Object_=(obj : Option[IdentifiableEntity]) : Option[IdentifiableEntity] = {
+ if(key.Policy == AvailabilityPolicy.Leased || (key.Policy == AvailabilityPolicy.Restricted && key.Object.isEmpty)) {
+ if(key.Object.isDefined) {
+ key.Object.get.Invalidate()
+ key.Object = None
+ }
+ key.Object = obj
+ if(obj.isDefined) {
+ import net.psforever.packet.game.PlanetSideGUID
+ obj.get.GUID = PlanetSideGUID(guid)
+ }
+ }
+ key.Object
+ }
+}
+
diff --git a/common/src/main/scala/net/psforever/objects/guid/key/Monitor.scala b/common/src/main/scala/net/psforever/objects/guid/key/Monitor.scala
new file mode 100644
index 000000000..61d4cda17
--- /dev/null
+++ b/common/src/main/scala/net/psforever/objects/guid/key/Monitor.scala
@@ -0,0 +1,13 @@
+// Copyright (c) 2017 PSForever
+package net.psforever.objects.guid.key
+
+import net.psforever.objects.entity.IdentifiableEntity
+import net.psforever.objects.guid.AvailabilityPolicy
+
+trait Monitor {
+ def Policy : AvailabilityPolicy.Value
+
+ def Object : Option[IdentifiableEntity]
+
+ def Object_=(objct : Option[IdentifiableEntity]) : Option[IdentifiableEntity]
+}
diff --git a/common/src/main/scala/net/psforever/objects/guid/key/SecureKey.scala b/common/src/main/scala/net/psforever/objects/guid/key/SecureKey.scala
new file mode 100644
index 000000000..694df2575
--- /dev/null
+++ b/common/src/main/scala/net/psforever/objects/guid/key/SecureKey.scala
@@ -0,0 +1,18 @@
+// Copyright (c) 2017 PSForever
+package net.psforever.objects.guid.key
+
+import net.psforever.objects.guid.AvailabilityPolicy
+
+/**
+ * An unmodifiable reference to an active number monitor object (`Key`).
+ * @param guid the number (globally unique identifier)
+ * @param key a reference to the monitor
+ */
+final class SecureKey(private val guid : Int, private val key : Monitor) {
+ def GUID : Int = guid
+
+ def Policy : AvailabilityPolicy.Value = key.Policy
+
+ import net.psforever.objects.entity.IdentifiableEntity
+ def Object : Option[IdentifiableEntity] = key.Object
+}
\ No newline at end of file
diff --git a/common/src/main/scala/net/psforever/objects/guid/misc/AscendingNumberSource.scala b/common/src/main/scala/net/psforever/objects/guid/misc/AscendingNumberSource.scala
new file mode 100644
index 000000000..c4534faba
--- /dev/null
+++ b/common/src/main/scala/net/psforever/objects/guid/misc/AscendingNumberSource.scala
@@ -0,0 +1,32 @@
+// Copyright (c) 2017 PSForever
+package net.psforever.objects.guid.misc
+
+/**
+ * This class is just a proof of concept model of a self-contained system.
+ */
+class AscendingNumberSource {
+ val pool : Array[Int] = Array.ofDim[Int](65536)
+ (0 to 65535).foreach(x => { pool(x) = x })
+ var head : Int = 0
+
+ def Get() : Int = {
+ val start : Int = head
+ if(pool(head) == -1) {
+ do {
+ head = (head + 1) % pool.length
+ }
+ while(pool(head) == -1 && head != start)
+ }
+ if(head == start) {
+ import net.psforever.objects.entity.NoGUIDException
+ throw NoGUIDException("no unused numbers available")
+ }
+ val outNumber : Int = head
+ pool(head) = -1
+ outNumber
+ }
+
+ def Return(number : Int) : Unit = {
+ pool(number) = number
+ }
+}
diff --git a/common/src/main/scala/net/psforever/objects/guid/misc/RegistrationTaskResolver.scala b/common/src/main/scala/net/psforever/objects/guid/misc/RegistrationTaskResolver.scala
new file mode 100644
index 000000000..f45d7e942
--- /dev/null
+++ b/common/src/main/scala/net/psforever/objects/guid/misc/RegistrationTaskResolver.scala
@@ -0,0 +1,137 @@
+// Copyright (c) 2017 PSForever
+package net.psforever.objects.guid.misc
+
+import java.util.concurrent.TimeoutException
+
+import akka.actor.{Actor, ActorRef, Cancellable}
+import net.psforever.objects.entity.IdentifiableEntity
+
+import scala.annotation.tailrec
+import scala.concurrent.ExecutionContext.Implicits.global
+import scala.concurrent.duration._
+import scala.util.{Failure, Success, Try}
+
+/**
+ * Accept a task in waiting and series of lesser tasks that complete the provided primary task.
+ * Receive periodic updates on the states of the lesser tasks and, when these sub-tasks have been accomplished,
+ * declare the primary task accomplished as well.
+ *
+ * This ia admittedly a simplistic model of task resolution, currently, and is rather specific and limited.
+ * Generalizing and expanding on this class in the future might be beneficial.
+ * @param obj the primary task
+ * @param list a series of sub-tasks that need to be completed before the pimrary task can be completed
+ * @param callback where to report about the pirmary task having succeeded or failed
+ * @param timeoutDuration a delay during which sub-tasks are permitted to be accomplished;
+ * after this grave period is over, the task has failed
+ */
+class RegistrationTaskResolver[T <: IdentifiableEntity](private val obj : T, private val list : List[T], callback : ActorRef, timeoutDuration : FiniteDuration) extends Actor {
+ /** sub-tasks that contribute to completion of the task */
+ private val checklist : Array[Boolean] = Array.fill[Boolean](list.length)(false)
+ /** whether or not it matters that sub-tasks are coming in */
+ private var valid : Boolean = true
+ /** declares when the task has taken too long to complete */
+ private val taskTimeout : Cancellable = context.system.scheduler.scheduleOnce(timeoutDuration, self, Failure(new TimeoutException(s"a task for $obj has timed out")))
+ private[this] val log = org.log4s.getLogger
+ ConfirmTask(Success(true)) //check for auto-completion
+
+ def receive : Receive = {
+ case Success(objn)=>
+ ConfirmTask(ConfirmSubtask(objn.asInstanceOf[T]))
+
+ case Failure(ex)=>
+ FailedTask(ex)
+
+ case msg =>
+ log.warn(s"unexpected message received - ${msg.toString}")
+ }
+
+ /**
+ * If this object is still accepting task resolutions, determine if that sub-task can be checked off.
+ * @param objn the sub-task entry
+ * @return a successful pass or a failure if the task can't be found;
+ * a "successful failure" if task resolutions are no longer accepted
+ */
+ private def ConfirmSubtask(objn : T) : Try[Boolean] = {
+ if(valid) {
+ if(MatchSubtask(objn, list.iterator)) {
+ Success(true)
+ }
+ else {
+ Failure(new Exception(s"can not find a subtask to check off - ${objn.toString}"))
+ }
+ }
+ else {
+ Success(false)
+ }
+ }
+
+ /**
+ * Find a sub-task from a `List` of sub-tasks and mark it as completed, if found.
+ * @param objn the sub-task entry
+ * @param iter_list an `Iterator` to the list of sub-tasks
+ * @param index the index of this entry;
+ * defaults to zero
+ * @return whether or not the subtask has been marked as completed
+ */
+ @tailrec private def MatchSubtask(objn : T, iter_list : Iterator[T], index : Int = 0) : Boolean = {
+ if(!iter_list.hasNext) {
+ false
+ }
+ else {
+ val subtask = iter_list.next
+ if(subtask.equals(objn)) {
+ checklist(index) = true
+ true
+ }
+ else {
+ MatchSubtask(objn, iter_list, index + 1)
+ }
+ }
+ }
+
+ /**
+ * Determine whether all sub-tasks have been completed successfully.
+ * If so, complete the primary task.
+ * @param subtaskComplete the status of the recent sub-task confirmation that triggered this confirmation request
+ */
+ private def ConfirmTask(subtaskComplete : Try[Boolean]) : Unit = {
+ if(valid) {
+ subtaskComplete match {
+ case Success(true) =>
+ if(!checklist.contains(false)) {
+ FulfillTask()
+ }
+ case Success(false) =>
+ log.warn(s"when checking a task for ${obj.toString}, arrived at a state where we previously failed a subtask but main task still valid")
+ case Failure(ex) =>
+ FailedTask(ex)
+ }
+ }
+ }
+
+ /**
+ * All sub-tasks have been completed; the main task can also be completed.
+ * Alert interested parties that the task is performed successfully.
+ * Stop as soon as possible.
+ */
+ private def FulfillTask() : Unit = {
+ valid = false
+ callback ! Success(obj)
+ taskTimeout.cancel()
+ context.stop(self)
+ }
+
+ /**
+ * The main task can not be completed.
+ * Clean up as much as possible and alert interested parties that the task has been dropped.
+ * Let this `Actor` stop gracefully.
+ * @param ex why the main task can not be completed
+ */
+ private def FailedTask(ex : Throwable) : Unit = {
+ valid = false
+ callback ! Failure(ex)
+ taskTimeout.cancel()
+ import akka.pattern.gracefulStop
+ gracefulStop(self, 2 seconds) //give time for any other messages; avoid dead letters
+ }
+}
diff --git a/common/src/main/scala/net/psforever/objects/guid/pool/ExclusivePool.scala b/common/src/main/scala/net/psforever/objects/guid/pool/ExclusivePool.scala
new file mode 100644
index 000000000..93081c545
--- /dev/null
+++ b/common/src/main/scala/net/psforever/objects/guid/pool/ExclusivePool.scala
@@ -0,0 +1,33 @@
+// Copyright (c) 2017 PSForever
+package net.psforever.objects.guid.pool
+
+import net.psforever.objects.guid.selector.NumberSelector
+
+import scala.util.{Failure, Success, Try}
+
+class ExclusivePool(numbers : List[Int]) extends SimplePool(numbers) {
+ private val pool : Array[Int] = Array.ofDim[Int](numbers.length)
+ numbers.indices.foreach(i => { pool(i) = i })
+
+ override def Count : Int = pool.count(value => value == -1)
+
+ override def Selector_=(slctr : NumberSelector) : Unit = {
+ super.Selector_=(slctr)
+ slctr.Format(pool)
+ }
+
+ override def Get() : Try[Int] = {
+ val index : Int = Selector.Get(pool)
+ if(index == -1) {
+ Failure(new Exception("there are no numbers available in the pool"))
+ }
+ else {
+ Success(numbers(index))
+ }
+ }
+
+ override def Return(number : Int) : Boolean = {
+ val index = Numbers.indexOf(number)
+ index != -1 && Selector.Return(index, pool)
+ }
+}
diff --git a/common/src/main/scala/net/psforever/objects/guid/pool/GenericPool.scala b/common/src/main/scala/net/psforever/objects/guid/pool/GenericPool.scala
new file mode 100644
index 000000000..9a4b1aa24
--- /dev/null
+++ b/common/src/main/scala/net/psforever/objects/guid/pool/GenericPool.scala
@@ -0,0 +1,89 @@
+// Copyright (c) 2017 PSForever
+package net.psforever.objects.guid.pool
+
+import net.psforever.objects.guid.selector.{NumberSelector, SpecificSelector}
+
+import scala.collection.mutable
+import scala.util.{Failure, Success, Try}
+
+class GenericPool(private val hub : mutable.LongMap[String], private val max : Int) extends NumberPool {
+ val numbers : mutable.ListBuffer[Int] = mutable.ListBuffer[Int]()
+ private val selector : SpecificSelector = new SpecificSelector
+ selector.SelectionIndex = -1
+
+ def Numbers : List[Int] = numbers.toList
+
+ def Count : Int = numbers.length
+
+ def Selector : NumberSelector = selector
+
+ def Selector_=(slctr : NumberSelector) : Unit = { } //intentionally blank
+
+ def Get() : Try[Int] = {
+ val specific = selector.SelectionIndex
+ selector.SelectionIndex = -1 //clear
+ if(specific == -1) {
+ val number = GenericPool.rand(hub.keys.toList, max)
+ hub += number.toLong -> "generic"
+ numbers += number
+ Success(number)
+ }
+ else if(hub.get(specific).isEmpty) {
+ hub += specific.toLong -> "generic"
+ numbers += specific
+ Success(specific)
+ }
+ else {
+ Failure(new Exception("selector was not initialized properly, or no numbers available in the pool"))
+ }
+ }
+
+ def Return(number : Int) : Boolean = {
+ val index : Int = numbers.indexOf(number)
+ if(index > -1) {
+ numbers.remove(index)
+ hub -= number
+ true
+ }
+ else {
+ false
+ }
+ }
+}
+
+object GenericPool {
+ /**
+ * Get some number that is not accounted for in any other fixed pool, making it available in this generic one.
+ *
+ * Although called "`rand`," this algorithm is not actually random.
+ * From a sorted list of numbers, with a minimum and a maximum value appended,
+ * it finds the two adjacent numbers that are the most distant.
+ * It finds an average whole integer number between the two.
+ *
+ * This solution gets expensive as the count of numbers in `list` increases.
+ * @param list all of the non-repeating numbers to be compared
+ * @param domainSize how many numbers can be supported
+ * @return midpoint of the largest distance between any two of the existing numbers, or -1
+ */
+ private def rand(list : List[Long], domainSize : Int) : Int = {
+ if(list.size < domainSize) {
+ //get a list of all assigned numbers with an appended min and max
+ val sortedList : List[Long] = -1L +: list.sorted :+ domainSize.toLong
+ //compare the delta between every two entries and find the start of that greatest delta comparison
+ var maxDelta : Long = -1
+ var maxDeltaIndex = -1
+ for(index <- 0 until (sortedList.length - 1)) {
+ val curr = sortedList(index + 1) - sortedList(index)
+ if(curr > maxDelta) {
+ maxDelta = curr
+ maxDeltaIndex = index
+ }
+ }
+ //find half of the distance between the two numbers with the greatest delta value
+ if(maxDelta > 1) { ((sortedList(maxDeltaIndex + 1) + sortedList(maxDeltaIndex)) / 2f).toInt } else { -1 }
+ }
+ else {
+ -1
+ }
+ }
+}
diff --git a/common/src/main/scala/net/psforever/objects/guid/pool/NumberPool.scala b/common/src/main/scala/net/psforever/objects/guid/pool/NumberPool.scala
new file mode 100644
index 000000000..c054b3b72
--- /dev/null
+++ b/common/src/main/scala/net/psforever/objects/guid/pool/NumberPool.scala
@@ -0,0 +1,20 @@
+// Copyright (c) 2017 PSForever
+package net.psforever.objects.guid.pool
+
+import net.psforever.objects.guid.selector.NumberSelector
+
+import scala.util.Try
+
+trait NumberPool {
+ def Numbers : List[Int]
+
+ def Count : Int
+
+ def Selector : NumberSelector
+
+ def Selector_=(slctr : NumberSelector) : Unit
+
+ def Get() : Try[Int]
+
+ def Return(number : Int) : Boolean
+}
diff --git a/common/src/main/scala/net/psforever/objects/guid/pool/SimplePool.scala b/common/src/main/scala/net/psforever/objects/guid/pool/SimplePool.scala
new file mode 100644
index 000000000..1b30ed4aa
--- /dev/null
+++ b/common/src/main/scala/net/psforever/objects/guid/pool/SimplePool.scala
@@ -0,0 +1,35 @@
+// Copyright (c) 2017 PSForever
+package net.psforever.objects.guid.pool
+
+import net.psforever.objects.guid.selector.{NumberSelector, StrictInOrderSelector}
+
+import scala.util.{Success, Try}
+
+class SimplePool(private val numbers : List[Int]) extends NumberPool {
+ if(numbers.count(_ < 0) > 0) {
+ throw new IllegalArgumentException("negative numbers not allowed in number pool")
+ }
+ else if (numbers.length != numbers.toSet.size) {
+ throw new IllegalArgumentException("duplicate numbers not allowed in number pool")
+ }
+ private var selector : NumberSelector = new StrictInOrderSelector
+
+ def Numbers : List[Int] = numbers
+
+ def Count : Int = 0
+
+ def Selector : NumberSelector = selector
+
+ def Selector_=(slctr : NumberSelector) : Unit = {
+ selector = slctr
+ }
+
+ def Get() : Try[Int] = {
+ val ary = numbers.indices.toArray
+ val index = selector.Get(ary)
+ selector.Return(index, ary) //reset, for the benefit of the selector
+ Success(numbers(index))
+ }
+
+ def Return(number : Int) : Boolean = numbers.indexOf(number) > -1
+}
diff --git a/common/src/main/scala/net/psforever/objects/guid/selector/NumberSelector.scala b/common/src/main/scala/net/psforever/objects/guid/selector/NumberSelector.scala
new file mode 100644
index 000000000..d414561d2
--- /dev/null
+++ b/common/src/main/scala/net/psforever/objects/guid/selector/NumberSelector.scala
@@ -0,0 +1,80 @@
+// Copyright (c) 2017 PSForever
+package net.psforever.objects.guid.selector
+
+/**
+ * The base class for all different sorts of number selection policies.
+ *
+ * The `Array`s called out as method parameters is always an `Array` of indexes for some other list.
+ * The indices in the `Array` are always the complete range of 0 to `n` numbers.
+ * It is recommended to initialize the `Array` with the rule `array(number) = number`.
+ * When they need to be flagged as "invalid" in some way, use some consistent system of negative numbers.
+ * (Recommendation: unless doing something fancy, just use -1.)
+ */
+abstract class NumberSelector {
+ /** The index for the selector when performing a number selection action, then modified to the "next" index. */
+ protected var selectionIndex : Int = 0
+ /** The index for the selector when performing a number return action, then modified for the "next" index. */
+ protected var ret : Int = 0
+
+ def SelectionIndex : Int = selectionIndex
+
+ def ReturnIndex : Int = ret
+
+ /**
+ * Accept a provided `pool` and select the next number.
+ *
+ * The main requirement for valid implementation of a `Get` selector is atomicity.
+ * While `Get` could be written to run again for every failure, this should not be anticipated.
+ * A success means a "success."
+ * A failure means that no "success" would be possible no matter how many times it might be run under the current conditions.
+ * The aforementioned conditions may change depending on the nature of the specific selector;
+ * but, the previous requirement should not be violated.
+ *
+ * `Get` is under no obligation to not modify its parameter `Array`.
+ * In fact, it should do this by default to provide additional feedback of its process.
+ * Pass a copy if data mutation is a concern.
+ * @param ary the `Array` of `Int` numbers from which to draw a new number
+ * @return an `Int` number
+ */
+ def Get(ary : Array[Int]) : Int
+
+ /**
+ * Give a number back to a specific collection following the principles of this selector.
+ *
+ * By default, a simple policy for returning numbers has been provided.
+ * This will not be sufficient for all selection actions that can be implemented so `override` where necessary.
+ *
+ * `Return` is under no obligation to leave its parameter `Array` unmodified.
+ * In fact, it should modify it by default to provide additional feedback of its process.
+ * Pass a copy if data mutation is a concern.
+ * @param number the number to be returned
+ * @param ary the `Array` of `Int` numbers to which the number is to be returned
+ * @return `true`, if this return was successful; `false`, otherwise
+ */
+ def Return(number : Int, ary : Array[Int]) : Boolean = {
+ if(ary(number) == -1) {
+ ary(number) = number
+ ret = number
+ true
+ }
+ else {
+ false
+ }
+ }
+
+ /**
+ * Accept the indexing pool from which numbers are selected and returned.
+ * Correct its format to suit the current `NumberSelector` algorithms.
+ *
+ * Moving all of the invalid negative-ones (-1) to the left of the current valid indices works for most selectors.
+ * The `selectionIndex` is set to the first valid number available from the left.
+ * The `ret` index is set to index zero.
+ * @param ary the `Array` of `Int` numbers
+ */
+ def Format(ary : Array[Int]) : Unit = {
+ val sorted = ary.sortWith( (b, a) => if(b == -1) { a > b } else { false } )
+ sorted.indices.foreach(n => ary(n) = sorted(n))
+ selectionIndex = sorted.count(_ == -1)
+ ret = 0
+ }
+}
diff --git a/common/src/main/scala/net/psforever/objects/guid/selector/OpportunisticSelector.scala b/common/src/main/scala/net/psforever/objects/guid/selector/OpportunisticSelector.scala
new file mode 100644
index 000000000..3405dd312
--- /dev/null
+++ b/common/src/main/scala/net/psforever/objects/guid/selector/OpportunisticSelector.scala
@@ -0,0 +1,25 @@
+// Copyright (c) 2017 PSForever
+package net.psforever.objects.guid.selector
+
+/**
+ * Get whichever number is next available.
+ * It is similar to `StrictInOrderSelector` but it does not stop if it runs into an unavailable number.
+ * It attempts to get each number in its listed incrementally from a starting index.
+ * The search wraps back around to the zero index to the same start index if necessary.
+ */
+class OpportunisticSelector extends NumberSelector {
+ override def Get(ary : Array[Int]) : Int = {
+ val start : Int = selectionIndex
+ if(ary(selectionIndex) == -1) {
+ val len : Int = ary.length
+ do {
+ selectionIndex = (selectionIndex + 1) % len
+ }
+ while(ary(selectionIndex) == -1 && selectionIndex != start)
+ }
+ val out : Int = ary(selectionIndex)
+ ary(selectionIndex) = -1
+ selectionIndex = (selectionIndex + (out >> 31) + 1) % ary.length //(out >> 31): 0 if positive or zero, -1 if negative
+ out
+ }
+}
diff --git a/common/src/main/scala/net/psforever/objects/guid/selector/RandomSelector.scala b/common/src/main/scala/net/psforever/objects/guid/selector/RandomSelector.scala
new file mode 100644
index 000000000..0e30607f6
--- /dev/null
+++ b/common/src/main/scala/net/psforever/objects/guid/selector/RandomSelector.scala
@@ -0,0 +1,68 @@
+// Copyright (c) 2017 PSForever
+package net.psforever.objects.guid.selector
+
+/**
+ * Get a pseudorandom number from a pool of numbers.
+ * The contained logic is similar to `RandomSequenceSelector`.
+ * It is not reliant of a shrinking pool that composes into some sequence of all the numbers, however;
+ * the numbers are re-introduced to the selection as long as the pool is used.
+ * This allows for the sequence to contain repeat numbers far before ever visiting all of the numbers once.
+ *
+ * During the selection process:
+ * The index is the position from where the selection begins, and the end of the `Array` is where the selection ends.
+ * Once a position between those two indices is selected, that number is extracted.
+ * The number at the start position is swapped into the position where the selection number was extracted.
+ * The start position is then set to an invalid number, and the start index is advanced.
+ * Repeat next request.
+ *
+ * During the return process:
+ * The returned number is added to the input `Array` at the position just before the current selection position.
+ * The selection index is then reversedback to re-include the returned number.
+ * The normal return index is not used in this algorithm.
+ * @see `RandomSequenceSelector`
+ */
+class RandomSelector extends NumberSelector {
+ private val rand : scala.util.Random = new scala.util.Random(System.currentTimeMillis())
+
+ /**
+ * Accept a provided `pool` and select the next number.
+ *
+ * ...
+ * @param ary the `Array` of `Int` numbers from which to draw a new number
+ * @return an `Int` number
+ */
+ override def Get(ary : Array[Int]) : Int = {
+ if(ary.length > selectionIndex) {
+ val selection : Int = rand.nextInt(ary.length - selectionIndex) + selectionIndex
+ val out : Int = ary(selection)
+ ary(selection) = ary(selectionIndex)
+ ary(selectionIndex) = -1
+ selectionIndex = selectionIndex + (out >> 31) + 1 //(out >> 31): 0 if positive or zero, -1 if negative
+ out
+ }
+ else {
+ -1
+ }
+ }
+
+ /**
+ * Give a number back to a specific collection following the principles of this selector.
+ *
+ * The number is always returned to a "used" index position near the front of the array.
+ * It locates this position by incrementally traversing the `Array` behind the position used in `Get`.
+ * Asides from selection, a disorderly reinsertion of numbers back into the pool is also a source of randomness.
+ * @param number the number to be returned
+ * @param ary the `Array` of `Int` numbers to which the number is to be returned
+ * @return `true`, if this return was successful; `false`, otherwise
+ */
+ override def Return(number : Int, ary : Array[Int]) : Boolean = {
+ if(selectionIndex > 0) {
+ ary(selectionIndex - 1) = number
+ selectionIndex -= 1
+ true
+ }
+ else {
+ false
+ }
+ }
+}
diff --git a/common/src/main/scala/net/psforever/objects/guid/selector/RandomSequenceSelector.scala b/common/src/main/scala/net/psforever/objects/guid/selector/RandomSequenceSelector.scala
new file mode 100644
index 000000000..c4ccae1cc
--- /dev/null
+++ b/common/src/main/scala/net/psforever/objects/guid/selector/RandomSequenceSelector.scala
@@ -0,0 +1,64 @@
+// Copyright (c) 2017 PSForever
+package net.psforever.objects.guid.selector
+
+/**
+ * Get a pseudorandom number from a pool of numbers.
+ * The output of this class, operating on an `Array` of `Int` values is contained to some sequence of all the numbers.
+ * Only after every number is selected once, may any number repeat.
+ * The pseudorandomness of any sequence of numbers is not only provided by an internal system `Random` but by the order or returned numbers.
+ * Consequentially, as any single sequence nears completion, the numbers remaining become more and more predictable.
+ *
+ * During the selection process:
+ * The index is the position from where the selection begins, and the end of the `Array` is where the selection ends.
+ * Once a position between those two indices is selected, that number is extracted.
+ * The number at the start position is swapped into the position where the selection number was extracted.
+ * The start position is then set to an invalid number, and the start index is advanced.
+ * Repeat next request.
+ *
+ * The return index trails behind the selection index as far as the order of the array is concerned at first.
+ * After some time, the selection index moves to the starting position of the array again and then the order is reversed.
+ * Until the return index wraps around to the beginning of the array too, it is considered the valid selection end position.
+ *
+ * During the return process:
+ * As the `Array` empties out from the first to the last index, the return process starts at the first index again.
+ * When a number is "returned," it is placed back into the input `Array` at the earliest available index.
+ * The return index is advanced.
+ * Neither the selection index nor the return index may pass each other,
+ * except when one reaches the end of the `Array` and wraps back around to that start.
+ * @see `RandomSelector`
+ */
+class RandomSequenceSelector extends NumberSelector {
+ private val rand : scala.util.Random = new scala.util.Random(System.currentTimeMillis())
+
+ /**
+ * Accept a provided "pool of numbers" and select the next number.
+ * @param ary the `Array` of `Int` numbers from which to draw a new number
+ * @return an `Int` number
+ */
+ override def Get(ary : Array[Int]) : Int = {
+ val last : Int = if(ret <= selectionIndex) { ary.length } else { ret }
+ val selection : Int = rand.nextInt(last - selectionIndex) + selectionIndex
+ val out : Int = ary(selection)
+ ary(selection) = ary(selectionIndex)
+ ary(selectionIndex) = -1
+ selectionIndex = (selectionIndex + (out >> 31) + 1) % ary.length //(out >> 31): 0 if positive or zero, -1 if negative
+ out
+ }
+
+ /**
+ * Give a number back to a specific collection following the principles of this selector.
+ * @param number the number to be returned
+ * @param ary the `Array` of `Int` numbers to which the number is to be returned
+ * @return `true`, if this return was successful; `false`, otherwise
+ */
+ override def Return(number : Int, ary : Array[Int]) : Boolean = {
+ if(ary(ret) == -1) {
+ ary(ret) = number
+ ret = (ret + 1) % ary.length
+ true
+ }
+ else {
+ false
+ }
+ }
+}
diff --git a/common/src/main/scala/net/psforever/objects/guid/selector/SpecificSelector.scala b/common/src/main/scala/net/psforever/objects/guid/selector/SpecificSelector.scala
new file mode 100644
index 000000000..4b5ca43d5
--- /dev/null
+++ b/common/src/main/scala/net/psforever/objects/guid/selector/SpecificSelector.scala
@@ -0,0 +1,54 @@
+// Copyright (c) 2017 PSForever
+package net.psforever.objects.guid.selector
+
+/**
+ * Get a specific number from a pool of numbers.
+ */
+class SpecificSelector extends NumberSelector {
+ /**
+ * Change the future selection index to match the number the user wants.
+ * Call `Get` to complete process.
+ * @param number the number
+ */
+ def SelectionIndex_=(number : Int) : Unit = {
+ selectionIndex = number
+ }
+
+ /**
+ * Get the specified number and the specified number only.
+ * @param ary the `Array` of `Int` numbers from which to draw a new number
+ * @return an `Int` number
+ */
+ override def Get(ary : Array[Int]) : Int = {
+ if(-1 < selectionIndex && selectionIndex < ary.length) {
+ val out = ary(selectionIndex)
+ ary(selectionIndex) = -1
+ out
+ }
+ else {
+ -1
+ }
+ }
+
+ /**
+ * Accept the indexing pool from which numbers are selected and returned.
+ * Correct its format to suit the current `NumberSelector` algorithms.
+ *
+ * All of the numbers are sorted to their proper indexed position in the `Array`.
+ * Every other number is an invalid negative-one (-1).
+ * The `selectionIndex` is also set to an invalid negative-one, as per the requirements of the selector.
+ * The `ret` index is set to index zero.
+ * @param ary the `Array` of `Int` numbers
+ */
+ override def Format(ary : Array[Int]) : Unit = {
+ val sorted = Array.fill(ary.length)(-1)
+ ary.foreach(n => {
+ if(n > -1) {
+ sorted(n) = n
+ }
+ })
+ sorted.copyToArray(ary)
+ selectionIndex = -1
+ ret = 0
+ }
+}
diff --git a/common/src/main/scala/net/psforever/objects/guid/selector/StrictInOrderSelector.scala b/common/src/main/scala/net/psforever/objects/guid/selector/StrictInOrderSelector.scala
new file mode 100644
index 000000000..c0d0196b3
--- /dev/null
+++ b/common/src/main/scala/net/psforever/objects/guid/selector/StrictInOrderSelector.scala
@@ -0,0 +1,38 @@
+// Copyright (c) 2017 PSForever
+package net.psforever.objects.guid.selector
+
+/**
+ * Get the next number in this pool incrementally.
+ * Starting at index 0, for example, select each subsequent number as it is available.
+ * Do not progress if a number is not available when requested.
+ */
+class StrictInOrderSelector extends NumberSelector {
+ override def Get(ary : Array[Int]) : Int = {
+ val out : Int = ary(selectionIndex)
+ ary(selectionIndex) = -1
+ selectionIndex = (selectionIndex + (out >> 31) + 1) % ary.length //(out >> 31): 0 if positive or zero, -1 if negative
+ out
+ }
+
+ /**
+ * Accept the indexing pool from which numbers are selected and returned.
+ * Correct its format to suit the current `NumberSelector` algorithms.
+ *
+ * All of the numbers are sorted to their proper indexed position in the `Array`.
+ * Every other number is an invalid negative-one (-1).
+ * The `selectionIndex` is set to the index of the first valid number, or zero if there are none.
+ * The `ret` index is set to index zero.
+ * @param ary the `Array` of `Int` numbers
+ */
+ override def Format(ary : Array[Int]) : Unit = {
+ val sorted = Array.fill(ary.length)(-1)
+ ary.foreach(n => {
+ if(n > -1) {
+ sorted(n) = n
+ }
+ })
+ sorted.copyToArray(ary)
+ selectionIndex = ary.find(n => n > -1).getOrElse(0)
+ ret = 0
+ }
+}
diff --git a/common/src/main/scala/net/psforever/objects/guid/source/Key.scala b/common/src/main/scala/net/psforever/objects/guid/source/Key.scala
new file mode 100644
index 000000000..7aeeef375
--- /dev/null
+++ b/common/src/main/scala/net/psforever/objects/guid/source/Key.scala
@@ -0,0 +1,25 @@
+// Copyright (c) 2017 PSForever
+package net.psforever.objects.guid.source
+
+import net.psforever.objects.entity.IdentifiableEntity
+import net.psforever.objects.guid.AvailabilityPolicy
+import net.psforever.objects.guid.key.Monitor
+
+private class Key extends Monitor {
+ private var policy : AvailabilityPolicy.Value = AvailabilityPolicy.Available
+ private var obj : Option[IdentifiableEntity] = None
+
+ def Policy : AvailabilityPolicy.Value = policy
+
+ def Policy_=(pol : AvailabilityPolicy.Value) : AvailabilityPolicy.Value = {
+ policy = pol
+ Policy
+ }
+
+ def Object : Option[IdentifiableEntity] = obj
+
+ def Object_=(objct : Option[IdentifiableEntity]) : Option[IdentifiableEntity] = {
+ obj = objct
+ Object
+ }
+}
diff --git a/common/src/main/scala/net/psforever/objects/guid/source/LimitedNumberSource.scala b/common/src/main/scala/net/psforever/objects/guid/source/LimitedNumberSource.scala
new file mode 100644
index 000000000..856323f9e
--- /dev/null
+++ b/common/src/main/scala/net/psforever/objects/guid/source/LimitedNumberSource.scala
@@ -0,0 +1,113 @@
+// Copyright (c) 2017 PSForever
+package net.psforever.objects.guid.source
+
+import net.psforever.objects.entity.IdentifiableEntity
+import net.psforever.objects.guid.key.{LoanedKey, SecureKey}
+import net.psforever.objects.guid.AvailabilityPolicy
+
+import scala.collection.mutable
+
+/**
+ * A `NumberSource` is considered a master "pool" of numbers from which all numbers are available to be drawn.
+ * The numbers are considered to be exclusive.
+ *
+ * Produce a series of numbers from 0 to a maximum number (inclusive) to be used as globally unique identifiers (GUIDs).
+ * @param max the highest number to be generated by this source;
+ * must be a positive integer or zero
+ * @throws IllegalArgumentException if `max` is less than zero (therefore the count of generated numbers is at most zero)
+ * @throws java.lang.NegativeArraySizeException if the count of numbers generated due to max is negative
+ */
+class LimitedNumberSource(max : Int) extends NumberSource {
+ if(max < 0) {
+ throw new IllegalArgumentException(s"non-negative integers only, not $max")
+ }
+ private val ary : Array[Key] = Array.ofDim[Key](max + 1)
+ (0 to max).foreach(x => { ary(x) = new Key })
+ private var allowRestrictions : Boolean = true
+
+ def Size : Int = ary.length
+
+ def CountAvailable : Int = ary.count(key => key.Policy == AvailabilityPolicy.Available)
+
+ def CountUsed : Int = ary.count(key => key.Policy != AvailabilityPolicy.Available)
+
+ def Get(number : Int) : Option[SecureKey] = {
+ if(Test(number)) {
+ Some(new SecureKey(number, ary(number)))
+ }
+ else {
+ None
+ }
+ }
+
+ def Available(number : Int) : Option[LoanedKey] = {
+ var out : Option[LoanedKey] = None
+ if(Test(number)) {
+ val key : Key = ary(number)
+ if(key.Policy == AvailabilityPolicy.Available) {
+ key.Policy = AvailabilityPolicy.Leased
+ out = Some(new LoanedKey(number, key))
+ }
+ }
+ out
+ }
+
+ /**
+ * Consume the number of a `Monitor` and release that number from its previous assignment/use.
+ * @param number the number
+ * @return any object previously using this number
+ */
+ def Return(number : Int) : Option[IdentifiableEntity] = {
+ var out : Option[IdentifiableEntity] = None
+ if(Test(number)) {
+ val existing : Key = ary(number)
+ if(existing.Policy == AvailabilityPolicy.Leased) {
+ out = existing.Object
+ existing.Policy = AvailabilityPolicy.Available
+ existing.Object = None
+ }
+ }
+ out
+ }
+
+ /**
+ * Produce a modifiable wrapper for the `Monitor` for this number, only if the number has not been used.
+ * This wrapped `Monitor` can only be assigned once and the number may not be `Return`ed to this source.
+ * @param number the number
+ * @return the wrapped `Monitor`
+ * @throws ArrayIndexOutOfBoundsException if the requested number is above or below the range
+ */
+ def Restrict(number : Int) : Option[LoanedKey] = {
+ if(allowRestrictions && Test(number)) {
+ val key : Key = ary(number)
+ key.Policy = AvailabilityPolicy.Restricted
+ Some(new LoanedKey(number, key))
+ }
+ else {
+ None
+ }
+ }
+
+ def FinalizeRestrictions : List[Int] = {
+ allowRestrictions = false
+ ary.zipWithIndex.filter(entry => entry._1.Policy == AvailabilityPolicy.Restricted).map(entry => entry._2).toList
+ }
+
+ def Clear() : List[IdentifiableEntity] = {
+ val outList : mutable.ListBuffer[IdentifiableEntity] = mutable.ListBuffer[IdentifiableEntity]()
+ for(x <- ary.indices) {
+ ary(x).Policy = AvailabilityPolicy.Available
+ if(ary(x).Object.isDefined) {
+ outList += ary(x).Object.get
+ ary(x).Object = None
+ }
+ }
+ outList.toList
+ }
+}
+
+object LimitedNumberSource {
+ def apply(max : Int) : LimitedNumberSource = {
+ new LimitedNumberSource(max)
+ }
+}
diff --git a/common/src/main/scala/net/psforever/objects/guid/source/MaxNumberSource.scala b/common/src/main/scala/net/psforever/objects/guid/source/MaxNumberSource.scala
new file mode 100644
index 000000000..ea5c969b6
--- /dev/null
+++ b/common/src/main/scala/net/psforever/objects/guid/source/MaxNumberSource.scala
@@ -0,0 +1,119 @@
+// Copyright (c) 2017 PSForever
+package net.psforever.objects.guid.source
+
+import net.psforever.objects.entity.IdentifiableEntity
+import net.psforever.objects.guid.key.{LoanedKey, SecureKey}
+import net.psforever.objects.guid.AvailabilityPolicy
+
+/**
+ * A `NumberSource` is considered a master "pool" of numbers from which all numbers are available to be drawn.
+ * The numbers are considered to be exclusive.
+ *
+ * This source utilizes all positive integers (to `Int.MaxValue`, anyway) and zero.
+ * It allocates number `Monitors` as it needs them.
+ * While this allows for a wide range of possible numbers, the internal structure expands and contracts as needed.
+ * The underlying flexible structure is a `LongMap` and is subject to constraints regarding `LongMap` growth.
+ */
+class MaxNumberSource() extends NumberSource {
+ import scala.collection.mutable
+ private val hash : mutable.LongMap[Key] = mutable.LongMap[Key]() //TODO consider seeding an initialBufferSize
+ private var allowRestrictions : Boolean = true
+
+ def Size : Int = Int.MaxValue
+
+ def CountAvailable : Int = Size - CountUsed
+
+ def CountUsed : Int = hash.size
+
+ override def Test(guid : Int) : Boolean = guid > -1
+
+ def Get(number : Int) : Option[SecureKey] = {
+ if(!Test(number)) {
+ None
+ }
+ else {
+ val existing : Option[Key] = hash.get(number).orElse({
+ val key : Key = new Key
+ key.Policy = AvailabilityPolicy.Available
+ hash.put(number, key)
+ Some(key)
+ })
+ Some(new SecureKey(number, existing.get))
+ }
+ }
+
+// def GetAll(list : List[Int]) : List[SecureKey] = {
+// list.map(number =>
+// hash.get(number) match {
+// case Some(key) =>
+// new SecureKey(number, key)
+// case _ =>
+// new SecureKey(number, new Key { Policy = AvailabilityPolicy.Available })
+// }
+// )
+// }
+//
+// def GetAll( p : Key => Boolean ) : List[SecureKey] = {
+// hash.filter(entry => p.apply(entry._2)).map(entry => new SecureKey(entry._1.toInt, entry._2)).toList
+// }
+
+ def Available(number : Int) : Option[LoanedKey] = {
+ if(!Test(number)) {
+ throw new IndexOutOfBoundsException("number can not be negative")
+ }
+ hash.get(number) match {
+ case Some(_) =>
+ None
+ case _ =>
+ val key : Key = new Key
+ key.Policy = AvailabilityPolicy.Leased
+ hash.put(number, key)
+ Some(new LoanedKey(number, key))
+ }
+ }
+
+ def Return(number : Int) : Option[IdentifiableEntity] = {
+ val existing = hash.get(number)
+ if(existing.isDefined && existing.get.Policy == AvailabilityPolicy.Leased) {
+ hash -= number
+ val obj = existing.get.Object
+ existing.get.Object = None
+ obj
+ }
+ else {
+ None
+ }
+ }
+
+ def Restrict(number : Int) : Option[LoanedKey] = {
+ if(allowRestrictions) {
+ val existing : Key = hash.get(number).orElse({
+ val key : Key = new Key
+ hash.put(number, key)
+ Some(key)
+ }).get
+ existing.Policy = AvailabilityPolicy.Restricted
+ Some(new LoanedKey(number, existing))
+ }
+ else {
+ None
+ }
+ }
+
+ def FinalizeRestrictions : List[Int] = {
+ allowRestrictions = false
+ hash.filter(entry => entry._2.Policy == AvailabilityPolicy.Restricted).map(entry => entry._1.toInt).toList
+ }
+
+ def Clear() : List[IdentifiableEntity] = {
+ val list : List[IdentifiableEntity] = hash.values.filter(key => key.Object.isDefined).map(key => key.Object.get).toList
+ hash.clear()
+ list
+ }
+}
+
+object MaxNumberSource {
+ def apply() : MaxNumberSource = {
+ new MaxNumberSource()
+ }
+}
\ No newline at end of file
diff --git a/common/src/main/scala/net/psforever/objects/guid/source/NumberSource.scala b/common/src/main/scala/net/psforever/objects/guid/source/NumberSource.scala
new file mode 100644
index 000000000..f169eabe9
--- /dev/null
+++ b/common/src/main/scala/net/psforever/objects/guid/source/NumberSource.scala
@@ -0,0 +1,154 @@
+// Copyright (c) 2017 PSForever
+package net.psforever.objects.guid.source
+
+import net.psforever.objects.entity.IdentifiableEntity
+import net.psforever.objects.guid.key.{LoanedKey, SecureKey}
+
+trait NumberSourceAccessors {
+ /**
+ * Produce an un-modifiable wrapper for the `Monitor` for this number.
+ * @param number the number
+ * @return the wrapped `Monitor`
+ */
+ def Get(number : Int) : Option[SecureKey]
+
+ /**
+ * Produce a modifiable wrapper for the `Monitor` for this number, only if the number has not been used.
+ * The `Monitor` should be updated before being wrapped, if necessary.
+ * @param number the number
+ * @return the wrapped `Monitor`, or `None`
+ */
+ def Available(number : Int) : Option[LoanedKey]
+
+ /**
+ * Consume a wrapped `Monitor` and release its number from its previous assignment/use.
+ * @param monitor the `Monitor`
+ * @return any object previously using this `Monitor`
+ */
+ def Return(monitor : SecureKey) : Option[IdentifiableEntity] = {
+ Return(monitor.GUID)
+ }
+
+ /**
+ * Consume a wrapped `Monitor` and release its number from its previous assignment/use.
+ * @param monitor the `Monitor`
+ * @return any object previously using this `Monitor`
+ */
+ def Return(monitor : LoanedKey) : Option[IdentifiableEntity] = {
+ Return(monitor.GUID)
+ }
+
+ /**
+ * Consume the number of a `Monitor` and release that number from its previous assignment/use.
+ * @param number the number
+ * @return any object previously using this number
+ */
+ def Return(number : Int) : Option[IdentifiableEntity]
+}
+
+/**
+ * A `NumberSource` is considered a master "pool" of numbers from which all numbers are available to be drawn.
+ * The numbers are considered to be exclusive.
+ *
+ * The following are guidelines for implementing classes.
+ * The numbers allocated to this source are from zero up through positive integers.
+ * When a number is drawn from the pool, it is flagged internally and can not be selected for drawing again until the flag is removed.
+ * Some flagging states are allowed to restrict that number for the whole lifespan of the source.
+ * This internal flagging is maintained by a "monitor" that should not directly get exposed.
+ * Use the provided indirect referencing containers - `SecureKey` and `LoanedKey`.
+ *
+ * The purpose of a `NumberSource` is to help facilitate globally unique identifiers (GUID, pl. GUIDs).
+ */
+trait NumberSource {
+ /**
+ * The count of numbers allocated to this source.
+ * @return the count
+ */
+ def Size : Int
+
+ /**
+ * The count of numbers that can still be drawn.
+ * @return the count
+ */
+ def CountAvailable : Int
+
+ /**
+ * The count of numbers that can not be drawn.
+ * @return the count
+ */
+ def CountUsed : Int
+
+ /**
+ * Is this number a member of this number source?
+ * @param number the number
+ * @return `true`, if it is a member; `false`, otherwise
+ */
+ def Test(number : Int) : Boolean = -1 < number && number < Size
+
+ /**
+ * Produce an un-modifiable wrapper for the `Monitor` for this number.
+ * @param number the number
+ * @return the wrapped `Monitor`
+ */
+ def Get(number : Int) : Option[SecureKey]
+
+ //def GetAll(list : List[Int]) : List[SecureKey]
+
+ //def GetAll(p : Key => Boolean) : List[SecureKey]
+
+ /**
+ * Produce a modifiable wrapper for the `Monitor` for this number, only if the number has not been used.
+ * The `Monitor` should be updated before being wrapped, if necessary.
+ * @param number the number
+ * @return the wrapped `Monitor`, or `None`
+ */
+ def Available(number : Int) : Option[LoanedKey]
+
+ /**
+ * Consume a wrapped `Monitor` and release its number from its previous assignment/use.
+ * @param monitor the `Monitor`
+ * @return any object previously using this `Monitor`
+ */
+ def Return(monitor : SecureKey) : Option[IdentifiableEntity] = {
+ Return(monitor.GUID)
+ }
+
+ /**
+ * Consume a wrapped `Monitor` and release its number from its previous assignment/use.
+ * @param monitor the `Monitor`
+ * @return any object previously using this `Monitor`
+ */
+ def Return(monitor : LoanedKey) : Option[IdentifiableEntity] = {
+ Return(monitor.GUID)
+ }
+
+ /**
+ * Consume the number of a `Monitor` and release that number from its previous assignment/use.
+ * @param number the number
+ * @return any object previously using this number
+ */
+ def Return(number : Int) : Option[IdentifiableEntity]
+
+ /**
+ * Produce a modifiable wrapper for the `Monitor` for this number, only if the number has not been used.
+ * This wrapped `Monitor` can only be assigned once and the number may not be `Return`ed to this source.
+ * @param number the number
+ * @return the wrapped `Monitor`
+ */
+ def Restrict(number : Int) : Option[LoanedKey]
+
+ /**
+ * Numbers from this source may not longer be marked as `Restricted`.
+ * @return the `List` of all numbers that have been restricted
+ */
+ def FinalizeRestrictions : List[Int]
+
+ import net.psforever.objects.entity.IdentifiableEntity
+ /**
+ * Reset all number `Monitor`s so that their underlying number is not longer treated as assigned.
+ * Perform some level of housecleaning to ensure that all dependencies are resolved in some manner.
+ * This is the only way to free `Monitors` that are marked as `Restricted`.
+ * @return a `List` of assignments maintained by all the currently-used number `Monitors`
+ */
+ def Clear() : List[IdentifiableEntity]
+}
diff --git a/common/src/main/scala/net/psforever/objects/inventory/GridInventory.scala b/common/src/main/scala/net/psforever/objects/inventory/GridInventory.scala
new file mode 100644
index 000000000..8307cea26
--- /dev/null
+++ b/common/src/main/scala/net/psforever/objects/inventory/GridInventory.scala
@@ -0,0 +1,586 @@
+// Copyright (c) 2017 PSForever
+package net.psforever.objects.inventory
+
+import java.util.concurrent.atomic.AtomicInteger
+
+import net.psforever.objects.equipment.Equipment
+import net.psforever.objects.EquipmentSlot
+import net.psforever.packet.game.PlanetSideGUID
+
+import scala.annotation.tailrec
+import scala.collection.immutable.Map
+import scala.collection.mutable
+import scala.util.{Failure, Success, Try}
+
+/**
+ * An inventory are used to stow `Equipment` when it does not exist visually in the game world.
+ *
+ * Visually, an inventory is understood as a rectangular region divided into cellular units.
+ * The `Equipment` that is placed into the inventory can also be represented as smaller rectangles, also composed of cells.
+ * The same number of cells of the item must overlap with the same number of cells of the inventory.
+ * No two items may have cells that overlap.
+ * This "grid" maintains a spatial distinction between items when they get stowed.
+ *
+ * It is not necessary to actually have a structural representation of the "grid."
+ * Adhering to such a data structure does speed up the actions upon the inventory and its contents in certain cases (where noted).
+ * The `HashMap` of items is used for quick object lookup.
+ * Use of the `HashMap` only is hitherto referred as "using the inventory as a `List`."
+ * The `Array` of spatial GUIDs is used for quick collision lookup.
+ * Use of the `Array` only is hitherto referred as "using the inventory as a grid."
+ */
+class GridInventory {
+ private var width : Int = 1
+ private var height : Int = 1
+ private var offset : Int = 0 //the effective index of the first cell in the inventory where offset >= 0
+
+ /* key - an integer (not especially meaningful beyond being unique); value - the card that represents the stowed item */
+ private val items : mutable.HashMap[Int, InventoryItem] = mutable.HashMap[Int, InventoryItem]()
+ private val entryIndex : AtomicInteger = new AtomicInteger(0)
+ private var grid : Array[Int] = Array.fill[Int](1)(-1)
+
+ def Items : Map[Int, InventoryItem] = items.toMap[Int, InventoryItem]
+
+ def Width : Int = width
+
+ def Height : Int = height
+
+ def Offset : Int = offset
+
+ /**
+ * Change the grid index offset value.
+ * @param fset the new offset value
+ * @return the current offset value
+ * @throws IndexOutOfBoundsException if the index is negative
+ */
+ def Offset_=(fset : Int) : Int = {
+ if(fset < 0) {
+ throw new IndexOutOfBoundsException(s"can not set index offset to negative number - $fset")
+ }
+ offset = fset
+ Offset
+ }
+
+ def Size : Int = items.size
+
+ /**
+ * Capacity is a measure how many squares in the grid inventory are unused (value of -1).
+ * It does not guarantee the cells are distributed in any configuration conductive to item stowing.
+ * @return the number of free cells
+ */
+ def Capacity : Int = {
+ TotalCapacity - items.values.foldLeft(0)((cnt, item) => cnt + (item.obj.Tile.width * item.obj.Tile.height))
+ }
+
+ /**
+ * The total number of cells in this inventory.
+ * @return the width multiplied by the height (`grid.length`, which is the same thing)
+ */
+ def TotalCapacity : Int = grid.length
+
+ /**
+ * The index of the last cell in this inventory.
+ * @return same as `Offset` plus the total number of cells in this inventory minus 1
+ */
+ def LastIndex : Int = Offset + TotalCapacity - 1
+
+ /**
+ * Get whatever is stowed in the inventory at the given index.
+ * @param slot the cell index
+ * @return an `EquipmentSlot` that contains whatever `Equipment` was stored in `slot`
+ */
+ def Slot(slot : Int) : EquipmentSlot = {
+ val actualSlot = slot - offset
+ if(actualSlot < 0 || actualSlot > grid.length) {
+ throw new IndexOutOfBoundsException(s"requested indices not in bounds of grid inventory - $actualSlot")
+ }
+ else {
+ new InventoryEquipmentSlot(slot, this)
+ }
+ }
+
+ /**
+ * Test whether a given piece of `Equipment` would collide with any stowed content in the inventory.
+ *
+ * A "collision" is considered a situation where the stowed placards of two items would overlap in some way.
+ * The gridkeeps track of the location of items by storing the primitive of their GUID in one or more cells.
+ * Two primitives can not be stored in the same cell.
+ * If placing two items into the same inventory leads to a situation where two primitive values might be in the same cell,
+ * that is a collision.
+ * @param start the cell index to test this `Equipment` for insertion
+ * @param item the `Equipment` to be tested
+ * @return a `List` of GUID values for all existing contents that this item would overlap if inserted
+ */
+ def CheckCollisions(start : Int, item : Equipment) : Try[List[Int]] = {
+ val tile : InventoryTile = item.Tile
+ CheckCollisions(start, tile.width, tile.height)
+ }
+
+ /**
+ * Test whether a given piece of `Equipment` would collide with any stowed content in the inventory.
+ *
+ * If there are fewer items stored in the inventory than there are cells required to represent the testing item,
+ * test the collision by iterating through the list of items.
+ * If there are more items, check that each cell that would be used for the testing items tile does not collide.
+ * The "testing item" in this case has already been transformed into its tile dimensions.
+ * @param start the cell index to test this `Equipment` for insertion
+ * @param w the width of the `Equipment` to be tested
+ * @param h the height of the `Equipment` to be tested
+ * @return a `List` of GUID values for all existing contents that this item would overlap if inserted
+ */
+ def CheckCollisions(start : Int, w : Int, h : Int) : Try[List[Int]] = {
+ if(items.isEmpty) {
+ Success(List.empty[Int])
+ }
+ else {
+ def check : (Int,Int,Int) => Try[List[Int]] = if(items.size < w * h) { CheckCollisionsAsList } else { CheckCollisionsAsGrid }
+ check(start, w, h)
+ }
+ }
+
+ /**
+ * Test whether a given piece of `Equipment` would collide with any stowed content in the inventory.
+ *
+ * Iterate over all stowed items and check each one whether or not it overlaps with the given region.
+ * This is a "using the inventory as a `List`" method.
+ * @param start the cell index to test this `Equipment` for insertion
+ * @param w the width of the `Equipment` to be tested
+ * @param h the height of the `Equipment` to be tested
+ * @return a `List` of GUID values for all existing contents that this item would overlap if inserted
+ * @throws IndexOutOfBoundsException if the region extends outside of the grid boundaries
+ */
+ def CheckCollisionsAsList(start : Int, w : Int, h : Int) : Try[List[Int]] = {
+ val actualSlot = start - offset
+ val startx : Int = actualSlot % width
+ val starty : Int = actualSlot / width
+ val startw : Int = startx + w - 1
+ val starth : Int = starty + h - 1
+ if(actualSlot < 0 || actualSlot >= grid.length || startw >= width || starth >= height) {
+ val bounds : String = if(startx < 0) { "left" } else if(startw >= width) { "right" } else { "bottom" }
+ Failure(new IndexOutOfBoundsException(s"requested region escapes the $bounds edge of the grid inventory - $startx, $starty; $w x $h"))
+ }
+ else {
+ val collisions : mutable.Set[Int] = mutable.Set[Int]()
+ items.values.foreach({ item : InventoryItem =>
+ val actualItemStart : Int = item.start - offset
+ val itemx : Int = actualItemStart % width
+ val itemy : Int = actualItemStart / width
+ val tile = item.obj.Tile
+ val clipsOnX : Boolean = if(itemx < startx) { itemx + tile.width > startx } else { itemx <= startw }
+ val clipsOnY : Boolean = if(itemy < starty) { itemy + tile.height > starty } else { itemy <= starth }
+ if(clipsOnX && clipsOnY) {
+ collisions += item.GUID.guid
+ }
+ })
+ Success(collisions.toList)
+ }
+ }
+
+ /**
+ * Test whether a given piece of `Equipment` would collide with any stowed content in the inventory.
+ *
+ * Iterate over all cells that would be occupied by a new value and check each one whether or not that cell has an existing value.
+ * This is a "using the inventory as a grid" method.
+ * @param start the cell index to test this `Equipment` for insertion
+ * @param w the width of the `Equipment` to be tested
+ * @param h the height of the `Equipment` to be tested
+ * @return a `List` of GUID values for all existing contents that this item would overlap if inserted
+ * @throws IndexOutOfBoundsException if the region extends outside of the grid boundaries
+ */
+ def CheckCollisionsAsGrid(start : Int, w : Int, h : Int) : Try[List[Int]] = {
+ val actualSlot = start - offset
+ if(actualSlot < 0 || actualSlot >= grid.length || (actualSlot % width) + w > width || (actualSlot / width) + h > height) {
+ val startx : Int = actualSlot % width
+ val starty : Int = actualSlot / width
+ val startw : Int = startx + w - 1
+ val bounds : String = if(startx < 0) { "left" } else if(startw >= width) { "right" } else { "bottom" }
+ Failure(new IndexOutOfBoundsException(s"requested region escapes the $bounds edge of the grid inventory - $startx, $starty; $w x $h"))
+ }
+ else {
+ val collisions : mutable.Set[Int] = mutable.Set[Int]()
+ var curr = actualSlot
+ for(_ <- 0 until h) {
+ for(col <- 0 until w) {
+ if(grid(curr + col) > -1) {
+ collisions += items(grid(curr + col)).GUID.guid
+ }
+ }
+ curr += width
+ }
+ Success(collisions.toList)
+ }
+ }
+
+ /**
+ * Find a blank space in the current inventory where a `tile` of given dimensions can be cleanly inserted.
+ * Brute-force method.
+ * @param tile the dimensions of the blank space
+ * @return the grid index of the upper left corner where equipment to which the `tile` belongs should be placed
+ */
+ def Fit(tile : InventoryTile) : Option[Int] = {
+ val tWidth = tile.width
+ val tHeight = tile.height
+ val gridIter = (0 until (grid.length - (tHeight - 1) * width))
+ .filter(cell => grid(cell) == -1 && (width - cell%width >= tWidth))
+ .iterator
+ recursiveFitTest(gridIter, tWidth, tHeight)
+ }
+
+ /**
+ * Find a blank space in the current inventory where a `tile` of given dimensions can be cleanly inserted.
+ * @param cells an iterator of all accepted indices in the `grid`
+ * @param tWidth the width of the blank space
+ * @param tHeight the height of the blank space
+ * @return the grid index of the upper left corner where equipment to which the `tile` belongs should be placed
+ */
+ @tailrec private def recursiveFitTest(cells : Iterator[Int], tWidth : Int, tHeight : Int) : Option[Int] = {
+ if(!cells.hasNext) {
+ None
+ }
+ else {
+ val index = cells.next + offset
+ CheckCollisionsAsGrid(index, tWidth, tHeight) match {
+ case Success(Nil) =>
+ Some(index)
+ case Success(_) =>
+ recursiveFitTest(cells, tWidth, tHeight)
+ case Failure(ex) =>
+ throw ex
+ }
+ }
+ }
+
+ /**
+ * Define a region of inventory grid cells and set them to a given value.
+ * @param start the initial inventory index
+ * @param w the width of the region
+ * @param h the height of the region
+ * @param value the value to set all the cells in the defined region;
+ * defaults to -1 (which is "nothing")
+ */
+ def SetCells(start : Int, w : Int, h : Int, value : Int = -1) : Unit = {
+ SetCellsOffset(math.max(start - offset, 0), w, h, value)
+ }
+
+ /**
+ * Define a region of inventory grid cells and set them to a given value.
+ * @param start the initial inventory index, without the inventory offset (required)
+ * @param w the width of the region
+ * @param h the height of the region
+ * @param value the value to set all the cells in the defined region;
+ * defaults to -1 (which is "nothing")
+ * @throws IndexOutOfBoundsException if the region extends outside of the grid boundaries
+ */
+ def SetCellsOffset(start : Int, w : Int, h : Int, value : Int = -1) : Unit = {
+ if(start < 0 || start > grid.length || (start % width) + w - 1 > width || (start / width) + h- 1 > height) {
+ val startx : Int = start % width
+ val starty : Int = start / width
+ val startw : Int = startx + w - 1
+ val bounds : String = if(startx < 0) { "left" } else if(startw >= width) { "right" } else { "bottom" }
+ throw new IndexOutOfBoundsException(s"requested region escapes the $bounds of the grid inventory - $startx, $starty; $w x $h")
+ }
+ else {
+ var curr = start
+ for(_ <- 0 until h) {
+ for(col <- 0 until w) {
+ grid(curr + col) = value
+ }
+ curr += width
+ }
+ }
+ }
+
+ def Insert(start : Int, obj : Equipment) : Boolean = {
+ val key : Int = entryIndex.getAndIncrement()
+ items.get(key) match {
+ case None => //no redundant insertions or other collisions
+ Insertion_CheckCollisions(start, obj, key)
+ case _ =>
+ false
+ }
+ }
+
+ def Insertion_CheckCollisions(start : Int, obj : Equipment, key : Int) : Boolean = {
+ CheckCollisions(start, obj) match {
+ case Success(Nil) =>
+ val card = InventoryItem(obj, start)
+ items += key -> card
+ val tile = obj.Tile
+ SetCells(start, tile.width, tile.height, key)
+ true
+ case _ =>
+ false
+ }
+ }
+
+ def +=(kv : (Int, Equipment)) : Boolean = Insert(kv._1, kv._2)
+
+// def InsertQuickly(start : Int, obj : Equipment) : Boolean = {
+// val guid : Int = obj.GUID.guid
+// val card = InventoryItemData(obj, start)
+// items += guid -> card
+// val tile = obj.Tile
+// SetCellsOffset(start, tile.width, tile.height, guid)
+// true
+// }
+
+ def Remove(index : Int) : Boolean = {
+ val key = grid(index - Offset)
+ items.remove(key) match {
+ case Some(item) =>
+ val tile = item.obj.Tile
+ SetCells(item.start, tile.width, tile.height)
+ true
+ case None =>
+ false
+ }
+ }
+
+ def -=(index : Int) : Boolean = Remove(index)
+
+ def Remove(guid : PlanetSideGUID) : Boolean = {
+ recursiveFindIdentifiedObject(items.keys.iterator, guid) match {
+ case Some(index) =>
+ val item = items.remove(index).get
+ val tile = item.obj.Tile
+ SetCells(item.start, tile.width, tile.height)
+ true
+ case None =>
+ false
+ }
+ }
+
+ def -=(guid : PlanetSideGUID) : Boolean = Remove(guid)
+
+ @tailrec private def recursiveFindIdentifiedObject(iter : Iterator[Int], guid : PlanetSideGUID) : Option[Int] = {
+ if(!iter.hasNext) {
+ None
+ }
+ else {
+ val index = iter.next
+ if(items(index).obj.GUID == guid) {
+ Some(index)
+ }
+ else {
+ recursiveFindIdentifiedObject(iter, guid)
+ }
+ }
+ }
+
+ /**
+ * Does this inventory contain an object with the given GUID?
+ * @param guid the GUID
+ * @return the discovered object, or `None`
+ */
+ def hasItem(guid : PlanetSideGUID) : Option[Equipment] = {
+ recursiveFindIdentifiedObject(items.keys.iterator, guid) match {
+ case Some(index) =>
+ Some(items(index).obj)
+ case None =>
+ None
+ }
+ }
+
+ /**
+ * Clear the inventory by removing all of its items.
+ * @return a `List` of the previous items in the inventory as their `InventoryItemData` tiles
+ */
+ def Clear() : List[InventoryItem] = {
+ val list = items.values.toList
+ items.clear
+ SetCellsOffset(0, width, height)
+ list
+ }
+
+ /**
+ * Change the size of the inventory, without regard for its current contents.
+ * This method replaces mutators for `Width` and `Height`.
+ * @param w the new width
+ * @param h the new height
+ * @throws IllegalArgumentException if the new size to be set is zero or less
+ */
+ def Resize(w : Int, h : Int) : Unit = {
+ if(w < 1 || h < 1) {
+ throw new IllegalArgumentException("area of inventory space must not be < 1")
+ }
+ width = w
+ height = h
+ grid = Array.fill[Int](w * h)(-1)
+ }
+}
+
+object GridInventory {
+ /**
+ * Overloaded constructor.
+ * @return a `GridInventory` object
+ */
+ def apply() : GridInventory = {
+ new GridInventory()
+ }
+
+ /**
+ * Overloaded constructor for initializing an inventory of specific dimensions.
+ * @param width the horizontal size of the inventory
+ * @param height the vertical size of the inventory
+ * @return a `GridInventory` object
+ */
+ def apply(width : Int, height : Int) : GridInventory = {
+ val obj = new GridInventory()
+ obj.Resize(width, height)
+ obj
+ }
+
+ /**
+ * Overloaded constructor for initializing an inventory of specific dimensions and index offset.
+ * @param width the horizontal size of the inventory
+ * @param height the vertical size of the inventory
+ * @param offset the effective index of the first cell in the inventory
+ * @return a `GridInventory` object
+ */
+ def apply(width : Int, height : Int, offset : Int) : GridInventory = {
+ val obj = new GridInventory()
+ obj.Resize(width, height)
+ obj.Offset = offset
+ obj
+ }
+
+ /**
+ * Accepting items that may or may not have previously been in an inventory,
+ * determine if there is a tight-fit arrangement for the items in the given inventory.
+ * Note that arrangement for future insertion.
+ * @param list a `List` of items to be potentially re-inserted
+ * @param predicate a condition to sort the previous `List` of elements
+ * @param inv the inventory in which they would be re-inserted in the future
+ * @return two `List`s of `Equipment`;
+ * the first `List` is composed of `InventoryItemData`s that will be reinserted at the new `start` index;
+ * the second list is composed of `Equipment` that will not be put back into the inventory
+ */
+ def recoverInventory(list : List[InventoryItem], inv : GridInventory, predicate : (InventoryItem, InventoryItem) => Boolean = StandardScaleSort) : (List[InventoryItem], List[Equipment]) = {
+ sortKnapsack(
+ list.sortWith(predicate),
+ inv.width,
+ inv.height
+ )
+ val (elements, out) = list.partition(p => p.start > -1)
+ elements.foreach(item => item.start += inv.Offset)
+ (elements, out.map(item => item.obj))
+ }
+
+ /**
+ * The default predicate used by the knapsack sort algorithm.
+ */
+ final val StandardScaleSort : (InventoryItem, InventoryItem) => Boolean =
+ (a, b) => {
+ val aTile = a.obj.Tile
+ val bTile = b.obj.Tile
+ if(aTile.width == bTile.width) {
+ aTile.height > bTile.height
+ }
+ else {
+ aTile.width > bTile.width
+ }
+ }
+
+ /**
+ * Start calculating the "optimal" fit for a `List` of items in an inventory of given size.
+ *
+ * The initial dimensions always fit a space of 0,0 to `width`, `height`.
+ * As locations for elements are discovered, the `start` index for that `List` element is changed in-place.
+ * If an element can not be re-inserted according to the algorithm, the `start` index is set to an invalid -1.
+ * @param list a `List` of items to be potentially re-inserted
+ * @param width the horizontal length of the inventory
+ * @param height the vertical length of the inventory
+ */
+ private def sortKnapsack(list : List[InventoryItem], width : Int, height : Int) : Unit = {
+ val root = new KnapsackNode(0, 0, width, height)
+ list.foreach(item => {
+ findKnapsackSpace(root, item.obj.Tile.width, item.obj.Tile.height) match {
+ case Some(node) =>
+ splitKnapsackSpace(node, item.obj.Tile.width, item.obj.Tile.height)
+ item.start = node.y * width + node.x
+ case _ => ;
+ item.start = -1
+ }
+ })
+ }
+
+ /**
+ * A binary tree node suitable for executing a hasty solution to the knapsack problem.
+ *
+ * All children are flush with their parent node and with each other.
+ * Horizontal space for the `down` child is emphasized over vertical space for the `right` child.
+ * By dividing and reducing a defined space like this, it can be tightly packed with a given number of elements.
+ *
+ * Due to the nature of the knapsack problem and the naivette of the algorithm, small holes in the solution are bound to crop-up.
+ * @param x the x-coordinate, upper left corner
+ * @param y the y-coordinate, upper left corner
+ * @param width the width
+ * @param height the height
+ */
+ private class KnapsackNode(var x : Int, var y : Int, var width : Int, var height : Int) {
+ private var used : Boolean = false
+ var down : Option[KnapsackNode] = None
+ var right : Option[KnapsackNode] = None
+
+ def Used : Boolean = used
+
+ /**
+ * Initialize the `down` and `right` children of this node.
+ */
+ def Split() : Unit = {
+ used = true
+ down = Some(new KnapsackNode(0,0,0,0))
+ right = Some(new KnapsackNode(0,0,0,0))
+ }
+
+ /**
+ * Change the dimensions of the node.
+ *
+ * Use: `{node}(nx, ny, nw, nh)`
+ * @param nx the new x-coordinate, upper left corner
+ * @param ny the new y-coordinate, upper left corner
+ * @param nw the new width
+ * @param nh the new height
+ */
+ def apply(nx : Int, ny : Int, nw : Int, nh : Int) : Unit = {
+ x = nx
+ y = ny
+ width = nw
+ height = nh
+ }
+ }
+
+ /**
+ * Search this node and its children for a space that can be occupied by an element of given dimensions.
+ * @param node the current node
+ * @param width width of the element
+ * @param height height of the element
+ * @return the selected node
+ */
+ private def findKnapsackSpace(node : KnapsackNode, width : Int, height : Int) : Option[KnapsackNode] = {
+ if(node.Used) {
+ findKnapsackSpace(node.right.get, width, height).orElse(findKnapsackSpace(node.down.get, width, height))
+ }
+ else if(width <= node.width && height <= node.height) {
+ Some(node)
+ }
+ else {
+ None
+ }
+ }
+
+ /**
+ * Populate the `down` and `right` nodes for the knapsack sort.
+ *
+ * This function carves node into three pieces.
+ * The third piece is the unspoken space occupied by the element of given dimensions.
+ * Specifically: `node.x`, `node.y` to `width`, `height`.
+ * @param node the current node
+ * @param width width of the element
+ * @param height height of the element
+ */
+ private def splitKnapsackSpace(node : KnapsackNode, width : Int, height : Int) : Unit = {
+ node.Split()
+ node.down.get(node.x, node.y + height, node.width, node.height - height)
+ node.right.get(node.x + width, node.y, node.width - width, height)
+ }
+}
diff --git a/common/src/main/scala/net/psforever/objects/inventory/InventoryEquipmentSlot.scala b/common/src/main/scala/net/psforever/objects/inventory/InventoryEquipmentSlot.scala
new file mode 100644
index 000000000..5e4515ed5
--- /dev/null
+++ b/common/src/main/scala/net/psforever/objects/inventory/InventoryEquipmentSlot.scala
@@ -0,0 +1,43 @@
+// Copyright (c) 2017 PSForever
+package net.psforever.objects.inventory
+
+import net.psforever.objects.OffhandEquipmentSlot
+import net.psforever.objects.equipment.{Equipment, EquipmentSize}
+
+/**
+ * A slot-like interface for a specific grid position in an inventory.
+ * The size is bound to anything that can be stowed, which encompasses most all `Equipment`.
+ * Furthermore, rather than operating on a fixed-size slot, this "slot" represents an inventory region that either includes `slot` or starts at `slot`.
+ * An object added to the underlying inventory from here can only be added with its initial point at `slot`.
+ * An object found at `slot`, however, can be removed even if the starting cell is prior to `slot.`
+ */
+class InventoryEquipmentSlot(private val slot : Int, private val inv : GridInventory) extends OffhandEquipmentSlot(EquipmentSize.Inventory) {
+ /**
+ * Attempt to stow an item into the inventory at the given position.
+ * @param assignEquipment the change in `Equipment` for this slot
+ * @return the `Equipment` in this slot
+ */
+ override def Equipment_=(assignEquipment : Option[Equipment]) : Option[Equipment] = {
+ assignEquipment match {
+ case Some(equip) =>
+ inv += slot -> equip
+ case None =>
+ inv -= slot
+ }
+ Equipment
+ }
+
+ /**
+ * Determine what `Equipment`, if any, is stowed in the inventory in the given position.
+ * @return the `Equipment` in this slot
+ */
+ override def Equipment : Option[Equipment] = {
+ inv.Items.find({ case ((_, item : InventoryItem)) => item.start == slot }) match {
+ case Some((_, item : InventoryItem)) =>
+ Some(item.obj)
+ case None =>
+ None
+ }
+ }
+}
+
diff --git a/common/src/main/scala/net/psforever/objects/inventory/InventoryItem.scala b/common/src/main/scala/net/psforever/objects/inventory/InventoryItem.scala
new file mode 100644
index 000000000..09000022a
--- /dev/null
+++ b/common/src/main/scala/net/psforever/objects/inventory/InventoryItem.scala
@@ -0,0 +1,23 @@
+// Copyright (c) 2017 PSForever
+package net.psforever.objects.inventory
+
+import net.psforever.objects.equipment.Equipment
+import net.psforever.packet.game.PlanetSideGUID
+
+/**
+ * Represent the image placard that is used to visually and spatially manipulate an item placed into the grid-like inventory.
+ * The unofficial term for this placard (the size of the placard) is a "tile."
+ * The size of the tile is usually fixed but the origin point of the tile can be changed.
+ * @param obj the item being placed into the inventory grid
+ * @param start the index of the upper-left square of the item's tile
+ */
+class InventoryItem(val obj : Equipment, var start : Int = 0) {
+ //TODO eventually move this object from storing the item directly to just storing its GUID?
+ def GUID : PlanetSideGUID = obj.GUID
+}
+
+object InventoryItem {
+ def apply(obj : Equipment, start : Int) : InventoryItem = {
+ new InventoryItem(obj, start)
+ }
+}
diff --git a/common/src/main/scala/net/psforever/objects/inventory/InventoryTile.scala b/common/src/main/scala/net/psforever/objects/inventory/InventoryTile.scala
new file mode 100644
index 000000000..c860a0c32
--- /dev/null
+++ b/common/src/main/scala/net/psforever/objects/inventory/InventoryTile.scala
@@ -0,0 +1,35 @@
+// Copyright (c) 2017 PSForever
+package net.psforever.objects.inventory
+
+/**
+ * A "tile" represents the size of the icon placard that is used by `Equipment` when placed into an inventory or visible slot.
+ * It is also used by some `ObjectDefinition`s to pass information about the size of an inventory itself.
+ * @param width the width of the tile
+ * @param height the height of the tile
+ * @throws IllegalArgumentException if either the width or the height are less than zero
+ */
+class InventoryTile(val width : Int, val height : Int) {
+ if(width < 0 || height < 0)
+ throw new IllegalArgumentException(s"tile has no area - width: $width, height: $height")
+
+ def Width : Int = width
+
+ def Height : Int = height
+}
+
+object InventoryTile {
+ final val None = InventoryTile(0,0) //technically invalid; used to indicate a vehicle with no trunk
+ final val Tile11 = InventoryTile(1,1) //placeholder size
+ final val Tile22 = InventoryTile(2,2) //grenades, boomer trigger
+ final val Tile23 = InventoryTile(2,3) //canister ammo
+ final val Tile42 = InventoryTile(4,2) //medkit
+ final val Tile33 = InventoryTile(3,3) //ammo box, pistols, ace
+ final val Tile44 = InventoryTile(4,4) //large ammo box
+ final val Tile55 = InventoryTile(5,5) //bfr ammo box
+ final val Tile63 = InventoryTile(6,3) //rifles
+ final val Tile93 = InventoryTile(9,3) //long-body weapons
+
+ def apply(w : Int, h : Int) : InventoryTile = {
+ new InventoryTile(w, h)
+ }
+}
diff --git a/common/src/main/scala/net/psforever/objects/terminals/OrderTerminalDefinition.scala b/common/src/main/scala/net/psforever/objects/terminals/OrderTerminalDefinition.scala
new file mode 100644
index 000000000..ee431ff77
--- /dev/null
+++ b/common/src/main/scala/net/psforever/objects/terminals/OrderTerminalDefinition.scala
@@ -0,0 +1,138 @@
+// Copyright (c) 2017 PSForever
+package net.psforever.objects.terminals
+
+import net.psforever.objects.InfantryLoadout.Simplification
+import net.psforever.objects.{Player, Tool}
+import net.psforever.objects.definition._
+import net.psforever.objects.equipment.Equipment
+import net.psforever.objects.inventory.InventoryItem
+import net.psforever.packet.game.ItemTransactionMessage
+
+import scala.annotation.switch
+
+class OrderTerminalDefinition extends TerminalDefinition(612) {
+ Name = "order_terminal"
+
+ /**
+ * The `Equipment` available from this `Terminal` on specific pages.
+ */
+ private val page0Stock : Map[String, ()=>Equipment] = infantryAmmunition ++ infantryWeapons
+ private val page2Stock : Map[String, ()=>Equipment] = supportAmmunition ++ supportWeapons
+
+ /**
+ * Process a `TransactionType.Buy` action by the user.
+ * @param player the player
+ * @param msg the original packet carrying the request
+ * @return an actionable message that explains how to process the request;
+ * either you attempt to purchase equipment or attempt to switch directly to a different exo-suit
+ */
+ def Buy(player : Player, msg : ItemTransactionMessage) : Terminal.Exchange = {
+ (msg.item_page : @switch) match {
+ case 0 => //Weapon tab
+ page0Stock.get(msg.item_name) match {
+ case Some(item) =>
+ Terminal.BuyEquipment(item())
+ case None =>
+ Terminal.NoDeal()
+ }
+ case 2 => //Support tab
+ page2Stock.get(msg.item_name) match {
+ case Some(item) =>
+ Terminal.BuyEquipment(item())
+ case None =>
+ Terminal.NoDeal()
+ }
+ case 3 => //Vehicle tab
+ vehicleAmmunition.get(msg.item_name) match {
+ case Some(item) =>
+ Terminal.BuyEquipment(item())
+ case None =>
+ Terminal.NoDeal()
+ }
+ case 1 => //Armor tab
+ suits.get(msg.item_name) match {
+ case Some((suit, subtype)) =>
+ Terminal.BuyExosuit(suit, subtype)
+ case None =>
+ Terminal.NoDeal()
+ }
+ case _ =>
+ Terminal.NoDeal()
+ }
+ }
+
+ /**
+ * Process a `TransactionType.Sell` action by the user.
+ * There is no specific `order_terminal` tab associated with this action.
+ * Selling `Equipment` is always permitted.
+ * @param player the player
+ * @param msg the original packet carrying the request
+ * @return an actionable message that explains how to process the request
+ */
+ def Sell(player : Player, msg : ItemTransactionMessage) : Terminal.Exchange = {
+ Terminal.SellEquipment()
+ }
+
+ /**
+ * Process a `TransactionType.InfantryLoadout` action by the user.
+ * `InfantryLoadout` objects are blueprints composed of exo-suit specifications and simplified `Equipment`-to-slot mappings.
+ * If a valid loadout is found, its data is transformed back into actual `Equipment` for return to the user.
+ * @param player the player
+ * @param msg the original packet carrying the request
+ * @return an actionable message that explains how to process the request
+ */
+ def InfantryLoadout(player : Player, msg : ItemTransactionMessage) : Terminal.Exchange = {
+ if(msg.item_page == 4) { //Favorites tab
+ player.LoadLoadout(msg.unk1) match {
+ case Some(loadout) =>
+ val holsters = loadout.Holsters.map(entry => { InventoryItem(BuildSimplifiedPattern(entry.item), entry.index) })
+ val inventory = loadout.Inventory.map(entry => { InventoryItem(BuildSimplifiedPattern(entry.item), entry.index) })
+ Terminal.InfantryLoadout(loadout.ExoSuit, loadout.Subtype, holsters, inventory)
+ case None =>
+ Terminal.NoDeal()
+ }
+ }
+ else {
+ Terminal.NoDeal()
+ }
+ }
+
+ /**
+ * Accept a simplified blueprint for some piece of `Equipment` and create an actual piece of `Equipment` based on it.
+ * Used specifically for the reconstruction of `Equipment` via an `InfantryLoadout`.
+ * @param entry the simplified blueprint
+ * @return some `Equipment` object
+ * @see `TerminalDefinition.MakeTool`
+ * `TerminalDefinition.MakeAmmoBox`
+ * `TerminalDefinition.MakeSimpleItem`
+ * `TerminalDefinition.MakeConstructionItem`
+ * `TerminalDefinition.MakeKit`
+ */
+ private def BuildSimplifiedPattern(entry : Simplification) : Equipment = {
+ import net.psforever.objects.InfantryLoadout._
+ entry match {
+ case obj : ShorthandTool =>
+ val ammo : List[AmmoBoxDefinition] = obj.ammo.map(fmode => { fmode.ammo.adef })
+ val tool = Tool(obj.tdef)
+ //makes Tools where an ammo slot may have one of its alternate ammo types
+ (0 until tool.MaxAmmoSlot).foreach(index => {
+ val slot = tool.AmmoSlots(index)
+ slot.AmmoTypeIndex += obj.ammo(index).ammoIndex
+ slot.Box = MakeAmmoBox(ammo(index), Some(obj.ammo(index).ammo.capacity))
+ })
+ tool
+
+ case obj : ShorthandAmmoBox =>
+ MakeAmmoBox(obj.adef, Some(obj.capacity))
+
+ case obj : ShorthandConstructionItem =>
+ MakeConstructionItem(obj.cdef)
+
+ case obj : ShorthandSimpleItem =>
+ MakeSimpleItem(obj.sdef)
+
+ case obj : ShorthandKit =>
+ MakeKit(obj.kdef)
+ }
+ }
+}
diff --git a/common/src/main/scala/net/psforever/objects/terminals/TemporaryTerminalMessages.scala b/common/src/main/scala/net/psforever/objects/terminals/TemporaryTerminalMessages.scala
new file mode 100644
index 000000000..85ae33758
--- /dev/null
+++ b/common/src/main/scala/net/psforever/objects/terminals/TemporaryTerminalMessages.scala
@@ -0,0 +1,13 @@
+// Copyright (c) 2017 PSForever
+package net.psforever.objects.terminals
+
+import net.psforever.types.PlanetSideEmpire
+
+//temporary location for these temporary messages
+object TemporaryTerminalMessages {
+ //TODO send original packets along with these messages
+ final case class Convert(faction : PlanetSideEmpire.Value)
+ final case class Hacked(faction : Option[PlanetSideEmpire.Value])
+ final case class Damaged(dm : Int)
+ final case class Repaired(rep : Int)
+}
diff --git a/common/src/main/scala/net/psforever/objects/terminals/Terminal.scala b/common/src/main/scala/net/psforever/objects/terminals/Terminal.scala
new file mode 100644
index 000000000..7c19d1423
--- /dev/null
+++ b/common/src/main/scala/net/psforever/objects/terminals/Terminal.scala
@@ -0,0 +1,151 @@
+// Copyright (c) 2017 PSForever
+package net.psforever.objects.terminals
+
+import akka.actor.{ActorContext, ActorRef, Props}
+import net.psforever.objects.{PlanetSideGameObject, Player}
+import net.psforever.objects.equipment.Equipment
+import net.psforever.objects.inventory.InventoryItem
+import net.psforever.packet.game.ItemTransactionMessage
+import net.psforever.types.{ExoSuitType, PlanetSideEmpire, TransactionType}
+
+/**
+ * na
+ * @param tdef the `ObjectDefinition` that constructs this object and maintains some of its immutable fields
+ */
+class Terminal(tdef : TerminalDefinition) extends PlanetSideGameObject {
+ /** Internal reference to the `Actor` for this `Terminal`, sets up by this `Terminal`. */
+ private var actor = ActorRef.noSender
+
+ /**
+ * Get access to the internal `TerminalControl` `Actor` for this `Terminal`.
+ * If called for the first time, create the said `Actor`.
+ * Must be called only after the globally unique identifier has been set.
+ * @param context the `ActorContext` under which this `Terminal`'s `Actor` will be created
+ * @return the `Terminal`'s `Actor`
+ */
+ def Actor(implicit context : ActorContext) : ActorRef = {
+ if(actor == ActorRef.noSender) {
+ actor = context.actorOf(Props(classOf[TerminalControl], this), s"${tdef.Name}_${GUID.guid}")
+ }
+ actor
+ }
+
+ //the following fields and related methods are neither finalized no integrated; GOTO Request
+ private var faction : PlanetSideEmpire.Value = PlanetSideEmpire.NEUTRAL
+ private var hackedBy : Option[PlanetSideEmpire.Value] = None
+ private var health : Int = 100 //TODO not real health value
+
+ def Faction : PlanetSideEmpire.Value = faction
+
+ def HackedBy : Option[PlanetSideEmpire.Value] = hackedBy
+
+ def Health : Int = health
+
+ def Convert(toFaction : PlanetSideEmpire.Value) : Unit = {
+ hackedBy = None
+ faction = toFaction
+ }
+
+ def HackedBy(toFaction : Option[PlanetSideEmpire.Value]) : Unit = {
+ hackedBy = if(toFaction.contains(faction)) { None } else { toFaction }
+ }
+
+ def Damaged(dam : Int) : Unit = {
+ health = Math.max(0, Health - dam)
+ }
+
+ def Repair(rep : Int) : Unit = {
+ health = Math.min(Health + rep, 100)
+ }
+
+ /**
+ * Process some `TransactionType` action requested by the user.
+ * @param player the player
+ * @param msg the original packet carrying the request
+ * @return an actionable message that explains what resulted from interacting with this `Terminal`
+ */
+ def Request(player : Player, msg : ItemTransactionMessage) : Terminal.Exchange = {
+ msg.transaction_type match {
+ case TransactionType.Buy =>
+ tdef.Buy(player, msg)
+
+ case TransactionType.Sell =>
+ tdef.Sell(player, msg)
+
+ case TransactionType.InfantryLoadout =>
+ tdef.InfantryLoadout(player, msg)
+
+ case _ =>
+ Terminal.NoDeal()
+ }
+ }
+
+ def Definition : TerminalDefinition = tdef
+}
+
+object Terminal {
+ /**
+ * Entry message into this `Terminal` that carries the request.
+ * Accessing an option in a `Terminal` normally always results in this message.
+ * @param player the player who sent this request message
+ * @param msg the original packet carrying the request
+ */
+ final case class Request(player : Player, msg : ItemTransactionMessage)
+
+ /**
+ * A basic `Trait` connecting all of the actionable `Terminal` response messages.
+ */
+ sealed trait Exchange
+
+ /**
+ * Message that carries the result of the processed request message back to the original user (`player`).
+ * @param player the player who sent this request message
+ * @param msg the original packet carrying the request
+ * @param response the result of the processed request
+ */
+ final case class TerminalMessage(player : Player, msg : ItemTransactionMessage, response : Exchange)
+
+ /**
+ * No action will result from interacting with this `Terminal`.
+ * A result of a processed request.
+ */
+ final case class NoDeal() extends Exchange
+ /**
+ * The `Player` exo-suit will be changed to the prescribed one.
+ * The subtype will be important if the user is swapping to an `ExoSuitType.MAX` exo-suit.
+ * A result of a processed request.
+ * @param exosuit the type of exo-suit
+ * @param subtype the exo-suit subtype, if any
+ */
+ final case class BuyExosuit(exosuit : ExoSuitType.Value, subtype : Int = 0) extends Exchange
+ /**
+ * A single piece of `Equipment` has been selected and will be given to the `Player`.
+ * The `Player` must decide what to do with it once it is in their control.
+ * A result of a processed request.
+ * @param item the `Equipment` being given to the player
+ */
+ final case class BuyEquipment(item : Equipment) extends Exchange
+ /**
+ * A roundabout message oft-times.
+ * Most `Terminals` should always allow `Player`s to dispose of some piece of `Equipment`.
+ * A result of a processed request.
+ */
+ //TODO if there are exceptions, find them
+ final case class SellEquipment() extends Exchange
+ /**
+ * Recover a former exo-suit and `Equipment` configuration that the `Player` possessed.
+ * A result of a processed request.
+ * @param exosuit the type of exo-suit
+ * @param subtype the exo-suit subtype, if any
+ * @param holsters the contents of the `Player`'s holsters
+ * @param inventory the contents of the `Player`'s inventory
+ */
+ final case class InfantryLoadout(exosuit : ExoSuitType.Value, subtype : Int = 0, holsters : List[InventoryItem], inventory : List[InventoryItem]) extends Exchange
+
+ import net.psforever.packet.game.PlanetSideGUID
+ def apply(guid : PlanetSideGUID, tdef : TerminalDefinition) : Terminal = {
+ val obj = new Terminal(tdef)
+ obj.GUID = guid
+ obj
+ }
+}
diff --git a/common/src/main/scala/net/psforever/objects/terminals/TerminalControl.scala b/common/src/main/scala/net/psforever/objects/terminals/TerminalControl.scala
new file mode 100644
index 000000000..7e9a71cfb
--- /dev/null
+++ b/common/src/main/scala/net/psforever/objects/terminals/TerminalControl.scala
@@ -0,0 +1,34 @@
+// Copyright (c) 2017 PSForever
+package net.psforever.objects.terminals
+
+import akka.actor.Actor
+
+/**
+ * An `Actor` that handles messages being dispatched to a specific `Terminal`.
+ *
+ * For now, the only important message being managed is `Terminal.Request`.
+ * @param term the `Terminal` object being governed
+ */
+class TerminalControl(term : Terminal) extends Actor {
+ def receive : Receive = {
+ case Terminal.Request(player, msg) =>
+ sender ! Terminal.TerminalMessage(player, msg, term.Request(player, msg))
+
+ case TemporaryTerminalMessages.Convert(fact) =>
+ term.Convert(fact)
+
+ case TemporaryTerminalMessages.Hacked(fact) =>
+ term.HackedBy(fact)
+
+ case TemporaryTerminalMessages.Damaged(dam) =>
+ term.Damaged(dam)
+
+ case TemporaryTerminalMessages.Repaired(rep) =>
+ term.Repair(rep)
+
+ case _ =>
+ sender ! Terminal.NoDeal()
+ }
+
+ override def toString : String = term.Definition.Name
+}
diff --git a/common/src/main/scala/net/psforever/objects/terminals/TerminalDefinition.scala b/common/src/main/scala/net/psforever/objects/terminals/TerminalDefinition.scala
new file mode 100644
index 000000000..a82fb739d
--- /dev/null
+++ b/common/src/main/scala/net/psforever/objects/terminals/TerminalDefinition.scala
@@ -0,0 +1,264 @@
+// Copyright (c) 2017 PSForever
+package net.psforever.objects.terminals
+
+import net.psforever.objects._
+import net.psforever.objects.definition._
+import net.psforever.objects.equipment.Equipment
+import net.psforever.packet.game.ItemTransactionMessage
+import net.psforever.types.ExoSuitType
+
+import scala.collection.immutable.HashMap
+
+/**
+ * The definition for any `Terminal`.
+ * @param objectId the object's identifier number
+ */
+abstract class TerminalDefinition(objectId : Int) extends ObjectDefinition(objectId) {
+ Name = "terminal"
+
+ /**
+ * The unimplemented functionality for this `Terminal`'s `TransactionType.Buy` activity.
+ */
+ def Buy(player : Player, msg : ItemTransactionMessage) : Terminal.Exchange
+
+ /**
+ * The unimplemented functionality for this `Terminal`'s `TransactionType.Sell` activity.
+ */
+ def Sell(player: Player, msg : ItemTransactionMessage) : Terminal.Exchange
+
+ /**
+ * The unimplemented functionality for this `Terminal`'s `TransactionType.InfantryLoadout` activity.
+ */
+ def InfantryLoadout(player : Player, msg : ItemTransactionMessage) : Terminal.Exchange
+
+ /**
+ * A `Map` of information for changing exo-suits.
+ * key - an identification string sent by the client
+ * value - a `Tuple` containing exo-suit specifications
+ */
+ protected val suits : Map[String, (ExoSuitType.Value, Int)] = Map(
+ "standard_issue_armor" -> (ExoSuitType.Standard, 0),
+ "lite_armor" -> (ExoSuitType.Agile, 0),
+ "med_armor" -> (ExoSuitType.Reinforced, 0)
+ //TODO max and infiltration suit
+ )
+
+ import net.psforever.objects.GlobalDefinitions._
+ /**
+ * A `Map` of operations for producing the `AmmoBox` `Equipment` for infantry-held weaponry.
+ * key - an identification string sent by the client
+ * value - a curried function that builds the object
+ */
+ protected val infantryAmmunition : HashMap[String, ()=>Equipment] = HashMap(
+ "9mmbullet" -> MakeAmmoBox(bullet_9mm),
+ "9mmbullet_AP" -> MakeAmmoBox(bullet_9mm_AP),
+ "shotgun_shell" -> MakeAmmoBox(shotgun_shell),
+ "shotgun_shell_AP" -> MakeAmmoBox(shotgun_shell_AP),
+ "energy_cell" -> MakeAmmoBox(energy_cell),
+ "anniversary_ammo" -> MakeAmmoBox(anniversary_ammo), //10mm multi-phase
+ "rocket" -> MakeAmmoBox(rocket),
+ "frag_cartridge" -> MakeAmmoBox(frag_cartridge),
+ "jammer_cartridge" -> MakeAmmoBox(jammer_cartridge),
+ "plasma_cartridge" -> MakeAmmoBox(plasma_cartridge),
+ "ancient_ammo_combo" -> MakeAmmoBox(ancient_ammo_combo),
+ "maelstrom_ammo" -> MakeAmmoBox(maelstrom_ammo),
+ "striker_missile_ammo" -> MakeAmmoBox(striker_missile_ammo),
+ "hunter_seeker_missile" -> MakeAmmoBox(hunter_seeker_missile), //phoenix missile
+ "lancer_cartridge" -> MakeAmmoBox(lancer_cartridge),
+ "bolt" -> MakeAmmoBox(bolt),
+ "oicw_ammo" -> MakeAmmoBox(oicw_ammo), //scorpion missile
+ "flamethrower_ammo" -> MakeAmmoBox(flamethrower_ammo)
+ )
+
+ /**
+ * A `Map` of operations for producing the `AmmoBox` `Equipment` for infantry-held utilities.
+ * key - an identification string sent by the client
+ * value - a curried function that builds the object
+ */
+ protected val supportAmmunition : HashMap[String, ()=>Equipment] = HashMap(
+ "health_canister" -> MakeAmmoBox(health_canister),
+ "armor_canister" -> MakeAmmoBox(armor_canister),
+ "upgrade_canister" -> MakeAmmoBox(upgrade_canister)
+ )
+
+ /**
+ * A `Map` of operations for producing the `AmmoBox` `Equipment` for vehicle-mounted weaponry.
+ * key - an identification string sent by the client
+ * value - a curried function that builds the object
+ */
+ protected val vehicleAmmunition : HashMap[String, ()=>Equipment] = HashMap(
+ "35mmbullet" -> MakeAmmoBox(bullet_35mm),
+ "hellfire_ammo" -> MakeAmmoBox(hellfire_ammo),
+ "liberator_bomb" -> MakeAmmoBox(liberator_bomb),
+ "25mmbullet" -> MakeAmmoBox(bullet_25mm),
+ "75mmbullet" -> MakeAmmoBox(bullet_75mm),
+ "heavy_grenade_mortar" -> MakeAmmoBox(heavy_grenade_mortar),
+ "reaver_rocket" -> MakeAmmoBox(reaver_rocket),
+ "20mmbullet" -> MakeAmmoBox(bullet_20mm),
+ "12mmbullet" -> MakeAmmoBox(bullet_12mm),
+ "wasp_rocket_ammo" -> MakeAmmoBox(wasp_rocket_ammo),
+ "wasp_gun_ammo" -> MakeAmmoBox(wasp_gun_ammo),
+ "aphelion_laser_ammo" -> MakeAmmoBox(aphelion_laser_ammo),
+ "aphelion_immolation_cannon_ammo" -> MakeAmmoBox(aphelion_immolation_cannon_ammo),
+ "aphelion_plasma_rocket_ammo" -> MakeAmmoBox(aphelion_plasma_rocket_ammo),
+ "aphelion_ppa_ammo" -> MakeAmmoBox(aphelion_ppa_ammo),
+ "aphelion_starfire_ammo" -> MakeAmmoBox(aphelion_starfire_ammo),
+ "skyguard_flak_cannon_ammo" -> MakeAmmoBox(skyguard_flak_cannon_ammo),
+ "flux_cannon_thresher_battery" -> MakeAmmoBox(flux_cannon_thresher_battery),
+ "fluxpod_ammo" -> MakeAmmoBox(fluxpod_ammo),
+ "pulse_battery" -> MakeAmmoBox(pulse_battery),
+ "heavy_rail_beam_battery" -> MakeAmmoBox(heavy_rail_beam_battery),
+ "15mmbullet" -> MakeAmmoBox(bullet_15mm),
+ "colossus_100mm_cannon_ammo" -> MakeAmmoBox(colossus_100mm_cannon_ammo),
+ "colossus_burster_ammo" -> MakeAmmoBox(colossus_burster_ammo),
+ "colossus_cluster_bomb_ammo" -> MakeAmmoBox(colossus_cluster_bomb_ammo),
+ "colossus_chaingun_ammo" -> MakeAmmoBox(colossus_chaingun_ammo),
+ "colossus_tank_cannon_ammo" -> MakeAmmoBox(colossus_tank_cannon_ammo),
+ "105mmbullet" -> MakeAmmoBox(bullet_105mm),
+ "gauss_cannon_ammo" -> MakeAmmoBox(gauss_cannon_ammo),
+ "peregrine_dual_machine_gun_ammo" -> MakeAmmoBox(peregrine_dual_machine_gun_ammo),
+ "peregrine_mechhammer_ammo" -> MakeAmmoBox(peregrine_mechhammer_ammo),
+ "peregrine_particle_cannon_ammo" -> MakeAmmoBox(peregrine_particle_cannon_ammo),
+ "peregrine_rocket_pod_ammo" -> MakeAmmoBox(peregrine_rocket_pod_ammo),
+ "peregrine_sparrow_ammo" -> MakeAmmoBox(peregrine_sparrow_ammo),
+ "150mmbullet" -> MakeAmmoBox(bullet_150mm)
+ )
+
+ /**
+ * A `Map` of operations for producing the `Tool` `Equipment` for infantry weapons.
+ * key - an identification string sent by the client
+ * value - a curried function that builds the object
+ */
+ protected val infantryWeapons : HashMap[String, ()=>Equipment] = HashMap(
+ "ilc9" -> MakeTool(ilc9, bullet_9mm),
+ "repeater" -> MakeTool(repeater, bullet_9mm),
+ "isp" -> MakeTool(isp, shotgun_shell), //amp
+ "beamer" -> MakeTool(beamer, energy_cell),
+ "suppressor" -> MakeTool(suppressor, bullet_9mm),
+ "anniversary_guna" -> MakeTool(anniversary_guna, anniversary_ammo), //tr stinger
+ "anniversary_gun" -> MakeTool(anniversary_gun, anniversary_ammo), //nc spear
+ "anniversary_gunb" -> MakeTool(anniversary_gunb, anniversary_ammo), //vs eraser
+ "cycler" -> MakeTool(cycler, bullet_9mm),
+ "gauss" -> MakeTool(gauss, bullet_9mm),
+ "pulsar" -> MakeTool(pulsar, energy_cell),
+ "punisher" -> MakeTool(punisher, List(bullet_9mm, rocket)),
+ "flechette" -> MakeTool(flechette, shotgun_shell),
+ "spiker" -> MakeTool(spiker, ancient_ammo_combo),
+ "frag_grenade" -> MakeTool(frag_grenade, frag_grenade_ammo),
+ "jammer_grenade" -> MakeTool(jammer_grenade, jammer_grenade_ammo),
+ "plasma_grenade" -> MakeTool(plasma_grenade, plasma_grenade_ammo),
+ "katana" -> MakeTool(katana, melee_ammo),
+ "chainblade" -> MakeTool(chainblade, melee_ammo),
+ "magcutter" -> MakeTool(magcutter, melee_ammo),
+ "forceblade" -> MakeTool(forceblade, melee_ammo),
+ "mini_chaingun" -> MakeTool(mini_chaingun, bullet_9mm),
+ "r_shotgun" -> MakeTool(r_shotgun, shotgun_shell), //jackhammer
+ "lasher" -> MakeTool(lasher, energy_cell),
+ "maelstrom" -> MakeTool(maelstrom, maelstrom_ammo),
+ "striker" -> MakeTool(striker, striker_missile_ammo),
+ "hunterseeker" -> MakeTool(hunterseeker, hunter_seeker_missile), //phoenix
+ "lancer" -> MakeTool(lancer, lancer_cartridge),
+ "phoenix" -> MakeTool(phoenix, phoenix_missile), //decimator
+ "rocklet" -> MakeTool(rocklet, rocket),
+ "thumper" -> MakeTool(thumper, frag_cartridge),
+ "radiator" -> MakeTool(radiator, ancient_ammo_combo),
+ "heavy_sniper" -> MakeTool(heavy_sniper, bolt), //hsr
+ "bolt_driver" -> MakeTool(bolt_driver, bolt),
+ "oicw" -> MakeTool(oicw, oicw_ammo), //scorpion
+ "flamethrower" -> MakeTool(flamethrower, flamethrower_ammo)
+ )
+
+ /**
+ * A `Map` of operations for producing the `Tool` `Equipment` for utilities.
+ * key - an identification string sent by the client
+ * value - a curried function that builds the object
+ */
+ protected val supportWeapons : HashMap[String, ()=>Equipment] = HashMap(
+ "medkit" -> MakeKit(medkit),
+ "super_medkit" -> MakeKit(super_medkit),
+ "super_armorkit" -> MakeKit(super_armorkit),
+ "super_staminakit" -> MakeKit(super_staminakit),
+ "medicalapplicator" -> MakeTool(medicalapplicator, health_canister),
+ "bank" -> MakeTool(bank, armor_canister),
+ "nano_dispenser" -> MakeTool(nano_dispenser, armor_canister),
+ //TODO "ace" -> MakeConstructionItem(ace),
+ //TODO "advanced_ace" -> MakeConstructionItem(advanced_ace),
+ "remote_electronics_kit" -> MakeSimpleItem(remote_electronics_kit),
+ "trek" -> MakeTool(trek, trek_ammo),
+ "command_detonater" -> MakeSimpleItem(command_detonater),
+ "flail_targeting_laser" -> MakeSimpleItem(flail_targeting_laser)
+ )
+
+ /**
+ * Create a new `Tool` from provided `EquipmentDefinition` objects.
+ * @param tdef the `ToolDefinition` objects
+ * @param adef an `AmmoBoxDefinition` object
+ * @return a partial function that, when called, creates the piece of `Equipment`
+ */
+ protected def MakeTool(tdef : ToolDefinition, adef : AmmoBoxDefinition)() : Tool = MakeTool(tdef, List(adef))
+
+ /**
+ * Create a new `Tool` from provided `EquipmentDefinition` objects.
+ * Only use this function to create default `Tools` with the default parameters.
+ * For example, loadouts can retain `Tool` information that utilizes alternate, valid ammunition types;
+ * and, this method function will not construct a complete object if provided that information.
+ * @param tdef the `ToolDefinition` objects
+ * @param adefs a `List` of `AmmoBoxDefinition` objects
+ * @return a curried function that, when called, creates the piece of `Equipment`
+ * @see `GlobalDefinitions`
+ * @see `OrderTerminalDefinition.BuildSimplifiedPattern`
+ */
+ protected def MakeTool(tdef : ToolDefinition, adefs : List[AmmoBoxDefinition])() : Tool = {
+ val obj = Tool(tdef)
+ (0 until obj.MaxAmmoSlot).foreach(index => {
+ val aType = adefs(index)
+ val ammo = MakeAmmoBox(aType, Some(obj.Definition.FireModes(index).Magazine)) //make internal magazine, full
+ (obj.AmmoSlots(index).Box = ammo) match {
+ case Some(_) => ; //this means it worked
+ case None =>
+ org.log4s.getLogger("TerminalDefinition").warn(s"plans do not match definition: trying to feed ${ammo.AmmoType} ammunition into Tool (${obj.Definition.ObjectId} @ $index)")
+ }
+ })
+ obj
+ }
+
+ /**
+ * Create a new `AmmoBox` from provided `EquipmentDefinition` objects.
+ * @param adef the `AmmoBoxDefinition` object
+ * @param capacity optional number of rounds in this `AmmoBox`, deviating from the `EquipmentDefinition`;
+ * necessary for constructing the magazine (`AmmoSlot`) of `Tool`s
+ * @return a curried function that, when called, creates the piece of `Equipment`
+ * @see `GlobalDefinitions`
+ */
+ protected def MakeAmmoBox(adef : AmmoBoxDefinition, capacity : Option[Int] = None)() : AmmoBox = {
+ val obj = AmmoBox(adef)
+ if(capacity.isDefined) {
+ obj.Capacity = capacity.get
+ }
+ obj
+ }
+
+ /**
+ * Create a new `Kit` from provided `EquipmentDefinition` objects.
+ * @param kdef the `KitDefinition` object
+ * @return a curried function that, when called, creates the piece of `Equipment`
+ * @see `GlobalDefinitions`
+ */
+ protected def MakeKit(kdef : KitDefinition)() : Kit = Kit(kdef)
+
+ /**
+ * Create a new `SimpleItem` from provided `EquipmentDefinition` objects.
+ * @param sdef the `SimpleItemDefinition` object
+ * @return a curried function that, when called, creates the piece of `Equipment`
+ * @see `GlobalDefinitions`
+ */
+ protected def MakeSimpleItem(sdef : SimpleItemDefinition)() : SimpleItem = SimpleItem(sdef)
+
+ /**
+ * Create a new `ConstructionItem` from provided `EquipmentDefinition` objects.
+ * @param cdef the `ConstructionItemDefinition` object
+ * @return a curried function that, when called, creates the piece of `Equipment`
+ * @see `GlobalDefinitions`
+ */
+ protected def MakeConstructionItem(cdef : ConstructionItemDefinition)() : ConstructionItem = ConstructionItem(cdef)
+}
diff --git a/common/src/main/scala/net/psforever/objects/vehicles/ANTResourceUtility.scala b/common/src/main/scala/net/psforever/objects/vehicles/ANTResourceUtility.scala
new file mode 100644
index 000000000..b7a32caa1
--- /dev/null
+++ b/common/src/main/scala/net/psforever/objects/vehicles/ANTResourceUtility.scala
@@ -0,0 +1,31 @@
+// Copyright (c) 2017 PSForever
+package net.psforever.objects.vehicles
+
+import net.psforever.objects.Vehicle
+
+/**
+ * A `Utility` designed to simulate the NTU-distributive functions of an ANT.
+ * @param objectId the object id that is associated with this sort of `Utility`
+ * @param vehicle the `Vehicle` to which this `Utility` is attached
+ */
+class ANTResourceUtility(objectId : Int, vehicle : Vehicle) extends Utility(objectId, vehicle) {
+ private var currentNTU : Int = 0
+
+ def NTU : Int = currentNTU
+
+ def NTU_=(ntu : Int) : Int = {
+ currentNTU = ntu
+ currentNTU = math.max(math.min(currentNTU, MaxNTU), 0)
+ NTU
+ }
+
+ def MaxNTU : Int = ANTResourceUtility.MaxNTU
+}
+
+object ANTResourceUtility {
+ private val MaxNTU : Int = 300 //TODO what should this value be?
+
+ def apply(objectId : Int, vehicle : Vehicle) : ANTResourceUtility = {
+ new ANTResourceUtility(objectId, vehicle)
+ }
+}
\ No newline at end of file
diff --git a/common/src/main/scala/net/psforever/objects/vehicles/Seat.scala b/common/src/main/scala/net/psforever/objects/vehicles/Seat.scala
new file mode 100644
index 000000000..263fa2e22
--- /dev/null
+++ b/common/src/main/scala/net/psforever/objects/vehicles/Seat.scala
@@ -0,0 +1,134 @@
+// Copyright (c) 2017 PSForever
+package net.psforever.objects.vehicles
+
+import net.psforever.objects.definition.SeatDefinition
+import net.psforever.objects.{Player, Vehicle}
+import net.psforever.packet.game.PlanetSideGUID
+import net.psforever.types.PlanetSideEmpire
+
+/**
+ * Server-side support for a slot that infantry players can occupy, ostensibly called a "seat" and treated like a "seat."
+ * (Players can sit in it.)
+ * @param seatDef the Definition that constructs this item and maintains some of its immutable fields
+ * @param vehicle the vehicle where this seat is installed
+ */
+class Seat(private val seatDef : SeatDefinition, private val vehicle : Vehicle) {
+ private var occupant : Option[PlanetSideGUID] = None
+ private var lockState : VehicleLockState.Value = VehicleLockState.Empire
+
+ /**
+ * The faction association of this `Seat` is tied directly to the connected `Vehicle`.
+ * @return the faction association
+ */
+ def Faction : PlanetSideEmpire.Value = {
+ vehicle.Faction
+ }
+
+ /**
+ * Is this seat occupied?
+ * @return the GUID of the player sitting in this seat, or `None` if it is left vacant
+ */
+ def Occupant : Option[PlanetSideGUID] = {
+ this.occupant
+ }
+
+ /**
+ * The player is trying to sit down.
+ * Seats are exclusive positions that can only hold one occupant at a time.
+ * @param player the player who wants to sit, or `None` if the occupant is getting up
+ * @return the GUID of the player sitting in this seat, or `None` if it is left vacant
+ */
+ def Occupant_=(player : Option[Player]) : Option[PlanetSideGUID] = {
+ if(player.isDefined) {
+ if(this.occupant.isEmpty) {
+ this.occupant = Some(player.get.GUID)
+ }
+ }
+ else {
+ this.occupant = None
+ }
+ this.occupant
+ }
+
+ /**
+ * Is this seat occupied?
+ * @return `true`, if it is occupied; `false`, otherwise
+ */
+ def isOccupied : Boolean = {
+ this.occupant.isDefined
+ }
+
+ def SeatLockState : VehicleLockState.Value = {
+ this.lockState
+ }
+
+ def SeatLockState_=(lockState : VehicleLockState.Value) : VehicleLockState.Value = {
+ this.lockState = lockState
+ SeatLockState
+ }
+
+ def ArmorRestriction : SeatArmorRestriction.Value = {
+ seatDef.ArmorRestriction
+ }
+
+ def Bailable : Boolean = {
+ seatDef.Bailable
+ }
+
+ def ControlledWeapon : Option[Int] = {
+ seatDef.ControlledWeapon
+ }
+
+ /**
+ * Given a player, can they access this `Seat` under its current restrictions and permissions.
+ * @param player the player who wants to sit
+ * @return `true` if the player can sit down in this `Seat`; `false`, otherwise
+ */
+ def CanUseSeat(player : Player) : Boolean = {
+ var access : Boolean = false
+ val owner : Option[PlanetSideGUID] = vehicle.Owner
+ lockState match {
+ case VehicleLockState.Locked =>
+ access = owner.isEmpty || (owner.isDefined && player.GUID == owner.get)
+ case VehicleLockState.Group =>
+ access = Faction == player.Faction //TODO this is not correct
+ case VehicleLockState.Empire =>
+ access = Faction == player.Faction
+ }
+ access
+ }
+
+ /**
+ * Override the string representation to provide additional information.
+ * @return the string output
+ */
+ override def toString : String = {
+ Seat.toString(this)
+ }
+}
+
+object Seat {
+ /**
+ * Overloaded constructor.
+ * @param vehicle the vehicle where this seat is installed
+ * @return a `Seat` object
+ */
+ def apply(seatDef : SeatDefinition, vehicle : Vehicle) : Seat = {
+ new Seat(seatDef, vehicle)
+ }
+
+ /**
+ * Provide a fixed string representation.
+ * @return the string output
+ */
+ def toString(obj : Seat) : String = {
+ val weaponStr = if(obj.ControlledWeapon.isDefined) { " (gunner)" } else { "" }
+ val seatStr = if(obj.isOccupied) {
+ "occupied by %d".format(obj.Occupant.get.guid)
+ }
+ else {
+ "unoccupied"
+ }
+ s"{Seat$weaponStr: $seatStr}"
+ }
+}
diff --git a/common/src/main/scala/net/psforever/objects/vehicles/SeatArmorRestriction.scala b/common/src/main/scala/net/psforever/objects/vehicles/SeatArmorRestriction.scala
new file mode 100644
index 000000000..391b3d86e
--- /dev/null
+++ b/common/src/main/scala/net/psforever/objects/vehicles/SeatArmorRestriction.scala
@@ -0,0 +1,18 @@
+// Copyright (c) 2017 PSForever
+package net.psforever.objects.vehicles
+
+/**
+ * An `Enumeration` of exo-suit-based seat access restrictions.
+ *
+ * The default value is `NoMax` as that is the most common seat.
+ * `NoReinforcedOrMax` is next most common.
+ * `MaxOnly` is a rare seat restriction found in pairs on Galaxies and on the large "Ground Transport" vehicles.
+ */
+object SeatArmorRestriction extends Enumeration {
+ type Type = Value
+
+ val MaxOnly,
+ NoMax,
+ NoReinforcedOrMax
+ = Value
+}
diff --git a/common/src/main/scala/net/psforever/objects/vehicles/Utility.scala b/common/src/main/scala/net/psforever/objects/vehicles/Utility.scala
new file mode 100644
index 000000000..172d7ed1e
--- /dev/null
+++ b/common/src/main/scala/net/psforever/objects/vehicles/Utility.scala
@@ -0,0 +1,104 @@
+// Copyright (c) 2017 PSForever
+package net.psforever.objects.vehicles
+
+import net.psforever.objects.entity.IdentifiableEntity
+import net.psforever.objects.Vehicle
+import net.psforever.packet.game.PlanetSideGUID
+import net.psforever.types.PlanetSideEmpire
+
+import scala.annotation.switch
+
+/**
+ * A `Utility` represents an unknown but functional entity that is attached to a `Vehicle` and is not a weapon or a seat.
+ * This is a placeholder solution until a better system is established.
+ * @param objectId the object id that is associated with this sort of `Utility`
+ * @param vehicle the `Vehicle` to which this `Utility` is attached
+ */
+class Utility(val objectId : Int, vehicle : Vehicle) extends IdentifiableEntity {
+ private var active : Boolean = false
+
+ /**
+ * The faction association of this `Utility` is tied directly to the connected `Vehicle`.
+ * @return the faction association
+ */
+ def Faction : PlanetSideEmpire.Value = {
+ vehicle.Faction
+ }
+
+ /**
+ * An "active" `Utility` can be used by players; an "inactive" one can not be used in its current state.
+ * @return whether this `Utility` is active.
+ */
+ def ActiveState : Boolean = {
+ this.active
+ }
+
+ /**
+ * Change the "active" state of this `Utility`.
+ * @param state the new active state
+ * @return the current active state after being changed
+ */
+ def ActiveState_=(state : Boolean) : Boolean = {
+ this.active = state
+ state
+ }
+
+ /**
+ * Override the string representation to provide additional information.
+ * @return the string output
+ */
+ override def toString : String = {
+ Utility.toString(this)
+ }
+}
+
+object Utility {
+ /**
+ * An overloaded constructor.
+ * @param objectId the object id the is associated with this sort of `Utility`
+ * @param vehicle the `Vehicle` to which this `Utility` is attached
+ * @return a `Utility` object
+ */
+ def apply(objectId : Int, vehicle : Vehicle) : Utility = {
+ new Utility(objectId, vehicle)
+ }
+
+ /**
+ * An overloaded constructor.
+ * @param objectId the object id the is associated with this sort of `Utility`
+ * @param vehicle the `Vehicle` to which this `Utility` is attached
+ * @return a `Utility` object
+ */
+ def apply(guid : PlanetSideGUID, objectId : Int, vehicle : Vehicle) : Utility = {
+ val obj = new Utility(objectId, vehicle)
+ obj.GUID = guid
+ obj
+ }
+
+ /**
+ * Create one of a specific type of utilities.
+ * @param objectId the object id that is associated with this sort of `Utility`
+ * @param vehicle the `Vehicle` to which this `Utility` is attached
+ * @return a permitted `Utility` object
+ */
+ def Select(objectId : Int, vehicle : Vehicle) : Utility = {
+ (objectId : @switch) match {
+ case 60 => //this is the object id of an ANT
+ ANTResourceUtility(objectId, vehicle)
+
+ case 49 | 519 | 613 | 614 => //ams parts
+ Utility(objectId, vehicle)
+
+ case _ =>
+ throw new IllegalArgumentException(s"the requested objectID #$objectId is not accepted as a valid Utility")
+ }
+ }
+
+ /**
+ * Provide a fixed string representation.
+ * @return the string output
+ */
+ def toString(obj : Utility) : String = {
+ s"{utility-${obj.objectId}}"
+ }
+}
diff --git a/common/src/main/scala/net/psforever/objects/vehicles/VehicleLockState.scala b/common/src/main/scala/net/psforever/objects/vehicles/VehicleLockState.scala
new file mode 100644
index 000000000..661567019
--- /dev/null
+++ b/common/src/main/scala/net/psforever/objects/vehicles/VehicleLockState.scala
@@ -0,0 +1,14 @@
+// Copyright (c) 2017 PSForever
+package net.psforever.objects.vehicles
+
+/**
+ * An `Enumeration` of various access states for vehicle components, such as the seats and the trunk.
+ */
+object VehicleLockState extends Enumeration {
+ type Type = Value
+
+ val Empire, //owner's whole faction
+ Group, //owner's squad/platoon only
+ Locked //owner only
+ = Value
+}
diff --git a/common/src/main/scala/net/psforever/packet/GamePacketOpcode.scala b/common/src/main/scala/net/psforever/packet/GamePacketOpcode.scala
index a502732a5..02b1db55a 100644
--- a/common/src/main/scala/net/psforever/packet/GamePacketOpcode.scala
+++ b/common/src/main/scala/net/psforever/packet/GamePacketOpcode.scala
@@ -429,7 +429,7 @@ object GamePacketOpcode extends Enumeration {
case 0x5b => noDecoder(OrbitalShuttleTimeMsg)
case 0x5c => noDecoder(AIDamage)
case 0x5d => game.DeployObjectMessage.decode
- case 0x5e => noDecoder(FavoritesRequest)
+ case 0x5e => game.FavoritesRequest.decode
case 0x5f => noDecoder(FavoritesResponse)
// OPCODES 0x60-6f
diff --git a/common/src/main/scala/net/psforever/packet/game/AvatarImplantMessage.scala b/common/src/main/scala/net/psforever/packet/game/AvatarImplantMessage.scala
index b87428fb2..424a14daf 100644
--- a/common/src/main/scala/net/psforever/packet/game/AvatarImplantMessage.scala
+++ b/common/src/main/scala/net/psforever/packet/game/AvatarImplantMessage.scala
@@ -1,49 +1,16 @@
// Copyright (c) 2017 PSForever
package net.psforever.packet.game
-import net.psforever.packet.{GamePacketOpcode, Marshallable, PacketHelpers, PlanetSideGamePacket}
+import net.psforever.packet.{GamePacketOpcode, Marshallable, PlanetSideGamePacket}
+import net.psforever.types.ImplantType
import scodec.Codec
import scodec.codecs._
-/**
- * An `Enumeration` of the available implants.
- */
-object ImplantType extends Enumeration {
- type Type = Value
- val AdvancedRegen,
- Targeting,
- AudioAmplifier,
- DarklightVision,
- MeleeBooster,
- PersonalShield,
- RangeMagnifier,
- Unknown7,
- SilentRun,
- Surge = Value
-
- implicit val codec = PacketHelpers.createEnumerationCodec(this, uint4L)
-}
-
/**
* Change the state of the implant.
- * Write better comments.
*
- * Implant:
- * `
- * 00 - Regeneration (advanced_regen)
- * 01 - Enhanced Targeting (targeting)
- * 02 - Audio Amplifier (audio_amplifier)
- * 03 - Darklight Vision (darklight_vision)
- * 04 - Melee Booster (melee_booster)
- * 05 - Personal Shield (personal_shield)
- * 06 - Range Magnifier (range_magnifier)
- * 07 - `None`
- * 08 - Sensor Shield (silent_run)
- * 09 - Surge (surge)
- * `
- *
- * Exploration
- * Where is Second Wind (second_wind)?
+ * The implant Second Wind is technically an invalid `ImplantType` for this packet.
+ * This owes to the unique activation trigger for that implant - a near-death experience of ~0HP.
* @param player_guid the player
* @param unk1 na
* @param unk2 na
diff --git a/common/src/main/scala/net/psforever/packet/game/BeginZoningMessage.scala b/common/src/main/scala/net/psforever/packet/game/BeginZoningMessage.scala
index bcdf964c8..f4cc6df2e 100644
--- a/common/src/main/scala/net/psforever/packet/game/BeginZoningMessage.scala
+++ b/common/src/main/scala/net/psforever/packet/game/BeginZoningMessage.scala
@@ -5,7 +5,8 @@ import net.psforever.packet.{GamePacketOpcode, Marshallable, PacketHelpers, Plan
import scodec.Codec
/**
- * Dispatched by the client after the current map has been fully loaded locally and its objects are ready to be initialized.
+ * Dispatched by the client after the current map has been fully loaded locally and its objects are ready to be initialized.
+ * This packet is a direct response to `LoadMapMessage`.
*
* When the server receives the packet, for each object on that map, it sends the packets to the client:
* - `SetEmpireMessage`
diff --git a/common/src/main/scala/net/psforever/packet/game/FavoritesRequest.scala b/common/src/main/scala/net/psforever/packet/game/FavoritesRequest.scala
new file mode 100644
index 000000000..3d9af5689
--- /dev/null
+++ b/common/src/main/scala/net/psforever/packet/game/FavoritesRequest.scala
@@ -0,0 +1,38 @@
+// Copyright (c) 2017 PSForever
+package net.psforever.packet.game
+
+import net.psforever.packet.{GamePacketOpcode, Marshallable, PacketHelpers, PlanetSideGamePacket}
+import scodec.Codec
+import scodec.codecs._
+
+object FavoritesAction extends Enumeration {
+ type Type = Value
+
+ val Unknown,
+ Save,
+ Delete = Value
+
+ implicit val codec = PacketHelpers.createEnumerationCodec(this, uint2L)
+}
+
+final case class FavoritesRequest(player_guid : PlanetSideGUID,
+ unk : Int,
+ action : FavoritesAction.Value,
+ line : Int,
+ label : Option[String])
+ extends PlanetSideGamePacket {
+ type Packet = FavoritesRequest
+ def opcode = GamePacketOpcode.FavoritesRequest
+ def encode = FavoritesRequest.encode(this)
+}
+
+object FavoritesRequest extends Marshallable[FavoritesRequest] {
+ implicit val codec : Codec[FavoritesRequest] = (
+ ("player_guid" | PlanetSideGUID.codec) ::
+ ("unk" | uint2L) ::
+ (("action" | FavoritesAction.codec) >>:~ { action =>
+ ("line" | uint4L) ::
+ conditional(action == FavoritesAction.Save, "label" | PacketHelpers.encodedWideString)
+ })
+ ).as[FavoritesRequest]
+}
diff --git a/common/src/main/scala/net/psforever/packet/game/PlanetsideAttributeMessage.scala b/common/src/main/scala/net/psforever/packet/game/PlanetsideAttributeMessage.scala
index f2faa5aad..125e521de 100644
--- a/common/src/main/scala/net/psforever/packet/game/PlanetsideAttributeMessage.scala
+++ b/common/src/main/scala/net/psforever/packet/game/PlanetsideAttributeMessage.scala
@@ -48,7 +48,7 @@ import scodec.codecs._
* 25 : BFR Anti Infantry
* 26 : ?! Removed Cert ?
* 27 : ?! Removed Cert ?
- * 28 : Reinforced ExoSuit
+ * 28 : Reinforced ExoSuitDefinition
* 29 : Infiltration Suit
* 30 : MAX (Burster)
* 31 : MAX (Dual-Cycler)
diff --git a/common/src/main/scala/net/psforever/packet/game/objectcreate/InventoryData.scala b/common/src/main/scala/net/psforever/packet/game/objectcreate/InventoryData.scala
index ef4fa92e6..f21590b4b 100644
--- a/common/src/main/scala/net/psforever/packet/game/objectcreate/InventoryData.scala
+++ b/common/src/main/scala/net/psforever/packet/game/objectcreate/InventoryData.scala
@@ -1,7 +1,7 @@
// Copyright (c) 2017 PSForever
package net.psforever.packet.game.objectcreate
-import InventoryItem._
+import InventoryItemData._
import net.psforever.packet.PacketHelpers
import scodec.Codec
import scodec.codecs._
@@ -19,7 +19,7 @@ import shapeless.{::, HNil}
*
* Inventories are usually prefaced with a single bit value not accounted for here to switch them "on."
* @param contents the items in the inventory
- * @see `InventoryItem`
+ * @see `InventoryItemData`
*/
final case class InventoryData(contents : List[InventoryItem] = List.empty) extends StreamBitSize {
override def bitsize : Long = {
@@ -57,10 +57,10 @@ object InventoryData {
/**
* A `Codec` for `0x17` `ObjectCreateMessage` data.
*/
- val codec : Codec[InventoryData] = codec(InventoryItem.codec)
+ val codec : Codec[InventoryData] = codec(InventoryItemData.codec)
/**
* A `Codec` for `0x18` `ObjectCreateDetailedMessage` data.
*/
- val codec_detailed : Codec[InventoryData] = codec(InventoryItem.codec_detailed)
+ val codec_detailed : Codec[InventoryData] = codec(InventoryItemData.codec_detailed)
}
diff --git a/common/src/main/scala/net/psforever/packet/game/objectcreate/InventoryItem.scala b/common/src/main/scala/net/psforever/packet/game/objectcreate/InventoryItemData.scala
similarity index 82%
rename from common/src/main/scala/net/psforever/packet/game/objectcreate/InventoryItem.scala
rename to common/src/main/scala/net/psforever/packet/game/objectcreate/InventoryItemData.scala
index 4b6a0191b..b07e3500a 100644
--- a/common/src/main/scala/net/psforever/packet/game/objectcreate/InventoryItem.scala
+++ b/common/src/main/scala/net/psforever/packet/game/objectcreate/InventoryItemData.scala
@@ -5,21 +5,21 @@ import net.psforever.packet.game.PlanetSideGUID
import scodec.Codec
/**
- * Mask the use of `InternalSlot` using a fake class called an `InventoryItem`.
+ * Mask the use of `InternalSlot` using a fake class called an `InventoryItemData`.
*/
-object InventoryItem {
+object InventoryItemData {
/**
- * Constructor for creating an `InventoryItem`.
+ * Constructor for creating an `InventoryItemData`.
* @param guid the GUID this object will be assigned
* @param slot a parent-defined slot identifier that explains where the child is to be attached to the parent
* @param obj the data used as representation of the object to be constructed
- * @return an `InventoryItem` object
+ * @return an `InventoryItemData` object
*/
def apply(objClass : Int, guid : PlanetSideGUID, slot : Int, obj : ConstructorData) : InventoryItem =
InternalSlot(objClass, guid, slot, obj)
/**
- * Alias `InventoryItem` to `InternalSlot`.
+ * Alias `InventoryItemData` to `InternalSlot`.
*/
type InventoryItem = InternalSlot
diff --git a/common/src/main/scala/net/psforever/packet/game/objectcreate/MountItem.scala b/common/src/main/scala/net/psforever/packet/game/objectcreate/MountItem.scala
index 0af87b305..922bade80 100644
--- a/common/src/main/scala/net/psforever/packet/game/objectcreate/MountItem.scala
+++ b/common/src/main/scala/net/psforever/packet/game/objectcreate/MountItem.scala
@@ -13,7 +13,7 @@ object MountItem {
* @param guid the GUID this object will be assigned
* @param slot a parent-defined slot identifier that explains where the child is to be attached to the parent
* @param obj the data used as representation of the object to be constructed
- * @return an `InventoryItem` object
+ * @return an `InventoryItemData` object
*/
def apply(objClass : Int, guid : PlanetSideGUID, slot : Int, obj : ConstructorData) : MountItem =
InternalSlot(objClass, guid, slot, obj)
diff --git a/common/src/main/scala/net/psforever/packet/game/objectcreate/ObjectClass.scala b/common/src/main/scala/net/psforever/packet/game/objectcreate/ObjectClass.scala
index 030def3d9..84fd562d2 100644
--- a/common/src/main/scala/net/psforever/packet/game/objectcreate/ObjectClass.scala
+++ b/common/src/main/scala/net/psforever/packet/game/objectcreate/ObjectClass.scala
@@ -275,7 +275,7 @@ object ObjectClass {
final val bank = 132
final val nano_dispenser = 577
final val command_detonater = 213
- final val laze_pointer = 297
+ final val flail_targeting_laser = 297
//ace deployables
final val ace = 32
final val advanced_ace = 39
@@ -612,7 +612,7 @@ object ObjectClass {
case ObjectClass.applicator => ConstructorData.genericCodec(DetailedWeaponData.codec, "tool")
case ObjectClass.bank => ConstructorData.genericCodec(DetailedWeaponData.codec, "tool")
case ObjectClass.command_detonater => ConstructorData.genericCodec(DetailedCommandDetonaterData.codec, "tool")
- case ObjectClass.laze_pointer => ConstructorData.genericCodec(DetailedWeaponData.codec, "tool")
+ case ObjectClass.flail_targeting_laser => ConstructorData.genericCodec(DetailedCommandDetonaterData.codec, "tool")
case ObjectClass.medicalapplicator => ConstructorData.genericCodec(DetailedWeaponData.codec, "tool")
case ObjectClass.nano_dispenser => ConstructorData.genericCodec(DetailedWeaponData.codec, "tool")
case ObjectClass.remote_electronics_kit => ConstructorData.genericCodec(DetailedREKData.codec, "tool")
@@ -894,7 +894,7 @@ object ObjectClass {
case ObjectClass.applicator => ConstructorData.genericCodec(WeaponData.codec, "tool")
case ObjectClass.bank => ConstructorData.genericCodec(WeaponData.codec, "tool")
case ObjectClass.command_detonater => ConstructorData.genericCodec(CommandDetonaterData.codec, "tool")
- case ObjectClass.laze_pointer => ConstructorData.genericCodec(WeaponData.codec, "tool")
+ case ObjectClass.flail_targeting_laser => ConstructorData.genericCodec(CommandDetonaterData.codec, "tool")
case ObjectClass.medicalapplicator => ConstructorData.genericCodec(WeaponData.codec, "tool")
case ObjectClass.nano_dispenser => ConstructorData.genericCodec(WeaponData.codec, "tool")
case ObjectClass.remote_electronics_kit => ConstructorData.genericCodec(REKData.codec, "tool")
@@ -1117,7 +1117,7 @@ object ObjectClass {
case ObjectClass.applicator => DroppedItemData.genericCodec(WeaponData.codec, "tool")
case ObjectClass.bank => DroppedItemData.genericCodec(WeaponData.codec, "tool")
case ObjectClass.command_detonater => DroppedItemData.genericCodec(CommandDetonaterData.codec, "tool")
- case ObjectClass.laze_pointer => DroppedItemData.genericCodec(WeaponData.codec, "tool")
+ case ObjectClass.flail_targeting_laser => DroppedItemData.genericCodec(CommandDetonaterData.codec, "tool")
case ObjectClass.medicalapplicator => DroppedItemData.genericCodec(WeaponData.codec, "tool")
case ObjectClass.nano_dispenser => DroppedItemData.genericCodec(WeaponData.codec, "tool")
case ObjectClass.remote_electronics_kit => DroppedItemData.genericCodec(REKData.codec, " tool")
diff --git a/common/src/main/scala/net/psforever/types/ImplantType.scala b/common/src/main/scala/net/psforever/types/ImplantType.scala
new file mode 100644
index 000000000..3449c0e45
--- /dev/null
+++ b/common/src/main/scala/net/psforever/types/ImplantType.scala
@@ -0,0 +1,38 @@
+// Copyright (c) 2017 PSForever
+package net.psforever.types
+
+import net.psforever.packet.PacketHelpers
+import scodec.codecs._
+
+/**
+ * An `Enumeration` of the available implants.
+ *
+ * Implant:
+ * `
+ * 00 - Regeneration (advanced_regen)
+ * 01 - Enhanced Targeting (targeting)
+ * 02 - Audio Amplifier (audio_amplifier)
+ * 03 - Darklight Vision (darklight_vision)
+ * 04 - Melee Booster (melee_booster)
+ * 05 - Personal Shield (personal_shield)
+ * 06 - Range Magnifier (range_magnifier)
+ * 07 - Second Wind `(na)`
+ * 08 - Sensor Shield (silent_run)
+ * 09 - Surge (surge)
+ * `
+ */
+object ImplantType extends Enumeration {
+ type Type = Value
+ val AdvancedRegen,
+ Targeting,
+ AudioAmplifier,
+ DarklightVision,
+ MeleeBooster,
+ PersonalShield,
+ RangeMagnifier,
+ SecondWind, //technically
+ SilentRun,
+ Surge = Value
+
+ implicit val codec = PacketHelpers.createEnumerationCodec(this, uint4L)
+}
diff --git a/common/src/main/scala/net/psforever/types/TransactionType.scala b/common/src/main/scala/net/psforever/types/TransactionType.scala
index dd2c0cc72..bf76e0257 100644
--- a/common/src/main/scala/net/psforever/types/TransactionType.scala
+++ b/common/src/main/scala/net/psforever/types/TransactionType.scala
@@ -12,7 +12,7 @@ object TransactionType extends Enumeration {
Sell, // or forget on certif term
Unk4,
Unk5,
- Infantry_Loadout,
+ InfantryLoadout,
Unk7
= Value
diff --git a/common/src/test/scala/game/AvatarImplantMessageTest.scala b/common/src/test/scala/game/AvatarImplantMessageTest.scala
index 3c2ace41d..166632534 100644
--- a/common/src/test/scala/game/AvatarImplantMessageTest.scala
+++ b/common/src/test/scala/game/AvatarImplantMessageTest.scala
@@ -4,6 +4,7 @@ package game
import org.specs2.mutable._
import net.psforever.packet._
import net.psforever.packet.game._
+import net.psforever.types.ImplantType
import scodec.bits._
class AvatarImplantMessageTest extends Specification {
diff --git a/common/src/test/scala/game/FavoritesRequestTest.scala b/common/src/test/scala/game/FavoritesRequestTest.scala
new file mode 100644
index 000000000..23adfccc1
--- /dev/null
+++ b/common/src/test/scala/game/FavoritesRequestTest.scala
@@ -0,0 +1,33 @@
+// Copyright (c) 2017 PSForever
+package game
+
+import org.specs2.mutable._
+import net.psforever.packet._
+import net.psforever.packet.game._
+import scodec.bits._
+
+class FavoritesRequestTest extends Specification {
+ val stringInfantry = hex"5E 4B00 1187 4500 7800 6100 6D00 7000 6C00 6500"
+
+ "decode (for infantry)" in {
+ PacketCoding.DecodePacket(stringInfantry).require match {
+ case FavoritesRequest(player_guid, unk, action, line, label) =>
+ player_guid mustEqual PlanetSideGUID(75)
+ unk mustEqual 0
+ action mustEqual FavoritesAction.Save
+ line mustEqual 1
+ label.isDefined mustEqual true
+ label.get mustEqual "Example"
+ case _ =>
+ ko
+ }
+ }
+
+ "encode (for infantry)" in {
+ val msg = FavoritesRequest(PlanetSideGUID(75), 0, FavoritesAction.Save, 1, Some("Example"))
+ val pkt = PacketCoding.EncodePacket(msg).require.toByteVector
+
+ pkt mustEqual stringInfantry
+ }
+}
+
diff --git a/common/src/test/scala/game/ObjectCreateDetailedMessageTest.scala b/common/src/test/scala/game/ObjectCreateDetailedMessageTest.scala
index f818ac402..ee9e38d19 100644
--- a/common/src/test/scala/game/ObjectCreateDetailedMessageTest.scala
+++ b/common/src/test/scala/game/ObjectCreateDetailedMessageTest.scala
@@ -391,16 +391,16 @@ class ObjectCreateDetailedMessageTest extends Specification {
false,
RibbonBars()
)
- val inv = InventoryItem(ObjectClass.beamer, PlanetSideGUID(76), 0, DetailedWeaponData(4, 8, ObjectClass.energy_cell, PlanetSideGUID(77), 0, DetailedAmmoBoxData(8, 16))) ::
- InventoryItem(ObjectClass.suppressor, PlanetSideGUID(78), 2, DetailedWeaponData(4, 8, ObjectClass.bullet_9mm, PlanetSideGUID(79), 0, DetailedAmmoBoxData(8, 25))) ::
- InventoryItem(ObjectClass.forceblade, PlanetSideGUID(80), 4, DetailedWeaponData(4, 8, ObjectClass.melee_ammo, PlanetSideGUID(81), 0, DetailedAmmoBoxData(8, 1))) ::
- InventoryItem(ObjectClass.locker_container, PlanetSideGUID(82), 5, DetailedAmmoBoxData(8, 1)) ::
- InventoryItem(ObjectClass.bullet_9mm, PlanetSideGUID(83), 6, DetailedAmmoBoxData(8, 50)) ::
- InventoryItem(ObjectClass.bullet_9mm, PlanetSideGUID(84), 9, DetailedAmmoBoxData(8, 50)) ::
- InventoryItem(ObjectClass.bullet_9mm, PlanetSideGUID(85), 12, DetailedAmmoBoxData(8, 50)) ::
- InventoryItem(ObjectClass.bullet_9mm_AP, PlanetSideGUID(86), 33, DetailedAmmoBoxData(8, 50)) ::
- InventoryItem(ObjectClass.energy_cell, PlanetSideGUID(87), 36, DetailedAmmoBoxData(8, 50)) ::
- InventoryItem(ObjectClass.remote_electronics_kit, PlanetSideGUID(88), 39, DetailedREKData(8)) ::
+ val inv = InventoryItemData(ObjectClass.beamer, PlanetSideGUID(76), 0, DetailedWeaponData(4, 8, ObjectClass.energy_cell, PlanetSideGUID(77), 0, DetailedAmmoBoxData(8, 16))) ::
+ InventoryItemData(ObjectClass.suppressor, PlanetSideGUID(78), 2, DetailedWeaponData(4, 8, ObjectClass.bullet_9mm, PlanetSideGUID(79), 0, DetailedAmmoBoxData(8, 25))) ::
+ InventoryItemData(ObjectClass.forceblade, PlanetSideGUID(80), 4, DetailedWeaponData(4, 8, ObjectClass.melee_ammo, PlanetSideGUID(81), 0, DetailedAmmoBoxData(8, 1))) ::
+ InventoryItemData(ObjectClass.locker_container, PlanetSideGUID(82), 5, DetailedAmmoBoxData(8, 1)) ::
+ InventoryItemData(ObjectClass.bullet_9mm, PlanetSideGUID(83), 6, DetailedAmmoBoxData(8, 50)) ::
+ InventoryItemData(ObjectClass.bullet_9mm, PlanetSideGUID(84), 9, DetailedAmmoBoxData(8, 50)) ::
+ InventoryItemData(ObjectClass.bullet_9mm, PlanetSideGUID(85), 12, DetailedAmmoBoxData(8, 50)) ::
+ InventoryItemData(ObjectClass.bullet_9mm_AP, PlanetSideGUID(86), 33, DetailedAmmoBoxData(8, 50)) ::
+ InventoryItemData(ObjectClass.energy_cell, PlanetSideGUID(87), 36, DetailedAmmoBoxData(8, 50)) ::
+ InventoryItemData(ObjectClass.remote_electronics_kit, PlanetSideGUID(88), 39, DetailedREKData(8)) ::
Nil
val obj = DetailedCharacterData(
app,
diff --git a/common/src/test/scala/game/ObjectCreateMessageTest.scala b/common/src/test/scala/game/ObjectCreateMessageTest.scala
index 7b779f8bc..af4d8df26 100644
--- a/common/src/test/scala/game/ObjectCreateMessageTest.scala
+++ b/common/src/test/scala/game/ObjectCreateMessageTest.scala
@@ -1076,9 +1076,9 @@ class ObjectCreateMessageTest extends Specification {
"encode (locker container)" in {
val obj = LockerContainerData(
InventoryData(
- InventoryItem(ObjectClass.nano_dispenser, PlanetSideGUID(2935), 0, WeaponData(0x6, 0x0, ObjectClass.armor_canister, PlanetSideGUID(3426), 0, AmmoBoxData())) ::
- InventoryItem(ObjectClass.armor_canister, PlanetSideGUID(4090), 45, AmmoBoxData()) ::
- InventoryItem(ObjectClass.armor_canister, PlanetSideGUID(3326), 78, AmmoBoxData()) ::
+ InventoryItemData(ObjectClass.nano_dispenser, PlanetSideGUID(2935), 0, WeaponData(0x6, 0x0, ObjectClass.armor_canister, PlanetSideGUID(3426), 0, AmmoBoxData())) ::
+ InventoryItemData(ObjectClass.armor_canister, PlanetSideGUID(4090), 45, AmmoBoxData()) ::
+ InventoryItemData(ObjectClass.armor_canister, PlanetSideGUID(3326), 78, AmmoBoxData()) ::
Nil
)
)
@@ -1127,11 +1127,11 @@ class ObjectCreateMessageTest extends Specification {
Some(ImplantEffects.NoEffects),
Some(Cosmetics(true, true, true, true, false)),
InventoryData(
- InventoryItem(ObjectClass.plasma_grenade, PlanetSideGUID(3662), 0, WeaponData(0, 0, ObjectClass.plasma_grenade_ammo, PlanetSideGUID(3751), 0, AmmoBoxData())) ::
- InventoryItem(ObjectClass.bank, PlanetSideGUID(3908), 1, WeaponData(0, 0, 1, ObjectClass.armor_canister, PlanetSideGUID(4143), 0, AmmoBoxData())) ::
- InventoryItem(ObjectClass.mini_chaingun, PlanetSideGUID(4164), 2, WeaponData(0, 0, ObjectClass.bullet_9mm, PlanetSideGUID(3728), 0, AmmoBoxData())) ::
- InventoryItem(ObjectClass.phoenix, PlanetSideGUID(3603), 3, WeaponData(0, 0, ObjectClass.phoenix_missile, PlanetSideGUID(3056), 0, AmmoBoxData())) ::
- InventoryItem(ObjectClass.chainblade, PlanetSideGUID(4088), 4, WeaponData(0, 0, 1, ObjectClass.melee_ammo, PlanetSideGUID(3279), 0, AmmoBoxData())) ::
+ InventoryItemData(ObjectClass.plasma_grenade, PlanetSideGUID(3662), 0, WeaponData(0, 0, ObjectClass.plasma_grenade_ammo, PlanetSideGUID(3751), 0, AmmoBoxData())) ::
+ InventoryItemData(ObjectClass.bank, PlanetSideGUID(3908), 1, WeaponData(0, 0, 1, ObjectClass.armor_canister, PlanetSideGUID(4143), 0, AmmoBoxData())) ::
+ InventoryItemData(ObjectClass.mini_chaingun, PlanetSideGUID(4164), 2, WeaponData(0, 0, ObjectClass.bullet_9mm, PlanetSideGUID(3728), 0, AmmoBoxData())) ::
+ InventoryItemData(ObjectClass.phoenix, PlanetSideGUID(3603), 3, WeaponData(0, 0, ObjectClass.phoenix_missile, PlanetSideGUID(3056), 0, AmmoBoxData())) ::
+ InventoryItemData(ObjectClass.chainblade, PlanetSideGUID(4088), 4, WeaponData(0, 0, 1, ObjectClass.melee_ammo, PlanetSideGUID(3279), 0, AmmoBoxData())) ::
Nil
),
DrawnSlot.Rifle1
diff --git a/common/src/test/scala/objects/ConverterTest.scala b/common/src/test/scala/objects/ConverterTest.scala
new file mode 100644
index 000000000..07af93659
--- /dev/null
+++ b/common/src/test/scala/objects/ConverterTest.scala
@@ -0,0 +1,200 @@
+// Copyright (c) 2017 PSForever
+package objects
+
+import net.psforever.objects.definition.converter.{ACEConverter, REKConverter}
+import net.psforever.objects._
+import net.psforever.objects.definition._
+import net.psforever.objects.equipment.CItem.{DeployedItem, Unit}
+import net.psforever.objects.equipment._
+import net.psforever.objects.inventory.InventoryTile
+import net.psforever.packet.game.PlanetSideGUID
+import net.psforever.packet.game.objectcreate._
+import net.psforever.types.{CharacterGender, PlanetSideEmpire, Vector3}
+import org.specs2.mutable.Specification
+
+import scala.util.Success
+
+class ConverterTest extends Specification {
+ "AmmoBox" should {
+ val bullet_9mm = AmmoBoxDefinition(28)
+ bullet_9mm.Capacity = 50
+
+ "convert to packet" in {
+ val obj = AmmoBox(bullet_9mm)
+ obj.Definition.Packet.DetailedConstructorData(obj) match {
+ case Success(pkt) =>
+ pkt mustEqual DetailedAmmoBoxData(8, 50)
+ case _ =>
+ ko
+ }
+ obj.Definition.Packet.ConstructorData(obj) match {
+ case Success(pkt) =>
+ pkt mustEqual AmmoBoxData()
+ case _ =>
+ ko
+ }
+ }
+ }
+
+ "Tool" should {
+ "convert to packet" in {
+ val tdef = ToolDefinition(1076)
+ tdef.Size = EquipmentSize.Rifle
+ tdef.AmmoTypes += Ammo.shotgun_shell
+ tdef.AmmoTypes += Ammo.shotgun_shell_AP
+ tdef.FireModes += new FireModeDefinition
+ tdef.FireModes.head.AmmoTypeIndices += 0
+ tdef.FireModes.head.AmmoTypeIndices += 1
+ tdef.FireModes.head.AmmoSlotIndex = 0
+ val obj : Tool = Tool(tdef)
+ val box = AmmoBox(PlanetSideGUID(90), new AmmoBoxDefinition(Ammo.shotgun_shell.id))
+ obj.AmmoSlots.head.Box = box
+ obj.AmmoSlots.head.Magazine = 30
+
+ obj.Definition.Packet.DetailedConstructorData(obj) match {
+ case Success(pkt) =>
+ pkt mustEqual DetailedWeaponData(4,8, Ammo.shotgun_shell.id, PlanetSideGUID(90), 0, DetailedAmmoBoxData(8, 30))
+ case _ =>
+ ko
+ }
+ obj.Definition.Packet.ConstructorData(obj) match {
+ case Success(pkt) =>
+ pkt mustEqual WeaponData(4,8, 0, Ammo.shotgun_shell.id, PlanetSideGUID(90), 0, AmmoBoxData())
+ case _ =>
+ ko
+ }
+ }
+ }
+
+ "Kit" should {
+ "convert to packet" in {
+ val kdef = KitDefinition(Kits.medkit)
+ val obj = Kit(PlanetSideGUID(90), kdef)
+ obj.Definition.Packet.DetailedConstructorData(obj) match {
+ case Success(pkt) =>
+ pkt mustEqual DetailedAmmoBoxData(0, 1)
+ case _ =>
+ ko
+ }
+ obj.Definition.Packet.ConstructorData(obj) match {
+ case Success(pkt) =>
+ pkt mustEqual AmmoBoxData()
+ case _ =>
+ ko
+ }
+ }
+
+ "ConstructionItem" should {
+ "convert to packet" in {
+ val cdef = ConstructionItemDefinition(Unit.advanced_ace)
+ cdef.Modes += DeployedItem.tank_traps
+ cdef.Modes += DeployedItem.portable_manned_turret_tr
+ cdef.Modes += DeployedItem.deployable_shield_generator
+ cdef.Tile = InventoryTile.Tile63
+ cdef.Packet = new ACEConverter()
+ val obj = ConstructionItem(PlanetSideGUID(90), cdef)
+ obj.Definition.Packet.DetailedConstructorData(obj) match {
+ case Success(pkt) =>
+ pkt mustEqual DetailedACEData(0)
+ case _ =>
+ ko
+ }
+ obj.Definition.Packet.ConstructorData(obj) match {
+ case Success(pkt) =>
+ pkt mustEqual ACEData(0,0)
+ case _ =>
+ ko
+ }
+ }
+ }
+ }
+
+ "SimpleItem" should {
+ "convert to packet" in {
+ val sdef = SimpleItemDefinition(SItem.remote_electronics_kit)
+ sdef.Packet = new REKConverter()
+ val obj = SimpleItem(PlanetSideGUID(90), sdef)
+ obj.Definition.Packet.DetailedConstructorData(obj) match {
+ case Success(pkt) =>
+ pkt mustEqual DetailedREKData(8)
+ case _ =>
+ ko
+ }
+ obj.Definition.Packet.ConstructorData(obj) match {
+ case Success(pkt) =>
+ pkt mustEqual REKData(8,0)
+ case _ =>
+ ko
+ }
+ }
+ }
+
+ "Player" should {
+ "convert to packet" in {
+ /*
+ Create an AmmoBoxDefinition with which to build two AmmoBoxes
+ Create a ToolDefinition with which to create a Tool
+ Load one of the AmmoBoxes into that Tool
+ Create a Player
+ Give the Player's Holster (2) the Tool
+ Place the remaining AmmoBox into the Player's inventory in the third slot (8)
+ */
+ val bullet_9mm = AmmoBoxDefinition(28)
+ bullet_9mm.Capacity = 50
+ val box1 = AmmoBox(PlanetSideGUID(90), bullet_9mm)
+ val box2 = AmmoBox(PlanetSideGUID(91), bullet_9mm)
+ val tdef = ToolDefinition(1076)
+ tdef.Name = "sample_weapon"
+ tdef.Size = EquipmentSize.Rifle
+ tdef.AmmoTypes += Ammo.bullet_9mm
+ tdef.FireModes += new FireModeDefinition
+ tdef.FireModes.head.AmmoTypeIndices += 0
+ tdef.FireModes.head.AmmoSlotIndex = 0
+ tdef.FireModes.head.Magazine = 18
+ val tool = Tool(PlanetSideGUID(92), tdef)
+ tool.AmmoSlots.head.Box = box1
+ val obj = Player(PlanetSideGUID(93), "Chord", PlanetSideEmpire.TR, CharacterGender.Male, 0, 5)
+ obj.Slot(2).Equipment = tool
+ obj.Inventory += 8 -> box2
+
+ obj.Definition.Packet.DetailedConstructorData(obj).isSuccess mustEqual true
+ ok //TODO write more of this test
+ }
+ }
+
+ "Vehicle" should {
+ "convert to packet" in {
+ val hellfire_ammo = AmmoBoxDefinition(Ammo.hellfire_ammo.id)
+
+ val fury_weapon_systema_def = ToolDefinition(ObjectClass.fury_weapon_systema)
+ fury_weapon_systema_def.Size = EquipmentSize.VehicleWeapon
+ fury_weapon_systema_def.AmmoTypes += Ammo.hellfire_ammo
+ fury_weapon_systema_def.FireModes += new FireModeDefinition
+ fury_weapon_systema_def.FireModes.head.AmmoTypeIndices += 0
+ fury_weapon_systema_def.FireModes.head.AmmoSlotIndex = 0
+ fury_weapon_systema_def.FireModes.head.Magazine = 2
+
+ val fury_def = VehicleDefinition(ObjectClass.fury)
+ fury_def.Seats += 0 -> new SeatDefinition()
+ fury_def.Seats(0).Bailable = true
+ fury_def.Seats(0).ControlledWeapon = Some(1)
+ fury_def.MountPoints += 0 -> 0
+ fury_def.MountPoints += 2 -> 0
+ fury_def.Weapons += 1 -> fury_weapon_systema_def
+ fury_def.TrunkSize = InventoryTile(11, 11)
+ fury_def.TrunkOffset = 30
+
+ val hellfire_ammo_box = AmmoBox(PlanetSideGUID(432), hellfire_ammo)
+
+ val fury = Vehicle(PlanetSideGUID(413), fury_def)
+ fury.Faction = PlanetSideEmpire.VS
+ fury.Position = Vector3(3674.8438f, 2732f, 91.15625f)
+ fury.Orientation = Vector3(0.0f, 0.0f, 90.0f)
+ fury.WeaponControlledFromSeat(0).get.GUID = PlanetSideGUID(400)
+ fury.WeaponControlledFromSeat(0).get.AmmoSlots.head.Box = hellfire_ammo_box
+
+ fury.Definition.Packet.ConstructorData(fury).isSuccess mustEqual true
+ ok //TODO write more of this test
+ }
+ }
+}
\ No newline at end of file
diff --git a/common/src/test/scala/objects/EntityTest.scala b/common/src/test/scala/objects/EntityTest.scala
new file mode 100644
index 000000000..68cac3ee8
--- /dev/null
+++ b/common/src/test/scala/objects/EntityTest.scala
@@ -0,0 +1,83 @@
+// Copyright (c) 2017 PSForever
+package objects
+
+import net.psforever.objects.PlanetSideGameObject
+import net.psforever.objects.definition.ObjectDefinition
+import net.psforever.objects.entity.NoGUIDException
+import net.psforever.packet.game.PlanetSideGUID
+import net.psforever.types.Vector3
+import org.specs2.mutable._
+
+class EntityTest extends Specification {
+ //both WorldEntity and IdentifiableEntity are components of PlanetSideGameObject
+ private class EntityTestClass extends PlanetSideGameObject {
+ def Definition : ObjectDefinition = new ObjectDefinition(0) { }
+ }
+
+ "SimpleWorldEntity" should {
+ "construct" in {
+ new EntityTestClass()
+ ok
+ }
+
+ "initialize" in {
+ val obj : EntityTestClass = new EntityTestClass()
+ obj.Position mustEqual Vector3(0f, 0f, 0f)
+ obj.Orientation mustEqual Vector3(0f, 0f, 0f)
+ obj.Velocity mustEqual Vector3(0f, 0f, 0f)
+ }
+
+ "mutate and access" in {
+ val obj : EntityTestClass = new EntityTestClass
+ obj.Position = Vector3(1f, 1f, 1f)
+ obj.Orientation = Vector3(2f, 2f, 2f)
+ obj.Velocity = Vector3(3f, 3f, 3f)
+
+ obj.Position mustEqual Vector3(1f, 1f, 1f)
+ obj.Orientation mustEqual Vector3(2f, 2f, 2f)
+ obj.Velocity mustEqual Vector3(3f, 3f, 3f)
+ }
+
+ "clamp Orientation" in {
+ val obj : EntityTestClass = new EntityTestClass
+ obj.Orientation = Vector3(-1f, 361f, -0f)
+ obj.Orientation mustEqual Vector3(359f, 1f, 0f)
+ }
+ }
+
+ "IdentifiableEntity" should {
+ "construct" in {
+ new EntityTestClass()
+ ok
+ }
+
+ "error while unset" in {
+ val obj : EntityTestClass = new EntityTestClass
+ obj.GUID must throwA[NoGUIDException]
+ }
+
+ "work after mutation" in {
+ val obj : EntityTestClass = new EntityTestClass
+ obj.GUID = PlanetSideGUID(1051)
+ obj.GUID mustEqual PlanetSideGUID(1051)
+ }
+
+ "work after multiple mutations" in {
+ val obj : EntityTestClass = new EntityTestClass
+ obj.GUID = PlanetSideGUID(1051)
+ obj.GUID mustEqual PlanetSideGUID(1051)
+ obj.GUID = PlanetSideGUID(30052)
+ obj.GUID mustEqual PlanetSideGUID(30052)
+ obj.GUID = PlanetSideGUID(62)
+ obj.GUID mustEqual PlanetSideGUID(62)
+ }
+
+ "invalidate and resume error" in {
+ val obj : EntityTestClass = new EntityTestClass
+ obj.GUID = PlanetSideGUID(1051)
+ obj.GUID mustEqual PlanetSideGUID(1051)
+ obj.Invalidate()
+ obj.GUID must throwA[NoGUIDException]
+ }
+ }
+}
diff --git a/common/src/test/scala/objects/EquipmentTest.scala b/common/src/test/scala/objects/EquipmentTest.scala
new file mode 100644
index 000000000..284d0b2b7
--- /dev/null
+++ b/common/src/test/scala/objects/EquipmentTest.scala
@@ -0,0 +1,258 @@
+// Copyright (c) 2017 PSForever
+package objects
+
+import net.psforever.objects._
+import net.psforever.objects.definition._
+import net.psforever.objects.equipment.CItem.{DeployedItem, Unit}
+import net.psforever.objects.equipment._
+import net.psforever.objects.inventory.InventoryTile
+import net.psforever.objects.GlobalDefinitions._
+import org.specs2.mutable._
+
+class EquipmentTest extends Specification {
+
+ "AmmoBox" should {
+ "define" in {
+ val obj = AmmoBoxDefinition(86)
+ obj.Capacity = 300
+ obj.Tile = InventoryTile.Tile44
+
+ obj.AmmoType mustEqual Ammo.aphelion_immolation_cannon_ammo
+ obj.Capacity mustEqual 300
+ obj.Tile.width mustEqual InventoryTile.Tile44.width
+ obj.Tile.height mustEqual InventoryTile.Tile44.height
+ obj.ObjectId mustEqual 86
+ }
+
+ "construct" in {
+ val obj = AmmoBox(bullet_9mm)
+ obj.AmmoType mustEqual Ammo.bullet_9mm
+ obj.Capacity mustEqual 50
+ }
+
+ "construct (2)" in {
+ val obj = AmmoBox(bullet_9mm, 150)
+ obj.AmmoType mustEqual Ammo.bullet_9mm
+ obj.Capacity mustEqual 150
+ }
+
+ "vary capacity" in {
+ val obj = AmmoBox(bullet_9mm, 0)
+ obj.Capacity mustEqual 1 //can not be initialized to 0
+ obj.Capacity = 75
+ obj.Capacity mustEqual 75
+ }
+
+ "limit capacity" in {
+ val obj = AmmoBox(bullet_9mm)
+ obj.Capacity mustEqual 50
+ obj.Capacity = -1
+ obj.Capacity mustEqual 0
+ obj.Capacity = 65536
+ obj.Capacity mustEqual 65535
+ }
+ }
+
+ "Tool" should {
+ "define" in {
+ val obj = ToolDefinition(1076)
+ obj.Name = "sample_weapon"
+ obj.Size = EquipmentSize.Rifle
+ obj.AmmoTypes += Ammo.shotgun_shell
+ obj.AmmoTypes += Ammo.shotgun_shell_AP
+ obj.FireModes += new FireModeDefinition
+ obj.FireModes.head.AmmoTypeIndices += 0
+ obj.FireModes.head.AmmoTypeIndices += 1
+ obj.FireModes.head.AmmoSlotIndex = 0
+ obj.FireModes.head.Magazine = 18
+ obj.FireModes.head.ResetAmmoIndexOnSwap = true
+ obj.FireModes += new FireModeDefinition
+ obj.FireModes(1).AmmoTypeIndices += 0
+ obj.FireModes(1).AmmoTypeIndices += 1
+ obj.FireModes(1).AmmoSlotIndex = 1
+ obj.FireModes(1).Chamber = 3
+ obj.FireModes(1).Magazine = 18
+ obj.Tile = InventoryTile.Tile93
+ obj.ObjectId mustEqual 1076
+ obj.Name mustEqual "sample_weapon"
+ obj.AmmoTypes.head mustEqual Ammo.shotgun_shell
+ obj.AmmoTypes(1) mustEqual Ammo.shotgun_shell_AP
+ obj.FireModes.head.AmmoTypeIndices.head mustEqual 0
+ obj.FireModes.head.AmmoTypeIndices(1) mustEqual 1
+ obj.FireModes.head.AmmoSlotIndex mustEqual 0
+ obj.FireModes.head.Chamber mustEqual 1
+ obj.FireModes.head.Magazine mustEqual 18
+ obj.FireModes.head.ResetAmmoIndexOnSwap mustEqual true
+ obj.FireModes(1).AmmoTypeIndices.head mustEqual 0
+ obj.FireModes(1).AmmoTypeIndices(1) mustEqual 1
+ obj.FireModes(1).AmmoSlotIndex mustEqual 1
+ obj.FireModes(1).Chamber mustEqual 3
+ obj.FireModes(1).Magazine mustEqual 18
+ obj.FireModes(1).ResetAmmoIndexOnSwap mustEqual false
+ obj.Tile.width mustEqual InventoryTile.Tile93.width
+ obj.Tile.height mustEqual InventoryTile.Tile93.height
+ }
+
+ "construct" in {
+ val obj : Tool = Tool(fury_weapon_systema)
+ obj.Definition.ObjectId mustEqual fury_weapon_systema.ObjectId
+ }
+
+ "fire mode" in {
+ //explanation: fury_weapon_systema has one fire mode and that fire mode is our only option
+ val obj : Tool = Tool(fury_weapon_systema)
+ obj.Magazine = obj.MaxMagazine
+ obj.Magazine mustEqual obj.Definition.FireModes.head.Magazine
+ //fmode = 0
+ obj.FireModeIndex mustEqual 0
+ obj.FireMode.Magazine mustEqual 2
+ obj.AmmoType mustEqual Ammo.hellfire_ammo
+ //fmode -> 1 (0)
+ obj.FireModeIndex = 1
+ obj.FireModeIndex mustEqual 0
+ obj.FireMode.Magazine mustEqual 2
+ obj.AmmoType mustEqual Ammo.hellfire_ammo
+ }
+
+ "multiple fire modes" in {
+ //explanation: sample_weapon has two fire modes; adjusting the FireMode changes between them
+ val tdef = ToolDefinition(1076)
+ tdef.Size = EquipmentSize.Rifle
+ tdef.AmmoTypes += Ammo.shotgun_shell
+ tdef.AmmoTypes += Ammo.shotgun_shell_AP
+ tdef.FireModes += new FireModeDefinition
+ tdef.FireModes.head.AmmoTypeIndices += 0
+ tdef.FireModes.head.AmmoSlotIndex = 0
+ tdef.FireModes.head.Magazine = 9
+ tdef.FireModes += new FireModeDefinition
+ tdef.FireModes(1).AmmoTypeIndices += 1
+ tdef.FireModes(1).AmmoSlotIndex = 1
+ tdef.FireModes(1).Magazine = 18
+ val obj : Tool = Tool(tdef)
+ //fmode = 0
+ obj.FireModeIndex mustEqual 0
+ obj.FireMode.Magazine mustEqual 9
+ obj.AmmoType mustEqual Ammo.shotgun_shell
+ //fmode -> 1
+ obj.NextFireMode
+ obj.FireModeIndex mustEqual 1
+ obj.FireMode.Magazine mustEqual 18
+ obj.AmmoType mustEqual Ammo.shotgun_shell_AP
+ //fmode -> 0
+ obj.NextFireMode
+ obj.FireModeIndex mustEqual 0
+ obj.FireMode.Magazine mustEqual 9
+ obj.AmmoType mustEqual Ammo.shotgun_shell
+ }
+
+ "multiple types of ammunition" in {
+ //explanation: obj has one fire mode and two ammunitions; adjusting the AmmoType changes between them
+ val tdef = ToolDefinition(1076)
+ tdef.Size = EquipmentSize.Rifle
+ tdef.AmmoTypes += Ammo.shotgun_shell
+ tdef.AmmoTypes += Ammo.shotgun_shell_AP
+ tdef.FireModes += new FireModeDefinition
+ tdef.FireModes.head.AmmoTypeIndices += 0
+ tdef.FireModes.head.AmmoTypeIndices += 1
+ tdef.FireModes.head.AmmoSlotIndex = 0
+ val obj : Tool = Tool(tdef)
+ //ammo = 0
+ obj.AmmoTypeIndex mustEqual 0
+ obj.AmmoType mustEqual Ammo.shotgun_shell
+ //ammo -> 1
+ obj.NextAmmoType
+ obj.AmmoTypeIndex mustEqual 1
+ obj.AmmoType mustEqual Ammo.shotgun_shell_AP
+ //ammo -> 2 (0)
+ obj.NextAmmoType
+ obj.AmmoTypeIndex mustEqual 0
+ obj.AmmoType mustEqual Ammo.shotgun_shell
+ }
+ }
+
+ "Kit" should {
+ "define" in {
+ val sample = KitDefinition(Kits.medkit)
+ sample.ObjectId mustEqual medkit.ObjectId
+ sample.Tile.width mustEqual medkit.Tile.width
+ sample.Tile.height mustEqual medkit.Tile.height
+ }
+
+ "construct" in {
+ val obj : Kit = Kit(medkit)
+ obj.Definition.ObjectId mustEqual medkit.ObjectId
+ }
+ }
+
+ "ConstructionItem" should {
+ val advanced_ace_tr = ConstructionItemDefinition(39)
+ advanced_ace_tr.Modes += DeployedItem.tank_traps
+ advanced_ace_tr.Modes += DeployedItem.portable_manned_turret_tr
+ advanced_ace_tr.Modes += DeployedItem.deployable_shield_generator
+ advanced_ace_tr.Tile = InventoryTile.Tile63
+
+ "define" in {
+ val sample = ConstructionItemDefinition(Unit.advanced_ace)
+ sample.Modes += DeployedItem.tank_traps
+ sample.Modes += DeployedItem.portable_manned_turret_tr
+ sample.Modes += DeployedItem.deployable_shield_generator
+ sample.Tile = InventoryTile.Tile63
+ sample.Modes.head mustEqual DeployedItem.tank_traps
+ sample.Modes(1) mustEqual DeployedItem.portable_manned_turret_tr
+ sample.Modes(2) mustEqual DeployedItem.deployable_shield_generator
+ sample.Tile.width mustEqual InventoryTile.Tile63.width
+ sample.Tile.height mustEqual InventoryTile.Tile63.height
+ }
+
+ "construct" in {
+ val obj : ConstructionItem = ConstructionItem(advanced_ace_tr)
+ obj.Definition.ObjectId mustEqual advanced_ace_tr.ObjectId
+ }
+
+ "fire mode" in {
+ //explanation: router_telepad has one fire mode and that fire mode is our only option
+ val router_telepad : ConstructionItemDefinition = ConstructionItemDefinition(Unit.router_telepad)
+ router_telepad.Modes += DeployedItem.router_telepad_deployable
+ val obj : ConstructionItem = ConstructionItem(router_telepad)
+ //fmode = 0
+ obj.FireModeIndex mustEqual 0
+ obj.FireMode mustEqual DeployedItem.router_telepad_deployable
+ //fmode -> 1 (0)
+ obj.FireModeIndex = 1
+ obj.FireModeIndex mustEqual 0
+ obj.FireMode mustEqual DeployedItem.router_telepad_deployable
+ }
+
+ "multiple fire modes" in {
+ //explanation: advanced_ace_tr has three fire modes; adjusting the FireMode changes between them
+ val obj : ConstructionItem = ConstructionItem(advanced_ace_tr)
+ //fmode = 0
+ obj.FireModeIndex mustEqual 0
+ obj.FireMode mustEqual DeployedItem.tank_traps
+ //fmode -> 1
+ obj.NextFireMode
+ obj.FireModeIndex mustEqual 1
+ obj.FireMode mustEqual DeployedItem.portable_manned_turret_tr
+ //fmode -> 2
+ obj.NextFireMode
+ obj.FireModeIndex mustEqual 2
+ obj.FireMode mustEqual DeployedItem.deployable_shield_generator
+ //fmode -> 0
+ obj.NextFireMode
+ obj.FireModeIndex mustEqual 0
+ obj.FireMode mustEqual DeployedItem.tank_traps
+ }
+ }
+
+ "SimpleItem" should {
+ "define" in {
+ val sample = SimpleItemDefinition(SItem.remote_electronics_kit)
+ sample.ObjectId mustEqual remote_electronics_kit.ObjectId
+ }
+
+ "construct" in {
+ val obj : SimpleItem = SimpleItem(remote_electronics_kit)
+ obj.Definition.ObjectId mustEqual remote_electronics_kit.ObjectId
+ }
+ }
+}
diff --git a/common/src/test/scala/objects/ImplantTest.scala b/common/src/test/scala/objects/ImplantTest.scala
new file mode 100644
index 000000000..e1f47936b
--- /dev/null
+++ b/common/src/test/scala/objects/ImplantTest.scala
@@ -0,0 +1,76 @@
+// Copyright (c) 2017 PSForever
+package objects
+
+import net.psforever.objects.Implant
+import net.psforever.objects.definition.{ImplantDefinition, Stance}
+import net.psforever.types.{ExoSuitType, ImplantType}
+import org.specs2.mutable._
+
+class ImplantTest extends Specification {
+ val sample = new ImplantDefinition(8) //variant of sensor shield/silent run
+ sample.Initialization = 90000 //1:30
+ sample.ActivationCharge = 3
+ sample.DurationChargeBase = 1
+ sample.DurationChargeByExoSuit += ExoSuitType.Agile -> 2
+ sample.DurationChargeByExoSuit += ExoSuitType.Reinforced -> 2
+ sample.DurationChargeByExoSuit += ExoSuitType.Standard -> 1
+ sample.DurationChargeByStance += Stance.Running -> 1
+
+ "define" in {
+ sample.Initialization mustEqual 90000
+ sample.ActivationCharge mustEqual 3
+ sample.DurationChargeBase mustEqual 1
+ sample.DurationChargeByExoSuit(ExoSuitType.Agile) mustEqual 2
+ sample.DurationChargeByExoSuit(ExoSuitType.Reinforced) mustEqual 2
+ sample.DurationChargeByExoSuit(ExoSuitType.Standard) mustEqual 1
+ sample.DurationChargeByExoSuit(ExoSuitType.Infiltration) mustEqual 0 //default value
+ sample.DurationChargeByStance(Stance.Running) mustEqual 1
+ sample.DurationChargeByStance(Stance.Crouching) mustEqual 0 //default value
+ sample.Type mustEqual ImplantType.SilentRun
+ }
+
+ "construct" in {
+ val obj = new Implant(sample)
+ obj.Definition.Type mustEqual sample.Type
+ obj.Active mustEqual false
+ obj.Ready mustEqual false
+ obj.Timer mustEqual 0
+ }
+
+ "reset/init their timer" in {
+ val obj = new Implant(sample)
+ obj.Timer mustEqual 0
+ obj.Reset()
+ obj.Timer mustEqual 90000
+ }
+
+ "reset/init their readiness condition" in {
+ val obj = new Implant(sample)
+ obj.Ready mustEqual false
+ obj.Timer = 0
+ obj.Ready mustEqual true
+ obj.Reset()
+ obj.Ready mustEqual false
+ }
+
+ "not activate until they are ready" in {
+ val obj = new Implant(sample)
+ obj.Active = true
+ obj.Active mustEqual false
+ obj.Timer = 0
+ obj.Active = true
+ obj.Active mustEqual true
+ }
+
+ "not cost energy while not active" in {
+ val obj = new Implant(sample)
+ obj.Charge(ExoSuitType.Reinforced, Stance.Running) mustEqual 0
+ }
+
+ "cost energy while active" in {
+ val obj = new Implant(sample)
+ obj.Timer = 0
+ obj.Active = true
+ obj.Charge(ExoSuitType.Reinforced, Stance.Running) mustEqual 4
+ }
+}
diff --git a/common/src/test/scala/objects/InventoryTest.scala b/common/src/test/scala/objects/InventoryTest.scala
new file mode 100644
index 000000000..52698fa5e
--- /dev/null
+++ b/common/src/test/scala/objects/InventoryTest.scala
@@ -0,0 +1,215 @@
+// Copyright (c) 2017 PSForever
+package objects
+
+import net.psforever.objects.{AmmoBox, SimpleItem}
+import net.psforever.objects.definition.SimpleItemDefinition
+import net.psforever.objects.inventory.{GridInventory, InventoryItem, InventoryTile}
+import net.psforever.objects.GlobalDefinitions._
+import net.psforever.packet.game.PlanetSideGUID
+import org.specs2.mutable._
+
+import scala.collection.mutable.ListBuffer
+import scala.util.Success
+
+class InventoryTest extends Specification {
+ val bullet9mmBox1 = AmmoBox(PlanetSideGUID(1), bullet_9mm)
+ val bullet9mmBox2 = AmmoBox(PlanetSideGUID(2), bullet_9mm)
+
+ "GridInventory" should {
+ "construct" in {
+ val obj : GridInventory = GridInventory()
+ obj.TotalCapacity mustEqual 1
+ obj.Capacity mustEqual 1
+ }
+
+ "resize" in {
+ val obj : GridInventory = GridInventory(9, 6)
+ obj.TotalCapacity mustEqual 54
+ obj.Capacity mustEqual 54
+ obj.Size mustEqual 0
+ }
+
+ "insert item" in {
+ val obj : GridInventory = GridInventory(9, 6)
+ obj.CheckCollisions(23, bullet9mmBox1) mustEqual Success(Nil)
+ obj += 2 -> bullet9mmBox1
+ obj.TotalCapacity mustEqual 54
+ obj.Capacity mustEqual 45
+ obj.Size mustEqual 1
+ obj.hasItem(PlanetSideGUID(1)) mustEqual Some(bullet9mmBox1)
+ obj.Clear()
+ obj.Size mustEqual 0
+ }
+
+ "check for collision with inventory border" in {
+ val obj : GridInventory = GridInventory(3, 3)
+ //safe
+ obj.CheckCollisionsAsList(0, 3, 3) mustEqual Success(Nil)
+ //right
+ obj.CheckCollisionsAsList(-1, 3, 3).isFailure mustEqual true
+ //left
+ obj.CheckCollisionsAsList(1, 3, 3).isFailure mustEqual true
+ //bottom
+ obj.CheckCollisionsAsList(3, 3, 3).isFailure mustEqual true
+ }
+
+ "check for item collision (right insert)" in {
+ val obj : GridInventory = GridInventory(9, 6)
+ obj += 0 -> bullet9mmBox1
+ obj.Capacity mustEqual 45
+ val w = bullet9mmBox2.Tile.width
+ val h = bullet9mmBox2.Tile.height
+ obj.CheckCollisionsAsList(0, w, h) mustEqual Success(1 :: Nil)
+ obj.CheckCollisionsAsList(1, w, h) mustEqual Success(1 :: Nil)
+ obj.CheckCollisionsAsList(2, w, h) mustEqual Success(1 :: Nil)
+ obj.CheckCollisionsAsList(3, w, h) mustEqual Success(Nil)
+ obj.CheckCollisionsAsGrid(0, w, h) mustEqual Success(1 :: Nil)
+ obj.CheckCollisionsAsGrid(1, w, h) mustEqual Success(1 :: Nil)
+ obj.CheckCollisionsAsGrid(2, w, h) mustEqual Success(1 :: Nil)
+ obj.CheckCollisionsAsGrid(3, w, h) mustEqual Success(Nil)
+ obj.Clear()
+ ok
+ }
+
+ "check for item collision (left insert)" in {
+ val obj : GridInventory = GridInventory(9, 6)
+ obj += 3 -> bullet9mmBox1
+ obj.Capacity mustEqual 45
+ val w = bullet9mmBox2.Tile.width
+ val h = bullet9mmBox2.Tile.height
+ obj.CheckCollisionsAsList(3, w, h) mustEqual Success(1 :: Nil)
+ obj.CheckCollisionsAsList(2, w, h) mustEqual Success(1 :: Nil)
+ obj.CheckCollisionsAsList(1, w, h) mustEqual Success(1 :: Nil)
+ obj.CheckCollisionsAsList(0, w, h) mustEqual Success(Nil)
+ obj.CheckCollisionsAsGrid(3, w, h) mustEqual Success(1 :: Nil)
+ obj.CheckCollisionsAsGrid(2, w, h) mustEqual Success(1 :: Nil)
+ obj.CheckCollisionsAsGrid(1, w, h) mustEqual Success(1 :: Nil)
+ obj.CheckCollisionsAsGrid(0, w, h) mustEqual Success(Nil)
+ obj.Clear()
+ ok
+ }
+
+ "check for item collision (below insert)" in {
+ val obj : GridInventory = GridInventory(9, 6)
+ obj += 0 -> bullet9mmBox1
+ obj.Capacity mustEqual 45
+ val w = bullet9mmBox2.Tile.width
+ val h = bullet9mmBox2.Tile.height
+ obj.CheckCollisionsAsList(0, w, h) mustEqual Success(1 :: Nil)
+ obj.CheckCollisionsAsList(9, w, h) mustEqual Success(1 :: Nil)
+ obj.CheckCollisionsAsList(18, w, h) mustEqual Success(1 :: Nil)
+ obj.CheckCollisionsAsList(27, w, h) mustEqual Success(Nil)
+ obj.CheckCollisionsAsGrid(0, w, h) mustEqual Success(1 :: Nil)
+ obj.CheckCollisionsAsGrid(9, w, h) mustEqual Success(1 :: Nil)
+ obj.CheckCollisionsAsGrid(18, w, h) mustEqual Success(1 :: Nil)
+ obj.CheckCollisionsAsGrid(27, w, h) mustEqual Success(Nil)
+ obj.Clear()
+ ok
+ }
+
+ "check for item collision (above insert)" in {
+ val obj : GridInventory = GridInventory(9, 6)
+ obj += 27 -> bullet9mmBox1
+ obj.Capacity mustEqual 45
+ val w = bullet9mmBox2.Tile.width
+ val h = bullet9mmBox2.Tile.height
+ obj.CheckCollisionsAsList(27, w, h) mustEqual Success(1 :: Nil)
+ obj.CheckCollisionsAsList(19, w, h) mustEqual Success(1 :: Nil)
+ obj.CheckCollisionsAsList(9, w, h) mustEqual Success(1 :: Nil)
+ obj.CheckCollisionsAsList(0, w, h) mustEqual Success(Nil)
+ obj.CheckCollisionsAsGrid(27, w, h) mustEqual Success(1 :: Nil)
+ obj.CheckCollisionsAsGrid(19, w, h) mustEqual Success(1 :: Nil)
+ obj.CheckCollisionsAsGrid(9, w, h) mustEqual Success(1 :: Nil)
+ obj.CheckCollisionsAsGrid(0, w, h) mustEqual Success(Nil)
+ obj.Clear()
+ ok
+ }
+
+ "block insertion if item collision" in {
+ val obj : GridInventory = GridInventory(9, 6)
+ obj += 0 -> bullet9mmBox1
+ obj.Capacity mustEqual 45
+ obj.hasItem(PlanetSideGUID(1)) mustEqual Some(bullet9mmBox1)
+ obj += 2 -> bullet9mmBox2
+ obj.hasItem(PlanetSideGUID(2)) mustEqual None
+ obj.Clear()
+ ok
+ }
+
+ "remove item" in {
+ val obj : GridInventory = GridInventory(9, 6)
+ obj += 0 -> bullet9mmBox1
+ obj.hasItem(PlanetSideGUID(1)) mustEqual Some(bullet9mmBox1)
+ obj -= PlanetSideGUID(1)
+ obj.hasItem(PlanetSideGUID(1)) mustEqual None
+ obj.Clear()
+ ok
+ }
+
+ "unblock insertion on item removal" in {
+ val obj : GridInventory = GridInventory(9, 6)
+ obj.CheckCollisions(23, bullet9mmBox1) mustEqual Success(Nil)
+ obj += 23 -> bullet9mmBox1
+ obj.hasItem(PlanetSideGUID(1)) mustEqual Some(bullet9mmBox1)
+ obj.CheckCollisions(23, bullet9mmBox1) mustEqual Success(1 :: Nil)
+ obj -= PlanetSideGUID(1)
+ obj.hasItem(PlanetSideGUID(1)) mustEqual None
+ obj.CheckCollisions(23, bullet9mmBox1) mustEqual Success(Nil)
+ obj.Clear()
+ ok
+ }
+
+ "attempt to fit an item" in {
+ val sampleDef22 = new SimpleItemDefinition(149)
+ sampleDef22.Tile = InventoryTile.Tile22
+ val sampleDef33 = new SimpleItemDefinition(149)
+ sampleDef33.Tile = InventoryTile.Tile33
+ val sampleDef63 = new SimpleItemDefinition(149)
+ sampleDef63.Tile = InventoryTile.Tile63
+
+ val obj : GridInventory = GridInventory(9, 9)
+ obj += 0 -> SimpleItem(PlanetSideGUID(0), sampleDef22)
+ obj += 20 -> SimpleItem(PlanetSideGUID(1), sampleDef63)
+ obj += 56 -> SimpleItem(PlanetSideGUID(2), sampleDef33)
+ obj.Fit(InventoryTile.Tile33) match {
+ case Some(x) =>
+ x mustEqual 50
+ case None =>
+ ko
+ }
+ ok
+ }
+
+ "attempt to fit all the items" in {
+ val sampleDef1 = new SimpleItemDefinition(149)
+ sampleDef1.Tile = InventoryTile.Tile22
+ val sampleDef2 = new SimpleItemDefinition(149)
+ sampleDef2.Tile = InventoryTile.Tile33
+ val sampleDef3 = new SimpleItemDefinition(149)
+ sampleDef3.Tile = InventoryTile.Tile42
+ val sampleDef4 = new SimpleItemDefinition(149)
+ sampleDef4.Tile = InventoryTile.Tile63
+
+ val list : ListBuffer[InventoryItem] = ListBuffer()
+ list += new InventoryItem(SimpleItem(PlanetSideGUID(0), sampleDef2), -1)
+ list += new InventoryItem(SimpleItem(PlanetSideGUID(1), sampleDef3), -1)
+ list += new InventoryItem(SimpleItem(PlanetSideGUID(2), sampleDef1), -1)
+ list += new InventoryItem(SimpleItem(PlanetSideGUID(3), sampleDef4), -1)
+ list += new InventoryItem(SimpleItem(PlanetSideGUID(4), sampleDef1), -1)
+ list += new InventoryItem(SimpleItem(PlanetSideGUID(5), sampleDef4), -1)
+ list += new InventoryItem(SimpleItem(PlanetSideGUID(6), sampleDef2), -1)
+ list += new InventoryItem(SimpleItem(PlanetSideGUID(7), sampleDef3), -1)
+ val obj : GridInventory = GridInventory(9, 9)
+
+ val (elements, out) = GridInventory.recoverInventory(list.toList, obj)
+ elements.length mustEqual 6
+ out.length mustEqual 2
+ elements.foreach(item => {
+ obj.Insert(item.start, item.obj) mustEqual true
+ })
+ out.head.Definition.Tile mustEqual InventoryTile.Tile22 //did not fit
+ out(1).Definition.Tile mustEqual InventoryTile.Tile22 //did not fit
+ ok
+ }
+ }
+}
diff --git a/common/src/test/scala/objects/NumberPoolActorTest.scala b/common/src/test/scala/objects/NumberPoolActorTest.scala
new file mode 100644
index 000000000..f0f710dd0
--- /dev/null
+++ b/common/src/test/scala/objects/NumberPoolActorTest.scala
@@ -0,0 +1,73 @@
+// Copyright (c) 2017 PSForever
+package objects
+
+import akka.actor.{ActorSystem, Props}
+import akka.pattern.ask
+import akka.util.Timeout
+
+import scala.concurrent.duration._
+import com.typesafe.config.ConfigFactory
+import net.psforever.objects.entity.IdentifiableEntity
+import net.psforever.objects.guid.NumberPoolHub
+import net.psforever.objects.guid.actor._
+
+import scala.collection.JavaConverters._
+import net.psforever.objects.guid.pool.ExclusivePool
+import net.psforever.objects.guid.selector.RandomSelector
+import net.psforever.objects.guid.source.LimitedNumberSource
+import org.specs2.mutable.Specification
+
+import scala.concurrent.Await
+import scala.util.{Failure, Try}
+import scala.concurrent.ExecutionContext.Implicits.global
+
+class NumberPoolActorTest extends Specification {
+ val config : java.util.Map[String,Object] = Map(
+ "akka.loggers" -> List("akka.event.slf4j.Slf4jLogger").asJava,
+ "akka.loglevel" -> "INFO",
+ "akka.logging-filter" -> "akka.event.slf4j.Slf4jLoggingFilter"
+ ).asJava
+ implicit val timeout = Timeout(100 milliseconds)
+
+ class TestEntity extends IdentifiableEntity
+
+ "NumberPoolActor" in {
+ val system : akka.actor.ActorSystem = ActorSystem("ActorTest", ConfigFactory.parseMap(config))
+ val pool = new ExclusivePool((25 to 50).toList)
+ pool.Selector = new RandomSelector
+ val poolActor = system.actorOf(Props(classOf[NumberPoolActor], pool), name = "poolActor")
+ val future = (poolActor ? NumberPoolActor.GetAnyNumber()).mapTo[Try[Int]]
+ future.onComplete(value => {
+ system.terminate
+ value.foreach {
+ case Failure(_) =>
+ ko
+ case _ => ;
+ }
+ })
+ Await.result(system.whenTerminated, Duration.Inf)
+ ok
+ }
+
+ "NumberPoolAccessorActor" in {
+ /*
+ Notes:
+ Receiver sets resultObject.complete to true and shuts down the ActorSystem.
+ If Receiver never gets the appropriate message, Await.result will timeout (and the exception will be caught safely).
+ */
+ val system : akka.actor.ActorSystem = ActorSystem("ActorTest", ConfigFactory.parseMap(config))
+ val hub = new NumberPoolHub(new LimitedNumberSource(51))
+ val pool = hub.AddPool("test", (25 to 50).toList)
+ pool.Selector = new RandomSelector
+ val poolActor = system.actorOf(Props(classOf[NumberPoolActor], pool), name = "poolActor")
+ val poolAccessor = system.actorOf(Props(classOf[NumberPoolAccessorActor], hub, pool, poolActor), name = "accessor")
+ val resultObject = new ResolutionObject
+ resultObject.complete mustEqual false
+ val receiver = system.actorOf(Props(classOf[Receiver], system, resultObject), "receiver")
+
+ val obj : TestEntity = new TestEntity
+ poolAccessor ! Register(obj, receiver)
+ try { Await.result(system.whenTerminated, 5 seconds) } catch { case _ : Exception => ; }
+ resultObject.complete mustEqual true
+ }
+}
diff --git a/common/src/test/scala/objects/NumberPoolHubTest.scala b/common/src/test/scala/objects/NumberPoolHubTest.scala
new file mode 100644
index 000000000..10738a182
--- /dev/null
+++ b/common/src/test/scala/objects/NumberPoolHubTest.scala
@@ -0,0 +1,285 @@
+// Copyright (c) 2017 PSForever
+package objects
+
+import net.psforever.objects.entity.IdentifiableEntity
+import net.psforever.objects.guid.NumberPoolHub
+import net.psforever.objects.guid.selector.RandomSelector
+import net.psforever.objects.guid.source.LimitedNumberSource
+import net.psforever.packet.game.PlanetSideGUID
+import org.specs2.mutable.Specification
+
+import scala.util.Success
+
+class NumberPoolHubTest extends Specification {
+ val numberList = 0 :: 1 :: 2 :: 3 :: 5 :: 8 :: 13 :: 21 :: Nil
+ val numberList1 = 0 :: 1 :: 2 :: 3 :: 5 :: Nil
+ val numberList2 = 8 :: 13 :: 21 :: 34 :: Nil
+ val numberSet1 = numberList1.toSet
+ val numberSet2 = numberList2.toSet
+ class EntityTestClass extends IdentifiableEntity
+
+ "NumberPoolHub" should {
+ "construct" in {
+ new NumberPoolHub(new LimitedNumberSource(51))
+ ok
+ }
+
+ "get a pool" in {
+ val obj = new NumberPoolHub(new LimitedNumberSource(51))
+ obj.GetPool("generic").isDefined mustEqual true //default pool
+ }
+
+ "add a pool" in {
+ val obj = new NumberPoolHub(new LimitedNumberSource(51))
+ obj.Numbers.isEmpty mustEqual true
+ obj.AddPool("fibonacci", numberList)
+ obj.Numbers.toSet.equals(numberList.toSet) mustEqual true
+ val pool = obj.GetPool("fibonacci")
+ pool.isDefined mustEqual true
+ pool.get.Numbers.equals(numberList)
+ }
+
+ "enumerate the content of all pools" in {
+ val obj = new NumberPoolHub(new LimitedNumberSource(51))
+ obj.AddPool("fibonacci1", numberList1)
+ obj.AddPool("fibonacci2", numberList2)
+ numberSet1.intersect(obj.Numbers.toSet) mustEqual numberSet1
+ numberSet2.intersect(obj.Numbers.toSet) mustEqual numberSet2
+ obj.Numbers.toSet.diff(numberSet1) mustEqual numberSet2
+ }
+
+ "remove a pool" in {
+ val obj = new NumberPoolHub(new LimitedNumberSource(51))
+ obj.Numbers.isEmpty mustEqual true
+ obj.AddPool("fibonacci", numberList)
+ obj.Numbers.toSet.equals(numberList.toSet) mustEqual true
+ obj.RemovePool("fibonacci").toSet.equals(numberList.toSet) mustEqual true
+ obj.Numbers.isEmpty mustEqual true
+ obj.GetPool("fibonacci") mustEqual None
+ }
+
+ "block removing the default 'generic' pool" in {
+ val obj = new NumberPoolHub(new LimitedNumberSource(51))
+ obj.RemovePool("generic") must throwA[IllegalArgumentException]
+ }
+
+ "block adding pools that use already-included numbers" in {
+ val obj = new NumberPoolHub(new LimitedNumberSource(51))
+ obj.AddPool("fibonacci1", numberList)
+ val numberList4 = 3 :: 7 :: 21 :: 34 :: 45 :: Nil
+ obj.AddPool("fibonacci2", numberList4) must throwA[IllegalArgumentException]
+ }
+
+ "enumerate only the content of all current pools" in {
+ val obj = new NumberPoolHub(new LimitedNumberSource(51))
+ obj.AddPool("fibonacci1", numberList1)
+ obj.AddPool("fibonacci2", numberList2)
+ numberSet1.intersect(obj.Numbers.toSet) mustEqual numberSet1
+ numberSet2.intersect(obj.Numbers.toSet) mustEqual numberSet2
+ obj.RemovePool("fibonacci1")
+ numberSet1.intersect(obj.Numbers.toSet) mustEqual Set() //no intersect
+ numberSet2.intersect(obj.Numbers.toSet) mustEqual numberSet2
+ }
+
+ "register an object to a pool" in {
+ val hub = new NumberPoolHub(new LimitedNumberSource(51))
+ hub.AddPool("fibonacci", numberList)
+ val obj = new EntityTestClass()
+ obj.GUID must throwA[Exception]
+ hub.register(obj, "fibonacci") match {
+ case Success(number) =>
+ obj.GUID mustEqual PlanetSideGUID(number)
+ case _ =>
+ ko
+ }
+ }
+
+ "lookup a registered object" in {
+ val hub = new NumberPoolHub(new LimitedNumberSource(51))
+ hub.AddPool("fibonacci", numberList)
+ val obj = new EntityTestClass()
+ hub.register(obj, "fibonacci") match {
+ case Success(number) =>
+ val objFromNumber = hub(number)
+ objFromNumber mustEqual Some(obj)
+ case _ =>
+ ko
+ }
+ }
+
+ "lookup the pool of a(n unassigned) number" in {
+ val hub = new NumberPoolHub(new LimitedNumberSource(51))
+ hub.AddPool("fibonacci1", numberList1)
+ hub.AddPool("fibonacci2", numberList2)
+ hub.WhichPool(13) mustEqual Some("fibonacci2")
+ }
+
+ "lookup the pool of a registered object" in {
+ val hub = new NumberPoolHub(new LimitedNumberSource(51))
+ hub.AddPool("fibonacci", numberList1)
+ val obj = new EntityTestClass()
+ hub.register(obj, "fibonacci")
+ hub.WhichPool(obj) mustEqual Some("fibonacci")
+ }
+
+ "register an object to a specific, unused number; it is assigned to pool 'generic'" in {
+ val hub = new NumberPoolHub(new LimitedNumberSource(51))
+ hub.AddPool("fibonacci", numberList1)
+ val obj = new EntityTestClass()
+ obj.GUID must throwA[Exception]
+ hub.register(obj, 44) match {
+ case Success(number) =>
+ obj.GUID mustEqual PlanetSideGUID(number)
+ hub.WhichPool(obj) mustEqual Some("generic")
+ case _ =>
+ ko
+ }
+ }
+
+ "register an object to a specific, pooled number" in {
+ val hub = new NumberPoolHub(new LimitedNumberSource(51))
+ hub.AddPool("fibonacci", numberList)
+ val obj = new EntityTestClass()
+ obj.GUID must throwA[Exception]
+ hub.register(obj, 13) match {
+ case Success(number) =>
+ obj.GUID mustEqual PlanetSideGUID(number)
+ hub.WhichPool(obj) mustEqual Some("fibonacci")
+ case _ =>
+ ko
+ }
+ }
+
+ "register an object without extra specifications; it is assigned to pool 'generic'" in {
+ val hub = new NumberPoolHub(new LimitedNumberSource(51))
+ val obj = new EntityTestClass()
+ hub.register(obj)
+ hub.WhichPool(obj) mustEqual Some("generic")
+ }
+
+ "unregister an object" in {
+ val hub = new NumberPoolHub(new LimitedNumberSource(51))
+ hub.AddPool("fibonacci", numberList)
+ val obj = new EntityTestClass()
+ hub.register(obj, "fibonacci")
+ hub.WhichPool(obj) mustEqual Some("fibonacci")
+ try { obj.GUID } catch { case _ : Exception => ko } //passes
+
+ hub.unregister(obj)
+ hub.WhichPool(obj) mustEqual None
+ obj.GUID must throwA[Exception] //fails
+ }
+
+ "not register an object to a different pool" in {
+ val hub = new NumberPoolHub(new LimitedNumberSource(51))
+ hub.AddPool("fibonacci1", numberList1)
+ hub.AddPool("fibonacci2", numberList2)
+ val obj = new EntityTestClass()
+ hub.register(obj, "fibonacci1")
+ hub.register(obj, "fibonacci2")
+ hub.WhichPool(obj).contains("fibonacci1") mustEqual true
+ }
+
+ "fail to unregister an object that is not registered to this hub" in {
+ val hub1 = new NumberPoolHub(new LimitedNumberSource(51))
+ val hub2 = new NumberPoolHub(new LimitedNumberSource(51))
+ hub1.AddPool("fibonacci", numberList)
+ hub2.AddPool("fibonacci", numberList)
+ val obj = new EntityTestClass()
+ hub1.register(obj, "fibonacci")
+ hub2.unregister(obj) must throwA[Exception]
+ }
+
+ "pre-register a specific, unused number" in {
+ val hub = new NumberPoolHub(new LimitedNumberSource(51))
+ hub.register(13) match {
+ case Success(_) =>
+ ok
+ case _ =>
+ ko
+ }
+ }
+
+ "pre-register a specific, pooled number" in {
+ val hub = new NumberPoolHub(new LimitedNumberSource(51))
+ hub.AddPool("fibonacci", numberList)
+ hub.register(13) match {
+ case Success(key) =>
+ key.GUID mustEqual 13
+ case _ =>
+ ko
+ }
+ }
+
+ "pre-register a number from a known pool" in {
+ val hub = new NumberPoolHub(new LimitedNumberSource(51))
+ hub.AddPool("fibonacci", numberList).Selector = new RandomSelector
+ hub.register("fibonacci") match {
+ case Success(key) =>
+ numberList.contains(key.GUID) mustEqual true
+ case _ =>
+ ko
+ }
+ }
+
+ "unregister a number" in {
+ val hub = new NumberPoolHub(new LimitedNumberSource(51))
+ hub.AddPool("fibonacci", numberList).Selector = new RandomSelector //leave this tagged on
+ val obj = new EntityTestClass()
+ hub.register(13) match {
+ case Success(key) =>
+ key.Object = obj
+ case _ =>
+ ko
+ }
+ hub.WhichPool(obj) mustEqual Some("fibonacci")
+ hub.unregister(13) match {
+ case Success(thing) =>
+ thing mustEqual Some(obj)
+ thing.get.GUID must throwA[Exception]
+ case _ =>
+ ko
+ }
+ }
+
+ "not affect the hidden restricted pool by adding a new pool" in {
+ val src = new LimitedNumberSource(51)
+ src.Restrict(4)
+ src.Restrict(8) //in fibonacci
+ src.Restrict(10)
+ src.Restrict(12)
+ val hub = new NumberPoolHub(src)
+ hub.AddPool("fibonacci", numberList) must throwA[IllegalArgumentException]
+ }
+
+ "not register an object to a number belonging to the restricted pool" in {
+ val src = new LimitedNumberSource(51)
+ src.Restrict(4)
+ val hub = new NumberPoolHub(src)
+ val obj = new EntityTestClass()
+ hub.register(obj, 4).isFailure mustEqual true
+ }
+
+ "not register an object to the restricted pool directly" in {
+ val src = new LimitedNumberSource(51)
+// src.Restrict(4)
+ val hub = new NumberPoolHub(src)
+ val obj = new EntityTestClass()
+ hub.register(obj, "").isFailure mustEqual true //the empty string represents the restricted pool
+ }
+
+ "not register a number belonging to the restricted pool" in {
+ val src = new LimitedNumberSource(51)
+ src.Restrict(4)
+ val hub = new NumberPoolHub(src)
+ hub.register(4).isFailure mustEqual true
+ }
+
+ "not unregister a number belonging to the restricted pool" in {
+ val src = new LimitedNumberSource(51)
+ src.Restrict(4)
+ val hub = new NumberPoolHub(src)
+ hub.unregister(4).isFailure mustEqual true
+ }
+ }
+}
diff --git a/common/src/test/scala/objects/NumberPoolTest.scala b/common/src/test/scala/objects/NumberPoolTest.scala
new file mode 100644
index 000000000..a8bbda2b4
--- /dev/null
+++ b/common/src/test/scala/objects/NumberPoolTest.scala
@@ -0,0 +1,194 @@
+// Copyright (c) 2017 PSForever
+package objects
+
+import net.psforever.objects.guid.pool.{ExclusivePool, GenericPool, SimplePool}
+import net.psforever.objects.guid.selector.SpecificSelector
+import org.specs2.mutable.Specification
+
+import scala.collection.mutable
+import scala.collection.mutable.ListBuffer
+import scala.util.Success
+
+class NumberPoolTest extends Specification {
+ "SimplePool" should {
+ "construct" in {
+ new SimplePool(0 :: 1 :: 2 :: Nil)
+ ok
+ }
+
+ "get a number" in {
+ val obj = new SimplePool((0 to 10).toList)
+ obj.Get() match {
+ case Success(number) =>
+ (-1 < number && number < 11) mustEqual true
+ case _ =>
+ ko
+ }
+ }
+
+ "return a number" in {
+ //returning a number for a SimplePool is actually just a way of checking that the number is in the "pool" at all
+ val obj = new SimplePool((0 to 10).toList)
+ obj.Get() match {
+ case Success(number) =>
+ obj.Return(number) mustEqual true
+ obj.Return(11) mustEqual false
+ obj.Return(number) mustEqual true
+ case _ =>
+ ko
+ }
+ }
+
+ "numbers remain available" in {
+ val obj = new SimplePool((0 to 10).toList)
+ obj.Selector = new SpecificSelector
+ obj.Selector.asInstanceOf[SpecificSelector].SelectionIndex = 8
+ obj.Get() mustEqual Success(8)
+ obj.Get() mustEqual Success(8) //compare to how SpecificSelector works otherwise - it would be an invalid return
+ }
+ }
+
+ "ExclusivePool" should {
+ "construct" in {
+ new ExclusivePool(0 :: 1 :: 2 :: Nil)
+ ok
+ }
+
+ "get a number" in {
+ val obj = new ExclusivePool((0 to 10).toList)
+ obj.Get() match {
+ case Success(number) =>
+ (-1 < number && number < 11) mustEqual true
+ case _ =>
+ ko
+ }
+ }
+
+ "get all the numbers" in {
+ val range = 0 to 10
+ val obj = new ExclusivePool((0 to 10).toList)
+ range.foreach(_ => {
+ obj.Get() match {
+ case Success(number) =>
+ (-1 < number && number < 11) mustEqual true
+ case _ =>
+ ko
+ }
+ })
+ ok
+ }
+
+ "return a number" in {
+ val obj = new ExclusivePool((0 to 10).toList)
+ obj.Get() match {
+ case Success(number) =>
+ try { obj.Return(number) mustEqual true } catch { case _ : Exception => ko }
+ case _ =>
+ ko
+ }
+ }
+
+ "return all the numbers" in {
+ val range = 0 to 10
+ val obj = new ExclusivePool((0 to 10).toList)
+ val list : ListBuffer[Int] = ListBuffer[Int]()
+ range.foreach(_ => {
+ obj.Get() match {
+ case Success(number) =>
+ list += number
+ case _ =>
+ }
+ })
+ list.foreach(number => {
+ try { obj.Return(number) mustEqual true } catch { case _ : Exception => ko }
+ })
+ ok
+ }
+ }
+
+ "GenericPool" should {
+ "construct" in {
+ new GenericPool(mutable.LongMap[String](), 11)
+ ok
+ }
+
+ "get a provided number" in {
+ val map = mutable.LongMap[String]()
+ val obj = new GenericPool(map, 11)
+ obj.Numbers.isEmpty mustEqual true
+ obj.Selector.asInstanceOf[SpecificSelector].SelectionIndex = 5
+ obj.Get() match {
+ case Success(number) =>
+ number mustEqual 5
+ map.contains(5) mustEqual true
+ map(5) mustEqual "generic"
+ obj.Numbers.contains(5) mustEqual true
+ case _ =>
+ ko
+ }
+ }
+
+ "return a number" in {
+ val map = mutable.LongMap[String]()
+ val obj = new GenericPool(map, 11)
+ obj.Selector.asInstanceOf[SpecificSelector].SelectionIndex = 5
+ obj.Get()
+ map.get(5) mustEqual Some("generic")
+ obj.Numbers.contains(5) mustEqual true
+ obj.Return(5) mustEqual true
+ map.get(5) mustEqual None
+ obj.Numbers.isEmpty mustEqual true
+ }
+
+ "block on numbers that are already defined" in {
+ val map = mutable.LongMap[String]()
+ map += 5L -> "test" //5 is defined
+ val obj = new GenericPool(map, 11)
+ obj.Numbers.isEmpty mustEqual true
+ obj.Selector.asInstanceOf[SpecificSelector].SelectionIndex = 5 //5 is requested
+ obj.Get() match {
+ case Success(_) =>
+ ko
+ case _ =>
+ obj.Numbers.isEmpty mustEqual true
+ }
+ }
+
+ "get a free number on own if none provided" in {
+ val map = mutable.LongMap[String]()
+ val obj = new GenericPool(map, 11)
+ obj.Get() match {
+ case Success(number) =>
+ number mustEqual 5
+ case _ =>
+ ko
+ }
+ }
+
+ "get a free number that is not already defined" in {
+ val map = mutable.LongMap[String]()
+ map += 5L -> "test" //5 is defined; think, -1 :: 5 :: 11
+ val obj = new GenericPool(map, 11)
+ obj.Get() match {
+ case Success(number) =>
+ number mustEqual 2 // think, -1 :: 2 :: 5 :: 11
+ case _ => ko
+ }
+
+ }
+
+ "get a free number that represents half of the largest delta" in {
+ val map = mutable.LongMap[String]()
+ map += 5L -> "test" //5 is defined; think, -1 :: 5 :: 11
+ map += 4L -> "test" //4 is defined; think, -1 :: 4 :: 5 :: 11
+ val obj = new GenericPool(map, 11)
+ obj.Get() match {
+ case Success(number) =>
+ number mustEqual 8 // think, -1 :: 4 :: 5 :: 8 :: 11
+ case _ =>
+ ko
+ }
+ }
+ }
+}
+
diff --git a/common/src/test/scala/objects/NumberSelectorTest.scala b/common/src/test/scala/objects/NumberSelectorTest.scala
new file mode 100644
index 000000000..463f96548
--- /dev/null
+++ b/common/src/test/scala/objects/NumberSelectorTest.scala
@@ -0,0 +1,326 @@
+// Copyright (c) 2017 PSForever
+package objects
+
+import net.psforever.objects.guid.selector.{RandomSequenceSelector, _}
+import org.specs2.mutable.Specification
+
+class NumberSelectorTest extends Specification {
+ def randArrayGen(n : Int = 26) : Array[Int] = {
+ val obj = Array.ofDim[Int](n)
+ (0 to 25).foreach(x => { obj(x) = x } )
+ obj
+ }
+
+ "RandomSequenceSelector" should {
+ "construct" in {
+ new RandomSequenceSelector
+ ok
+ }
+
+ "get a number" in {
+ val obj = new RandomSequenceSelector
+ obj.Get(randArrayGen()) mustNotEqual -1
+ }
+
+ "return a number" in {
+ val obj = new RandomSequenceSelector
+ val ary = randArrayGen()
+ val number = obj.Get(ary)
+ number mustNotEqual -1
+ ary.head mustEqual -1 //regardless of which number we actually got, the head of the array is now -1
+ obj.Return(number, ary)
+ ary.head mustEqual number //the returned number is at the head of the array
+ }
+
+ "get all numbers" in {
+ val n = 26
+ val obj = new RandomSequenceSelector
+ val ary = randArrayGen(n)
+ (0 until n).foreach(_ => { obj.Get(ary) mustNotEqual -1 } )
+ ok
+ }
+
+ "return all numbers" in {
+ val n = 26
+ val obj = new RandomSequenceSelector
+ val ary1 = randArrayGen(n)
+ val ary2 = randArrayGen(n)
+ (0 until n).foreach(index => { ary2(index) = obj.Get(ary1) } ) //move numbers from ary1 to ary2
+ ary2.toSet.diff(ary1.toSet).size mustEqual n //no numbers between ary2 and ary1 match
+ (0 until n).foreach(index => { obj.Return(ary2(index), ary1) mustEqual true } ) //return numbers from ary2 to ary1
+ ary2.toSet.diff(ary1.toSet).size mustEqual 0 //no difference in the content between ary2 and ary1
+ }
+
+ "gets invalid index when exhausted" in {
+ val n = 26
+ val obj = new RandomSequenceSelector
+ val ary = randArrayGen(n)
+ (0 until n).foreach(_ => { obj.Get(ary) mustNotEqual -1 } )
+ obj.Get(ary) mustEqual -1
+ }
+
+ "format an array" in {
+ val ary = Array[Int](1, -1, 5, 3, -1, 2)
+ (new RandomSequenceSelector).Format(ary)
+ ary mustEqual Array[Int](-1, -1, 1, 5, 3, 2)
+ }
+ }
+
+ "RandomSelector" should {
+ "construct" in {
+ new RandomSelector
+ ok
+ }
+
+ "get a number" in {
+ val obj = new RandomSelector
+ obj.Get(randArrayGen()) mustNotEqual -1
+ }
+
+ "return a number" in {
+ val obj = new RandomSelector
+ val ary = randArrayGen()
+ val number = obj.Get(ary)
+ number mustNotEqual -1
+ ary.head mustEqual -1 //regardless of which number we actually got, the head of the array is now -1
+ obj.Return(number, ary)
+ ary.head mustEqual number //the returned number is at the head of the array
+ }
+
+ "get all numbers" in {
+ val n = 26
+ val obj = new RandomSelector
+ val ary = randArrayGen(n)
+ (0 until n).foreach(_ => { obj.Get(ary) mustNotEqual -1 } )
+ ok
+ }
+
+ "return all numbers" in {
+ val n = 26
+ val obj = new RandomSelector
+ val ary1 = randArrayGen(n)
+ val ary2 = randArrayGen(n)
+ (0 until n).foreach(index => { ary2(index) = obj.Get(ary1) } ) //move numbers from ary1 to ary2
+ ary2.toSet.diff(ary1.toSet).size mustEqual n //no numbers between ary2 and ary1 match
+ (0 until n).foreach(index => { obj.Return(ary2(index), ary1) mustEqual true } ) //return numbers from ary2 to ary1
+ ary2.toSet.diff(ary1.toSet).size mustEqual 0 //no difference in the content between ary2 and ary1
+ }
+
+ "gets invalid index when exhausted" in {
+ val n = 26
+ val obj = new RandomSelector
+ val ary = randArrayGen(n)
+ (0 until n).foreach(_ => { obj.Get(ary) mustNotEqual -1 } )
+ obj.Get(ary) mustEqual -1
+ }
+
+ "format an array" in {
+ val ary = Array[Int](1, -1, 5, 3, -1, 2)
+ (new RandomSelector).Format(ary)
+ ary mustEqual Array[Int](-1, -1, 1, 5, 3, 2)
+ }
+ }
+
+ "StrictInOrderSelector" should {
+ "construct" in {
+ new StrictInOrderSelector
+ ok
+ }
+
+ "get a number" in {
+ val obj = new StrictInOrderSelector
+ obj.Get(randArrayGen()) mustNotEqual -1
+ }
+
+ "return a number" in {
+ val obj = new StrictInOrderSelector
+ val ary = randArrayGen()
+ val number = obj.Get(ary)
+ number mustNotEqual -1
+ ary.head mustEqual -1 //regardless of which number we actually got, the head of the array is now -1
+ obj.Return(number, ary)
+ ary.head mustEqual number //the returned number is at the head of the array
+ }
+
+ "get all numbers" in {
+ val n = 26
+ val obj = new StrictInOrderSelector
+ val ary = randArrayGen()
+ (0 until n).foreach(_ => { obj.Get(ary) mustNotEqual -1 } )
+ ok
+ }
+
+ "return all numbers" in {
+ val n = 26
+ val obj = new StrictInOrderSelector
+ val ary1 = randArrayGen(n)
+ val ary2 = randArrayGen(n)
+ (0 until n).foreach(index => { ary2(index) = obj.Get(ary1) } ) //move numbers from ary1 to ary2
+ ary2.toSet.diff(ary1.toSet).size mustEqual n //no numbers between ary2 and ary1 match
+ (0 until n).foreach(index => { obj.Return(ary2(index), ary1) mustEqual true } ) //return numbers from ary2 to ary1
+ ary2.toSet.diff(ary1.toSet).size mustEqual 0 //no difference in the content between ary2 and ary1
+ }
+
+ "gets invalid index when exhausted" in {
+ val n = 26
+ val obj = new StrictInOrderSelector
+ val ary = randArrayGen(n)
+ (0 until n).foreach(_ => { obj.Get(ary) mustNotEqual -1 } )
+ obj.Get(ary) mustEqual -1
+ }
+
+ "wait until number is available" in {
+ val n = 26
+ val obj = new StrictInOrderSelector
+ val ary = randArrayGen(n)
+ (0 until n).foreach(_ => { obj.Get(ary) mustNotEqual -1 } )
+ obj.Get(ary) mustEqual -1
+ obj.Return(1, ary) //return a number that isn't the one StrictOrder is waiting on
+ obj.Get(ary) mustEqual -1
+ obj.Return(0, ary) //return the number StrictOrder wants
+ obj.Get(ary) mustEqual 0
+ obj.Get(ary) mustEqual 1
+ }
+
+ "format an array" in {
+ val ary = Array[Int](1, -1, 5, 3, -1, 2)
+ (new StrictInOrderSelector).Format(ary)
+ ary mustEqual Array[Int](-1, 1, 2, 3, -1, 5)
+ }
+ }
+
+ "OpportunisticSelector" should {
+ "construct" in {
+ new OpportunisticSelector
+ ok
+ }
+
+ "get a number" in {
+ val obj = new OpportunisticSelector
+ obj.Get(randArrayGen()) mustNotEqual -1
+ }
+
+ "return a number" in {
+ val obj = new OpportunisticSelector
+ val ary = randArrayGen()
+ val number = obj.Get(ary)
+ number mustNotEqual -1
+ ary.head mustEqual -1 //regardless of which number we actually got, the head of the array is now -1
+ obj.Return(number, ary)
+ ary.head mustEqual number //the returned number is at the head of the array
+ }
+
+ "get all numbers" in {
+ val obj = new OpportunisticSelector
+ val ary = randArrayGen()
+ (0 to 25).foreach(_ => { obj.Get(ary) mustNotEqual -1 } )
+ ok
+ }
+
+ "return all numbers" in {
+ val n = 26
+ val obj = new OpportunisticSelector
+ val ary1 = randArrayGen(n)
+ val ary2 = randArrayGen(n)
+ (0 until n).foreach(index => { ary2(index) = obj.Get(ary1) } ) //move numbers from ary1 to ary2
+ ary2.toSet.diff(ary1.toSet).size mustEqual n //no numbers between ary2 and ary1 match
+ (0 until n).foreach(index => { obj.Return(ary2(index), ary1) mustEqual true } ) //return numbers from ary2 to ary1
+ ary2.toSet.diff(ary1.toSet).size mustEqual 0 //no difference in the content between ary2 and ary1
+ }
+
+ "gets invalid index when exhausted" in {
+ val n = 26
+ val obj = new OpportunisticSelector
+ val ary = randArrayGen(n)
+ (0 until n).foreach(_ => { obj.Get(ary) mustNotEqual -1 } )
+ obj.Get(ary) mustEqual -1
+ }
+
+ "format an array" in {
+ val ary = Array[Int](1, -1, 5, 3, -1, 2)
+ (new OpportunisticSelector).Format(ary)
+ ary mustEqual Array[Int](-1, -1, 1, 5, 3, 2)
+ }
+ }
+
+ "SpecificSelector" should {
+ "construct" in {
+ new SpecificSelector
+ ok
+ }
+
+ "get a number" in {
+ val obj = new SpecificSelector
+ val ary = randArrayGen()
+ obj.SelectionIndex = 5
+ obj.Get(ary) mustEqual 5
+ obj.Get(ary) mustEqual -1 //now that 5 has been selected, the selector will only get a -1 from that position
+ }
+
+ "return a number" in {
+ val obj = new SpecificSelector
+ val ary = randArrayGen()
+ obj.SelectionIndex = 5
+ val number = obj.Get(ary)
+ number mustEqual 5
+ obj.Get(ary) mustEqual -1
+ obj.Return(number, ary)
+ obj.Get(ary) mustEqual number //the returned number is at the head of the array
+ }
+
+ "return a number (2)" in {
+ val obj = new SpecificSelector
+ val ary = randArrayGen()
+ obj.SelectionIndex = 5
+ val number = obj.Get(ary)
+ number mustEqual 5
+ obj.Get(ary) mustEqual -1
+ ary(number) mustEqual -1
+
+ obj.SelectionIndex = 10 //even if we move the selection index, the number will return to its last position
+ obj.Return(number, ary)
+ ary(number) mustEqual number //the returned number at the original index
+ obj.Get(ary) mustEqual 10 //of course, with the selection index changed, we will not get the same position next time
+ }
+
+ "get all numbers" in {
+ val n = 26
+ val obj = new SpecificSelector
+ val ary = randArrayGen(n)
+ (0 until n).foreach(i => {
+ obj.SelectionIndex = i
+ obj.Get(ary) mustEqual i
+ })
+ ok
+ }
+
+ "return all numbers" in {
+ val n = 26
+ val obj = new SpecificSelector
+ val ary1 = randArrayGen(n)
+ val ary2 = randArrayGen(n)
+ (0 until n).foreach(index => {
+ obj.SelectionIndex = index
+ ary2(index) = obj.Get(ary1)
+ }) //move numbers from ary1 to ary2
+ ary2.toSet.diff(ary1.toSet).size mustEqual n //no numbers between ary2 and ary1 match
+ (0 until n).foreach(index => { obj.Return(ary2(index), ary1) mustEqual true } ) //return numbers from ary2 to ary1
+ ary2.toSet.diff(ary1.toSet).size mustEqual 0 //no difference in the content between ary2 and ary1
+ }
+
+ "gets invalid index when exhausted" in {
+ val obj = new SpecificSelector
+ val ary = randArrayGen()
+ obj.SelectionIndex = 5
+ obj.Get(ary) mustEqual 5
+ obj.Get(ary) mustEqual -1 //yes, it really is that simple
+ }
+
+ "format an array" in {
+ val ary = Array[Int](1, -1, 5, 3, -1, 2)
+ (new SpecificSelector).Format(ary)
+ ary mustEqual Array[Int](-1, 1, 2, 3, -1, 5)
+ }
+ }
+}
+
diff --git a/common/src/test/scala/objects/NumberSourceTest.scala b/common/src/test/scala/objects/NumberSourceTest.scala
new file mode 100644
index 000000000..dc4660617
--- /dev/null
+++ b/common/src/test/scala/objects/NumberSourceTest.scala
@@ -0,0 +1,359 @@
+// Copyright (c) 2017 PSForever
+package objects
+
+import net.psforever.objects.guid.key.{LoanedKey, SecureKey}
+import net.psforever.objects.guid.AvailabilityPolicy
+import org.specs2.mutable.Specification
+
+class NumberSourceTest extends Specification {
+ import net.psforever.objects.entity.IdentifiableEntity
+ private class TestClass extends IdentifiableEntity
+
+ "MaxNumberSource" should {
+ import net.psforever.objects.guid.source.MaxNumberSource
+ "construct" in {
+ val obj = MaxNumberSource()
+ obj.Size mustEqual Int.MaxValue
+ obj.CountAvailable mustEqual Int.MaxValue
+ obj.CountUsed mustEqual 0
+ }
+
+ "get a number" in {
+ val obj = MaxNumberSource()
+ val result : Option[LoanedKey] = obj.Available(5)
+ result.isDefined mustEqual true
+ result.get.GUID mustEqual 5
+ result.get.Policy mustEqual AvailabilityPolicy.Leased
+ result.get.Object mustEqual None
+ obj.Size mustEqual Int.MaxValue
+ obj.CountAvailable mustEqual Int.MaxValue - 1
+ obj.CountUsed mustEqual 1
+ }
+
+ "assign the number" in {
+ val obj = MaxNumberSource()
+ val result : Option[LoanedKey] = obj.Available(5)
+ result.isDefined mustEqual true
+ result.get.Object = new TestClass()
+ ok
+ }
+
+ "return a number (unused)" in {
+ val obj = MaxNumberSource()
+ val result : Option[LoanedKey] = obj.Available(5)
+ result.isDefined mustEqual true
+ result.get.GUID mustEqual 5
+ obj.CountUsed mustEqual 1
+ val ret = obj.Return(result.get)
+ ret mustEqual None
+ obj.CountUsed mustEqual 0
+ }
+
+ "return a number (assigned)" in {
+ val obj = MaxNumberSource()
+ val test = new TestClass()
+ val result : Option[LoanedKey] = obj.Available(5)
+ result.isDefined mustEqual true
+ result.get.GUID mustEqual 5
+ result.get.Object = test
+ obj.CountUsed mustEqual 1
+ val ret = obj.Return(result.get)
+ ret mustEqual Some(test)
+ obj.CountUsed mustEqual 0
+ }
+
+ "restrict a number (unassigned)" in {
+ val obj = MaxNumberSource()
+ val result : Option[LoanedKey] = obj.Restrict(5)
+ result.isDefined mustEqual true
+ result.get.GUID mustEqual 5
+ result.get.Policy mustEqual AvailabilityPolicy.Restricted
+ result.get.Object mustEqual None
+ }
+
+ "restrict a number (assigned + multiple assignments)" in {
+ val obj = MaxNumberSource()
+ val test1 = new TestClass()
+ val test2 = new TestClass()
+ val result : Option[LoanedKey] = obj.Restrict(5)
+ result.get.GUID mustEqual 5
+ result.get.Policy mustEqual AvailabilityPolicy.Restricted
+ result.get.Object mustEqual None
+ result.get.Object = None //assignment 1
+ result.get.Object mustEqual None //still unassigned
+ result.get.Object = test1 //assignment 2
+ result.get.Object mustEqual Some(test1)
+ result.get.Object = test2 //assignment 3
+ result.get.Object mustEqual Some(test1) //same as above
+ }
+
+ "return a restricted number (correctly fail)" in {
+ val obj = MaxNumberSource()
+ val test = new TestClass()
+ val result : Option[LoanedKey] = obj.Restrict(5)
+ result.get.GUID mustEqual 5
+ result.get.Policy mustEqual AvailabilityPolicy.Restricted
+ result.get.Object = test
+
+ obj.Return(5)
+ val result2 : Option[SecureKey] = obj.Get(5)
+ result2.get.GUID mustEqual 5
+ result2.get.Policy mustEqual AvailabilityPolicy.Restricted
+ result2.get.Object mustEqual Some(test)
+ }
+
+ "restrict a previously-assigned number" in {
+ val obj = MaxNumberSource()
+ val test = new TestClass()
+ val result1 : Option[LoanedKey] = obj.Available(5)
+ result1.isDefined mustEqual true
+ result1.get.Policy mustEqual AvailabilityPolicy.Leased
+ result1.get.Object = test
+ val result2 : Option[LoanedKey] = obj.Restrict(5)
+ result2.isDefined mustEqual true
+ result2.get.Policy mustEqual AvailabilityPolicy.Restricted
+ result2.get.Object mustEqual Some(test)
+ }
+
+ "check a number (not previously gotten)" in {
+ val obj = MaxNumberSource()
+ val result2 : Option[SecureKey] = obj.Get(5)
+ result2.get.GUID mustEqual 5
+ result2.get.Policy mustEqual AvailabilityPolicy.Available
+ result2.get.Object mustEqual None
+ }
+
+ "check a number (previously gotten)" in {
+ val obj = MaxNumberSource()
+ val result : Option[LoanedKey] = obj.Available(5)
+ result.isDefined mustEqual true
+ result.get.GUID mustEqual 5
+ result.get.Policy mustEqual AvailabilityPolicy.Leased
+ result.get.Object mustEqual None
+ val result2 : Option[SecureKey] = obj.Get(5)
+ result2.get.GUID mustEqual 5
+ result2.get.Policy mustEqual AvailabilityPolicy.Leased
+ result2.get.Object mustEqual None
+ }
+
+ "check a number (assigned)" in {
+ val obj = MaxNumberSource()
+ val result : Option[LoanedKey] = obj.Available(5)
+ result.isDefined mustEqual true
+ result.get.GUID mustEqual 5
+ result.get.Policy mustEqual AvailabilityPolicy.Leased
+ result.get.Object = new TestClass()
+ val result2 : Option[SecureKey] = obj.Get(5)
+ result2.get.GUID mustEqual 5
+ result2.get.Policy mustEqual AvailabilityPolicy.Leased
+ result2.get.Object mustEqual result.get.Object
+ }
+
+ "check a number (assigned and returned)" in {
+ val obj = MaxNumberSource()
+ val test = new TestClass()
+ val result : Option[LoanedKey] = obj.Available(5)
+ result.get.Policy mustEqual AvailabilityPolicy.Leased
+ result.get.Object = test
+ val result2 : Option[SecureKey] = obj.Get(5)
+ result2.get.Policy mustEqual AvailabilityPolicy.Leased
+ result2.get.Object.get === test
+ obj.Return(5) mustEqual Some(test)
+ val result3 : Option[SecureKey] = obj.Get(5)
+ result3.get.Policy mustEqual AvailabilityPolicy.Available
+ result3.get.Object mustEqual None
+ }
+
+ "clear" in {
+ val obj = MaxNumberSource()
+ val test1 = new TestClass()
+ val test2 = new TestClass()
+ obj.Available(5) //no assignment
+ obj.Available(10).get.Object = test1
+ obj.Available(15).get.Object = test2
+ obj.Restrict(15)
+ obj.Restrict(20).get.Object = test1
+ obj.CountUsed mustEqual 4
+
+ val list : List[IdentifiableEntity] = obj.Clear()
+ obj.CountUsed mustEqual 0
+ list.size mustEqual 3
+ list.count(obj => { obj == test1 }) mustEqual 2
+ list.count(obj => { obj == test2 }) mustEqual 1
+ }
+ }
+
+ "LimitedNumberSource" should {
+ import net.psforever.objects.guid.source.LimitedNumberSource
+ "construct" in {
+ val obj = LimitedNumberSource(25)
+ obj.Size mustEqual 26
+ obj.CountAvailable mustEqual 26
+ obj.CountUsed mustEqual 0
+ }
+
+ "get a number" in {
+ val obj = LimitedNumberSource(25)
+ val result : Option[LoanedKey] = obj.Available(5)
+ result.isDefined mustEqual true
+ result.get.GUID mustEqual 5
+ result.get.Policy mustEqual AvailabilityPolicy.Leased
+ result.get.Object mustEqual None
+ obj.Size mustEqual 26
+ obj.CountAvailable mustEqual 25
+ obj.CountUsed mustEqual 1
+ }
+
+ "assign the number" in {
+ val obj = LimitedNumberSource(25)
+ val result : Option[LoanedKey] = obj.Available(5)
+ result.isDefined mustEqual true
+ result.get.Object = new TestClass()
+ ok
+ }
+
+ "return a number (unused)" in {
+ val obj = LimitedNumberSource(25)
+ val result : Option[LoanedKey] = obj.Available(5)
+ result.isDefined mustEqual true
+ result.get.GUID mustEqual 5
+ obj.CountUsed mustEqual 1
+ val ret = obj.Return(result.get)
+ ret mustEqual None
+ obj.CountUsed mustEqual 0
+ }
+
+ "return a number (assigned)" in {
+ val obj = LimitedNumberSource(25)
+ val test = new TestClass()
+ val result : Option[LoanedKey] = obj.Available(5)
+ result.isDefined mustEqual true
+ result.get.GUID mustEqual 5
+ result.get.Object = test
+ obj.CountUsed mustEqual 1
+ val ret = obj.Return(result.get)
+ ret mustEqual Some(test)
+ obj.CountUsed mustEqual 0
+ }
+
+ "restrict a number (unassigned)" in {
+ val obj = LimitedNumberSource(25)
+ val result : Option[LoanedKey] = obj.Restrict(5)
+ result.isDefined mustEqual true
+ result.get.GUID mustEqual 5
+ result.get.Policy mustEqual AvailabilityPolicy.Restricted
+ result.get.Object mustEqual None
+ }
+
+ "restrict a number (assigned + multiple assignments)" in {
+ val obj = LimitedNumberSource(25)
+ val test1 = new TestClass()
+ val test2 = new TestClass()
+ val result : Option[LoanedKey] = obj.Restrict(5)
+ result.get.GUID mustEqual 5
+ result.get.Policy mustEqual AvailabilityPolicy.Restricted
+ result.get.Object mustEqual None
+ result.get.Object = None //assignment 1
+ result.get.Object mustEqual None //still unassigned
+ result.get.Object = test1 //assignment 2
+ result.get.Object mustEqual Some(test1)
+ result.get.Object = test2 //assignment 3
+ result.get.Object mustEqual Some(test1) //same as above
+ }
+
+ "return a restricted number (correctly fail)" in {
+ val obj = LimitedNumberSource(25)
+ val test = new TestClass()
+ val result : Option[LoanedKey] = obj.Restrict(5)
+ result.get.GUID mustEqual 5
+ result.get.Policy mustEqual AvailabilityPolicy.Restricted
+ result.get.Object = test
+
+ obj.Return(5)
+ val result2 : Option[SecureKey] = obj.Get(5)
+ result2.get.GUID mustEqual 5
+ result2.get.Policy mustEqual AvailabilityPolicy.Restricted
+ result2.get.Object mustEqual Some(test)
+ }
+
+ "restrict a previously-assigned number" in {
+ val obj = LimitedNumberSource(25)
+ val test = new TestClass()
+ val result1 : Option[LoanedKey] = obj.Available(5)
+ result1.isDefined mustEqual true
+ result1.get.Policy mustEqual AvailabilityPolicy.Leased
+ result1.get.Object = test
+ val result2 : Option[LoanedKey] = obj.Restrict(5)
+ result2.isDefined mustEqual true
+ result2.get.Policy mustEqual AvailabilityPolicy.Restricted
+ result2.get.Object mustEqual Some(test)
+ }
+
+ "check a number (not previously gotten)" in {
+ val obj = LimitedNumberSource(25)
+ val result2 : Option[SecureKey] = obj.Get(5)
+ result2.get.GUID mustEqual 5
+ result2.get.Policy mustEqual AvailabilityPolicy.Available
+ result2.get.Object mustEqual None
+ }
+
+ "check a number (previously gotten)" in {
+ val obj = LimitedNumberSource(25)
+ val result : Option[LoanedKey] = obj.Available(5)
+ result.isDefined mustEqual true
+ result.get.GUID mustEqual 5
+ result.get.Policy mustEqual AvailabilityPolicy.Leased
+ result.get.Object mustEqual None
+ val result2 : Option[SecureKey] = obj.Get(5)
+ result2.get.GUID mustEqual 5
+ result2.get.Policy mustEqual AvailabilityPolicy.Leased
+ result2.get.Object mustEqual None
+ }
+
+ "check a number (assigned)" in {
+ val obj = LimitedNumberSource(25)
+ val result : Option[LoanedKey] = obj.Available(5)
+ result.isDefined mustEqual true
+ result.get.GUID mustEqual 5
+ result.get.Policy mustEqual AvailabilityPolicy.Leased
+ result.get.Object = new TestClass()
+ val result2 : Option[SecureKey] = obj.Get(5)
+ result2.get.GUID mustEqual 5
+ result2.get.Policy mustEqual AvailabilityPolicy.Leased
+ result2.get.Object mustEqual result.get.Object
+ }
+
+ "check a number (assigned and returned)" in {
+ val obj = LimitedNumberSource(25)
+ val test = new TestClass()
+ val result : Option[LoanedKey] = obj.Available(5)
+ result.get.Policy mustEqual AvailabilityPolicy.Leased
+ result.get.Object = test
+ val result2 : Option[SecureKey] = obj.Get(5)
+ result2.get.Policy mustEqual AvailabilityPolicy.Leased
+ result2.get.Object.get mustEqual test
+ obj.Return(5) mustEqual Some(test)
+ val result3 : Option[SecureKey] = obj.Get(5)
+ result3.get.Policy mustEqual AvailabilityPolicy.Available
+ result3.get.Object mustEqual None
+ }
+
+ "clear" in {
+ val obj = LimitedNumberSource(25)
+ val test1 = new TestClass()
+ val test2 = new TestClass()
+ obj.Available(5) //no assignment
+ obj.Available(10).get.Object = test1
+ obj.Available(15).get.Object = test2
+ obj.Restrict(15)
+ obj.Restrict(20).get.Object = test1
+ obj.CountUsed mustEqual 4
+
+ val list : List[IdentifiableEntity] = obj.Clear()
+ obj.CountUsed mustEqual 0
+ list.size mustEqual 3
+ list.count(obj => obj == test1) mustEqual 2
+ list.count(obj => obj == test2) mustEqual 1
+ }
+ }
+}
diff --git a/common/src/test/scala/objects/PlayerTest.scala b/common/src/test/scala/objects/PlayerTest.scala
new file mode 100644
index 000000000..fd2d3fc5d
--- /dev/null
+++ b/common/src/test/scala/objects/PlayerTest.scala
@@ -0,0 +1,153 @@
+// Copyright (c) 2017 PSForever
+package objects
+
+import net.psforever.objects.{Implant, Player, SimpleItem}
+import net.psforever.objects.definition.{ImplantDefinition, SimpleItemDefinition}
+import net.psforever.objects.equipment.EquipmentSize
+import net.psforever.types.{CharacterGender, ExoSuitType, ImplantType, PlanetSideEmpire}
+import org.specs2.mutable._
+
+class PlayerTest extends Specification {
+ "construct" in {
+ val obj = new Player("Chord", PlanetSideEmpire.TR, CharacterGender.Male, 0, 5)
+ obj.isAlive mustEqual false
+ }
+
+ "(re)spawn" in {
+ val obj = new Player("Chord", PlanetSideEmpire.TR, CharacterGender.Male, 0, 5)
+ obj.isAlive mustEqual false
+ obj.Health mustEqual 0
+ obj.Stamina mustEqual 0
+ obj.Armor mustEqual 0
+ obj.Spawn
+ obj.isAlive mustEqual true
+ obj.Health mustEqual obj.MaxHealth
+ obj.Stamina mustEqual obj.MaxStamina
+ obj.Armor mustEqual obj.MaxArmor
+ }
+
+ "init (Standard Exo-Suit)" in {
+ val obj = new Player("Chord", PlanetSideEmpire.TR, CharacterGender.Male, 0, 5)
+ obj.ExoSuit mustEqual ExoSuitType.Standard
+ obj.Slot(0).Size mustEqual EquipmentSize.Pistol
+ obj.Slot(1).Size mustEqual EquipmentSize.Blocked
+ obj.Slot(2).Size mustEqual EquipmentSize.Rifle
+ obj.Slot(3).Size mustEqual EquipmentSize.Blocked
+ obj.Slot(4).Size mustEqual EquipmentSize.Melee
+ obj.Inventory.Width mustEqual 9
+ obj.Inventory.Height mustEqual 6
+ obj.Inventory.Offset mustEqual 6
+ }
+
+ "die" in {
+ val obj = new Player("Chord", PlanetSideEmpire.TR, CharacterGender.Male, 0, 5)
+ obj.Spawn
+ obj.Armor = 35 //50 -> 35
+ obj.isAlive mustEqual true
+ obj.Health mustEqual obj.MaxHealth
+ obj.Stamina mustEqual obj.MaxStamina
+ obj.Armor mustEqual 35
+ obj.Die
+ obj.isAlive mustEqual false
+ obj.Health mustEqual 0
+ obj.Stamina mustEqual 0
+ obj.Armor mustEqual 35
+ }
+
+ "draw equipped holsters only" in {
+ val wep = SimpleItem(SimpleItemDefinition(149))
+ val obj = new Player("Chord", PlanetSideEmpire.TR, CharacterGender.Male, 0, 5)
+ obj.Slot(1).Size = EquipmentSize.Pistol
+ obj.Slot(1).Equipment = wep
+ obj.DrawnSlot mustEqual Player.HandsDownSlot
+ obj.DrawnSlot = 0
+ obj.DrawnSlot mustEqual Player.HandsDownSlot
+ obj.DrawnSlot = 1
+ obj.DrawnSlot mustEqual 1
+ }
+
+ "remember the last drawn holster" in {
+ val wep1 = SimpleItem(SimpleItemDefinition(149))
+ val wep2 = SimpleItem(SimpleItemDefinition(149))
+ val obj = new Player("Chord", PlanetSideEmpire.TR, CharacterGender.Male, 0, 5)
+ obj.Slot(0).Size = EquipmentSize.Pistol
+ obj.Slot(0).Equipment = wep1
+ obj.Slot(1).Size = EquipmentSize.Pistol
+ obj.Slot(1).Equipment = wep2
+ obj.DrawnSlot mustEqual Player.HandsDownSlot //default value
+ obj.LastDrawnSlot mustEqual 0 //default value
+
+ obj.DrawnSlot = 1
+ obj.DrawnSlot mustEqual 1
+ obj.LastDrawnSlot mustEqual 0 //default value; sorry
+
+ obj.DrawnSlot = 0
+ obj.DrawnSlot mustEqual 0
+ obj.LastDrawnSlot mustEqual 1
+
+ obj.DrawnSlot = Player.HandsDownSlot
+ obj.DrawnSlot mustEqual Player.HandsDownSlot
+ obj.LastDrawnSlot mustEqual 0
+
+ obj.DrawnSlot = 1
+ obj.DrawnSlot mustEqual 1
+ obj.LastDrawnSlot mustEqual 0
+
+ obj.DrawnSlot = 0
+ obj.DrawnSlot mustEqual 0
+ obj.LastDrawnSlot mustEqual 1
+
+ obj.DrawnSlot = 1
+ obj.DrawnSlot mustEqual 1
+ obj.LastDrawnSlot mustEqual 0
+
+ obj.DrawnSlot = Player.HandsDownSlot
+ obj.DrawnSlot mustEqual Player.HandsDownSlot
+ obj.LastDrawnSlot mustEqual 1
+ }
+
+ "install no implants until a slot is unlocked" in {
+ val testplant : Implant = Implant(ImplantDefinition(1))
+ val obj = new Player("Chord", PlanetSideEmpire.TR, CharacterGender.Male, 0, 5)
+ obj.Implants(0).Unlocked mustEqual false
+ obj.Implant(0) mustEqual None
+ obj.InstallImplant(testplant)
+ obj.Implant(0) mustEqual None
+ obj.Implant(ImplantType(1)) mustEqual None
+
+ obj.Implants(0).Unlocked = true
+ obj.InstallImplant(testplant)
+ obj.Implant(0) mustEqual Some(testplant.Definition.Type)
+ obj.Implant(ImplantType(1)) mustEqual Some(testplant)
+ }
+
+ "uninstall implants" in {
+ val testplant : Implant = Implant(ImplantDefinition(1))
+ val obj = new Player("Chord", PlanetSideEmpire.TR, CharacterGender.Male, 0, 5)
+ obj.Implants(0).Unlocked = true
+ obj.InstallImplant(testplant)
+ obj.Implant(ImplantType(1)) mustEqual Some(testplant)
+
+ obj.UninstallImplant(ImplantType(1))
+ obj.Implant(0) mustEqual None
+ obj.Implant(ImplantType(1)) mustEqual None
+ }
+
+ "administrate" in {
+ val obj = new Player("Chord", PlanetSideEmpire.TR, CharacterGender.Male, 0, 5)
+ obj.Admin mustEqual false
+ Player.Administrate(obj, true)
+ obj.Admin mustEqual true
+ Player.Administrate(obj, false)
+ obj.Admin mustEqual false
+ }
+
+ "spectate" in {
+ val obj = new Player("Chord", PlanetSideEmpire.TR, CharacterGender.Male, 0, 5)
+ obj.Spectator mustEqual false
+ Player.Spectate(obj, true)
+ obj.Spectator mustEqual true
+ Player.Spectate(obj, false)
+ obj.Spectator mustEqual false
+ }
+}
diff --git a/common/src/test/scala/objects/Receiver.scala b/common/src/test/scala/objects/Receiver.scala
new file mode 100644
index 000000000..bb1894ec5
--- /dev/null
+++ b/common/src/test/scala/objects/Receiver.scala
@@ -0,0 +1,28 @@
+// Copyright (c) 2017 PSForever
+package objects
+
+import akka.actor.{Actor, ActorSystem}
+import net.psforever.objects.entity.IdentifiableEntity
+
+import scala.util.{Failure, Success}
+
+class ResolutionObject {
+ var complete = false
+}
+
+/**
+ * This is for file NumberPoolActorTest, for its tests.
+ * Attempting to define this class in the aforementioned file causes a "can not find constructor" issue.
+ */
+class Receiver(private val system : ActorSystem, result : ResolutionObject) extends Actor {
+ def receive : Receive = {
+ case Success(objct : IdentifiableEntity) =>
+ objct.GUID //this will throw a NoGUIDException if it fails
+ result.complete = true
+ system.terminate()
+ case Failure(ex) =>
+ org.log4s.getLogger.error(s"object did not register - ${ex.getMessage}")
+ system.terminate()
+ }
+}
+//TODO Look into whether that was a legitimate issue or whether I (the user) was in error during Actor initialization later.
diff --git a/pslogin/src/main/scala/AvatarService.scala b/pslogin/src/main/scala/AvatarService.scala
new file mode 100644
index 000000000..377a92496
--- /dev/null
+++ b/pslogin/src/main/scala/AvatarService.scala
@@ -0,0 +1,228 @@
+// Copyright (c) 2016 PSForever.net to present
+import akka.actor.Actor
+import akka.event.{ActorEventBus, SubchannelClassification}
+import akka.util.Subclassification
+import net.psforever.objects.equipment.Equipment
+import net.psforever.packet.game.objectcreate.ConstructorData
+import net.psforever.types.ExoSuitType
+import net.psforever.packet.game.{PlanetSideGUID, PlayerStateMessageUpstream}
+import net.psforever.types.Vector3
+
+sealed trait Action
+
+sealed trait Response
+
+final case class Join(channel : String)
+final case class Leave()
+final case class LeaveAll()
+
+object AvatarAction {
+ final case class ArmorChanged(player_guid : PlanetSideGUID, suit : ExoSuitType.Value, subtype : Int) extends Action
+ //final case class DropItem(pos : Vector3, orient : Vector3, item : PlanetSideGUID) extends Action
+ final case class EquipmentInHand(player_guid : PlanetSideGUID, slot : Int, item : Equipment) extends Action
+ final case class EquipmentOnGround(player_guid : PlanetSideGUID, pos : Vector3, orient : Vector3, item : Equipment) extends Action
+ final case class LoadPlayer(player_guid : PlanetSideGUID, pdata : ConstructorData) extends Action
+// final case class LoadMap(msg : PlanetSideGUID) extends Action
+// final case class unLoadMap(msg : PlanetSideGUID) extends Action
+ final case class ObjectDelete(player_guid : PlanetSideGUID, item_guid : PlanetSideGUID, unk : Int = 0) extends Action
+ final case class ObjectHeld(player_guid : PlanetSideGUID, slot : Int) extends Action
+ final case class PlanetsideAttribute(player_guid : PlanetSideGUID, attribute_type : Int, attribute_value : Long) extends Action
+ final case class PlayerState(player_guid : PlanetSideGUID, msg : PlayerStateMessageUpstream, spectator : Boolean, weaponInHand : Boolean) extends Action
+ final case class Reload(player_guid : PlanetSideGUID, mag : Int) extends Action
+// final case class PlayerStateShift(killer : PlanetSideGUID, victim : PlanetSideGUID) extends Action
+// final case class DestroyDisplay(killer : PlanetSideGUID, victim : PlanetSideGUID) extends Action
+// final case class HitHintReturn(killer : PlanetSideGUID, victim : PlanetSideGUID) extends Action
+// final case class ChangeWeapon(unk1 : Int, sessionId : Long) extends Action
+}
+
+object AvatarServiceResponse {
+ final case class ArmorChanged(suit : ExoSuitType.Value, subtype : Int) extends Response
+ //final case class DropItem(pos : Vector3, orient : Vector3, item : PlanetSideGUID) extends Response
+ final case class EquipmentInHand(slot : Int, item : Equipment) extends Response
+ final case class EquipmentOnGround(pos : Vector3, orient : Vector3, item : Equipment) extends Response
+ final case class LoadPlayer(pdata : ConstructorData) extends Response
+// final case class unLoadMap() extends Response
+// final case class LoadMap() extends Response
+ final case class ObjectDelete(item_guid : PlanetSideGUID, unk : Int) extends Response
+ final case class ObjectHeld(slot : Int) extends Response
+ final case class PlanetSideAttribute(attribute_type : Int, attribute_value : Long) extends Response
+ final case class PlayerState(msg : PlayerStateMessageUpstream, spectator : Boolean, weaponInHand : Boolean) extends Response
+ final case class Reload(mag : Int) extends Response
+// final case class PlayerStateShift(itemID : PlanetSideGUID) extends Response
+// final case class DestroyDisplay(itemID : PlanetSideGUID) extends Response
+// final case class HitHintReturn(itemID : PlanetSideGUID) extends Response
+// final case class ChangeWeapon(facingYaw : Int) extends Response
+}
+
+final case class AvatarServiceMessage(forChannel : String, actionMessage : Action)
+
+final case class AvatarServiceResponse(toChannel : String, avatar_guid : PlanetSideGUID, replyMessage : Response)
+
+/*
+ /avatar/
+ */
+
+class AvatarEventBus extends ActorEventBus with SubchannelClassification {
+ type Event = AvatarServiceResponse
+ type Classifier = String
+
+ protected def classify(event: Event): Classifier = event.toChannel
+
+ protected def subclassification = new Subclassification[Classifier] {
+ def isEqual(x: Classifier, y: Classifier) = x == y
+ def isSubclass(x: Classifier, y: Classifier) = x.startsWith(y)
+ }
+
+ protected def publish(event: Event, subscriber: Subscriber): Unit = {
+ subscriber ! event
+ }
+}
+
+class AvatarService extends Actor {
+ //import AvatarServiceResponse._
+ private [this] val log = org.log4s.getLogger
+
+ override def preStart = {
+ log.info("Starting...")
+ }
+
+ val AvatarEvents = new AvatarEventBus
+
+ /*val channelMap = Map(
+ AvatarMessageType.CMT_OPEN -> AvatarPath("local")
+ )*/
+
+ def receive = {
+ case Join(channel) =>
+ val path = "/Avatar/" + channel
+ val who = sender()
+
+ log.info(s"$who has joined $path")
+
+ AvatarEvents.subscribe(who, path)
+ case Leave() =>
+ AvatarEvents.unsubscribe(sender())
+ case LeaveAll() =>
+ AvatarEvents.unsubscribe(sender())
+
+ case AvatarServiceMessage(forChannel, action) =>
+ action match {
+ case AvatarAction.ArmorChanged(player_guid, suit, subtype) =>
+ AvatarEvents.publish(
+ AvatarServiceResponse("/Avatar/" + forChannel, player_guid, AvatarServiceResponse.ArmorChanged(suit, subtype))
+ )
+ case AvatarAction.EquipmentInHand(player_guid, slot, obj) =>
+ AvatarEvents.publish(
+ AvatarServiceResponse("/Avatar/" + forChannel, player_guid, AvatarServiceResponse.EquipmentInHand(slot, obj))
+ )
+ case AvatarAction.EquipmentOnGround(player_guid, pos, orient, obj) =>
+ AvatarEvents.publish(
+ AvatarServiceResponse("/Avatar/" + forChannel, player_guid, AvatarServiceResponse.EquipmentOnGround(pos, orient, obj))
+ )
+ case AvatarAction.LoadPlayer(player_guid, pdata) =>
+ AvatarEvents.publish(
+ AvatarServiceResponse("/Avatar/" + forChannel, player_guid, AvatarServiceResponse.LoadPlayer(pdata))
+ )
+ case AvatarAction.ObjectDelete(player_guid, item_guid, unk) =>
+ AvatarEvents.publish(
+ AvatarServiceResponse("/Avatar/" + forChannel, player_guid, AvatarServiceResponse.ObjectDelete(item_guid, unk))
+ )
+ case AvatarAction.ObjectHeld(player_guid, slot) =>
+ AvatarEvents.publish(
+ AvatarServiceResponse("/Avatar/" + forChannel, player_guid, AvatarServiceResponse.ObjectHeld(slot))
+ )
+ case AvatarAction.PlanetsideAttribute(guid, attribute_type, attribute_value) =>
+ AvatarEvents.publish(
+ AvatarServiceResponse("/Avatar/" + forChannel, guid, AvatarServiceResponse.PlanetSideAttribute(attribute_type, attribute_value))
+ )
+ case AvatarAction.PlayerState(guid, msg, spectator, weapon) =>
+ AvatarEvents.publish(
+ AvatarServiceResponse("/Avatar/" + forChannel, guid, AvatarServiceResponse.PlayerState(msg, spectator, weapon))
+ )
+ case AvatarAction.Reload(player_guid, mag) =>
+ AvatarEvents.publish(
+ AvatarServiceResponse("/Avatar/" + forChannel, player_guid, AvatarServiceResponse.Reload(mag))
+ )
+ case _ => ;
+ }
+
+ /*
+ case AvatarService.PlayerStateMessage(msg) =>
+ // log.info(s"NEW: ${m}")
+ val playerOpt: Option[PlayerAvatar] = PlayerMasterList.getPlayer(msg.avatar_guid)
+ if (playerOpt.isDefined) {
+ val player: PlayerAvatar = playerOpt.get
+ AvatarEvents.publish(AvatarMessage("/Avatar/" + player.continent, msg.avatar_guid,
+ AvatarServiceReply.PlayerStateMessage(msg.pos, msg.vel, msg.facingYaw, msg.facingPitch, msg.facingYawUpper, msg.is_crouching, msg.is_jumping, msg.jump_thrust, msg.is_cloaked)
+ ))
+
+ }
+ case AvatarService.LoadMap(msg) =>
+ val playerOpt: Option[PlayerAvatar] = PlayerMasterList.getPlayer(msg.guid)
+ if (playerOpt.isDefined) {
+ val player: PlayerAvatar = playerOpt.get
+ AvatarEvents.publish(AvatarMessage("/Avatar/" + player.continent, PlanetSideGUID(msg.guid),
+ AvatarServiceReply.LoadMap()
+ ))
+ }
+ case AvatarService.unLoadMap(msg) =>
+ val playerOpt: Option[PlayerAvatar] = PlayerMasterList.getPlayer(msg.guid)
+ if (playerOpt.isDefined) {
+ val player: PlayerAvatar = playerOpt.get
+ AvatarEvents.publish(AvatarMessage("/Avatar/" + player.continent, PlanetSideGUID(msg.guid),
+ AvatarServiceReply.unLoadMap()
+ ))
+ }
+ case AvatarService.ObjectHeld(msg) =>
+ val playerOpt: Option[PlayerAvatar] = PlayerMasterList.getPlayer(msg.guid)
+ if (playerOpt.isDefined) {
+ val player: PlayerAvatar = playerOpt.get
+ AvatarEvents.publish(AvatarMessage("/Avatar/" + player.continent, PlanetSideGUID(msg.guid),
+ AvatarServiceReply.ObjectHeld()
+ ))
+ }
+ case AvatarService.PlanetsideAttribute(guid, attribute_type, attribute_value) =>
+ val playerOpt: Option[PlayerAvatar] = PlayerMasterList.getPlayer(guid)
+ if (playerOpt.isDefined) {
+ val player: PlayerAvatar = playerOpt.get
+ AvatarEvents.publish(AvatarMessage("/Avatar/" + player.continent, guid,
+ AvatarServiceReply.PlanetSideAttribute(attribute_type, attribute_value)
+ ))
+ }
+ case AvatarService.PlayerStateShift(killer, guid) =>
+ val playerOpt: Option[PlayerAvatar] = PlayerMasterList.getPlayer(guid)
+ if (playerOpt.isDefined) {
+ val player: PlayerAvatar = playerOpt.get
+ AvatarEvents.publish(AvatarMessage("/Avatar/" + player.continent, guid,
+ AvatarServiceReply.PlayerStateShift(killer)
+ ))
+ }
+ case AvatarService.DestroyDisplay(killer, victim) =>
+ val playerOpt: Option[PlayerAvatar] = PlayerMasterList.getPlayer(victim)
+ if (playerOpt.isDefined) {
+ val player: PlayerAvatar = playerOpt.get
+ AvatarEvents.publish(AvatarMessage("/Avatar/" + player.continent, victim,
+ AvatarServiceReply.DestroyDisplay(killer)
+ ))
+ }
+ case AvatarService.HitHintReturn(source_guid,victim_guid) =>
+ val playerOpt: Option[PlayerAvatar] = PlayerMasterList.getPlayer(source_guid)
+ if (playerOpt.isDefined) {
+ val player: PlayerAvatar = playerOpt.get
+ AvatarEvents.publish(AvatarMessage("/Avatar/" + player.continent, victim_guid,
+ AvatarServiceReply.DestroyDisplay(source_guid)
+ ))
+ }
+ case AvatarService.ChangeWeapon(unk1, sessionId) =>
+ val playerOpt: Option[PlayerAvatar] = PlayerMasterList.getPlayer(sessionId)
+ if (playerOpt.isDefined) {
+ val player: PlayerAvatar = playerOpt.get
+ AvatarEvents.publish(AvatarMessage("/Avatar/" + player.continent, PlanetSideGUID(player.guid),
+ AvatarServiceReply.ChangeWeapon(unk1)
+ ))
+ }
+ */
+ case msg =>
+ log.info(s"Unhandled message $msg from $sender")
+ }
+}
diff --git a/pslogin/src/main/scala/PsLogin.scala b/pslogin/src/main/scala/PsLogin.scala
index 4745594d8..fefc32c80 100644
--- a/pslogin/src/main/scala/PsLogin.scala
+++ b/pslogin/src/main/scala/PsLogin.scala
@@ -4,6 +4,7 @@ import java.io.File
import java.util.Locale
import akka.actor.{ActorSystem, Props}
+import akka.routing.RandomPool
import ch.qos.logback.classic.LoggerContext
import ch.qos.logback.classic.joran.JoranConfigurator
import ch.qos.logback.core.joran.spi.JoranException
@@ -11,6 +12,10 @@ import ch.qos.logback.core.status._
import ch.qos.logback.core.util.StatusPrinter
import com.typesafe.config.ConfigFactory
import net.psforever.crypto.CryptoInterface
+import net.psforever.objects.guid.{NumberPoolHub, TaskResolver}
+import net.psforever.objects.guid.actor.{NumberPoolAccessorActor, NumberPoolActor}
+import net.psforever.objects.guid.selector.RandomSelector
+import net.psforever.objects.guid.source.LimitedNumberSource
import org.slf4j
import org.fusesource.jansi.Ansi._
import org.fusesource.jansi.Ansi.Color._
@@ -25,7 +30,7 @@ object PsLogin {
var args : Array[String] = Array()
var config : java.util.Map[String,Object] = null
- var system : akka.actor.ActorSystem = null
+ implicit var system : akka.actor.ActorSystem = null
var loginRouter : akka.actor.Props = null
var worldRouter : akka.actor.Props = null
var loginListener : akka.actor.ActorRef = null
@@ -195,6 +200,25 @@ object PsLogin {
)
*/
+ val serviceManager = ServiceManager.boot
+
+ //experimental guid code
+ val hub = new NumberPoolHub(new LimitedNumberSource(65536))
+ val pool1 = hub.AddPool("test1", (400 to 599).toList)
+ val poolActor1 = system.actorOf(Props(classOf[NumberPoolActor], pool1), name = "poolActor1")
+ pool1.Selector = new RandomSelector
+ val pool2 = hub.AddPool("test2", (600 to 799).toList)
+ val poolActor2 = system.actorOf(Props(classOf[NumberPoolActor], pool2), name = "poolActor2")
+ pool2.Selector = new RandomSelector
+
+ serviceManager ! ServiceManager.Register(Props(classOf[NumberPoolAccessorActor], hub, pool1, poolActor1), "accessor1")
+ serviceManager ! ServiceManager.Register(Props(classOf[NumberPoolAccessorActor], hub, pool2, poolActor2), "accessor2")
+
+ //task resolver
+ serviceManager ! ServiceManager.Register(RandomPool(50).props(Props[TaskResolver]), "taskResolver")
+
+ serviceManager ! ServiceManager.Register(Props[AvatarService], "avatar")
+
/** Create two actors for handling the login and world server endpoints */
loginRouter = Props(new SessionRouter("Login", loginTemplate))
worldRouter = Props(new SessionRouter("World", worldTemplate))
diff --git a/pslogin/src/main/scala/ServiceManager.scala b/pslogin/src/main/scala/ServiceManager.scala
new file mode 100644
index 000000000..40f2c4c0f
--- /dev/null
+++ b/pslogin/src/main/scala/ServiceManager.scala
@@ -0,0 +1,64 @@
+// Copyright (c) 2017 PSForever
+import akka.actor.{Actor, ActorIdentity, ActorRef, ActorSystem, Identify, Props}
+
+import scala.collection.mutable
+
+object ServiceManager {
+ var serviceManager = Actor.noSender
+
+ def boot(implicit system : ActorSystem) = {
+ serviceManager = system.actorOf(Props[ServiceManager], "service")
+ serviceManager
+ }
+
+ case class Register(props : Props, name : String)
+ case class Lookup(name : String)
+ case class LookupResult(request : String, endpoint : ActorRef)
+}
+
+class ServiceManager extends Actor {
+ import ServiceManager._
+ private [this] val log = org.log4s.getLogger
+
+ var nextLookupId : Long = 0
+ val lookups : mutable.LongMap[RequestEntry] = mutable.LongMap()
+
+ override def preStart = {
+ log.info("Starting...")
+ }
+
+ def receive = {
+ case Register(props, name) =>
+ log.info(s"Registered $name service")
+ context.actorOf(props, name)
+ case Lookup(name) =>
+ context.actorSelection(name) ! Identify(nextLookupId)
+ lookups += nextLookupId -> RequestEntry(name, sender())
+ nextLookupId += 1
+
+ case ActorIdentity(id, Some(ref)) =>
+ val idNumber = id.asInstanceOf[Long]
+ lookups.get(idNumber) match {
+ case Some(RequestEntry(name, sender)) =>
+ sender ! LookupResult(name, ref)
+ lookups.remove(idNumber)
+ case _ =>
+ //TODO something
+ }
+
+ case ActorIdentity(id, None) =>
+ val idNumber = id.asInstanceOf[Long]
+ lookups.get(idNumber) match {
+ case Some(RequestEntry(name, _)) =>
+ log.error(s"request #$idNumber for service `$name` came back empty; it may not exist")
+ lookups.remove(idNumber)
+ case _ =>
+ //TODO something
+ }
+
+ case default =>
+ log.error(s"invalid message received - $default")
+ }
+
+ protected case class RequestEntry(request : String, responder : ActorRef)
+}
diff --git a/pslogin/src/main/scala/WorldSessionActor.scala b/pslogin/src/main/scala/WorldSessionActor.scala
index f3940e9f5..ae61a4cf3 100644
--- a/pslogin/src/main/scala/WorldSessionActor.scala
+++ b/pslogin/src/main/scala/WorldSessionActor.scala
@@ -1,29 +1,59 @@
// Copyright (c) 2017 PSForever
+import java.util.concurrent.atomic.AtomicInteger
+
import akka.actor.{Actor, ActorRef, Cancellable, MDCContextAware}
import net.psforever.packet.{PlanetSideGamePacket, _}
import net.psforever.packet.control._
-import net.psforever.packet.game._
+import net.psforever.packet.game.{ObjectCreateDetailedMessage, _}
import scodec.Attempt.{Failure, Successful}
import scodec.bits._
import org.log4s.MDC
import MDCContextAware.Implicits._
+import ServiceManager.Lookup
+import net.psforever.objects._
+import net.psforever.objects.entity.IdentifiableEntity
+import net.psforever.objects.equipment._
+import net.psforever.objects.guid.{Task, TaskResolver}
+import net.psforever.objects.guid.actor.{Register, Unregister}
+import net.psforever.objects.inventory.{GridInventory, InventoryItem}
+import net.psforever.objects.terminals.{OrderTerminalDefinition, Terminal}
import net.psforever.packet.game.objectcreate._
import net.psforever.types._
+import scala.annotation.tailrec
+
class WorldSessionActor extends Actor with MDCContextAware {
private[this] val log = org.log4s.getLogger
- private case class PokeClient()
+ private final case class PokeClient()
+ private final case class ServerLoaded()
+ private final case class PlayerLoaded(tplayer : Player)
+ private final case class ListAccountCharacters()
+ private final case class SetCurrentAvatar(tplayer : Player)
+ private final case class Continent_GiveItemFromGround(tplyaer : Player, item : Option[Equipment]) //TODO wrong place, move later
var sessionId : Long = 0
var leftRef : ActorRef = ActorRef.noSender
var rightRef : ActorRef = ActorRef.noSender
+ var avatarService = Actor.noSender
+ var accessor = Actor.noSender
+ var taskResolver = Actor.noSender
- var clientKeepAlive : Cancellable = null
+ var clientKeepAlive : Cancellable = WorldSessionActor.DefaultCancellable
override def postStop() = {
if(clientKeepAlive != null)
clientKeepAlive.cancel()
+
+ avatarService ! Leave()
+ LivePlayerList.Remove(sessionId) match {
+ case Some(tplayer) =>
+ val guid = tplayer.GUID
+ avatarService ! AvatarServiceMessage(tplayer.Continent, AvatarAction.ObjectDelete(guid, guid))
+ taskResolver ! UnregisterAvatar(tplayer)
+ //TODO normally, the actual player avatar persists a minute or so after the user disconnects
+ case None => ;
+ }
}
def receive = Initializing
@@ -34,6 +64,10 @@ class WorldSessionActor extends Actor with MDCContextAware {
leftRef = sender()
rightRef = right.asInstanceOf[ActorRef]
+ ServiceManager.serviceManager ! Lookup("avatar")
+ ServiceManager.serviceManager ! Lookup("accessor1")
+ ServiceManager.serviceManager ! Lookup("taskResolver")
+
context.become(Started)
case _ =>
log.error("Unknown message")
@@ -41,14 +75,415 @@ class WorldSessionActor extends Actor with MDCContextAware {
}
def Started : Receive = {
+ case ServiceManager.LookupResult("avatar", endpoint) =>
+ avatarService = endpoint
+ log.info("ID: " + sessionId + " Got avatar service " + endpoint)
+ case ServiceManager.LookupResult("accessor1", endpoint) =>
+ accessor = endpoint
+ log.info("ID: " + sessionId + " Got guid service " + endpoint)
+ case ServiceManager.LookupResult("taskResolver", endpoint) =>
+ taskResolver = endpoint
+ log.info("ID: " + sessionId + " Got task resolver service " + endpoint)
+
case ctrl @ ControlPacket(_, _) =>
handlePktContainer(ctrl)
case game @ GamePacket(_, _, _) =>
handlePktContainer(game)
// temporary hack to keep the client from disconnecting
case PokeClient() =>
- sendResponse(PacketCoding.CreateGamePacket(0, KeepAliveMessage(0)))
- case default => failWithError(s"Invalid packet class received: $default")
+ sendResponse(PacketCoding.CreateGamePacket(0, KeepAliveMessage()))
+
+ case AvatarServiceResponse(_, guid, reply) =>
+ reply match {
+ case AvatarServiceResponse.ArmorChanged(suit, subtype) =>
+ if(player.GUID != guid) {
+ sendResponse(PacketCoding.CreateGamePacket(0, ArmorChangedMessage(guid, suit, subtype)))
+ }
+
+ case AvatarServiceResponse.EquipmentInHand(slot, item) =>
+ if(player.GUID != guid) {
+ val definition = item.Definition
+ sendResponse(
+ PacketCoding.CreateGamePacket(0,
+ ObjectCreateMessage(
+ definition.ObjectId,
+ item.GUID,
+ ObjectCreateMessageParent(guid, slot),
+ definition.Packet.ConstructorData(item).get
+ )
+ )
+ )
+ }
+
+ case AvatarServiceResponse.EquipmentOnGround(pos, orient, item) =>
+ if(player.GUID != guid) {
+ val definition = item.Definition
+ sendResponse(
+ PacketCoding.CreateGamePacket(0,
+ ObjectCreateMessage(
+ definition.ObjectId,
+ item.GUID,
+ DroppedItemData(PlacementData(pos, Vector3(0f, 0f, orient.z)), definition.Packet.ConstructorData(item).get)
+ )
+ )
+ )
+ }
+
+ case AvatarServiceResponse.LoadPlayer(pdata) =>
+ if(player.GUID != guid) {
+ sendResponse(
+ PacketCoding.CreateGamePacket(
+ 0,
+ ObjectCreateMessage(ObjectClass.avatar, guid, pdata)
+ )
+ )
+ }
+
+ case AvatarServiceResponse.ObjectDelete(item_guid, unk) =>
+ if(player.GUID != guid) {
+ sendResponse(PacketCoding.CreateGamePacket(0, ObjectDeleteMessage(item_guid, unk)))
+ }
+
+ case AvatarServiceResponse.ObjectHeld(slot) =>
+ if(player.GUID != guid) {
+ sendResponse(PacketCoding.CreateGamePacket(0, ObjectHeldMessage(guid, slot, true)))
+ }
+
+ case AvatarServiceResponse.PlanetSideAttribute(attribute_type, attribute_value) =>
+ if(player.GUID != guid) {
+ sendResponse(PacketCoding.CreateGamePacket(0, PlanetsideAttributeMessage(guid, attribute_type, attribute_value)))
+ }
+
+ case AvatarServiceResponse.PlayerState(msg, spectating, weaponInHand) =>
+ if(player.GUID != guid) {
+ val now = System.currentTimeMillis()
+ val (location, time, distanceSq) : (Vector3, Long, Float) = if(spectating) {
+ (Vector3(2, 2, 2), 0L, 0f)
+ }
+ else {
+ val before = player.lastSeenStreamMessage(guid.guid)
+ val dist = WorldSessionActor.DistanceSquared(player.Position, msg.pos)
+ (msg.pos, now - before, dist)
+ }
+
+ if(spectating ||
+ ((distanceSq < 900 || weaponInHand) && time > 200) ||
+ (distanceSq < 10000 && time > 500) ||
+ (distanceSq < 160000 && (msg.is_jumping || time < 200)) ||
+ (distanceSq < 160000 && msg.vel.isEmpty && time > 2000) ||
+ (distanceSq < 160000 && time > 1000) ||
+ (distanceSq > 160000 && time > 5000))
+ {
+ sendResponse(
+ PacketCoding.CreateGamePacket(0,
+ PlayerStateMessage(
+ guid,
+ location,
+ msg.vel,
+ msg.facingYaw,
+ msg.facingPitch,
+ msg.facingYawUpper,
+ 0,
+ msg.is_crouching,
+ msg.is_jumping,
+ msg.jump_thrust,
+ msg.is_cloaked
+ )
+ )
+ )
+ player.lastSeenStreamMessage(guid.guid) = now
+ }
+ }
+
+ case AvatarServiceResponse.Reload(mag) =>
+ if(player.GUID != guid) {
+ sendResponse(PacketCoding.CreateGamePacket(0, ReloadMessage(guid, mag, 0)))
+ }
+
+ case _ => ;
+ }
+
+ case Terminal.TerminalMessage(tplayer, msg, order) =>
+ order match {
+ case Terminal.BuyExosuit(exosuit, subtype) =>
+ if(tplayer.ExoSuit == exosuit) { //just refresh armor points
+ //we should never actually reach this point through conventional in-game methods
+ sendResponse(PacketCoding.CreateGamePacket(0, ItemTransactionResultMessage (msg.terminal_guid, TransactionType.Buy, true)))
+ tplayer.Armor = tplayer.MaxArmor
+ sendResponse(PacketCoding.CreateGamePacket(0, PlanetsideAttributeMessage(tplayer.GUID, 4, tplayer.Armor)))
+ avatarService ! AvatarServiceMessage(tplayer.Continent, AvatarAction.PlanetsideAttribute(tplayer.GUID, 4, tplayer.Armor))
+ }
+ else { //load a complete new exo-suit and shuffle the inventory around
+ //TODO if we're transitioning into a MAX suit, the subtype dictates the type of arm(s) if the holster list is empty
+ //save inventory before it gets cleared (empty holsters)
+ sendResponse(PacketCoding.CreateGamePacket(0, ItemTransactionResultMessage (msg.terminal_guid, TransactionType.Buy, true)))
+ val beforeHolsters = clearHolsters(tplayer.Holsters().iterator)
+ val beforeInventory = tplayer.Inventory.Clear()
+ //change suit (clear inventory and change holster sizes; note: holsters must be empty before this point)
+ Player.SuitSetup(tplayer, exosuit)
+ tplayer.Armor = tplayer.MaxArmor
+ //delete everything
+ (beforeHolsters ++ beforeInventory).foreach({ elem =>
+ sendResponse(PacketCoding.CreateGamePacket(0, ObjectDeleteMessage(elem.obj.GUID, 0)))
+ })
+ beforeHolsters.foreach({ elem =>
+ avatarService ! AvatarServiceMessage(tplayer.Continent, AvatarAction.ObjectDelete(tplayer.GUID, elem.obj.GUID))
+ })
+ //report change
+ sendResponse(PacketCoding.CreateGamePacket(0, ArmorChangedMessage(tplayer.GUID, exosuit, subtype)))
+ avatarService ! AvatarServiceMessage(player.Continent, AvatarAction.ArmorChanged(tplayer.GUID, exosuit, subtype))
+ sendResponse(PacketCoding.CreateGamePacket(0, PlanetsideAttributeMessage(tplayer.GUID, 4, tplayer.Armor)))
+ avatarService ! AvatarServiceMessage(player.Continent, AvatarAction.PlanetsideAttribute(tplayer.GUID, 4, tplayer.Armor))
+ //fill holsters
+ val (afterHolsters, toInventory) = beforeHolsters.partition(elem => elem.obj.Size == tplayer.Slot(elem.start).Size)
+ afterHolsters.foreach({elem => tplayer.Slot(elem.start).Equipment = elem.obj })
+ val finalInventory = fillEmptyHolsters(tplayer.Holsters().iterator, toInventory ++ beforeInventory)
+ //draw holsters
+ (0 until 5).foreach({index =>
+ tplayer.Slot(index).Equipment match {
+ case Some(obj) =>
+ val definition = obj.Definition
+ sendResponse(
+ PacketCoding.CreateGamePacket(0,
+ ObjectCreateDetailedMessage(
+ definition.ObjectId,
+ obj.GUID,
+ ObjectCreateMessageParent(tplayer.GUID, index),
+ definition.Packet.DetailedConstructorData(obj).get
+ )
+ )
+ )
+ avatarService ! AvatarServiceMessage(player.Continent, AvatarAction.EquipmentInHand(player.GUID, index, obj))
+ case None => ;
+ }
+ })
+ //re-draw equipment held in free hand
+ tplayer.FreeHand.Equipment match {
+ case Some(item) =>
+ val definition = item.Definition
+ sendResponse(
+ PacketCoding.CreateGamePacket(0,
+ ObjectCreateDetailedMessage(
+ definition.ObjectId,
+ item.GUID,
+ ObjectCreateMessageParent(tplayer.GUID, Player.FreeHandSlot),
+ definition.Packet.DetailedConstructorData(item).get
+ )
+ )
+ )
+ case None => ;
+ }
+ //put items back into inventory
+ val (stow, drop) = GridInventory.recoverInventory(finalInventory, tplayer.Inventory)
+ stow.foreach(elem => {
+ tplayer.Inventory.Insert(elem.start, elem.obj)
+ val obj = elem.obj
+ val definition = obj.Definition
+ sendResponse(
+ PacketCoding.CreateGamePacket(0,
+ ObjectCreateDetailedMessage(
+ definition.ObjectId,
+ obj.GUID,
+ ObjectCreateMessageParent(tplayer.GUID, elem.start),
+ definition.Packet.DetailedConstructorData(obj).get
+ )
+ )
+ )
+ })
+ //drop items on ground
+ val pos = tplayer.Position
+ val orient = tplayer.Orientation
+ drop.foreach(obj => {
+ obj.Position = pos
+ obj.Orientation = orient
+ val definition = obj.Definition
+ sendResponse(
+ PacketCoding.CreateGamePacket(0,
+ ObjectCreateMessage(
+ definition.ObjectId,
+ obj.GUID,
+ DroppedItemData(PlacementData(pos, Vector3(0f, 0f, orient.z)), definition.Packet.ConstructorData(obj).get)
+ )
+ )
+ )
+ avatarService ! AvatarServiceMessage(tplayer.Continent, AvatarAction.EquipmentOnGround(tplayer.GUID, pos, orient, obj))
+ })
+ }
+
+ case Terminal.BuyEquipment(item) => ;
+ tplayer.Fit(item) match {
+ case Some(index) =>
+ sendResponse(PacketCoding.CreateGamePacket(0, ItemTransactionResultMessage (msg.terminal_guid, TransactionType.Buy, true)))
+ PutEquipmentInSlot(tplayer, item, index)
+ case None =>
+ sendResponse(PacketCoding.CreateGamePacket(0, ItemTransactionResultMessage (msg.terminal_guid, TransactionType.Buy, false)))
+ }
+
+ case Terminal.SellEquipment() =>
+ tplayer.FreeHand.Equipment match {
+ case Some(item) =>
+ if(item.GUID == msg.item_guid) {
+ sendResponse(PacketCoding.CreateGamePacket(0, ItemTransactionResultMessage (msg.terminal_guid, TransactionType.Sell, true)))
+ RemoveEquipmentFromSlot(tplayer, item, Player.FreeHandSlot)
+ }
+ case None =>
+ sendResponse(PacketCoding.CreateGamePacket(0, ItemTransactionResultMessage (msg.terminal_guid, TransactionType.Sell, false)))
+ }
+
+ case Terminal.InfantryLoadout(exosuit, subtype, holsters, inventory) =>
+ //TODO optimizations against replacing Equipment with the exact same Equipment and potentially for recycling existing Equipment
+ log.info(s"$tplayer wants to change equipment loadout to their option #${msg.unk1 + 1}")
+ sendResponse(PacketCoding.CreateGamePacket(0, ItemTransactionResultMessage (msg.terminal_guid, TransactionType.InfantryLoadout, true)))
+ val beforeHolsters = clearHolsters(tplayer.Holsters().iterator)
+ val beforeInventory = tplayer.Inventory.Clear()
+ val beforeFreeHand = tplayer.FreeHand.Equipment
+ //change suit (clear inventory and change holster sizes; note: holsters must be empty before this point)
+ Player.SuitSetup(tplayer, exosuit)
+ tplayer.Armor = tplayer.MaxArmor
+ //delete everything
+ beforeHolsters.foreach({ elem =>
+ avatarService ! AvatarServiceMessage(tplayer.Continent, AvatarAction.ObjectDelete(tplayer.GUID, elem.obj.GUID))
+ })
+ (beforeHolsters ++ beforeInventory).foreach({ elem =>
+ sendResponse(PacketCoding.CreateGamePacket(0, ObjectDeleteMessage(elem.obj.GUID, 0)))
+ taskResolver ! UnregisterEquipment(elem.obj)
+ })
+ //report change
+ sendResponse(PacketCoding.CreateGamePacket(0, ArmorChangedMessage(tplayer.GUID, exosuit, 0)))
+ avatarService ! AvatarServiceMessage(tplayer.Continent, AvatarAction.ArmorChanged(tplayer.GUID, exosuit, subtype))
+ sendResponse(PacketCoding.CreateGamePacket(0, PlanetsideAttributeMessage(tplayer.GUID, 4, tplayer.Armor)))
+ avatarService ! AvatarServiceMessage(tplayer.Continent, AvatarAction.PlanetsideAttribute(tplayer.GUID, 4, tplayer.Armor))
+ //re-draw equipment held in free hand
+ beforeFreeHand match {
+ //TODO was any previous free hand item deleted?
+ case Some(item) =>
+ tplayer.FreeHand.Equipment = beforeFreeHand
+ val definition = item.Definition
+ sendResponse(
+ PacketCoding.CreateGamePacket(0,
+ ObjectCreateDetailedMessage(
+ definition.ObjectId,
+ item.GUID,
+ ObjectCreateMessageParent(tplayer.GUID, Player.FreeHandSlot),
+ definition.Packet.DetailedConstructorData(item).get
+ )
+ )
+ )
+ case None => ;
+ }
+ //draw holsters
+ holsters.foreach(entry => {
+ PutEquipmentInSlot(tplayer, entry.obj, entry.start)
+ })
+ //put items into inventory
+ inventory.foreach(entry => {
+ PutEquipmentInSlot(tplayer, entry.obj, entry.start)
+ })
+ //TODO drop items on ground
+
+ case Terminal.NoDeal() =>
+ log.warn(s"$tplayer made a request but the terminal rejected the order $msg")
+ sendResponse(PacketCoding.CreateGamePacket(0, ItemTransactionResultMessage(msg.terminal_guid, msg.transaction_type, false)))
+ }
+
+ case ListAccountCharacters =>
+ val gen : AtomicInteger = new AtomicInteger(1)
+
+ //load characters
+ SetCharacterSelectScreenGUID(player, gen)
+ val health = player.Health
+ val stamina = player.Stamina
+ val armor = player.Armor
+ player.Spawn
+ sendResponse(PacketCoding.CreateGamePacket(0,
+ ObjectCreateMessage(ObjectClass.avatar, player.GUID, player.Definition.Packet.ConstructorData(player).get)
+ ))
+ if(health > 0) { //player can not be dead; stay spawned as alive
+ player.Health = health
+ player.Stamina = stamina
+ player.Armor = armor
+ }
+ sendResponse(PacketCoding.CreateGamePacket(0, CharacterInfoMessage(15,PlanetSideZoneID(10000), 41605313, player.GUID, false, 6404428)))
+ RemoveCharacterSelectScreenGUID(player)
+
+ sendResponse(PacketCoding.CreateGamePacket(0, CharacterInfoMessage(0, PlanetSideZoneID(1), 0, PlanetSideGUID(0), true, 0)))
+
+ case PlayerLoaded(tplayer) =>
+ log.info(s"Player $tplayer has been loaded")
+ //init for whole server
+ //...
+ sendResponse(
+ PacketCoding.CreateGamePacket(0,
+ BuildingInfoUpdateMessage(
+ PlanetSideGUID(6), //Ceryshen
+ PlanetSideGUID(2), //Anguta
+ 8, //80% NTU
+ true, //Base hacked
+ PlanetSideEmpire.NC, //Base hacked by NC
+ 600000, //10 minutes remaining for hack
+ PlanetSideEmpire.VS, //Base owned by VS
+ 0, //!! Field != 0 will cause malformed packet. See class def.
+ None,
+ PlanetSideGeneratorState.Critical, //Generator critical
+ true, //Respawn tubes destroyed
+ true, //Force dome active
+ 16, //Tech plant lattice benefit
+ 0,
+ Nil, //!! Field > 0 will cause malformed packet. See class def.
+ 0,
+ false,
+ 8, //!! Field != 8 will cause malformed packet. See class def.
+ None,
+ true, //Boosted spawn room pain field
+ true //Boosted generator room pain field
+ )
+ )
+ )
+ sendResponse(PacketCoding.CreateGamePacket(0, ContinentalLockUpdateMessage(PlanetSideGUID(13), PlanetSideEmpire.VS))) // "The VS have captured the VS Sanctuary."
+ sendResponse(PacketCoding.CreateGamePacket(0, BroadcastWarpgateUpdateMessage(PlanetSideGUID(13), PlanetSideGUID(1), false, false, true))) // VS Sanctuary: Inactive Warpgate -> Broadcast Warpgate
+ //LoadMapMessage -> BeginZoningMessage
+ sendResponse(PacketCoding.CreateGamePacket(0, LoadMapMessage("map13","home3",40100,25,true,3770441820L))) //VS Sanctuary
+ //load the now-registered player
+ tplayer.Spawn
+ sendResponse(PacketCoding.CreateGamePacket(0,
+ ObjectCreateDetailedMessage(ObjectClass.avatar, tplayer.GUID, tplayer.Definition.Packet.DetailedConstructorData(tplayer).get)
+ ))
+ avatarService ! AvatarServiceMessage(tplayer.Continent, AvatarAction.LoadPlayer(tplayer.GUID, tplayer.Definition.Packet.ConstructorData(tplayer).get))
+ log.debug(s"ObjectCreateDetailedMessage: ${tplayer.Definition.Packet.DetailedConstructorData(tplayer).get}")
+
+ case SetCurrentAvatar(tplayer) =>
+ //avatar-specific
+ val guid = tplayer.GUID
+ LivePlayerList.Assign(sessionId, guid)
+ sendResponse(PacketCoding.CreateGamePacket(0, SetCurrentAvatarMessage(guid,0,0)))
+ sendResponse(PacketCoding.CreateGamePacket(0, CreateShortcutMessage(guid, 1, 0, true, Shortcut.MEDKIT)))
+
+ //temporary location
+ case Continent_GiveItemFromGround(tplayer, item) =>
+ item match {
+ case Some(obj) =>
+ val obj_guid = obj.GUID
+ tplayer.Fit(obj) match {
+ case Some(slot) =>
+ PickupItemFromGround(obj_guid)
+ tplayer.Slot(slot).Equipment = item
+ sendResponse(PacketCoding.CreateGamePacket(0, ObjectAttachMessage(tplayer.GUID, obj_guid, slot)))
+ avatarService ! AvatarServiceMessage(tplayer.Continent, AvatarAction.ObjectDelete(tplayer.GUID, obj_guid))
+ if(-1 < slot && slot < 5) {
+ avatarService ! AvatarServiceMessage(tplayer.Continent, AvatarAction.EquipmentInHand(tplayer.GUID, slot, obj))
+ }
+ case None =>
+ DropItemOnGround(obj, obj.Position, obj.Orientation) //restore
+ }
+ case None => ;
+ }
+
+ case WorldSessionActor.ResponseToSelf(pkt) =>
+ log.info(s"Received a direct message: $pkt")
+ sendResponse(pkt)
+
+ case default =>
+ failWithError(s"Invalid packet class received: $default")
}
def handlePkt(pkt : PlanetSidePacket) : Unit = pkt match {
@@ -106,160 +541,184 @@ class WorldSessionActor extends Actor with MDCContextAware {
}
}
- //val objectHex = hex"18 57 0C 00 00 BC 84 B0 06 C2 D7 65 53 5C A1 60 00 01 34 40 00 09 70 49 00 6C 00 6C 00 6C 00 49 00 49 00 49 00 6C 00 6C 00 6C 00 49 00 6C 00 49 00 6C 00 6C 00 49 00 6C 00 6C 00 6C 00 49 00 6C 00 6C 00 49 00 84 52 70 76 1E 80 80 00 00 00 00 00 3F FF C0 00 00 00 20 00 00 0F F6 A7 03 FF FF FF FF FF FF FF FF FF FF FF FF FF FF FF FD 90 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 01 90 01 90 00 64 00 00 01 00 7E C8 00 C8 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 01 C0 00 42 C5 46 86 C7 00 00 00 80 00 00 12 40 78 70 65 5F 73 61 6E 63 74 75 61 72 79 5F 68 65 6C 70 90 78 70 65 5F 74 68 5F 66 69 72 65 6D 6F 64 65 73 8B 75 73 65 64 5F 62 65 61 6D 65 72 85 6D 61 70 31 33 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 01 0A 23 02 60 04 04 40 00 00 10 00 06 02 08 14 D0 08 0C 80 00 02 00 02 6B 4E 00 82 88 00 00 02 00 00 C0 41 C0 9E 01 01 90 00 00 64 00 44 2A 00 10 91 00 00 00 40 00 18 08 38 94 40 20 32 00 00 00 80 19 05 48 02 17 20 00 00 08 00 70 29 80 43 64 00 00 32 00 0E 05 40 08 9C 80 00 06 40 01 C0 AA 01 19 90 00 00 C8 00 3A 15 80 28 72 00 00 19 00 04 0A B8 05 26 40 00 03 20 06 C2 58 00 A7 88 00 00 02 00 00 80 00 00"
- //currently, the character's starting BEP is discarded due to unknown bit format
- val app = CharacterAppearanceData(
- PlacementData(
- Vector3(3674.8438f, 2726.789f, 91.15625f),
- Vector3(0f, 0f, 90f)
- ),
- BasicCharacterData(
- "IlllIIIlllIlIllIlllIllI",
- PlanetSideEmpire.VS,
- CharacterGender.Female,
- 41,
- 1
- ),
- 3,
- false,
- false,
- ExoSuitType.Standard,
- "",
- 0,
- false,
- 0, 181,
- true,
- GrenadeState.None,
- false,
- false,
- false,
- RibbonBars()
- )
- val inv = InventoryItem(ObjectClass.beamer, PlanetSideGUID(76), 0, DetailedWeaponData(4, 8, ObjectClass.energy_cell, PlanetSideGUID(77), 0, DetailedAmmoBoxData(8, 16))) ::
- InventoryItem(ObjectClass.suppressor, PlanetSideGUID(78), 2, DetailedWeaponData(4, 8, ObjectClass.bullet_9mm, PlanetSideGUID(79), 0, DetailedAmmoBoxData(8, 25))) ::
- InventoryItem(ObjectClass.forceblade, PlanetSideGUID(80), 4, DetailedWeaponData(4, 8, ObjectClass.melee_ammo, PlanetSideGUID(81), 0, DetailedAmmoBoxData(8, 1))) ::
- InventoryItem(ObjectClass.locker_container, PlanetSideGUID(82), 5, DetailedAmmoBoxData(8, 1)) ::
- InventoryItem(ObjectClass.bullet_9mm, PlanetSideGUID(83), 6, DetailedAmmoBoxData(8, 50)) ::
- InventoryItem(ObjectClass.bullet_9mm, PlanetSideGUID(84), 9, DetailedAmmoBoxData(8, 50)) ::
- InventoryItem(ObjectClass.bullet_9mm, PlanetSideGUID(85), 12, DetailedAmmoBoxData(8, 50)) ::
- InventoryItem(ObjectClass.bullet_9mm_AP, PlanetSideGUID(86), 33, DetailedAmmoBoxData(8, 50)) ::
- InventoryItem(ObjectClass.energy_cell, PlanetSideGUID(87), 36, DetailedAmmoBoxData(8, 50)) ::
- InventoryItem(ObjectClass.remote_electronics_kit, PlanetSideGUID(88), 39, DetailedREKData(8)) ::
- Nil
- val obj = DetailedCharacterData(
- app,
- 100, 100,
- 50,
- 1, 7, 7,
- 100, 100,
- 28, 4, 44, 84, 104, 1900,
- "xpe_sanctuary_help" :: "xpe_th_firemodes" :: "used_beamer" :: "map13" :: Nil,
- List.empty,
- InventoryData(inv),
- DrawnSlot.None
- )
- val objectHex = ObjectCreateDetailedMessage(ObjectClass.avatar, PlanetSideGUID(75), obj)
+ val terminal = Terminal(PlanetSideGUID(55000), new OrderTerminalDefinition)
+
+ import net.psforever.objects.GlobalDefinitions._
+ //this part is created by the player (should be in case of ConnectToWorldRequestMessage, maybe)
+ val energy_cell_box1 = AmmoBox(energy_cell)
+ val energy_cell_box2 = AmmoBox(energy_cell, 16)
+ val bullet_9mm_box1 = AmmoBox(bullet_9mm)
+ val bullet_9mm_box2 = AmmoBox(bullet_9mm)
+ val bullet_9mm_box3 = AmmoBox(bullet_9mm)
+ val bullet_9mm_box4 = AmmoBox(bullet_9mm, 25)
+ val bullet_9mm_AP_box = AmmoBox(bullet_9mm_AP)
+ val melee_ammo_box = AmmoBox(melee_ammo)
+ val
+ beamer1 = Tool(beamer)
+ beamer1.AmmoSlots.head.Box = energy_cell_box2
+ val
+ suppressor1 = Tool(suppressor)
+ suppressor1.AmmoSlots.head.Box = bullet_9mm_box4
+ val
+ forceblade1 = Tool(forceblade)
+ forceblade1.AmmoSlots.head.Box = melee_ammo_box
+ val rek = SimpleItem(remote_electronics_kit)
+ val lockerContainer = LockerContainer()
+ val
+ player = Player("IlllIIIlllIlIllIlllIllI", PlanetSideEmpire.VS, CharacterGender.Female, 41, 1)
+ player.Position = Vector3(3674.8438f, 2726.789f, 91.15625f)
+ player.Orientation = Vector3(0f, 0f, 90f)
+ player.Continent = "home3"
+ player.Slot(0).Equipment = beamer1
+ player.Slot(2).Equipment = suppressor1
+ player.Slot(4).Equipment = forceblade1
+ player.Slot(5).Equipment = lockerContainer
+ player.Slot(6).Equipment = bullet_9mm_box1
+ player.Slot(9).Equipment = bullet_9mm_box2
+ player.Slot(12).Equipment = bullet_9mm_box3
+ player.Slot(33).Equipment = bullet_9mm_AP_box
+ player.Slot(36).Equipment = energy_cell_box1
+ player.Slot(39).Equipment = rek
+
+ //for player2
+ val energy_cell_box3 = AmmoBox(PlanetSideGUID(187), energy_cell)
+ val energy_cell_box4 = AmmoBox(PlanetSideGUID(177), energy_cell, 16)
+ val bullet_9mm_box5 = AmmoBox(PlanetSideGUID(183), bullet_9mm)
+ val bullet_9mm_box6 = AmmoBox(PlanetSideGUID(184), bullet_9mm)
+ val bullet_9mm_box7 = AmmoBox(PlanetSideGUID(185), bullet_9mm)
+ val bullet_9mm_box8 = AmmoBox(PlanetSideGUID(179), bullet_9mm, 25)
+ val bullet_9mm_AP_box2 = AmmoBox(PlanetSideGUID(186), bullet_9mm_AP)
+ val melee_ammo_box2 = AmmoBox(PlanetSideGUID(181), melee_ammo)
+
+ val
+ beamer2 = Tool(PlanetSideGUID(176), beamer)
+ beamer2.AmmoSlots.head.Box = energy_cell_box4
+ val
+ suppressor2 = Tool(PlanetSideGUID(178), suppressor)
+ suppressor2.AmmoSlots.head.Box = bullet_9mm_box8
+ val
+ forceblade2 = Tool(PlanetSideGUID(180), forceblade)
+ forceblade2.AmmoSlots.head.Box = melee_ammo_box2
+ val
+ rek2 = SimpleItem(PlanetSideGUID(188), remote_electronics_kit)
+ val
+ lockerContainer2 = LockerContainer(PlanetSideGUID(182))
+ val
+ player2 = Player(PlanetSideGUID(275), "Doppelganger", PlanetSideEmpire.NC, CharacterGender.Female, 41, 1)
+ player2.Position = Vector3(3680f, 2726.789f, 91.15625f)
+ player2.Orientation = Vector3(0f, 0f, 0f)
+ player2.Continent = "home3"
+ player2.Slot(0).Equipment = beamer2
+ player2.Slot(2).Equipment = suppressor2
+ player2.Slot(4).Equipment = forceblade2
+ player2.Slot(5).Equipment = lockerContainer2
+ player2.Slot(6).Equipment = bullet_9mm_box5
+ player2.Slot(9).Equipment = bullet_9mm_box6
+ player2.Slot(12).Equipment = bullet_9mm_box7
+ player2.Slot(33).Equipment = bullet_9mm_AP_box2
+ player2.Slot(36).Equipment = energy_cell_box3
+ player2.Slot(39).Equipment = rek2
+ player2.Spawn
+
+ val hellfire_ammo_box = AmmoBox(PlanetSideGUID(432), hellfire_ammo)
+
+ val
+ fury1 = Vehicle(PlanetSideGUID(313), fury)
+ fury1.Faction = PlanetSideEmpire.VS
+ fury1.Position = Vector3(3674.8438f, 2732f, 91.15625f)
+ fury1.Orientation = Vector3(0.0f, 0.0f, 90.0f)
+ fury1.WeaponControlledFromSeat(0).get.GUID = PlanetSideGUID(300)
+ fury1.WeaponControlledFromSeat(0).get.AmmoSlots.head.Box = hellfire_ammo_box
+
+ val object2Hex = ObjectCreateMessage(ObjectClass.avatar, PlanetSideGUID(275), player2.Definition.Packet.ConstructorData(player2).get)
+ val furyHex = ObjectCreateMessage(ObjectClass.fury, PlanetSideGUID(313), fury1.Definition.Packet.ConstructorData(fury1).get)
def handleGamePkt(pkt : PlanetSideGamePacket) = pkt match {
case ConnectToWorldRequestMessage(server, token, majorVersion, minorVersion, revision, buildDate, unk) =>
-
val clientVersion = s"Client Version: $majorVersion.$minorVersion.$revision, $buildDate"
-
log.info(s"New world login to $server with Token:$token. $clientVersion")
+ self ! ListAccountCharacters
- // ObjectCreateMessage
- sendResponse(PacketCoding.CreateGamePacket(0, objectHex))
- // XXX: hard coded message
- sendRawResponse(hex"14 0F 00 00 00 10 27 00 00 C1 D8 7A 02 4B 00 26 5C B0 80 00 ")
+ case msg @ CharacterCreateRequestMessage(name, head, voice, gender, empire) =>
+ log.info("Handling " + msg)
+ sendResponse(PacketCoding.CreateGamePacket(0, ActionResultMessage(true, None)))
+ self ! ListAccountCharacters
- // NOTE: PlanetSideZoneID just chooses the background
- sendResponse(PacketCoding.CreateGamePacket(0,
- CharacterInfoMessage(0, PlanetSideZoneID(1), 0, PlanetSideGUID(0), true, 0)))
case msg @ CharacterRequestMessage(charId, action) =>
log.info("Handling " + msg)
-
action match {
case CharacterRequestAction.Delete =>
sendResponse(PacketCoding.CreateGamePacket(0, ActionResultMessage(false, Some(1))))
case CharacterRequestAction.Select =>
- objectHex match {
- case obj @ ObjectCreateDetailedMessage(len, cls, guid, _, _) =>
- log.debug("Object: " + obj)
- // LoadMapMessage 13714 in mossy .gcap
- // XXX: hardcoded shit
- sendResponse(PacketCoding.CreateGamePacket(0, LoadMapMessage("map13","home3",40100,25,true,3770441820L))) //VS Sanctuary
- sendResponse(PacketCoding.CreateGamePacket(0, ZonePopulationUpdateMessage(PlanetSideGUID(13), 414, 138, 0, 138, 0, 138, 0, 138, 0)))
- sendResponse(PacketCoding.CreateGamePacket(0, objectHex))
+ LivePlayerList.Add(sessionId, player)
+ //check can spawn on last continent/location from player
+ //if yes, get continent guid accessors
+ //if no, get sanctuary guid accessors and reset the player's expectations
+ taskResolver ! RegisterAvatar(player)
- // These object_guids are specfic to VS Sanc
- sendResponse(PacketCoding.CreateGamePacket(0, SetEmpireMessage(PlanetSideGUID(2), PlanetSideEmpire.VS))) //HART building C
- sendResponse(PacketCoding.CreateGamePacket(0, SetEmpireMessage(PlanetSideGUID(29), PlanetSideEmpire.NC))) //South Villa Gun Tower
-
- sendResponse(PacketCoding.CreateGamePacket(0, TimeOfDayMessage(1191182336)))
- sendResponse(PacketCoding.CreateGamePacket(0, ContinentalLockUpdateMessage(PlanetSideGUID(13), PlanetSideEmpire.VS))) // "The VS have captured the VS Sanctuary."
- sendResponse(PacketCoding.CreateGamePacket(0, BroadcastWarpgateUpdateMessage(PlanetSideGUID(13), PlanetSideGUID(1), false, false, true))) // VS Sanctuary: Inactive Warpgate -> Broadcast Warpgate
-
- sendResponse(PacketCoding.CreateGamePacket(0,BuildingInfoUpdateMessage(
- PlanetSideGUID(6), //Ceryshen
- PlanetSideGUID(2), //Anguta
- 8, //80% NTU
- true, //Base hacked
- PlanetSideEmpire.NC, //Base hacked by NC
- 600000, //10 minutes remaining for hack
- PlanetSideEmpire.VS, //Base owned by VS
- 0, //!! Field != 0 will cause malformed packet. See class def.
- None,
- PlanetSideGeneratorState.Critical, //Generator critical
- true, //Respawn tubes destroyed
- true, //Force dome active
- 16, //Tech plant lattice benefit
- 0,
- Nil, //!! Field > 0 will cause malformed packet. See class def.
- 0,
- false,
- 8, //!! Field != 8 will cause malformed packet. See class def.
- None,
- true, //Boosted spawn room pain field
- true))) //Boosted generator room pain field
-
- sendResponse(PacketCoding.CreateGamePacket(0, SetCurrentAvatarMessage(guid,0,0)))
- sendResponse(PacketCoding.CreateGamePacket(0, CreateShortcutMessage(guid, 1, 0, true, Shortcut.MEDKIT)))
- sendResponse(PacketCoding.CreateGamePacket(0, ReplicationStreamMessage(5, Some(6), Vector(SquadListing())))) //clear squad list
-
- val fury = VehicleData(
- CommonFieldData(
- PlacementData(3674.8438f, 2732f, 91.15625f, 0.0f, 0.0f, 90.0f),
- PlanetSideEmpire.VS, 4
- ),
- 255,
- MountItem(ObjectClass.fury_weapon_systema, PlanetSideGUID(400), 1,
- WeaponData(0x6, 0x8, 0, ObjectClass.hellfire_ammo, PlanetSideGUID(432), 0, AmmoBoxData(0x8))
- )
- )
- sendResponse(PacketCoding.CreateGamePacket(0, ObjectCreateMessage(ObjectClass.fury, PlanetSideGUID(413), fury)))
-
- import scala.concurrent.duration._
- import scala.concurrent.ExecutionContext.Implicits.global
- clientKeepAlive = context.system.scheduler.schedule(0 seconds, 500 milliseconds, self, PokeClient())
- }
+ import scala.concurrent.duration._
+ import scala.concurrent.ExecutionContext.Implicits.global
+ clientKeepAlive = context.system.scheduler.schedule(0 seconds, 500 milliseconds, self, PokeClient())
case default =>
log.error("Unsupported " + default + " in " + msg)
}
- case msg @ CharacterCreateRequestMessage(name, head, voice, gender, empire) =>
- log.info("Handling " + msg)
-
- sendResponse(PacketCoding.CreateGamePacket(0, ActionResultMessage(true, None)))
- sendResponse(PacketCoding.CreateGamePacket(0,
- CharacterInfoMessage(0, PlanetSideZoneID(0), 0, PlanetSideGUID(0), true, 0)))
case KeepAliveMessage(code) =>
sendResponse(PacketCoding.CreateGamePacket(0, KeepAliveMessage()))
case msg @ BeginZoningMessage() =>
log.info("Reticulating splines ...")
+ //map-specific initializations (VS sanctuary)
+ sendResponse(PacketCoding.CreateGamePacket(0, SetEmpireMessage(PlanetSideGUID(2), PlanetSideEmpire.VS))) //HART building C
+ sendResponse(PacketCoding.CreateGamePacket(0, SetEmpireMessage(PlanetSideGUID(29), PlanetSideEmpire.NC))) //South Villa Gun Tower
+ sendResponse(PacketCoding.CreateGamePacket(0, object2Hex))
+ //sendResponse(PacketCoding.CreateGamePacket(0, furyHex))
- case msg @ PlayerStateMessageUpstream(avatar_guid, pos, vel, yaw, pitch, yawUpper, seq_time, unk3, is_crouching, is_jumping, unk4, is_cloaking, unk5, unk6) =>
- //log.info("PlayerState: " + msg)
+ sendResponse(PacketCoding.CreateGamePacket(0, ZonePopulationUpdateMessage(PlanetSideGUID(13), 414, 138, 0, 138, 0, 138, 0, 138, 0)))
+ sendResponse(PacketCoding.CreateGamePacket(0, TimeOfDayMessage(1191182336)))
+ sendResponse(PacketCoding.CreateGamePacket(0, ReplicationStreamMessage(5, Some(6), Vector(SquadListing())))) //clear squad list
+
+ //all players are part of the same zone right now, so don't expect much
+ val continent = player.Continent
+ val player_guid = player.GUID
+ LivePlayerList.WorldPopulation({ case (_, char : Player) => char.Continent == continent && char.HasGUID && char.GUID != player_guid}).foreach(char => {
+ sendResponse(
+ PacketCoding.CreateGamePacket(0,
+ ObjectCreateMessage(ObjectClass.avatar, char.GUID, char.Definition.Packet.ConstructorData(char).get)
+ )
+ )
+ })
+ //all items are part of a single zone right now, so don't expect much
+ WorldSessionActor.equipmentOnGround.foreach(item => {
+ val definition = item.Definition
+ sendResponse(
+ PacketCoding.CreateGamePacket(0,
+ ObjectCreateMessage(
+ definition.ObjectId,
+ item.GUID,
+ DroppedItemData(PlacementData(item.Position, item.Orientation), definition.Packet.ConstructorData(item).get)
+ )
+ )
+ )
+ })
+
+ avatarService ! Join("home3")
+ self ! SetCurrentAvatar(player)
+
+ case msg @ PlayerStateMessageUpstream(avatar_guid, pos, vel, yaw, pitch, yaw_upper, seq_time, unk3, is_crouching, is_jumping, unk4, is_cloaking, unk5, unk6) =>
+ player.Position = pos
+ player.Velocity = vel
+ player.Orientation = Vector3(player.Orientation.x, pitch, yaw)
+ player.FacingYawUpper = yaw_upper
+ player.Crouching = is_crouching
+ player.Jumping = is_jumping
+
+ val wepInHand : Boolean = player.Slot(player.DrawnSlot).Equipment match {
+ case Some(item) => item.Definition == bolt_driver
+ case None => false
+ }
+ avatarService ! AvatarServiceMessage(player.Continent, AvatarAction.PlayerState(avatar_guid, msg, player.Spectator, wepInHand))
+ //log.info("PlayerState: " + msg)
case msg @ ChildObjectStateMessage(object_guid, pitch, yaw) =>
//log.info("ChildObjectState: " + msg)
@@ -312,19 +771,50 @@ class WorldSessionActor extends Actor with MDCContextAware {
case msg @ DropItemMessage(item_guid) =>
log.info("DropItem: " + msg)
- //item dropped where you spawn in VS Sanctuary
- sendResponse(PacketCoding.CreateGamePacket(0, ObjectDetachMessage(PlanetSideGUID(75), item_guid, app.pos.coord, 0, 0, 0)))
+ player.FreeHand.Equipment match {
+ case Some(item) =>
+ if(item.GUID == item_guid) {
+ player.FreeHand.Equipment = None
+ DropItemOnGround(item, player.Position, player.Orientation)
+ sendResponse(PacketCoding.CreateGamePacket(0, ObjectDetachMessage(player.GUID, item.GUID, player.Position, 0f, 0f, player.Orientation.z)))
+ avatarService ! AvatarServiceMessage(player.Continent, AvatarAction.EquipmentOnGround(player.GUID, player.Position, player.Orientation, item))
+ }
+ else {
+ log.warn(s"item in hand was ${item.GUID} but trying to drop $item_guid; nothing will be dropped")
+ }
+ case None =>
+ log.error(s"$player wanted to drop an item, but it was not in hand")
+ }
case msg @ PickupItemMessage(item_guid, player_guid, unk1, unk2) =>
log.info("PickupItem: " + msg)
- sendResponse(PacketCoding.CreateGamePacket(0, PickupItemMessage(item_guid, player_guid, unk1, unk2)))
- sendResponse(PacketCoding.CreateGamePacket(0, ObjectAttachMessage(player_guid, item_guid, 250))) // item on mouse
+ self ! Continent_GiveItemFromGround(player, PickupItemFromGround(item_guid))
case msg @ ReloadMessage(item_guid, ammo_clip, unk1) =>
log.info("Reload: " + msg)
- sendResponse(PacketCoding.CreateGamePacket(0, ReloadMessage(item_guid, 123, unk1)))
+ val reloadValue = player.Slot(player.DrawnSlot).Equipment match {
+ case Some(item) =>
+ item match {
+ case tool : Tool =>
+ tool.FireMode.Magazine
+ case _ =>
+ 0
+ }
+ case None =>
+ 0
+ }
+ //TODO hunt for ammunition in inventory
+ if(reloadValue > 0) {
+ sendResponse(PacketCoding.CreateGamePacket(0, ReloadMessage(item_guid, reloadValue, unk1)))
+ }
case msg @ ObjectHeldMessage(avatar_guid, held_holsters, unk1) =>
+ val before = player.DrawnSlot
+ val after = player.DrawnSlot = held_holsters
+ if(before != after) {
+ val slot = if(after == Player.HandsDownSlot) { before } else { after }
+ avatarService ! AvatarServiceMessage(player.Continent, AvatarAction.ObjectHeld(player.GUID, slot))
+ }
log.info("ObjectHeld: " + msg)
case msg @ AvatarJumpMessage(state) =>
@@ -349,17 +839,69 @@ class WorldSessionActor extends Actor with MDCContextAware {
}
case msg @ RequestDestroyMessage(object_guid) =>
- log.info("RequestDestroy: " + msg)
// TODO: Make sure this is the correct response in all cases
- sendResponse(PacketCoding.CreateGamePacket(0, ObjectDeleteMessage(object_guid, 0)))
+ player.Find(object_guid) match {
+ case Some(slot) =>
+ taskResolver ! RemoveEquipmentFromSlot(player, player.Slot(slot).Equipment.get, slot)
+ log.info("RequestDestroy: " + msg)
+ case None =>
+ sendResponse(PacketCoding.CreateGamePacket(0, ObjectDeleteMessage(object_guid, 0)))
+ log.warn(s"RequestDestroy: object $object_guid not found")
+ }
case msg @ ObjectDeleteMessage(object_guid, unk1) =>
sendResponse(PacketCoding.CreateGamePacket(0, ObjectDeleteMessage(object_guid, 0)))
log.info("ObjectDelete: " + msg)
case msg @ MoveItemMessage(item_guid, avatar_guid_1, avatar_guid_2, dest, unk1) =>
- sendResponse(PacketCoding.CreateGamePacket(0, ObjectAttachMessage(avatar_guid_1,item_guid,dest)))
- log.info("MoveItem: " + msg)
+ player.Find(item_guid) match {
+ case Some(index) =>
+ val indexSlot = player.Slot(index)
+ val destSlot = player.Slot(dest)
+ val item = indexSlot.Equipment.get
+ val destItem = destSlot.Equipment
+ indexSlot.Equipment = None
+ destSlot.Equipment = None
+
+ (destSlot.Equipment = item) match {
+ case Some(_) => //move item
+ log.info(s"MoveItem: $item_guid moved from $avatar_guid_1 @ $index to $avatar_guid_1 @ $dest")
+ //continue on to the code following the next match statement after resolving the match statement
+ destItem match {
+ case Some(item2) => //second item to swap?
+ (indexSlot.Equipment = destItem) match {
+ case Some(_) => //yes, swap
+ log.info(s"MoveItem: ${item2.GUID} swapped to $avatar_guid_1 @ $index")
+ //we must shuffle items around cleanly to avoid causing icons to "disappear"
+ if(index == Player.FreeHandSlot) { //temporarily put in safe location, A -> C
+ sendResponse(PacketCoding.CreateGamePacket(0, ObjectDetachMessage(player.GUID, item.GUID, Vector3(0f, 0f, 0f), 0f, 0f, 0f))) //ground
+ }
+ else {
+ sendResponse(PacketCoding.CreateGamePacket(0, ObjectAttachMessage(player.GUID, item.GUID, Player.FreeHandSlot))) //free hand
+ }
+ sendResponse(PacketCoding.CreateGamePacket(0, ObjectAttachMessage(player.GUID, item2.GUID, index))) //B -> A
+ if(0 <= index && index < 5) {
+ avatarService ! AvatarServiceMessage(player.Continent, AvatarAction.EquipmentInHand(player.GUID, index, item2))
+ }
+
+ case None => //can't complete the swap; drop the other item on the ground
+ sendResponse(PacketCoding.CreateGamePacket(0, ObjectDetachMessage(player.GUID, item2.GUID, player.Position, 0f, 0f, player.Orientation.z))) //ground
+ avatarService ! AvatarServiceMessage(player.Continent, AvatarAction.EquipmentOnGround(player.GUID, player.Position, player.Orientation, item2))
+ }
+
+ case None => ; //just move item over
+ }
+ sendResponse(PacketCoding.CreateGamePacket(0, ObjectAttachMessage(avatar_guid_1, item_guid, dest)))
+ if(0 <= dest && dest < 5) {
+ avatarService ! AvatarServiceMessage(player.Continent, AvatarAction.EquipmentInHand(player.GUID, dest, item))
+ }
+
+ case None => //restore original contents
+ indexSlot.Equipment = item
+ destSlot.Equipment = destItem
+ }
+ case None => ;
+ }
case msg @ ChangeAmmoMessage(item_guid, unk1) =>
log.info("ChangeAmmo: " + msg)
@@ -379,7 +921,7 @@ class WorldSessionActor extends Actor with MDCContextAware {
sendResponse(PacketCoding.CreateGamePacket(0, GenericObjectStateMsg(object_guid, 16)))
}
- case msg @ UnuseItemMessage(player, item) =>
+ case msg @ UnuseItemMessage(player_guid, item) =>
log.info("UnuseItem: " + msg)
case msg @ DeployObjectMessage(guid, unk1, pos, roll, pitch, yaw, unk2) =>
@@ -389,12 +931,24 @@ class WorldSessionActor extends Actor with MDCContextAware {
log.info("GenericObjectState: " + msg)
case msg @ ItemTransactionMessage(terminal_guid, transaction_type, item_page, item_name, unk1, item_guid) =>
- if(transaction_type == TransactionType.Sell) {
- sendResponse(PacketCoding.CreateGamePacket(0, ObjectDeleteMessage(item_guid, 0)))
- sendResponse(PacketCoding.CreateGamePacket(0, ItemTransactionResultMessage(terminal_guid, transaction_type, true)))
- }
+ terminal.Actor ! Terminal.Request(player, msg)
log.info("ItemTransaction: " + msg)
+ case msg @ FavoritesRequest(player_guid, unk, action, line, label) =>
+ if(player.GUID == player_guid) {
+ val name = label.getOrElse("missing_loadout_name")
+ action match {
+ case FavoritesAction.Unknown => ;
+ case FavoritesAction.Save =>
+ player.SaveLoadout(name, line)
+ sendResponse(PacketCoding.CreateGamePacket(0, FavoritesMessage(0, player_guid, line, name)))
+ case FavoritesAction.Delete =>
+ player.DeleteLoadout(line)
+ sendResponse(PacketCoding.CreateGamePacket(0, FavoritesMessage(0, player_guid, line, "")))
+ }
+ }
+ log.info("FavoritesRequest: " + msg)
+
case msg @ WeaponDelayFireMessage(seq_time, weapon_guid) =>
log.info("WeaponDelayFire: " + msg)
@@ -424,7 +978,7 @@ class WorldSessionActor extends Actor with MDCContextAware {
sendResponse(PacketCoding.CreateGamePacket(0, msg)) //should be safe; replace with ObjectDetachMessage later
log.info("DismountVehicleMsg: " + msg)
- case msg @ DeployRequestMessage(player, entity, unk1, unk2, unk3, pos) =>
+ case msg @ DeployRequestMessage(player_guid, entity, unk1, unk2, unk3, pos) =>
//if you try to deploy, can not undeploy
log.info("DeployRequest: " + msg)
@@ -456,7 +1010,7 @@ class WorldSessionActor extends Actor with MDCContextAware {
case msg @ FriendsRequest(action, friend) =>
log.info("FriendsRequest: "+msg)
- case msg @ HitHint(source, player) =>
+ case msg @ HitHint(source, player_guid) =>
log.info("HitHint: "+msg)
case msg @ WeaponDryFireMessage(weapon) =>
@@ -468,6 +1022,498 @@ class WorldSessionActor extends Actor with MDCContextAware {
case default => log.error(s"Unhandled GamePacket $pkt")
}
+ /**
+ * Iterate over a group of `EquipmentSlot`s, some of which may be occupied with an item.
+ * Remove any encountered items and add them to an output `List`.
+ * @param iter the `Iterator` of `EquipmentSlot`s
+ * @param index a number that equals the "current" holster slot (`EquipmentSlot`)
+ * @param list a persistent `List` of `Equipment` in the holster slots
+ * @return a `List` of `Equipment` in the holster slots
+ */
+ @tailrec private def clearHolsters(iter : Iterator[EquipmentSlot], index : Int = 0, list : List[InventoryItem] = Nil) : List[InventoryItem] = {
+ if(!iter.hasNext) {
+ list
+ }
+ else {
+ val slot = iter.next
+ slot.Equipment match {
+ case Some(equipment) =>
+ slot.Equipment = None
+ clearHolsters(iter, index + 1, InventoryItem(equipment, index) +: list)
+ case None =>
+ clearHolsters(iter, index + 1, list)
+ }
+ }
+ }
+
+ /**
+ * Iterate over a group of `EquipmentSlot`s, some of which may be occupied with an item.
+ * For any slots that are not yet occupied by an item, search through the `List` and find an item that fits in that slot.
+ * Add that item to the slot and remove it from the list.
+ * @param iter the `Iterator` of `EquipmentSlot`s
+ * @param list a `List` of all `Equipment` that is not yet assigned to a holster slot or an inventory slot
+ * @return the `List` of all `Equipment` not yet assigned to a holster slot or an inventory slot
+ */
+ @tailrec private def fillEmptyHolsters(iter : Iterator[EquipmentSlot], list : List[InventoryItem]) : List[InventoryItem] = {
+ if(!iter.hasNext) {
+ list
+ }
+ else {
+ val slot = iter.next
+ if(slot.Equipment.isEmpty) {
+ list.find(item => item.obj.Size == slot.Size) match {
+ case Some(obj) =>
+ val index = list.indexOf(obj)
+ slot.Equipment = obj.obj
+ fillEmptyHolsters(iter, list.take(index) ++ list.drop(index + 1))
+ case None =>
+ fillEmptyHolsters(iter, list)
+ }
+ }
+ else {
+ fillEmptyHolsters(iter, list)
+ }
+ }
+ }
+
+ /**
+ * Iterate over a group of `EquipmentSlot`s, some of which may be occupied with an item.
+ * Use `func` on any discovered `Equipment` to transform items into tasking, and add the tasking to a `List`.
+ * @param iter the `Iterator` of `EquipmentSlot`s
+ * @param func the function used to build tasking from any discovered `Equipment`
+ * @param list a persistent `List` of `Equipment` tasking
+ * @return a `List` of `Equipment` tasking
+ */
+ @tailrec private def recursiveHolsterTaskBuilding(iter : Iterator[EquipmentSlot], func : ((Equipment)=>TaskResolver.GiveTask), list : List[TaskResolver.GiveTask] = Nil) : List[TaskResolver.GiveTask] = {
+ if(!iter.hasNext) {
+ list
+ }
+ else {
+ iter.next.Equipment match {
+ case Some(item) =>
+ recursiveHolsterTaskBuilding(iter, func, list :+ func(item))
+ case None =>
+ recursiveHolsterTaskBuilding(iter, func, list)
+ }
+ }
+ }
+
+ /**
+ * Construct tasking that coordinates the following:
+ * 1) Accept a new piece of `Equipment` and register it with a globally unique identifier.
+ * 2) Once it is registered, give the `Equipment` to `target`.
+ * @param target what object will accept the new `Equipment`
+ * @param obj the new `Equipment`
+ * @param index the slot where the new `Equipment` will be placed
+ * @see `RegisterEquipment`
+ * @see `PutInSlot`
+ */
+ private def PutEquipmentInSlot(target : Player, obj : Equipment, index : Int) : Unit = {
+ val regTask = RegisterEquipment(obj)
+ obj match {
+ case tool : Tool =>
+ val linearToolTask = TaskResolver.GiveTask(regTask.task) +: regTask.subs
+ taskResolver ! TaskResolver.GiveTask(PutInSlot(target, tool, index).task, linearToolTask)
+ case _ =>
+ taskResolver ! TaskResolver.GiveTask(PutInSlot(target, obj, index).task, List(regTask))
+ }
+ }
+
+ /**
+ * Construct tasking that coordinates the following:
+ * 1) Remove a new piece of `Equipment` from where it is currently stored.
+ * 2) Once it is removed, un-register the `Equipment`'s globally unique identifier.
+ * @param target the object that currently possesses the `Equipment`
+ * @param obj the `Equipment`
+ * @param index the slot from where the `Equipment` will be removed
+ * @see `UnregisterEquipment`
+ * @see `RemoveFromSlot`
+ */
+ private def RemoveEquipmentFromSlot(target : Player, obj : Equipment, index : Int) : Unit = {
+ val regTask = UnregisterEquipment(obj)
+ //to avoid an error from a GUID-less object from being searchable, it is removed from the inventory first
+ obj match {
+ case _ : Tool =>
+ taskResolver ! TaskResolver.GiveTask(regTask.task, RemoveFromSlot(target, obj, index) +: regTask.subs)
+ case _ =>
+ taskResolver ! TaskResolver.GiveTask(regTask.task, RemoveFromSlot(target, obj, index) :: Nil)
+ }
+ }
+
+ /**
+ * Construct tasking that registers an object with the a globally unique identifier selected from a pool of numbers.
+ * The object in question is not considered to have any form of internal complexity.
+ * @param obj the object being registered
+ * @return a `TaskResolver.GiveTask` message
+ */
+ private def RegisterObjectTask(obj : IdentifiableEntity) : TaskResolver.GiveTask = {
+ TaskResolver.GiveTask(
+ new Task() {
+ private val localObject = obj
+ private val localAccessor = accessor
+
+ override def isComplete : Task.Resolution.Value = {
+ try {
+ localObject.GUID
+ Task.Resolution.Success
+ }
+ catch {
+ case _ : Exception =>
+ Task.Resolution.Incomplete
+ }
+ }
+
+ def Execute(resolver : ActorRef) : Unit = {
+ localAccessor ! Register(localObject, resolver)
+ }
+ })
+ }
+
+ /**
+ * Construct tasking that registers an object that is an object of type `Tool`.
+ * `Tool` objects have internal structures called "ammo slots;"
+ * each ammo slot contains a register-able `AmmoBox` object.
+ * @param obj the object being registered
+ * @return a `TaskResolver.GiveTask` message
+ */
+ private def RegisterTool(obj : Tool) : TaskResolver.GiveTask = {
+ val ammoTasks : List[TaskResolver.GiveTask] = (0 until obj.MaxAmmoSlot).map(ammoIndex => RegisterObjectTask(obj.AmmoSlots(ammoIndex).Box)).toList
+ TaskResolver.GiveTask(RegisterObjectTask(obj).task, ammoTasks)
+ }
+
+ /**
+ * Construct tasking that registers an object, determining whether it is a complex object of type `Tool` or a more simple object type.
+ * @param obj the object being registered
+ * @return a `TaskResolver.GiveTask` message
+ */
+ private def RegisterEquipment(obj : Equipment) : TaskResolver.GiveTask = {
+ obj match {
+ case tool : Tool =>
+ RegisterTool(tool)
+ case _ =>
+ RegisterObjectTask(obj)
+ }
+ }
+
+ /**
+ * Construct tasking that gives the `Equipment` to `target`.
+ * @param target what object will accept the new `Equipment`
+ * @param obj the new `Equipment`
+ * @param index the slot where the new `Equipment` will be placed
+ * @return a `TaskResolver.GiveTask` message
+ */
+ private def PutInSlot(target : Player, obj : Equipment, index : Int) : TaskResolver.GiveTask = {
+ TaskResolver.GiveTask(
+ new Task() {
+ private val localTarget = target
+ private val localIndex = index
+ private val localObject = obj
+ private val localAnnounce = self
+
+ override def isComplete : Task.Resolution.Value = {
+ if(localTarget.Slot(localIndex).Equipment.contains(localObject)) {
+ Task.Resolution.Success
+ }
+ else {
+ Task.Resolution.Incomplete
+ }
+ }
+
+ def Execute(resolver : ActorRef) : Unit = {
+ localTarget.Slot(localIndex).Equipment = localObject
+ resolver ! scala.util.Success(localObject)
+ }
+
+ override def onSuccess() : Unit = {
+ val definition = localObject.Definition
+ localAnnounce ! WorldSessionActor.ResponseToSelf(
+ PacketCoding.CreateGamePacket(0,
+ ObjectCreateDetailedMessage(
+ definition.ObjectId,
+ localObject.GUID,
+ ObjectCreateMessageParent(localTarget.GUID, localIndex),
+ definition.Packet.DetailedConstructorData(localObject).get
+ )
+ )
+ )
+ if(0 <= localIndex && localIndex < 5) {
+ avatarService ! AvatarServiceMessage(localTarget.Continent, AvatarAction.EquipmentInHand(localTarget.GUID, localIndex, localObject))
+ }
+ }
+ })
+ }
+
+ /**
+ * Construct tasking that registers all aspects of a `Player` avatar.
+ * `Players` are complex objects that contain a variety of other register-able objects and each of these objects much be handled.
+ * @param tplayer the avatar `Player`
+ * @return a `TaskResolver.GiveTask` message
+ */
+ private def RegisterAvatar(tplayer : Player) : TaskResolver.GiveTask = {
+ val holsterTasks = recursiveHolsterTaskBuilding(tplayer.Holsters().iterator, RegisterEquipment)
+ val fifthHolsterTask = tplayer.Slot(5).Equipment match {
+ case Some(item) =>
+ RegisterEquipment(item) :: Nil
+ case None =>
+ List.empty[TaskResolver.GiveTask];
+ }
+ val inventoryTasks = tplayer.Inventory.Items.map({ case((_ : Int, entry : InventoryItem)) => RegisterEquipment(entry.obj)})
+ TaskResolver.GiveTask(
+ new Task() {
+ private val localPlayer = tplayer
+ private val localAnnounce = self
+
+ override def isComplete : Task.Resolution.Value = {
+ Task.Resolution.Incomplete
+ }
+
+ def Execute(resolver : ActorRef) : Unit = {
+ localAnnounce ! PlayerLoaded(localPlayer) //alerts WSA
+ resolver ! scala.util.Success(localPlayer)
+ }
+ }, RegisterObjectTask(tplayer) +: (holsterTasks ++ fifthHolsterTask ++ inventoryTasks)
+ )
+ }
+
+ /**
+ * Construct tasking that un-registers an object.
+ * The object in question is not considered to have any form of internal complexity.
+ * @param obj the object being un-registered
+ * @return a `TaskResolver.GiveTask` message
+ */
+ private def UnregisterObjectTask(obj : IdentifiableEntity) : TaskResolver.GiveTask = {
+ TaskResolver.GiveTask(
+ new Task() {
+ private val localObject = obj
+ private val localAccessor = accessor
+
+ override def isComplete : Task.Resolution.Value = {
+ try {
+ localObject.GUID
+ Task.Resolution.Incomplete
+ }
+ catch {
+ case _ : Exception =>
+ Task.Resolution.Success
+ }
+ }
+
+ def Execute(resolver : ActorRef) : Unit = {
+ localAccessor ! Unregister(localObject, resolver)
+ }
+ }
+ )
+ }
+
+ /**
+ * Construct tasking that un-registers an object that is an object of type `Tool`.
+ * `Tool` objects have internal structures called "ammo slots;"
+ * each ammo slot contains a register-able `AmmoBox` object.
+ * @param obj the object being un-registered
+ * @return a `TaskResolver.GiveTask` message
+ */
+ private def UnregisterTool(obj : Tool) : TaskResolver.GiveTask = {
+ val ammoTasks : List[TaskResolver.GiveTask] = (0 until obj.MaxAmmoSlot).map(ammoIndex => UnregisterObjectTask(obj.AmmoSlots(ammoIndex).Box)).toList
+ TaskResolver.GiveTask(UnregisterObjectTask(obj).task, ammoTasks)
+ }
+
+ /**
+ * Construct tasking that un-registers an object, determining whether it is a complex object of type `Tool` or a more simple object type.
+ * @param obj the object being registered
+ * @return a `TaskResolver.GiveTask` message
+ */
+ private def UnregisterEquipment(obj : Equipment) : TaskResolver.GiveTask = {
+ obj match {
+ case tool : Tool =>
+ UnregisterTool(tool)
+ case _ =>
+ UnregisterObjectTask(obj)
+ }
+ }
+
+ /**
+ * Construct tasking that removes the `Equipment` to `target`.
+ * @param target what object that contains the `Equipment`
+ * @param obj the `Equipment`
+ * @param index the slot where the `Equipment` is stored
+ * @return a `TaskResolver.GiveTask` message
+ */
+ private def RemoveFromSlot(target : Player, obj : Equipment, index : Int) : TaskResolver.GiveTask = {
+ TaskResolver.GiveTask(
+ new Task() {
+ private val localTarget = target
+ private val localIndex = index
+ private val localObject = obj
+ private val localObjectGUID = obj.GUID
+ private val localAnnounce = self //self may not be the same when it executes
+
+ override def isComplete : Task.Resolution.Value = {
+ if(localTarget.Slot(localIndex).Equipment.contains(localObject)) {
+ Task.Resolution.Incomplete
+ }
+ else {
+ Task.Resolution.Success
+ }
+ }
+
+ def Execute(resolver : ActorRef) : Unit = {
+ localTarget.Slot(localIndex).Equipment = None
+ resolver ! scala.util.Success(localObject)
+ }
+
+ override def onSuccess() : Unit = {
+ localAnnounce ! WorldSessionActor.ResponseToSelf(PacketCoding.CreateGamePacket(0, ObjectDeleteMessage(localObjectGUID, 0)))
+ if(0 <= localIndex && localIndex < 5) {
+ avatarService ! AvatarServiceMessage(localTarget.Continent, AvatarAction.ObjectDelete(localTarget.GUID, localObjectGUID))
+ }
+ }
+ })
+ }
+
+ /**
+ * Construct tasking that un-registers all aspects of a `Player` avatar.
+ * `Players` are complex objects that contain a variety of other register-able objects and each of these objects much be handled.
+ * @param tplayer the avatar `Player`
+ * @return a `TaskResolver.GiveTask` message
+ */
+ private def UnregisterAvatar(tplayer : Player) : TaskResolver.GiveTask = {
+ val holsterTasks = recursiveHolsterTaskBuilding(tplayer.Holsters().iterator, UnregisterEquipment)
+ val inventoryTasks = tplayer.Inventory.Items.map({ case((_ : Int, entry : InventoryItem)) => UnregisterEquipment(entry.obj)})
+ val fifthHolsterTask = tplayer.Slot(5).Equipment match {
+ case Some(item) =>
+ UnregisterEquipment(item) :: Nil
+ case None =>
+ List.empty[TaskResolver.GiveTask];
+ }
+ TaskResolver.GiveTask(UnregisterObjectTask(tplayer).task, holsterTasks ++ fifthHolsterTask ++ inventoryTasks)
+ }
+
+ /**
+ * After a client has connected to the server, their account is used to generate a list of characters.
+ * On the character selection screen, each of these characters is made to exist temporarily when one is selected.
+ * This "character select screen" is an isolated portion of the client, so it does not have any external constraints.
+ * Temporary global unique identifiers are assigned to the underlying `Player` objects so that they can be turned into packets.
+ * @param tplayer the `Player` object
+ * @param gen a constant source of incremental unique numbers
+ */
+ private def SetCharacterSelectScreenGUID(tplayer : Player, gen : AtomicInteger) : Unit = {
+ tplayer.Holsters().foreach(holster => {
+ SetCharacterSelectScreenGUID_SelectEquipment(holster.Equipment, gen)
+ })
+ tplayer.Inventory.Items.foreach({ case((_, entry : InventoryItem)) =>
+ SetCharacterSelectScreenGUID_SelectEquipment(Some(entry.obj), gen)
+ })
+ tplayer.Slot(5).Equipment.get.GUID = PlanetSideGUID(gen.getAndIncrement)
+ tplayer.GUID = PlanetSideGUID(gen.getAndIncrement)
+ }
+
+ /**
+ * Assists in assigning temporary global unique identifiers.
+ * If the item is a `Tool`, handle the embedded `AmmoBox` objects in each ammunition slot.
+ * Whether or not, give the object itself a GUID as well.
+ * @param item the piece of `Equipment`
+ * @param gen a constant source of incremental unique numbers
+ */
+ private def SetCharacterSelectScreenGUID_SelectEquipment(item : Option[Equipment], gen : AtomicInteger) : Unit = {
+ item match {
+ case Some(tool : Tool) =>
+ tool.AmmoSlots.foreach(slot => { slot.Box.GUID = PlanetSideGUID(gen.getAndIncrement) })
+ tool.GUID = PlanetSideGUID(gen.getAndIncrement)
+ case Some(item : Equipment) =>
+ item.GUID = PlanetSideGUID(gen.getAndIncrement)
+ case None => ;
+ }
+ }
+
+ /**
+ * After the user has selected a character to load from the "character select screen,"
+ * the temporary global unique identifiers used for that screen are stripped from the underlying `Player` object that was selected.
+ * Characters that were not selected may be destroyed along with their temporary GUIDs.
+ * @param tplayer the `Player` object
+ */
+ private def RemoveCharacterSelectScreenGUID(tplayer : Player) : Unit = {
+ tplayer.Holsters().foreach(holster => {
+ RemoveCharacterSelectScreenGUID_SelectEquipment(holster.Equipment)
+ })
+ tplayer.Inventory.Items.foreach({ case((_, entry : InventoryItem)) =>
+ RemoveCharacterSelectScreenGUID_SelectEquipment(Some(entry.obj))
+ })
+ tplayer.Slot(5).Equipment.get.Invalidate()
+ tplayer.Invalidate()
+ }
+
+ /**
+ * Assists in stripping temporary global unique identifiers.
+ * If the item is a `Tool`, handle the embedded `AmmoBox` objects in each ammunition slot.
+ * Whether or not, remove the GUID from the object itself.
+ * @param item the piece of `Equipment`
+ */
+ private def RemoveCharacterSelectScreenGUID_SelectEquipment(item : Option[Equipment]) : Unit = {
+ item match {
+ case Some(item : Tool) =>
+ item.AmmoSlots.foreach(slot => { slot.Box.Invalidate() })
+ item.Invalidate()
+ case Some(item : Equipment) =>
+ item.Invalidate()
+ case None => ;
+ }
+ }
+
+ /**
+ * Add an object to the local `List` of objects on the ground.
+ * @param item the `Equipment` to be dropped
+ * @param pos where the `item` will be dropped
+ * @param orient in what direction the item will face when dropped
+ * @return the global unique identifier of the object
+ */
+ private def DropItemOnGround(item : Equipment, pos : Vector3, orient : Vector3) : PlanetSideGUID = {
+ item.Position = pos
+ item.Orientation = orient
+ WorldSessionActor.equipmentOnGround += item
+ item.GUID
+ }
+
+ // private def FindItemOnGround(item_guid : PlanetSideGUID) : Option[Equipment] = {
+ // equipmentOnGround.find(item => item.GUID == item_guid)
+ // }
+
+ /**
+ * Remove an object from the local `List` of objects on the ground.
+ * @param item_guid the `Equipment` to be picked up
+ * @return the object being picked up
+ */
+ private def PickupItemFromGround(item_guid : PlanetSideGUID) : Option[Equipment] = {
+ recursiveFindItemOnGround(WorldSessionActor.equipmentOnGround.iterator, item_guid) match {
+ case Some(index) =>
+ Some(WorldSessionActor.equipmentOnGround.remove(index))
+ case None =>
+ None
+ }
+ }
+
+ /**
+ * Shift through objects on the ground to find the location of a specific item.
+ * @param iter an `Iterator` of `Equipment`
+ * @param item_guid the global unique identifier of the piece of `Equipment` being sought
+ * @param index the current position in the array-list structure used to create the `Iterator`
+ * @return the index of the object matching `item_guid`, if found;
+ * `None`, otherwise
+ */
+ @tailrec private def recursiveFindItemOnGround(iter : Iterator[Equipment], item_guid : PlanetSideGUID, index : Int = 0) : Option[Int] = {
+ if(!iter.hasNext) {
+ None
+ }
+ else {
+ val item : Equipment = iter.next
+ if(item.GUID == item_guid) {
+ Some(index)
+ }
+ else {
+ recursiveFindItemOnGround(iter, item_guid, index + 1)
+ }
+ }
+ }
+
def failWithError(error : String) = {
log.error(error)
//sendResponse(PacketCoding.CreateControlPacket(ConnectionClose()))
@@ -488,3 +1534,32 @@ class WorldSessionActor extends Actor with MDCContextAware {
sendResponse(RawPacket(pkt))
}
}
+
+object WorldSessionActor {
+ final case class ResponseToSelf(pkt : GamePacket)
+
+ /**
+ * A placeholder `Cancellable` object.
+ */
+ private final val DefaultCancellable = new Cancellable() {
+ def cancel : Boolean = true
+ def isCancelled() : Boolean = true
+ }
+
+ //TODO this is a temporary local system; replace it in the future
+ //in the future, items dropped on the ground will be managed by a data structure on an external Actor representing the continent
+ //like so: WSA -> /GetItemOnGround/ -> continent -> /GiveItemFromGround/ -> WSA
+ import scala.collection.mutable.ListBuffer
+ private val equipmentOnGround : ListBuffer[Equipment] = ListBuffer[Equipment]()
+
+ def Distance(pos1 : Vector3, pos2 : Vector3) : Float = {
+ math.sqrt(DistanceSquared(pos1, pos2)).toFloat
+ }
+
+ def DistanceSquared(pos1 : Vector3, pos2 : Vector3) : Float = {
+ val dx : Float = pos1.x - pos2.x
+ val dy : Float = pos1.y - pos2.y
+ val dz : Float = pos1.z - pos2.z
+ (dx * dx) + (dy * dy) + (dz * dz)
+ }
+}