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:
FateJH 2017-05-30 18:46:01 -04:00
parent 9a8e1e8f95
commit 4bcef8ce98
134 changed files with 13169 additions and 225 deletions

View 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})"
}
}

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

View file

@ -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
}
}

View file

@ -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
}
}

View file

@ -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
}
}
}

File diff suppressed because it is too large Load diff

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

View file

@ -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()
}
}

View file

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

View 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
}
}

View 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>
* &nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;`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>
* &nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;`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>
* &nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;`LivePlayerList.Get(guid)`<br>
* 4b) Also in between those same previous steps, a range of characters may be queried based on provided statistics.<br>
* &nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;`LivePlayerList.WorldPopulation(...)`<br>
* 5) When the user leaves the game, his character's entries are removed from the mappings.<br>
* &nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;`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
}

View file

@ -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
}
}

View file

@ -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
}

View file

@ -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})"
}
}

View 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})]"
}
}

View 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
}
}

View 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
}
}

View 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)"
}
}

View file

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

View file

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

View file

@ -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
}
}

View file

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

View file

@ -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
}
}

View file

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

View file

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

View file

@ -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
}

View file

@ -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
}
}

View file

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

View file

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

View file

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

View file

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

View file

@ -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
}
}

View file

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

View file

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

View file

@ -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 }
}
}

View file

@ -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())
}
}

View file

@ -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())
}
}

View file

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

View file

@ -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
}
}

View file

@ -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")) }
}

View file

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

View file

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

View file

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

View file

@ -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
}

View file

@ -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
}
}

View file

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

View file

@ -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)

View file

@ -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
}
}

View file

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

View file

@ -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}"
}
}

View file

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

View file

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

View file

@ -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
}

View file

@ -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
}
}
}

View file

@ -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()
}
}

View file

@ -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
}

View file

@ -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
}

View file

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

View file

@ -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
}

View file

@ -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
}
}
}

View 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
}
}

View file

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

View file

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

View file

@ -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"))
}
}
}

View file

@ -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
}
}

View file

@ -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"))
}
}
}

View file

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

View file

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

View file

@ -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)

View file

@ -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)

View file

@ -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
}
}

View file

@ -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]
}

View file

@ -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
}

View file

@ -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
}
}

View file

@ -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
}
}

View file

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

View file

@ -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
}
}
}

View file

@ -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
}

View file

@ -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
}

View file

@ -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
}
}

View file

@ -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
}
}

View file

@ -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
}
}
}

View file

@ -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
}
}
}

View file

@ -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
}
}

View file

@ -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
}
}

View file

@ -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
}
}

View file

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

View file

@ -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()
}
}

View file

@ -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]
}

View file

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

View file

@ -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
}
}
}

View file

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

View file

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

View file

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

View file

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

View file

@ -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
}
}

View file

@ -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
}

View file

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

View file

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

View 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