mirror of
https://github.com/2revoemag/PSF-BotServer.git
synced 2026-02-21 15:43:36 +00:00
Merge pull request #157 from Fate-JH/player-avatar-2
The Suppressor Update
This commit is contained in:
commit
3e5e8a2573
135 changed files with 13363 additions and 225 deletions
57
common/src/main/scala/net/psforever/objects/AmmoBox.scala
Normal file
57
common/src/main/scala/net/psforever/objects/AmmoBox.scala
Normal file
|
|
@ -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})"
|
||||
}
|
||||
}
|
||||
19
common/src/main/scala/net/psforever/objects/Avatars.scala
Normal file
19
common/src/main/scala/net/psforever/objects/Avatars.scala
Normal file
|
|
@ -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)
|
||||
}
|
||||
|
|
@ -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
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,67 @@
|
|||
// 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
|
||||
}
|
||||
|
||||
/**
|
||||
* Determine what `Equipment` is stowed in the given position.
|
||||
* @return the `Equipment` in this slot
|
||||
*/
|
||||
def Equipment : Option[Equipment] = tool
|
||||
|
||||
/**
|
||||
* Attempt to stow an item at the given position.
|
||||
* @param assignEquipment the change in `Equipment` for this slot
|
||||
* @return the `Equipment` in this slot
|
||||
*/
|
||||
def Equipment_=(assignEquipment : Equipment) : Option[Equipment] = {
|
||||
Equipment = Some(assignEquipment)
|
||||
}
|
||||
|
||||
/**
|
||||
* Attempt to stow an item at the given position.
|
||||
* @param assignEquipment the change in `Equipment` for this slot
|
||||
* @return the `Equipment` in this slot
|
||||
*/
|
||||
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
|
||||
}
|
||||
}
|
||||
|
|
@ -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
|
||||
}
|
||||
}
|
||||
}
|
||||
1165
common/src/main/scala/net/psforever/objects/GlobalDefinitions.scala
Normal file
1165
common/src/main/scala/net/psforever/objects/GlobalDefinitions.scala
Normal file
File diff suppressed because it is too large
Load diff
86
common/src/main/scala/net/psforever/objects/Implant.scala
Normal file
86
common/src/main/scala/net/psforever/objects/Implant.scala
Normal file
|
|
@ -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).<br>
|
||||
* <br>
|
||||
* 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)
|
||||
}
|
||||
}
|
||||
|
|
@ -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.<br>
|
||||
* <br>
|
||||
* 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.<br>
|
||||
* <br>
|
||||
* 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()
|
||||
}
|
||||
}
|
||||
|
|
@ -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.<br>
|
||||
* <br>
|
||||
* `InfantryLoadout` objects are composed of the following information, as if a blueprint:<br>
|
||||
* - the avatar's current exo-suit<br>
|
||||
* - the type of specialization, called a "subtype" (mechanized assault exo-suits only)<br>
|
||||
* - the contents of the avatar's occupied holster slots<br>
|
||||
* - the contents of the avatar's occupied inventory<br>
|
||||
* `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.<br>
|
||||
* <br>
|
||||
* 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.<br>
|
||||
* <br>
|
||||
* 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)
|
||||
}
|
||||
}
|
||||
}
|
||||
26
common/src/main/scala/net/psforever/objects/Kit.scala
Normal file
26
common/src/main/scala/net/psforever/objects/Kit.scala
Normal file
|
|
@ -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
|
||||
}
|
||||
}
|
||||
184
common/src/main/scala/net/psforever/objects/LivePlayerList.scala
Normal file
184
common/src/main/scala/net/psforever/objects/LivePlayerList.scala
Normal file
|
|
@ -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.<br>
|
||||
* <br>
|
||||
* Use:<br>
|
||||
* 1) When a users logs in during `WorldSessionActor`, associate that user's session id and the character.<br>
|
||||
* `LivePlayerList.Add(session, player)`<br>
|
||||
* 2) When that user's chosen character is declared his avatar using `SetCurrentAvatarMessage`,
|
||||
* also associate the user's session with their current GUID.<br>
|
||||
* `LivePlayerList.Assign(session, guid)`<br>
|
||||
* 3) Repeat the previous step for as many times the user's GUID changes, especially during the aforementioned condition.<br>
|
||||
* 4a) In between the previous two steps, a user's character may be referenced by their current GUID.<br>
|
||||
* `LivePlayerList.Get(guid)`<br>
|
||||
* 4b) Also in between those same previous steps, a range of characters may be queried based on provided statistics.<br>
|
||||
* `LivePlayerList.WorldPopulation(...)`<br>
|
||||
* 5) When the user leaves the game, his character's entries are removed from the mappings.<br>
|
||||
* `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.<br>
|
||||
* <br>
|
||||
* 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
|
||||
}
|
||||
|
|
@ -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
|
||||
}
|
||||
}
|
||||
|
|
@ -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
|
||||
}
|
||||
|
|
@ -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})"
|
||||
}
|
||||
}
|
||||
590
common/src/main/scala/net/psforever/objects/Player.scala
Normal file
590
common/src/main/scala/net/psforever/objects/Player.scala
Normal file
|
|
@ -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})]"
|
||||
}
|
||||
}
|
||||
22
common/src/main/scala/net/psforever/objects/SimpleItem.scala
Normal file
22
common/src/main/scala/net/psforever/objects/SimpleItem.scala
Normal file
|
|
@ -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
|
||||
}
|
||||
}
|
||||
166
common/src/main/scala/net/psforever/objects/Tool.scala
Normal file
166
common/src/main/scala/net/psforever/objects/Tool.scala
Normal file
|
|
@ -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.<br>
|
||||
* <br>
|
||||
* "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.")<br>
|
||||
* <br>
|
||||
* 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
|
||||
}
|
||||
}
|
||||
375
common/src/main/scala/net/psforever/objects/Vehicle.scala
Normal file
375
common/src/main/scala/net/psforever/objects/Vehicle.scala
Normal file
|
|
@ -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.<br>
|
||||
* <br>
|
||||
* 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.)<br>
|
||||
* <br>
|
||||
* 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)"
|
||||
}
|
||||
}
|
||||
|
|
@ -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)
|
||||
}
|
||||
}
|
||||
|
|
@ -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)
|
||||
}
|
||||
}
|
||||
|
|
@ -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
|
||||
}
|
||||
}
|
||||
|
|
@ -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)
|
||||
}
|
||||
}
|
||||
|
|
@ -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
|
||||
}
|
||||
}
|
||||
|
|
@ -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).<br>
|
||||
* <br>
|
||||
* 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)
|
||||
}
|
||||
}
|
||||
|
|
@ -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)
|
||||
}
|
||||
}
|
||||
|
|
@ -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.<br>
|
||||
* <br>
|
||||
* 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
|
||||
}
|
||||
|
|
@ -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
|
||||
}
|
||||
}
|
||||
|
|
@ -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)
|
||||
}
|
||||
}
|
||||
|
|
@ -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)
|
||||
}
|
||||
}
|
||||
|
|
@ -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)
|
||||
}
|
||||
}
|
||||
|
|
@ -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))
|
||||
}
|
||||
}
|
||||
|
|
@ -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
|
||||
}
|
||||
}
|
||||
|
|
@ -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
|
||||
)
|
||||
)
|
||||
}
|
||||
}
|
||||
|
|
@ -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))
|
||||
}
|
||||
}
|
||||
|
|
@ -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 }
|
||||
}
|
||||
}
|
||||
|
|
@ -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())
|
||||
}
|
||||
}
|
||||
|
|
@ -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())
|
||||
}
|
||||
}
|
||||
|
|
@ -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))
|
||||
}
|
||||
}
|
||||
|
|
@ -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
|
||||
}
|
||||
}
|
||||
|
|
@ -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")) }
|
||||
}
|
||||
|
|
@ -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))
|
||||
}
|
||||
}
|
||||
|
|
@ -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))
|
||||
}
|
||||
}
|
||||
|
|
@ -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)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -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
|
||||
}
|
||||
|
|
@ -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.<br>
|
||||
* <br>
|
||||
* "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
|
||||
}
|
||||
}
|
||||
|
|
@ -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)))
|
||||
}
|
||||
}
|
||||
|
|
@ -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)
|
||||
|
|
@ -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
|
||||
}
|
||||
}
|
||||
|
|
@ -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))
|
||||
}
|
||||
|
|
@ -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}"
|
||||
}
|
||||
}
|
||||
|
|
@ -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)
|
||||
}
|
||||
|
|
@ -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)
|
||||
}
|
||||
}
|
||||
|
|
@ -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
|
||||
}
|
||||
|
|
@ -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.<br>
|
||||
* <br>
|
||||
* 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
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -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()
|
||||
}
|
||||
}
|
||||
|
|
@ -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.<br>
|
||||
* <br>
|
||||
* 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
|
||||
}
|
||||
|
|
@ -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
|
||||
}
|
||||
|
|
@ -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)
|
||||
}
|
||||
|
|
@ -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
|
||||
}
|
||||
|
||||
|
|
@ -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.<br>
|
||||
* <br>
|
||||
* 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.<br>
|
||||
* <br>
|
||||
* Use:<br>
|
||||
* 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.<br>
|
||||
* <br>
|
||||
* Use:<br>
|
||||
* 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.<br>
|
||||
* <br>
|
||||
* 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.<br>
|
||||
* <br>
|
||||
* 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.<br>
|
||||
* <br>
|
||||
* `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.<br>
|
||||
* <br>
|
||||
* `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.<br>
|
||||
* <br>
|
||||
* 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.<br>
|
||||
* <br>
|
||||
* 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
|
||||
}
|
||||
}
|
||||
}
|
||||
26
common/src/main/scala/net/psforever/objects/guid/Task.scala
Normal file
26
common/src/main/scala/net/psforever/objects/guid/Task.scala
Normal file
|
|
@ -0,0 +1,26 @@
|
|||
// 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 = {
|
||||
System.nanoTime()
|
||||
//java.time.Instant.now().getEpochSecond
|
||||
}
|
||||
|
||||
object Resolution extends Enumeration {
|
||||
val Success, Incomplete, Failure = Value
|
||||
}
|
||||
}
|
||||
|
|
@ -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`.<br>
|
||||
* <br>
|
||||
* 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.<br>
|
||||
* <br>
|
||||
* 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)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -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))
|
||||
}
|
||||
}
|
||||
|
|
@ -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.<br>
|
||||
* <br>
|
||||
* 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.<br>
|
||||
* <br>
|
||||
* 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`.<br>
|
||||
* 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"))
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -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.<br>
|
||||
* <br>
|
||||
* 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
|
||||
pool.Selector = specific
|
||||
specific.SelectionIndex = pool.Numbers.indexOf(number)
|
||||
val out : Try[Int] = pool.Get()
|
||||
pool.Selector = original
|
||||
out
|
||||
}
|
||||
}
|
||||
|
|
@ -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`.<br>
|
||||
* <br>
|
||||
* 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.<br>
|
||||
* <br>
|
||||
* 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"))
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -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.<br>
|
||||
* <br>
|
||||
* 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))
|
||||
}
|
||||
}
|
||||
|
|
@ -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.<br>
|
||||
* <br>
|
||||
* 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))
|
||||
}
|
||||
}
|
||||
|
|
@ -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)
|
||||
|
|
@ -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)
|
||||
|
||||
|
|
@ -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
|
||||
}
|
||||
}
|
||||
|
||||
|
|
@ -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]
|
||||
}
|
||||
|
|
@ -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
|
||||
}
|
||||
|
|
@ -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
|
||||
}
|
||||
}
|
||||
|
|
@ -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.<br>
|
||||
* <br>
|
||||
* 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
|
||||
}
|
||||
}
|
||||
|
|
@ -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)
|
||||
}
|
||||
}
|
||||
|
|
@ -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.<br>
|
||||
* <br>
|
||||
* 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.<br>
|
||||
* <br>
|
||||
* 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
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -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
|
||||
}
|
||||
|
|
@ -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
|
||||
}
|
||||
|
|
@ -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.<br>
|
||||
* <br>
|
||||
* 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.<br>
|
||||
* <br>
|
||||
* 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.<br>
|
||||
* <br>
|
||||
* `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.<br>
|
||||
* <br>
|
||||
* 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.
|
||||
* <br>
|
||||
* `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.<br>
|
||||
* <br>
|
||||
* 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
|
||||
}
|
||||
}
|
||||
|
|
@ -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
|
||||
}
|
||||
}
|
||||
|
|
@ -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.<br>
|
||||
* <br>
|
||||
* During the selection process:<br>
|
||||
* 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.<br>
|
||||
* <br>
|
||||
* During the return process:<br>
|
||||
* 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.<br>
|
||||
* <br>
|
||||
* ...
|
||||
* @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.<br>
|
||||
* <br>
|
||||
* 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
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -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.<br>
|
||||
* <br>
|
||||
* During the selection process:<br>
|
||||
* 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.<br>
|
||||
* <br>
|
||||
* 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.<br>
|
||||
* <br>
|
||||
* During the return process:<br>
|
||||
* 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
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -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.<br>
|
||||
* <br>
|
||||
* 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
|
||||
}
|
||||
}
|
||||
|
|
@ -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.<br>
|
||||
* <br>
|
||||
* 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
|
||||
}
|
||||
}
|
||||
|
|
@ -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
|
||||
}
|
||||
}
|
||||
|
|
@ -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.<br>
|
||||
* <br>
|
||||
* 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)
|
||||
}
|
||||
}
|
||||
|
|
@ -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.<br>
|
||||
* <br>
|
||||
* 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()
|
||||
}
|
||||
}
|
||||
|
|
@ -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.<br>
|
||||
* <br>
|
||||
* 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`.<br>
|
||||
* <br>
|
||||
* 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]
|
||||
}
|
||||
|
|
@ -0,0 +1,606 @@
|
|||
// 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.<br>
|
||||
* <br>
|
||||
* 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.<br>
|
||||
* <br>
|
||||
* 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.<br>
|
||||
* <br>
|
||||
* 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.
|
||||
* @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 {
|
||||
CheckCollisionsVar(start, w, h) match {
|
||||
case Success(list) =>
|
||||
Success(list.map({ f => f.obj.GUID.guid }))
|
||||
case Failure(ex) =>
|
||||
Failure(ex)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Test whether a given piece of `Equipment` would collide with any stowed content in the inventory.<br>
|
||||
* <br>
|
||||
* 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 existing items that an item of this scale would overlap if inserted
|
||||
*/
|
||||
def CheckCollisionsVar(start : Int, w : Int, h : Int) : Try[List[InventoryItem]] = {
|
||||
if(items.size < w * h) {
|
||||
CheckCollisionsAsList(start, w, h)
|
||||
}
|
||||
else {
|
||||
CheckCollisionsAsGrid(start, w, h)
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Test whether a given piece of `Equipment` would collide with any stowed content in the inventory.<br>
|
||||
* <br>
|
||||
* 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 existing items that an item of this scale 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[InventoryItem]] = {
|
||||
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[InventoryItem] = mutable.Set[InventoryItem]()
|
||||
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
|
||||
}
|
||||
})
|
||||
Success(collisions.toList)
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Test whether a given piece of `Equipment` would collide with any stowed content in the inventory.<br>
|
||||
* <br>
|
||||
* 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 existing items that an item of this scale 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[InventoryItem]] = {
|
||||
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[InventoryItem] = mutable.Set[InventoryItem]()
|
||||
var curr = actualSlot
|
||||
for(_ <- 0 until h) {
|
||||
for(col <- 0 until w) {
|
||||
if(grid(curr + col) > -1) {
|
||||
collisions += items(grid(curr + col))
|
||||
}
|
||||
}
|
||||
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.<br>
|
||||
* <br>
|
||||
* 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.<br>
|
||||
* <br>
|
||||
* 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.<br>
|
||||
* <br>
|
||||
* 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.<br>
|
||||
* <br>
|
||||
* 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.<br>
|
||||
* <br>
|
||||
* 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)
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,60 @@
|
|||
// Copyright (c) 2017 PSForever
|
||||
package net.psforever.objects.inventory
|
||||
|
||||
import net.psforever.objects.OffhandEquipmentSlot
|
||||
import net.psforever.objects.equipment.{Equipment, EquipmentSize}
|
||||
|
||||
import scala.util.{Failure, Success}
|
||||
|
||||
/**
|
||||
* A slot-like interface for a specific grid position in an inventory.
|
||||
* The size is typically bound to anything that can be stowed which encompasses most all `Equipment`.
|
||||
* The capacity of this `EquipmentSlot` is essentially treated as 1x1.
|
||||
* Upon insertions, however, the capacity temporarily is treated as the size of the item being inserted (unless `None`).
|
||||
* This allows a proper check for insertion collision.<br>
|
||||
* <br>
|
||||
* Rather than operating on a fixed-size slot, this "slot" represents an inventory region that either includes `slot` or starts at `slot`.
|
||||
* When determining the contents of the inventory at `slot`, only that singular cell is checked.
|
||||
* When removing an item from `slot`, the item in inventory only has to be positioned in such a way that overlaps with `slot`.
|
||||
* When adding an item to `slot`, `slot` is treated as the upper left corner (the initial point) of a larger capacity region.<br>
|
||||
* <br>
|
||||
* The following diagrams demonstrate the coordinate association:<br>
|
||||
* ` - - - - - - - - - - - - - - -`<br>
|
||||
* ` - - - - - - r r x - - - - - -`<br>
|
||||
* ` - - s - - - r r x - - - i i -`<br>
|
||||
* ` - - - - - - x x x - - - i i -`<br>
|
||||
* ` - - - - - - - - - - - - - - -`<br>
|
||||
* ... where 's' is the 1x1 slot,
|
||||
* 'r' is the corner of any 2x2 item that can be removed ('x' is a potential affected edge),
|
||||
* and 'i' is the region checked for a 2x2 insertion into `slot`.
|
||||
*/
|
||||
class InventoryEquipmentSlot(private val slot : Int, private val inv : GridInventory) extends OffhandEquipmentSlot(EquipmentSize.Inventory) {
|
||||
override def Equipment_=(assignEquipment : Option[Equipment]) : Option[Equipment] = {
|
||||
assignEquipment match {
|
||||
case Some(equip) =>
|
||||
val tile = equip.Definition.Tile
|
||||
inv.CheckCollisionsVar(slot, tile.Width, tile.Height) match {
|
||||
case Success(Nil) => inv += slot -> equip
|
||||
case _ => ;
|
||||
}
|
||||
|
||||
case None =>
|
||||
inv -= slot
|
||||
}
|
||||
Equipment
|
||||
}
|
||||
|
||||
override def Equipment : Option[Equipment] = {
|
||||
inv.CheckCollisionsAsGrid(slot,1,1) match {
|
||||
case Success(list) =>
|
||||
list.headOption match {
|
||||
case Some(found) =>
|
||||
Some(found.obj)
|
||||
case None =>
|
||||
None
|
||||
}
|
||||
case Failure(_) =>
|
||||
None
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -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)
|
||||
}
|
||||
}
|
||||
|
|
@ -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)
|
||||
}
|
||||
}
|
||||
|
|
@ -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`<br>
|
||||
* `TerminalDefinition.MakeAmmoBox`<br>
|
||||
* `TerminalDefinition.MakeSimpleItem`<br>
|
||||
* `TerminalDefinition.MakeConstructionItem`<br>
|
||||
* `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)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -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)
|
||||
}
|
||||
|
|
@ -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
|
||||
}
|
||||
}
|
||||
|
|
@ -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`.<br>
|
||||
* <br>
|
||||
* 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
|
||||
}
|
||||
|
|
@ -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)
|
||||
}
|
||||
|
|
@ -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)
|
||||
}
|
||||
}
|
||||
134
common/src/main/scala/net/psforever/objects/vehicles/Seat.scala
Normal file
134
common/src/main/scala/net/psforever/objects/vehicles/Seat.scala
Normal file
|
|
@ -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}"
|
||||
}
|
||||
}
|
||||
Some files were not shown because too many files have changed in this diff Show more
Loading…
Add table
Add a link
Reference in a new issue