mirror of
https://github.com/psforever/PSF-LoginServer.git
synced 2026-01-19 18:44:45 +00:00
new stuff for player server classes; this update is not yet complete
adjusted sample Reload code and added insertion and removal functions for inventory more work on player classes; moving PacketResolution to another branch decoupling GUIDs from objects; introduced Ammo enum; minor adjustments to inventory system; different object/class hierarchy transferring basic files from another branch converted from get/set to accessor/mutator; resolved conflict from name changes refactored basic components such as GUID and location/orientation utilities kludge; more fields are given accessor and mutators; create package for vehicle-specific classes GUID assurance, now with less object creation test files; changes to how AmmoBox initializes sorry, a little bit of everything, so much I forgot to write it all down switched to a unified fire mode object internal to a Tool importing a heavily modified version of my GUID manager objects from the laternate branch; not finished or tested yet created a Trait to make Key private, sources and selectors to allow NumberPools to exist independent of a NumberSource; placed Ascending into a misc folder swapped the Return methods for selectors so that the more primitive logic is the one that needs to be overriden; renamed a selector to be more common; had to update some copyright messages fixed major logic issue with NumberPool; added comments to NumberSource files NumberSource tests simplified and made more consistent the method naming convention of NumberSources comments for NumberSelectors starting on NumberSelector tests modifications that should better support number pools; added a pool hub that acts on a predefined source adjustment to how Tools and FireModeDefintion keep track of ammunition and ammunition slots; I don't think this is sufficient small additions to Tools; filled out simple tests for other three Selectors added object lookup notation for the pool hub added more NumberSelector tests; removed the word 'Number' from subclass names re-named classes, re-packaged classes, re-named packages; created new Selector, split pools to create a fallback for the NumberPoolHub changes to NumberPool classes; tests on NumberPool classes changes to NumberPool classes; tests on NumberPool classes2 some robust testing for NumberPoolHub, plus necessary modifications to other files register and unregister functions now use Success and Failure conditions, save for one true thrown Exception reduced the flow of certain algorithm chains, mainly by adding match statements and removing Exceptions error message text the same thing as the last commit, but with NumberPools rather than NumberPoolHub various types of freeform registration added sorting functions to Selectors to allow for hotswapping for number pools, especially to and from SpecificSelector; tests for NumberPoolHub get numbers from an Array of indices, not the list of Numbers, in SimplePool added a class to represent the four types of recovery kits comments on Kit files created package for supporting equipment classes; renamed /definition/ package adding class for items that construct deployables, the router telepad included added SimpleItem, classes for game Equipment that do not have internal state; re-organized ObjectDefinition files and the game objects they create to more naturally move around EquipmentSize and InventoryTile (size) added SimpleItem tests (what they are...); removed example code that has hogging an import from AmmoBox auto-sort for loading and fitting former inventory content back into the inventory method of finding first available position to fit an certain size block in the inventory changed CheckCollision return type to provide Try[List[Int]; fixed all existing references and tests wrote comments for GridInventory methods; changed insertion param to be of form 'key -> element' adding features to Player; created definitions for Player class; re-grouped ConstructionItem enumerations initial work on implants; shuffled classes to better accommodate the new implant system, I think wrote some tests for Implants; fixing Implant logic wrote tests for Player class and made adjustments where necessary basic initialization during Player creation based on exo-suit type three wrapper Actors for the normal classes comments on code modified tests to improve accountability; added Resolver class to deal with multiple tasks that contribute to a larger task changed Tools to an internal AmmoBox; don't have to def -= symbol if I def _= symbol LivePlayerList -> MasterPlayerList, and added a Fit def for Player that checks holsters as well as inventory example of packet conversion can be found with AmmoBoxDefinition added conversion for ToolDefinition added all Equipment packet conversion functionality; started working on Avatar-related conversions continued effort towards a working Player packet conversion test subclasses of Equipment apparently do not need to overide the type of the PacketConverter for generics the logical conclusion: it doesn't matter what generics Packet returns so long as it returns an ObjectCreateConverter[] type separated converters from definitions into files changed some configuration information to final; added a bunch of converters, not fully tested though changed function names in converters replaced WSA packet-driven OCDM with Player object OCDM; upgrade to Float angular data added partial support for LockerContainer; changed Equipment defaults to a common value changes to AvatarConverter to include 5th slot; changes to VehicleConverter to make work; implementation of Fury in Vehicle->packet example in WSA added a seat definition and renovated how the weapon controlled from a seat can be found comments to files mainly; non-essential functionality to some classes, mostly access determination moved converter tests to their own test file write more of this test added ServiceManager, as it is useful pool range changes added AvatarService, as it is useful straightened out the GUID actors; added the static method for adding AmmoBoxes (to be converted later) chnages to task resolution operation complicated Task and TaskResolver logic is now possible; for example, you can schedule giving an AmmoBox a GUID, before giving a Tool a GUID, before placing the Tool in a player's hand; see Suppressor example in WSA separated the Task trait and the TaskResolver actor into their own classes, moving the former RegistrationTaskResolver class into the /misc/ folder; deleted old backup copy of HubActor; modifications for PoC and supported tests added better support and protection against putting things in the wrong hand when using inventories and the Player.Slot(n) function GlobalDefinitions file; added laze pointer as an SItem, and gave it the command detonater management code; additionally fixed spelling of 'detonat[o]r' in Codec; early Terminal class work updated tests to GlobalDefinitions entries; Terminal works but I don't like it played with GUID pooling workflow, though to little avail; modifications to Terminal purchasing workflow, but still very volatile modified NumberPoolActor and NumberPoolAccessor to make them more straightforward and not use akka ask as a go-between fixed recovery options so that they do not cause more messages trailing newline InventoryItem (packet data) renamed InventoryItemData to remove ambiguity; Terminal functionality improved, allowing for swapping of exo-suits and the restoration of equipment positions remove yet-unsupported Terminal messaging made Terminal message more specific; can now put equipment into empty slot on exo-suit change; should report changes better re-organized function calls to preserved items removed from holster slots on exo-suit change moved predicate to the end of the list of params for recoverInventory so that repetition can be eliminated and a default value can be assigned issues with making Tool; committing changes before revert of NumberPoolActor and NumberPoolAccessorActor to see if those broke it a necessary evil, the reverting of these two Actors; subtask resolution does not work unless I do so, for now restored the registration portion of tasking back to where it previously was (and better?) NumberPoolActor and the ...AccessorActor are back to a comfortable place (and better?) re-draw object in hand when switching exo-suits; build AmmoBoxes for Tool during Terminal-controlled creation, not Tool-controlled creation order of task cleanup reversed to avoid index mismatch; added itsm to TerminalDefinition common 5x5 AmmoBox size; added vehicle weapon ammo boxes to terminal added error catching messages; stopped odd double-registering issue early resolved issue where multiple subtasks started their main task multiple times; added checks that an object does not register a new GUID when it already has one wrote unregistration code for Selling items back through the Terminal, repairing logic along the way; also, wrote a top-level GUID find for the Player for use of MoveItem added framework for starting on Loadouts; managed issue with parent tasks starting before being summoned by child subtasks, often resulting in the complete skip of the execution phase of the parent; refactored registration tasks in WSA modified Tool structure, exposing the AmmoSlot list a bit more stuff stuff Tool ammo slot changes to default and comments basic loadout framework for Infantry; need to integrate initial work on FavoritesRequest packet tests for FavoritesRequest packet increased size of number pool for testing; wrote an algorithm that translates to and from the simplified version of objects stored in loadouts refactored the tasking for adding Equipment and removing Equipment updated the inventory so the Map of items does not have to rely on the GUID of an item being set before the item is inserted untested routine for registering a player character; pushing all changes before making significant changes to the client init code structure added to comments of BeginZoningMessage; transitioned player through and initial step of a more proper login GUID association the current avatar is properly registered and there is something of a workflow with the messages and packets corrected another bit of logic where inventories used to be indexed by object GUID in AvatarConverter; reversed unregister-remove task sequence such that GUID-less object is not allowed to exist in a stable object hierarchy working Loadout loading added identification functions to GlobalDefinitions; echo ObjectDelete back to client accidentally got rid of something in WSA, but now restored; adding extra details to Terminal operations separated Terminal into separate files and moved files into their own package under \objects\ for now; can delete loadouts now in WSA better handling of ReloadMessage and MoveItemMessage framework for better support involving dropping and picking up items code comments and small modifications, such as the location and structure of the Terminal Equipment definitions wrote comments in GlobalDefinitions; modified code so that a primitive form of player synchronization now occurs for future testing added code to display already-dropped Equipment on the ground; limitations are explained; moved TaskResolver to more a global location, though I don't know if that helps modified avatar unregister logic to ensure vacating player is deleted from other clients 'properly' more comments; improved checks for MoveItemMessage; squared distances as necessary subtle changes to login scripting so that test character is always offered re-organizing the functions in WSA so that only the local objects separate the two message processing blocks
This commit is contained in:
parent
9a8e1e8f95
commit
4bcef8ce98
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,53 @@
|
|||
// Copyright (c) 2017 PSForever
|
||||
package net.psforever.objects
|
||||
|
||||
import net.psforever.objects.equipment.{Equipment, EquipmentSize}
|
||||
|
||||
/**
|
||||
* A size-checked unit of storage (or mounting) for `Equipment`.
|
||||
* Unlike inventory space, anything placed in "slot" space is expected to be visible to the game world in some fashion.
|
||||
*/
|
||||
class EquipmentSlot {
|
||||
private var size : EquipmentSize.Value = EquipmentSize.Blocked
|
||||
private var tool : Option[Equipment] = None
|
||||
//TODO eventually move this object from storing the item directly to just storing its GUID?
|
||||
|
||||
def Size : EquipmentSize.Value = size
|
||||
|
||||
def Size_=(assignSize : EquipmentSize.Value) : EquipmentSize.Value = {
|
||||
if(tool.isEmpty) {
|
||||
size = assignSize
|
||||
}
|
||||
Size
|
||||
}
|
||||
|
||||
def Equipment : Option[Equipment] = tool
|
||||
|
||||
def Equipment_=(assignEquipment : Equipment) : Option[Equipment] = {
|
||||
Equipment = Some(assignEquipment)
|
||||
}
|
||||
|
||||
def Equipment_=(assignEquipment : Option[Equipment]) : Option[Equipment] = {
|
||||
if(assignEquipment.isDefined) { //if new equipment is defined, don't put it in the slot if the slot is being used
|
||||
if(tool.isEmpty && EquipmentSize.isEqual(size, assignEquipment.get.Size)) {
|
||||
tool = assignEquipment
|
||||
}
|
||||
}
|
||||
else {
|
||||
tool = None
|
||||
}
|
||||
Equipment
|
||||
}
|
||||
}
|
||||
|
||||
object EquipmentSlot {
|
||||
def apply() : EquipmentSlot = {
|
||||
new EquipmentSlot()
|
||||
}
|
||||
|
||||
def apply(size : EquipmentSize.Value) : EquipmentSlot = {
|
||||
val slot = new EquipmentSlot()
|
||||
slot.Size = size
|
||||
slot
|
||||
}
|
||||
}
|
||||
|
|
@ -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
|
||||
}
|
||||
}
|
||||
}
|
||||
1163
common/src/main/scala/net/psforever/objects/GlobalDefinitions.scala
Normal file
1163
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
|
||||
}
|
||||
}
|
||||
}
|
||||
23
common/src/main/scala/net/psforever/objects/guid/Task.scala
Normal file
23
common/src/main/scala/net/psforever/objects/guid/Task.scala
Normal file
|
|
@ -0,0 +1,23 @@
|
|||
// Copyright (c) 2017 PSForever
|
||||
package net.psforever.objects.guid
|
||||
|
||||
import akka.actor.ActorRef
|
||||
|
||||
trait Task {
|
||||
def Execute(resolver : ActorRef) : Unit
|
||||
def isComplete : Task.Resolution.Value = Task.Resolution.Incomplete
|
||||
def Timeout : Long = 200L //milliseconds
|
||||
def onSuccess() : Unit = { }
|
||||
def onFailure(ex : Throwable) : Unit = { }
|
||||
def onTimeout(ex : Throwable) : Unit = onFailure(ex)
|
||||
def onAbort(ex : Throwable) : Unit = { }
|
||||
def Cleanup() : Unit = { }
|
||||
}
|
||||
|
||||
object Task {
|
||||
def TimeNow : Long = { java.time.Instant.now().getEpochSecond }
|
||||
|
||||
object Resolution extends Enumeration {
|
||||
val Success, Incomplete, Failure = Value
|
||||
}
|
||||
}
|
||||
|
|
@ -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
|
||||
specific.SelectionIndex = pool.Numbers.indexOf(number)
|
||||
pool.Selector = specific
|
||||
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,586 @@
|
|||
// Copyright (c) 2017 PSForever
|
||||
package net.psforever.objects.inventory
|
||||
|
||||
import java.util.concurrent.atomic.AtomicInteger
|
||||
|
||||
import net.psforever.objects.equipment.Equipment
|
||||
import net.psforever.objects.EquipmentSlot
|
||||
import net.psforever.packet.game.PlanetSideGUID
|
||||
|
||||
import scala.annotation.tailrec
|
||||
import scala.collection.immutable.Map
|
||||
import scala.collection.mutable
|
||||
import scala.util.{Failure, Success, Try}
|
||||
|
||||
/**
|
||||
* An inventory are used to stow `Equipment` when it does not exist visually in the game world.<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.<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 GUID values for all existing contents that this item would overlap if inserted
|
||||
*/
|
||||
def CheckCollisions(start : Int, w : Int, h : Int) : Try[List[Int]] = {
|
||||
if(items.isEmpty) {
|
||||
Success(List.empty[Int])
|
||||
}
|
||||
else {
|
||||
def check : (Int,Int,Int) => Try[List[Int]] = if(items.size < w * h) { CheckCollisionsAsList } else { CheckCollisionsAsGrid }
|
||||
check(start, w, h)
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Test whether a given piece of `Equipment` would collide with any stowed content in the inventory.<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 GUID values for all existing contents that this item would overlap if inserted
|
||||
* @throws IndexOutOfBoundsException if the region extends outside of the grid boundaries
|
||||
*/
|
||||
def CheckCollisionsAsList(start : Int, w : Int, h : Int) : Try[List[Int]] = {
|
||||
val actualSlot = start - offset
|
||||
val startx : Int = actualSlot % width
|
||||
val starty : Int = actualSlot / width
|
||||
val startw : Int = startx + w - 1
|
||||
val starth : Int = starty + h - 1
|
||||
if(actualSlot < 0 || actualSlot >= grid.length || startw >= width || starth >= height) {
|
||||
val bounds : String = if(startx < 0) { "left" } else if(startw >= width) { "right" } else { "bottom" }
|
||||
Failure(new IndexOutOfBoundsException(s"requested region escapes the $bounds edge of the grid inventory - $startx, $starty; $w x $h"))
|
||||
}
|
||||
else {
|
||||
val collisions : mutable.Set[Int] = mutable.Set[Int]()
|
||||
items.values.foreach({ item : InventoryItem =>
|
||||
val actualItemStart : Int = item.start - offset
|
||||
val itemx : Int = actualItemStart % width
|
||||
val itemy : Int = actualItemStart / width
|
||||
val tile = item.obj.Tile
|
||||
val clipsOnX : Boolean = if(itemx < startx) { itemx + tile.width > startx } else { itemx <= startw }
|
||||
val clipsOnY : Boolean = if(itemy < starty) { itemy + tile.height > starty } else { itemy <= starth }
|
||||
if(clipsOnX && clipsOnY) {
|
||||
collisions += item.GUID.guid
|
||||
}
|
||||
})
|
||||
Success(collisions.toList)
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Test whether a given piece of `Equipment` would collide with any stowed content in the inventory.<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 GUID values for all existing contents that this item would overlap if inserted
|
||||
* @throws IndexOutOfBoundsException if the region extends outside of the grid boundaries
|
||||
*/
|
||||
def CheckCollisionsAsGrid(start : Int, w : Int, h : Int) : Try[List[Int]] = {
|
||||
val actualSlot = start - offset
|
||||
if(actualSlot < 0 || actualSlot >= grid.length || (actualSlot % width) + w > width || (actualSlot / width) + h > height) {
|
||||
val startx : Int = actualSlot % width
|
||||
val starty : Int = actualSlot / width
|
||||
val startw : Int = startx + w - 1
|
||||
val bounds : String = if(startx < 0) { "left" } else if(startw >= width) { "right" } else { "bottom" }
|
||||
Failure(new IndexOutOfBoundsException(s"requested region escapes the $bounds edge of the grid inventory - $startx, $starty; $w x $h"))
|
||||
}
|
||||
else {
|
||||
val collisions : mutable.Set[Int] = mutable.Set[Int]()
|
||||
var curr = actualSlot
|
||||
for(_ <- 0 until h) {
|
||||
for(col <- 0 until w) {
|
||||
if(grid(curr + col) > -1) {
|
||||
collisions += items(grid(curr + col)).GUID.guid
|
||||
}
|
||||
}
|
||||
curr += width
|
||||
}
|
||||
Success(collisions.toList)
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Find a blank space in the current inventory where a `tile` of given dimensions can be cleanly inserted.
|
||||
* Brute-force method.
|
||||
* @param tile the dimensions of the blank space
|
||||
* @return the grid index of the upper left corner where equipment to which the `tile` belongs should be placed
|
||||
*/
|
||||
def Fit(tile : InventoryTile) : Option[Int] = {
|
||||
val tWidth = tile.width
|
||||
val tHeight = tile.height
|
||||
val gridIter = (0 until (grid.length - (tHeight - 1) * width))
|
||||
.filter(cell => grid(cell) == -1 && (width - cell%width >= tWidth))
|
||||
.iterator
|
||||
recursiveFitTest(gridIter, tWidth, tHeight)
|
||||
}
|
||||
|
||||
/**
|
||||
* Find a blank space in the current inventory where a `tile` of given dimensions can be cleanly inserted.
|
||||
* @param cells an iterator of all accepted indices in the `grid`
|
||||
* @param tWidth the width of the blank space
|
||||
* @param tHeight the height of the blank space
|
||||
* @return the grid index of the upper left corner where equipment to which the `tile` belongs should be placed
|
||||
*/
|
||||
@tailrec private def recursiveFitTest(cells : Iterator[Int], tWidth : Int, tHeight : Int) : Option[Int] = {
|
||||
if(!cells.hasNext) {
|
||||
None
|
||||
}
|
||||
else {
|
||||
val index = cells.next + offset
|
||||
CheckCollisionsAsGrid(index, tWidth, tHeight) match {
|
||||
case Success(Nil) =>
|
||||
Some(index)
|
||||
case Success(_) =>
|
||||
recursiveFitTest(cells, tWidth, tHeight)
|
||||
case Failure(ex) =>
|
||||
throw ex
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Define a region of inventory grid cells and set them to a given value.
|
||||
* @param start the initial inventory index
|
||||
* @param w the width of the region
|
||||
* @param h the height of the region
|
||||
* @param value the value to set all the cells in the defined region;
|
||||
* defaults to -1 (which is "nothing")
|
||||
*/
|
||||
def SetCells(start : Int, w : Int, h : Int, value : Int = -1) : Unit = {
|
||||
SetCellsOffset(math.max(start - offset, 0), w, h, value)
|
||||
}
|
||||
|
||||
/**
|
||||
* Define a region of inventory grid cells and set them to a given value.
|
||||
* @param start the initial inventory index, without the inventory offset (required)
|
||||
* @param w the width of the region
|
||||
* @param h the height of the region
|
||||
* @param value the value to set all the cells in the defined region;
|
||||
* defaults to -1 (which is "nothing")
|
||||
* @throws IndexOutOfBoundsException if the region extends outside of the grid boundaries
|
||||
*/
|
||||
def SetCellsOffset(start : Int, w : Int, h : Int, value : Int = -1) : Unit = {
|
||||
if(start < 0 || start > grid.length || (start % width) + w - 1 > width || (start / width) + h- 1 > height) {
|
||||
val startx : Int = start % width
|
||||
val starty : Int = start / width
|
||||
val startw : Int = startx + w - 1
|
||||
val bounds : String = if(startx < 0) { "left" } else if(startw >= width) { "right" } else { "bottom" }
|
||||
throw new IndexOutOfBoundsException(s"requested region escapes the $bounds of the grid inventory - $startx, $starty; $w x $h")
|
||||
}
|
||||
else {
|
||||
var curr = start
|
||||
for(_ <- 0 until h) {
|
||||
for(col <- 0 until w) {
|
||||
grid(curr + col) = value
|
||||
}
|
||||
curr += width
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
def Insert(start : Int, obj : Equipment) : Boolean = {
|
||||
val key : Int = entryIndex.getAndIncrement()
|
||||
items.get(key) match {
|
||||
case None => //no redundant insertions or other collisions
|
||||
Insertion_CheckCollisions(start, obj, key)
|
||||
case _ =>
|
||||
false
|
||||
}
|
||||
}
|
||||
|
||||
def Insertion_CheckCollisions(start : Int, obj : Equipment, key : Int) : Boolean = {
|
||||
CheckCollisions(start, obj) match {
|
||||
case Success(Nil) =>
|
||||
val card = InventoryItem(obj, start)
|
||||
items += key -> card
|
||||
val tile = obj.Tile
|
||||
SetCells(start, tile.width, tile.height, key)
|
||||
true
|
||||
case _ =>
|
||||
false
|
||||
}
|
||||
}
|
||||
|
||||
def +=(kv : (Int, Equipment)) : Boolean = Insert(kv._1, kv._2)
|
||||
|
||||
// def InsertQuickly(start : Int, obj : Equipment) : Boolean = {
|
||||
// val guid : Int = obj.GUID.guid
|
||||
// val card = InventoryItemData(obj, start)
|
||||
// items += guid -> card
|
||||
// val tile = obj.Tile
|
||||
// SetCellsOffset(start, tile.width, tile.height, guid)
|
||||
// true
|
||||
// }
|
||||
|
||||
def Remove(index : Int) : Boolean = {
|
||||
val key = grid(index - Offset)
|
||||
items.remove(key) match {
|
||||
case Some(item) =>
|
||||
val tile = item.obj.Tile
|
||||
SetCells(item.start, tile.width, tile.height)
|
||||
true
|
||||
case None =>
|
||||
false
|
||||
}
|
||||
}
|
||||
|
||||
def -=(index : Int) : Boolean = Remove(index)
|
||||
|
||||
def Remove(guid : PlanetSideGUID) : Boolean = {
|
||||
recursiveFindIdentifiedObject(items.keys.iterator, guid) match {
|
||||
case Some(index) =>
|
||||
val item = items.remove(index).get
|
||||
val tile = item.obj.Tile
|
||||
SetCells(item.start, tile.width, tile.height)
|
||||
true
|
||||
case None =>
|
||||
false
|
||||
}
|
||||
}
|
||||
|
||||
def -=(guid : PlanetSideGUID) : Boolean = Remove(guid)
|
||||
|
||||
@tailrec private def recursiveFindIdentifiedObject(iter : Iterator[Int], guid : PlanetSideGUID) : Option[Int] = {
|
||||
if(!iter.hasNext) {
|
||||
None
|
||||
}
|
||||
else {
|
||||
val index = iter.next
|
||||
if(items(index).obj.GUID == guid) {
|
||||
Some(index)
|
||||
}
|
||||
else {
|
||||
recursiveFindIdentifiedObject(iter, guid)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Does this inventory contain an object with the given GUID?
|
||||
* @param guid the GUID
|
||||
* @return the discovered object, or `None`
|
||||
*/
|
||||
def hasItem(guid : PlanetSideGUID) : Option[Equipment] = {
|
||||
recursiveFindIdentifiedObject(items.keys.iterator, guid) match {
|
||||
case Some(index) =>
|
||||
Some(items(index).obj)
|
||||
case None =>
|
||||
None
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Clear the inventory by removing all of its items.
|
||||
* @return a `List` of the previous items in the inventory as their `InventoryItemData` tiles
|
||||
*/
|
||||
def Clear() : List[InventoryItem] = {
|
||||
val list = items.values.toList
|
||||
items.clear
|
||||
SetCellsOffset(0, width, height)
|
||||
list
|
||||
}
|
||||
|
||||
/**
|
||||
* Change the size of the inventory, without regard for its current contents.
|
||||
* This method replaces mutators for `Width` and `Height`.
|
||||
* @param w the new width
|
||||
* @param h the new height
|
||||
* @throws IllegalArgumentException if the new size to be set is zero or less
|
||||
*/
|
||||
def Resize(w : Int, h : Int) : Unit = {
|
||||
if(w < 1 || h < 1) {
|
||||
throw new IllegalArgumentException("area of inventory space must not be < 1")
|
||||
}
|
||||
width = w
|
||||
height = h
|
||||
grid = Array.fill[Int](w * h)(-1)
|
||||
}
|
||||
}
|
||||
|
||||
object GridInventory {
|
||||
/**
|
||||
* Overloaded constructor.
|
||||
* @return a `GridInventory` object
|
||||
*/
|
||||
def apply() : GridInventory = {
|
||||
new GridInventory()
|
||||
}
|
||||
|
||||
/**
|
||||
* Overloaded constructor for initializing an inventory of specific dimensions.
|
||||
* @param width the horizontal size of the inventory
|
||||
* @param height the vertical size of the inventory
|
||||
* @return a `GridInventory` object
|
||||
*/
|
||||
def apply(width : Int, height : Int) : GridInventory = {
|
||||
val obj = new GridInventory()
|
||||
obj.Resize(width, height)
|
||||
obj
|
||||
}
|
||||
|
||||
/**
|
||||
* Overloaded constructor for initializing an inventory of specific dimensions and index offset.
|
||||
* @param width the horizontal size of the inventory
|
||||
* @param height the vertical size of the inventory
|
||||
* @param offset the effective index of the first cell in the inventory
|
||||
* @return a `GridInventory` object
|
||||
*/
|
||||
def apply(width : Int, height : Int, offset : Int) : GridInventory = {
|
||||
val obj = new GridInventory()
|
||||
obj.Resize(width, height)
|
||||
obj.Offset = offset
|
||||
obj
|
||||
}
|
||||
|
||||
/**
|
||||
* Accepting items that may or may not have previously been in an inventory,
|
||||
* determine if there is a tight-fit arrangement for the items in the given inventory.
|
||||
* Note that arrangement for future insertion.
|
||||
* @param list a `List` of items to be potentially re-inserted
|
||||
* @param predicate a condition to sort the previous `List` of elements
|
||||
* @param inv the inventory in which they would be re-inserted in the future
|
||||
* @return two `List`s of `Equipment`;
|
||||
* the first `List` is composed of `InventoryItemData`s that will be reinserted at the new `start` index;
|
||||
* the second list is composed of `Equipment` that will not be put back into the inventory
|
||||
*/
|
||||
def recoverInventory(list : List[InventoryItem], inv : GridInventory, predicate : (InventoryItem, InventoryItem) => Boolean = StandardScaleSort) : (List[InventoryItem], List[Equipment]) = {
|
||||
sortKnapsack(
|
||||
list.sortWith(predicate),
|
||||
inv.width,
|
||||
inv.height
|
||||
)
|
||||
val (elements, out) = list.partition(p => p.start > -1)
|
||||
elements.foreach(item => item.start += inv.Offset)
|
||||
(elements, out.map(item => item.obj))
|
||||
}
|
||||
|
||||
/**
|
||||
* The default predicate used by the knapsack sort algorithm.
|
||||
*/
|
||||
final val StandardScaleSort : (InventoryItem, InventoryItem) => Boolean =
|
||||
(a, b) => {
|
||||
val aTile = a.obj.Tile
|
||||
val bTile = b.obj.Tile
|
||||
if(aTile.width == bTile.width) {
|
||||
aTile.height > bTile.height
|
||||
}
|
||||
else {
|
||||
aTile.width > bTile.width
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Start calculating the "optimal" fit for a `List` of items in an inventory of given size.<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,43 @@
|
|||
// Copyright (c) 2017 PSForever
|
||||
package net.psforever.objects.inventory
|
||||
|
||||
import net.psforever.objects.OffhandEquipmentSlot
|
||||
import net.psforever.objects.equipment.{Equipment, EquipmentSize}
|
||||
|
||||
/**
|
||||
* A slot-like interface for a specific grid position in an inventory.
|
||||
* The size is bound to anything that can be stowed, which encompasses most all `Equipment`.
|
||||
* Furthermore, rather than operating on a fixed-size slot, this "slot" represents an inventory region that either includes `slot` or starts at `slot`.
|
||||
* An object added to the underlying inventory from here can only be added with its initial point at `slot`.
|
||||
* An object found at `slot`, however, can be removed even if the starting cell is prior to `slot.`
|
||||
*/
|
||||
class InventoryEquipmentSlot(private val slot : Int, private val inv : GridInventory) extends OffhandEquipmentSlot(EquipmentSize.Inventory) {
|
||||
/**
|
||||
* Attempt to stow an item into the inventory at the given position.
|
||||
* @param assignEquipment the change in `Equipment` for this slot
|
||||
* @return the `Equipment` in this slot
|
||||
*/
|
||||
override def Equipment_=(assignEquipment : Option[Equipment]) : Option[Equipment] = {
|
||||
assignEquipment match {
|
||||
case Some(equip) =>
|
||||
inv += slot -> equip
|
||||
case None =>
|
||||
inv -= slot
|
||||
}
|
||||
Equipment
|
||||
}
|
||||
|
||||
/**
|
||||
* Determine what `Equipment`, if any, is stowed in the inventory in the given position.
|
||||
* @return the `Equipment` in this slot
|
||||
*/
|
||||
override def Equipment : Option[Equipment] = {
|
||||
inv.Items.find({ case ((_, item : InventoryItem)) => item.start == slot }) match {
|
||||
case Some((_, item : InventoryItem)) =>
|
||||
Some(item.obj)
|
||||
case None =>
|
||||
None
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
|
@ -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…
Reference in a new issue