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