From 4bcef8ce9888325032bbe4dd9ce77c6453d5d147 Mon Sep 17 00:00:00 2001 From: FateJH Date: Tue, 30 May 2017 18:46:01 -0400 Subject: [PATCH] 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 --- .../scala/net/psforever/objects/AmmoBox.scala | 57 + .../scala/net/psforever/objects/Avatars.scala | 19 + .../psforever/objects/ConstructionItem.scala | 38 + .../net/psforever/objects/EquipmentSlot.scala | 53 + .../psforever/objects/ExoSuitDefinition.scala | 128 ++ .../psforever/objects/GlobalDefinitions.scala | 1163 ++++++++++++++ .../scala/net/psforever/objects/Implant.scala | 86 + .../net/psforever/objects/ImplantSlot.scala | 62 + .../psforever/objects/InfantryLoadout.scala | 265 ++++ .../scala/net/psforever/objects/Kit.scala | 26 + .../psforever/objects/LivePlayerList.scala | 184 +++ .../psforever/objects/LockerContainer.scala | 34 + .../objects/OffhandEquipmentSlot.scala | 20 + .../objects/PlanetSideGameObject.scala | 47 + .../scala/net/psforever/objects/Player.scala | 590 +++++++ .../net/psforever/objects/SimpleItem.scala | 22 + .../scala/net/psforever/objects/Tool.scala | 166 ++ .../scala/net/psforever/objects/Vehicle.scala | 375 +++++ .../definition/AmmoBoxDefinition.scala | 33 + .../objects/definition/AvatarDefinition.scala | 24 + .../objects/definition/BasicDefinition.scala | 13 + .../ConstructionItemDefinition.scala | 23 + .../definition/EquipmentDefinition.scala | 39 + .../definition/ImplantDefinition.scala | 93 ++ .../objects/definition/KitDefinition.scala | 29 + .../objects/definition/ObjectDefinition.scala | 42 + .../objects/definition/SeatDefinition.scala | 44 + .../definition/SimpleItemDefinition.scala | 21 + .../objects/definition/ToolDefinition.scala | 24 + .../definition/VehicleDefinition.scala | 77 + .../definition/converter/ACEConverter.scala | 17 + .../definition/converter/AMSConverter.scala | 44 + .../definition/converter/ANTConverter.scala | 29 + .../converter/AmmoBoxConverter.scala | 17 + .../converter/AvatarConverter.scala | 171 ++ .../converter/BoomerTriggerConverter.scala | 17 + .../converter/CommandDetonaterConverter.scala | 17 + .../definition/converter/KitConverter.scala | 17 + .../converter/LockerContainerConverter.scala | 35 + .../converter/PacketConverter.scala | 71 + .../definition/converter/REKConverter.scala | 17 + .../definition/converter/ToolConverter.scala | 30 + .../converter/VehicleConverter.scala | 62 + .../objects/entity/Identifiable.scala | 13 + .../objects/entity/IdentifiableEntity.scala | 96 ++ .../objects/entity/MobileWorldEntity.scala | 45 + .../objects/entity/NoGUIDException.scala | 6 + .../objects/entity/SimpleWorldEntity.scala | 52 + .../psforever/objects/entity/TimeEntry.scala | 13 + .../objects/entity/WorldEntity.scala | 26 + .../psforever/objects/equipment/Ammo.scala | 93 ++ .../psforever/objects/equipment/CItem.scala | 28 + .../objects/equipment/Equipment.scala | 21 + .../objects/equipment/EquipmentSize.scala | 38 + .../equipment/FireModeDefinition.scala | 69 + .../objects/equipment/FireModeSwitch.scala | 21 + .../psforever/objects/equipment/Kits.scala | 12 + .../psforever/objects/equipment/SItem.scala | 9 + .../objects/guid/AvailabilityPolicy.scala | 21 + .../objects/guid/NumberPoolHub.scala | 474 ++++++ .../net/psforever/objects/guid/Task.scala | 23 + .../psforever/objects/guid/TaskResolver.scala | 393 +++++ .../objects/guid/actor/IsRegistered.scala | 31 + .../guid/actor/NumberPoolAccessorActor.scala | 216 +++ .../objects/guid/actor/NumberPoolActor.scala | 96 ++ .../guid/actor/NumberPoolHubActor.scala | 212 +++ .../objects/guid/actor/Register.scala | 80 + .../objects/guid/actor/Unregister.scala | 23 + .../guid/actor/UnregisterFailure.scala | 11 + .../guid/actor/UnregisterSuccess.scala | 11 + .../objects/guid/key/LoanedKey.scala | 46 + .../psforever/objects/guid/key/Monitor.scala | 13 + .../objects/guid/key/SecureKey.scala | 18 + .../guid/misc/AscendingNumberSource.scala | 32 + .../guid/misc/RegistrationTaskResolver.scala | 137 ++ .../objects/guid/pool/ExclusivePool.scala | 33 + .../objects/guid/pool/GenericPool.scala | 89 ++ .../objects/guid/pool/NumberPool.scala | 20 + .../objects/guid/pool/SimplePool.scala | 35 + .../guid/selector/NumberSelector.scala | 80 + .../guid/selector/OpportunisticSelector.scala | 25 + .../guid/selector/RandomSelector.scala | 68 + .../selector/RandomSequenceSelector.scala | 64 + .../guid/selector/SpecificSelector.scala | 54 + .../guid/selector/StrictInOrderSelector.scala | 38 + .../psforever/objects/guid/source/Key.scala | 25 + .../guid/source/LimitedNumberSource.scala | 113 ++ .../objects/guid/source/MaxNumberSource.scala | 119 ++ .../objects/guid/source/NumberSource.scala | 154 ++ .../objects/inventory/GridInventory.scala | 586 +++++++ .../inventory/InventoryEquipmentSlot.scala | 43 + .../objects/inventory/InventoryItem.scala | 23 + .../objects/inventory/InventoryTile.scala | 35 + .../terminals/OrderTerminalDefinition.scala | 138 ++ .../terminals/TemporaryTerminalMessages.scala | 13 + .../objects/terminals/Terminal.scala | 151 ++ .../objects/terminals/TerminalControl.scala | 34 + .../terminals/TerminalDefinition.scala | 264 ++++ .../objects/vehicles/ANTResourceUtility.scala | 31 + .../net/psforever/objects/vehicles/Seat.scala | 134 ++ .../vehicles/SeatArmorRestriction.scala | 18 + .../psforever/objects/vehicles/Utility.scala | 104 ++ .../objects/vehicles/VehicleLockState.scala | 14 + .../psforever/packet/GamePacketOpcode.scala | 2 +- .../packet/game/AvatarImplantMessage.scala | 41 +- .../packet/game/BeginZoningMessage.scala | 3 +- .../packet/game/FavoritesRequest.scala | 38 + .../game/PlanetsideAttributeMessage.scala | 2 +- .../game/objectcreate/InventoryData.scala | 8 +- ...toryItem.scala => InventoryItemData.scala} | 10 +- .../packet/game/objectcreate/MountItem.scala | 2 +- .../game/objectcreate/ObjectClass.scala | 8 +- .../net/psforever/types/ImplantType.scala | 38 + .../net/psforever/types/TransactionType.scala | 2 +- .../scala/game/AvatarImplantMessageTest.scala | 1 + .../scala/game/FavoritesRequestTest.scala | 33 + .../ObjectCreateDetailedMessageTest.scala | 20 +- .../scala/game/ObjectCreateMessageTest.scala | 16 +- .../test/scala/objects/ConverterTest.scala | 200 +++ .../src/test/scala/objects/EntityTest.scala | 83 + .../test/scala/objects/EquipmentTest.scala | 258 +++ .../src/test/scala/objects/ImplantTest.scala | 76 + .../test/scala/objects/InventoryTest.scala | 215 +++ .../scala/objects/NumberPoolActorTest.scala | 73 + .../scala/objects/NumberPoolHubTest.scala | 285 ++++ .../test/scala/objects/NumberPoolTest.scala | 194 +++ .../scala/objects/NumberSelectorTest.scala | 326 ++++ .../test/scala/objects/NumberSourceTest.scala | 359 +++++ .../src/test/scala/objects/PlayerTest.scala | 153 ++ common/src/test/scala/objects/Receiver.scala | 28 + pslogin/src/main/scala/AvatarService.scala | 228 +++ pslogin/src/main/scala/PsLogin.scala | 26 +- pslogin/src/main/scala/ServiceManager.scala | 64 + .../src/main/scala/WorldSessionActor.scala | 1377 +++++++++++++++-- 134 files changed, 13169 insertions(+), 225 deletions(-) create mode 100644 common/src/main/scala/net/psforever/objects/AmmoBox.scala create mode 100644 common/src/main/scala/net/psforever/objects/Avatars.scala create mode 100644 common/src/main/scala/net/psforever/objects/ConstructionItem.scala create mode 100644 common/src/main/scala/net/psforever/objects/EquipmentSlot.scala create mode 100644 common/src/main/scala/net/psforever/objects/ExoSuitDefinition.scala create mode 100644 common/src/main/scala/net/psforever/objects/GlobalDefinitions.scala create mode 100644 common/src/main/scala/net/psforever/objects/Implant.scala create mode 100644 common/src/main/scala/net/psforever/objects/ImplantSlot.scala create mode 100644 common/src/main/scala/net/psforever/objects/InfantryLoadout.scala create mode 100644 common/src/main/scala/net/psforever/objects/Kit.scala create mode 100644 common/src/main/scala/net/psforever/objects/LivePlayerList.scala create mode 100644 common/src/main/scala/net/psforever/objects/LockerContainer.scala create mode 100644 common/src/main/scala/net/psforever/objects/OffhandEquipmentSlot.scala create mode 100644 common/src/main/scala/net/psforever/objects/PlanetSideGameObject.scala create mode 100644 common/src/main/scala/net/psforever/objects/Player.scala create mode 100644 common/src/main/scala/net/psforever/objects/SimpleItem.scala create mode 100644 common/src/main/scala/net/psforever/objects/Tool.scala create mode 100644 common/src/main/scala/net/psforever/objects/Vehicle.scala create mode 100644 common/src/main/scala/net/psforever/objects/definition/AmmoBoxDefinition.scala create mode 100644 common/src/main/scala/net/psforever/objects/definition/AvatarDefinition.scala create mode 100644 common/src/main/scala/net/psforever/objects/definition/BasicDefinition.scala create mode 100644 common/src/main/scala/net/psforever/objects/definition/ConstructionItemDefinition.scala create mode 100644 common/src/main/scala/net/psforever/objects/definition/EquipmentDefinition.scala create mode 100644 common/src/main/scala/net/psforever/objects/definition/ImplantDefinition.scala create mode 100644 common/src/main/scala/net/psforever/objects/definition/KitDefinition.scala create mode 100644 common/src/main/scala/net/psforever/objects/definition/ObjectDefinition.scala create mode 100644 common/src/main/scala/net/psforever/objects/definition/SeatDefinition.scala create mode 100644 common/src/main/scala/net/psforever/objects/definition/SimpleItemDefinition.scala create mode 100644 common/src/main/scala/net/psforever/objects/definition/ToolDefinition.scala create mode 100644 common/src/main/scala/net/psforever/objects/definition/VehicleDefinition.scala create mode 100644 common/src/main/scala/net/psforever/objects/definition/converter/ACEConverter.scala create mode 100644 common/src/main/scala/net/psforever/objects/definition/converter/AMSConverter.scala create mode 100644 common/src/main/scala/net/psforever/objects/definition/converter/ANTConverter.scala create mode 100644 common/src/main/scala/net/psforever/objects/definition/converter/AmmoBoxConverter.scala create mode 100644 common/src/main/scala/net/psforever/objects/definition/converter/AvatarConverter.scala create mode 100644 common/src/main/scala/net/psforever/objects/definition/converter/BoomerTriggerConverter.scala create mode 100644 common/src/main/scala/net/psforever/objects/definition/converter/CommandDetonaterConverter.scala create mode 100644 common/src/main/scala/net/psforever/objects/definition/converter/KitConverter.scala create mode 100644 common/src/main/scala/net/psforever/objects/definition/converter/LockerContainerConverter.scala create mode 100644 common/src/main/scala/net/psforever/objects/definition/converter/PacketConverter.scala create mode 100644 common/src/main/scala/net/psforever/objects/definition/converter/REKConverter.scala create mode 100644 common/src/main/scala/net/psforever/objects/definition/converter/ToolConverter.scala create mode 100644 common/src/main/scala/net/psforever/objects/definition/converter/VehicleConverter.scala create mode 100644 common/src/main/scala/net/psforever/objects/entity/Identifiable.scala create mode 100644 common/src/main/scala/net/psforever/objects/entity/IdentifiableEntity.scala create mode 100644 common/src/main/scala/net/psforever/objects/entity/MobileWorldEntity.scala create mode 100644 common/src/main/scala/net/psforever/objects/entity/NoGUIDException.scala create mode 100644 common/src/main/scala/net/psforever/objects/entity/SimpleWorldEntity.scala create mode 100644 common/src/main/scala/net/psforever/objects/entity/TimeEntry.scala create mode 100644 common/src/main/scala/net/psforever/objects/entity/WorldEntity.scala create mode 100644 common/src/main/scala/net/psforever/objects/equipment/Ammo.scala create mode 100644 common/src/main/scala/net/psforever/objects/equipment/CItem.scala create mode 100644 common/src/main/scala/net/psforever/objects/equipment/Equipment.scala create mode 100644 common/src/main/scala/net/psforever/objects/equipment/EquipmentSize.scala create mode 100644 common/src/main/scala/net/psforever/objects/equipment/FireModeDefinition.scala create mode 100644 common/src/main/scala/net/psforever/objects/equipment/FireModeSwitch.scala create mode 100644 common/src/main/scala/net/psforever/objects/equipment/Kits.scala create mode 100644 common/src/main/scala/net/psforever/objects/equipment/SItem.scala create mode 100644 common/src/main/scala/net/psforever/objects/guid/AvailabilityPolicy.scala create mode 100644 common/src/main/scala/net/psforever/objects/guid/NumberPoolHub.scala create mode 100644 common/src/main/scala/net/psforever/objects/guid/Task.scala create mode 100644 common/src/main/scala/net/psforever/objects/guid/TaskResolver.scala create mode 100644 common/src/main/scala/net/psforever/objects/guid/actor/IsRegistered.scala create mode 100644 common/src/main/scala/net/psforever/objects/guid/actor/NumberPoolAccessorActor.scala create mode 100644 common/src/main/scala/net/psforever/objects/guid/actor/NumberPoolActor.scala create mode 100644 common/src/main/scala/net/psforever/objects/guid/actor/NumberPoolHubActor.scala create mode 100644 common/src/main/scala/net/psforever/objects/guid/actor/Register.scala create mode 100644 common/src/main/scala/net/psforever/objects/guid/actor/Unregister.scala create mode 100644 common/src/main/scala/net/psforever/objects/guid/actor/UnregisterFailure.scala create mode 100644 common/src/main/scala/net/psforever/objects/guid/actor/UnregisterSuccess.scala create mode 100644 common/src/main/scala/net/psforever/objects/guid/key/LoanedKey.scala create mode 100644 common/src/main/scala/net/psforever/objects/guid/key/Monitor.scala create mode 100644 common/src/main/scala/net/psforever/objects/guid/key/SecureKey.scala create mode 100644 common/src/main/scala/net/psforever/objects/guid/misc/AscendingNumberSource.scala create mode 100644 common/src/main/scala/net/psforever/objects/guid/misc/RegistrationTaskResolver.scala create mode 100644 common/src/main/scala/net/psforever/objects/guid/pool/ExclusivePool.scala create mode 100644 common/src/main/scala/net/psforever/objects/guid/pool/GenericPool.scala create mode 100644 common/src/main/scala/net/psforever/objects/guid/pool/NumberPool.scala create mode 100644 common/src/main/scala/net/psforever/objects/guid/pool/SimplePool.scala create mode 100644 common/src/main/scala/net/psforever/objects/guid/selector/NumberSelector.scala create mode 100644 common/src/main/scala/net/psforever/objects/guid/selector/OpportunisticSelector.scala create mode 100644 common/src/main/scala/net/psforever/objects/guid/selector/RandomSelector.scala create mode 100644 common/src/main/scala/net/psforever/objects/guid/selector/RandomSequenceSelector.scala create mode 100644 common/src/main/scala/net/psforever/objects/guid/selector/SpecificSelector.scala create mode 100644 common/src/main/scala/net/psforever/objects/guid/selector/StrictInOrderSelector.scala create mode 100644 common/src/main/scala/net/psforever/objects/guid/source/Key.scala create mode 100644 common/src/main/scala/net/psforever/objects/guid/source/LimitedNumberSource.scala create mode 100644 common/src/main/scala/net/psforever/objects/guid/source/MaxNumberSource.scala create mode 100644 common/src/main/scala/net/psforever/objects/guid/source/NumberSource.scala create mode 100644 common/src/main/scala/net/psforever/objects/inventory/GridInventory.scala create mode 100644 common/src/main/scala/net/psforever/objects/inventory/InventoryEquipmentSlot.scala create mode 100644 common/src/main/scala/net/psforever/objects/inventory/InventoryItem.scala create mode 100644 common/src/main/scala/net/psforever/objects/inventory/InventoryTile.scala create mode 100644 common/src/main/scala/net/psforever/objects/terminals/OrderTerminalDefinition.scala create mode 100644 common/src/main/scala/net/psforever/objects/terminals/TemporaryTerminalMessages.scala create mode 100644 common/src/main/scala/net/psforever/objects/terminals/Terminal.scala create mode 100644 common/src/main/scala/net/psforever/objects/terminals/TerminalControl.scala create mode 100644 common/src/main/scala/net/psforever/objects/terminals/TerminalDefinition.scala create mode 100644 common/src/main/scala/net/psforever/objects/vehicles/ANTResourceUtility.scala create mode 100644 common/src/main/scala/net/psforever/objects/vehicles/Seat.scala create mode 100644 common/src/main/scala/net/psforever/objects/vehicles/SeatArmorRestriction.scala create mode 100644 common/src/main/scala/net/psforever/objects/vehicles/Utility.scala create mode 100644 common/src/main/scala/net/psforever/objects/vehicles/VehicleLockState.scala create mode 100644 common/src/main/scala/net/psforever/packet/game/FavoritesRequest.scala rename common/src/main/scala/net/psforever/packet/game/objectcreate/{InventoryItem.scala => InventoryItemData.scala} (82%) create mode 100644 common/src/main/scala/net/psforever/types/ImplantType.scala create mode 100644 common/src/test/scala/game/FavoritesRequestTest.scala create mode 100644 common/src/test/scala/objects/ConverterTest.scala create mode 100644 common/src/test/scala/objects/EntityTest.scala create mode 100644 common/src/test/scala/objects/EquipmentTest.scala create mode 100644 common/src/test/scala/objects/ImplantTest.scala create mode 100644 common/src/test/scala/objects/InventoryTest.scala create mode 100644 common/src/test/scala/objects/NumberPoolActorTest.scala create mode 100644 common/src/test/scala/objects/NumberPoolHubTest.scala create mode 100644 common/src/test/scala/objects/NumberPoolTest.scala create mode 100644 common/src/test/scala/objects/NumberSelectorTest.scala create mode 100644 common/src/test/scala/objects/NumberSourceTest.scala create mode 100644 common/src/test/scala/objects/PlayerTest.scala create mode 100644 common/src/test/scala/objects/Receiver.scala create mode 100644 pslogin/src/main/scala/AvatarService.scala create mode 100644 pslogin/src/main/scala/ServiceManager.scala diff --git a/common/src/main/scala/net/psforever/objects/AmmoBox.scala b/common/src/main/scala/net/psforever/objects/AmmoBox.scala new file mode 100644 index 000000000..f95cebb74 --- /dev/null +++ b/common/src/main/scala/net/psforever/objects/AmmoBox.scala @@ -0,0 +1,57 @@ +// Copyright (c) 2017 PSForever +package net.psforever.objects + +import net.psforever.objects.definition.AmmoBoxDefinition +import net.psforever.objects.equipment.{Ammo, Equipment} + +class AmmoBox(private val ammoDef : AmmoBoxDefinition, + cap : Option[Int] = None + ) extends Equipment { + private var capacity = if(cap.isDefined) { AmmoBox.limitCapacity(cap.get, 1) } else { FullCapacity } + + def AmmoType : Ammo.Value = ammoDef.AmmoType + + def Capacity : Int = capacity + + def Capacity_=(toCapacity : Int) : Int = { + capacity = AmmoBox.limitCapacity(toCapacity) + Capacity + } + + def FullCapacity : Int = ammoDef.Capacity + + def Definition : AmmoBoxDefinition = ammoDef + + override def toString : String = { + AmmoBox.toString(this) + } +} + +object AmmoBox { + def apply(ammoDef : AmmoBoxDefinition) : AmmoBox = { + new AmmoBox(ammoDef) + } + + def apply(ammoDef : AmmoBoxDefinition, capacity : Int) : AmmoBox = { + new AmmoBox(ammoDef, Some(capacity)) + } + + import net.psforever.packet.game.PlanetSideGUID + def apply(guid : PlanetSideGUID, ammoDef : AmmoBoxDefinition) : AmmoBox = { + val obj = new AmmoBox(ammoDef) + obj.GUID = guid + obj + } + + def apply(guid : PlanetSideGUID, ammoDef : AmmoBoxDefinition, capacity : Int) : AmmoBox = { + val obj = new AmmoBox(ammoDef, Some(capacity)) + obj.GUID = guid + obj + } + + def limitCapacity(count : Int, min : Int = 0) : Int = math.min(math.max(min, count), 65535) + + def toString(obj : AmmoBox) : String = { + s"box of ${obj.AmmoType} ammo (${obj.Capacity})" + } +} diff --git a/common/src/main/scala/net/psforever/objects/Avatars.scala b/common/src/main/scala/net/psforever/objects/Avatars.scala new file mode 100644 index 000000000..cb9d49d09 --- /dev/null +++ b/common/src/main/scala/net/psforever/objects/Avatars.scala @@ -0,0 +1,19 @@ +// Copyright (c) 2017 PSForever +package net.psforever.objects + +/** + * An `Enumeration` of all the avatar types in the game, paired with their object id as the `Value`. + * #121 is the most important. + */ +object Avatars extends Enumeration { + final val avatar = Value(121) + final val avatar_bot = Value(122) + final val avatar_bot_agile = Value(123) + final val avatar_bot_agile_no_weapon = Value(124) + final val avatar_bot_max = Value(125) + final val avatar_bot_max_no_weapon = Value(126) + final val avatar_bot_reinforced = Value(127) + final val avatar_bot_reinforced_no_weapon = Value(128) + final val avatar_bot_standard = Value(129) + final val avatar_bot_standard_no_weapon = Value(130) +} diff --git a/common/src/main/scala/net/psforever/objects/ConstructionItem.scala b/common/src/main/scala/net/psforever/objects/ConstructionItem.scala new file mode 100644 index 000000000..6a6229551 --- /dev/null +++ b/common/src/main/scala/net/psforever/objects/ConstructionItem.scala @@ -0,0 +1,38 @@ +// Copyright (c) 2017 PSForever +package net.psforever.objects + +import net.psforever.objects.definition.ConstructionItemDefinition +import net.psforever.objects.equipment.{CItem, Equipment, FireModeSwitch} + +class ConstructionItem(private val cItemDef : ConstructionItemDefinition) extends Equipment with FireModeSwitch[CItem.DeployedItem.Value] { + private var fireModeIndex : Int = 0 + + def FireModeIndex : Int = fireModeIndex + + def FireModeIndex_=(index : Int) : Int = { + fireModeIndex = index % cItemDef.Modes.length + FireModeIndex + } + + def FireMode : CItem.DeployedItem.Value = cItemDef.Modes(fireModeIndex) + + def NextFireMode : CItem.DeployedItem.Value = { + FireModeIndex = FireModeIndex + 1 + FireMode + } + + def Definition : ConstructionItemDefinition = cItemDef +} + +object ConstructionItem { + def apply(cItemDef : ConstructionItemDefinition) : ConstructionItem = { + new ConstructionItem(cItemDef) + } + + import net.psforever.packet.game.PlanetSideGUID + def apply(guid : PlanetSideGUID, cItemDef : ConstructionItemDefinition) : ConstructionItem = { + val obj = new ConstructionItem(cItemDef) + obj.GUID = guid + obj + } +} diff --git a/common/src/main/scala/net/psforever/objects/EquipmentSlot.scala b/common/src/main/scala/net/psforever/objects/EquipmentSlot.scala new file mode 100644 index 000000000..7cb698b1b --- /dev/null +++ b/common/src/main/scala/net/psforever/objects/EquipmentSlot.scala @@ -0,0 +1,53 @@ +// Copyright (c) 2017 PSForever +package net.psforever.objects + +import net.psforever.objects.equipment.{Equipment, EquipmentSize} + +/** + * A size-checked unit of storage (or mounting) for `Equipment`. + * Unlike inventory space, anything placed in "slot" space is expected to be visible to the game world in some fashion. + */ +class EquipmentSlot { + private var size : EquipmentSize.Value = EquipmentSize.Blocked + private var tool : Option[Equipment] = None + //TODO eventually move this object from storing the item directly to just storing its GUID? + + def Size : EquipmentSize.Value = size + + def Size_=(assignSize : EquipmentSize.Value) : EquipmentSize.Value = { + if(tool.isEmpty) { + size = assignSize + } + Size + } + + def Equipment : Option[Equipment] = tool + + def Equipment_=(assignEquipment : Equipment) : Option[Equipment] = { + Equipment = Some(assignEquipment) + } + + def Equipment_=(assignEquipment : Option[Equipment]) : Option[Equipment] = { + if(assignEquipment.isDefined) { //if new equipment is defined, don't put it in the slot if the slot is being used + if(tool.isEmpty && EquipmentSize.isEqual(size, assignEquipment.get.Size)) { + tool = assignEquipment + } + } + else { + tool = None + } + Equipment + } +} + +object EquipmentSlot { + def apply() : EquipmentSlot = { + new EquipmentSlot() + } + + def apply(size : EquipmentSize.Value) : EquipmentSlot = { + val slot = new EquipmentSlot() + slot.Size = size + slot + } +} diff --git a/common/src/main/scala/net/psforever/objects/ExoSuitDefinition.scala b/common/src/main/scala/net/psforever/objects/ExoSuitDefinition.scala new file mode 100644 index 000000000..0ef2ba55f --- /dev/null +++ b/common/src/main/scala/net/psforever/objects/ExoSuitDefinition.scala @@ -0,0 +1,128 @@ +// Copyright (c) 2017 PSForever +package net.psforever.objects + +import net.psforever.objects.equipment.EquipmentSize +import net.psforever.objects.inventory.InventoryTile +import net.psforever.types.ExoSuitType + +/** + * A definition for producing the personal armor the player wears. + * Players are influenced by the exo-suit they wear in a variety of ways, with speed and available equipment slots being major differences. + * @param suitType the `Enumeration` corresponding to this exo-suit + */ +class ExoSuitDefinition(private val suitType : ExoSuitType.Value) { + private var permission : Int = 0 //TODO certification type? + private var maxArmor : Int = 0 + private val holsters : Array[EquipmentSize.Value] = Array.fill[EquipmentSize.Value](5)(EquipmentSize.Blocked) + private var inventoryScale : InventoryTile = InventoryTile.Tile11 //override with custom InventoryTile + private var inventoryOffset : Int = 0 + + def SuitType : ExoSuitType.Value = suitType + + def MaxArmor : Int = maxArmor + + def MaxArmor_=(armor : Int) : Int = { + maxArmor = math.min(math.max(0, armor), 65535) + MaxArmor + } + + def InventoryScale : InventoryTile = inventoryScale + + def InventoryScale_=(scale : InventoryTile) : InventoryTile = { + inventoryScale = scale + InventoryScale + } + + def InventoryOffset : Int = inventoryOffset + + def InventoryOffset_=(offset : Int) : Int = { + inventoryOffset = offset + InventoryOffset + } + + def Holsters : Array[EquipmentSize.Value] = holsters + + def Holster(slot : Int) : EquipmentSize.Value = { + if(slot >= 0 && slot < 5) { + holsters(slot) + } + else { + EquipmentSize.Blocked + } + } + + def Holster(slot : Int, value : EquipmentSize.Value) : EquipmentSize.Value = { + if(slot >= 0 && slot < 5) { + holsters(slot) = value + holsters(slot) + } + else { + EquipmentSize.Blocked + } + } +} + +object ExoSuitDefinition { + final val Standard = ExoSuitDefinition(ExoSuitType.Standard) + Standard.MaxArmor = 50 + Standard.InventoryScale = new InventoryTile(9,6) + Standard.InventoryOffset = 6 + Standard.Holster(0, EquipmentSize.Pistol) + Standard.Holster(2, EquipmentSize.Rifle) + Standard.Holster(4, EquipmentSize.Melee) + + final val Agile = ExoSuitDefinition(ExoSuitType.Agile) + Agile.MaxArmor = 100 + Agile.InventoryScale = new InventoryTile(9,9) + Agile.InventoryOffset = 6 + Agile.Holster(0, EquipmentSize.Pistol) + Agile.Holster(1, EquipmentSize.Pistol) + Agile.Holster(2, EquipmentSize.Rifle) + Agile.Holster(4, EquipmentSize.Melee) + + final val Reinforced = ExoSuitDefinition(ExoSuitType.Reinforced) + Reinforced.permission = 1 + Reinforced.MaxArmor = 200 + Reinforced.InventoryScale = new InventoryTile(12,9) + Reinforced.InventoryOffset = 6 + Reinforced.Holster(0, EquipmentSize.Pistol) + Reinforced.Holster(1, EquipmentSize.Pistol) + Reinforced.Holster(2, EquipmentSize.Rifle) + Reinforced.Holster(3, EquipmentSize.Rifle) + Reinforced.Holster(4, EquipmentSize.Melee) + + final val Infiltration = ExoSuitDefinition(ExoSuitType.Standard) + Infiltration.permission = 1 + Infiltration.MaxArmor = 0 + Infiltration.InventoryScale = new InventoryTile(6,6) + Infiltration.InventoryOffset = 6 + Infiltration.Holster(0, EquipmentSize.Pistol) + Infiltration.Holster(4, EquipmentSize.Melee) + + final val MAX = ExoSuitDefinition(ExoSuitType.MAX) + MAX.permission = 1 + MAX.MaxArmor = 650 + MAX.InventoryScale = new InventoryTile(16,12) + MAX.InventoryOffset = 6 + MAX.Holster(0, EquipmentSize.Max) + MAX.Holster(4, EquipmentSize.Melee) + + def apply(suitType : ExoSuitType.Value) : ExoSuitDefinition = { + new ExoSuitDefinition(suitType) + } + + /** + * A function to retrieve the correct defintion of an exo-suit from the type of exo-suit. + * @param suit the `Enumeration` corresponding to this exo-suit + * @return the exo-suit definition + */ + def Select(suit : ExoSuitType.Value) : ExoSuitDefinition = { + suit match { + case ExoSuitType.Agile => ExoSuitDefinition.Agile + case ExoSuitType.Infiltration => ExoSuitDefinition.Infiltration + case ExoSuitType.MAX => ExoSuitDefinition.MAX + case ExoSuitType.Reinforced => ExoSuitDefinition.Reinforced + case _ => ExoSuitDefinition.Standard + } + } +} diff --git a/common/src/main/scala/net/psforever/objects/GlobalDefinitions.scala b/common/src/main/scala/net/psforever/objects/GlobalDefinitions.scala new file mode 100644 index 000000000..4f3df693c --- /dev/null +++ b/common/src/main/scala/net/psforever/objects/GlobalDefinitions.scala @@ -0,0 +1,1163 @@ +// Copyright (c) 2017 PSForever +package net.psforever.objects + +import net.psforever.objects.definition._ +import net.psforever.objects.definition.converter.{CommandDetonaterConverter, REKConverter} +import net.psforever.objects.equipment.CItem.DeployedItem +import net.psforever.objects.equipment._ +import net.psforever.objects.inventory.InventoryTile +import net.psforever.packet.game.objectcreate.ObjectClass +import net.psforever.types.PlanetSideEmpire + +object GlobalDefinitions { + /** + * Given a faction, provide the standard assault melee weapon. + * @param faction the faction + * @return the `ToolDefinition` for the melee weapon + */ + def StandardMelee(faction : PlanetSideEmpire.Value) : ToolDefinition = { + faction match { + case PlanetSideEmpire.TR => chainblade + case PlanetSideEmpire.NC => magcutter + case PlanetSideEmpire.VS => forceblade + case PlanetSideEmpire.NEUTRAL => chainblade //do NOT hand out the katana + } + } + + /** + * Given a faction, provide the satndard assault pistol. + * @param faction the faction + * @return the `ToolDefinition` for the pistol + */ + def StandardPistol(faction : PlanetSideEmpire.Value) : ToolDefinition = { + faction match { + case PlanetSideEmpire.TR => repeater + case PlanetSideEmpire.NC => isp + case PlanetSideEmpire.VS => beamer + case PlanetSideEmpire.NEUTRAL => ilc9 + } + } + + /** + * For a given faction, provide the ammunition for the standard assault pistol. + * The ammunition value here must work with the result of obtaining the pistol using the faction. + * @param faction the faction + * @return thr `AmmoBoxDefinition` for the pistol's ammo + * @see `GlobalDefinitions.StandardPistol` + */ + def StandardPistolAmmo(faction : PlanetSideEmpire.Value) : AmmoBoxDefinition = { + faction match { + case PlanetSideEmpire.TR => bullet_9mm + case PlanetSideEmpire.NC => shotgun_shell + case PlanetSideEmpire.VS => energy_cell + case PlanetSideEmpire.NEUTRAL => bullet_9mm + } + } + + /** + * For a given faction, provide the medium assault pistol. + * The medium assault pistols all use the same ammunition so there is no point for a separate selection function. + * @param faction the faction + * @return the `ToolDefinition` for the pistol + */ + def MediumPistol(faction : PlanetSideEmpire.Value) : ToolDefinition = { + faction match { + case PlanetSideEmpire.TR => anniversary_guna + case PlanetSideEmpire.NC => anniversary_gun + case PlanetSideEmpire.VS => anniversary_gunb + case PlanetSideEmpire.NEUTRAL => ilc9 //do not hand out the spiker + } + } + + /** + * For a given faction, provide the medium assault rifle. + * For `Neutral` or `Black Ops`, just return a Suppressor. + * @param faction the faction + * @return the `ToolDefinition` for the rifle + */ + def MediumRifle(faction : PlanetSideEmpire.Value) : ToolDefinition = { + faction match { + case PlanetSideEmpire.TR => cycler + case PlanetSideEmpire.NC => gauss + case PlanetSideEmpire.VS => pulsar + case PlanetSideEmpire.NEUTRAL => suppressor //the Punisher would be messy to have to code for + } + } + + /** + * For a given faction, provide the ammunition for the medium assault rifle. + * The ammunition value here must work with the result of obtaining the rifle using the faction. + * @param faction the faction + * @return thr `AmmoBoxDefinition` for the rifle's ammo + * @see `GlobalDefinitions.MediumRifle` + */ + def MediumRifleAmmo(faction : PlanetSideEmpire.Value) : AmmoBoxDefinition = { + faction match { + case PlanetSideEmpire.TR => bullet_9mm + case PlanetSideEmpire.NC => bullet_9mm + case PlanetSideEmpire.VS => energy_cell + case PlanetSideEmpire.NEUTRAL => bullet_9mm + } + } + + /** + * For a given faction, provide the heavy assault rifle. + * For `Neutral` or `Black Ops`, just return a Suppressor. + * @param faction the faction + * @return the `ToolDefinition` for the rifle + */ + def HeavyRifle(faction : PlanetSideEmpire.Value) : ToolDefinition = { + faction match { + case PlanetSideEmpire.TR => mini_chaingun + case PlanetSideEmpire.NC => r_shotgun + case PlanetSideEmpire.VS => lasher + case PlanetSideEmpire.NEUTRAL => suppressor //do not hand out the maelstrom + } + } + + /** + * For a given faction, provide the ammunition for the heavy assault rifle. + * The ammunition value here must work with the result of obtaining the rifle using the faction. + * @param faction the faction + * @return thr `AmmoBoxDefinition` for the rifle's ammo + * @see `GlobalDefinitions.HeavyRifle` + */ + def HeavyRifleAmmo(faction : PlanetSideEmpire.Value) : AmmoBoxDefinition = { + faction match { + case PlanetSideEmpire.TR => bullet_9mm + case PlanetSideEmpire.NC => shotgun_shell + case PlanetSideEmpire.VS => energy_cell + case PlanetSideEmpire.NEUTRAL => bullet_9mm + } + } + + /** + * For a given faction, provide the anti-vehicular launcher. + * @param faction the faction + * @return the `ToolDefinition` for the launcher + */ + def AntiVehicular(faction : PlanetSideEmpire.Value) : ToolDefinition = { + faction match { + case PlanetSideEmpire.TR => striker + case PlanetSideEmpire.NC => hunterseeker + case PlanetSideEmpire.VS => lancer + case PlanetSideEmpire.NEUTRAL => phoenix + } + } + + /** + * For a given faction, provide the ammunition for the anti-vehicular launcher. + * The ammunition value here must work with the result of obtaining the anti-vehicular launcher using the faction. + * @param faction the faction + * @return thr `AmmoBoxDefinition` for the launcher's ammo + * @see `GlobalDefinitions.AntiVehicular` + */ + def AntiVehicularAmmo(faction : PlanetSideEmpire.Value) : AmmoBoxDefinition = { + faction match { + case PlanetSideEmpire.TR => striker_missile_ammo + case PlanetSideEmpire.NC => hunter_seeker_missile + case PlanetSideEmpire.VS => lancer_cartridge + case PlanetSideEmpire.NEUTRAL => phoenix_missile //careful - does not exist as an AmmoBox normally + } + } + + /** + * Using the definition for a piece of `Equipment` determine with which faction it aligns if it is a weapon. + * Only checks `Tool` objects. + * Useful for determining if some item has to be dropped during an activity like `InfantryLoadout` switching. + * @param edef the `EquipmentDefinition` of the item + * @return the faction alignment, or `Neutral` + */ + def isFactionWeapon(edef : EquipmentDefinition) : PlanetSideEmpire.Value = { + edef match { + case `chainblade` | `repeater` | `anniversary_guna` | `cycler` | `mini_chaingun` | `striker` => + PlanetSideEmpire.TR + case `magcutter` | `isp` | `anniversary_gun` | `gauss` | `r_shotgun` | `hunterseeker` => + PlanetSideEmpire.NC + case `forceblade` | `beamer` | `anniversary_gunb` | `pulsar` | `lasher` | `lancer` => + PlanetSideEmpire.VS + case _ => + PlanetSideEmpire.NEUTRAL + } + } + + /** + * Using the definition for a piece of `Equipment` determine with which faction it aligns. + * Checks both `Tool` objects and unique `AmmoBox` objects. + * @param edef the `EquipmentDefinition` of the item + * @return the faction alignment, or `Neutral` + */ + def isFactionEquipment(edef : EquipmentDefinition) : PlanetSideEmpire.Value = { + edef match { + case `chainblade` | `repeater` | `anniversary_guna` | `cycler` | `mini_chaingun` | `striker` | `striker_missile_ammo` => + PlanetSideEmpire.TR + case `magcutter` | `isp` | `anniversary_gun` | `gauss` | `r_shotgun` | `hunterseeker` | `hunter_seeker_missile` => + PlanetSideEmpire.NC + case `forceblade` | `beamer` | `anniversary_gunb` | `pulsar` | `lasher` | `lancer` | `energy_cell` | `lancer_cartridge` => + PlanetSideEmpire.VS + case _ => + PlanetSideEmpire.NEUTRAL + } + } + + /** + * Using the definition for a piece of `Equipment` determine whether it is a "cavern weapon." + * Useful for determining if some item has to be dropped during an activity like `InfantryLoadout` switching. + * @param edef the `EquipmentDefinition` of the item + * @return `true`, if it is; otherwise, `false` + */ + def isCavernWeapon(edef : EquipmentDefinition) : Boolean = { + edef match { + case `spiker` | `maelstrom` | `radiator` => true + case _ => false + } + } + + /** + * Using the definition for a piece of `Equipment` determine whether it is "cavern equipment." + * @param edef the `EquipmentDefinition` of the item + * @return `true`, if it is; otherwise, `false` + */ + def isCavernEquipment(edef : EquipmentDefinition) : Boolean = { + edef match { + case `spiker` | `maelstrom` | `radiator` | `ancient_ammo_combo` | `maelstrom_ammo` => true + case _ => false + } + } + + /** + * Using the definition for a piece of `Equipment` determine whether it is "special." + * "Special equipment" is any non-standard `Equipment` that, while it can be obtained from a `Terminal`, has artificial prerequisites. + * For example, the Kits are unlocked as rewards for holiday events and require possessing a specific `MeritCommendation`. + * @param edef the `EquipmentDefinition` of the item + * @return `true`, if it is; otherwise, `false` + */ + def isSpecialEquipment(edef : EquipmentDefinition) : Boolean = { + edef match { + case `super_medkit` | `super_armorkit` | `super_staminakit` | `katana` => + true + case _ => + false + } + } + + val + medkit = KitDefinition(Kits.medkit) + + val + super_medkit = KitDefinition(Kits.super_medkit) + + val + super_armorkit = KitDefinition(Kits.super_armorkit) + + val + super_staminakit = KitDefinition(Kits.super_staminakit) //super stimpak + + val + melee_ammo = AmmoBoxDefinition(Ammo.melee_ammo) + + val + frag_grenade_ammo = AmmoBoxDefinition(Ammo.frag_grenade_ammo) + + val + plasma_grenade_ammo = AmmoBoxDefinition(Ammo.plasma_grenade_ammo) + + val + jammer_grenade_ammo = AmmoBoxDefinition(Ammo.jammer_grenade_ammo) + + val + bullet_9mm = AmmoBoxDefinition(Ammo.bullet_9mm) + bullet_9mm.Capacity = 50 + bullet_9mm.Tile = InventoryTile.Tile33 + + val + bullet_9mm_AP = AmmoBoxDefinition(Ammo.bullet_9mm_AP) + bullet_9mm_AP.Capacity = 50 + bullet_9mm_AP.Tile = InventoryTile.Tile33 + + val + shotgun_shell = AmmoBoxDefinition(Ammo.shotgun_shell) + shotgun_shell.Capacity = 32 + shotgun_shell.Tile = InventoryTile.Tile33 + + val + shotgun_shell_AP = AmmoBoxDefinition(Ammo.shotgun_shell_AP) + shotgun_shell_AP.Capacity = 32 + shotgun_shell_AP.Tile = InventoryTile.Tile33 + + val + energy_cell = AmmoBoxDefinition(Ammo.energy_cell) + energy_cell.Capacity = 50 + energy_cell.Tile = InventoryTile.Tile33 + + val + anniversary_ammo = AmmoBoxDefinition(Ammo.anniversary_ammo) //10mm multi-phase + anniversary_ammo.Capacity = 30 + anniversary_ammo.Tile = InventoryTile.Tile33 + + val + ancient_ammo_combo = AmmoBoxDefinition(Ammo.ancient_ammo_combo) + ancient_ammo_combo.Capacity = 30 + ancient_ammo_combo.Tile = InventoryTile.Tile33 + + val + maelstrom_ammo = AmmoBoxDefinition(Ammo.maelstrom_ammo) + maelstrom_ammo.Capacity = 50 + maelstrom_ammo.Tile = InventoryTile.Tile33 + + val + phoenix_missile = AmmoBoxDefinition(Ammo.phoenix_missile) //decimator missile + + val + striker_missile_ammo = AmmoBoxDefinition(Ammo.striker_missile_ammo) + striker_missile_ammo.Capacity = 15 + striker_missile_ammo.Tile = InventoryTile.Tile44 + + val + hunter_seeker_missile = AmmoBoxDefinition(Ammo.hunter_seeker_missile) //phoenix missile + hunter_seeker_missile.Capacity = 9 + hunter_seeker_missile.Tile = InventoryTile.Tile44 + + val + lancer_cartridge = AmmoBoxDefinition(Ammo.lancer_cartridge) + lancer_cartridge.Capacity = 18 + lancer_cartridge.Tile = InventoryTile.Tile44 + + val + rocket = AmmoBoxDefinition(Ammo.rocket) + rocket.Capacity = 15 + rocket.Tile = InventoryTile.Tile33 + + val + frag_cartridge = AmmoBoxDefinition(Ammo.frag_cartridge) + frag_cartridge.Capacity = 12 + frag_cartridge.Tile = InventoryTile.Tile33 + + val + plasma_cartridge = AmmoBoxDefinition(Ammo.plasma_cartridge) + plasma_cartridge.Capacity = 12 + plasma_cartridge.Tile = InventoryTile.Tile33 + + val + jammer_cartridge = AmmoBoxDefinition(Ammo.jammer_cartridge) + jammer_cartridge.Capacity = 12 + jammer_cartridge.Tile = InventoryTile.Tile33 + + val + bolt = AmmoBoxDefinition(Ammo.bolt) + bolt.Capacity = 10 + bolt.Tile = InventoryTile.Tile33 + + val + oicw_ammo = AmmoBoxDefinition(Ammo.oicw_ammo) //scorpion missile + oicw_ammo.Capacity = 10 + oicw_ammo.Tile = InventoryTile.Tile44 + + val + flamethrower_ammo = AmmoBoxDefinition(Ammo.flamethrower_ammo) + flamethrower_ammo.Capacity = 100 + flamethrower_ammo.Tile = InventoryTile.Tile44 + + val + health_canister = AmmoBoxDefinition(Ammo.health_canister) + health_canister.Capacity = 100 + health_canister.Tile = InventoryTile.Tile33 + + val + armor_canister = AmmoBoxDefinition(Ammo.armor_canister) + armor_canister.Capacity = 100 + armor_canister.Tile = InventoryTile.Tile33 + + val + upgrade_canister = AmmoBoxDefinition(Ammo.upgrade_canister) + upgrade_canister.Capacity = 100 + upgrade_canister.Tile = InventoryTile.Tile33 + + val + trek_ammo = AmmoBoxDefinition(Ammo.trek_ammo) +// + val + bullet_35mm = AmmoBoxDefinition(Ammo.bullet_35mm) //liberator nosegun + bullet_35mm.Capacity = 100 + bullet_35mm.Tile = InventoryTile.Tile44 + + val + aphelion_laser_ammo = AmmoBoxDefinition(Ammo.aphelion_laser_ammo) + aphelion_laser_ammo.Capacity = 165 + aphelion_laser_ammo.Tile = InventoryTile.Tile44 + + val + aphelion_immolation_cannon_ammo = AmmoBoxDefinition(Ammo.aphelion_immolation_cannon_ammo) + aphelion_immolation_cannon_ammo.Capacity = 100 + aphelion_immolation_cannon_ammo.Tile = InventoryTile.Tile55 + + val + aphelion_plasma_rocket_ammo = AmmoBoxDefinition(Ammo.aphelion_plasma_rocket_ammo) + aphelion_plasma_rocket_ammo.Capacity = 195 + aphelion_plasma_rocket_ammo.Tile = InventoryTile.Tile55 + + val + aphelion_ppa_ammo = AmmoBoxDefinition(Ammo.aphelion_ppa_ammo) + aphelion_ppa_ammo.Capacity = 110 + aphelion_ppa_ammo.Tile = InventoryTile.Tile44 + + val + aphelion_starfire_ammo = AmmoBoxDefinition(Ammo.aphelion_starfire_ammo) + aphelion_starfire_ammo.Capacity = 132 + aphelion_starfire_ammo.Tile = InventoryTile.Tile44 + + val + skyguard_flak_cannon_ammo = AmmoBoxDefinition(Ammo.skyguard_flak_cannon_ammo) + skyguard_flak_cannon_ammo.Capacity = 200 + skyguard_flak_cannon_ammo.Tile = InventoryTile.Tile44 + + val + flux_cannon_thresher_battery = AmmoBoxDefinition(Ammo.flux_cannon_thresher_battery) + flux_cannon_thresher_battery.Capacity = 150 + flux_cannon_thresher_battery.Tile = InventoryTile.Tile44 + + val + fluxpod_ammo = AmmoBoxDefinition(Ammo.fluxpod_ammo) + fluxpod_ammo.Capacity = 80 + fluxpod_ammo.Tile = InventoryTile.Tile44 + + val + hellfire_ammo = AmmoBoxDefinition(Ammo.hellfire_ammo) + hellfire_ammo.Capacity = 24 + hellfire_ammo.Tile = InventoryTile.Tile44 + + val + liberator_bomb = AmmoBoxDefinition(Ammo.liberator_bomb) + liberator_bomb.Capacity = 20 + liberator_bomb.Tile = InventoryTile.Tile44 + + val + bullet_25mm = AmmoBoxDefinition(Ammo.bullet_25mm) //liberator tailgun + bullet_25mm.Capacity = 150 + bullet_25mm.Tile = InventoryTile.Tile44 + + val + bullet_75mm = AmmoBoxDefinition(Ammo.bullet_75mm) //lightning shell + bullet_75mm.Capacity = 100 + bullet_75mm.Tile = InventoryTile.Tile44 + + val + heavy_grenade_mortar = AmmoBoxDefinition(Ammo.heavy_grenade_mortar) //marauder and gal gunship + heavy_grenade_mortar.Capacity = 100 + heavy_grenade_mortar.Tile = InventoryTile.Tile44 + + val + pulse_battery = AmmoBoxDefinition(Ammo.pulse_battery) + pulse_battery.Capacity = 100 + pulse_battery.Tile = InventoryTile.Tile44 + + val + heavy_rail_beam_battery = AmmoBoxDefinition(Ammo.heavy_rail_beam_battery) + heavy_rail_beam_battery.Capacity = 100 + heavy_rail_beam_battery.Tile = InventoryTile.Tile44 + + val + reaver_rocket = AmmoBoxDefinition(Ammo.reaver_rocket) + reaver_rocket.Capacity = 12 + reaver_rocket.Tile = InventoryTile.Tile44 + + val + bullet_20mm = AmmoBoxDefinition(Ammo.bullet_20mm) //reaver nosegun + bullet_20mm.Capacity = 200 + bullet_20mm.Tile = InventoryTile.Tile44 + + val + bullet_12mm = AmmoBoxDefinition(Ammo.bullet_12mm) //common + bullet_12mm.Capacity = 200 + bullet_12mm.Tile = InventoryTile.Tile44 + + val + wasp_rocket_ammo = AmmoBoxDefinition(Ammo.wasp_rocket_ammo) + wasp_rocket_ammo.Capacity = 6 + wasp_rocket_ammo.Tile = InventoryTile.Tile44 + + val + wasp_gun_ammo = AmmoBoxDefinition(Ammo.wasp_gun_ammo) //wasp nosegun + wasp_gun_ammo.Capacity = 150 + wasp_gun_ammo.Tile = InventoryTile.Tile44 + + val + bullet_15mm = AmmoBoxDefinition(Ammo.bullet_15mm) + bullet_15mm.Capacity = 360 + bullet_15mm.Tile = InventoryTile.Tile44 + + val + colossus_100mm_cannon_ammo = AmmoBoxDefinition(Ammo.colossus_100mm_cannon_ammo) + colossus_100mm_cannon_ammo.Capacity = 90 + colossus_100mm_cannon_ammo.Tile = InventoryTile.Tile55 + + val + colossus_burster_ammo = AmmoBoxDefinition(Ammo.colossus_burster_ammo) + colossus_burster_ammo.Capacity = 235 + colossus_burster_ammo.Tile = InventoryTile.Tile44 + + val + colossus_cluster_bomb_ammo = AmmoBoxDefinition(Ammo.colossus_cluster_bomb_ammo) //colossus mortar launcher shells + colossus_cluster_bomb_ammo.Capacity = 150 + colossus_cluster_bomb_ammo.Tile = InventoryTile.Tile55 + + val + colossus_chaingun_ammo = AmmoBoxDefinition(Ammo.colossus_chaingun_ammo) + colossus_chaingun_ammo.Capacity = 600 + colossus_chaingun_ammo.Tile = InventoryTile.Tile44 + + val + colossus_tank_cannon_ammo = AmmoBoxDefinition(Ammo.colossus_tank_cannon_ammo) + colossus_tank_cannon_ammo.Capacity = 110 + colossus_tank_cannon_ammo.Tile = InventoryTile.Tile44 + + val + bullet_105mm = AmmoBoxDefinition(Ammo.bullet_105mm) //prowler 100mm cannon shell + bullet_105mm.Capacity = 100 + bullet_105mm.Tile = InventoryTile.Tile44 + + val + gauss_cannon_ammo = AmmoBoxDefinition(Ammo.gauss_cannon_ammo) + gauss_cannon_ammo.Capacity = 15 + gauss_cannon_ammo.Tile = InventoryTile.Tile44 + + val + peregrine_dual_machine_gun_ammo = AmmoBoxDefinition(Ammo.peregrine_dual_machine_gun_ammo) + peregrine_dual_machine_gun_ammo.Capacity = 240 + peregrine_dual_machine_gun_ammo.Tile = InventoryTile.Tile44 + + val + peregrine_mechhammer_ammo = AmmoBoxDefinition(Ammo.peregrine_mechhammer_ammo) + peregrine_mechhammer_ammo.Capacity = 30 + peregrine_mechhammer_ammo.Tile = InventoryTile.Tile44 + + val + peregrine_particle_cannon_ammo = AmmoBoxDefinition(Ammo.peregrine_particle_cannon_ammo) + peregrine_particle_cannon_ammo.Capacity = 40 + peregrine_particle_cannon_ammo.Tile = InventoryTile.Tile55 + + val + peregrine_rocket_pod_ammo = AmmoBoxDefinition(Ammo.peregrine_rocket_pod_ammo) + peregrine_rocket_pod_ammo.Capacity = 275 + peregrine_rocket_pod_ammo.Tile = InventoryTile.Tile55 + + val + peregrine_sparrow_ammo = AmmoBoxDefinition(Ammo.peregrine_sparrow_ammo) + peregrine_sparrow_ammo.Capacity = 150 + peregrine_sparrow_ammo.Tile = InventoryTile.Tile44 + + val + bullet_150mm = AmmoBoxDefinition(Ammo.bullet_150mm) + bullet_150mm.Capacity = 50 + bullet_150mm.Tile = InventoryTile.Tile44 + + val + chainblade = ToolDefinition(ObjectClass.chainblade) + chainblade.Size = EquipmentSize.Melee + chainblade.AmmoTypes += Ammo.melee_ammo + chainblade.FireModes += new FireModeDefinition + chainblade.FireModes.head.AmmoTypeIndices += 0 + chainblade.FireModes.head.AmmoSlotIndex = 0 + chainblade.FireModes.head.Magazine = 1 + chainblade.FireModes += new FireModeDefinition + chainblade.FireModes(1).AmmoTypeIndices += 0 + chainblade.FireModes(1).AmmoSlotIndex = 0 + chainblade.FireModes(1).Magazine = 1 + + val + magcutter = ToolDefinition(ObjectClass.magcutter) + magcutter.Size = EquipmentSize.Melee + magcutter.AmmoTypes += Ammo.melee_ammo + magcutter.FireModes += new FireModeDefinition + magcutter.FireModes.head.AmmoTypeIndices += 0 + magcutter.FireModes.head.AmmoSlotIndex = 0 + magcutter.FireModes.head.Magazine = 1 + magcutter.FireModes += new FireModeDefinition + magcutter.FireModes(1).AmmoTypeIndices += 0 + magcutter.FireModes(1).AmmoSlotIndex = 0 + magcutter.FireModes(1).Magazine = 1 + + val + forceblade = ToolDefinition(ObjectClass.forceblade) + forceblade.Size = EquipmentSize.Melee + forceblade.AmmoTypes += Ammo.melee_ammo + forceblade.FireModes += new FireModeDefinition + forceblade.FireModes.head.AmmoTypeIndices += 0 + forceblade.FireModes.head.AmmoSlotIndex = 0 + forceblade.FireModes.head.Magazine = 1 + forceblade.FireModes.head.Chamber = 0 + forceblade.FireModes += new FireModeDefinition + forceblade.FireModes(1).AmmoTypeIndices += 0 + forceblade.FireModes(1).AmmoSlotIndex = 0 + forceblade.FireModes(1).Magazine = 1 + forceblade.FireModes(1).Chamber = 0 + + val + katana = ToolDefinition(ObjectClass.katana) + katana.Size = EquipmentSize.Melee + katana.AmmoTypes += Ammo.melee_ammo + katana.FireModes += new FireModeDefinition + katana.FireModes.head.AmmoTypeIndices += 0 + katana.FireModes.head.AmmoSlotIndex = 0 + katana.FireModes.head.Magazine = 1 + katana.FireModes.head.Chamber = 0 + katana.FireModes += new FireModeDefinition + katana.FireModes(1).AmmoTypeIndices += 0 + katana.FireModes(1).AmmoSlotIndex = 0 + katana.FireModes(1).Magazine = 1 + katana.FireModes(1).Chamber = 0 + + val + frag_grenade = ToolDefinition(ObjectClass.frag_grenade) + frag_grenade.Size = EquipmentSize.Pistol + frag_grenade.AmmoTypes += Ammo.frag_grenade_ammo + frag_grenade.FireModes += new FireModeDefinition + frag_grenade.FireModes.head.AmmoTypeIndices += 0 + frag_grenade.FireModes.head.AmmoSlotIndex = 0 + frag_grenade.FireModes.head.Magazine = 3 + frag_grenade.FireModes += new FireModeDefinition + frag_grenade.FireModes(1).AmmoTypeIndices += 0 + frag_grenade.FireModes(1).AmmoSlotIndex = 0 + frag_grenade.FireModes(1).Magazine = 3 + frag_grenade.Tile = InventoryTile.Tile22 + + val + plasma_grenade = ToolDefinition(ObjectClass.plasma_grenade) + plasma_grenade.Size = EquipmentSize.Pistol + plasma_grenade.AmmoTypes += Ammo.plasma_grenade_ammo + plasma_grenade.FireModes += new FireModeDefinition + plasma_grenade.FireModes.head.AmmoTypeIndices += 0 + plasma_grenade.FireModes.head.AmmoSlotIndex = 0 + plasma_grenade.FireModes.head.Magazine = 3 + plasma_grenade.FireModes += new FireModeDefinition + plasma_grenade.FireModes(1).AmmoTypeIndices += 0 + plasma_grenade.FireModes(1).AmmoSlotIndex = 0 + plasma_grenade.FireModes(1).Magazine = 3 + plasma_grenade.Tile = InventoryTile.Tile22 + + val + jammer_grenade = ToolDefinition(ObjectClass.jammer_grenade) + jammer_grenade.Size = EquipmentSize.Pistol + jammer_grenade.AmmoTypes += Ammo.jammer_grenade_ammo + jammer_grenade.FireModes += new FireModeDefinition + jammer_grenade.FireModes.head.AmmoTypeIndices += 0 + jammer_grenade.FireModes.head.AmmoSlotIndex = 0 + jammer_grenade.FireModes.head.Magazine = 3 + jammer_grenade.FireModes += new FireModeDefinition + jammer_grenade.FireModes(1).AmmoTypeIndices += 0 + jammer_grenade.FireModes(1).AmmoSlotIndex = 0 + jammer_grenade.FireModes(1).Magazine = 3 + jammer_grenade.Tile = InventoryTile.Tile22 + + val + repeater = ToolDefinition(ObjectClass.repeater) + repeater.Size = EquipmentSize.Pistol + repeater.AmmoTypes += Ammo.bullet_9mm + repeater.AmmoTypes += Ammo.bullet_9mm_AP + repeater.FireModes += new FireModeDefinition + repeater.FireModes.head.AmmoTypeIndices += 0 + repeater.FireModes.head.AmmoTypeIndices += 1 + repeater.FireModes.head.AmmoSlotIndex = 0 + repeater.FireModes.head.Magazine = 20 + repeater.Tile = InventoryTile.Tile33 + + val + isp = ToolDefinition(ObjectClass.isp) //mag-scatter + isp.Size = EquipmentSize.Pistol + isp.AmmoTypes += Ammo.shotgun_shell + isp.AmmoTypes += Ammo.shotgun_shell_AP + isp.FireModes += new FireModeDefinition + isp.FireModes.head.AmmoTypeIndices += 0 + isp.FireModes.head.AmmoTypeIndices += 1 + isp.FireModes.head.AmmoSlotIndex = 0 + isp.FireModes.head.Magazine = 8 + isp.Tile = InventoryTile.Tile33 + + val + beamer = ToolDefinition(ObjectClass.beamer) + beamer.Size = EquipmentSize.Pistol + beamer.AmmoTypes += Ammo.energy_cell + beamer.FireModes += new FireModeDefinition + beamer.FireModes.head.AmmoTypeIndices += 0 + beamer.FireModes.head.AmmoSlotIndex = 0 + beamer.FireModes.head.Magazine = 16 + beamer.FireModes += new FireModeDefinition + beamer.FireModes(1).AmmoTypeIndices += 0 + beamer.FireModes(1).AmmoSlotIndex = 0 + beamer.FireModes(1).Magazine = 16 + beamer.Tile = InventoryTile.Tile33 + + val + ilc9 = ToolDefinition(ObjectClass.ilc9) //amp + ilc9.Size = EquipmentSize.Pistol + ilc9.AmmoTypes += Ammo.bullet_9mm + ilc9.AmmoTypes += Ammo.bullet_9mm_AP + ilc9.FireModes += new FireModeDefinition + ilc9.FireModes.head.AmmoTypeIndices += 0 + ilc9.FireModes.head.AmmoTypeIndices += 1 + ilc9.FireModes.head.AmmoSlotIndex = 0 + ilc9.FireModes.head.Magazine = 30 + ilc9.Tile = InventoryTile.Tile33 + + val + suppressor = ToolDefinition(ObjectClass.suppressor) + suppressor.Size = EquipmentSize.Rifle + suppressor.AmmoTypes += Ammo.bullet_9mm + suppressor.AmmoTypes += Ammo.bullet_9mm_AP + suppressor.FireModes += new FireModeDefinition + suppressor.FireModes.head.AmmoTypeIndices += 0 + suppressor.FireModes.head.AmmoTypeIndices += 1 + suppressor.FireModes.head.AmmoSlotIndex = 0 + suppressor.FireModes.head.Magazine = 25 + suppressor.Tile = InventoryTile.Tile63 + + val + punisher = ToolDefinition(ObjectClass.punisher) + punisher.Size = EquipmentSize.Rifle + punisher.AmmoTypes += Ammo.bullet_9mm + punisher.AmmoTypes += Ammo.bullet_9mm_AP + punisher.AmmoTypes += Ammo.rocket + punisher.AmmoTypes += Ammo.frag_cartridge + punisher.AmmoTypes += Ammo.jammer_cartridge + punisher.AmmoTypes += Ammo.plasma_cartridge + punisher.FireModes += new FireModeDefinition + punisher.FireModes.head.AmmoTypeIndices += 0 + punisher.FireModes.head.AmmoTypeIndices += 1 + punisher.FireModes.head.AmmoSlotIndex = 0 + punisher.FireModes.head.Magazine = 30 + punisher.FireModes += new FireModeDefinition + punisher.FireModes(1).AmmoTypeIndices += 2 + punisher.FireModes(1).AmmoTypeIndices += 3 + punisher.FireModes(1).AmmoTypeIndices += 4 + punisher.FireModes(1).AmmoTypeIndices += 5 + punisher.FireModes(1).AmmoSlotIndex = 1 + punisher.FireModes(1).Magazine = 1 + punisher.Tile = InventoryTile.Tile63 + + val + flechette = ToolDefinition(ObjectClass.flechette) //sweeper + flechette.Size = EquipmentSize.Rifle + flechette.AmmoTypes += Ammo.shotgun_shell + flechette.AmmoTypes += Ammo.shotgun_shell_AP + flechette.FireModes += new FireModeDefinition + flechette.FireModes.head.AmmoTypeIndices += 0 + flechette.FireModes.head.AmmoTypeIndices += 1 + flechette.FireModes.head.AmmoSlotIndex = 0 + flechette.FireModes.head.Magazine = 12 //12 shells * 8 pellets = 96 + flechette.Tile = InventoryTile.Tile63 + + val + cycler = ToolDefinition(ObjectClass.cycler) + cycler.Size = EquipmentSize.Rifle + cycler.AmmoTypes += Ammo.bullet_9mm + cycler.AmmoTypes += Ammo.bullet_9mm_AP + cycler.FireModes += new FireModeDefinition + cycler.FireModes.head.AmmoTypeIndices += 0 + cycler.FireModes.head.AmmoTypeIndices += 1 + cycler.FireModes.head.AmmoSlotIndex = 0 + cycler.FireModes.head.Magazine = 50 + cycler.Tile = InventoryTile.Tile63 + + val + gauss = ToolDefinition(ObjectClass.gauss) + gauss.Size = EquipmentSize.Rifle + gauss.AmmoTypes += Ammo.bullet_9mm + gauss.AmmoTypes += Ammo.bullet_9mm_AP + gauss.FireModes += new FireModeDefinition + gauss.FireModes.head.AmmoTypeIndices += 0 + gauss.FireModes.head.AmmoTypeIndices += 1 + gauss.FireModes.head.AmmoSlotIndex = 0 + gauss.FireModes.head.Magazine = 30 + gauss.Tile = InventoryTile.Tile63 + + val + pulsar = ToolDefinition(ObjectClass.pulsar) + pulsar.Size = EquipmentSize.Rifle + pulsar.AmmoTypes += Ammo.energy_cell + pulsar.FireModes += new FireModeDefinition + pulsar.FireModes.head.AmmoTypeIndices += 0 + pulsar.FireModes.head.AmmoSlotIndex = 0 + pulsar.FireModes.head.Magazine = 40 + pulsar.FireModes += new FireModeDefinition + pulsar.FireModes(1).AmmoTypeIndices += 0 + pulsar.FireModes(1).AmmoSlotIndex = 0 + pulsar.FireModes(1).Magazine = 40 + pulsar.Tile = InventoryTile.Tile63 + + val + anniversary_guna = ToolDefinition(ObjectClass.anniversary_guna) //tr stinger + anniversary_guna.Size = EquipmentSize.Pistol + anniversary_guna.AmmoTypes += Ammo.anniversary_ammo + anniversary_guna.FireModes += new FireModeDefinition + anniversary_guna.FireModes.head.AmmoTypeIndices += 0 + anniversary_guna.FireModes.head.AmmoSlotIndex = 0 + anniversary_guna.FireModes.head.Magazine = 6 + anniversary_guna.FireModes += new FireModeDefinition + anniversary_guna.FireModes(1).AmmoTypeIndices += 0 + anniversary_guna.FireModes(1).AmmoSlotIndex = 0 + anniversary_guna.FireModes(1).Magazine = 6 + anniversary_guna.FireModes(1).Chamber = 6 + anniversary_guna.Tile = InventoryTile.Tile33 + + val + anniversary_gun = ToolDefinition(ObjectClass.anniversary_gun) //nc spear + anniversary_gun.Size = EquipmentSize.Pistol + anniversary_gun.AmmoTypes += Ammo.anniversary_ammo + anniversary_gun.FireModes += new FireModeDefinition + anniversary_gun.FireModes.head.AmmoTypeIndices += 0 + anniversary_gun.FireModes.head.AmmoSlotIndex = 0 + anniversary_gun.FireModes.head.Magazine = 6 + anniversary_gun.FireModes += new FireModeDefinition + anniversary_gun.FireModes(1).AmmoTypeIndices += 0 + anniversary_gun.FireModes(1).AmmoSlotIndex = 0 + anniversary_gun.FireModes(1).Magazine = 6 + anniversary_gun.FireModes(1).Chamber = 6 + anniversary_gun.Tile = InventoryTile.Tile33 + + val + anniversary_gunb = ToolDefinition(ObjectClass.anniversary_gunb) //vs eraser + anniversary_gunb.Size = EquipmentSize.Pistol + anniversary_gunb.AmmoTypes += Ammo.anniversary_ammo + anniversary_gunb.FireModes += new FireModeDefinition + anniversary_gunb.FireModes.head.AmmoTypeIndices += 0 + anniversary_gunb.FireModes.head.AmmoSlotIndex = 0 + anniversary_gunb.FireModes.head.Magazine = 6 + anniversary_gunb.FireModes += new FireModeDefinition + anniversary_gunb.FireModes(1).AmmoTypeIndices += 0 + anniversary_gunb.FireModes(1).AmmoSlotIndex = 0 + anniversary_gunb.FireModes(1).Magazine = 6 + anniversary_gunb.FireModes(1).Chamber = 6 + anniversary_gunb.Tile = InventoryTile.Tile33 + + val + spiker = ToolDefinition(ObjectClass.spiker) + spiker.Size = EquipmentSize.Pistol + spiker.AmmoTypes += Ammo.ancient_ammo_combo + spiker.FireModes += new FireModeDefinition + spiker.FireModes.head.AmmoTypeIndices += 0 + spiker.FireModes.head.AmmoSlotIndex = 0 + spiker.FireModes.head.Magazine = 25 + spiker.Tile = InventoryTile.Tile33 + + val + mini_chaingun = ToolDefinition(ObjectClass.mini_chaingun) + mini_chaingun.Size = EquipmentSize.Rifle + mini_chaingun.AmmoTypes += Ammo.bullet_9mm + mini_chaingun.AmmoTypes += Ammo.bullet_9mm_AP + mini_chaingun.FireModes += new FireModeDefinition + mini_chaingun.FireModes.head.AmmoTypeIndices += 0 + mini_chaingun.FireModes.head.AmmoTypeIndices += 1 + mini_chaingun.FireModes.head.AmmoSlotIndex = 0 + mini_chaingun.FireModes.head.Magazine = 100 + mini_chaingun.Tile = InventoryTile.Tile93 + + val + r_shotgun = ToolDefinition(ObjectClass.r_shotgun) //jackhammer + r_shotgun.Size = EquipmentSize.Rifle + r_shotgun.AmmoTypes += Ammo.shotgun_shell + r_shotgun.AmmoTypes += Ammo.shotgun_shell_AP + r_shotgun.FireModes += new FireModeDefinition + r_shotgun.FireModes.head.AmmoTypeIndices += 0 + r_shotgun.FireModes.head.AmmoTypeIndices += 1 + r_shotgun.FireModes.head.AmmoSlotIndex = 0 + r_shotgun.FireModes.head.Magazine = 16 //16 shells * 8 pellets = 128 + r_shotgun.FireModes += new FireModeDefinition + r_shotgun.FireModes(1).AmmoTypeIndices += 0 + r_shotgun.FireModes(1).AmmoTypeIndices += 1 + r_shotgun.FireModes(1).AmmoSlotIndex = 0 + r_shotgun.FireModes(1).Magazine = 16 //16 shells * 8 pellets = 128 + r_shotgun.FireModes(1).Chamber = 3 + r_shotgun.Tile = InventoryTile.Tile93 + + val + lasher = ToolDefinition(ObjectClass.lasher) + lasher.Size = EquipmentSize.Rifle + lasher.AmmoTypes += Ammo.energy_cell + lasher.FireModes += new FireModeDefinition + lasher.FireModes.head.AmmoTypeIndices += 0 + lasher.FireModes.head.AmmoSlotIndex = 0 + lasher.FireModes.head.Magazine = 35 + lasher.FireModes += new FireModeDefinition + lasher.FireModes(1).AmmoTypeIndices += 0 + lasher.FireModes(1).AmmoSlotIndex = 0 + lasher.FireModes(1).Magazine = 35 + lasher.Tile = InventoryTile.Tile93 + + val + maelstrom = ToolDefinition(ObjectClass.maelstrom) + maelstrom.Size = EquipmentSize.Rifle + maelstrom.AmmoTypes += Ammo.maelstrom_ammo + maelstrom.FireModes += new FireModeDefinition + maelstrom.FireModes.head.AmmoTypeIndices += 0 + maelstrom.FireModes.head.AmmoSlotIndex = 0 + maelstrom.FireModes.head.Magazine = 150 + maelstrom.FireModes += new FireModeDefinition + maelstrom.FireModes(1).AmmoTypeIndices += 0 + maelstrom.FireModes(1).AmmoSlotIndex = 0 + maelstrom.FireModes(1).Magazine = 150 + maelstrom.FireModes += new FireModeDefinition + maelstrom.FireModes(2).AmmoTypeIndices += 0 + maelstrom.FireModes(2).AmmoSlotIndex = 0 + maelstrom.FireModes(2).Magazine = 150 + maelstrom.Tile = InventoryTile.Tile93 + + val + phoenix = ToolDefinition(ObjectClass.phoenix) //decimator + phoenix.Size = EquipmentSize.Rifle + phoenix.AmmoTypes += Ammo.phoenix_missile + phoenix.FireModes += new FireModeDefinition + phoenix.FireModes.head.AmmoTypeIndices += 0 + phoenix.FireModes.head.AmmoSlotIndex = 0 + phoenix.FireModes.head.Magazine = 3 + phoenix.FireModes += new FireModeDefinition + phoenix.FireModes(1).AmmoTypeIndices += 0 + phoenix.FireModes(1).AmmoSlotIndex = 0 + phoenix.FireModes(1).Magazine = 3 + phoenix.Tile = InventoryTile.Tile93 + + val + striker = ToolDefinition(ObjectClass.striker) + striker.Size = EquipmentSize.Rifle + striker.AmmoTypes += Ammo.striker_missile_ammo + striker.FireModes += new FireModeDefinition + striker.FireModes.head.AmmoTypeIndices += 0 + striker.FireModes.head.AmmoSlotIndex = 0 + striker.FireModes.head.Magazine = 5 + striker.FireModes += new FireModeDefinition + striker.FireModes(1).AmmoTypeIndices += 0 + striker.FireModes(1).AmmoSlotIndex = 0 + striker.FireModes(1).Magazine = 5 + striker.Tile = InventoryTile.Tile93 + + val + hunterseeker = ToolDefinition(ObjectClass.hunterseeker) //phoenix + hunterseeker.Size = EquipmentSize.Rifle + hunterseeker.AmmoTypes += Ammo.hunter_seeker_missile + hunterseeker.FireModes += new FireModeDefinition + hunterseeker.FireModes.head.AmmoTypeIndices += 0 + hunterseeker.FireModes.head.AmmoSlotIndex = 0 + hunterseeker.FireModes.head.Magazine = 1 + hunterseeker.FireModes += new FireModeDefinition + hunterseeker.FireModes(1).AmmoTypeIndices += 0 + hunterseeker.FireModes(1).AmmoSlotIndex = 0 + hunterseeker.FireModes(1).Magazine = 1 + hunterseeker.Tile = InventoryTile.Tile93 + + val + lancer = ToolDefinition(ObjectClass.lancer) + lancer.Size = EquipmentSize.Rifle + lancer.AmmoTypes += Ammo.lancer_cartridge + lancer.FireModes += new FireModeDefinition + lancer.FireModes.head.AmmoTypeIndices += 0 + lancer.FireModes.head.AmmoSlotIndex = 0 + lancer.FireModes.head.Magazine = 6 + lancer.Tile = InventoryTile.Tile93 + + val + rocklet = ToolDefinition(ObjectClass.rocklet) + rocklet.Size = EquipmentSize.Rifle + rocklet.AmmoTypes += Ammo.rocket + rocklet.AmmoTypes += Ammo.frag_cartridge + rocklet.FireModes += new FireModeDefinition + rocklet.FireModes.head.AmmoTypeIndices += 0 + rocklet.FireModes.head.AmmoTypeIndices += 1 + rocklet.FireModes.head.AmmoSlotIndex = 0 + rocklet.FireModes.head.Magazine = 6 + rocklet.FireModes += new FireModeDefinition + rocklet.FireModes(1).AmmoTypeIndices += 0 + rocklet.FireModes(1).AmmoTypeIndices += 1 + rocklet.FireModes(1).AmmoSlotIndex = 0 + rocklet.FireModes(1).Magazine = 6 + rocklet.FireModes(1).Chamber = 6 + rocklet.Tile = InventoryTile.Tile63 + + val + thumper = ToolDefinition(ObjectClass.thumper) + thumper.Size = EquipmentSize.Rifle + thumper.AmmoTypes += Ammo.frag_cartridge + thumper.AmmoTypes += Ammo.plasma_cartridge + thumper.AmmoTypes += Ammo.jammer_cartridge + thumper.FireModes += new FireModeDefinition + thumper.FireModes.head.AmmoTypeIndices += 0 + thumper.FireModes.head.AmmoTypeIndices += 1 + thumper.FireModes.head.AmmoTypeIndices += 2 + thumper.FireModes.head.AmmoSlotIndex = 0 + thumper.FireModes.head.Magazine = 6 + thumper.FireModes += new FireModeDefinition + thumper.FireModes(1).AmmoTypeIndices += 0 + thumper.FireModes(1).AmmoTypeIndices += 1 + thumper.FireModes(1).AmmoTypeIndices += 2 + thumper.FireModes(1).AmmoSlotIndex = 0 + thumper.FireModes(1).Magazine = 6 + thumper.Tile = InventoryTile.Tile63 + + val + radiator = ToolDefinition(ObjectClass.radiator) + radiator.Size = EquipmentSize.Rifle + radiator.AmmoTypes += Ammo.ancient_ammo_combo + radiator.FireModes += new FireModeDefinition + radiator.FireModes.head.AmmoTypeIndices += 0 + radiator.FireModes.head.AmmoSlotIndex = 0 + radiator.FireModes.head.Magazine = 25 + radiator.FireModes += new FireModeDefinition + radiator.FireModes(1).AmmoTypeIndices += 0 + radiator.FireModes(1).AmmoSlotIndex = 0 + radiator.FireModes(1).Magazine = 25 + radiator.Tile = InventoryTile.Tile63 + + val + heavy_sniper = ToolDefinition(ObjectClass.heavy_sniper) //hsr + heavy_sniper.Size = EquipmentSize.Rifle + heavy_sniper.AmmoTypes += Ammo.bolt + heavy_sniper.FireModes += new FireModeDefinition + heavy_sniper.FireModes.head.AmmoTypeIndices += 0 + heavy_sniper.FireModes.head.AmmoSlotIndex = 0 + heavy_sniper.FireModes.head.Magazine = 10 + heavy_sniper.Tile = InventoryTile.Tile93 + + val + bolt_driver = ToolDefinition(ObjectClass.bolt_driver) + bolt_driver.Size = EquipmentSize.Rifle + bolt_driver.AmmoTypes += Ammo.bolt + bolt_driver.FireModes += new FireModeDefinition + bolt_driver.FireModes.head.AmmoTypeIndices += 0 + bolt_driver.FireModes.head.AmmoSlotIndex = 0 + bolt_driver.FireModes.head.Magazine = 1 + bolt_driver.Tile = InventoryTile.Tile93 + + val + oicw = ToolDefinition(ObjectClass.oicw) //scorpion + oicw.Size = EquipmentSize.Rifle + oicw.AmmoTypes += Ammo.oicw_ammo + oicw.FireModes += new FireModeDefinition + oicw.FireModes.head.AmmoTypeIndices += 0 + oicw.FireModes.head.AmmoSlotIndex = 0 + oicw.FireModes.head.Magazine = 1 + oicw.FireModes += new FireModeDefinition + oicw.FireModes(1).AmmoTypeIndices += 0 + oicw.FireModes(1).AmmoSlotIndex = 0 + oicw.FireModes(1).Magazine = 1 + oicw.Tile = InventoryTile.Tile93 + + val + flamethrower = ToolDefinition(ObjectClass.flamethrower) + flamethrower.Size = EquipmentSize.Rifle + flamethrower.AmmoTypes += Ammo.flamethrower_ammo + flamethrower.FireModes += new FireModeDefinition + flamethrower.FireModes.head.AmmoTypeIndices += 0 + flamethrower.FireModes.head.AmmoSlotIndex = 0 + flamethrower.FireModes.head.Magazine = 100 + flamethrower.FireModes.head.Chamber = 5 + flamethrower.FireModes += new FireModeDefinition + flamethrower.FireModes(1).AmmoTypeIndices += 0 + flamethrower.FireModes(1).AmmoSlotIndex = 0 + flamethrower.FireModes(1).Magazine = 100 + flamethrower.FireModes(1).Chamber = 50 + flamethrower.Tile = InventoryTile.Tile63 + + val + medicalapplicator = ToolDefinition(ObjectClass.medicalapplicator) + medicalapplicator.Size = EquipmentSize.Pistol + medicalapplicator.AmmoTypes += Ammo.health_canister + medicalapplicator.FireModes += new FireModeDefinition + medicalapplicator.FireModes.head.AmmoTypeIndices += 0 + medicalapplicator.FireModes.head.AmmoSlotIndex = 0 + medicalapplicator.FireModes.head.Magazine = 100 + medicalapplicator.FireModes += new FireModeDefinition + medicalapplicator.FireModes(1).AmmoTypeIndices += 0 + medicalapplicator.FireModes(1).AmmoSlotIndex = 0 + medicalapplicator.FireModes(1).Magazine = 100 + medicalapplicator.Tile = InventoryTile.Tile33 + + val + nano_dispenser = ToolDefinition(ObjectClass.nano_dispenser) + nano_dispenser.Size = EquipmentSize.Rifle + nano_dispenser.AmmoTypes += Ammo.armor_canister + nano_dispenser.AmmoTypes += Ammo.upgrade_canister + nano_dispenser.FireModes += new FireModeDefinition + nano_dispenser.FireModes.head.AmmoTypeIndices += 0 + nano_dispenser.FireModes.head.AmmoTypeIndices += 1 + nano_dispenser.FireModes.head.AmmoSlotIndex = 0 + nano_dispenser.FireModes.head.Magazine = 100 + nano_dispenser.Tile = InventoryTile.Tile63 + + val + bank = ToolDefinition(ObjectClass.bank) + bank.Size = EquipmentSize.Pistol + bank.AmmoTypes += Ammo.armor_canister + bank.FireModes += new FireModeDefinition + bank.FireModes.head.AmmoTypeIndices += 0 + bank.FireModes.head.AmmoSlotIndex = 0 + bank.FireModes.head.Magazine = 100 + bank.FireModes += new FireModeDefinition + bank.FireModes(1).AmmoTypeIndices += 0 + bank.FireModes(1).AmmoSlotIndex = 0 + bank.FireModes(1).Magazine = 100 + bank.Tile = InventoryTile.Tile33 + + val + remote_electronics_kit = SimpleItemDefinition(SItem.remote_electronics_kit) + remote_electronics_kit.Packet = new REKConverter + remote_electronics_kit.Tile = InventoryTile.Tile33 + + val + trek = ToolDefinition(ObjectClass.trek) + trek.Size = EquipmentSize.Pistol + trek.AmmoTypes += Ammo.trek_ammo + trek.FireModes += new FireModeDefinition + trek.FireModes.head.AmmoTypeIndices += 0 + trek.FireModes.head.AmmoSlotIndex = 0 + trek.FireModes.head.Magazine = 4 + trek.FireModes += new FireModeDefinition + trek.FireModes(1).AmmoTypeIndices += 0 + trek.FireModes(1).AmmoSlotIndex = 0 + trek.FireModes(1).Magazine = 0 + trek.Tile = InventoryTile.Tile33 + + val + flail_targeting_laser = SimpleItemDefinition(SItem.flail_targeting_laser) + flail_targeting_laser.Packet = new CommandDetonaterConverter + + val + command_detonater = SimpleItemDefinition(SItem.command_detonater) + command_detonater.Packet = new CommandDetonaterConverter + + val + ace = ConstructionItemDefinition(CItem.Unit.ace) + ace.Modes += DeployedItem.boomer + ace.Modes += DeployedItem.he_mine + ace.Modes += DeployedItem.jammer_mine + ace.Modes += DeployedItem.spitfire_turret + ace.Modes += DeployedItem.spitfire_cloaked + ace.Modes += DeployedItem.spitfire_aa + ace.Modes += DeployedItem.motionalarmsensor + ace.Modes += DeployedItem.sensor_shield + ace.Tile = InventoryTile.Tile33 + + val + advanced_ace = ConstructionItemDefinition(CItem.Unit.advanced_ace) + advanced_ace.Modes += DeployedItem.tank_traps + advanced_ace.Modes += DeployedItem.portable_manned_turret + advanced_ace.Modes += DeployedItem.deployable_shield_generator + advanced_ace.Tile = InventoryTile.Tile63 + + val + fury_weapon_systema = ToolDefinition(ObjectClass.fury_weapon_systema) + fury_weapon_systema.Size = EquipmentSize.VehicleWeapon + fury_weapon_systema.AmmoTypes += Ammo.hellfire_ammo + fury_weapon_systema.FireModes += new FireModeDefinition + fury_weapon_systema.FireModes.head.AmmoTypeIndices += 0 + fury_weapon_systema.FireModes.head.AmmoSlotIndex = 0 + fury_weapon_systema.FireModes.head.Magazine = 2 + + val + fury = VehicleDefinition(ObjectClass.fury) + fury.Seats += 0 -> new SeatDefinition() + fury.Seats(0).Bailable = true + fury.Seats(0).ControlledWeapon = Some(1) + fury.MountPoints += 0 -> 0 + fury.MountPoints += 2 -> 0 + fury.Weapons += 1 -> fury_weapon_systema + fury.TrunkSize = InventoryTile(11, 11) + fury.TrunkOffset = 30 +} diff --git a/common/src/main/scala/net/psforever/objects/Implant.scala b/common/src/main/scala/net/psforever/objects/Implant.scala new file mode 100644 index 000000000..2d7483f71 --- /dev/null +++ b/common/src/main/scala/net/psforever/objects/Implant.scala @@ -0,0 +1,86 @@ +// Copyright (c) 2017 PSForever +package net.psforever.objects + +import net.psforever.objects.definition.{ImplantDefinition, Stance} +import net.psforever.types.{ExoSuitType, ImplantType} + +/** + * A type of installable player utility that grants a perk, usually in exchange for stamina (energy).
+ *
+ * An implant starts with a never-to-initialized timer value of -1 and will not report as `Ready` until the timer is 0. + * The `Timer`, however, will report to the user a time of 0 since negative time does not make sense. + * Although the `Timer` can be manually set, using `Reset` is the better way to default the initialization timer to the correct amount. + * An external script will be necessary to operate the actual initialization countdown. + * An implant must be `Ready` before it can be `Active`. + * The `Timer` must be set (or reset) (or countdown) to 0 to be `Ready` and then it must be activated. + * @param implantDef the `ObjectDefinition` that constructs this item and maintains some of its immutable fields + */ +class Implant(implantDef : ImplantDefinition) { + private var active : Boolean = false + private var initTimer : Long = -1L + + def Name : String = implantDef.Name + + def Ready : Boolean = initTimer == 0L + + def Active : Boolean = active + + def Active_=(isActive : Boolean) : Boolean = { + active = Ready && isActive + Active + } + + def Timer : Long = math.max(0, initTimer) + + def Timer_=(time : Long) : Long = { + initTimer = math.max(0, time) + Timer + } + + def MaxTimer : Long = implantDef.Initialization + + def ActivationCharge : Int = Definition.ActivationCharge + + /** + * Calculate the stamina consumption of the implant for any given moment of being active after its activation. + * As implant energy use can be influenced by both exo-suit worn and general stance held, both are considered. + * @param suit the exo-suit being worn + * @param stance the player's stance + * @return the amount of stamina (energy) that is consumed + */ + def Charge(suit : ExoSuitType.Value, stance : Stance.Value) : Int = { + if(active) { + implantDef.DurationChargeBase + implantDef.DurationChargeByExoSuit(suit) + implantDef.DurationChargeByStance(stance) + } + else { + 0 + } + } + + /** + * Place an implant back in its initializing state. + */ + def Reset() : Unit = { + Active = false + Timer = MaxTimer + } + + /** + * Place an implant back in its pre-initialization state. + * The implant is inactive and can not proceed to a `Ready` condition naturally from this state. + */ + def Jammed() : Unit = { + Active = false + Timer = -1 + } + + def Definition : ImplantDefinition = implantDef +} + +object Implant { + def default : Implant = new Implant(ImplantDefinition(ImplantType.RangeMagnifier)) + + def apply(implantDef : ImplantDefinition) : Implant = { + new Implant(implantDef) + } +} diff --git a/common/src/main/scala/net/psforever/objects/ImplantSlot.scala b/common/src/main/scala/net/psforever/objects/ImplantSlot.scala new file mode 100644 index 000000000..00951f503 --- /dev/null +++ b/common/src/main/scala/net/psforever/objects/ImplantSlot.scala @@ -0,0 +1,62 @@ +// Copyright (c) 2017 PSForever +package net.psforever.objects + +import net.psforever.objects.definition.ImplantDefinition +import net.psforever.types.ImplantType + +/** + * A slot "on the player" into which an implant is installed.
+ *
+ * In total, players have three implant slots. + * At battle rank one (BR1), however, all of those slots are locked. + * The player earns implants at BR16, BR12, and BR18. + * A locked implant slot can not be used. + * (The code uses "not yet unlocked" logic.) + * When unlocked, an implant may be installed into that slot.
+ *
+ * The default implant that the underlying slot utilizes is the "Range Magnifier." + * Until the `Installed` condition is some value other than `None`, however, the implant in the slot will not work. + */ +class ImplantSlot { + /** is this slot available for holding an implant */ + private var unlocked : Boolean = false + /** what implant is currently installed in this slot; None if there is no implant currently installed */ + private var installed : Option[ImplantType.Value] = None + /** the entry for that specific implant used by the a player; always occupied by some type of implant */ + private var implant : Implant = ImplantSlot.default + + def Unlocked : Boolean = unlocked + + def Unlocked_=(lock : Boolean) : Boolean = { + unlocked = lock + Unlocked + } + + def Installed : Option[ImplantType.Value] = installed + + def Implant : Option[Implant] = if(Installed.isDefined) { Some(implant) } else { None } + + def Implant_=(anImplant : Option[Implant]) : Option[Implant] = { + anImplant match { + case Some(module) => + Implant = module + case None => + installed = None + } + Implant + } + + def Implant_=(anImplant : Implant) : Option[Implant] = { + implant = anImplant + installed = Some(anImplant.Definition.Type) + Implant + } +} + +object ImplantSlot { + private val default = new Implant(ImplantDefinition(ImplantType.RangeMagnifier)) + + def apply() : ImplantSlot = { + new ImplantSlot() + } +} diff --git a/common/src/main/scala/net/psforever/objects/InfantryLoadout.scala b/common/src/main/scala/net/psforever/objects/InfantryLoadout.scala new file mode 100644 index 000000000..ac1c27df7 --- /dev/null +++ b/common/src/main/scala/net/psforever/objects/InfantryLoadout.scala @@ -0,0 +1,265 @@ +// Copyright (c) 2017 PSForever +package net.psforever.objects + +import net.psforever.objects.definition._ +import net.psforever.objects.equipment.Equipment +import net.psforever.objects.inventory.InventoryItem +import net.psforever.types.ExoSuitType + +import scala.annotation.tailrec + +/** + * From a `Player` their current exo-suit and their `Equipment`, retain a set of instructions to reconstruct this arrangement.
+ *
+ * `InfantryLoadout` objects are composed of the following information, as if a blueprint:
+ * - the avatar's current exo-suit
+ * - the type of specialization, called a "subtype" (mechanized assault exo-suits only)
+ * - the contents of the avatar's occupied holster slots
+ * - the contents of the avatar's occupied inventory
+ * `Equipment` contents of the holsters and of the formal inventory region will be condensed into a simplified form. + * These are also "blueprints." + * At its most basic, this simplification will merely comprise the former object's `EquipmentDefinition`. + * For items that are already simple - `Kit` objects and `SimpleItem` objects - this form will not be too far removed. + * For more complicated affairs like `Tool` objects and `AmmoBox` objects, only essential information will be retained.
+ *
+ * The deconstructed blueprint can be applied to any avatar. + * They are, however, typically tied to unique users and unique characters. + * For reasons of certifications, however, permissions on that avatar may affect what `Equipment` can be distributed. + * Even a whole blueprint can be denied if the user lacks the necessary exo-suit certification. + * A completely new piece of `Equipment` is constructed when the `Loadout` is regurgitated.
+ *
+ * The fifth tab on an `order_terminal` window is for "Favorite" blueprints for `InfantryLoadout` entries. + * The ten-long list is initialized with `FavoritesMessage` packets. + * Specific entries are loaded or removed using `FavoritesRequest` packets. + * @param player the player + * @param label the name by which this inventory will be known when displayed in a Favorites list + */ +class InfantryLoadout(player : Player, private val label : String) { + /** the exo-suit */ + private val exosuit : ExoSuitType.Value = player.ExoSuit + /** the MAX specialization, to differentiate the three types of MAXes who all use the same exo-suit name */ + private val subtype = + if(exosuit == ExoSuitType.MAX) { + import net.psforever.packet.game.objectcreate.ObjectClass + player.Holsters().head.Equipment.get.Definition.ObjectId match { + case ObjectClass.trhev_dualcycler | ObjectClass.nchev_scattercannon | ObjectClass.vshev_quasar => + 1 + case ObjectClass.trhev_pounder | ObjectClass.nchev_falcon | ObjectClass.vshev_comet => + 2 + case ObjectClass.trhev_burster | ObjectClass.nchev_sparrow | ObjectClass.vshev_starfire => + 3 + case _ => + 0 + } + } + else { + 0 + } + /** simplified representation of the holster `Equipment` */ + private val holsters : List[InfantryLoadout.SimplifiedEntry] = + InfantryLoadout.packageSimplifications(player.Holsters()) + /** simplified representation of the inventory `Equipment` */ + private val inventory : List[InfantryLoadout.SimplifiedEntry] = + InfantryLoadout.packageSimplifications(player.Inventory.Items.values.toList) + + /** + * The label by which this `InfantryLoadout` is called. + * @return the label + */ + def Label : String = label + + /** + * The exo-suit in which the avatar will be dressed. + * Might be restricted and, thus, restrict the rest of the `Equipment` from being constructed and given. + * @return the exo-suit + */ + def ExoSuit : ExoSuitType.Value = exosuit + + /** + * The mechanized assault exo-suit specialization number that indicates whether the MAX performs: + * anti-infantry (1), + * anti-vehicular (2), + * or anti-air work (3). + * The major distinction is the type of arm weapons that MAX is equipped. + * When the blueprint doesn't call for a MAX, the number will be 0. + * @return the specialization number + */ + def Subtype : Int = subtype + + /** + * The `Equipment` in the `Player`'s holster slots when this `InfantryLoadout` is created. + * @return a `List` of the holster item blueprints + */ + def Holsters : List[InfantryLoadout.SimplifiedEntry] = holsters + + /** + * The `Equipment` in the `Player`'s inventory region when this `InfantryLoadout` is created. + * @return a `List` of the inventory item blueprints + */ + def Inventory : List[InfantryLoadout.SimplifiedEntry] = inventory +} + +object InfantryLoadout { + /** + * A basic `Trait` connecting all of the `Equipment` blueprints. + */ + sealed trait Simplification + + /** + * An entry in the `InfantryLoadout`, wrapping around a slot index and what is in the slot index. + * @param item the `Equipment` + * @param index the slot number where the `Equipment` is to be stowed + * @see `InventoryItem` + */ + final case class SimplifiedEntry(item: Simplification, index: Int) + + /** + * The simplified form of an `AmmoBox`. + * @param adef the `AmmoBoxDefinition` that describes this future object + * @param capacity the amount of ammunition, if any, to initialize; + * if `None`, then the previous `AmmoBoxDefinition` will be referenced for the amount later + */ + final case class ShorthandAmmoBox(adef : AmmoBoxDefinition, capacity : Int) extends Simplification + /** + * The simplified form of a `Tool`. + * @param tdef the `ToolDefinition` that describes this future object + * @param ammo the blueprints to construct the correct number of ammunition slots in the `Tool` + */ + final case class ShorthandTool(tdef : ToolDefinition, ammo : List[ShorthandAmmotSlot]) extends Simplification + /** + * The simplified form of a `Tool` `FireMode` + * @param ammoIndex the index that points to the type of ammunition this slot currently uses + * @param ammo a `ShorthandAmmoBox` object to load into that slot + */ + final case class ShorthandAmmotSlot(ammoIndex : Int, ammo : ShorthandAmmoBox) + /** + * The simplified form of a `ConstructionItem`. + * @param cdef the `ConstructionItemDefinition` that describes this future object + */ + final case class ShorthandConstructionItem(cdef : ConstructionItemDefinition) extends Simplification + /** + * The simplified form of a `SimpleItem`. + * @param sdef the `SimpleItemDefinition` that describes this future object + */ + final case class ShorthandSimpleItem(sdef : SimpleItemDefinition) extends Simplification + /** + * The simplified form of a `Kit`. + * @param kdef the `KitDefinition` that describes this future object + */ + final case class ShorthandKit(kdef : KitDefinition) extends Simplification + + /** + * Overloaded entry point for constructing simplified blueprints from holster slot equipment. + * @param equipment the holster slots + * @return a `List` of simplified `Equipment` + */ + private def packageSimplifications(equipment : Array[EquipmentSlot]) : List[SimplifiedEntry] = { + recursiveHolsterSimplifications(equipment.iterator) + } + + /** + * Overloaded entry point for constructing simplified blueprints from inventory region equipment. + * @param equipment the enumerated contents of the inventory + * @return a `List` of simplified `Equipment` + */ + private def packageSimplifications(equipment : List[InventoryItem]) : List[SimplifiedEntry] = { + recursiveInventorySimplifications(equipment.iterator) + } + + /** + * Traverse a `Player`'s holsters and transform occupied slots into simplified blueprints for the contents of that slot. + * The holsters are fixed positions and can be unoccupied. + * Only occupied holsters are transformed into blueprints. + * The `index` field is necessary as the `Iterator` for the holsters lacks self-knowledge about slot position. + * @param iter an `Iterator` + * @param index the starting index; + * defaults to 0 and increments automatically + * @param list an updating `List` of simplified `Equipment` blueprints; + * empty, by default + * @return a `List` of simplified `Equipment` blueprints + */ + @tailrec private def recursiveHolsterSimplifications(iter : Iterator[EquipmentSlot], index : Int = 0, list : List[SimplifiedEntry] = Nil) : List[SimplifiedEntry] = { + if(!iter.hasNext) { + list + } + else { + val entry = iter.next + entry.Equipment match { + case Some(obj) => + recursiveHolsterSimplifications(iter, index + 1, list :+ SimplifiedEntry(buildSimplification(obj), index)) + case None => + recursiveHolsterSimplifications(iter, index + 1, list) + } + } + } + + /** + * Traverse a `Player`'s inventory and transform `Equipment` into simplified blueprints. + * @param iter an `Iterator` + * @param list an updating `List` of simplified `Equipment` blueprints; + * empty, by default + * @return a `List` of simplified `Equipment` blueprints + */ + @tailrec private def recursiveInventorySimplifications(iter : Iterator[InventoryItem], list : List[SimplifiedEntry] = Nil) : List[SimplifiedEntry] = { + if(!iter.hasNext) { + list + } + else { + val entry = iter.next + recursiveInventorySimplifications(iter, list :+ SimplifiedEntry(buildSimplification(entry.obj), entry.start)) + } + } + + /** + * Ammunition slots are internal connection points where `AmmoBox` units and their characteristics represent a `Tool`'s magazine. + * Their simplification process has a layer of complexity that ensures that the content of the slot matches the type of content that should be in the slot. + * If it does not, it extracts information about the slot from the `EquipmentDefinition` and sets the blueprints to that. + * @param iter an `Iterator` + * @param list an updating `List` of simplified ammo slot blueprints; + * empty, by default + * @return a `List` of simplified ammo slot blueprints + * @see `Tool.FireModeSlot` + */ + @tailrec private def recursiveFireModeSimplications(iter : Iterator[Tool.FireModeSlot], list : List[ShorthandAmmotSlot] = Nil) : List[ShorthandAmmotSlot] = { + if(!iter.hasNext) { + list + } + else { + val entry = iter.next + val fmodeSimp = if(entry.Box.AmmoType == entry.AmmoType) { + ShorthandAmmotSlot( + entry.AmmoTypeIndex, + ShorthandAmmoBox(entry.Box.Definition, entry.Box.Capacity) + ) + } + else { + ShorthandAmmotSlot( + entry.AmmoTypeIndex, + ShorthandAmmoBox(AmmoBoxDefinition(entry.Tool.Definition.AmmoTypes(entry.Definition.AmmoTypeIndices.head).id), 1) + ) + } + recursiveFireModeSimplications(iter, list :+ fmodeSimp) + } + } + + /** + * Accept a piece of `Equipment` and transform it into a simplified blueprint. + * @param obj the `Equipment` + * @return the simplified blueprint + */ + private def buildSimplification(obj : Equipment) : Simplification = { + obj match { + case obj : Tool => + val flist = recursiveFireModeSimplications(obj.AmmoSlots.iterator) + ShorthandTool(obj.Definition, flist) + case obj : AmmoBox => + ShorthandAmmoBox(obj.Definition, obj.Capacity) + case obj : ConstructionItem => + ShorthandConstructionItem(obj.Definition) + case obj : SimpleItem => + ShorthandSimpleItem(obj.Definition) + case obj : Kit => + ShorthandKit(obj.Definition) + } + } +} diff --git a/common/src/main/scala/net/psforever/objects/Kit.scala b/common/src/main/scala/net/psforever/objects/Kit.scala new file mode 100644 index 000000000..e8c89d83d --- /dev/null +++ b/common/src/main/scala/net/psforever/objects/Kit.scala @@ -0,0 +1,26 @@ +// Copyright (c) 2017 PSForever +package net.psforever.objects + +import net.psforever.objects.definition.KitDefinition +import net.psforever.objects.equipment.Equipment + +/** + * A one-time-use recovery item that can be applied by the player while held within their inventory. + * @param kitDef the `ObjectDefinition` that constructs this item and maintains some of its immutable fields + */ +class Kit(private val kitDef : KitDefinition) extends Equipment { + def Definition : KitDefinition = kitDef +} + +object Kit { + def apply(kitDef : KitDefinition) : Kit = { + new Kit(kitDef) + } + + import net.psforever.packet.game.PlanetSideGUID + def apply(guid : PlanetSideGUID, kitDef : KitDefinition) : Kit = { + val obj = new Kit(kitDef) + obj.GUID = guid + obj + } +} diff --git a/common/src/main/scala/net/psforever/objects/LivePlayerList.scala b/common/src/main/scala/net/psforever/objects/LivePlayerList.scala new file mode 100644 index 000000000..c0ad0e8f3 --- /dev/null +++ b/common/src/main/scala/net/psforever/objects/LivePlayerList.scala @@ -0,0 +1,184 @@ +// Copyright (c) 2017 PSForever +package net.psforever.objects + +import net.psforever.packet.game.PlanetSideGUID + +import scala.collection.concurrent.{Map, TrieMap} + +/** + * See the companion object for class and method documentation. + * `LivePlayerList` is a singleton and this private class lacks exposure. + */ +private class LivePlayerList { + /** key - the session id; value - a `Player` object */ + private val sessionMap : Map[Long, Player] = new TrieMap[Long, Player] + /** key - the global unique identifier; value - the session id */ + private val playerMap : Map[Int, Long] = new TrieMap[Int, Long] + + def WorldPopulation(predicate : ((_, Player)) => Boolean) : List[Player] = { + sessionMap.filter(predicate).map({ case(_, char) => char }).toList + } + + def Add(sessionId : Long, player : Player) : Boolean = { + sessionMap.values.find(char => char.equals(player)) match { + case None => + sessionMap.putIfAbsent(sessionId, player).isEmpty + true + case Some(_) => + false + } + } + + def Remove(sessionId : Long) : Option[Player] = { + sessionMap.remove(sessionId) match { + case Some(char) => + playerMap.find({ case(_, sess) => sess == sessionId }) match { + case Some((guid, _)) => + playerMap.remove(guid) + case None => ; + } + Some(char) + case None => + None + } + } + + def Get(guid : PlanetSideGUID) : Option[Player] = { + Get(guid.guid) + } + + def Get(guid : Int) : Option[Player] = { + playerMap.get(guid) match { + case Some(sess) => + sessionMap.get(sess) + case _ => + None + } + } + + def Assign(sessionId : Long, guid : PlanetSideGUID) : Boolean = Assign(sessionId, guid.guid) + + def Assign(sessionId : Long, guid : Int) : Boolean = { + sessionMap.find({ case(sess, _) => sess == sessionId}) match { + case Some((_, char)) => + if(char.GUID.guid == guid) { + playerMap.find({ case(_, sess) => sess == sessionId }) match { + case Some((id, _)) => + playerMap.remove(id) + case None => ; + } + playerMap.put(guid, sessionId) + true + } + else { + false + } + + case None => + false + } + } + + def Shutdown : List[Player] = { + val list = sessionMap.values.toList + sessionMap.clear + playerMap.clear + list + } +} + +/** + * A class for storing `Player` mappings for users that are currently online. + * The mapping system is tightly coupled between the `Player` class and to an instance of `WorldSessionActor`. + * A loose coupling between the current globally unique identifier (GUID) and the user is also present.
+ *
+ * Use:
+ * 1) When a users logs in during `WorldSessionActor`, associate that user's session id and the character.
+ *        `LivePlayerList.Add(session, player)`
+ * 2) When that user's chosen character is declared his avatar using `SetCurrentAvatarMessage`, + * also associate the user's session with their current GUID.
+ *        `LivePlayerList.Assign(session, guid)`
+ * 3) Repeat the previous step for as many times the user's GUID changes, especially during the aforementioned condition.
+ * 4a) In between the previous two steps, a user's character may be referenced by their current GUID.
+ *        `LivePlayerList.Get(guid)`
+ * 4b) Also in between those same previous steps, a range of characters may be queried based on provided statistics.
+ *        `LivePlayerList.WorldPopulation(...)`
+ * 5) When the user leaves the game, his character's entries are removed from the mappings.
+ *        `LivePlayerList.Remove(session)` + */ +object LivePlayerList { + /** As `LivePlayerList` is a singleton, an object of `LivePlayerList` is automatically instantiated. */ + private val Instance : LivePlayerList = new LivePlayerList + + /** + * Given some criteria, examine the mapping of user characters and find the ones that fulfill the requirements.
+ *
+ * Note the signature carefully. + * A two-element tuple is checked, but only the second element of that tuple - a character - is eligible for being queried. + * The first element is ignored. + * Even a predicate as simple as `{ case ((x : Long, _)) => x > 0 }` will not work for that reason. + * @param predicate the conditions for filtering the live `Player`s + * @return a list of users's `Player`s that fit the criteria + */ + def WorldPopulation(predicate : ((_, Player)) => Boolean) : List[Player] = Instance.WorldPopulation(predicate) + + /** + * Create a mapped entry between the user's session and a user's character. + * Neither the player nor the session may exist in the current mappings if this is to work. + * @param sessionId the session + * @param player the character + * @return `true`, if the session was association was made; `false`, otherwise + */ + def Add(sessionId : Long, player : Player) : Boolean = Instance.Add(sessionId, player) + + /** + * Remove all entries related to the given session identifier from the mappings. + * The player no longer counts as "online." + * This function cleans up __all__ associations - those created by `Add`, and those created by `Assign`. + * @param sessionId the session + * @return any character that was afffected by the mapping removal + */ + def Remove(sessionId : Long) : Option[Player] = Instance.Remove(sessionId) + + /** + * Get a user's character from the mappings. + * @param guid the current GUID of the character + * @return the character, if it can be found using the GUID + */ + def Get(guid : PlanetSideGUID) : Option[Player] = Instance.Get(guid) + + /** + * Get a user's character from the mappings. + * @param guid the current GUID of the character + * @return the character, if it can be found using the GUID + */ + def Get(guid : Int) : Option[Player] = Instance.Get(guid) + + /** + * Given a session that maps to a user's character, create a mapping between the character's current GUID and the session. + * If the user already has a GUID in the mappings, remove it and assert the new one. + * @param sessionId the session + * @param guid the GUID to associate with the character; + * technically, it has already been assigned and should be findable using `{character}.GUID.guid` + * @return `true`, if the mapping was created; + * `false`, if the session can not be found or if the character's GUID doesn't match the one provided + */ + def Assign(sessionId : Long, guid : PlanetSideGUID) : Boolean = Instance.Assign(sessionId, guid) + + /** + * Given a session that maps to a user's character, create a mapping between the character's current GUID and the session. + * If the user already has a GUID in the mappings, remove it and assert the new one. + * @param sessionId the session + * @param guid the GUID to associate with the character; + * technically, it has already been assigned and should be findable using `{character}.GUID.guid` + * @return `true`, if the mapping was created; + * `false`, if the session can not be found or if the character's GUID doesn't match the one provided + */ + def Assign(sessionId : Long, guid : Int) : Boolean = Instance.Assign(sessionId, guid) + + /** + * Hastily remove all mappings and ids. + * @return an unsorted list of the characters that were still online + */ + def Shutdown : List[Player] = Instance.Shutdown +} diff --git a/common/src/main/scala/net/psforever/objects/LockerContainer.scala b/common/src/main/scala/net/psforever/objects/LockerContainer.scala new file mode 100644 index 000000000..0e5357839 --- /dev/null +++ b/common/src/main/scala/net/psforever/objects/LockerContainer.scala @@ -0,0 +1,34 @@ +// Copyright (c) 2017 PSForever +package net.psforever.objects + +import net.psforever.objects.definition.EquipmentDefinition +import net.psforever.objects.definition.converter.LockerContainerConverter +import net.psforever.objects.equipment.{Equipment, EquipmentSize} +import net.psforever.objects.inventory.GridInventory + +class LockerContainer extends Equipment { + private val inventory = GridInventory() //? + + def Inventory : GridInventory = inventory + + def Fit(obj : Equipment) : Option[Int] = inventory.Fit(obj.Definition.Tile) + + def Definition : EquipmentDefinition = new EquipmentDefinition(456) { + Name = "locker container" + Size = EquipmentSize.Inventory + Packet = new LockerContainerConverter() + } +} + +object LockerContainer { + def apply() : LockerContainer = { + new LockerContainer() + } + + import net.psforever.packet.game.PlanetSideGUID + def apply(guid : PlanetSideGUID) : LockerContainer = { + val obj = new LockerContainer() + obj.GUID = guid + obj + } +} diff --git a/common/src/main/scala/net/psforever/objects/OffhandEquipmentSlot.scala b/common/src/main/scala/net/psforever/objects/OffhandEquipmentSlot.scala new file mode 100644 index 000000000..6ac7ebb0a --- /dev/null +++ b/common/src/main/scala/net/psforever/objects/OffhandEquipmentSlot.scala @@ -0,0 +1,20 @@ +// Copyright (c) 2017 PSForever +package net.psforever.objects + +import net.psforever.objects.equipment.EquipmentSize + +/** + * A size-checked unit of storage (or mounting) for `Equipment`. + * Unlike conventional `EquipmentSlot` space, this size of allowable `Equipment` is fixed. + * @param size the permanent size of the `Equipment` allowed in this slot + */ +class OffhandEquipmentSlot(size : EquipmentSize.Value) extends EquipmentSlot { + super.Size_=(size) + + /** + * Not allowed to change the slot size manually. + * @param assignSize the changed in capacity for this slot + * @return the capacity for this slot + */ + override def Size_=(assignSize : EquipmentSize.Value) : EquipmentSize.Value = Size +} \ No newline at end of file diff --git a/common/src/main/scala/net/psforever/objects/PlanetSideGameObject.scala b/common/src/main/scala/net/psforever/objects/PlanetSideGameObject.scala new file mode 100644 index 000000000..4e781c092 --- /dev/null +++ b/common/src/main/scala/net/psforever/objects/PlanetSideGameObject.scala @@ -0,0 +1,47 @@ +// Copyright (c) 2017 PSForever +package net.psforever.objects + +import net.psforever.objects.definition.ObjectDefinition +import net.psforever.objects.entity.{IdentifiableEntity, SimpleWorldEntity, WorldEntity} +import net.psforever.types.Vector3 + +/** + * A basic class that indicates an entity that exists somewhere in the world and has a globally unique identifier. + */ +abstract class PlanetSideGameObject extends IdentifiableEntity with WorldEntity { + private var entity : WorldEntity = new SimpleWorldEntity() + + def Entity : WorldEntity = entity + + def Entity_=(newEntity : WorldEntity) : Unit = { + entity = newEntity + } + + def Position : Vector3 = Entity.Position + + def Position_=(vec : Vector3) : Vector3 = { + Entity.Position = vec + } + + def Orientation : Vector3 = Entity.Orientation + + def Orientation_=(vec : Vector3) : Vector3 = { + Entity.Orientation = vec + } + + def Velocity : Option[Vector3] = Entity.Velocity + + def Velocity_=(vec : Option[Vector3]) : Option[Vector3] = { + Entity.Velocity = vec + } + + def Definition : ObjectDefinition +} + +object PlanetSideGameObject { + def toString(obj : PlanetSideGameObject) : String = { + val guid : String = try { obj.GUID.guid.toString } catch { case _ : Exception => "NOGUID" } + val P = obj.Position + s"[$guid](x,y,z=${P.x%.3f},${P.y%.3f},${P.z%.3f})" + } +} diff --git a/common/src/main/scala/net/psforever/objects/Player.scala b/common/src/main/scala/net/psforever/objects/Player.scala new file mode 100644 index 000000000..85e830e75 --- /dev/null +++ b/common/src/main/scala/net/psforever/objects/Player.scala @@ -0,0 +1,590 @@ +// Copyright (c) 2017 PSForever +package net.psforever.objects + +import net.psforever.objects.definition.AvatarDefinition +import net.psforever.objects.equipment.{Equipment, EquipmentSize} +import net.psforever.objects.inventory.{GridInventory, InventoryItem} +import net.psforever.packet.game.PlanetSideGUID +import net.psforever.types._ + +import scala.annotation.tailrec + +class Player(private val name : String, + private val faction : PlanetSideEmpire.Value, + private val sex : CharacterGender.Value, + private val voice : Int, + private val head : Int + ) extends PlanetSideGameObject { + private var alive : Boolean = false + private var backpack : Boolean = false + private var health : Int = 0 + private var stamina : Int = 0 + private var armor : Int = 0 + private var maxHealth : Int = 100 //TODO affected by empire benefits, territory benefits, and bops + private var maxStamina : Int = 100 //does anything affect this? + + private var exosuit : ExoSuitType.Value = ExoSuitType.Standard + private val freeHand : EquipmentSlot = new OffhandEquipmentSlot(EquipmentSize.Any) + private val holsters : Array[EquipmentSlot] = Array.fill[EquipmentSlot](5)(new EquipmentSlot) + private val fifthSlot : EquipmentSlot = new OffhandEquipmentSlot(EquipmentSize.Inventory) + private val inventory : GridInventory = GridInventory() + private var drawnSlot : Int = Player.HandsDownSlot + private var lastDrawnSlot : Int = 0 + + private val loadouts : Array[Option[InfantryLoadout]] = Array.fill[Option[InfantryLoadout]](10)(None) + + private val implants : Array[ImplantSlot] = Array.fill[ImplantSlot](3)(new ImplantSlot) + +// private var tosRibbon : MeritCommendation.Value = MeritCommendation.None +// private var upperRibbon : MeritCommendation.Value = MeritCommendation.None +// private var middleRibbon : MeritCommendation.Value = MeritCommendation.None +// private var lowerRibbon : MeritCommendation.Value = MeritCommendation.None + + private var facingYawUpper : Float = 0f + private var crouching : Boolean = false + private var jumping : Boolean = false + private var cloaked : Boolean = false + private var backpackAccess : Option[PlanetSideGUID] = None + + private var sessionId : Long = 0 + private var admin : Boolean = false + private var spectator : Boolean = false + + private var vehicleSeated : Option[PlanetSideGUID] = None + private var vehicleOwned : Option[PlanetSideGUID] = None + + private var continent : String = "home2" //actually, the zoneId + private var playerDef : AvatarDefinition = Player.definition + + //SouNourS things + /** Last medkituse. */ + var lastMedkit : Long = 0 + var death_by : Int = 0 + var doors : Array[Int] = Array.ofDim(120) + var doorsTime : Array[Long] = Array.ofDim(120) + var lastSeenStreamMessage : Array[Long] = Array.fill[Long](65535)(0L) + var lastShotSeq_time : Int = -1 + /** The player is shooting. */ + var shooting : Boolean = false + /** From PlanetsideAttributeMessage */ + var PlanetsideAttribute : Array[Long] = Array.ofDim(120) + + Player.SuitSetup(this, ExoSuit) + + def Name : String = name + + def Faction : PlanetSideEmpire.Value = faction + + def Sex : CharacterGender.Value = sex + + def Voice : Int = voice + + def Head : Int = head + + def isAlive : Boolean = alive + + def isBackpack : Boolean = backpack + + def Spawn : Boolean = { + if(!isAlive && !isBackpack) { + alive = true + Health = MaxHealth + Stamina = MaxStamina + Armor = MaxArmor + ResetAllImplants() + } + isAlive + } + + def Die : Boolean = { + alive = false + Health = 0 + Stamina = 0 + false + } + + def Release : Boolean = { + if(!isAlive) { + backpack = true + true + } + else { + false + } + } + + def Health : Int = health + + def Health_=(assignHealth : Int) : Int = { + health = if(isAlive) { math.min(math.max(0, assignHealth), MaxHealth) } else { 0 } + Health + } + + def MaxHealth : Int = maxHealth + + def MaxHealth_=(max : Int) : Int = { + maxHealth = math.min(math.max(0, max), 65535) + MaxHealth + } + + def Stamina : Int = stamina + + def Stamina_=(assignEnergy : Int) : Int = { + stamina = if(isAlive) { math.min(math.max(0, assignEnergy), MaxStamina) } else { 0 } + Stamina + } + + def MaxStamina : Int = maxStamina + + def MaxStamina_=(max : Int) : Int = { + maxStamina = math.min(math.max(0, max), 65535) + MaxStamina + } + + def Armor : Int = armor + + def Armor_=(assignArmor : Int) : Int = { + armor = if(isAlive) { math.min(math.max(0, assignArmor), MaxArmor) } else { 0 } + Armor + } + + def MaxArmor : Int = ExoSuitDefinition.Select(exosuit).MaxArmor + + def Slot(slot : Int) : EquipmentSlot = { + if(inventory.Offset <= slot && slot <= inventory.LastIndex) { + inventory.Slot(slot) + } + else if(slot > -1 && slot < 5) { + holsters(slot) + } + else if(slot == 5) { + fifthSlot + } + else if(slot == Player.FreeHandSlot) { + freeHand + } + else { + new OffhandEquipmentSlot(EquipmentSize.Blocked) + } + } + + def Holsters() : Array[EquipmentSlot] = holsters + + def Inventory : GridInventory = inventory + + def Fit(obj : Equipment) : Option[Int] = { + recursiveHolsterFit(holsters.iterator, obj.Size) match { + case Some(index) => + Some(index) + case None => + inventory.Fit(obj.Definition.Tile) match { + case Some(index) => + Some(index) + case None => + if(freeHand.Equipment.isDefined) { None } else { Some(Player.FreeHandSlot) } + } + } + } + + @tailrec private def recursiveHolsterFit(iter : Iterator[EquipmentSlot], objSize : EquipmentSize.Value, index : Int = 0) : Option[Int] = { + if(!iter.hasNext) { + None + } + else { + val slot = iter.next + if(slot.Equipment.isEmpty && slot.Size.equals(objSize)) { + Some(index) + } + else { + recursiveHolsterFit(iter, objSize, index + 1) + } + } + } + + def Equip(slot : Int, obj : Equipment) : Boolean = { + if(-1 < slot && slot < 5) { + holsters(slot).Equipment = obj + true + } + else if(slot == Player.FreeHandSlot) { + freeHand.Equipment = obj + true + } + else { + inventory += slot -> obj + } + } + + def FreeHand = freeHand + + def FreeHand_=(item : Option[Equipment]) : Option[Equipment] = { + if(freeHand.Equipment.isEmpty || item.isEmpty) { + freeHand.Equipment = item + } + FreeHand.Equipment + } + + def SaveLoadout(label : String, line : Int) : Unit = { + loadouts(line) = Some(new InfantryLoadout(this, label)) + } + + def LoadLoadout(line : Int) : Option[InfantryLoadout] = loadouts(line) + + def DeleteLoadout(line : Int) : Unit = { + loadouts(line) = None + } + + def Find(obj : Equipment) : Option[Int] = Find(obj.GUID) + + def Find(guid : PlanetSideGUID) : Option[Int] = { + findInHolsters(holsters.iterator, guid) match { + case Some(index) => + Some(index) + case None => + findInInventory(inventory.Items.values.iterator, guid) match { + case Some(index) => + Some(index) + case None => + if(freeHand.Equipment.isDefined && freeHand.Equipment.get.GUID == guid) { + Some(Player.FreeHandSlot) + } + else { + None + } + } + } + } + + @tailrec private def findInHolsters(iter : Iterator[EquipmentSlot], guid : PlanetSideGUID, index : Int = 0) : Option[Int] = { + if(!iter.hasNext) { + None + } + else { + val slot = iter.next + if(slot.Equipment.isDefined && slot.Equipment.get.GUID == guid) { + Some(index) + } + else { + findInHolsters(iter, guid, index + 1) + } + } + } + + @tailrec private def findInInventory(iter : Iterator[InventoryItem], guid : PlanetSideGUID) : Option[Int] = { + if(!iter.hasNext) { + None + } + else { + val item = iter.next + if(item.obj.GUID == guid) { + Some(item.start) + } + else { + findInInventory(iter, guid) + } + } + } + + def DrawnSlot : Int = drawnSlot + + def DrawnSlot_=(slot : Int = Player.HandsDownSlot) : Int = { + if(slot != drawnSlot) { + val origDrawnSlot : Int = drawnSlot + if(slot == Player.HandsDownSlot) { + drawnSlot = slot + } + else if(-1 < slot && slot < 5 && holsters(slot).Equipment.isDefined) { + drawnSlot = slot + } + lastDrawnSlot = if(-1 < origDrawnSlot && origDrawnSlot < 5) { origDrawnSlot } else { lastDrawnSlot } + } + DrawnSlot + } + + def LastDrawnSlot : Int = lastDrawnSlot + + def ExoSuit : ExoSuitType.Value = exosuit + + def ExoSuit_=(suit : ExoSuitType.Value) : Unit = { + exosuit = suit + } + + def Implants : Array[ImplantSlot] = implants + + def Implant(slot : Int) : Option[ImplantType.Value] = { + if(-1 < slot && slot < implants.length) { implants(slot).Installed } else { None } + } + + def Implant(implantType : ImplantType.Value) : Option[Implant] = { + implants.find(_.Installed.contains(implantType)) match { + case Some(slot) => + slot.Implant + case None => + None + } + } + + def InstallImplant(implant : Implant) : Boolean = { + getAvailableImplantSlot(implants.iterator, implant.Definition.Type) match { + case Some(slot) => + slot.Implant = implant + slot.Implant.get.Reset() + true + case None => + false + } + } + + @tailrec private def getAvailableImplantSlot(iter : Iterator[ImplantSlot], implantType : ImplantType.Value) : Option[ImplantSlot] = { + if(!iter.hasNext) { + None + } + else { + val slot = iter.next + if(!slot.Unlocked || slot.Installed.contains(implantType)) { + None + } + else if(slot.Installed.isEmpty) { + Some(slot) + } + else { + getAvailableImplantSlot(iter, implantType) + } + } + } + + def UninstallImplant(implantType : ImplantType.Value) : Boolean = { + implants.find({slot => slot.Installed.contains(implantType)}) match { + case Some(slot) => + slot.Implant = None + true + case None => + false + } + } + + def ResetAllImplants() : Unit = { + implants.foreach(slot => { + slot.Implant match { + case Some(implant) => + implant.Reset() + case None => ; + } + }) + } + + def FacingYawUpper : Float = facingYawUpper + + def FacingYawUpper_=(facing : Float) : Float = { + facingYawUpper = facing + FacingYawUpper + } + + def Crouching : Boolean = crouching + + def Crouching_=(crouched : Boolean) : Boolean = { + crouching = crouched + Crouching + } + + def Jumping : Boolean = jumping + + def Jumping_=(jumped : Boolean) : Boolean = { + jumping = jumped + Jumping + } + + def Cloaked : Boolean = jumping + + def Cloaked_=(isCloaked : Boolean) : Boolean = { + cloaked = isCloaked + Cloaked + } + + def AccessingBackpack : Option[PlanetSideGUID] = backpackAccess + + def AccessingBackpack_=(guid : PlanetSideGUID) : Option[PlanetSideGUID] = { + AccessingBackpack = Some(guid) + } + + /** + * Change which player has access to the backpack of this player. + * A player may only access to the backpack of a dead released player, and only if no one else has access at the moment. + * @param guid the player who wishes to access the backpack + * @return the player who is currently allowed to access the backpack + */ + def AccessingBackpack_=(guid : Option[PlanetSideGUID]) : Option[PlanetSideGUID] = { + guid match { + case None => + backpackAccess = None + case Some(player) => + if(isBackpack && backpackAccess.isEmpty) { + backpackAccess = Some(player) + } + } + AccessingBackpack + } + + /** + * Can the other `player` access the contents of this `Player`'s backpack? + * @param player a player attempting to access this backpack + * @return `true`, if the `player` is permitted access; `false`, otherwise + */ + def CanAccessBackpack(player : Player) : Boolean = { + isBackpack && (backpackAccess.isEmpty || backpackAccess.contains(player.GUID)) + } + + def SessionId : Long = sessionId + + def Admin : Boolean = admin + + def Spectator : Boolean = spectator + + def Continent : String = continent + + def VehicleSeated : Option[PlanetSideGUID] = vehicleSeated + + def VehicleSeated_=(vehicle : Vehicle) : Option[PlanetSideGUID] = { + vehicleSeated = Some(vehicle.GUID) + VehicleSeated + } + + def VehicleSeated_=(guid : Option[PlanetSideGUID]) : Option[PlanetSideGUID] = { + vehicleSeated = guid + VehicleSeated + } + + def VehicleOwned : Option[PlanetSideGUID] = vehicleOwned + + def VehicleOwned_=(vehicle : Vehicle) : Option[PlanetSideGUID] = { + vehicleOwned = Some(vehicle.GUID) + VehicleOwned + } + + def VehicleOwned_=(guid : Option[PlanetSideGUID]) : Option[PlanetSideGUID] = { + vehicleOwned = guid + VehicleOwned + } + + def Continent_=(zoneId : String) : String = { + continent = zoneId + Continent + } + + def Definition : AvatarDefinition = playerDef + + override def toString : String = { + Player.toString(this) + } + + def canEqual(other: Any): Boolean = other.isInstanceOf[Player] + + override def equals(other : Any) : Boolean = other match { + case that: Player => + (that canEqual this) && + name == that.name && + faction == that.faction && + sex == that.sex && + voice == that.voice && + head == that.head + case _ => + false + } + + override def hashCode() : Int = { + val state = Seq(name, faction, sex, voice, head) + state.map(_.hashCode()).foldLeft(0)((a, b) => 31 * a + b) + } +} + +object Player { + final private val definition : AvatarDefinition = new AvatarDefinition(121) + final val FreeHandSlot : Int = 250 + final val HandsDownSlot : Int = 255 + + def apply(name : String, faction : PlanetSideEmpire.Value, sex : CharacterGender.Value, voice : Int, head : Int) : Player = { + new Player(name, faction, sex, voice, head) + } + + def apply(guid : PlanetSideGUID, name : String, faction : PlanetSideEmpire.Value, sex : CharacterGender.Value, voice : Int, head : Int) : Player = { + val obj = new Player(name, faction, sex, voice, head) + obj.GUID = guid + obj + } + + /** + * Change the type of `AvatarDefinition` is used to define the player. + * @param player the player + * @param avatarDef the player's new definition entry + * @return the changed player + */ + def apply(player : Player, avatarDef : AvatarDefinition) : Player = { + player.playerDef = avatarDef + player + } + + def apply(player : Player, sessId : Long) : Player = { + player.sessionId = sessId + player + } + + def SuitSetup(player : Player, eSuit : ExoSuitType.Value) : Unit = { + val esuitDef : ExoSuitDefinition = ExoSuitDefinition.Select(eSuit) + //exosuit + player.ExoSuit = eSuit + //inventory + player.Inventory.Clear() + player.Inventory.Resize(esuitDef.InventoryScale.width, esuitDef.InventoryScale.height) + player.Inventory.Offset = esuitDef.InventoryOffset + //holsters + (0 until 5).foreach(index => { player.Slot(index).Size = esuitDef.Holster(index) }) + } + + def ChangeSessionId(player : Player, session : Long) : Long = { + player.sessionId = session + player.SessionId + } + + def Administrate(player : Player, isAdmin : Boolean) : Player = { + player.admin = isAdmin + player + } + + def Spectate(player : Player, isSpectator : Boolean) : Player = { + player.spectator = isSpectator + player + } + + def Release(player : Player) : Player = { + if(player.Release) { + val obj = new Player(player.Name, player.Faction, player.Sex, player.Voice, player.Head) + obj.VehicleOwned = player.VehicleOwned + obj.Continent = player.Continent + //hand over loadouts + (0 until 10).foreach(index => { + obj.loadouts(index) = player.loadouts(index) + }) + //hand over implants + (0 until 3).foreach(index => { + if(obj.Implants(index).Unlocked = player.Implants(index).Unlocked) { + obj.Implants(index).Implant = player.Implants(index).Implant + } + }) + //hand over knife + obj.Slot(4).Equipment = player.Slot(4).Equipment + player.Slot(4).Equipment = None + //hand over ??? + obj.fifthSlot.Equipment = player.fifthSlot.Equipment + player.fifthSlot.Equipment = None + obj + } + else { + player + } + } + + def toString(obj : Player) : String = { + val name : String = if(obj.VehicleSeated.isDefined) { s"[${obj.name}, ${obj.VehicleSeated.get.guid}]" } else { obj.Name } + s"[player $name, ${obj.Faction} (${obj.Health}/${obj.MaxHealth})(${obj.Armor}/${obj.MaxArmor})]" + } +} diff --git a/common/src/main/scala/net/psforever/objects/SimpleItem.scala b/common/src/main/scala/net/psforever/objects/SimpleItem.scala new file mode 100644 index 000000000..75158c30b --- /dev/null +++ b/common/src/main/scala/net/psforever/objects/SimpleItem.scala @@ -0,0 +1,22 @@ +// Copyright (c) 2017 PSForever +package net.psforever.objects + +import net.psforever.objects.definition.SimpleItemDefinition +import net.psforever.objects.equipment.Equipment + +class SimpleItem(private val simpDef : SimpleItemDefinition) extends Equipment { + def Definition : SimpleItemDefinition = simpDef +} + +object SimpleItem { + def apply(simpDef : SimpleItemDefinition) : SimpleItem = { + new SimpleItem(simpDef) + } + + import net.psforever.packet.game.PlanetSideGUID + def apply(guid : PlanetSideGUID, simpDef : SimpleItemDefinition) : SimpleItem = { + val obj = new SimpleItem(simpDef) + obj.GUID = guid + obj + } +} diff --git a/common/src/main/scala/net/psforever/objects/Tool.scala b/common/src/main/scala/net/psforever/objects/Tool.scala new file mode 100644 index 000000000..85b2c484c --- /dev/null +++ b/common/src/main/scala/net/psforever/objects/Tool.scala @@ -0,0 +1,166 @@ +// Copyright (c) 2017 PSForever +package net.psforever.objects + +import net.psforever.objects.definition.{AmmoBoxDefinition, ToolDefinition} +import net.psforever.objects.equipment.{Ammo, Equipment, FireModeDefinition, FireModeSwitch} +import net.psforever.packet.game.PlanetSideGUID + +import scala.annotation.tailrec + +/** + * A type of utility that can be wielded and loaded with certain other game elements.
+ *
+ * "Tool" is a very mechanical name while this class is intended for various weapons and support items. + * The primary trait of a `Tool` is that it has something that counts as an "ammunition," + * depleted as the `Tool` is used, replaceable as long as one has an appropriate type of `AmmoBox` object. + * (The former is always called "consuming;" the latter, "reloading.")
+ *
+ * Some weapons Chainblade have ammunition but do not consume it. + * @param toolDef the `ObjectDefinition` that constructs this item and maintains some of its immutable fields + */ +class Tool(private val toolDef : ToolDefinition) extends Equipment with FireModeSwitch[FireModeDefinition] { + private var fireModeIndex : Int = 0 + private val ammoSlot : List[Tool.FireModeSlot] = Tool.LoadDefinition(this) + + def FireModeIndex : Int = fireModeIndex + + def FireModeIndex_=(index : Int) : Int = { + fireModeIndex = index % toolDef.FireModes.length + FireModeIndex + } + + def FireMode : FireModeDefinition = toolDef.FireModes(fireModeIndex) + + def NextFireMode : FireModeDefinition = { + FireModeIndex = FireModeIndex + 1 + FireMode + } + + def AmmoTypeIndex : Int = ammoSlot(fireModeIndex).AmmoTypeIndex + + def AmmoTypeIndex_=(index : Int) : Int = { + ammoSlot(fireModeIndex).AmmoTypeIndex = index % FireMode.AmmoTypeIndices.length + AmmoTypeIndex + } + + def AmmoType : Ammo.Value = toolDef.AmmoTypes(AmmoTypeIndex) + + def NextAmmoType : Ammo.Value = { + AmmoTypeIndex = AmmoTypeIndex + 1 + AmmoType + } + + def Magazine : Int = ammoSlot(fireModeIndex).Magazine + + def Magazine_=(mag : Int) : Int = { + ammoSlot(fireModeIndex).Magazine = Math.min(Math.max(0, mag), MaxMagazine) + Magazine + } + + def MaxMagazine : Int = FireMode.Magazine + + def NextDischarge : Int = math.min(Magazine, FireMode.Chamber) + + def AmmoSlots : List[Tool.FireModeSlot] = ammoSlot + + def MaxAmmoSlot : Int = ammoSlot.length + + def Definition : ToolDefinition = toolDef + + override def toString : String = { + Tool.toString(this) + } +} + +object Tool { + def apply(toolDef : ToolDefinition) : Tool = { + new Tool(toolDef) + } + + def apply(guid : PlanetSideGUID, toolDef : ToolDefinition) : Tool = { + val obj = new Tool(toolDef) + obj.GUID = guid + obj + } + + /** + * Use the `*Definition` that was provided to this object to initialize its fields and settings. + * @param tool the `Tool` being initialized + */ + def LoadDefinition(tool : Tool) : List[FireModeSlot] = { + val tdef : ToolDefinition = tool.Definition + val maxSlot = tdef.FireModes.maxBy(fmode => fmode.AmmoSlotIndex).AmmoSlotIndex + buildFireModes(tool, (0 to maxSlot).iterator, tdef.FireModes.toList) + } + + @tailrec private def buildFireModes(tool : Tool, iter : Iterator[Int], fmodes : List[FireModeDefinition], list : List[FireModeSlot] = Nil) : List[FireModeSlot] = { + if(!iter.hasNext) { + list + } + else { + val index = iter.next + fmodes.filter(fmode => fmode.AmmoSlotIndex == index) match { + case fmode :: _ => + buildFireModes(tool, iter, fmodes, list :+ new FireModeSlot(tool, fmode)) + case Nil => + throw new IllegalArgumentException(s"tool ${tool.Definition.Name} ammo slot #$index is missing a fire mode specification; do not skip") + } + } + } + + def toString(obj : Tool) : String = { + s"${obj.Definition.Name} (mode=${obj.FireModeIndex}-${obj.AmmoType})(${obj.Magazine}/${obj.MaxMagazine})" + } + + /** + * A hidden class that manages the specifics of the given ammunition for the current fire mode of this tool. + * It operates much closer to an "ammunition feed" rather than a fire mode. + * The relationship to fire modes is at least one-to-one and at most one-to-many. + */ + class FireModeSlot(private val tool : Tool, private val fdef : FireModeDefinition) { + /* + By way of demonstration: + Suppressors have one fire mode, two types of ammunition, one slot (2) + MA Pistols have two fire modes, one type of ammunition, one slot (1) + Jackhammers have two fire modes, two types of ammunition, one slot (2) + Punishers have two fire modes, five types of ammunition, two slots (2, 3) + */ + + /** if this fire mode has multiple types of ammunition */ + private var ammoTypeIndex : Int = fdef.AmmoTypeIndices.head + /** a reference to the actual `AmmoBox` of this slot; will not synch up with `AmmoType` immediately */ + private var box : AmmoBox = AmmoBox(AmmoBoxDefinition(AmmoType)) //defaults to box of one round of the default type for this slot + + def AmmoTypeIndex : Int = ammoTypeIndex + + def AmmoTypeIndex_=(index : Int) : Int = { + ammoTypeIndex = index + AmmoTypeIndex + } + + def AmmoType : Ammo.Value = tool.Definition.AmmoTypes(ammoTypeIndex) + + def Magazine : Int = box.Capacity + + def Magazine_=(mag : Int) : Int = { + box.Capacity = mag + Magazine + } + + def Box : AmmoBox = box + + def Box_=(toBox : AmmoBox) : Option[AmmoBox] = { + if(toBox.AmmoType == AmmoType) { + box = toBox + Some(Box) + } + else { + None + } + } + + def Tool : Tool = tool + + def Definition : FireModeDefinition = fdef + } +} diff --git a/common/src/main/scala/net/psforever/objects/Vehicle.scala b/common/src/main/scala/net/psforever/objects/Vehicle.scala new file mode 100644 index 000000000..206fc78ff --- /dev/null +++ b/common/src/main/scala/net/psforever/objects/Vehicle.scala @@ -0,0 +1,375 @@ +// Copyright (c) 2017 PSForever +package net.psforever.objects + +import net.psforever.objects.definition.VehicleDefinition +import net.psforever.objects.equipment.{Equipment, EquipmentSize} +import net.psforever.objects.inventory.GridInventory +import net.psforever.objects.vehicles.{Seat, Utility, VehicleLockState} +import net.psforever.packet.game.PlanetSideGUID +import net.psforever.packet.game.objectcreate.DriveState +import net.psforever.types.PlanetSideEmpire + +import scala.collection.mutable + +/** + * The server-side support object that represents a vehicle.
+ *
+ * All infantry seating, all mounted weapons, and the trunk space are considered part of the same index hierarchy. + * Generally, all seating is declared first - the driver and passengers and and gunners. + * Following that are the mounted weapons and other utilities. + * Trunk space starts being indexed afterwards. + * The first seat is always the op;erator (driver/pilot). + * "Passengers" are seats that are not the operator and are not in control of a mounted weapon. + * "Gunners" are seats that are not the operator and ARE in control of a mounted weapon. + * (The operator can be in control of a weapon - that is the whole point of a turret.)
+ *
+ * Having said all that, to keep it simple, infantry seating, mounted weapons, and utilities are stored in separate `Map`s. + * @param vehicleDef the vehicle's definition entry' + * stores and unloads pertinent information about the `Vehicle`'s configuration; + * used in the initialization process (`loadVehicleDefinition`) + */ +class Vehicle(private val vehicleDef : VehicleDefinition) extends PlanetSideGameObject { + private var faction : PlanetSideEmpire.Value = PlanetSideEmpire.TR + private var owner : Option[PlanetSideGUID] = None + private var health : Int = 1 + private var shields : Int = 0 + private var deployed : DriveState.Value = DriveState.Mobile + private var decal : Int = 0 + private var trunkLockState : VehicleLockState.Value = VehicleLockState.Locked + private var trunkAccess : Option[PlanetSideGUID] = None + + private val seats : mutable.HashMap[Int, Seat] = mutable.HashMap() + private val weapons : mutable.HashMap[Int, EquipmentSlot] = mutable.HashMap() + private val utilities : mutable.ArrayBuffer[Utility] = mutable.ArrayBuffer() + private val trunk : GridInventory = GridInventory() + + //init + LoadDefinition() + + /** + * Override this method to perform any special setup that is not standardized to `*Definition`. + * @see `Vehicle.LoadDefinition` + */ + protected def LoadDefinition() : Unit = { + Vehicle.LoadDefinition(this) + } + + def Faction : PlanetSideEmpire.Value = { + this.faction + } + + def Faction_=(faction : PlanetSideEmpire.Value) : PlanetSideEmpire.Value = { + this.faction = faction + faction + } + + def Owner : Option[PlanetSideGUID] = { + this.owner + } + + def Owner_=(owner : Option[PlanetSideGUID]) : Option[PlanetSideGUID] = { + this.owner = owner + owner + } + + def Health : Int = { + this.health + } + + def Health_=(health : Int) : Int = { + this.health = health + health + } + + def MaxHealth : Int = { + this.vehicleDef.MaxHealth + } + + def Shields : Int = { + this.shields + } + + def Shields_=(strength : Int) : Int = { + this.shields = strength + strength + } + + def MaxShields : Int = { + vehicleDef.MaxShields + } + + def Configuration : DriveState.Value = { + this.deployed + } + + def Configuration_=(deploy : DriveState.Value) : DriveState.Value = { + if(vehicleDef.Deployment) { + this.deployed = deploy + } + Configuration + } + + def Decal : Int = { + this.decal + } + + def Decal_=(decal : Int) : Int = { + this.decal = decal + decal + } + + /** + * Given the index of an entry mounting point, return the infantry-accessible `Seat` associated with it. + * @param mountPoint an index representing the seat position / mounting point + * @return a seat number, or `None` + */ + def GetSeatFromMountPoint(mountPoint : Int) : Option[Int] = { + vehicleDef.MountPoints.get(mountPoint) + } + + /** + * Get the seat at the index. + * The specified "seat" can only accommodate a player as opposed to weapon mounts which share the same indexing system. + * @param seatNumber an index representing the seat position / mounting point + * @return a `Seat`, or `None` + */ + def Seat(seatNumber : Int) : Option[Seat] = { + if(seatNumber >= 0 && seatNumber < this.seats.size) { + this.seats.get(seatNumber) + } + else { + None + } + } + + def Seats : List[Seat] = { + seats.values.toList + } + + def Weapons : mutable.HashMap[Int, EquipmentSlot] = weapons + + /** + * Get the weapon at the index. + * @param wepNumber an index representing the seat position / mounting point + * @return a weapon, or `None` + */ + def ControlledWeapon(wepNumber : Int) : Option[Equipment] = { + val slot = this.weapons.get(wepNumber) + if(slot.isDefined) { + slot.get.Equipment + } + else { + None + } + } + + /** + * Given a player who may be a passenger, retrieve an index where this player is seated. + * @param player the player + * @return a seat by index, or `None` if the `player` is not actually seated in this `Vehicle` + */ + def PassengerInSeat(player : Player) : Option[Int] = { + var outSeat : Option[Int] = None + val GUID = player.GUID + for((seatNumber, seat) <- this.seats) { + val occupant : Option[PlanetSideGUID] = seat.Occupant + if(occupant.isDefined && occupant.get == GUID) { + outSeat = Some(seatNumber) + } + } + outSeat + } + + /** + * Given a valid seat number, retrieve an index where a weapon controlled from this seat is attached. + * @param seatNumber the seat number + * @return a mounted weapon by index, or `None` if either the seat doesn't exist or there is no controlled weapon + */ + def WeaponControlledFromSeat(seatNumber : Int) : Option[Tool] = { + Seat(seatNumber) match { + case Some(seat) => + wepFromSeat(seat) + case None => + None + } + } + + private def wepFromSeat(seat : Seat) : Option[Tool] = { + seat.ControlledWeapon match { + case Some(index) => + wepFromSeat(index) + case None => + None + } + } + + private def wepFromSeat(wepIndex : Int) : Option[Tool] = { + weapons.get(wepIndex) match { + case Some(wep) => + wep.Equipment.asInstanceOf[Option[Tool]] + case None => + None + } + } + + def Utilities : mutable.ArrayBuffer[Utility] = utilities + + /** + * Get a referenece ot a certain `Utility` attached to this `Vehicle`. + * @param utilNumber the attachment number of the `Utility` + * @return the `Utility` or `None` (if invalid) + */ + def Utility(utilNumber : Int) : Option[Utility] = { + if(utilNumber >= 0 && utilNumber < this.utilities.size) { + Some(this.utilities(utilNumber)) + } + else { + None + } + } + + /** + * A reference to the `Vehicle` `Trunk` space. + * @return this `Vehicle` `Trunk` + */ + def Trunk : GridInventory = { + this.trunk + } + + def AccessingTrunk : Option[PlanetSideGUID] = trunkAccess + + def AccessingTrunk_=(guid : PlanetSideGUID) : Option[PlanetSideGUID] = { + AccessingTrunk = Some(guid) + } + + /** + * Change which player has access to the trunk of this vehicle. + * A player may only gain access to the trunk if no one else has access to the trunk at the moment. + * @param guid the player who wishes to access the trunk + * @return the player who is currently allowed to access the trunk + */ + def AccessingTrunk_=(guid : Option[PlanetSideGUID]) : Option[PlanetSideGUID] = { + guid match { + case None => + trunkAccess = None + case Some(player) => + if(trunkAccess.isEmpty) { + trunkAccess = Some(player) + } + } + AccessingTrunk + } + + /** + * Can this `player` access the contents of this `Vehicle`'s `Trunk` given its current access permissions? + * @param player a player attempting to access this `Trunk` + * @return `true`, if the `player` is permitted access; `false`, otherwise + */ + def CanAccessTrunk(player : Player) : Boolean = { + if(trunkAccess.isEmpty || trunkAccess.contains(player.GUID)) { + trunkLockState match { + case VehicleLockState.Locked => //only the owner + owner.isEmpty || (owner.isDefined && player.GUID == owner.get) + case VehicleLockState.Group => //anyone in the owner's squad or platoon + faction == player.Faction //TODO this is not correct + case VehicleLockState.Empire => //anyone of the owner's faction + faction == player.Faction + } + } + else { + false + } + } + + /** + * Check access to the `Trunk`. + * @return the current access value for the `Vehicle` `Trunk` + */ + def TrunkLockState : VehicleLockState.Value = { + this.trunkLockState + } + + /** + * Change the access value for the trunk. + * @param lockState the new access value for the `Vehicle` `Trunk` + * @return the current access value for the `Vehicle` `Trunk` after the change + */ + def TrunkLockState_=(lockState : VehicleLockState.Value) : VehicleLockState.Value = { + this.trunkLockState = lockState + lockState + } + + /** + * This is the definition entry that is used to store and unload pertinent information about the `Vehicle`. + * @return the vehicle's definition entry + */ + def Definition : VehicleDefinition = vehicleDef + + /** + * Override the string representation to provide additional information. + * @return the string output + */ + override def toString : String = { + Vehicle.toString(this) + } +} + +object Vehicle { + /** + * Overloaded constructor. + * @param vehicleDef the vehicle's definition entry + * @return a `Vwehicle` object + */ + def apply(vehicleDef : VehicleDefinition) : Vehicle = { + new Vehicle(vehicleDef) + } + /** + * Overloaded constructor. + * @param vehicleDef the vehicle's definition entry + * @return a `Vwehicle` object + */ + def apply(guid : PlanetSideGUID, vehicleDef : VehicleDefinition) : Vehicle = { + val obj = new Vehicle(vehicleDef) + obj.GUID = guid + obj + } + + /** + * Use the `*Definition` that was provided to this object to initialize its fields and settings. + * @param vehicle the `Vehicle` being initialized + * @see `{object}.LoadDefinition` + */ + def LoadDefinition(vehicle : Vehicle) : Vehicle = { + val vdef : VehicleDefinition = vehicle.Definition + //general stuff + vehicle.Health = vdef.MaxHealth + //create weapons + for((num, definition) <- vdef.Weapons) { + val slot = EquipmentSlot(EquipmentSize.VehicleWeapon) + slot.Equipment = Tool(definition) + vehicle.weapons += num -> slot + vehicle + } + //create seats + for((num, seatDef) <- vdef.Seats) { + vehicle.seats += num -> Seat(seatDef, vehicle) + } + for(i <- vdef.Utilities) { + //TODO utilies must be loaded and wired on a case-by-case basis? + vehicle.Utilities += Utility.Select(i, vehicle) + } + //trunk + vehicle.trunk.Resize(vdef.TrunkSize.width, vdef.TrunkSize.height) + vehicle.trunk.Offset = vdef.TrunkOffset + vehicle + } + + /** + * Provide a fixed string representation. + * @return the string output + */ + def toString(obj : Vehicle) : String = { + val occupancy = obj.Seats.count(seat => seat.isOccupied) + s"${obj.Definition.Name}, owned by ${obj.Owner}: (${obj.Health}/${obj.MaxHealth})(${obj.Shields}/${obj.MaxShields}) ($occupancy)" + } +} diff --git a/common/src/main/scala/net/psforever/objects/definition/AmmoBoxDefinition.scala b/common/src/main/scala/net/psforever/objects/definition/AmmoBoxDefinition.scala new file mode 100644 index 000000000..9598245fb --- /dev/null +++ b/common/src/main/scala/net/psforever/objects/definition/AmmoBoxDefinition.scala @@ -0,0 +1,33 @@ +// Copyright (c) 2017 PSForever +package net.psforever.objects.definition + +import net.psforever.objects.definition.converter.AmmoBoxConverter +import net.psforever.objects.equipment.Ammo + +class AmmoBoxDefinition(objectId : Int) extends EquipmentDefinition(objectId) { + import net.psforever.objects.equipment.EquipmentSize + private val ammoType : Ammo.Value = Ammo(objectId) //let throw NoSuchElementException + private var capacity : Int = 1 + Name = "ammo box" + Size = EquipmentSize.Inventory + Packet = new AmmoBoxConverter() + + def AmmoType : Ammo.Value = ammoType + + def Capacity : Int = capacity + + def Capacity_=(capacity : Int) : Int = { + this.capacity = capacity + Capacity + } +} + +object AmmoBoxDefinition { + def apply(objectId: Int) : AmmoBoxDefinition = { + new AmmoBoxDefinition(objectId) + } + + def apply(ammoType : Ammo.Value) : AmmoBoxDefinition = { + new AmmoBoxDefinition(ammoType.id) + } +} diff --git a/common/src/main/scala/net/psforever/objects/definition/AvatarDefinition.scala b/common/src/main/scala/net/psforever/objects/definition/AvatarDefinition.scala new file mode 100644 index 000000000..0214b3087 --- /dev/null +++ b/common/src/main/scala/net/psforever/objects/definition/AvatarDefinition.scala @@ -0,0 +1,24 @@ +// Copyright (c) 2017 PSForever +package net.psforever.objects.definition + +import net.psforever.objects.definition.converter.AvatarConverter +import net.psforever.objects.Avatars + +/** + * The definition for game objects that look like other people, and also for players. + * @param objectId the object's identifier number + */ +class AvatarDefinition(objectId : Int) extends ObjectDefinition(objectId) { + Avatars(objectId) //let throw NoSuchElementException + Packet = new AvatarConverter() +} + +object AvatarDefinition { + def apply(objectId: Int) : AvatarDefinition = { + new AvatarDefinition(objectId) + } + + def apply(avatar : Avatars.Value) : AvatarDefinition = { + new AvatarDefinition(avatar.id) + } +} diff --git a/common/src/main/scala/net/psforever/objects/definition/BasicDefinition.scala b/common/src/main/scala/net/psforever/objects/definition/BasicDefinition.scala new file mode 100644 index 000000000..f7a7b61bc --- /dev/null +++ b/common/src/main/scala/net/psforever/objects/definition/BasicDefinition.scala @@ -0,0 +1,13 @@ +// Copyright (c) 2017 PSForever +package net.psforever.objects.definition + +abstract class BasicDefinition { + private var name : String = "definition" + + def Name : String = name + + def Name_=(name : String) : String = { + this.name = name + Name + } +} diff --git a/common/src/main/scala/net/psforever/objects/definition/ConstructionItemDefinition.scala b/common/src/main/scala/net/psforever/objects/definition/ConstructionItemDefinition.scala new file mode 100644 index 000000000..4a3eb9bfb --- /dev/null +++ b/common/src/main/scala/net/psforever/objects/definition/ConstructionItemDefinition.scala @@ -0,0 +1,23 @@ +// Copyright (c) 2017 PSForever +package net.psforever.objects.definition + +import net.psforever.objects.equipment.CItem + +import scala.collection.mutable.ListBuffer + +class ConstructionItemDefinition(objectId : Int) extends EquipmentDefinition(objectId) { + CItem.Unit(objectId) //let throw NoSuchElementException + private val modes : ListBuffer[CItem.DeployedItem.Value] = ListBuffer() + + def Modes : ListBuffer[CItem.DeployedItem.Value] = modes +} + +object ConstructionItemDefinition { + def apply(objectId : Int) : ConstructionItemDefinition = { + new ConstructionItemDefinition(objectId) + } + + def apply(cItem : CItem.Unit.Value) : ConstructionItemDefinition = { + new ConstructionItemDefinition(cItem.id) + } +} diff --git a/common/src/main/scala/net/psforever/objects/definition/EquipmentDefinition.scala b/common/src/main/scala/net/psforever/objects/definition/EquipmentDefinition.scala new file mode 100644 index 000000000..d74e5fdb3 --- /dev/null +++ b/common/src/main/scala/net/psforever/objects/definition/EquipmentDefinition.scala @@ -0,0 +1,39 @@ +// Copyright (c) 2017 PSForever +package net.psforever.objects.definition + +import net.psforever.objects.equipment.EquipmentSize +import net.psforever.objects.inventory.InventoryTile + +/** + * The definition for any piece of `Equipment`. + * @param objectId the object's identifier number + */ +abstract class EquipmentDefinition(objectId : Int) extends ObjectDefinition(objectId) { + /** the size of the item when placed in an EquipmentSlot / holster / mounting */ + private var size : EquipmentSize.Value = EquipmentSize.Blocked + /** the size of the item when placed in the grid inventory space */ + private var tile : InventoryTile = InventoryTile.Tile11 + /** a correction for the z-coordinate for some dropped items to avoid sinking into the ground */ + private var dropOffset : Float = 0f + + def Size : EquipmentSize.Value = size + + def Size_=(newSize : EquipmentSize.Value) : EquipmentSize.Value = { + size = newSize + Size + } + + def Tile : InventoryTile = tile + + def Tile_=(newTile : InventoryTile) : InventoryTile = { + tile = newTile + Tile + } + + def DropOffset : Float = dropOffset + + def DropOffset(offset : Float) : Float = { + dropOffset = offset + DropOffset + } +} diff --git a/common/src/main/scala/net/psforever/objects/definition/ImplantDefinition.scala b/common/src/main/scala/net/psforever/objects/definition/ImplantDefinition.scala new file mode 100644 index 000000000..1825b58e8 --- /dev/null +++ b/common/src/main/scala/net/psforever/objects/definition/ImplantDefinition.scala @@ -0,0 +1,93 @@ +// Copyright (c) 2017 PSForever +package net.psforever.objects.definition + +import net.psforever.types.{ExoSuitType, ImplantType} + +import scala.collection.mutable + +/** + * An `Enumeration` of a variety of poses or generalized movement. + */ +object Stance extends Enumeration { + val Crouching, + Standing, + Walking, //not used, but should still be defined + Running = Value +} + +/** + * The definition for an installable player utility that grants a perk, usually in exchange for stamina (energy).
+ *
+ * Most of the definition deals with the costs of activation and operation. + * When activated by the user, an `activationCharge` may be deducted form that user's stamina reserves. + * This does not necessarily have to be a non-zero value. + * Passive implants are always active and thus have no cost. + * After being activated, a non-passive implant consumes a specific amount of stamina each second. + * This cost is modified by how the user is standing and what type of exo-suit they are wearing. + * The `durationChargeBase` is the lowest cost for an implant. + * Modifiers for exo-suit type and stance type are then added onto this base cost. + * For example: wearing `Reinforced` costs 2 stamina but costs only 1 stamina in all other cases. + * Assuming that is the only cost, the definition would have a base charge of 1 and a `Reinforced` modifier of 1. + * @param implantType the type of implant that is defined + * @see `ImplantType` + */ +class ImplantDefinition(private val implantType : Int) extends BasicDefinition { + ImplantType(implantType) + /** how long it takes the implant to spin-up; is milliseconds */ + private var initialization : Long = 0L + /** a passive certification is activated as soon as it is ready (or other condition) */ + private var passive : Boolean = false + /** how much turning on the implant costs */ + private var activationCharge : Int = 0 + /** how much energy does this implant cost to remain active per second*/ + private var durationChargeBase : Int = 0 + /** how much more energy does the implant cost for this exo-suit */ + private val durationChargeByExoSuit = mutable.HashMap[ExoSuitType.Value, Int]().withDefaultValue(0) + /** how much more energy does the implant cost for this stance */ + private val durationChargeByStance = mutable.HashMap[Stance.Value, Int]().withDefaultValue(0) + Name = "implant" + + def Initialization : Long = initialization + + def Initialization_=(time : Long) : Long = { + initialization = math.max(0, time) + Initialization + } + + def Passive : Boolean = passive + + def Passive_=(isPassive : Boolean) : Boolean = { + passive = isPassive + Passive + } + + def ActivationCharge : Int = activationCharge + + def ActivationCharge_=(charge : Int) : Int = { + activationCharge = math.max(0, charge) + ActivationCharge + } + + def DurationChargeBase : Int = durationChargeBase + + def DurationChargeBase_=(charge : Int) : Int = { + durationChargeBase = math.max(0, charge) + DurationChargeBase + } + + def DurationChargeByExoSuit : mutable.Map[ExoSuitType.Value, Int] = durationChargeByExoSuit + + def DurationChargeByStance : mutable.Map[Stance.Value, Int] = durationChargeByStance + + def Type : ImplantType.Value = ImplantType(implantType) +} + +object ImplantDefinition { + def apply(implantType : Int) : ImplantDefinition = { + new ImplantDefinition(implantType) + } + + def apply(implantType : ImplantType.Value) : ImplantDefinition = { + new ImplantDefinition(implantType.id) + } +} diff --git a/common/src/main/scala/net/psforever/objects/definition/KitDefinition.scala b/common/src/main/scala/net/psforever/objects/definition/KitDefinition.scala new file mode 100644 index 000000000..f9d7ceae1 --- /dev/null +++ b/common/src/main/scala/net/psforever/objects/definition/KitDefinition.scala @@ -0,0 +1,29 @@ +// Copyright (c) 2017 PSForever +package net.psforever.objects.definition + +import net.psforever.objects.definition.converter.KitConverter +import net.psforever.objects.equipment.Kits + +/** + * The definition for a personal one-time-use recovery item. + * @param objectId the object's identifier number + */ +class KitDefinition(objectId : Int) extends EquipmentDefinition(objectId) { + import net.psforever.objects.equipment.EquipmentSize + import net.psforever.objects.inventory.InventoryTile + Kits(objectId) //let throw NoSuchElementException + Size = EquipmentSize.Inventory + Tile = InventoryTile.Tile42 + Name = "kit" + Packet = new KitConverter() +} + +object KitDefinition { + def apply(objectId: Int) : KitDefinition = { + new KitDefinition(objectId) + } + + def apply(kit : Kits.Value) : KitDefinition = { + new KitDefinition(kit.id) + } +} diff --git a/common/src/main/scala/net/psforever/objects/definition/ObjectDefinition.scala b/common/src/main/scala/net/psforever/objects/definition/ObjectDefinition.scala new file mode 100644 index 000000000..d1c0decca --- /dev/null +++ b/common/src/main/scala/net/psforever/objects/definition/ObjectDefinition.scala @@ -0,0 +1,42 @@ +// Copyright (c) 2017 PSForever +package net.psforever.objects.definition + +import net.psforever.objects.PlanetSideGameObject +import net.psforever.objects.definition.converter.{ObjectCreateConverter, PacketConverter} + +/** + * Associate an object's canned in-game representation with its basic game identification unit. + * The extension of this `class` would identify the common data necessary to construct such a given game object.
+ *
+ * The converter transforms a game object that is created by this `ObjectDefinition` into packet data through method-calls. + * The field for this converter is a `PacketConverter`, the superclass for `ObjectCreateConverter`; + * the type of the mutator's parameter is `ObjectCreateConverter` of a wildcard `tparam`; + * and, the accessor return type is `ObjectCreateConverter[PlanetSideGameObject]`, a minimum-true statement. + * The actual type of the converter at a given point, casted or otherwise, is mostly meaningless. + * Casting the external object does not mutate any of the types used by the methods within that object. + * So long as it is an `ObjectCreatePacket`, those methods can be called correctly for a game object of the desired type. + * @param objectId the object's identifier number + */ +abstract class ObjectDefinition(private val objectId : Int) extends BasicDefinition { + /** a data converter for this type of object */ + protected var packet : PacketConverter = new ObjectCreateConverter[PlanetSideGameObject]() { } + Name = "object definition" + + /** + * Get the conversion object. + * @return + */ + final def Packet : ObjectCreateConverter[PlanetSideGameObject] = packet.asInstanceOf[ObjectCreateConverter[PlanetSideGameObject]] + + /** + * Assign this definition a conversion object. + * @param pkt the new converter + * @return the current converter, after assignment + */ + final def Packet_=(pkt : ObjectCreateConverter[_]) : PacketConverter = { + packet = pkt + Packet + } + + def ObjectId : Int = objectId +} diff --git a/common/src/main/scala/net/psforever/objects/definition/SeatDefinition.scala b/common/src/main/scala/net/psforever/objects/definition/SeatDefinition.scala new file mode 100644 index 000000000..5535274e2 --- /dev/null +++ b/common/src/main/scala/net/psforever/objects/definition/SeatDefinition.scala @@ -0,0 +1,44 @@ +// Copyright (c) 2017 PSForever +package net.psforever.objects.definition + +import net.psforever.objects.vehicles.SeatArmorRestriction + +/** + * The definition for a seat. + */ +class SeatDefinition extends BasicDefinition { + /** a restriction on the type of exo-suit a person can wear */ + private var armorRestriction : SeatArmorRestriction.Value = SeatArmorRestriction.NoMax + /** the user can escape while the vehicle is moving */ + private var bailable : Boolean = false + /** any controlled weapon */ + private var weaponMount : Option[Int] = None + Name = "seat" + + def ArmorRestriction : SeatArmorRestriction.Value = { + this.armorRestriction + } + + def ArmorRestriction_=(restriction : SeatArmorRestriction.Value) : SeatArmorRestriction.Value = { + this.armorRestriction = restriction + restriction + } + + def Bailable : Boolean = { + this.bailable + } + + def Bailable_=(canBail : Boolean) : Boolean = { + this.bailable = canBail + canBail + } + + def ControlledWeapon : Option[Int] = { + this.weaponMount + } + + def ControlledWeapon_=(seat : Option[Int]) : Option[Int] = { + this.weaponMount = seat + ControlledWeapon + } +} diff --git a/common/src/main/scala/net/psforever/objects/definition/SimpleItemDefinition.scala b/common/src/main/scala/net/psforever/objects/definition/SimpleItemDefinition.scala new file mode 100644 index 000000000..b122a0d7a --- /dev/null +++ b/common/src/main/scala/net/psforever/objects/definition/SimpleItemDefinition.scala @@ -0,0 +1,21 @@ +// Copyright (c) 2017 PSForever +package net.psforever.objects.definition + +import net.psforever.objects.equipment.SItem + +class SimpleItemDefinition(objectId : Int) extends EquipmentDefinition(objectId) { + import net.psforever.objects.equipment.EquipmentSize + SItem(objectId) //let throw NoSuchElementException + Name = "tool" + Size = EquipmentSize.Pistol //all items +} + +object SimpleItemDefinition { + def apply(objectId : Int) : SimpleItemDefinition = { + new SimpleItemDefinition(objectId) + } + + def apply(simpItem : SItem.Value) : SimpleItemDefinition = { + new SimpleItemDefinition(simpItem.id) + } +} diff --git a/common/src/main/scala/net/psforever/objects/definition/ToolDefinition.scala b/common/src/main/scala/net/psforever/objects/definition/ToolDefinition.scala new file mode 100644 index 000000000..2dbb7303e --- /dev/null +++ b/common/src/main/scala/net/psforever/objects/definition/ToolDefinition.scala @@ -0,0 +1,24 @@ +// Copyright (c) 2017 PSForever +package net.psforever.objects.definition + +import net.psforever.objects.definition.converter.ToolConverter +import net.psforever.objects.equipment.{Ammo, FireModeDefinition} + +import scala.collection.mutable + +class ToolDefinition(objectId : Int) extends EquipmentDefinition(objectId) { + private val ammoTypes : mutable.ListBuffer[Ammo.Value] = new mutable.ListBuffer[Ammo.Value] + private val fireModes : mutable.ListBuffer[FireModeDefinition] = new mutable.ListBuffer[FireModeDefinition] + Name = "tool" + Packet = new ToolConverter() + + def AmmoTypes : mutable.ListBuffer[Ammo.Value] = ammoTypes + + def FireModes : mutable.ListBuffer[FireModeDefinition] = fireModes +} + +object ToolDefinition { + def apply(objectId : Int) : ToolDefinition = { + new ToolDefinition(objectId) + } +} diff --git a/common/src/main/scala/net/psforever/objects/definition/VehicleDefinition.scala b/common/src/main/scala/net/psforever/objects/definition/VehicleDefinition.scala new file mode 100644 index 000000000..d7c67fe6c --- /dev/null +++ b/common/src/main/scala/net/psforever/objects/definition/VehicleDefinition.scala @@ -0,0 +1,77 @@ +// Copyright (c) 2017 PSForever +package net.psforever.objects.definition + +import net.psforever.objects.definition.converter.VehicleConverter +import net.psforever.objects.inventory.InventoryTile + +import scala.collection.mutable + +/** + * An object definition system used to construct and retain the parameters of various vehicles. + * @param objectId the object id the is associated with this sort of `Vehicle` + */ +class VehicleDefinition(objectId : Int) extends ObjectDefinition(objectId) { + private var maxHealth : Int = 100 + private var maxShields : Int = 0 + /* key - seat index, value - seat object */ + private val seats : mutable.HashMap[Int, SeatDefinition] = mutable.HashMap[Int, SeatDefinition]() + /* key - entry point index, value - seat index */ + private val mountPoints : mutable.HashMap[Int, Int] = mutable.HashMap() + /* key - seat index (where this weapon attaches during object construction), value - the weapon on an EquipmentSlot */ + private val weapons : mutable.HashMap[Int, ToolDefinition] = mutable.HashMap[Int, ToolDefinition]() + private var deployment : Boolean = false + private val utilities : mutable.ArrayBuffer[Int] = mutable.ArrayBuffer[Int]() + private var trunkSize : InventoryTile = InventoryTile.None + private var trunkOffset: Int = 0 + Name = "vehicle" + Packet = new VehicleConverter + + def MaxHealth : Int = maxHealth + + def MaxHealth_=(health : Int) : Int = { + maxHealth = health + MaxHealth + } + + def MaxShields : Int = maxShields + + def MaxShields_=(shields : Int) : Int = { + maxShields = shields + MaxShields + } + + def Seats : mutable.HashMap[Int, SeatDefinition] = seats + + def MountPoints : mutable.HashMap[Int, Int] = mountPoints + + def Weapons : mutable.HashMap[Int, ToolDefinition] = weapons + + def Deployment : Boolean = deployment + + def Deployment_=(deployable : Boolean) : Boolean = { + deployment = deployable + Deployment + } + + def Utilities : mutable.ArrayBuffer[Int] = utilities + + def TrunkSize : InventoryTile = trunkSize + + def TrunkSize_=(tile : InventoryTile) : InventoryTile = { + trunkSize = tile + TrunkSize + } + + def TrunkOffset : Int = trunkOffset + + def TrunkOffset_=(offset : Int) : Int = { + trunkOffset = offset + TrunkOffset + } +} + +object VehicleDefinition { + def apply(objectId: Int) : VehicleDefinition = { + new VehicleDefinition(objectId) + } +} diff --git a/common/src/main/scala/net/psforever/objects/definition/converter/ACEConverter.scala b/common/src/main/scala/net/psforever/objects/definition/converter/ACEConverter.scala new file mode 100644 index 000000000..d45acda1f --- /dev/null +++ b/common/src/main/scala/net/psforever/objects/definition/converter/ACEConverter.scala @@ -0,0 +1,17 @@ +// Copyright (c) 2017 PSForever +package net.psforever.objects.definition.converter + +import net.psforever.objects.ConstructionItem +import net.psforever.packet.game.objectcreate.{ACEData, DetailedACEData} + +import scala.util.{Success, Try} + +class ACEConverter extends ObjectCreateConverter[ConstructionItem]() { + override def ConstructorData(obj : ConstructionItem) : Try[ACEData] = { + Success(ACEData(0,0)) + } + + override def DetailedConstructorData(obj : ConstructionItem) : Try[DetailedACEData] = { + Success(DetailedACEData(0)) + } +} diff --git a/common/src/main/scala/net/psforever/objects/definition/converter/AMSConverter.scala b/common/src/main/scala/net/psforever/objects/definition/converter/AMSConverter.scala new file mode 100644 index 000000000..ee4cfc8e4 --- /dev/null +++ b/common/src/main/scala/net/psforever/objects/definition/converter/AMSConverter.scala @@ -0,0 +1,44 @@ +// Copyright (c) 2017 PSForever +package net.psforever.objects.definition.converter + +import net.psforever.objects.Vehicle +import net.psforever.packet.game.PlanetSideGUID +import net.psforever.packet.game.objectcreate.{AMSData, CommonFieldData, ObjectClass, PlacementData} + +import scala.util.{Success, Try} + +class AMSConverter extends ObjectCreateConverter[Vehicle] { + /* Vehicles do not have a conversion for `0x18` packet data. */ + + override def ConstructorData(obj : Vehicle) : Try[AMSData] = { + Success( + AMSData( + CommonFieldData( + PlacementData(obj.Position, obj.Orientation, obj.Velocity), + obj.Faction, + 0, + if(obj.Owner.isDefined) { obj.Owner.get } else { PlanetSideGUID(0) } //this is the owner field, right? + ), + 0, + obj.Health, + 0, + obj.Configuration, + 0, + ReferenceUtility(obj, ObjectClass.matrix_terminalc), + ReferenceUtility(obj, ObjectClass.ams_respawn_tube), + ReferenceUtility(obj, ObjectClass.order_terminala), + ReferenceUtility(obj, ObjectClass.order_terminalb) + ) + ) + } + + /** + * For an object with a list of utilities, find a specific kind of utility. + * @param obj the game object + * @param objectId the utility being sought + * @return the global unique identifier of the utility + */ + private def ReferenceUtility(obj : Vehicle, objectId : Int) : PlanetSideGUID = { + obj.Utilities.find(util => util.objectId == objectId).head.GUID + } +} diff --git a/common/src/main/scala/net/psforever/objects/definition/converter/ANTConverter.scala b/common/src/main/scala/net/psforever/objects/definition/converter/ANTConverter.scala new file mode 100644 index 000000000..19c8c729c --- /dev/null +++ b/common/src/main/scala/net/psforever/objects/definition/converter/ANTConverter.scala @@ -0,0 +1,29 @@ +// Copyright (c) 2017 PSForever +package net.psforever.objects.definition.converter + +import net.psforever.objects.Vehicle +import net.psforever.packet.game.PlanetSideGUID +import net.psforever.packet.game.objectcreate.{ANTData, CommonFieldData, PlacementData} + +import scala.util.{Success, Try} + +class ANTConverter extends ObjectCreateConverter[Vehicle] { + /* Vehicles do not have a conversion for `0x18` packet data. */ + + override def ConstructorData(obj : Vehicle) : Try[ANTData] = { + Success( + ANTData( + CommonFieldData( + PlacementData(obj.Position, obj.Orientation,obj.Velocity), + obj.Faction, + 0, + if(obj.Owner.isDefined) { obj.Owner.get } else { PlanetSideGUID(0) } //this is the owner field, right? + ), + 0, + obj.Health, + 0, + obj.Configuration + ) + ) + } +} diff --git a/common/src/main/scala/net/psforever/objects/definition/converter/AmmoBoxConverter.scala b/common/src/main/scala/net/psforever/objects/definition/converter/AmmoBoxConverter.scala new file mode 100644 index 000000000..890a9963d --- /dev/null +++ b/common/src/main/scala/net/psforever/objects/definition/converter/AmmoBoxConverter.scala @@ -0,0 +1,17 @@ +// Copyright (c) 2017 PSForever +package net.psforever.objects.definition.converter + +import net.psforever.objects.AmmoBox +import net.psforever.packet.game.objectcreate.{AmmoBoxData, DetailedAmmoBoxData} + +import scala.util.{Success, Try} + +class AmmoBoxConverter extends ObjectCreateConverter[AmmoBox] { + override def ConstructorData(obj : AmmoBox) : Try[AmmoBoxData] = { + Success(AmmoBoxData()) + } + + override def DetailedConstructorData(obj : AmmoBox) : Try[DetailedAmmoBoxData] = { + Success(DetailedAmmoBoxData(8, obj.Capacity)) + } +} diff --git a/common/src/main/scala/net/psforever/objects/definition/converter/AvatarConverter.scala b/common/src/main/scala/net/psforever/objects/definition/converter/AvatarConverter.scala new file mode 100644 index 000000000..e94547b1c --- /dev/null +++ b/common/src/main/scala/net/psforever/objects/definition/converter/AvatarConverter.scala @@ -0,0 +1,171 @@ +// Copyright (c) 2017 PSForever +package net.psforever.objects.definition.converter + +import net.psforever.objects.{EquipmentSlot, Player} +import net.psforever.objects.equipment.Equipment +import net.psforever.packet.game.PlanetSideGUID +import net.psforever.packet.game.objectcreate.{BasicCharacterData, CharacterAppearanceData, CharacterData, DetailedCharacterData, DrawnSlot, InternalSlot, InventoryData, PlacementData, RibbonBars, UniformStyle} +import net.psforever.types.GrenadeState + +import scala.annotation.tailrec +import scala.util.{Success, Try} + +class AvatarConverter extends ObjectCreateConverter[Player]() { + override def ConstructorData(obj : Player) : Try[CharacterData] = { + Success( + CharacterData( + MakeAppearanceData(obj), + obj.Health / obj.MaxHealth * 255, //TODO not precise + obj.Armor / obj.MaxArmor * 255, //TODO not precise + UniformStyle.Normal, + 0, + None, //TODO cosmetics + None, //TODO implant effects + InventoryData(MakeHolsters(obj, BuildEquipment).sortBy(_.parentSlot)), + GetDrawnSlot(obj) + ) + ) + //TODO tidy this mess up + } + + override def DetailedConstructorData(obj : Player) : Try[DetailedCharacterData] = { + Success( + DetailedCharacterData( + MakeAppearanceData(obj), + obj.MaxHealth, + obj.Health, + obj.Armor, + 1, 7, 7, + obj.MaxStamina, + obj.Stamina, + 28, 4, 44, 84, 104, 1900, + List.empty[String], //TODO fte list + List.empty[String], //TODO tutorial list + InventoryData((MakeHolsters(obj, BuildDetailedEquipment) ++ MakeFifthSlot(obj) ++ MakeInventory(obj)).sortBy(_.parentSlot)), + GetDrawnSlot(obj) + ) + ) + //TODO tidy this mess up + } + + /** + * Compose some data from a `Player` into a representation common to both `CharacterData` and `DetailedCharacterData`. + * @param obj the `Player` game object + * @return the resulting `CharacterAppearanceData` + */ + private def MakeAppearanceData(obj : Player) : CharacterAppearanceData = { + CharacterAppearanceData( + PlacementData(obj.Position, obj.Orientation, obj.Velocity), + BasicCharacterData(obj.Name, obj.Faction, obj.Sex, obj.Voice, obj.Head), + 0, + false, + false, + obj.ExoSuit, + "", + 0, + obj.isBackpack, + obj.Orientation.y.toInt, + obj.FacingYawUpper.toInt, + true, + GrenadeState.None, + false, + false, + false, + RibbonBars() + ) + } + + /** + * Given a player with an inventory, convert the contents of that inventory into converted-decoded packet data. + * The inventory is not represented in a `0x17` `Player`, so the conversion is only valid for `0x18` avatars. + * It will always be "`Detailed`". + * @param obj the `Player` game object + * @return a list of all items that were in the inventory in decoded packet form + */ + private def MakeInventory(obj : Player) : List[InternalSlot] = { + obj.Inventory.Items + .map({ + case(_, item) => + val equip : Equipment = item.obj + InternalSlot(equip.Definition.ObjectId, equip.GUID, item.start, equip.Definition.Packet.DetailedConstructorData(equip).get) + }).toList + } + /** + * Given a player with equipment holsters, convert the contents of those holsters into converted-decoded packet data. + * The decoded packet form is determined by the function in the parameters as both `0x17` and `0x18` conversions are available, + * with exception to the contents of the fifth slot. + * The fifth slot is only represented if the `Player` is an `0x18` type. + * @param obj the `Player` game object + * @param builder the function used to transform to the decoded packet form + * @return a list of all items that were in the holsters in decoded packet form + */ + private def MakeHolsters(obj : Player, builder : ((Int, Equipment) => InternalSlot)) : List[InternalSlot] = { + recursiveMakeHolsters(obj.Holsters().iterator, builder) + } + + /** + * Given a player with equipment holsters, convert any content of the fifth holster slot into converted-decoded packet data. + * The fifth holster is a curious divider between the standard holsters and the formal inventory. + * This fifth slot is only ever represented if the `Player` is an `0x18` type. + * @param obj the `Player` game object + * @return a list of any item that was in the fifth holster in decoded packet form + */ + private def MakeFifthSlot(obj : Player) : List[InternalSlot] = { + obj.Slot(5).Equipment match { + case Some(equip) => + BuildDetailedEquipment(5, equip) :: Nil + case _ => + Nil + } + } + + /** + * A builder method for turning an object into `0x17` decoded packet form. + * @param index the position of the object + * @param equip the game object + * @return the game object in decoded packet form + */ + private def BuildEquipment(index : Int, equip : Equipment) : InternalSlot = { + InternalSlot(equip.Definition.ObjectId, equip.GUID, index, equip.Definition.Packet.ConstructorData(equip).get) + } + + /** + * A builder method for turning an object into `0x18` decoded packet form. + * @param index the position of the object + * @param equip the game object + * @return the game object in decoded packet form + */ + private def BuildDetailedEquipment(index : Int, equip : Equipment) : InternalSlot = { + InternalSlot(equip.Definition.ObjectId, equip.GUID, index, equip.Definition.Packet.DetailedConstructorData(equip).get) + } + + @tailrec private def recursiveMakeHolsters(iter : Iterator[EquipmentSlot], builder : ((Int, Equipment) => InternalSlot), list : List[InternalSlot] = Nil, index : Int = 0) : List[InternalSlot] = { + if(!iter.hasNext) { + list + } + else { + val slot : EquipmentSlot = iter.next + if(slot.Equipment.isDefined) { + val equip : Equipment = slot.Equipment.get + recursiveMakeHolsters( + iter, + builder, + list :+ builder(index, equip), + index + 1 + ) + } + else { + recursiveMakeHolsters(iter, builder, list, index + 1) + } + } + } + + /** + * Resolve which holster the player has drawn, if any. + * @param obj the `Player` game object + * @return the holster's Enumeration value + */ + private def GetDrawnSlot(obj : Player) : DrawnSlot.Value = { + try { DrawnSlot(obj.DrawnSlot) } catch { case _ : Exception => DrawnSlot.None } + } +} diff --git a/common/src/main/scala/net/psforever/objects/definition/converter/BoomerTriggerConverter.scala b/common/src/main/scala/net/psforever/objects/definition/converter/BoomerTriggerConverter.scala new file mode 100644 index 000000000..6f817afe6 --- /dev/null +++ b/common/src/main/scala/net/psforever/objects/definition/converter/BoomerTriggerConverter.scala @@ -0,0 +1,17 @@ +// Copyright (c) 2017 PSForever +package net.psforever.objects.definition.converter + +import net.psforever.objects.SimpleItem +import net.psforever.packet.game.objectcreate.{BoomerTriggerData, DetailedBoomerTriggerData} + +import scala.util.{Success, Try} + +class BoomerTriggerConverter extends ObjectCreateConverter[SimpleItem]() { + override def ConstructorData(obj : SimpleItem) : Try[BoomerTriggerData] = { + Success(BoomerTriggerData()) + } + + override def DetailedConstructorData(obj : SimpleItem) : Try[DetailedBoomerTriggerData] = { + Success(DetailedBoomerTriggerData()) + } +} diff --git a/common/src/main/scala/net/psforever/objects/definition/converter/CommandDetonaterConverter.scala b/common/src/main/scala/net/psforever/objects/definition/converter/CommandDetonaterConverter.scala new file mode 100644 index 000000000..014d5bd82 --- /dev/null +++ b/common/src/main/scala/net/psforever/objects/definition/converter/CommandDetonaterConverter.scala @@ -0,0 +1,17 @@ +// Copyright (c) 2017 PSForever +package net.psforever.objects.definition.converter + +import net.psforever.objects.SimpleItem +import net.psforever.packet.game.objectcreate.{CommandDetonaterData, DetailedCommandDetonaterData} + +import scala.util.{Success, Try} + +class CommandDetonaterConverter extends ObjectCreateConverter[SimpleItem]() { + override def ConstructorData(obj : SimpleItem) : Try[CommandDetonaterData] = { + Success(CommandDetonaterData()) + } + + override def DetailedConstructorData(obj : SimpleItem) : Try[DetailedCommandDetonaterData] = { + Success(DetailedCommandDetonaterData()) + } +} diff --git a/common/src/main/scala/net/psforever/objects/definition/converter/KitConverter.scala b/common/src/main/scala/net/psforever/objects/definition/converter/KitConverter.scala new file mode 100644 index 000000000..1b0e137df --- /dev/null +++ b/common/src/main/scala/net/psforever/objects/definition/converter/KitConverter.scala @@ -0,0 +1,17 @@ +// Copyright (c) 2017 PSForever +package net.psforever.objects.definition.converter + +import net.psforever.objects.Kit +import net.psforever.packet.game.objectcreate.{AmmoBoxData, DetailedAmmoBoxData} + +import scala.util.{Success, Try} + +class KitConverter extends ObjectCreateConverter[Kit]() { + override def ConstructorData(obj : Kit) : Try[AmmoBoxData] = { + Success(AmmoBoxData()) + } + + override def DetailedConstructorData(obj : Kit) : Try[DetailedAmmoBoxData] = { + Success(DetailedAmmoBoxData(0, 1)) + } +} diff --git a/common/src/main/scala/net/psforever/objects/definition/converter/LockerContainerConverter.scala b/common/src/main/scala/net/psforever/objects/definition/converter/LockerContainerConverter.scala new file mode 100644 index 000000000..0ef5205bf --- /dev/null +++ b/common/src/main/scala/net/psforever/objects/definition/converter/LockerContainerConverter.scala @@ -0,0 +1,35 @@ +// Copyright (c) 2017 PSForever +package net.psforever.objects.definition.converter + +import net.psforever.objects.LockerContainer +import net.psforever.objects.equipment.Equipment +import net.psforever.objects.inventory.GridInventory +import net.psforever.packet.game.PlanetSideGUID +import net.psforever.packet.game.objectcreate.{DetailedAmmoBoxData, InternalSlot, InventoryData, LockerContainerData} + +import scala.util.{Success, Try} + +class LockerContainerConverter extends ObjectCreateConverter[LockerContainer]() { + override def ConstructorData(obj : LockerContainer) : Try[LockerContainerData] = { + Success(LockerContainerData(InventoryData(MakeInventory(obj.Inventory)))) + } + + override def DetailedConstructorData(obj : LockerContainer) : Try[DetailedAmmoBoxData] = { + Success(DetailedAmmoBoxData(8, 1)) //same format as AmmoBox data + } + + /** + * Transform a list of contained items into a list of contained `InternalSlot` objects. + * All objects will take the form of data as if found in an `0x17` packet. + * @param inv the inventory container + * @return a list of all items that were in the inventory in decoded packet form + */ + private def MakeInventory(inv : GridInventory) : List[InternalSlot] = { + inv.Items + .map({ + case(guid, item) => + val equip : Equipment = item.obj + InternalSlot(equip.Definition.ObjectId, PlanetSideGUID(guid), item.start, equip.Definition.Packet.ConstructorData(equip).get) + }).toList + } +} diff --git a/common/src/main/scala/net/psforever/objects/definition/converter/PacketConverter.scala b/common/src/main/scala/net/psforever/objects/definition/converter/PacketConverter.scala new file mode 100644 index 000000000..663b1a2a3 --- /dev/null +++ b/common/src/main/scala/net/psforever/objects/definition/converter/PacketConverter.scala @@ -0,0 +1,71 @@ +// Copyright (c) 2017 PSForever +package net.psforever.objects.definition.converter + +import net.psforever.objects.PlanetSideGameObject +import net.psforever.packet.game.objectcreate.ConstructorData + +import scala.util.{Failure, Try} + +/** + * The base trait for polymorphic assignment for `ObjectCreateConverter`. + */ +sealed trait PacketConverter + +/** + * A converter that accepts an object and prepares it for transformation into an `0x17` packet or an `0x18` packet. + * This is the decoded packet form of the game object, as if hexadecimal data from a packet was decoded. + * @tparam A the type of game object + */ +abstract class ObjectCreateConverter[A <: PlanetSideGameObject] extends PacketConverter { +// def ObjectCreate(obj : A) : Try[ObjectCreateMessage] = { +// Success( +// ObjectCreateMessage(obj.Definition.ObjectId, obj.GUID, +// DroppedItemData( +// PlacementData(obj.Position, obj.Orientation.x.toInt, obj.Orientation.y.toInt, obj.Orientation.z.toInt, Some(obj.Velocity)), +// ConstructorData(obj).get +// ) +// ) +// ) +// } +// +// def ObjectCreate(obj : A, info : PlacementData) : Try[ObjectCreateMessage] = { +// Success(ObjectCreateMessage(obj.Definition.ObjectId, obj.GUID, DroppedItemData(info, ConstructorData(obj).get))) +// } +// +// def ObjectCreate(obj : A, info : ObjectCreateMessageParent) : Try[ObjectCreateMessage] = { +// Success(ObjectCreateMessage(obj.Definition.ObjectId, obj.GUID, info, ConstructorData(obj).get)) +// } +// +// def ObjectCreateDetailed(obj : A) : Try[ObjectCreateDetailedMessage] = { +// Success( +// ObjectCreateDetailedMessage(obj.Definition.ObjectId, obj.GUID, +// DroppedItemData( +// PlacementData(obj.Position, obj.Orientation.x.toInt, obj.Orientation.y.toInt, obj.Orientation.z.toInt, Some(obj.Velocity)), +// DetailedConstructorData(obj).get +// ) +// ) +// ) +// } +// +// def ObjectCreateDetailed(obj : A, info : PlacementData) : Try[ObjectCreateDetailedMessage] = { +// Success(ObjectCreateDetailedMessage(obj.Definition.ObjectId, obj.GUID, DroppedItemData(info, DetailedConstructorData(obj).get))) +// } +// +// def ObjectCreateDetailed(obj : A, info : ObjectCreateMessageParent) : Try[ObjectCreateDetailedMessage] = { +// Success(ObjectCreateDetailedMessage(obj.Definition.ObjectId, obj.GUID, info, DetailedConstructorData(obj).get)) +// } + + /** + * Take a game object and transform it into its equivalent data for an `0x17` packet. + * @param obj the game object + * @return the specific `ConstructorData` that is equivalent to this object + */ + def ConstructorData(obj : A) : Try[ConstructorData] = { Failure(new NoSuchMethodException(s"method not defined for object $obj")) } + + /** + * Take a game object and transform it into its equivalent data for an `0x18` packet. + * @param obj the game object + * @return the specific `ConstructorData` that is equivalent to this object + */ + def DetailedConstructorData(obj : A) : Try[ConstructorData] = { Failure(new NoSuchMethodException(s"method not defined for object $obj")) } +} diff --git a/common/src/main/scala/net/psforever/objects/definition/converter/REKConverter.scala b/common/src/main/scala/net/psforever/objects/definition/converter/REKConverter.scala new file mode 100644 index 000000000..b35c789a4 --- /dev/null +++ b/common/src/main/scala/net/psforever/objects/definition/converter/REKConverter.scala @@ -0,0 +1,17 @@ +// Copyright (c) 2017 PSForever +package net.psforever.objects.definition.converter + +import net.psforever.objects.SimpleItem +import net.psforever.packet.game.objectcreate.{DetailedREKData, REKData} + +import scala.util.{Success, Try} + +class REKConverter extends ObjectCreateConverter[SimpleItem]() { + override def ConstructorData(obj : SimpleItem) : Try[REKData] = { + Success(REKData(8,0)) + } + + override def DetailedConstructorData(obj : SimpleItem) : Try[DetailedREKData] = { + Success(DetailedREKData(8)) + } +} \ No newline at end of file diff --git a/common/src/main/scala/net/psforever/objects/definition/converter/ToolConverter.scala b/common/src/main/scala/net/psforever/objects/definition/converter/ToolConverter.scala new file mode 100644 index 000000000..eb386d6a1 --- /dev/null +++ b/common/src/main/scala/net/psforever/objects/definition/converter/ToolConverter.scala @@ -0,0 +1,30 @@ +// Copyright (c) 2017 PSForever +package net.psforever.objects.definition.converter + +import net.psforever.objects.Tool +import net.psforever.packet.game.objectcreate.{DetailedWeaponData, InternalSlot, WeaponData} + +import scala.collection.mutable.ListBuffer +import scala.util.{Success, Try} + +class ToolConverter extends ObjectCreateConverter[Tool]() { + override def ConstructorData(obj : Tool) : Try[WeaponData] = { + val maxSlot : Int = obj.MaxAmmoSlot + val slots : ListBuffer[InternalSlot] = ListBuffer[InternalSlot]() + (0 until maxSlot).foreach(index => { + val box = obj.AmmoSlots(index).Box + slots += InternalSlot(box.Definition.ObjectId, box.GUID, index, box.Definition.Packet.ConstructorData(box).get) + }) + Success(WeaponData(4,8, obj.FireModeIndex, slots.toList)(maxSlot)) + } + + override def DetailedConstructorData(obj : Tool) : Try[DetailedWeaponData] = { + val maxSlot : Int = obj.MaxAmmoSlot + val slots : ListBuffer[InternalSlot] = ListBuffer[InternalSlot]() + (0 until maxSlot).foreach(index => { + val box = obj.AmmoSlots(index).Box + slots += InternalSlot(box.Definition.ObjectId, box.GUID, index, box.Definition.Packet.DetailedConstructorData(box).get) + }) + Success(DetailedWeaponData(4,8, slots.toList)(maxSlot)) + } +} diff --git a/common/src/main/scala/net/psforever/objects/definition/converter/VehicleConverter.scala b/common/src/main/scala/net/psforever/objects/definition/converter/VehicleConverter.scala new file mode 100644 index 000000000..39ea3403d --- /dev/null +++ b/common/src/main/scala/net/psforever/objects/definition/converter/VehicleConverter.scala @@ -0,0 +1,62 @@ +// Copyright (c) 2017 PSForever +package net.psforever.objects.definition.converter + +import net.psforever.objects.equipment.Equipment +import net.psforever.objects.{EquipmentSlot, Vehicle} +import net.psforever.packet.game.PlanetSideGUID +import net.psforever.packet.game.objectcreate.MountItem.MountItem +import net.psforever.packet.game.objectcreate.{CommonFieldData, DriveState, MountItem, PlacementData, VehicleData} + +import scala.annotation.tailrec +import scala.util.{Success, Try} + +class VehicleConverter extends ObjectCreateConverter[Vehicle]() { + /* Vehicles do not have a conversion for `0x18` packet data. */ + + override def ConstructorData(obj : Vehicle) : Try[VehicleData] = { + Success( + VehicleData( + CommonFieldData( + PlacementData(obj.Position, obj.Orientation, obj.Velocity), + obj.Faction, + 0, + if(obj.Owner.isDefined) { obj.Owner.get } else { PlanetSideGUID(0) } //this is the owner field, right? + ), + 0, + obj.Health / obj.MaxHealth * 255, //TODO not precise + 0, + DriveState.Mobile, + false, + 0, + Some(MakeMountings(obj).sortBy(_.parentSlot)) + ) + ) + //TODO work utilities into this mess? + } + + /** + * For an object with a list of weapon mountings, convert those weapons into data as if found in an `0x17` packet. + * @param obj the Vehicle game object + * @return the converted data + */ + private def MakeMountings(obj : Vehicle) : List[MountItem] = recursiveMakeMountings(obj.Weapons.iterator) + + @tailrec private def recursiveMakeMountings(iter : Iterator[(Int,EquipmentSlot)], list : List[MountItem] = Nil) : List[MountItem] = { + if(!iter.hasNext) { + list + } + else { + val (index, slot) = iter.next + if(slot.Equipment.isDefined) { + val equip : Equipment = slot.Equipment.get + recursiveMakeMountings( + iter, + list :+ MountItem(equip.Definition.ObjectId, equip.GUID, index, equip.Definition.Packet.ConstructorData(equip).get) + ) + } + else { + recursiveMakeMountings(iter, list) + } + } + } +} diff --git a/common/src/main/scala/net/psforever/objects/entity/Identifiable.scala b/common/src/main/scala/net/psforever/objects/entity/Identifiable.scala new file mode 100644 index 000000000..52327008e --- /dev/null +++ b/common/src/main/scala/net/psforever/objects/entity/Identifiable.scala @@ -0,0 +1,13 @@ +// Copyright (c) 2017 PSForever +package net.psforever.objects.entity + +import net.psforever.packet.game.PlanetSideGUID + +/** + * Identifiable represents anything that has its own globally unique identifier (GUID). + */ +trait Identifiable { + def GUID : PlanetSideGUID + + def GUID_=(guid : PlanetSideGUID) : PlanetSideGUID +} diff --git a/common/src/main/scala/net/psforever/objects/entity/IdentifiableEntity.scala b/common/src/main/scala/net/psforever/objects/entity/IdentifiableEntity.scala new file mode 100644 index 000000000..de158182d --- /dev/null +++ b/common/src/main/scala/net/psforever/objects/entity/IdentifiableEntity.scala @@ -0,0 +1,96 @@ +// Copyright (c) 2017 PSForever +package net.psforever.objects.entity + +import net.psforever.packet.game.PlanetSideGUID + +/** + * Represent any entity that must have its own globally unique identifier (GUID) to be functional.
+ *
+ * "Testing" the object refers to the act of acquiring a reference to the GUID the object is using. + * This object starts with a container class that represents a unprepared GUID state and raises an `Exception` when tested. + * Setting a proper `PlanetSideGUID` replaces that container class with a container class that returns the GUID when tested. + * The object can be invalidated, restoring the previous `Exception`-raising condition. + * @throws `NoGUIDException` if there is no GUID to give + */ +abstract class IdentifiableEntity extends Identifiable { + private val container : GUIDContainable = GUIDContainer() + private var current : GUIDContainable = IdentifiableEntity.noGUIDContainer + + def HasGUID : Boolean = { + try { + GUID + true + } + catch { + case _ : NoGUIDException => + false + } + } + + def GUID : PlanetSideGUID = current.GUID + + def GUID_=(guid : PlanetSideGUID) : PlanetSideGUID = { + current = container + current.GUID = guid + GUID + } + + def Invalidate() : Unit = { + current = IdentifiableEntity.noGUIDContainer + } +} + +object IdentifiableEntity { + private val noGUIDContainer : GUIDContainable = new NoGUIDContainer +} + +/** + * Mask the `Identifiable` `trait`. + */ +sealed trait GUIDContainable extends Identifiable + +/** + * Hidden container that represents an object that is not ready to be used by the game. + */ +private case class NoGUIDContainer() extends GUIDContainable { + /** + * Raise an `Exception` because we have no GUID to give. + * @throws `NoGUIDException` always + * @return never returns + */ + def GUID : PlanetSideGUID = { + throw NoGUIDException("object has not initialized a global identifier") + } + + /** + * Normally, this should never be called. + * @param toGuid the globally unique identifier + * @return never returns + */ + def GUID_=(toGuid : PlanetSideGUID) : PlanetSideGUID = { + throw NoGUIDException("can not initialize a global identifier with this object") + } +} + +/** + * Hidden container that represents an object that has a working GUID and is ready to be used by the game. + * @param guid the object's globally unique identifier; + * defaults to a GUID equal to 0 + */ +private case class GUIDContainer(private var guid : PlanetSideGUID = PlanetSideGUID(0)) extends GUIDContainable { + /** + * Provide the GUID used to initialize this object. + * @return the GUID + */ + def GUID : PlanetSideGUID = guid + + /** + * Exchange the previous GUID for a new one, re-using this container. + * @param toGuid the globally unique identifier + * @return the GUID + */ + def GUID_=(toGuid : PlanetSideGUID) : PlanetSideGUID = { + guid = toGuid + GUID + } +} diff --git a/common/src/main/scala/net/psforever/objects/entity/MobileWorldEntity.scala b/common/src/main/scala/net/psforever/objects/entity/MobileWorldEntity.scala new file mode 100644 index 000000000..33de4798f --- /dev/null +++ b/common/src/main/scala/net/psforever/objects/entity/MobileWorldEntity.scala @@ -0,0 +1,45 @@ +// Copyright (c) 2017 PSForever +package net.psforever.objects.entity + +import net.psforever.types.Vector3 + +import scala.collection.mutable + +class MobileWorldEntity extends WorldEntity { + private var coords : mutable.Stack[TimeEntry] = mutable.Stack(TimeEntry.invalid) //history of last #n positional updates + private var orient : mutable.Stack[TimeEntry] = mutable.Stack(TimeEntry.invalid) //history of last #n orientation updates + private var vel : Option[Vector3] = None + + def Position : Vector3 = coords.head.entry + + def Position_=(vec : Vector3) : Vector3 = { + coords = MobileWorldEntity.pushNewStack(coords, vec, SimpleWorldEntity.validatePositionEntry) + Position + } + + def AllPositions : scala.collection.immutable.List[TimeEntry] = coords.toList + + def Orientation : Vector3 = orient.head.entry + + def Orientation_=(vec : Vector3) : Vector3 = { + orient = MobileWorldEntity.pushNewStack(orient, vec, SimpleWorldEntity.validateOrientationEntry) + Orientation + } + + def AllOrientations : scala.collection.immutable.List[TimeEntry] = orient.toList + + def Velocity : Option[Vector3] = vel + + def Velocity_=(vec : Option[Vector3]) : Option[Vector3] = { + vel = vec + vel + } + + override def toString : String = WorldEntity.toString(this) +} + +object MobileWorldEntity { + def pushNewStack(lst : mutable.Stack[TimeEntry], newEntry : Vector3, validate : (Vector3) => Vector3) : mutable.Stack[TimeEntry] = { + lst.slice(0, 199).push(TimeEntry(validate(newEntry))) + } +} diff --git a/common/src/main/scala/net/psforever/objects/entity/NoGUIDException.scala b/common/src/main/scala/net/psforever/objects/entity/NoGUIDException.scala new file mode 100644 index 000000000..9685b27ad --- /dev/null +++ b/common/src/main/scala/net/psforever/objects/entity/NoGUIDException.scala @@ -0,0 +1,6 @@ +// Copyright (c) 2017 PSForever +package net.psforever.objects.entity + +case class NoGUIDException(private val message: String = "", + private val cause: Throwable = None.orNull + ) extends RuntimeException(message, cause) diff --git a/common/src/main/scala/net/psforever/objects/entity/SimpleWorldEntity.scala b/common/src/main/scala/net/psforever/objects/entity/SimpleWorldEntity.scala new file mode 100644 index 000000000..704dab21e --- /dev/null +++ b/common/src/main/scala/net/psforever/objects/entity/SimpleWorldEntity.scala @@ -0,0 +1,52 @@ +// Copyright (c) 2017 PSForever +package net.psforever.objects.entity + +import net.psforever.types.Vector3 + +class SimpleWorldEntity extends WorldEntity { + private var coords : Vector3 = Vector3(0f, 0f, 0f) + private var orient : Vector3 = Vector3(0f, 0f, 0f) + private var vel : Option[Vector3] = None + + def Position : Vector3 = coords + + def Position_=(vec : Vector3) : Vector3 = { + coords = SimpleWorldEntity.validatePositionEntry(vec) + Position + } + + def Orientation : Vector3 = orient + + def Orientation_=(vec : Vector3) : Vector3 = { + orient = SimpleWorldEntity.validateOrientationEntry(vec) + Orientation + } + + def Velocity : Option[Vector3] = vel + + def Velocity_=(vec : Option[Vector3]) : Option[Vector3] = { + vel = vec + Velocity + } + + override def toString : String = WorldEntity.toString(this) +} + +object SimpleWorldEntity { + def validatePositionEntry(vec : Vector3) : Vector3 = vec + + def validateOrientationEntry(vec : Vector3) : Vector3 = { + val x = clampAngle(vec.x) + val y = clampAngle(vec.y) + val z = clampAngle(vec.z) + Vector3(x, y, z) + } + + def clampAngle(ang : Float) : Float = { + var ang2 = ang % 360f + if(ang2 < 0f) { + ang2 += 360f + } + ang2 + } +} diff --git a/common/src/main/scala/net/psforever/objects/entity/TimeEntry.scala b/common/src/main/scala/net/psforever/objects/entity/TimeEntry.scala new file mode 100644 index 000000000..f6fcc417d --- /dev/null +++ b/common/src/main/scala/net/psforever/objects/entity/TimeEntry.scala @@ -0,0 +1,13 @@ +// Copyright (c) 2017 PSForever +package net.psforever.objects.entity + +import net.psforever.types.Vector3 + +case class TimeEntry(entry : net.psforever.types.Vector3)(implicit time : Long = org.joda.time.DateTime.now.getMillis) + +object TimeEntry { + val invalid = TimeEntry(Vector3(0f, 0f, 0f))(0L) + + def apply(x : Float, y : Float, z : Float) : TimeEntry = + TimeEntry(Vector3(x, y, z)) +} diff --git a/common/src/main/scala/net/psforever/objects/entity/WorldEntity.scala b/common/src/main/scala/net/psforever/objects/entity/WorldEntity.scala new file mode 100644 index 000000000..2d742603d --- /dev/null +++ b/common/src/main/scala/net/psforever/objects/entity/WorldEntity.scala @@ -0,0 +1,26 @@ +// Copyright (c) 2017 PSForever +package net.psforever.objects.entity + +import net.psforever.types.Vector3 + +trait WorldEntity { + def Position : Vector3 + + def Position_=(vec : Vector3) : Vector3 + + def Orientation : Vector3 + + def Orientation_=(vec : Vector3) : Vector3 + + def Velocity : Option[Vector3] + + def Velocity_=(vec : Option[Vector3]) : Option[Vector3] + + def Velocity_=(vec : Vector3) : Option[Vector3] = Velocity = Some(vec) +} + +object WorldEntity { + def toString(obj : WorldEntity) : String = { + s"pos=${obj.Position}, ori=${obj.Orientation}" + } +} diff --git a/common/src/main/scala/net/psforever/objects/equipment/Ammo.scala b/common/src/main/scala/net/psforever/objects/equipment/Ammo.scala new file mode 100644 index 000000000..e7228abcb --- /dev/null +++ b/common/src/main/scala/net/psforever/objects/equipment/Ammo.scala @@ -0,0 +1,93 @@ +// Copyright (c) 2017 PSForever +package net.psforever.objects.equipment + +/** + * An `Enumeration` of all the ammunition types in the game, paired with their object id as the `Value`. + */ +object Ammo extends Enumeration { + final val bullet_105mm = Value(0) + final val bullet_12mm = Value(3) + final val bullet_150mm = Value(6) + final val bullet_15mm = Value(9) + final val bullet_20mm = Value(16) + final val bullet_25mm = Value(19) + final val bullet_35mm = Value(21) + final val bullet_75mm = Value(25) + final val bullet_9mm = Value(28) + final val bullet_9mm_AP = Value(29) + final val ancient_ammo_combo = Value(50) + final val ancient_ammo_vehicle = Value(51) + final val anniversary_ammo = Value(54) + final val aphelion_immolation_cannon_ammo = Value(86) + final val aphelion_laser_ammo = Value(89) + final val aphelion_plasma_rocket_ammo = Value(97) + final val aphelion_ppa_ammo = Value(101) + final val aphelion_starfire_ammo = Value(106) + final val armor_canister = Value(111) + final val armor_siphon_ammo = Value(112) + final val bolt = Value(145) + final val burster_ammo = Value(154) + final val colossus_100mm_cannon_ammo = Value(180) + final val colossus_burster_ammo = Value(186) + final val colossus_chaingun_ammo = Value(191) + final val colossus_cluster_bomb_ammo = Value(195) + final val colossus_tank_cannon_ammo = Value(205) + final val comet_ammo = Value(209) + final val dualcycler_ammo = Value(265) + final val energy_cell = Value(272) + final val energy_gun_ammo = Value(275) + final val falcon_ammo = Value(285) + final val firebird_missile = Value(287) + final val flamethrower_ammo = Value(300) + final val flux_cannon_thresher_battery = Value(307) + final val fluxpod_ammo = Value(310) + final val frag_cartridge = Value(327) + final val frag_grenade_ammo = Value(331) + final val gauss_cannon_ammo = Value(345) + final val grenade = Value(370) + final val health_canister = Value(389) + final val heavy_grenade_mortar = Value(391) + final val heavy_rail_beam_battery = Value(393) + final val hellfire_ammo = Value(399) + final val hunter_seeker_missile = Value(403) + final val jammer_cartridge = Value(413) + final val jammer_grenade_ammo = Value(417) + final val lancer_cartridge = Value(426) + final val liberator_bomb = Value(434) + final val maelstrom_ammo = Value(463) + final val melee_ammo = Value(540) + final val mine = Value(550) + final val mine_sweeper_ammo = Value(553) + final val ntu_siphon_ammo = Value(595) + final val oicw_ammo = Value(600) + final val pellet_gun_ammo = Value(630) + final val peregrine_dual_machine_gun_ammo = Value(637) + final val peregrine_mechhammer_ammo = Value(645) + final val peregrine_particle_cannon_ammo = Value(653) + final val peregrine_rocket_pod_ammo = Value(656) + final val peregrine_sparrow_ammo = Value(659) + final val phalanx_ammo = Value(664) + final val phoenix_missile = Value(674) + final val plasma_cartridge = Value(677) + final val plasma_grenade_ammo = Value(681) + final val pounder_ammo = Value(693) + final val pulse_battery = Value(704) + final val quasar_ammo = Value(712) + final val reaver_rocket = Value(722) + final val rocket = Value(734) + final val scattercannon_ammo = Value(745) + final val shotgun_shell = Value(755) + final val shotgun_shell_AP = Value(756) + final val six_shooter_ammo = Value(762) + final val skyguard_flak_cannon_ammo = Value(786) + final val sparrow_ammo = Value(791) + final val spitfire_aa_ammo = Value(820) + final val spitfire_ammo = Value(823) + final val starfire_ammo = Value(830) + final val striker_missile_ammo = Value(839) + final val trek_ammo = Value(877) + final val upgrade_canister = Value(922) + final val wasp_gun_ammo = Value(998) + final val wasp_rocket_ammo = Value(1000) + final val winchester_ammo = Value(1004) +} diff --git a/common/src/main/scala/net/psforever/objects/equipment/CItem.scala b/common/src/main/scala/net/psforever/objects/equipment/CItem.scala new file mode 100644 index 000000000..0582bfded --- /dev/null +++ b/common/src/main/scala/net/psforever/objects/equipment/CItem.scala @@ -0,0 +1,28 @@ +// Copyright (c) 2017 PSForever +package net.psforever.objects.equipment + +object CItem { + object Unit extends Enumeration { + final val ace = Value(32) + final val advanced_ace = Value(39) //fdu + final val router_telepad = Value(743) + } + + object DeployedItem extends Enumeration { + final val boomer = Value(148) + final val deployable_shield_generator = Value(240) + final val he_mine = Value(388) + final val jammer_mine = Value(420) //disruptor mine + final val motionalarmsensor = Value(575) + final val sensor_shield = Value(752) //sensor disruptor + final val spitfire_aa = Value(819) //cerebus turret + final val spitfire_cloaked = Value(825) //shadow turret + final val spitfire_turret = Value(826) + final val tank_traps = Value(849) //trap + final val portable_manned_turret = Value(685) + final val portable_manned_turret_nc = Value(686) + final val portable_manned_turret_tr = Value(687) + final val portable_manned_turret_vs = Value(688) + final val router_telepad_deployable = Value(744) + } +} diff --git a/common/src/main/scala/net/psforever/objects/equipment/Equipment.scala b/common/src/main/scala/net/psforever/objects/equipment/Equipment.scala new file mode 100644 index 000000000..6bf28bd14 --- /dev/null +++ b/common/src/main/scala/net/psforever/objects/equipment/Equipment.scala @@ -0,0 +1,21 @@ +// Copyright (c) 2017 PSForever +package net.psforever.objects.equipment + +import net.psforever.objects.PlanetSideGameObject +import net.psforever.objects.definition.EquipmentDefinition +import net.psforever.objects.inventory.InventoryTile + +/** + * `Equipment` is anything that can be: + * placed into a slot of a certain "size"; + * and, placed into an inventory system; + * and, special carried (like a lattice logic unit); + * and, dropped on the ground in the game world and render where it was deposited. + */ +abstract class Equipment extends PlanetSideGameObject { + def Size : EquipmentSize.Value = Definition.Size + + def Tile : InventoryTile = Definition.Tile + + def Definition : EquipmentDefinition +} diff --git a/common/src/main/scala/net/psforever/objects/equipment/EquipmentSize.scala b/common/src/main/scala/net/psforever/objects/equipment/EquipmentSize.scala new file mode 100644 index 000000000..484d91609 --- /dev/null +++ b/common/src/main/scala/net/psforever/objects/equipment/EquipmentSize.scala @@ -0,0 +1,38 @@ +// Copyright (c) 2017 PSForever +package net.psforever.objects.equipment + +object EquipmentSize extends Enumeration { + val + Blocked, + Melee, //special + Pistol, //2x2 and 3x3 + Rifle, //6x3 and 9x3 + Max, //max weapon only + VehicleWeapon, //vehicle-mounted weapons + Inventory, //reserved + Any + = Value + + /** + * Perform custom size comparison.
+ *
+ * In almost all cases, the only time two sizes are equal is if they are the same size. + * If either size is `Blocked`, however, they will never be equal. + * If either size is `Inventory` or `Any`, however, they will always be equal. + * Size comparison is important for putting `Equipment` in size-fitted slots, but not for much else. + * @param type1 the first size + * @param type2 the second size + * @return `true`, if they are equal; `false`, otherwise + */ + def isEqual(type1 : EquipmentSize.Value, type2 : EquipmentSize.Value) : Boolean = { + if(type1 >= Inventory || type2 >= Inventory) { + true + } + else if(type1 == Blocked || type2 == Blocked) { + false + } + else { + type1 == type2 + } + } +} diff --git a/common/src/main/scala/net/psforever/objects/equipment/FireModeDefinition.scala b/common/src/main/scala/net/psforever/objects/equipment/FireModeDefinition.scala new file mode 100644 index 000000000..2c02a54c8 --- /dev/null +++ b/common/src/main/scala/net/psforever/objects/equipment/FireModeDefinition.scala @@ -0,0 +1,69 @@ +// Copyright (c) 2017 PSForever +package net.psforever.objects.equipment + +import scala.collection.mutable + +class FireModeDefinition { +// private var ammoTypes : mutable.ListBuffer[Ammo.Value] = mutable.ListBuffer[Ammo.Value]() //ammo types valid for this fire mode + private val ammoTypeIndices : mutable.ListBuffer[Int] = mutable.ListBuffer[Int]() //indices pointing to all ammo types used + private var ammoSlotIndex : Int = 0 //ammunition slot number this fire mode utilizes + private var chamber : Int = 1 //how many rounds are queued to be fired at once, e.g., 3 for the Jackhammer's triple burst + private var magazine : Int = 1 //how many rounds are queued for each reload cycle + private var target : Any = _ //target designation (self? other?) + private var resetAmmoIndexOnSwap : Boolean = false //when changing fire modes, do not attempt to match previous mode's ammo type + + //damage modifiers will follow here ... + +// def AmmoTypes : mutable.ListBuffer[Ammo.Value] = ammoTypes +// +// def AmmoTypes_=(ammo : Ammo.Value) : mutable.ListBuffer[Ammo.Value] = { +// ammoTypes += ammo +// } + + def AmmoSlotIndex : Int = ammoSlotIndex + + def AmmoSlotIndex_=(index : Int) : Int = { + ammoSlotIndex = index + AmmoSlotIndex + } + + def AmmoTypeIndices : mutable.ListBuffer[Int] = ammoTypeIndices + + def AmmoTypeIndices_=(index : Int) : mutable.ListBuffer[Int] = { + ammoTypeIndices += index + } + + def Chamber : Int = chamber + + def Chamber_=(inChamber : Int) : Int = { + chamber = inChamber + Chamber + } + + def Magazine : Int = magazine + + def Magazine_=(inMagazine : Int) : Int = { + magazine = inMagazine + Magazine + } + + def Target : Any = target + + def Target_+(setAsTarget : Any) : Any = { + target = setAsTarget + Target + } + + def ResetAmmoIndexOnSwap : Boolean = resetAmmoIndexOnSwap + + def ResetAmmoIndexOnSwap_=(reset : Boolean) : Boolean = { + resetAmmoIndexOnSwap = reset + ResetAmmoIndexOnSwap + } +} + +object FireModeDefinition { + def apply() : FireModeDefinition = { + new FireModeDefinition() + } +} diff --git a/common/src/main/scala/net/psforever/objects/equipment/FireModeSwitch.scala b/common/src/main/scala/net/psforever/objects/equipment/FireModeSwitch.scala new file mode 100644 index 000000000..6a3596cc5 --- /dev/null +++ b/common/src/main/scala/net/psforever/objects/equipment/FireModeSwitch.scala @@ -0,0 +1,21 @@ +// Copyright (c) 2017 PSForever +package net.psforever.objects.equipment + +/** + * Fire mode is a non-complex method of representing variance in `Equipment` output.
+ *
+ * All weapons and some support items have fire modes, though most only have one. + * The number of fire modes is visually indicated by the bubbles next to the icon of the `Equipment` in a holster slot. + * The specifics of how a fire mode affects the output is left to implementation and execution. + * Contrast how `Tool`s deal with multiple types of ammunition. + * @tparam Mode the type parameter representing the fire mode + */ +trait FireModeSwitch[Mode] { + def FireModeIndex : Int + + def FireModeIndex_=(index : Int) : Int + + def FireMode : Mode + + def NextFireMode : Mode +} \ No newline at end of file diff --git a/common/src/main/scala/net/psforever/objects/equipment/Kits.scala b/common/src/main/scala/net/psforever/objects/equipment/Kits.scala new file mode 100644 index 000000000..daffc3655 --- /dev/null +++ b/common/src/main/scala/net/psforever/objects/equipment/Kits.scala @@ -0,0 +1,12 @@ +// Copyright (c) 2017 PSForever +package net.psforever.objects.equipment + +/** + * An `Enumeration` of the kit types in the game, paired with their object id as the `Value`. + */ +object Kits extends Enumeration { + final val medkit = Value(536) + final val super_armorkit = Value(842) //super repair kit + final val super_medkit = Value(843) + final val super_staminakit = Value(844) //super stimpack +} diff --git a/common/src/main/scala/net/psforever/objects/equipment/SItem.scala b/common/src/main/scala/net/psforever/objects/equipment/SItem.scala new file mode 100644 index 000000000..8aea0f858 --- /dev/null +++ b/common/src/main/scala/net/psforever/objects/equipment/SItem.scala @@ -0,0 +1,9 @@ +// Copyright (c) 2017 PSForever +package net.psforever.objects.equipment + +object SItem extends Enumeration { + final val boomer_trigger = Value(149) + final val command_detonater = Value(213) //cud + final val flail_targeting_laser = Value(297) + final val remote_electronics_kit = Value(728) +} diff --git a/common/src/main/scala/net/psforever/objects/guid/AvailabilityPolicy.scala b/common/src/main/scala/net/psforever/objects/guid/AvailabilityPolicy.scala new file mode 100644 index 000000000..2775de758 --- /dev/null +++ b/common/src/main/scala/net/psforever/objects/guid/AvailabilityPolicy.scala @@ -0,0 +1,21 @@ +// Copyright (c) 2017 PSForever +package net.psforever.objects.guid + +/** + * The availability of individual GUIDs is maintained by the given policy. + */ +object AvailabilityPolicy extends Enumeration { + type Type = Value + + /** + * An `AVAILABLE` GUID is ready and waiting to be `LEASED` for use. + * A `LEASED` GUID has been issued and is currently being used. + * A `RESTRICTED` GUID can never be freed. It is allowed, however, to be assigned once as if it were `LEASED`. + */ + val + Available, + Leased, + Restricted + = Value +} + diff --git a/common/src/main/scala/net/psforever/objects/guid/NumberPoolHub.scala b/common/src/main/scala/net/psforever/objects/guid/NumberPoolHub.scala new file mode 100644 index 000000000..e360df533 --- /dev/null +++ b/common/src/main/scala/net/psforever/objects/guid/NumberPoolHub.scala @@ -0,0 +1,474 @@ +// Copyright (c) 2017 PSForever +package net.psforever.objects.guid + +import net.psforever.objects.entity.{IdentifiableEntity, NoGUIDException} +import net.psforever.objects.guid.key.LoanedKey +import net.psforever.objects.guid.pool.{ExclusivePool, GenericPool, NumberPool} +import net.psforever.objects.guid.source.NumberSource +import net.psforever.packet.game.PlanetSideGUID + +import scala.util.{Failure, Success, Try} + +/** + * A master object that manages `NumberPool`s when they are applied to a single `NumberSource`. + * It catalogs the numbers and ensures the pool contents are unique to each other.
+ *
+ * All globally unique numbers are sorted into user-defined groups called pools. + * Pools are intended to pre-allocate certain numbers to certain tasks. + * Two default pools also exist - "generic," for all numbers not formally placed into a pool, and a hidden restricted pool. + * The former can accept a variety of numbers on the source not known at initialization time loaded into it. + * The latter can only be set by the `NumberSource` and can not be affected once this object is created. + * @param source the number source object + */ +class NumberPoolHub(private val source : NumberSource) { + import scala.collection.mutable + private val hash : mutable.HashMap[String, NumberPool] = mutable.HashMap[String, NumberPool]() + private val bigpool : mutable.LongMap[String] = mutable.LongMap[String]() + hash += "generic" -> new GenericPool(bigpool, source.Size) + source.FinalizeRestrictions.foreach(i => bigpool += i.toLong -> "") //these numbers can never be pooled; the source can no longer restrict numbers + + /** + * Given a globally unique identifier, rweturn any object registered to it.
+ *
+ * Use:
+ * For `val obj = new NumberPoolHub(...)` use `obj(number)`. + * @param number the unique number to attempt to retrieve from the `source` + * @return the object that is assigned to the number + */ + def apply(number : PlanetSideGUID) : Option[IdentifiableEntity] = this(number.guid) + + /** + * Given a globally unique identifier, rweturn any object registered to it.
+ *
+ * Use:
+ * For `val obj = new NumberPoolHub(...)` use `obj(number)`. + * @param number the unique number to attempt to retrieve from the `source` + * @return the object that is assigned to the number + */ + def apply(number : Int) : Option[IdentifiableEntity] = source.Get(number).orElse(return None).get.Object + + def Numbers : List[Int] = bigpool.keys.map(key => key.toInt).toList + + /** + * Create a new number pool with the given label and the given numbers.
+ *
+ * Creating number pools is a task that should only be performed at whatever counts as the initialization stage. + * Nothing technically blocks it being done during runtime; + * however, stability is best served by doing it only once and while nothing else risk affecting the numbers. + * Unlike "live" functionality which often returns as `Success` or `Failure`, this is considered a critical operation. + * As thus, `Exceptions` are permitted since a fault of the pool's creation will disrupt normal operations. + * @param name the name of the pool + * @param pool the `List` of numbers that will belong to the pool + * @return the newly-created number pool + * @throws IllegalArgumentException if the pool is already defined; + * if the pool contains numbers the source does not + * if the pool contains numbers from already existing pools + */ + def AddPool(name : String, pool : List[Int]) : NumberPool = { + if(hash.get(name).isDefined) { + throw new IllegalArgumentException(s"can not add pool $name - name already known to this hub?") + } + if(source.Size <= pool.max) { + throw new IllegalArgumentException(s"can not add pool $name - max(pool) is greater than source.size") + } + val collision = bigpool.keys.map(n => n.toInt).toSet.intersect(pool.toSet) + if(collision.nonEmpty) { + throw new IllegalArgumentException(s"can not add pool $name - it contains the following redundant numbers: ${collision.toString}") + } + pool.foreach(i => bigpool += i.toLong -> name) + hash += name -> new ExclusivePool(pool) + hash(name) + } + + /** + * Remove an existing number pool with the given label from the list of number pools.
+ *
+ * Removing number pools is a task that should only be performed at whatever counts as the termination stage. + * All the same reasoning applies as with `AddPool` above. + * Although an easy operation would move all the assigned numbers in the removing pool to the "generic" pool, + * doing so is ill-advised both for the reasoning above and because that creates unreliability. + * @param name the name of the pool + * @return the `List` of numbers that belonged to the pool + * @throws IllegalArgumentException if the pool doesn't exist or is not removed (removable) + */ + def RemovePool(name : String) : List[Int] = { + if(name.equals("generic") || name.equals("")) { + throw new IllegalArgumentException("can not remove pool - generic or restricted") + } + val pool = hash.get(name).orElse({ + throw new IllegalArgumentException(s"can not remove pool - $name does not exist") + }).get + if(pool.Count > 0) { + throw new IllegalArgumentException(s"can not remove pool - $name is being used") + } + + hash.remove(name) + pool.Numbers.foreach(number => bigpool -= number) + pool.Numbers + } + + /** + * Get the number pool known by this name. + * It will not return correctly for any number that is in the "restricted" pool. + * @param name the name of the pool + * @return a reference to the number pool, or `None` + */ + def GetPool(name : String) : Option[NumberPool] = if(name.equals("")) { None } else { hash.get(name) } + + /** + * na + * @return na + */ + def Pools : mutable.HashMap[String, NumberPool] = hash + + /** + * Reference a specific number's pool.
+ *
+ * `WhichPool(Int)` does not require the number to be registered at the time it is used. + * It does not return anything for an unregistered unpooled number - + * a number that would be part of the "generic" nonstandard pool. + * It only reports "generic" if that number is registered. + * It will not return correctly for any number that is in the "restricted" pool. + * @param number a number + * @return the name of the number pool to which this item belongs + */ + def WhichPool(number : Int) : Option[String] = { + val name = bigpool.get(number) + if(name.contains("")) { None } else { name } + } + + /** + * Reference a specific number's pool.
+ *
+ * `WhichPool(IdentifiableEntity)` does require the object to be registered to be found. + * It checks that the object is registered, and that it is registered to the local source object. + * @param obj an object + * @return the name of the number pool to which this item belongs + */ + def WhichPool(obj : IdentifiableEntity) : Option[String] = { + try { + val number : Int = obj.GUID.guid + val entry = source.Get(number) + if(entry.isDefined && entry.get.Object.contains(obj)) { WhichPool(number) } else { None } + } + catch { + case _ : Exception => + None + } + } + + /** + * Register an object to any available selection (of the "generic" number pool). + * @param obj an object being registered + * @return the number the was given to the object + */ + def register(obj : IdentifiableEntity) : Try[Int] = register(obj, "generic") + + /** + * Register an object to a specific number if it is available. + * @param obj an object being registered + * @param number the number whose assignment is requested + * @return the number the was given to the object + */ + def register(obj : IdentifiableEntity, number : Int) : Try[Int] = { + bigpool.get(number.toLong) match { + case Some(name) => + register_GetSpecificNumberFromPool(name, number) match { + case Success(key) => + key.Object = obj + Success(obj.GUID.guid) + case Failure(ex) => + Failure(new Exception(s"trying to register an object to a specific number but, ${ex.getMessage}")) + } + case None => + import net.psforever.objects.guid.selector.SpecificSelector + hash("generic").Selector.asInstanceOf[SpecificSelector].SelectionIndex = number + register(obj, "generic") + } + } + + /** + * Asides from using the `name` parameter to find the number pool, + * this method also removes the `number` from that number pool of its own accord. + * The "{pool}.Selector = new SpecificSelector" technique is used to safely remove the number. + * It will disrupt the internal order of the number pool set by its current selector and reset it to a neutral state. + * @param name the local pool name + * @param number the number whose assignment is requested + * @return the number the was given to the object + * @see `NumberPool.Selector_=(NumberSelector)` + */ + private def register_GetSpecificNumberFromPool(name : String, number : Int) : Try[LoanedKey]= { + hash.get(name) match { + case Some(pool) => + val slctr = pool.Selector + import net.psforever.objects.guid.selector.SpecificSelector + val specific = new SpecificSelector + specific.SelectionIndex = number + pool.Selector = specific + pool.Get() + pool.Selector = slctr + register_GetAvailableNumberFromSource(number) + case None => + Failure(new Exception(s"number pool $name not defined")) + } + } + + private def register_GetAvailableNumberFromSource(number : Int) : Try[LoanedKey] = { + source.Available(number) match { + case Some(key) => + Success(key) + case None => + Failure(new Exception(s"number $number is unavailable")) + } + } + + /** + * Register an object to a specific number pool. + * @param obj an object being registered + * @param name the local pool name + * @return the number the was given to the object + */ + def register(obj : IdentifiableEntity, name : String) : Try[Int] = { + try { + register_CheckNumberAgainstDesiredPool(obj, name, obj.GUID.guid) + } + catch { + case _ : Exception => + register_GetPool(name) match { + case Success(key) => + key.Object = obj + Success(obj.GUID.guid) + case Failure(ex) => + Failure(new Exception(s"trying to register an object but, ${ex.getMessage}")) + } + } + } + + private def register_CheckNumberAgainstDesiredPool(obj : IdentifiableEntity, name : String, number : Int) : Try[Int] = { + val directKey = source.Get(number) + if(directKey.isEmpty || !directKey.get.Object.contains(obj)) { + Failure(new Exception("object already registered, but not to this source")) + } + else if(!WhichPool(number).contains(name)) { + //TODO obj is not registered to the desired pool; is this okay? + Success(number) + } + else { + Success(number) + } + } + + private def register_GetPool(name : String) : Try[LoanedKey] = { + hash.get(name) match { + case Some(pool) => + register_GetNumberFromDesiredPool(pool) + case _ => + Failure(new Exception(s"number pool $name not defined")) + } + } + + private def register_GetNumberFromDesiredPool(pool : NumberPool) : Try[LoanedKey] = { + pool.Get() match { + case Success(number) => + register_GetMonitorFromSource(number) + case Failure(ex) => + Failure(ex) + } + } + + private def register_GetMonitorFromSource(number : Int) : Try[LoanedKey] = { + source.Available(number) match { + case Some(key) => + Success(key) + case _ => + throw NoGUIDException(s"a pool gave us a number $number that is actually unavailable") //stop the show; this is terrible! + } + } + + /** + * Register a specific number. + * @param number the number whose assignment is requested + * @return the monitor for a number + */ + def register(number : Int) : Try[LoanedKey] = { + WhichPool(number) match { + case None => + import net.psforever.objects.guid.selector.SpecificSelector + hash("generic").Selector.asInstanceOf[SpecificSelector].SelectionIndex = number + register_GetPool("generic") + case Some(name) => + register_GetSpecificNumberFromPool(name, number) + } + } + + /** + * Register a number selected automatically from the named pool. + * @param name the local pool name + * @return the monitor for a number + */ + def register(name : String) : Try[LoanedKey] = register_GetPool(name) + + /** + * na + * @param obj an object being registered + * @param number the number whose assignment is requested + * @return an object that has been registered + */ + def latterPartRegister(obj : IdentifiableEntity, number : Int) : Try[IdentifiableEntity] = { + register_GetMonitorFromSource(number) match { + case Success(monitor) => + monitor.Object = obj + Success(obj) + case Failure(ex) => + Failure(ex) + } + } + + /** + * Unregister a specific object. + * @param obj an object being unregistered + * @return the number previously associated with the object + */ + def unregister(obj : IdentifiableEntity) : Try[Int] = { + unregister_GetPoolFromObject(obj) match { + case Success(pool) => + val number = obj.GUID.guid + pool.Return(number) + source.Return(number) + obj.Invalidate() + Success(number) + case Failure(ex) => + Failure(new Exception(s"can not unregister this object: ${ex.getMessage}")) + } + } + + def unregister_GetPoolFromObject(obj : IdentifiableEntity) : Try[NumberPool] = { + WhichPool(obj) match { + case Some(name) => + unregister_GetPool(name) + case None => + Failure(throw new Exception("can not find a pool for this object")) + } + } + + private def unregister_GetPool(name : String) : Try[NumberPool] = { + hash.get(name) match { + case Some(pool) => + Success(pool) + case None => + Failure(new Exception(s"no pool by the name of '$name'")) + } + } + + /** + * Unregister a specific number. + * @param number the number previously assigned(?) + * @return the object, if any, previous associated with the number + */ + def unregister(number : Int) : Try[Option[IdentifiableEntity]] = { + if(source.Test(number)) { + unregister_GetObjectFromSource(number) + } + else { + Failure(new Exception(s"can not unregister a number $number that this source does not own") ) + } + } + + private def unregister_GetObjectFromSource(number : Int) : Try[Option[IdentifiableEntity]] = { + source.Return(number) match { + case Some(obj) => + unregister_ReturnObjectToPool(obj) + case None => + unregister_ReturnNumberToPool(number) //nothing is wrong, but we'll check the pool + } + } + + private def unregister_ReturnObjectToPool(obj : IdentifiableEntity) : Try[Option[IdentifiableEntity]] = { + val number = obj.GUID.guid + unregister_GetPoolFromNumber(number) match { + case Success(pool) => + pool.Return(number) + obj.Invalidate() + Success(Some(obj)) + case Failure(ex) => + source.Available(number) //undo + Failure(new Exception(s"started unregistering, but ${ex.getMessage}")) + } + } + + private def unregister_ReturnNumberToPool(number : Int) : Try[Option[IdentifiableEntity]] = { + unregister_GetPoolFromNumber(number) match { + case Success(pool) => + pool.Return(number) + Success(None) + case _ => //though everything else went fine, we must still fail if this number was restricted all along + if(!bigpool.get(number).contains("")) { + Success(None) + } + else { + Failure(new Exception(s"can not unregister this number $number")) + } + } + } + + private def unregister_GetPoolFromNumber(number : Int) : Try[NumberPool] = { + WhichPool(number) match { + case Some(name) => + unregister_GetPool(name) + case None => + Failure(new Exception(s"no pool using number $number")) + } + } + + /** + * For accessing the `Return` function of the contained `NumberSource` directly. + * @param number the number to return. + * @return any object previously using this number + */ + def latterPartUnregister(number : Int) : Option[IdentifiableEntity] = source.Return(number) + + /** + * Determines if the object is registered.
+ *
+ * Three conditions are necessary to determine this condition for objects. + * (1) A registered object has a globally unique identifier. + * (2) A registered object is known to the `source` by that identifier. + * (3) The registered object can be found attached to that entry from the source. + * @param obj an object + * @return `true`, if the number is registered; `false`, otherwise + * @see `isRegistered(Int)` + */ + def isRegistered(obj : IdentifiableEntity) : Boolean = { + try { + source.Get(obj.GUID.guid) match { + case Some(monitor) => + monitor.Object.contains(obj) + case None => + false + } + } + catch { + case _ : NoGUIDException => + false + } + } + + /** + * Determines if the number is registered.
+ *
+ * Two conditions are necessary to determine this condition for numbers. + * (1) A registered number is known to the `source`. + * (2) A register number is known as `Leased` to the `source`. + * @param number the number previously assigned(?) + * @return `true`, if the number is registered; `false`, otherwise + * @see `isRegistered(IdentifiableEntity)` + */ + def isRegistered(number : Int) : Boolean = { + source.Get(number) match { + case Some(monitor) => + monitor.Policy == AvailabilityPolicy.Leased + case None => + false + } + } +} diff --git a/common/src/main/scala/net/psforever/objects/guid/Task.scala b/common/src/main/scala/net/psforever/objects/guid/Task.scala new file mode 100644 index 000000000..f744c2af2 --- /dev/null +++ b/common/src/main/scala/net/psforever/objects/guid/Task.scala @@ -0,0 +1,23 @@ +// Copyright (c) 2017 PSForever +package net.psforever.objects.guid + +import akka.actor.ActorRef + +trait Task { + def Execute(resolver : ActorRef) : Unit + def isComplete : Task.Resolution.Value = Task.Resolution.Incomplete + def Timeout : Long = 200L //milliseconds + def onSuccess() : Unit = { } + def onFailure(ex : Throwable) : Unit = { } + def onTimeout(ex : Throwable) : Unit = onFailure(ex) + def onAbort(ex : Throwable) : Unit = { } + def Cleanup() : Unit = { } +} + +object Task { + def TimeNow : Long = { java.time.Instant.now().getEpochSecond } + + object Resolution extends Enumeration { + val Success, Incomplete, Failure = Value + } +} diff --git a/common/src/main/scala/net/psforever/objects/guid/TaskResolver.scala b/common/src/main/scala/net/psforever/objects/guid/TaskResolver.scala new file mode 100644 index 000000000..bea02e663 --- /dev/null +++ b/common/src/main/scala/net/psforever/objects/guid/TaskResolver.scala @@ -0,0 +1,393 @@ +// Copyright (c) 2017 PSForever +package net.psforever.objects.guid + +import java.util.concurrent.TimeoutException + +import akka.actor.{Actor, ActorRef, Cancellable} +import akka.routing.Broadcast + +import scala.annotation.tailrec +import scala.collection.mutable.ListBuffer +import scala.concurrent.duration._ +import scala.concurrent.ExecutionContext.Implicits.global +import scala.util.{Failure, Success} + +class TaskResolver() extends Actor { + /** list of all work currently managed by this TaskResolver */ + private val tasks : ListBuffer[TaskResolver.TaskEntry] = new ListBuffer[TaskResolver.TaskEntry] + /** scheduled examination of all managed work */ + private var timeoutCleanup : Cancellable = TaskResolver.DefaultCancellable + //private[this] val log = org.log4s.getLogger + + /** + * Deal with any tasks that are still enqueued with this expiring `TaskResolver`.
+ *
+ * First, eliminate all timed-out tasks. + * Secondly, deal with all tasks that have reported "success" but have not yet been handled. + * Finally, all other remaining tasks should be treated as if they had failed. + */ + override def aroundPostStop() = { + super.aroundPostStop() + + timeoutCleanup.cancel() + TimeoutCleanup() + OnSuccess() + val ex : Throwable = new Exception(s"a task is being stopped") + OnFailure(ex) + tasks.indices.foreach({index => + val entry = tasks(index) + PropagateAbort(index, ex) + if(entry.isASubtask) { + entry.supertaskRef ! Failure(ex) //alert our superior task's resolver we have completed + } + }) + } + + def receive : Receive = { + case TaskResolver.GiveTask(aTask, Nil) => + GiveTask(aTask) + + case TaskResolver.GiveTask(aTask, subtasks) => + QueueSubtasks(aTask, subtasks) + + case TaskResolver.GiveSubtask(aTask, subtasks, resolver) => + QueueSubtasks(aTask, subtasks, true, resolver) + + case TaskResolver.CompletedSubtask() => + ExecuteNewTasks() + + case Success(_) => //ignore the contents as unreliable + OnSuccess() + + case Failure(ex) => + OnFailure(ex) + + case TaskResolver.AbortTask(task, ex) => + OnAbort(task, ex) + + case TaskResolver.TimeoutCleanup() => + TimeoutCleanup() + + case _ => ; + } + + /** + * Accept simple work and perform it. + * @param aTask the work to be completed + */ + private def GiveTask(aTask : Task) : Unit = { + val entry : TaskResolver.TaskEntry = TaskResolver.TaskEntry(aTask) + tasks += entry + entry.Execute(self) //send this Actor; aesthetically pleasant expression + StartTimeoutCheck() + } + + /** + * Start the periodic checks for a task that has run for too long (timed-out), unless those checks are already running. + */ + private def StartTimeoutCheck() : Unit = { + if(timeoutCleanup.isCancelled) { + timeoutCleanup = context.system.scheduler.schedule(500 milliseconds, 500 milliseconds, self, TaskResolver.TimeoutCleanup()) + } + } + + /** + * Accept complicated work and divide it into a main task and tasks that must be handled before the main task. + * Do not start the main task until all of the aforementioned "sub-tasks" are completed.
+ *
+ * Sub-tasks can be nested many times. + * All immediate sub-tasks count as the primary sub-tasks for the current main task. + * Each pair of main task and sub-tasks, for every sub-task discovered, is passed on to another `TaskResolver` for completion. + * The parent of this `TaskResolver` is the router logic for all brethren `TaskResolver` `Actors`. + * @param task the work to be completed + * @param subtasks other work that needs to be completed first + * @param isSubTask `true`, if this task counts as internal or as a leaf in the chain of `Task` dependency; + * `false`, by default, if we are the top of the chain fo dependency + * @param resolver the `TaskResolver` that distributed this work, thus determining that this work is a sub-task; + * by default, no one, as the work is identified as a main task + */ + private def QueueSubtasks(task : Task, subtasks : List[TaskResolver.GiveTask], isSubTask : Boolean = false, resolver : ActorRef = Actor.noSender) : Unit = { + val sublist : List[Task] = subtasks.map(task => task.task) + val entry : TaskResolver.TaskEntry = TaskResolver.TaskEntry(task, sublist, isSubTask, resolver) + tasks += entry + if(sublist.isEmpty) { //a leaf in terms of task dependency; so, not dependent on any other work + entry.Execute(self) + } + else { + subtasks.foreach({subtask => + context.parent ! TaskResolver.GiveSubtask(subtask.task, subtask.subs, self) //route back to submit subtask to pool + }) + } + StartTimeoutCheck() + } + + /** + * Perform these checks when a task has reported successful completion to this TaskResolver. + * Since the `Success(_)` can not be associated with a specific task, every task and subtask will be checked. + */ + private def OnSuccess(): Unit = { + //by reversing the List, we can remove TaskEntries without disrupting the order + TaskResolver.filterCompletion(tasks.indices.reverseIterator, tasks.toList, Task.Resolution.Success).foreach({index => + val entry = tasks(index) + entry.task.onSuccess() + if(entry.isASubtask) { + entry.supertaskRef ! TaskResolver.CompletedSubtask() //alert our dependent task's resolver that we have completed + } + TaskCleanup(index) + }) + } + + /** + * Scan across a group of sub-tasks and determine if the associated main `Task` may execute. + * All of the sub-tasks must report a `Success` completion status before the main work can begin. + */ + private def ExecuteNewTasks() : Unit = { + tasks.filter({taskEntry => taskEntry.subtasks.nonEmpty}).foreach(entry => { + if(TaskResolver.filterCompletionMatch(entry.subtasks.iterator, Task.Resolution.Success)) { + entry.Execute(self) + StartTimeoutCheck() + } + }) + } + + /** + * Perform these checks when a task has reported failure to this TaskResolver. + * Since the `Failure(Throwable)` can not be associated with a specific task, every task and subtask will be checked. + * Consequently, the specific `Throwable` that contains the error message may have nothing to do with the failed task. + * @param ex a `Throwable` that reports what happened to the task + */ + private def OnFailure(ex : Throwable) : Unit = { + TaskResolver.filterCompletion(tasks.indices.reverseIterator, tasks.toList, Task.Resolution.Failure).foreach({index => + val entry = tasks(index) + PropagateAbort(index, ex) + entry.task.onFailure(ex) //TODO let the error be disjoint? + if(entry.isASubtask) { + entry.supertaskRef ! Failure(ex) //alert our superior task's resolver we have completed + } + }) + FaultSubtasks() + } + + /** + * Scan across a group of sub-tasks and, if any have reported `Failure`, report to the main `Task` that it should fail as well. + */ + private def FaultSubtasks() : Unit = { + tasks.indices.filter({index => tasks(index).subtasks.nonEmpty}).reverse.foreach(index => { + val entry = tasks(index) + if(TaskResolver.filterCompletionMatch(entry.subtasks.iterator, Task.Resolution.Failure)) { + val ex : Throwable = new Exception(s"a task ${entry.task} had a subtask that failed") + entry.task.onFailure(ex) + if(entry.isASubtask) { + entry.supertaskRef ! Failure(ex) //alert our superior task's resolver we have completed + } + TaskCleanup(index) + } + }) + } + + /** + * If a specific `Task` is governed by this `TaskResolver`, find its index and dispose of it and its known sub-tasks. + * @param task the work to be found + * @param ex a `Throwable` that reports what happened to the work + */ + private def OnAbort(task : Task, ex : Throwable) : Unit = { + TaskResolver.findTaskIndex(tasks.iterator, task) match { + case Some(index) => + PropagateAbort(index, ex) + case None => ; + } + } + + /** + * If a specific `Task` is governed by this `TaskResolver`, dispose of it and its known sub-tasks. + * @param index the index of the discovered work + * @param ex a `Throwable` that reports what happened to the work + */ + private def PropagateAbort(index : Int, ex : Throwable) : Unit = { + tasks(index).subtasks.foreach({subtask => + if(subtask.isComplete == Task.Resolution.Success) { + subtask.onAbort(ex) + } + context.parent ! Broadcast(TaskResolver.AbortTask(subtask, ex)) + }) + TaskCleanup(index) + } + + /** + * Find all tasks that have been running for too long and declare them as timed-out. + * Run periodically, as long as work is being performed. + */ + private def TimeoutCleanup() : Unit = { + TaskResolver.filterTimeout(tasks.indices.reverseIterator, tasks.toList, Task.TimeNow).foreach({index => + val ex : Throwable = new TimeoutException(s"a task ${tasks(index).task} has timed out") + tasks(index).task.onTimeout(ex) + PropagateAbort(index, ex) + }) + } + + /** + * Remove a `Task` that has reported completion. + * @param index an index of work in the `List` of `Task`s + */ + private def TaskCleanup(index : Int) : Unit = { + tasks(index).task.Cleanup() + tasks.remove(index) + if(tasks.isEmpty) { + timeoutCleanup.cancel() + } + } +} + +object TaskResolver { + /** + * Give this `TaskResolver` simple work to be performed. + * @param task the work to be completed + * @param subs other work that needs to be completed first + */ + final case class GiveTask(task : Task, subs : List[GiveTask] = Nil) + + /** + * Pass around complex work to be performed. + * @param task the work to be completed + * @param subs other work that needs to be completed first + * @param resolver the `TaskResolver` that will handle work that depends on the outcome of this work + */ + private final case class GiveSubtask(task : Task, subs : List[GiveTask], resolver : ActorRef) + + /** + * Run a scheduled timed-out `Task` check. + */ + private final case class TimeoutCleanup() + + /** + * + */ + private final case class CompletedSubtask() + + /** + * A `Broadcast` message designed to find and remove a particular task from this series of routed `Actors`. + * @param task the work to be removed + * @param ex an explanation why the work is being aborted + */ + private final case class AbortTask(task : Task, ex : Throwable) + + /** + * Storage unit for a specific unit of work, plus extra information. + * @param task the work to be completed + * @param subtasks other work that needs to be completed first + * @param isASubtask whether this work is intermediary or the last in a dependency chain + * @param supertaskRef the `TaskResolver` that will handle work that depends on the outcome of this work + */ + private final case class TaskEntry(task : Task, subtasks : List[Task] = Nil, isASubtask : Boolean = false, supertaskRef : ActorRef = Actor.noSender) { + private var start : Long = 0L + private var isExecuting : Boolean = false + + def Start : Long = start + + def Executing : Boolean = isExecuting + + def Execute(ref : ActorRef) : Unit = { + if(!isExecuting) { + start = Task.TimeNow + isExecuting = true + task.Execute(ref) + } + } + } + + /** + * A placeholder `Cancellable` object for the time-out checking functionality. + */ + private final val DefaultCancellable = new Cancellable() { + def cancel : Boolean = true + def isCancelled() : Boolean = true + } + + /** + * Find the index of the targeted `Task`, if it is enqueued here. + * @param iter an `Iterator` of + * @param task a target `Task` + * @param index the current index in the aforementioned `List`; + * defaults to 0 + * @return the index of the discovered task, or `None` + */ + @tailrec private def findTaskIndex(iter : Iterator[TaskResolver.TaskEntry], task : Task, index : Int = 0) : Option[Int] = { + if(!iter.hasNext) { + None + } + else { + if(iter.next.task == task) { + Some(index) + } + else { + findTaskIndex(iter, task, index + 1) + } + } + } + + /** + * Scan across a group of tasks to determine which ones match the target completion status. + * @param iter an `Iterator` of enqueued `TaskEntry` indices + * @param resolution the target completion status + * @param indexList a persistent `List` of indices + * @return the `List` of all valid `Task` indices + */ + @tailrec private def filterCompletion(iter : Iterator[Int], tasks : List[TaskEntry], resolution : Task.Resolution.Value, indexList : List[Int] = Nil) : List[Int] = { + if(!iter.hasNext) { + indexList + } + else { + val index : Int = iter.next + if(tasks(index).task.isComplete == resolution) { + filterCompletion(iter, tasks, resolution, indexList :+ index) + } + else { + filterCompletion(iter, tasks, resolution, indexList) + } + } + } + + /** + * Scan across a group of sub-tasks to determine if they all match the target completion status. + * @param iter an `Iterator` of enqueued sub-tasks + * @param resolution the target completion status + * @return `true`, if all tasks match the complete status; + * `false`, otherwise + */ + @tailrec private def filterCompletionMatch(iter : Iterator[Task], resolution : Task.Resolution.Value) : Boolean = { + if(!iter.hasNext) { + true + } + else { + if(iter.next.isComplete == resolution) { + filterCompletionMatch(iter, resolution) + } + else { + false + } + } + } + + /** + * Find the indices of all enqueued work that has timed-out. + * @param iter an `Iterator` of enqueued `TaskEntry` indices + * @param now the current time in milliseconds + * @param indexList a persistent `List` of indices + * @return the `List` of all valid `Task` indices + */ + @tailrec private def filterTimeout(iter : Iterator[Int], tasks : List[TaskEntry], now : Long, indexList : List[Int] = Nil) : List[Int] = { + if(!iter.hasNext) { + indexList + } + else { + val index : Int = iter.next + val taskEntry = tasks(index) + if(taskEntry.Executing && taskEntry.task.isComplete == Task.Resolution.Incomplete && now - taskEntry.Start > taskEntry.task.Timeout) { + filterTimeout(iter, tasks, now, indexList :+ index) + } + else { + filterTimeout(iter, tasks, now, indexList) + } + } + } +} diff --git a/common/src/main/scala/net/psforever/objects/guid/actor/IsRegistered.scala b/common/src/main/scala/net/psforever/objects/guid/actor/IsRegistered.scala new file mode 100644 index 000000000..b680c4698 --- /dev/null +++ b/common/src/main/scala/net/psforever/objects/guid/actor/IsRegistered.scala @@ -0,0 +1,31 @@ +// Copyright (c) 2017 PSForever +package net.psforever.objects.guid.actor + +import net.psforever.objects.entity.IdentifiableEntity + +/** + * A message for requesting information about the registration status of an object or a number. + * @param obj the optional object + * @param number the optional number + */ +final case class IsRegistered(obj : Option[IdentifiableEntity], number : Option[Int]) + +object IsRegistered { + /** + * Overloaded constructor for querying an object's status. + * @param obj the object + * @return an `IsRegistered` object + */ + def apply(obj : IdentifiableEntity) : IsRegistered = { + new IsRegistered(Some(obj), None) + } + + /** + * Overloaded constructor for querying a number's status. + * @param number the number + * @return an `IsRegistered` object + */ + def apply(number : Int) : IsRegistered = { + new IsRegistered(None, Some(number)) + } +} diff --git a/common/src/main/scala/net/psforever/objects/guid/actor/NumberPoolAccessorActor.scala b/common/src/main/scala/net/psforever/objects/guid/actor/NumberPoolAccessorActor.scala new file mode 100644 index 000000000..ec35632c2 --- /dev/null +++ b/common/src/main/scala/net/psforever/objects/guid/actor/NumberPoolAccessorActor.scala @@ -0,0 +1,216 @@ +// Copyright (c) 2017 PSForever +package net.psforever.objects.guid.actor + +import akka.actor.{Actor, ActorRef} +import akka.pattern.ask +import akka.util.Timeout +import net.psforever.objects.entity.IdentifiableEntity +import net.psforever.objects.guid.NumberPoolHub +import net.psforever.objects.guid.pool.NumberPool + +import scala.concurrent.duration._ +import scala.util.{Failure, Success} + +/** + * An `Actor` that wraps around the `Actor` for a `NumberPool` and automates a portion of the number registration process.
+ *
+ * The `NumberPoolActor` that is created is used as the synchronized "gate" through which the number selection process occurs. + * This `Actor` `ask`s the internal `Actor` and then waits on that `Future` to resolve. + * For the registration process, once it resolves, a number for the accompanying object has been chosen. + * The last part involves configuring the `NumberSource` of the hub so that it knows. + * For the process of revoking registration, the number from the object is returned to the pool. + * Like during the registration process, the `NumberSource` is then also updated.
+ *
+ * The object is always registered using the underlying governed `NumberPool`. + * The object will not unregister if the object or its number are not recognized as members previously registered to the `NumberPool`.
+ * Whether or not an object or a specific number has been registered is always possible. + * The scope encompasses the whole of the associated `NumberSource` as opposed to just this `NumberPool`. + * @param hub the `NumberPoolHub` this `Actor` manipulates + * @param pool the specific `NumberPool` this `Actor` maintains + * @param poolActor a shared `Actor` that governs this `NumberPool` + */ +class NumberPoolAccessorActor(private val hub : NumberPoolHub, private val pool : NumberPool, private val poolActor : ActorRef) extends Actor { + //the timeout is for when we ask the poolActor + private implicit val timeout = Timeout(50 milliseconds) + private[this] val log = org.log4s.getLogger + + private final case class GUIDRequest(obj : IdentifiableEntity, replyTo : ActorRef) + private val requestQueue : collection.mutable.LongMap[GUIDRequest] = new collection.mutable.LongMap() + private var index : Long = Long.MinValue + + def receive : Receive = { + //register + case Register(obj, _, None, call) => + try { + obj.GUID //stop if object has a GUID; sometimes this happens + log.warn(s"$obj already registered") + } + catch { + case _ : Exception => + val id : Long = index + index += 1 + requestQueue += id -> GUIDRequest(obj, call.getOrElse(sender())) + poolActor ! NumberPoolActor.GetAnyNumber(Some(id)) + } + + case Register(obj, _, Some(number), call) => + try { + obj.GUID //stop if object has a GUID; sometimes this happens + log.warn(s"$obj already registered") + } + catch { + case _ : Exception => + val id : Long = index + index += 1 + requestQueue += id -> GUIDRequest(obj, call.getOrElse(sender())) + poolActor ! NumberPoolActor.GetSpecificNumber(number, Some(id)) + } + + case NumberPoolActor.GiveNumber(number, id) => + id match { + case Some(nid : Long) => + Register(nid, requestQueue.remove(nid), number) + case _ => + pool.Return(number) //recovery? + log.warn(s"received a number but there is no request to process it; returning number to pool") + } + + case NumberPoolActor.NoNumber(ex, id) => + val req = id match { + case Some(nid : Long) => + val req = requestQueue.remove(nid) + if(req.isDefined) { s"$req" } else { s"a corresponding request $nid was not found;" } + case _ => + "generic request;" //should be unreachable + } + log.warn(s"a number was not drawn from the pool; $req $ex") + + //unregister + case Unregister(obj, call) => + val callback = call.getOrElse(sender()) + try { + val number = obj.GUID.guid + if(pool.Numbers.contains(number) && hub.WhichPool(obj).isDefined) { + val id : Long = index + index += 1 + requestQueue += id -> GUIDRequest(obj, callback) + poolActor ! NumberPoolActor.ReturnNumber(number, Some(id)) + } + else { + callback ! Failure(new Exception(s"the GUID of object $obj - $number - is not a part of this number pool")) + } + } + catch { + case msg : Exception => + callback ! Failure(msg) + } + + case NumberPoolActor.ReturnNumberResult(number, None, id) => + id match { + case Some(nid : Long) => + Unregister(nid, requestQueue.remove(nid), number) + case _ => + NumberPoolActor.GetSpecificNumber(pool, number) //recovery? + log.error(s"returned a number but there is no request to process it; recovering the number from pool") + } + + case NumberPoolActor.ReturnNumberResult(number, ex, id) => + val req = id match { + case Some(nid : Long) => + val req = requestQueue.remove(nid) + if(req.isDefined) { s"$req" } else { s"a corresponding request $nid was not found;" } + case _ => + "generic request;" //should be unreachable + } + log.warn(s"a number $number was not returned to the pool; $req $ex") + + //common + case IsRegistered(Some(obj), None) => + sender ! hub.isRegistered(obj) + + case IsRegistered(None, Some(number)) => + sender ! hub.isRegistered(number) + + case NumberPoolActor.ReturnNumber(number, _) => + sender ! (poolActor ? NumberPoolActor.ReturnNumber(number)) + + case msg => + log.warn(s"unexpected message received - $msg") + } + + /** + * A step of the object registration process. + * If there is a successful request object to be found, complete the registration request. + * @param id the identifier of this request + * @param request the request data + * @param number the number that was drawn from the `NumberPool` + */ + private def Register(id : Long, request : Option[GUIDRequest], number : Int) : Unit = { + request match { + case Some(GUIDRequest(obj, replyTo)) => + processRegisterResult(obj, number, replyTo) + case None => + pool.Return(number) //recovery? + log.warn(s"received a number but the request for it is missing; returning number to pool") + } + } + + /** + * A step of the object registration process. + * This step completes the registration by consulting the `NumberSource`. + * @param obj the object + * @param number the number to use + * @param callback an optional callback `ActorRef` + */ + private def processRegisterResult(obj : IdentifiableEntity, number : Int, callback : ActorRef) : Unit = { + try { + obj.GUID + pool.Return(number) //recovery? + callback ! Success(obj) + } + catch { + case _ : Exception => + hub.latterPartRegister(obj, number) match { + case Success(_) => + callback ! Success(obj) + case Failure(ex) => + pool.Return(number) //recovery? + callback ! Failure(ex) + } + } + } + + /** + * A step of the object un-registration process. + * If there is a successful request object to be found, complete the registration request. + * @param id the identifier of this request + * @param request the request data + * @param number the number that was drawn from the `NumberPool` + */ + private def Unregister(id : Long, request : Option[GUIDRequest], number : Int) : Unit = { + request match { + case Some(GUIDRequest(obj, replyTo)) => + processUnregisterResult(obj, obj.GUID.guid, replyTo) + case None => + NumberPoolActor.GetSpecificNumber(pool, number) //recovery? + log.error(s"returned a number but the rest of the request is missing; recovering the number from pool") + } + } + + /** + * A step of the object un-registration process. + * This step completes revoking the object's registration by consulting the `NumberSource`. + * @param obj the object + * @param callback an optional callback `ActorRef` + */ + private def processUnregisterResult(obj : IdentifiableEntity, number : Int, callback : ActorRef) : Unit = { + hub.latterPartUnregister(number) match { + case Some(_) => + obj.Invalidate() + callback ! Success(obj) + case None => + NumberPoolActor.GetSpecificNumber(pool, number) //recovery? + callback ! Failure(new Exception(s"failed to unregister a number; this may be a critical error")) + } + } +} diff --git a/common/src/main/scala/net/psforever/objects/guid/actor/NumberPoolActor.scala b/common/src/main/scala/net/psforever/objects/guid/actor/NumberPoolActor.scala new file mode 100644 index 000000000..961360470 --- /dev/null +++ b/common/src/main/scala/net/psforever/objects/guid/actor/NumberPoolActor.scala @@ -0,0 +1,96 @@ +// Copyright (c) 2017 PSForever +package net.psforever.objects.guid.actor + +import akka.actor.Actor +import net.psforever.objects.guid.pool.NumberPool +import net.psforever.objects.guid.selector.{NumberSelector, SpecificSelector} + +import scala.util.{Failure, Success, Try} + +/** + * An `Actor` that wraps around a `NumberPool` and regulates access to it.
+ *
+ * Wrapping around the pool like this forces a FIFO order to requests for numbers from the pool. + * This synchronization only lasts as long as this `Actor` is the only one for the given pool. + * In the distribution of globaly unique identifiers, this is extremely important. + * `NumberPool`s are used as the primary determination of whether a number is available at any given moment. + * The categorization of the pool is also important, though for a contextually-sensitive reason. + * @param pool the `NumberPool` being manipulated + */ +class NumberPoolActor(pool : NumberPool) extends Actor { + private[this] val log = org.log4s.getLogger + + def receive : Receive = { + case NumberPoolActor.GetAnyNumber(id) => + sender ! (pool.Get() match { + case Success(value) => + NumberPoolActor.GiveNumber(value, id) + case Failure(ex) => ; + NumberPoolActor.NoNumber(ex, id) + }) + + case NumberPoolActor.GetSpecificNumber(number, id) => + sender ! (NumberPoolActor.GetSpecificNumber(pool, number) match { + case Success(value) => + NumberPoolActor.GiveNumber(value, id) + case Failure(ex) => ; + NumberPoolActor.NoNumber(ex, id) + }) + + case NumberPoolActor.ReturnNumber(number, id) => + val result = pool.Return(number) + val ex : Option[Throwable] = if(!result) { Some(new Exception("number was not returned")) } else { None } + sender ! NumberPoolActor.ReturnNumberResult(number, ex, id) + + case msg => + log.info(s"received an unexpected message - ${msg.toString}") + } +} + +object NumberPoolActor { + /** + * A message to invoke the current `NumberSelector`'s functionality. + * @param id a potential identifier to associate this request + */ + final case class GetAnyNumber(id : Option[Any] = None) + + /** + * A message to invoke a `SpecificSelector` to acquire the specific `number`, if it is available in this pool. + * @param number the pre-selected number + * @param id a potential identifier to associate this request + */ + final case class GetSpecificNumber(number : Int, id : Option[Any] = None) + + /** + * A message to distribute the `number` that was drawn. + * @param number the pre-selected number + * @param id a potential identifier to associate this request + */ + final case class GiveNumber(number : Int, id : Option[Any] = None) + + final case class NoNumber(ex : Throwable, id : Option[Any] = None) + + /** + * A message to invoke the `Return` functionality of the current `NumberSelector`. + * @param number the number + */ + final case class ReturnNumber(number : Int, id : Option[Any] = None) + + final case class ReturnNumberResult(number : Int, ex : Option[Throwable], id : Option[Any] = None) + + /** + * Use the `SpecificSelector` on this pool to extract a specific object from the pool, if it is included and available. + * @param pool the `NumberPool` to draw from + * @param number the number requested + * @return the number requested, or an error + */ + def GetSpecificNumber(pool : NumberPool, number : Int) : Try[Int] = { + val original : NumberSelector = pool.Selector + val specific : SpecificSelector = new SpecificSelector + specific.SelectionIndex = pool.Numbers.indexOf(number) + pool.Selector = specific + val out : Try[Int] = pool.Get() + pool.Selector = original + out + } +} diff --git a/common/src/main/scala/net/psforever/objects/guid/actor/NumberPoolHubActor.scala b/common/src/main/scala/net/psforever/objects/guid/actor/NumberPoolHubActor.scala new file mode 100644 index 000000000..d1a9af128 --- /dev/null +++ b/common/src/main/scala/net/psforever/objects/guid/actor/NumberPoolHubActor.scala @@ -0,0 +1,212 @@ +// Copyright (c) 2017 PSForever +package net.psforever.objects.guid.actor + +import akka.pattern.ask +import akka.util.Timeout +import akka.actor.{Actor, ActorRef, Props} + +import net.psforever.objects.entity.IdentifiableEntity +import net.psforever.objects.guid.NumberPoolHub +import net.psforever.objects.guid.pool.NumberPool + +import scala.collection.mutable +import scala.concurrent.Future +import scala.concurrent.duration._ +import scala.util.{Failure, Success, Try} + +/** + * An incoming message for retrieving a specific `NumberPoolAccessorActor`. + * @param name the name of the accessor's `NumberPool` + */ +final case class RequestPoolActor(name : String) + +/** + * An outgoing message for giving a specific `NumberPoolAccessorActor`. + * @param name the name of the accessor's `NumberPool`, for reference + * @param actor the accessor + */ +final case class DeliverPoolActor(name : String, actor : ActorRef) + +/** + * An `Actor` that wraps around the management system for `NumberPools`.
+ *
+ * By just instantiating, this object builds and stores a `NumberPoolAccessorActor` for each `NumberPool` known to the `hub`. + * Additional `NumberPool`s created by the `hub` need to be paired with a created accessor manually. + * Each accessor is the primary entry point to a registration process for the specific `NumberPool` it represents. + * The `hub` `Actor` itself distribute any registration task it receives out to an applicable accessor of which it is aware. + * It will attempt to revoke registration on its own, without relying on the functionality from any accessor.
+ *
+ * In the same way that `NumberPoolHub` is a tool for keeping track of `NumberPool` objects, + * its `Actor` is a tool for keeping track of accessors created from `NumberPool` objects. + * It is very, however, for handling unspecific revoke tasks. + * @param hub the central `NumberPool` management object for an embedded `NumberSource` object + */ +class NumberPoolHubActor(private val hub : NumberPoolHub) extends Actor { + private val actorHash : mutable.HashMap[String, ActorRef] = mutable.HashMap[String, ActorRef]() + hub.Pools.foreach({ case(name, pool) => CreatePoolActor(name, pool) }) + implicit val timeout = Timeout(50 milliseconds) + private[this] val log = org.log4s.getLogger + + def receive : Receive = { + case RequestPoolActor(name) => + sender ! (GetPoolActor(name) match { + case Success(poolActor) => + DeliverPoolActor(name, poolActor) + case Failure(ex) => + Failure(ex) + }) + + case Register(obj, name, None, callback) => + HubRegister(obj, name, callback) + + case Register(obj, name, Some(number), callback) => + HubRegister(obj, name, number, callback) + + //common + case IsRegistered(Some(obj), None) => + sender ! hub.isRegistered(obj) + + case IsRegistered(None, Some(number)) => + sender ! hub.isRegistered(number) + + case Unregister(obj, callback) => + Unregister(obj, if(callback.isEmpty) { sender } else { callback.get }) + + case msg => + log.warn(s"unexpected message received - ${msg.toString}") + } + + /** + * From a name, find an existing `NumberPoolAccessorActor`. + * @param name the accessor's name + * @return the accessor that was requested + */ + private def GetPoolActor(name : String) : Try[ActorRef] = { + actorHash.get(name) match { + case Some(actor) => + Success(actor) + case _ => + Failure(new Exception(s"number pool $name not defined")) + } + } + + /** + * Create a new `NumberPoolAccessorActor` and add it to the local collection of accessors. + * @param name the accessor's name + * @param pool the underlying `NumberPool` + */ + private def CreatePoolActor(name : String, pool : NumberPool) : Unit = { + actorHash.get(name) match { + case None => + actorHash += name -> context.actorOf(Props(classOf[NumberPoolAccessorActor], hub, pool), s"${name}Actor") + case Some(_) => + //TODO complain? + } + } + + /** + * A step of the object registration process. + * Select a valid `NumberPoolAccessorActor` and pass a task onto it. + * @param obj an object + * @param name a potential accessor pool + * @param callback an optional callback `ActorRef` + */ + private def HubRegister(obj : IdentifiableEntity, name : Option[String], callback : Option[ActorRef]) : Unit = { + val genericPool = actorHash("generic") + val pool = if(name.isDefined) { actorHash.get(name.get).orElse(Some(genericPool)).get } else { genericPool } + pool ! Register(obj, None, None, callback) + } + + /** + * A step of the object registration process. + * Determine to which `NumberPool` the `number` belongs. + * @param obj an object + * @param name a potential accessor pool + * @param number a potential number + * @param callback an optional callback `ActorRef` + */ + private def HubRegister(obj : IdentifiableEntity, name : Option[String], number : Int, callback : Option[ActorRef]) : Unit = { + hub.WhichPool(number) match { + case Some(poolname) => + HubRegister_GetActor(obj, name, poolname, number, callback) + case None => + self ! Register(obj, name, None, callback) + } + } + + /** + * A step of the object registration process. + * Pass a task onto an accessor or, if the accessor can not be found, attempt to recover. + * @param obj an object + * @param name a potential accessor pool + * @param poolname the suggested accessor pool + * @param number a potential number + * @param callback an optional callback `ActorRef` + */ + private def HubRegister_GetActor(obj : IdentifiableEntity, name : Option[String], poolname : String, number : Int, callback : Option[ActorRef]) : Unit = { + actorHash.get(poolname) match { + case Some(pool) => + pool ! Register(obj, None, Some(number), callback) + case None => + HubRegister_MissingActor(obj, name, poolname, number, callback) + } + } + + /** + * A step of the object registration process. + * If an accessor could not be found in the last step, attempt to create the accessor. + * If the accessor can not be created, the `number` can not be used; + * fall back on the original pool (`name`). + * @param obj an object + * @param name a potential accessor pool + * @param poolname the suggested accessor pool + * @param number a potential number + * @param callback an optional callback `ActorRef` + */ + private def HubRegister_MissingActor(obj : IdentifiableEntity, name : Option[String], poolname : String, number : Int, callback : Option[ActorRef]) : Unit = { + hub.GetPool(poolname) match { + case Some(pool) => + CreatePoolActor(poolname, pool) + actorHash(poolname) ! Register(obj, None, Some(number), callback) + case None => + log.error(s"matched number $number to pool $poolname, but could not find $poolname when asked") + self ! Register(obj, name, None, callback) + } + } + + /** + * A step of the object un-registration process. + * This step locates the `NumberPool` to which this object is a member. + * If found, it prepares a `Future` to resolve later regarding whether the `NumberPool` accepted the number. + * @param obj the object + * @param callback a callback `ActorRef` + */ + private def Unregister(obj : IdentifiableEntity, callback : ActorRef) : Unit = { + hub.WhichPool(obj) match { + case Some(name) => + val objToUnregister = obj + val poolName = name + processUnregisterResult(objToUnregister, (actorHash(poolName) ? NumberPoolActor.ReturnNumber(objToUnregister.GUID.guid)).mapTo[Boolean], callback) + case None => + callback ! UnregisterFailure(obj, new Exception("could not find pool object is member of")) + } + } + + /** + * A step of the object un-registration process. + * This step completes revoking the object's registration by consulting the `NumberSource`. + * @param obj the object + * @param result whether the number was returned in the last step + * @param callback a callback `ActorRef` + */ + private def processUnregisterResult(obj : IdentifiableEntity, result : Future[Boolean], callback : ActorRef) : Unit = { + import scala.concurrent.ExecutionContext.Implicits.global + result.foreach { + case true => + hub.latterPartUnregister(obj.GUID.guid) + callback ! UnregisterSuccess(obj) + case false => + callback ! UnregisterFailure(obj, new Exception("could not find object to remove")) + } + } +} diff --git a/common/src/main/scala/net/psforever/objects/guid/actor/Register.scala b/common/src/main/scala/net/psforever/objects/guid/actor/Register.scala new file mode 100644 index 000000000..3ba4807b9 --- /dev/null +++ b/common/src/main/scala/net/psforever/objects/guid/actor/Register.scala @@ -0,0 +1,80 @@ +// Copyright (c) 2017 PSForever +package net.psforever.objects.guid.actor + +import akka.actor.ActorRef +import net.psforever.objects.entity.IdentifiableEntity + +/** + * A message for accepting object-number registration requests.
+ *
+ * The callback is actually an `ActorRef` to which a `RegisterSuccess` message or a `RegisterFailure` message is sent. + * This is as opposed to what a "callback" is normally - a function. + * @param obj the mandatory object + * @param name the optional name of the number pool to which this object is registered + * @param number the optional number pre-selected for registering this object + * @param callback the optional custom callback for the messages from the success or failure conditions + */ +final case class Register(obj : IdentifiableEntity, name : Option[String], number : Option[Int], callback : Option[ActorRef]) + +object Register { + /** + * Overloaded constructor, accepting just the object. + * @param obj the object to be registered + * @return a `Register` object + */ + def apply(obj : IdentifiableEntity) : Register = { + new Register(obj, None, None, None) + } + + /** + * Overloaded constructor, accepting the object and a callback. + * @param obj the object to be registered + * @param callback the custom callback for the messages from the success or failure conditions + * @return a `Register` object + */ + def apply(obj : IdentifiableEntity, callback : ActorRef) : Register = { + new Register(obj, None, None, Some(callback)) + } + + /** + * Overloaded constructor, accepting an object and a pre-selected number. + * @param obj the object to be registered + * @param number the pre-selected number + * @return a `Register` object + */ + def apply(obj : IdentifiableEntity, number : Int) : Register = { + new Register(obj, None, Some(number), None) + } + + /** + * Overloaded constructor, accepting an object, a pre-selected number, and a callback. + * @param obj the object to be registered + * @param number the pre-selected number + * @param callback the custom callback for the messages from the success or failure conditions + * @return a `Register` object + */ + def apply(obj : IdentifiableEntity, number : Int, callback : ActorRef) : Register = { + new Register(obj, None, Some(number), Some(callback)) + } + + /** + * Overloaded constructor, accepting an object and a number pool. + * @param obj the object to be registered + * @param name the number pool name + * @return a `Register` object + */ + def apply(obj : IdentifiableEntity, name : String) : Register = { + new Register(obj, Some(name), None, None) + } + + /** + * Overloaded constructor, accepting an object, a number pool, and a callback. + * @param obj the object to be registered + * @param name the number pool name + * @param callback the custom callback for the messages from the success or failure conditions + * @return a `Register` object + */ + def apply(obj : IdentifiableEntity, name : String, callback : ActorRef) : Register = { + new Register(obj, Some(name), None, Some(callback)) + } +} diff --git a/common/src/main/scala/net/psforever/objects/guid/actor/Unregister.scala b/common/src/main/scala/net/psforever/objects/guid/actor/Unregister.scala new file mode 100644 index 000000000..1258d998a --- /dev/null +++ b/common/src/main/scala/net/psforever/objects/guid/actor/Unregister.scala @@ -0,0 +1,23 @@ +// Copyright (c) 2017 PSForever +package net.psforever.objects.guid.actor + +import akka.actor.ActorRef +import net.psforever.objects.entity.IdentifiableEntity + +/** + * A message for accepting object-number unregistration requests. + * When given to a number pool (`NumberPoolAccessorActor`), that `Actor` assumes itself to have the object. + * When given to a hub object (`NumberPoolHubActor`), it will attempt to determine which pool currently has the object.
+ *
+ * The callback is actually an `ActorRef` to which a `RegisterSuccess` message or a `RegisterFailure` message is sent. + * This is as opposed to what a "callback" is normally - a function. + * @param obj the mandatory object + * @param callback the optional custom callback for the messages from the success or failure conditions + */ +final case class Unregister(obj : IdentifiableEntity, callback : Option[ActorRef] = None) + +object Unregister { + def apply(obj : IdentifiableEntity, callback : ActorRef) : Unregister = { + Unregister(obj, Some(callback)) + } +} diff --git a/common/src/main/scala/net/psforever/objects/guid/actor/UnregisterFailure.scala b/common/src/main/scala/net/psforever/objects/guid/actor/UnregisterFailure.scala new file mode 100644 index 000000000..60eb0ce3c --- /dev/null +++ b/common/src/main/scala/net/psforever/objects/guid/actor/UnregisterFailure.scala @@ -0,0 +1,11 @@ +// Copyright (c) 2017 PSForever +package net.psforever.objects.guid.actor + +import net.psforever.objects.entity.IdentifiableEntity + +/** + * A message for when an object has failed to be unregistered for some reason. + * @param obj the object + * @param ex the reason that the registration process failed + */ +final case class UnregisterFailure(obj : IdentifiableEntity, ex : Throwable) diff --git a/common/src/main/scala/net/psforever/objects/guid/actor/UnregisterSuccess.scala b/common/src/main/scala/net/psforever/objects/guid/actor/UnregisterSuccess.scala new file mode 100644 index 000000000..603de46ad --- /dev/null +++ b/common/src/main/scala/net/psforever/objects/guid/actor/UnregisterSuccess.scala @@ -0,0 +1,11 @@ +// Copyright (c) 2017 PSForever +package net.psforever.objects.guid.actor + +import net.psforever.objects.entity.IdentifiableEntity + +/** + * A message for when an object has been unregistered. + * @param obj the object + */ +final case class UnregisterSuccess(obj : IdentifiableEntity) + diff --git a/common/src/main/scala/net/psforever/objects/guid/key/LoanedKey.scala b/common/src/main/scala/net/psforever/objects/guid/key/LoanedKey.scala new file mode 100644 index 000000000..28901abe7 --- /dev/null +++ b/common/src/main/scala/net/psforever/objects/guid/key/LoanedKey.scala @@ -0,0 +1,46 @@ +// Copyright (c) 2017 PSForever +package net.psforever.objects.guid.key + +import net.psforever.objects.entity.IdentifiableEntity +import net.psforever.objects.guid.AvailabilityPolicy + +/** + * The only indirect public access a queued number monitor object (`Key`) is allowed. + * @param guid the GUID represented by this indirect key + * @param key a private reference to the original key + */ +class LoanedKey(private val guid : Int, private val key : Monitor) { + def GUID : Int = guid + + def Policy : AvailabilityPolicy.Value = key.Policy + + def Object : Option[IdentifiableEntity] = key.Object + + /** + * na + * @param obj the object that should hold this GUID + * @return `true`, if the assignment worked; `false`, otherwise + */ + def Object_=(obj : IdentifiableEntity) : Option[IdentifiableEntity] = Object_=(Some(obj)) + + /** + * na + * @param obj the object that should hold this GUID + * @return `true`, if the assignment worked; `false`, otherwise + */ + def Object_=(obj : Option[IdentifiableEntity]) : Option[IdentifiableEntity] = { + if(key.Policy == AvailabilityPolicy.Leased || (key.Policy == AvailabilityPolicy.Restricted && key.Object.isEmpty)) { + if(key.Object.isDefined) { + key.Object.get.Invalidate() + key.Object = None + } + key.Object = obj + if(obj.isDefined) { + import net.psforever.packet.game.PlanetSideGUID + obj.get.GUID = PlanetSideGUID(guid) + } + } + key.Object + } +} + diff --git a/common/src/main/scala/net/psforever/objects/guid/key/Monitor.scala b/common/src/main/scala/net/psforever/objects/guid/key/Monitor.scala new file mode 100644 index 000000000..61d4cda17 --- /dev/null +++ b/common/src/main/scala/net/psforever/objects/guid/key/Monitor.scala @@ -0,0 +1,13 @@ +// Copyright (c) 2017 PSForever +package net.psforever.objects.guid.key + +import net.psforever.objects.entity.IdentifiableEntity +import net.psforever.objects.guid.AvailabilityPolicy + +trait Monitor { + def Policy : AvailabilityPolicy.Value + + def Object : Option[IdentifiableEntity] + + def Object_=(objct : Option[IdentifiableEntity]) : Option[IdentifiableEntity] +} diff --git a/common/src/main/scala/net/psforever/objects/guid/key/SecureKey.scala b/common/src/main/scala/net/psforever/objects/guid/key/SecureKey.scala new file mode 100644 index 000000000..694df2575 --- /dev/null +++ b/common/src/main/scala/net/psforever/objects/guid/key/SecureKey.scala @@ -0,0 +1,18 @@ +// Copyright (c) 2017 PSForever +package net.psforever.objects.guid.key + +import net.psforever.objects.guid.AvailabilityPolicy + +/** + * An unmodifiable reference to an active number monitor object (`Key`). + * @param guid the number (globally unique identifier) + * @param key a reference to the monitor + */ +final class SecureKey(private val guid : Int, private val key : Monitor) { + def GUID : Int = guid + + def Policy : AvailabilityPolicy.Value = key.Policy + + import net.psforever.objects.entity.IdentifiableEntity + def Object : Option[IdentifiableEntity] = key.Object +} \ No newline at end of file diff --git a/common/src/main/scala/net/psforever/objects/guid/misc/AscendingNumberSource.scala b/common/src/main/scala/net/psforever/objects/guid/misc/AscendingNumberSource.scala new file mode 100644 index 000000000..c4534faba --- /dev/null +++ b/common/src/main/scala/net/psforever/objects/guid/misc/AscendingNumberSource.scala @@ -0,0 +1,32 @@ +// Copyright (c) 2017 PSForever +package net.psforever.objects.guid.misc + +/** + * This class is just a proof of concept model of a self-contained system. + */ +class AscendingNumberSource { + val pool : Array[Int] = Array.ofDim[Int](65536) + (0 to 65535).foreach(x => { pool(x) = x }) + var head : Int = 0 + + def Get() : Int = { + val start : Int = head + if(pool(head) == -1) { + do { + head = (head + 1) % pool.length + } + while(pool(head) == -1 && head != start) + } + if(head == start) { + import net.psforever.objects.entity.NoGUIDException + throw NoGUIDException("no unused numbers available") + } + val outNumber : Int = head + pool(head) = -1 + outNumber + } + + def Return(number : Int) : Unit = { + pool(number) = number + } +} diff --git a/common/src/main/scala/net/psforever/objects/guid/misc/RegistrationTaskResolver.scala b/common/src/main/scala/net/psforever/objects/guid/misc/RegistrationTaskResolver.scala new file mode 100644 index 000000000..f45d7e942 --- /dev/null +++ b/common/src/main/scala/net/psforever/objects/guid/misc/RegistrationTaskResolver.scala @@ -0,0 +1,137 @@ +// Copyright (c) 2017 PSForever +package net.psforever.objects.guid.misc + +import java.util.concurrent.TimeoutException + +import akka.actor.{Actor, ActorRef, Cancellable} +import net.psforever.objects.entity.IdentifiableEntity + +import scala.annotation.tailrec +import scala.concurrent.ExecutionContext.Implicits.global +import scala.concurrent.duration._ +import scala.util.{Failure, Success, Try} + +/** + * Accept a task in waiting and series of lesser tasks that complete the provided primary task. + * Receive periodic updates on the states of the lesser tasks and, when these sub-tasks have been accomplished, + * declare the primary task accomplished as well.
+ *
+ * This ia admittedly a simplistic model of task resolution, currently, and is rather specific and limited. + * Generalizing and expanding on this class in the future might be beneficial. + * @param obj the primary task + * @param list a series of sub-tasks that need to be completed before the pimrary task can be completed + * @param callback where to report about the pirmary task having succeeded or failed + * @param timeoutDuration a delay during which sub-tasks are permitted to be accomplished; + * after this grave period is over, the task has failed + */ +class RegistrationTaskResolver[T <: IdentifiableEntity](private val obj : T, private val list : List[T], callback : ActorRef, timeoutDuration : FiniteDuration) extends Actor { + /** sub-tasks that contribute to completion of the task */ + private val checklist : Array[Boolean] = Array.fill[Boolean](list.length)(false) + /** whether or not it matters that sub-tasks are coming in */ + private var valid : Boolean = true + /** declares when the task has taken too long to complete */ + private val taskTimeout : Cancellable = context.system.scheduler.scheduleOnce(timeoutDuration, self, Failure(new TimeoutException(s"a task for $obj has timed out"))) + private[this] val log = org.log4s.getLogger + ConfirmTask(Success(true)) //check for auto-completion + + def receive : Receive = { + case Success(objn)=> + ConfirmTask(ConfirmSubtask(objn.asInstanceOf[T])) + + case Failure(ex)=> + FailedTask(ex) + + case msg => + log.warn(s"unexpected message received - ${msg.toString}") + } + + /** + * If this object is still accepting task resolutions, determine if that sub-task can be checked off. + * @param objn the sub-task entry + * @return a successful pass or a failure if the task can't be found; + * a "successful failure" if task resolutions are no longer accepted + */ + private def ConfirmSubtask(objn : T) : Try[Boolean] = { + if(valid) { + if(MatchSubtask(objn, list.iterator)) { + Success(true) + } + else { + Failure(new Exception(s"can not find a subtask to check off - ${objn.toString}")) + } + } + else { + Success(false) + } + } + + /** + * Find a sub-task from a `List` of sub-tasks and mark it as completed, if found. + * @param objn the sub-task entry + * @param iter_list an `Iterator` to the list of sub-tasks + * @param index the index of this entry; + * defaults to zero + * @return whether or not the subtask has been marked as completed + */ + @tailrec private def MatchSubtask(objn : T, iter_list : Iterator[T], index : Int = 0) : Boolean = { + if(!iter_list.hasNext) { + false + } + else { + val subtask = iter_list.next + if(subtask.equals(objn)) { + checklist(index) = true + true + } + else { + MatchSubtask(objn, iter_list, index + 1) + } + } + } + + /** + * Determine whether all sub-tasks have been completed successfully. + * If so, complete the primary task. + * @param subtaskComplete the status of the recent sub-task confirmation that triggered this confirmation request + */ + private def ConfirmTask(subtaskComplete : Try[Boolean]) : Unit = { + if(valid) { + subtaskComplete match { + case Success(true) => + if(!checklist.contains(false)) { + FulfillTask() + } + case Success(false) => + log.warn(s"when checking a task for ${obj.toString}, arrived at a state where we previously failed a subtask but main task still valid") + case Failure(ex) => + FailedTask(ex) + } + } + } + + /** + * All sub-tasks have been completed; the main task can also be completed. + * Alert interested parties that the task is performed successfully. + * Stop as soon as possible. + */ + private def FulfillTask() : Unit = { + valid = false + callback ! Success(obj) + taskTimeout.cancel() + context.stop(self) + } + + /** + * The main task can not be completed. + * Clean up as much as possible and alert interested parties that the task has been dropped. + * Let this `Actor` stop gracefully. + * @param ex why the main task can not be completed + */ + private def FailedTask(ex : Throwable) : Unit = { + valid = false + callback ! Failure(ex) + taskTimeout.cancel() + import akka.pattern.gracefulStop + gracefulStop(self, 2 seconds) //give time for any other messages; avoid dead letters + } +} diff --git a/common/src/main/scala/net/psforever/objects/guid/pool/ExclusivePool.scala b/common/src/main/scala/net/psforever/objects/guid/pool/ExclusivePool.scala new file mode 100644 index 000000000..93081c545 --- /dev/null +++ b/common/src/main/scala/net/psforever/objects/guid/pool/ExclusivePool.scala @@ -0,0 +1,33 @@ +// Copyright (c) 2017 PSForever +package net.psforever.objects.guid.pool + +import net.psforever.objects.guid.selector.NumberSelector + +import scala.util.{Failure, Success, Try} + +class ExclusivePool(numbers : List[Int]) extends SimplePool(numbers) { + private val pool : Array[Int] = Array.ofDim[Int](numbers.length) + numbers.indices.foreach(i => { pool(i) = i }) + + override def Count : Int = pool.count(value => value == -1) + + override def Selector_=(slctr : NumberSelector) : Unit = { + super.Selector_=(slctr) + slctr.Format(pool) + } + + override def Get() : Try[Int] = { + val index : Int = Selector.Get(pool) + if(index == -1) { + Failure(new Exception("there are no numbers available in the pool")) + } + else { + Success(numbers(index)) + } + } + + override def Return(number : Int) : Boolean = { + val index = Numbers.indexOf(number) + index != -1 && Selector.Return(index, pool) + } +} diff --git a/common/src/main/scala/net/psforever/objects/guid/pool/GenericPool.scala b/common/src/main/scala/net/psforever/objects/guid/pool/GenericPool.scala new file mode 100644 index 000000000..9a4b1aa24 --- /dev/null +++ b/common/src/main/scala/net/psforever/objects/guid/pool/GenericPool.scala @@ -0,0 +1,89 @@ +// Copyright (c) 2017 PSForever +package net.psforever.objects.guid.pool + +import net.psforever.objects.guid.selector.{NumberSelector, SpecificSelector} + +import scala.collection.mutable +import scala.util.{Failure, Success, Try} + +class GenericPool(private val hub : mutable.LongMap[String], private val max : Int) extends NumberPool { + val numbers : mutable.ListBuffer[Int] = mutable.ListBuffer[Int]() + private val selector : SpecificSelector = new SpecificSelector + selector.SelectionIndex = -1 + + def Numbers : List[Int] = numbers.toList + + def Count : Int = numbers.length + + def Selector : NumberSelector = selector + + def Selector_=(slctr : NumberSelector) : Unit = { } //intentionally blank + + def Get() : Try[Int] = { + val specific = selector.SelectionIndex + selector.SelectionIndex = -1 //clear + if(specific == -1) { + val number = GenericPool.rand(hub.keys.toList, max) + hub += number.toLong -> "generic" + numbers += number + Success(number) + } + else if(hub.get(specific).isEmpty) { + hub += specific.toLong -> "generic" + numbers += specific + Success(specific) + } + else { + Failure(new Exception("selector was not initialized properly, or no numbers available in the pool")) + } + } + + def Return(number : Int) : Boolean = { + val index : Int = numbers.indexOf(number) + if(index > -1) { + numbers.remove(index) + hub -= number + true + } + else { + false + } + } +} + +object GenericPool { + /** + * Get some number that is not accounted for in any other fixed pool, making it available in this generic one.
+ *
+ * Although called "`rand`," this algorithm is not actually random. + * From a sorted list of numbers, with a minimum and a maximum value appended, + * it finds the two adjacent numbers that are the most distant. + * It finds an average whole integer number between the two.
+ *
+ * This solution gets expensive as the count of numbers in `list` increases. + * @param list all of the non-repeating numbers to be compared + * @param domainSize how many numbers can be supported + * @return midpoint of the largest distance between any two of the existing numbers, or -1 + */ + private def rand(list : List[Long], domainSize : Int) : Int = { + if(list.size < domainSize) { + //get a list of all assigned numbers with an appended min and max + val sortedList : List[Long] = -1L +: list.sorted :+ domainSize.toLong + //compare the delta between every two entries and find the start of that greatest delta comparison + var maxDelta : Long = -1 + var maxDeltaIndex = -1 + for(index <- 0 until (sortedList.length - 1)) { + val curr = sortedList(index + 1) - sortedList(index) + if(curr > maxDelta) { + maxDelta = curr + maxDeltaIndex = index + } + } + //find half of the distance between the two numbers with the greatest delta value + if(maxDelta > 1) { ((sortedList(maxDeltaIndex + 1) + sortedList(maxDeltaIndex)) / 2f).toInt } else { -1 } + } + else { + -1 + } + } +} diff --git a/common/src/main/scala/net/psforever/objects/guid/pool/NumberPool.scala b/common/src/main/scala/net/psforever/objects/guid/pool/NumberPool.scala new file mode 100644 index 000000000..c054b3b72 --- /dev/null +++ b/common/src/main/scala/net/psforever/objects/guid/pool/NumberPool.scala @@ -0,0 +1,20 @@ +// Copyright (c) 2017 PSForever +package net.psforever.objects.guid.pool + +import net.psforever.objects.guid.selector.NumberSelector + +import scala.util.Try + +trait NumberPool { + def Numbers : List[Int] + + def Count : Int + + def Selector : NumberSelector + + def Selector_=(slctr : NumberSelector) : Unit + + def Get() : Try[Int] + + def Return(number : Int) : Boolean +} diff --git a/common/src/main/scala/net/psforever/objects/guid/pool/SimplePool.scala b/common/src/main/scala/net/psforever/objects/guid/pool/SimplePool.scala new file mode 100644 index 000000000..1b30ed4aa --- /dev/null +++ b/common/src/main/scala/net/psforever/objects/guid/pool/SimplePool.scala @@ -0,0 +1,35 @@ +// Copyright (c) 2017 PSForever +package net.psforever.objects.guid.pool + +import net.psforever.objects.guid.selector.{NumberSelector, StrictInOrderSelector} + +import scala.util.{Success, Try} + +class SimplePool(private val numbers : List[Int]) extends NumberPool { + if(numbers.count(_ < 0) > 0) { + throw new IllegalArgumentException("negative numbers not allowed in number pool") + } + else if (numbers.length != numbers.toSet.size) { + throw new IllegalArgumentException("duplicate numbers not allowed in number pool") + } + private var selector : NumberSelector = new StrictInOrderSelector + + def Numbers : List[Int] = numbers + + def Count : Int = 0 + + def Selector : NumberSelector = selector + + def Selector_=(slctr : NumberSelector) : Unit = { + selector = slctr + } + + def Get() : Try[Int] = { + val ary = numbers.indices.toArray + val index = selector.Get(ary) + selector.Return(index, ary) //reset, for the benefit of the selector + Success(numbers(index)) + } + + def Return(number : Int) : Boolean = numbers.indexOf(number) > -1 +} diff --git a/common/src/main/scala/net/psforever/objects/guid/selector/NumberSelector.scala b/common/src/main/scala/net/psforever/objects/guid/selector/NumberSelector.scala new file mode 100644 index 000000000..d414561d2 --- /dev/null +++ b/common/src/main/scala/net/psforever/objects/guid/selector/NumberSelector.scala @@ -0,0 +1,80 @@ +// Copyright (c) 2017 PSForever +package net.psforever.objects.guid.selector + +/** + * The base class for all different sorts of number selection policies.
+ *
+ * The `Array`s called out as method parameters is always an `Array` of indexes for some other list. + * The indices in the `Array` are always the complete range of 0 to `n` numbers. + * It is recommended to initialize the `Array` with the rule `array(number) = number`. + * When they need to be flagged as "invalid" in some way, use some consistent system of negative numbers. + * (Recommendation: unless doing something fancy, just use -1.) + */ +abstract class NumberSelector { + /** The index for the selector when performing a number selection action, then modified to the "next" index. */ + protected var selectionIndex : Int = 0 + /** The index for the selector when performing a number return action, then modified for the "next" index. */ + protected var ret : Int = 0 + + def SelectionIndex : Int = selectionIndex + + def ReturnIndex : Int = ret + + /** + * Accept a provided `pool` and select the next number.
+ *
+ * The main requirement for valid implementation of a `Get` selector is atomicity. + * While `Get` could be written to run again for every failure, this should not be anticipated. + * A success means a "success." + * A failure means that no "success" would be possible no matter how many times it might be run under the current conditions. + * The aforementioned conditions may change depending on the nature of the specific selector; + * but, the previous requirement should not be violated.
+ *
+ * `Get` is under no obligation to not modify its parameter `Array`. + * In fact, it should do this by default to provide additional feedback of its process. + * Pass a copy if data mutation is a concern. + * @param ary the `Array` of `Int` numbers from which to draw a new number + * @return an `Int` number + */ + def Get(ary : Array[Int]) : Int + + /** + * Give a number back to a specific collection following the principles of this selector.
+ *
+ * By default, a simple policy for returning numbers has been provided. + * This will not be sufficient for all selection actions that can be implemented so `override` where necessary. + *
+ * `Return` is under no obligation to leave its parameter `Array` unmodified. + * In fact, it should modify it by default to provide additional feedback of its process. + * Pass a copy if data mutation is a concern. + * @param number the number to be returned + * @param ary the `Array` of `Int` numbers to which the number is to be returned + * @return `true`, if this return was successful; `false`, otherwise + */ + def Return(number : Int, ary : Array[Int]) : Boolean = { + if(ary(number) == -1) { + ary(number) = number + ret = number + true + } + else { + false + } + } + + /** + * Accept the indexing pool from which numbers are selected and returned. + * Correct its format to suit the current `NumberSelector` algorithms.
+ *
+ * Moving all of the invalid negative-ones (-1) to the left of the current valid indices works for most selectors. + * The `selectionIndex` is set to the first valid number available from the left. + * The `ret` index is set to index zero. + * @param ary the `Array` of `Int` numbers + */ + def Format(ary : Array[Int]) : Unit = { + val sorted = ary.sortWith( (b, a) => if(b == -1) { a > b } else { false } ) + sorted.indices.foreach(n => ary(n) = sorted(n)) + selectionIndex = sorted.count(_ == -1) + ret = 0 + } +} diff --git a/common/src/main/scala/net/psforever/objects/guid/selector/OpportunisticSelector.scala b/common/src/main/scala/net/psforever/objects/guid/selector/OpportunisticSelector.scala new file mode 100644 index 000000000..3405dd312 --- /dev/null +++ b/common/src/main/scala/net/psforever/objects/guid/selector/OpportunisticSelector.scala @@ -0,0 +1,25 @@ +// Copyright (c) 2017 PSForever +package net.psforever.objects.guid.selector + +/** + * Get whichever number is next available. + * It is similar to `StrictInOrderSelector` but it does not stop if it runs into an unavailable number. + * It attempts to get each number in its listed incrementally from a starting index. + * The search wraps back around to the zero index to the same start index if necessary. + */ +class OpportunisticSelector extends NumberSelector { + override def Get(ary : Array[Int]) : Int = { + val start : Int = selectionIndex + if(ary(selectionIndex) == -1) { + val len : Int = ary.length + do { + selectionIndex = (selectionIndex + 1) % len + } + while(ary(selectionIndex) == -1 && selectionIndex != start) + } + val out : Int = ary(selectionIndex) + ary(selectionIndex) = -1 + selectionIndex = (selectionIndex + (out >> 31) + 1) % ary.length //(out >> 31): 0 if positive or zero, -1 if negative + out + } +} diff --git a/common/src/main/scala/net/psforever/objects/guid/selector/RandomSelector.scala b/common/src/main/scala/net/psforever/objects/guid/selector/RandomSelector.scala new file mode 100644 index 000000000..0e30607f6 --- /dev/null +++ b/common/src/main/scala/net/psforever/objects/guid/selector/RandomSelector.scala @@ -0,0 +1,68 @@ +// Copyright (c) 2017 PSForever +package net.psforever.objects.guid.selector + +/** + * Get a pseudorandom number from a pool of numbers. + * The contained logic is similar to `RandomSequenceSelector`. + * It is not reliant of a shrinking pool that composes into some sequence of all the numbers, however; + * the numbers are re-introduced to the selection as long as the pool is used. + * This allows for the sequence to contain repeat numbers far before ever visiting all of the numbers once.
+ *
+ * During the selection process:
+ * The index is the position from where the selection begins, and the end of the `Array` is where the selection ends. + * Once a position between those two indices is selected, that number is extracted. + * The number at the start position is swapped into the position where the selection number was extracted. + * The start position is then set to an invalid number, and the start index is advanced. + * Repeat next request.
+ *
+ * During the return process:
+ * The returned number is added to the input `Array` at the position just before the current selection position. + * The selection index is then reversedback to re-include the returned number. + * The normal return index is not used in this algorithm. + * @see `RandomSequenceSelector` + */ +class RandomSelector extends NumberSelector { + private val rand : scala.util.Random = new scala.util.Random(System.currentTimeMillis()) + + /** + * Accept a provided `pool` and select the next number.
+ *
+ * ... + * @param ary the `Array` of `Int` numbers from which to draw a new number + * @return an `Int` number + */ + override def Get(ary : Array[Int]) : Int = { + if(ary.length > selectionIndex) { + val selection : Int = rand.nextInt(ary.length - selectionIndex) + selectionIndex + val out : Int = ary(selection) + ary(selection) = ary(selectionIndex) + ary(selectionIndex) = -1 + selectionIndex = selectionIndex + (out >> 31) + 1 //(out >> 31): 0 if positive or zero, -1 if negative + out + } + else { + -1 + } + } + + /** + * Give a number back to a specific collection following the principles of this selector.
+ *
+ * The number is always returned to a "used" index position near the front of the array. + * It locates this position by incrementally traversing the `Array` behind the position used in `Get`. + * Asides from selection, a disorderly reinsertion of numbers back into the pool is also a source of randomness. + * @param number the number to be returned + * @param ary the `Array` of `Int` numbers to which the number is to be returned + * @return `true`, if this return was successful; `false`, otherwise + */ + override def Return(number : Int, ary : Array[Int]) : Boolean = { + if(selectionIndex > 0) { + ary(selectionIndex - 1) = number + selectionIndex -= 1 + true + } + else { + false + } + } +} diff --git a/common/src/main/scala/net/psforever/objects/guid/selector/RandomSequenceSelector.scala b/common/src/main/scala/net/psforever/objects/guid/selector/RandomSequenceSelector.scala new file mode 100644 index 000000000..c4ccae1cc --- /dev/null +++ b/common/src/main/scala/net/psforever/objects/guid/selector/RandomSequenceSelector.scala @@ -0,0 +1,64 @@ +// Copyright (c) 2017 PSForever +package net.psforever.objects.guid.selector + +/** + * Get a pseudorandom number from a pool of numbers. + * The output of this class, operating on an `Array` of `Int` values is contained to some sequence of all the numbers. + * Only after every number is selected once, may any number repeat. + * The pseudorandomness of any sequence of numbers is not only provided by an internal system `Random` but by the order or returned numbers. + * Consequentially, as any single sequence nears completion, the numbers remaining become more and more predictable.
+ *
+ * During the selection process:
+ * The index is the position from where the selection begins, and the end of the `Array` is where the selection ends. + * Once a position between those two indices is selected, that number is extracted. + * The number at the start position is swapped into the position where the selection number was extracted. + * The start position is then set to an invalid number, and the start index is advanced. + * Repeat next request.
+ *
+ * The return index trails behind the selection index as far as the order of the array is concerned at first. + * After some time, the selection index moves to the starting position of the array again and then the order is reversed. + * Until the return index wraps around to the beginning of the array too, it is considered the valid selection end position.
+ *
+ * During the return process:
+ * As the `Array` empties out from the first to the last index, the return process starts at the first index again. + * When a number is "returned," it is placed back into the input `Array` at the earliest available index. + * The return index is advanced. + * Neither the selection index nor the return index may pass each other, + * except when one reaches the end of the `Array` and wraps back around to that start. + * @see `RandomSelector` + */ +class RandomSequenceSelector extends NumberSelector { + private val rand : scala.util.Random = new scala.util.Random(System.currentTimeMillis()) + + /** + * Accept a provided "pool of numbers" and select the next number. + * @param ary the `Array` of `Int` numbers from which to draw a new number + * @return an `Int` number + */ + override def Get(ary : Array[Int]) : Int = { + val last : Int = if(ret <= selectionIndex) { ary.length } else { ret } + val selection : Int = rand.nextInt(last - selectionIndex) + selectionIndex + val out : Int = ary(selection) + ary(selection) = ary(selectionIndex) + ary(selectionIndex) = -1 + selectionIndex = (selectionIndex + (out >> 31) + 1) % ary.length //(out >> 31): 0 if positive or zero, -1 if negative + out + } + + /** + * Give a number back to a specific collection following the principles of this selector. + * @param number the number to be returned + * @param ary the `Array` of `Int` numbers to which the number is to be returned + * @return `true`, if this return was successful; `false`, otherwise + */ + override def Return(number : Int, ary : Array[Int]) : Boolean = { + if(ary(ret) == -1) { + ary(ret) = number + ret = (ret + 1) % ary.length + true + } + else { + false + } + } +} diff --git a/common/src/main/scala/net/psforever/objects/guid/selector/SpecificSelector.scala b/common/src/main/scala/net/psforever/objects/guid/selector/SpecificSelector.scala new file mode 100644 index 000000000..4b5ca43d5 --- /dev/null +++ b/common/src/main/scala/net/psforever/objects/guid/selector/SpecificSelector.scala @@ -0,0 +1,54 @@ +// Copyright (c) 2017 PSForever +package net.psforever.objects.guid.selector + +/** + * Get a specific number from a pool of numbers. + */ +class SpecificSelector extends NumberSelector { + /** + * Change the future selection index to match the number the user wants. + * Call `Get` to complete process. + * @param number the number + */ + def SelectionIndex_=(number : Int) : Unit = { + selectionIndex = number + } + + /** + * Get the specified number and the specified number only. + * @param ary the `Array` of `Int` numbers from which to draw a new number + * @return an `Int` number + */ + override def Get(ary : Array[Int]) : Int = { + if(-1 < selectionIndex && selectionIndex < ary.length) { + val out = ary(selectionIndex) + ary(selectionIndex) = -1 + out + } + else { + -1 + } + } + + /** + * Accept the indexing pool from which numbers are selected and returned. + * Correct its format to suit the current `NumberSelector` algorithms.
+ *
+ * All of the numbers are sorted to their proper indexed position in the `Array`. + * Every other number is an invalid negative-one (-1). + * The `selectionIndex` is also set to an invalid negative-one, as per the requirements of the selector. + * The `ret` index is set to index zero. + * @param ary the `Array` of `Int` numbers + */ + override def Format(ary : Array[Int]) : Unit = { + val sorted = Array.fill(ary.length)(-1) + ary.foreach(n => { + if(n > -1) { + sorted(n) = n + } + }) + sorted.copyToArray(ary) + selectionIndex = -1 + ret = 0 + } +} diff --git a/common/src/main/scala/net/psforever/objects/guid/selector/StrictInOrderSelector.scala b/common/src/main/scala/net/psforever/objects/guid/selector/StrictInOrderSelector.scala new file mode 100644 index 000000000..c0d0196b3 --- /dev/null +++ b/common/src/main/scala/net/psforever/objects/guid/selector/StrictInOrderSelector.scala @@ -0,0 +1,38 @@ +// Copyright (c) 2017 PSForever +package net.psforever.objects.guid.selector + +/** + * Get the next number in this pool incrementally. + * Starting at index 0, for example, select each subsequent number as it is available. + * Do not progress if a number is not available when requested. + */ +class StrictInOrderSelector extends NumberSelector { + override def Get(ary : Array[Int]) : Int = { + val out : Int = ary(selectionIndex) + ary(selectionIndex) = -1 + selectionIndex = (selectionIndex + (out >> 31) + 1) % ary.length //(out >> 31): 0 if positive or zero, -1 if negative + out + } + + /** + * Accept the indexing pool from which numbers are selected and returned. + * Correct its format to suit the current `NumberSelector` algorithms.
+ *
+ * All of the numbers are sorted to their proper indexed position in the `Array`. + * Every other number is an invalid negative-one (-1). + * The `selectionIndex` is set to the index of the first valid number, or zero if there are none. + * The `ret` index is set to index zero. + * @param ary the `Array` of `Int` numbers + */ + override def Format(ary : Array[Int]) : Unit = { + val sorted = Array.fill(ary.length)(-1) + ary.foreach(n => { + if(n > -1) { + sorted(n) = n + } + }) + sorted.copyToArray(ary) + selectionIndex = ary.find(n => n > -1).getOrElse(0) + ret = 0 + } +} diff --git a/common/src/main/scala/net/psforever/objects/guid/source/Key.scala b/common/src/main/scala/net/psforever/objects/guid/source/Key.scala new file mode 100644 index 000000000..7aeeef375 --- /dev/null +++ b/common/src/main/scala/net/psforever/objects/guid/source/Key.scala @@ -0,0 +1,25 @@ +// Copyright (c) 2017 PSForever +package net.psforever.objects.guid.source + +import net.psforever.objects.entity.IdentifiableEntity +import net.psforever.objects.guid.AvailabilityPolicy +import net.psforever.objects.guid.key.Monitor + +private class Key extends Monitor { + private var policy : AvailabilityPolicy.Value = AvailabilityPolicy.Available + private var obj : Option[IdentifiableEntity] = None + + def Policy : AvailabilityPolicy.Value = policy + + def Policy_=(pol : AvailabilityPolicy.Value) : AvailabilityPolicy.Value = { + policy = pol + Policy + } + + def Object : Option[IdentifiableEntity] = obj + + def Object_=(objct : Option[IdentifiableEntity]) : Option[IdentifiableEntity] = { + obj = objct + Object + } +} diff --git a/common/src/main/scala/net/psforever/objects/guid/source/LimitedNumberSource.scala b/common/src/main/scala/net/psforever/objects/guid/source/LimitedNumberSource.scala new file mode 100644 index 000000000..856323f9e --- /dev/null +++ b/common/src/main/scala/net/psforever/objects/guid/source/LimitedNumberSource.scala @@ -0,0 +1,113 @@ +// Copyright (c) 2017 PSForever +package net.psforever.objects.guid.source + +import net.psforever.objects.entity.IdentifiableEntity +import net.psforever.objects.guid.key.{LoanedKey, SecureKey} +import net.psforever.objects.guid.AvailabilityPolicy + +import scala.collection.mutable + +/** + * A `NumberSource` is considered a master "pool" of numbers from which all numbers are available to be drawn. + * The numbers are considered to be exclusive.
+ *
+ * Produce a series of numbers from 0 to a maximum number (inclusive) to be used as globally unique identifiers (GUIDs). + * @param max the highest number to be generated by this source; + * must be a positive integer or zero + * @throws IllegalArgumentException if `max` is less than zero (therefore the count of generated numbers is at most zero) + * @throws java.lang.NegativeArraySizeException if the count of numbers generated due to max is negative + */ +class LimitedNumberSource(max : Int) extends NumberSource { + if(max < 0) { + throw new IllegalArgumentException(s"non-negative integers only, not $max") + } + private val ary : Array[Key] = Array.ofDim[Key](max + 1) + (0 to max).foreach(x => { ary(x) = new Key }) + private var allowRestrictions : Boolean = true + + def Size : Int = ary.length + + def CountAvailable : Int = ary.count(key => key.Policy == AvailabilityPolicy.Available) + + def CountUsed : Int = ary.count(key => key.Policy != AvailabilityPolicy.Available) + + def Get(number : Int) : Option[SecureKey] = { + if(Test(number)) { + Some(new SecureKey(number, ary(number))) + } + else { + None + } + } + + def Available(number : Int) : Option[LoanedKey] = { + var out : Option[LoanedKey] = None + if(Test(number)) { + val key : Key = ary(number) + if(key.Policy == AvailabilityPolicy.Available) { + key.Policy = AvailabilityPolicy.Leased + out = Some(new LoanedKey(number, key)) + } + } + out + } + + /** + * Consume the number of a `Monitor` and release that number from its previous assignment/use. + * @param number the number + * @return any object previously using this number + */ + def Return(number : Int) : Option[IdentifiableEntity] = { + var out : Option[IdentifiableEntity] = None + if(Test(number)) { + val existing : Key = ary(number) + if(existing.Policy == AvailabilityPolicy.Leased) { + out = existing.Object + existing.Policy = AvailabilityPolicy.Available + existing.Object = None + } + } + out + } + + /** + * Produce a modifiable wrapper for the `Monitor` for this number, only if the number has not been used. + * This wrapped `Monitor` can only be assigned once and the number may not be `Return`ed to this source. + * @param number the number + * @return the wrapped `Monitor` + * @throws ArrayIndexOutOfBoundsException if the requested number is above or below the range + */ + def Restrict(number : Int) : Option[LoanedKey] = { + if(allowRestrictions && Test(number)) { + val key : Key = ary(number) + key.Policy = AvailabilityPolicy.Restricted + Some(new LoanedKey(number, key)) + } + else { + None + } + } + + def FinalizeRestrictions : List[Int] = { + allowRestrictions = false + ary.zipWithIndex.filter(entry => entry._1.Policy == AvailabilityPolicy.Restricted).map(entry => entry._2).toList + } + + def Clear() : List[IdentifiableEntity] = { + val outList : mutable.ListBuffer[IdentifiableEntity] = mutable.ListBuffer[IdentifiableEntity]() + for(x <- ary.indices) { + ary(x).Policy = AvailabilityPolicy.Available + if(ary(x).Object.isDefined) { + outList += ary(x).Object.get + ary(x).Object = None + } + } + outList.toList + } +} + +object LimitedNumberSource { + def apply(max : Int) : LimitedNumberSource = { + new LimitedNumberSource(max) + } +} diff --git a/common/src/main/scala/net/psforever/objects/guid/source/MaxNumberSource.scala b/common/src/main/scala/net/psforever/objects/guid/source/MaxNumberSource.scala new file mode 100644 index 000000000..ea5c969b6 --- /dev/null +++ b/common/src/main/scala/net/psforever/objects/guid/source/MaxNumberSource.scala @@ -0,0 +1,119 @@ +// Copyright (c) 2017 PSForever +package net.psforever.objects.guid.source + +import net.psforever.objects.entity.IdentifiableEntity +import net.psforever.objects.guid.key.{LoanedKey, SecureKey} +import net.psforever.objects.guid.AvailabilityPolicy + +/** + * A `NumberSource` is considered a master "pool" of numbers from which all numbers are available to be drawn. + * The numbers are considered to be exclusive.
+ *
+ * This source utilizes all positive integers (to `Int.MaxValue`, anyway) and zero. + * It allocates number `Monitors` as it needs them. + * While this allows for a wide range of possible numbers, the internal structure expands and contracts as needed. + * The underlying flexible structure is a `LongMap` and is subject to constraints regarding `LongMap` growth. + */ +class MaxNumberSource() extends NumberSource { + import scala.collection.mutable + private val hash : mutable.LongMap[Key] = mutable.LongMap[Key]() //TODO consider seeding an initialBufferSize + private var allowRestrictions : Boolean = true + + def Size : Int = Int.MaxValue + + def CountAvailable : Int = Size - CountUsed + + def CountUsed : Int = hash.size + + override def Test(guid : Int) : Boolean = guid > -1 + + def Get(number : Int) : Option[SecureKey] = { + if(!Test(number)) { + None + } + else { + val existing : Option[Key] = hash.get(number).orElse({ + val key : Key = new Key + key.Policy = AvailabilityPolicy.Available + hash.put(number, key) + Some(key) + }) + Some(new SecureKey(number, existing.get)) + } + } + +// def GetAll(list : List[Int]) : List[SecureKey] = { +// list.map(number => +// hash.get(number) match { +// case Some(key) => +// new SecureKey(number, key) +// case _ => +// new SecureKey(number, new Key { Policy = AvailabilityPolicy.Available }) +// } +// ) +// } +// +// def GetAll( p : Key => Boolean ) : List[SecureKey] = { +// hash.filter(entry => p.apply(entry._2)).map(entry => new SecureKey(entry._1.toInt, entry._2)).toList +// } + + def Available(number : Int) : Option[LoanedKey] = { + if(!Test(number)) { + throw new IndexOutOfBoundsException("number can not be negative") + } + hash.get(number) match { + case Some(_) => + None + case _ => + val key : Key = new Key + key.Policy = AvailabilityPolicy.Leased + hash.put(number, key) + Some(new LoanedKey(number, key)) + } + } + + def Return(number : Int) : Option[IdentifiableEntity] = { + val existing = hash.get(number) + if(existing.isDefined && existing.get.Policy == AvailabilityPolicy.Leased) { + hash -= number + val obj = existing.get.Object + existing.get.Object = None + obj + } + else { + None + } + } + + def Restrict(number : Int) : Option[LoanedKey] = { + if(allowRestrictions) { + val existing : Key = hash.get(number).orElse({ + val key : Key = new Key + hash.put(number, key) + Some(key) + }).get + existing.Policy = AvailabilityPolicy.Restricted + Some(new LoanedKey(number, existing)) + } + else { + None + } + } + + def FinalizeRestrictions : List[Int] = { + allowRestrictions = false + hash.filter(entry => entry._2.Policy == AvailabilityPolicy.Restricted).map(entry => entry._1.toInt).toList + } + + def Clear() : List[IdentifiableEntity] = { + val list : List[IdentifiableEntity] = hash.values.filter(key => key.Object.isDefined).map(key => key.Object.get).toList + hash.clear() + list + } +} + +object MaxNumberSource { + def apply() : MaxNumberSource = { + new MaxNumberSource() + } +} \ No newline at end of file diff --git a/common/src/main/scala/net/psforever/objects/guid/source/NumberSource.scala b/common/src/main/scala/net/psforever/objects/guid/source/NumberSource.scala new file mode 100644 index 000000000..f169eabe9 --- /dev/null +++ b/common/src/main/scala/net/psforever/objects/guid/source/NumberSource.scala @@ -0,0 +1,154 @@ +// Copyright (c) 2017 PSForever +package net.psforever.objects.guid.source + +import net.psforever.objects.entity.IdentifiableEntity +import net.psforever.objects.guid.key.{LoanedKey, SecureKey} + +trait NumberSourceAccessors { + /** + * Produce an un-modifiable wrapper for the `Monitor` for this number. + * @param number the number + * @return the wrapped `Monitor` + */ + def Get(number : Int) : Option[SecureKey] + + /** + * Produce a modifiable wrapper for the `Monitor` for this number, only if the number has not been used. + * The `Monitor` should be updated before being wrapped, if necessary. + * @param number the number + * @return the wrapped `Monitor`, or `None` + */ + def Available(number : Int) : Option[LoanedKey] + + /** + * Consume a wrapped `Monitor` and release its number from its previous assignment/use. + * @param monitor the `Monitor` + * @return any object previously using this `Monitor` + */ + def Return(monitor : SecureKey) : Option[IdentifiableEntity] = { + Return(monitor.GUID) + } + + /** + * Consume a wrapped `Monitor` and release its number from its previous assignment/use. + * @param monitor the `Monitor` + * @return any object previously using this `Monitor` + */ + def Return(monitor : LoanedKey) : Option[IdentifiableEntity] = { + Return(monitor.GUID) + } + + /** + * Consume the number of a `Monitor` and release that number from its previous assignment/use. + * @param number the number + * @return any object previously using this number + */ + def Return(number : Int) : Option[IdentifiableEntity] +} + +/** + * A `NumberSource` is considered a master "pool" of numbers from which all numbers are available to be drawn. + * The numbers are considered to be exclusive.
+ *
+ * The following are guidelines for implementing classes. + * The numbers allocated to this source are from zero up through positive integers. + * When a number is drawn from the pool, it is flagged internally and can not be selected for drawing again until the flag is removed. + * Some flagging states are allowed to restrict that number for the whole lifespan of the source. + * This internal flagging is maintained by a "monitor" that should not directly get exposed. + * Use the provided indirect referencing containers - `SecureKey` and `LoanedKey`.
+ *
+ * The purpose of a `NumberSource` is to help facilitate globally unique identifiers (GUID, pl. GUIDs). + */ +trait NumberSource { + /** + * The count of numbers allocated to this source. + * @return the count + */ + def Size : Int + + /** + * The count of numbers that can still be drawn. + * @return the count + */ + def CountAvailable : Int + + /** + * The count of numbers that can not be drawn. + * @return the count + */ + def CountUsed : Int + + /** + * Is this number a member of this number source? + * @param number the number + * @return `true`, if it is a member; `false`, otherwise + */ + def Test(number : Int) : Boolean = -1 < number && number < Size + + /** + * Produce an un-modifiable wrapper for the `Monitor` for this number. + * @param number the number + * @return the wrapped `Monitor` + */ + def Get(number : Int) : Option[SecureKey] + + //def GetAll(list : List[Int]) : List[SecureKey] + + //def GetAll(p : Key => Boolean) : List[SecureKey] + + /** + * Produce a modifiable wrapper for the `Monitor` for this number, only if the number has not been used. + * The `Monitor` should be updated before being wrapped, if necessary. + * @param number the number + * @return the wrapped `Monitor`, or `None` + */ + def Available(number : Int) : Option[LoanedKey] + + /** + * Consume a wrapped `Monitor` and release its number from its previous assignment/use. + * @param monitor the `Monitor` + * @return any object previously using this `Monitor` + */ + def Return(monitor : SecureKey) : Option[IdentifiableEntity] = { + Return(monitor.GUID) + } + + /** + * Consume a wrapped `Monitor` and release its number from its previous assignment/use. + * @param monitor the `Monitor` + * @return any object previously using this `Monitor` + */ + def Return(monitor : LoanedKey) : Option[IdentifiableEntity] = { + Return(monitor.GUID) + } + + /** + * Consume the number of a `Monitor` and release that number from its previous assignment/use. + * @param number the number + * @return any object previously using this number + */ + def Return(number : Int) : Option[IdentifiableEntity] + + /** + * Produce a modifiable wrapper for the `Monitor` for this number, only if the number has not been used. + * This wrapped `Monitor` can only be assigned once and the number may not be `Return`ed to this source. + * @param number the number + * @return the wrapped `Monitor` + */ + def Restrict(number : Int) : Option[LoanedKey] + + /** + * Numbers from this source may not longer be marked as `Restricted`. + * @return the `List` of all numbers that have been restricted + */ + def FinalizeRestrictions : List[Int] + + import net.psforever.objects.entity.IdentifiableEntity + /** + * Reset all number `Monitor`s so that their underlying number is not longer treated as assigned. + * Perform some level of housecleaning to ensure that all dependencies are resolved in some manner. + * This is the only way to free `Monitors` that are marked as `Restricted`. + * @return a `List` of assignments maintained by all the currently-used number `Monitors` + */ + def Clear() : List[IdentifiableEntity] +} diff --git a/common/src/main/scala/net/psforever/objects/inventory/GridInventory.scala b/common/src/main/scala/net/psforever/objects/inventory/GridInventory.scala new file mode 100644 index 000000000..8307cea26 --- /dev/null +++ b/common/src/main/scala/net/psforever/objects/inventory/GridInventory.scala @@ -0,0 +1,586 @@ +// Copyright (c) 2017 PSForever +package net.psforever.objects.inventory + +import java.util.concurrent.atomic.AtomicInteger + +import net.psforever.objects.equipment.Equipment +import net.psforever.objects.EquipmentSlot +import net.psforever.packet.game.PlanetSideGUID + +import scala.annotation.tailrec +import scala.collection.immutable.Map +import scala.collection.mutable +import scala.util.{Failure, Success, Try} + +/** + * An inventory are used to stow `Equipment` when it does not exist visually in the game world.
+ *
+ * Visually, an inventory is understood as a rectangular region divided into cellular units. + * The `Equipment` that is placed into the inventory can also be represented as smaller rectangles, also composed of cells. + * The same number of cells of the item must overlap with the same number of cells of the inventory. + * No two items may have cells that overlap. + * This "grid" maintains a spatial distinction between items when they get stowed.
+ *
+ * It is not necessary to actually have a structural representation of the "grid." + * Adhering to such a data structure does speed up the actions upon the inventory and its contents in certain cases (where noted). + * The `HashMap` of items is used for quick object lookup. + * Use of the `HashMap` only is hitherto referred as "using the inventory as a `List`." + * The `Array` of spatial GUIDs is used for quick collision lookup. + * Use of the `Array` only is hitherto referred as "using the inventory as a grid." + */ +class GridInventory { + private var width : Int = 1 + private var height : Int = 1 + private var offset : Int = 0 //the effective index of the first cell in the inventory where offset >= 0 + + /* key - an integer (not especially meaningful beyond being unique); value - the card that represents the stowed item */ + private val items : mutable.HashMap[Int, InventoryItem] = mutable.HashMap[Int, InventoryItem]() + private val entryIndex : AtomicInteger = new AtomicInteger(0) + private var grid : Array[Int] = Array.fill[Int](1)(-1) + + def Items : Map[Int, InventoryItem] = items.toMap[Int, InventoryItem] + + def Width : Int = width + + def Height : Int = height + + def Offset : Int = offset + + /** + * Change the grid index offset value. + * @param fset the new offset value + * @return the current offset value + * @throws IndexOutOfBoundsException if the index is negative + */ + def Offset_=(fset : Int) : Int = { + if(fset < 0) { + throw new IndexOutOfBoundsException(s"can not set index offset to negative number - $fset") + } + offset = fset + Offset + } + + def Size : Int = items.size + + /** + * Capacity is a measure how many squares in the grid inventory are unused (value of -1). + * It does not guarantee the cells are distributed in any configuration conductive to item stowing. + * @return the number of free cells + */ + def Capacity : Int = { + TotalCapacity - items.values.foldLeft(0)((cnt, item) => cnt + (item.obj.Tile.width * item.obj.Tile.height)) + } + + /** + * The total number of cells in this inventory. + * @return the width multiplied by the height (`grid.length`, which is the same thing) + */ + def TotalCapacity : Int = grid.length + + /** + * The index of the last cell in this inventory. + * @return same as `Offset` plus the total number of cells in this inventory minus 1 + */ + def LastIndex : Int = Offset + TotalCapacity - 1 + + /** + * Get whatever is stowed in the inventory at the given index. + * @param slot the cell index + * @return an `EquipmentSlot` that contains whatever `Equipment` was stored in `slot` + */ + def Slot(slot : Int) : EquipmentSlot = { + val actualSlot = slot - offset + if(actualSlot < 0 || actualSlot > grid.length) { + throw new IndexOutOfBoundsException(s"requested indices not in bounds of grid inventory - $actualSlot") + } + else { + new InventoryEquipmentSlot(slot, this) + } + } + + /** + * Test whether a given piece of `Equipment` would collide with any stowed content in the inventory.
+ *
+ * A "collision" is considered a situation where the stowed placards of two items would overlap in some way. + * The gridkeeps track of the location of items by storing the primitive of their GUID in one or more cells. + * Two primitives can not be stored in the same cell. + * If placing two items into the same inventory leads to a situation where two primitive values might be in the same cell, + * that is a collision. + * @param start the cell index to test this `Equipment` for insertion + * @param item the `Equipment` to be tested + * @return a `List` of GUID values for all existing contents that this item would overlap if inserted + */ + def CheckCollisions(start : Int, item : Equipment) : Try[List[Int]] = { + val tile : InventoryTile = item.Tile + CheckCollisions(start, tile.width, tile.height) + } + + /** + * Test whether a given piece of `Equipment` would collide with any stowed content in the inventory.
+ *
+ * If there are fewer items stored in the inventory than there are cells required to represent the testing item, + * test the collision by iterating through the list of items. + * If there are more items, check that each cell that would be used for the testing items tile does not collide. + * The "testing item" in this case has already been transformed into its tile dimensions. + * @param start the cell index to test this `Equipment` for insertion + * @param w the width of the `Equipment` to be tested + * @param h the height of the `Equipment` to be tested + * @return a `List` of GUID values for all existing contents that this item would overlap if inserted + */ + def CheckCollisions(start : Int, w : Int, h : Int) : Try[List[Int]] = { + if(items.isEmpty) { + Success(List.empty[Int]) + } + else { + def check : (Int,Int,Int) => Try[List[Int]] = if(items.size < w * h) { CheckCollisionsAsList } else { CheckCollisionsAsGrid } + check(start, w, h) + } + } + + /** + * Test whether a given piece of `Equipment` would collide with any stowed content in the inventory.
+ *
+ * Iterate over all stowed items and check each one whether or not it overlaps with the given region. + * This is a "using the inventory as a `List`" method. + * @param start the cell index to test this `Equipment` for insertion + * @param w the width of the `Equipment` to be tested + * @param h the height of the `Equipment` to be tested + * @return a `List` of GUID values for all existing contents that this item would overlap if inserted + * @throws IndexOutOfBoundsException if the region extends outside of the grid boundaries + */ + def CheckCollisionsAsList(start : Int, w : Int, h : Int) : Try[List[Int]] = { + val actualSlot = start - offset + val startx : Int = actualSlot % width + val starty : Int = actualSlot / width + val startw : Int = startx + w - 1 + val starth : Int = starty + h - 1 + if(actualSlot < 0 || actualSlot >= grid.length || startw >= width || starth >= height) { + val bounds : String = if(startx < 0) { "left" } else if(startw >= width) { "right" } else { "bottom" } + Failure(new IndexOutOfBoundsException(s"requested region escapes the $bounds edge of the grid inventory - $startx, $starty; $w x $h")) + } + else { + val collisions : mutable.Set[Int] = mutable.Set[Int]() + items.values.foreach({ item : InventoryItem => + val actualItemStart : Int = item.start - offset + val itemx : Int = actualItemStart % width + val itemy : Int = actualItemStart / width + val tile = item.obj.Tile + val clipsOnX : Boolean = if(itemx < startx) { itemx + tile.width > startx } else { itemx <= startw } + val clipsOnY : Boolean = if(itemy < starty) { itemy + tile.height > starty } else { itemy <= starth } + if(clipsOnX && clipsOnY) { + collisions += item.GUID.guid + } + }) + Success(collisions.toList) + } + } + + /** + * Test whether a given piece of `Equipment` would collide with any stowed content in the inventory.
+ *
+ * Iterate over all cells that would be occupied by a new value and check each one whether or not that cell has an existing value. + * This is a "using the inventory as a grid" method. + * @param start the cell index to test this `Equipment` for insertion + * @param w the width of the `Equipment` to be tested + * @param h the height of the `Equipment` to be tested + * @return a `List` of GUID values for all existing contents that this item would overlap if inserted + * @throws IndexOutOfBoundsException if the region extends outside of the grid boundaries + */ + def CheckCollisionsAsGrid(start : Int, w : Int, h : Int) : Try[List[Int]] = { + val actualSlot = start - offset + if(actualSlot < 0 || actualSlot >= grid.length || (actualSlot % width) + w > width || (actualSlot / width) + h > height) { + val startx : Int = actualSlot % width + val starty : Int = actualSlot / width + val startw : Int = startx + w - 1 + val bounds : String = if(startx < 0) { "left" } else if(startw >= width) { "right" } else { "bottom" } + Failure(new IndexOutOfBoundsException(s"requested region escapes the $bounds edge of the grid inventory - $startx, $starty; $w x $h")) + } + else { + val collisions : mutable.Set[Int] = mutable.Set[Int]() + var curr = actualSlot + for(_ <- 0 until h) { + for(col <- 0 until w) { + if(grid(curr + col) > -1) { + collisions += items(grid(curr + col)).GUID.guid + } + } + curr += width + } + Success(collisions.toList) + } + } + + /** + * Find a blank space in the current inventory where a `tile` of given dimensions can be cleanly inserted. + * Brute-force method. + * @param tile the dimensions of the blank space + * @return the grid index of the upper left corner where equipment to which the `tile` belongs should be placed + */ + def Fit(tile : InventoryTile) : Option[Int] = { + val tWidth = tile.width + val tHeight = tile.height + val gridIter = (0 until (grid.length - (tHeight - 1) * width)) + .filter(cell => grid(cell) == -1 && (width - cell%width >= tWidth)) + .iterator + recursiveFitTest(gridIter, tWidth, tHeight) + } + + /** + * Find a blank space in the current inventory where a `tile` of given dimensions can be cleanly inserted. + * @param cells an iterator of all accepted indices in the `grid` + * @param tWidth the width of the blank space + * @param tHeight the height of the blank space + * @return the grid index of the upper left corner where equipment to which the `tile` belongs should be placed + */ + @tailrec private def recursiveFitTest(cells : Iterator[Int], tWidth : Int, tHeight : Int) : Option[Int] = { + if(!cells.hasNext) { + None + } + else { + val index = cells.next + offset + CheckCollisionsAsGrid(index, tWidth, tHeight) match { + case Success(Nil) => + Some(index) + case Success(_) => + recursiveFitTest(cells, tWidth, tHeight) + case Failure(ex) => + throw ex + } + } + } + + /** + * Define a region of inventory grid cells and set them to a given value. + * @param start the initial inventory index + * @param w the width of the region + * @param h the height of the region + * @param value the value to set all the cells in the defined region; + * defaults to -1 (which is "nothing") + */ + def SetCells(start : Int, w : Int, h : Int, value : Int = -1) : Unit = { + SetCellsOffset(math.max(start - offset, 0), w, h, value) + } + + /** + * Define a region of inventory grid cells and set them to a given value. + * @param start the initial inventory index, without the inventory offset (required) + * @param w the width of the region + * @param h the height of the region + * @param value the value to set all the cells in the defined region; + * defaults to -1 (which is "nothing") + * @throws IndexOutOfBoundsException if the region extends outside of the grid boundaries + */ + def SetCellsOffset(start : Int, w : Int, h : Int, value : Int = -1) : Unit = { + if(start < 0 || start > grid.length || (start % width) + w - 1 > width || (start / width) + h- 1 > height) { + val startx : Int = start % width + val starty : Int = start / width + val startw : Int = startx + w - 1 + val bounds : String = if(startx < 0) { "left" } else if(startw >= width) { "right" } else { "bottom" } + throw new IndexOutOfBoundsException(s"requested region escapes the $bounds of the grid inventory - $startx, $starty; $w x $h") + } + else { + var curr = start + for(_ <- 0 until h) { + for(col <- 0 until w) { + grid(curr + col) = value + } + curr += width + } + } + } + + def Insert(start : Int, obj : Equipment) : Boolean = { + val key : Int = entryIndex.getAndIncrement() + items.get(key) match { + case None => //no redundant insertions or other collisions + Insertion_CheckCollisions(start, obj, key) + case _ => + false + } + } + + def Insertion_CheckCollisions(start : Int, obj : Equipment, key : Int) : Boolean = { + CheckCollisions(start, obj) match { + case Success(Nil) => + val card = InventoryItem(obj, start) + items += key -> card + val tile = obj.Tile + SetCells(start, tile.width, tile.height, key) + true + case _ => + false + } + } + + def +=(kv : (Int, Equipment)) : Boolean = Insert(kv._1, kv._2) + +// def InsertQuickly(start : Int, obj : Equipment) : Boolean = { +// val guid : Int = obj.GUID.guid +// val card = InventoryItemData(obj, start) +// items += guid -> card +// val tile = obj.Tile +// SetCellsOffset(start, tile.width, tile.height, guid) +// true +// } + + def Remove(index : Int) : Boolean = { + val key = grid(index - Offset) + items.remove(key) match { + case Some(item) => + val tile = item.obj.Tile + SetCells(item.start, tile.width, tile.height) + true + case None => + false + } + } + + def -=(index : Int) : Boolean = Remove(index) + + def Remove(guid : PlanetSideGUID) : Boolean = { + recursiveFindIdentifiedObject(items.keys.iterator, guid) match { + case Some(index) => + val item = items.remove(index).get + val tile = item.obj.Tile + SetCells(item.start, tile.width, tile.height) + true + case None => + false + } + } + + def -=(guid : PlanetSideGUID) : Boolean = Remove(guid) + + @tailrec private def recursiveFindIdentifiedObject(iter : Iterator[Int], guid : PlanetSideGUID) : Option[Int] = { + if(!iter.hasNext) { + None + } + else { + val index = iter.next + if(items(index).obj.GUID == guid) { + Some(index) + } + else { + recursiveFindIdentifiedObject(iter, guid) + } + } + } + + /** + * Does this inventory contain an object with the given GUID? + * @param guid the GUID + * @return the discovered object, or `None` + */ + def hasItem(guid : PlanetSideGUID) : Option[Equipment] = { + recursiveFindIdentifiedObject(items.keys.iterator, guid) match { + case Some(index) => + Some(items(index).obj) + case None => + None + } + } + + /** + * Clear the inventory by removing all of its items. + * @return a `List` of the previous items in the inventory as their `InventoryItemData` tiles + */ + def Clear() : List[InventoryItem] = { + val list = items.values.toList + items.clear + SetCellsOffset(0, width, height) + list + } + + /** + * Change the size of the inventory, without regard for its current contents. + * This method replaces mutators for `Width` and `Height`. + * @param w the new width + * @param h the new height + * @throws IllegalArgumentException if the new size to be set is zero or less + */ + def Resize(w : Int, h : Int) : Unit = { + if(w < 1 || h < 1) { + throw new IllegalArgumentException("area of inventory space must not be < 1") + } + width = w + height = h + grid = Array.fill[Int](w * h)(-1) + } +} + +object GridInventory { + /** + * Overloaded constructor. + * @return a `GridInventory` object + */ + def apply() : GridInventory = { + new GridInventory() + } + + /** + * Overloaded constructor for initializing an inventory of specific dimensions. + * @param width the horizontal size of the inventory + * @param height the vertical size of the inventory + * @return a `GridInventory` object + */ + def apply(width : Int, height : Int) : GridInventory = { + val obj = new GridInventory() + obj.Resize(width, height) + obj + } + + /** + * Overloaded constructor for initializing an inventory of specific dimensions and index offset. + * @param width the horizontal size of the inventory + * @param height the vertical size of the inventory + * @param offset the effective index of the first cell in the inventory + * @return a `GridInventory` object + */ + def apply(width : Int, height : Int, offset : Int) : GridInventory = { + val obj = new GridInventory() + obj.Resize(width, height) + obj.Offset = offset + obj + } + + /** + * Accepting items that may or may not have previously been in an inventory, + * determine if there is a tight-fit arrangement for the items in the given inventory. + * Note that arrangement for future insertion. + * @param list a `List` of items to be potentially re-inserted + * @param predicate a condition to sort the previous `List` of elements + * @param inv the inventory in which they would be re-inserted in the future + * @return two `List`s of `Equipment`; + * the first `List` is composed of `InventoryItemData`s that will be reinserted at the new `start` index; + * the second list is composed of `Equipment` that will not be put back into the inventory + */ + def recoverInventory(list : List[InventoryItem], inv : GridInventory, predicate : (InventoryItem, InventoryItem) => Boolean = StandardScaleSort) : (List[InventoryItem], List[Equipment]) = { + sortKnapsack( + list.sortWith(predicate), + inv.width, + inv.height + ) + val (elements, out) = list.partition(p => p.start > -1) + elements.foreach(item => item.start += inv.Offset) + (elements, out.map(item => item.obj)) + } + + /** + * The default predicate used by the knapsack sort algorithm. + */ + final val StandardScaleSort : (InventoryItem, InventoryItem) => Boolean = + (a, b) => { + val aTile = a.obj.Tile + val bTile = b.obj.Tile + if(aTile.width == bTile.width) { + aTile.height > bTile.height + } + else { + aTile.width > bTile.width + } + } + + /** + * Start calculating the "optimal" fit for a `List` of items in an inventory of given size.
+ *
+ * The initial dimensions always fit a space of 0,0 to `width`, `height`. + * As locations for elements are discovered, the `start` index for that `List` element is changed in-place. + * If an element can not be re-inserted according to the algorithm, the `start` index is set to an invalid -1. + * @param list a `List` of items to be potentially re-inserted + * @param width the horizontal length of the inventory + * @param height the vertical length of the inventory + */ + private def sortKnapsack(list : List[InventoryItem], width : Int, height : Int) : Unit = { + val root = new KnapsackNode(0, 0, width, height) + list.foreach(item => { + findKnapsackSpace(root, item.obj.Tile.width, item.obj.Tile.height) match { + case Some(node) => + splitKnapsackSpace(node, item.obj.Tile.width, item.obj.Tile.height) + item.start = node.y * width + node.x + case _ => ; + item.start = -1 + } + }) + } + + /** + * A binary tree node suitable for executing a hasty solution to the knapsack problem.
+ *
+ * All children are flush with their parent node and with each other. + * Horizontal space for the `down` child is emphasized over vertical space for the `right` child. + * By dividing and reducing a defined space like this, it can be tightly packed with a given number of elements.
+ *
+ * Due to the nature of the knapsack problem and the naivette of the algorithm, small holes in the solution are bound to crop-up. + * @param x the x-coordinate, upper left corner + * @param y the y-coordinate, upper left corner + * @param width the width + * @param height the height + */ + private class KnapsackNode(var x : Int, var y : Int, var width : Int, var height : Int) { + private var used : Boolean = false + var down : Option[KnapsackNode] = None + var right : Option[KnapsackNode] = None + + def Used : Boolean = used + + /** + * Initialize the `down` and `right` children of this node. + */ + def Split() : Unit = { + used = true + down = Some(new KnapsackNode(0,0,0,0)) + right = Some(new KnapsackNode(0,0,0,0)) + } + + /** + * Change the dimensions of the node.
+ *
+ * Use: `{node}(nx, ny, nw, nh)` + * @param nx the new x-coordinate, upper left corner + * @param ny the new y-coordinate, upper left corner + * @param nw the new width + * @param nh the new height + */ + def apply(nx : Int, ny : Int, nw : Int, nh : Int) : Unit = { + x = nx + y = ny + width = nw + height = nh + } + } + + /** + * Search this node and its children for a space that can be occupied by an element of given dimensions. + * @param node the current node + * @param width width of the element + * @param height height of the element + * @return the selected node + */ + private def findKnapsackSpace(node : KnapsackNode, width : Int, height : Int) : Option[KnapsackNode] = { + if(node.Used) { + findKnapsackSpace(node.right.get, width, height).orElse(findKnapsackSpace(node.down.get, width, height)) + } + else if(width <= node.width && height <= node.height) { + Some(node) + } + else { + None + } + } + + /** + * Populate the `down` and `right` nodes for the knapsack sort.
+ *
+ * This function carves node into three pieces. + * The third piece is the unspoken space occupied by the element of given dimensions. + * Specifically: `node.x`, `node.y` to `width`, `height`. + * @param node the current node + * @param width width of the element + * @param height height of the element + */ + private def splitKnapsackSpace(node : KnapsackNode, width : Int, height : Int) : Unit = { + node.Split() + node.down.get(node.x, node.y + height, node.width, node.height - height) + node.right.get(node.x + width, node.y, node.width - width, height) + } +} diff --git a/common/src/main/scala/net/psforever/objects/inventory/InventoryEquipmentSlot.scala b/common/src/main/scala/net/psforever/objects/inventory/InventoryEquipmentSlot.scala new file mode 100644 index 000000000..5e4515ed5 --- /dev/null +++ b/common/src/main/scala/net/psforever/objects/inventory/InventoryEquipmentSlot.scala @@ -0,0 +1,43 @@ +// Copyright (c) 2017 PSForever +package net.psforever.objects.inventory + +import net.psforever.objects.OffhandEquipmentSlot +import net.psforever.objects.equipment.{Equipment, EquipmentSize} + +/** + * A slot-like interface for a specific grid position in an inventory. + * The size is bound to anything that can be stowed, which encompasses most all `Equipment`. + * Furthermore, rather than operating on a fixed-size slot, this "slot" represents an inventory region that either includes `slot` or starts at `slot`. + * An object added to the underlying inventory from here can only be added with its initial point at `slot`. + * An object found at `slot`, however, can be removed even if the starting cell is prior to `slot.` + */ +class InventoryEquipmentSlot(private val slot : Int, private val inv : GridInventory) extends OffhandEquipmentSlot(EquipmentSize.Inventory) { + /** + * Attempt to stow an item into the inventory at the given position. + * @param assignEquipment the change in `Equipment` for this slot + * @return the `Equipment` in this slot + */ + override def Equipment_=(assignEquipment : Option[Equipment]) : Option[Equipment] = { + assignEquipment match { + case Some(equip) => + inv += slot -> equip + case None => + inv -= slot + } + Equipment + } + + /** + * Determine what `Equipment`, if any, is stowed in the inventory in the given position. + * @return the `Equipment` in this slot + */ + override def Equipment : Option[Equipment] = { + inv.Items.find({ case ((_, item : InventoryItem)) => item.start == slot }) match { + case Some((_, item : InventoryItem)) => + Some(item.obj) + case None => + None + } + } +} + diff --git a/common/src/main/scala/net/psforever/objects/inventory/InventoryItem.scala b/common/src/main/scala/net/psforever/objects/inventory/InventoryItem.scala new file mode 100644 index 000000000..09000022a --- /dev/null +++ b/common/src/main/scala/net/psforever/objects/inventory/InventoryItem.scala @@ -0,0 +1,23 @@ +// Copyright (c) 2017 PSForever +package net.psforever.objects.inventory + +import net.psforever.objects.equipment.Equipment +import net.psforever.packet.game.PlanetSideGUID + +/** + * Represent the image placard that is used to visually and spatially manipulate an item placed into the grid-like inventory. + * The unofficial term for this placard (the size of the placard) is a "tile." + * The size of the tile is usually fixed but the origin point of the tile can be changed. + * @param obj the item being placed into the inventory grid + * @param start the index of the upper-left square of the item's tile + */ +class InventoryItem(val obj : Equipment, var start : Int = 0) { + //TODO eventually move this object from storing the item directly to just storing its GUID? + def GUID : PlanetSideGUID = obj.GUID +} + +object InventoryItem { + def apply(obj : Equipment, start : Int) : InventoryItem = { + new InventoryItem(obj, start) + } +} diff --git a/common/src/main/scala/net/psforever/objects/inventory/InventoryTile.scala b/common/src/main/scala/net/psforever/objects/inventory/InventoryTile.scala new file mode 100644 index 000000000..c860a0c32 --- /dev/null +++ b/common/src/main/scala/net/psforever/objects/inventory/InventoryTile.scala @@ -0,0 +1,35 @@ +// Copyright (c) 2017 PSForever +package net.psforever.objects.inventory + +/** + * A "tile" represents the size of the icon placard that is used by `Equipment` when placed into an inventory or visible slot. + * It is also used by some `ObjectDefinition`s to pass information about the size of an inventory itself. + * @param width the width of the tile + * @param height the height of the tile + * @throws IllegalArgumentException if either the width or the height are less than zero + */ +class InventoryTile(val width : Int, val height : Int) { + if(width < 0 || height < 0) + throw new IllegalArgumentException(s"tile has no area - width: $width, height: $height") + + def Width : Int = width + + def Height : Int = height +} + +object InventoryTile { + final val None = InventoryTile(0,0) //technically invalid; used to indicate a vehicle with no trunk + final val Tile11 = InventoryTile(1,1) //placeholder size + final val Tile22 = InventoryTile(2,2) //grenades, boomer trigger + final val Tile23 = InventoryTile(2,3) //canister ammo + final val Tile42 = InventoryTile(4,2) //medkit + final val Tile33 = InventoryTile(3,3) //ammo box, pistols, ace + final val Tile44 = InventoryTile(4,4) //large ammo box + final val Tile55 = InventoryTile(5,5) //bfr ammo box + final val Tile63 = InventoryTile(6,3) //rifles + final val Tile93 = InventoryTile(9,3) //long-body weapons + + def apply(w : Int, h : Int) : InventoryTile = { + new InventoryTile(w, h) + } +} diff --git a/common/src/main/scala/net/psforever/objects/terminals/OrderTerminalDefinition.scala b/common/src/main/scala/net/psforever/objects/terminals/OrderTerminalDefinition.scala new file mode 100644 index 000000000..ee431ff77 --- /dev/null +++ b/common/src/main/scala/net/psforever/objects/terminals/OrderTerminalDefinition.scala @@ -0,0 +1,138 @@ +// Copyright (c) 2017 PSForever +package net.psforever.objects.terminals + +import net.psforever.objects.InfantryLoadout.Simplification +import net.psforever.objects.{Player, Tool} +import net.psforever.objects.definition._ +import net.psforever.objects.equipment.Equipment +import net.psforever.objects.inventory.InventoryItem +import net.psforever.packet.game.ItemTransactionMessage + +import scala.annotation.switch + +class OrderTerminalDefinition extends TerminalDefinition(612) { + Name = "order_terminal" + + /** + * The `Equipment` available from this `Terminal` on specific pages. + */ + private val page0Stock : Map[String, ()=>Equipment] = infantryAmmunition ++ infantryWeapons + private val page2Stock : Map[String, ()=>Equipment] = supportAmmunition ++ supportWeapons + + /** + * Process a `TransactionType.Buy` action by the user. + * @param player the player + * @param msg the original packet carrying the request + * @return an actionable message that explains how to process the request; + * either you attempt to purchase equipment or attempt to switch directly to a different exo-suit + */ + def Buy(player : Player, msg : ItemTransactionMessage) : Terminal.Exchange = { + (msg.item_page : @switch) match { + case 0 => //Weapon tab + page0Stock.get(msg.item_name) match { + case Some(item) => + Terminal.BuyEquipment(item()) + case None => + Terminal.NoDeal() + } + case 2 => //Support tab + page2Stock.get(msg.item_name) match { + case Some(item) => + Terminal.BuyEquipment(item()) + case None => + Terminal.NoDeal() + } + case 3 => //Vehicle tab + vehicleAmmunition.get(msg.item_name) match { + case Some(item) => + Terminal.BuyEquipment(item()) + case None => + Terminal.NoDeal() + } + case 1 => //Armor tab + suits.get(msg.item_name) match { + case Some((suit, subtype)) => + Terminal.BuyExosuit(suit, subtype) + case None => + Terminal.NoDeal() + } + case _ => + Terminal.NoDeal() + } + } + + /** + * Process a `TransactionType.Sell` action by the user. + * There is no specific `order_terminal` tab associated with this action. + * Selling `Equipment` is always permitted. + * @param player the player + * @param msg the original packet carrying the request + * @return an actionable message that explains how to process the request + */ + def Sell(player : Player, msg : ItemTransactionMessage) : Terminal.Exchange = { + Terminal.SellEquipment() + } + + /** + * Process a `TransactionType.InfantryLoadout` action by the user. + * `InfantryLoadout` objects are blueprints composed of exo-suit specifications and simplified `Equipment`-to-slot mappings. + * If a valid loadout is found, its data is transformed back into actual `Equipment` for return to the user. + * @param player the player + * @param msg the original packet carrying the request + * @return an actionable message that explains how to process the request + */ + def InfantryLoadout(player : Player, msg : ItemTransactionMessage) : Terminal.Exchange = { + if(msg.item_page == 4) { //Favorites tab + player.LoadLoadout(msg.unk1) match { + case Some(loadout) => + val holsters = loadout.Holsters.map(entry => { InventoryItem(BuildSimplifiedPattern(entry.item), entry.index) }) + val inventory = loadout.Inventory.map(entry => { InventoryItem(BuildSimplifiedPattern(entry.item), entry.index) }) + Terminal.InfantryLoadout(loadout.ExoSuit, loadout.Subtype, holsters, inventory) + case None => + Terminal.NoDeal() + } + } + else { + Terminal.NoDeal() + } + } + + /** + * Accept a simplified blueprint for some piece of `Equipment` and create an actual piece of `Equipment` based on it. + * Used specifically for the reconstruction of `Equipment` via an `InfantryLoadout`. + * @param entry the simplified blueprint + * @return some `Equipment` object + * @see `TerminalDefinition.MakeTool`
+ * `TerminalDefinition.MakeAmmoBox`
+ * `TerminalDefinition.MakeSimpleItem`
+ * `TerminalDefinition.MakeConstructionItem`
+ * `TerminalDefinition.MakeKit` + */ + private def BuildSimplifiedPattern(entry : Simplification) : Equipment = { + import net.psforever.objects.InfantryLoadout._ + entry match { + case obj : ShorthandTool => + val ammo : List[AmmoBoxDefinition] = obj.ammo.map(fmode => { fmode.ammo.adef }) + val tool = Tool(obj.tdef) + //makes Tools where an ammo slot may have one of its alternate ammo types + (0 until tool.MaxAmmoSlot).foreach(index => { + val slot = tool.AmmoSlots(index) + slot.AmmoTypeIndex += obj.ammo(index).ammoIndex + slot.Box = MakeAmmoBox(ammo(index), Some(obj.ammo(index).ammo.capacity)) + }) + tool + + case obj : ShorthandAmmoBox => + MakeAmmoBox(obj.adef, Some(obj.capacity)) + + case obj : ShorthandConstructionItem => + MakeConstructionItem(obj.cdef) + + case obj : ShorthandSimpleItem => + MakeSimpleItem(obj.sdef) + + case obj : ShorthandKit => + MakeKit(obj.kdef) + } + } +} diff --git a/common/src/main/scala/net/psforever/objects/terminals/TemporaryTerminalMessages.scala b/common/src/main/scala/net/psforever/objects/terminals/TemporaryTerminalMessages.scala new file mode 100644 index 000000000..85ae33758 --- /dev/null +++ b/common/src/main/scala/net/psforever/objects/terminals/TemporaryTerminalMessages.scala @@ -0,0 +1,13 @@ +// Copyright (c) 2017 PSForever +package net.psforever.objects.terminals + +import net.psforever.types.PlanetSideEmpire + +//temporary location for these temporary messages +object TemporaryTerminalMessages { + //TODO send original packets along with these messages + final case class Convert(faction : PlanetSideEmpire.Value) + final case class Hacked(faction : Option[PlanetSideEmpire.Value]) + final case class Damaged(dm : Int) + final case class Repaired(rep : Int) +} diff --git a/common/src/main/scala/net/psforever/objects/terminals/Terminal.scala b/common/src/main/scala/net/psforever/objects/terminals/Terminal.scala new file mode 100644 index 000000000..7c19d1423 --- /dev/null +++ b/common/src/main/scala/net/psforever/objects/terminals/Terminal.scala @@ -0,0 +1,151 @@ +// Copyright (c) 2017 PSForever +package net.psforever.objects.terminals + +import akka.actor.{ActorContext, ActorRef, Props} +import net.psforever.objects.{PlanetSideGameObject, Player} +import net.psforever.objects.equipment.Equipment +import net.psforever.objects.inventory.InventoryItem +import net.psforever.packet.game.ItemTransactionMessage +import net.psforever.types.{ExoSuitType, PlanetSideEmpire, TransactionType} + +/** + * na + * @param tdef the `ObjectDefinition` that constructs this object and maintains some of its immutable fields + */ +class Terminal(tdef : TerminalDefinition) extends PlanetSideGameObject { + /** Internal reference to the `Actor` for this `Terminal`, sets up by this `Terminal`. */ + private var actor = ActorRef.noSender + + /** + * Get access to the internal `TerminalControl` `Actor` for this `Terminal`. + * If called for the first time, create the said `Actor`. + * Must be called only after the globally unique identifier has been set. + * @param context the `ActorContext` under which this `Terminal`'s `Actor` will be created + * @return the `Terminal`'s `Actor` + */ + def Actor(implicit context : ActorContext) : ActorRef = { + if(actor == ActorRef.noSender) { + actor = context.actorOf(Props(classOf[TerminalControl], this), s"${tdef.Name}_${GUID.guid}") + } + actor + } + + //the following fields and related methods are neither finalized no integrated; GOTO Request + private var faction : PlanetSideEmpire.Value = PlanetSideEmpire.NEUTRAL + private var hackedBy : Option[PlanetSideEmpire.Value] = None + private var health : Int = 100 //TODO not real health value + + def Faction : PlanetSideEmpire.Value = faction + + def HackedBy : Option[PlanetSideEmpire.Value] = hackedBy + + def Health : Int = health + + def Convert(toFaction : PlanetSideEmpire.Value) : Unit = { + hackedBy = None + faction = toFaction + } + + def HackedBy(toFaction : Option[PlanetSideEmpire.Value]) : Unit = { + hackedBy = if(toFaction.contains(faction)) { None } else { toFaction } + } + + def Damaged(dam : Int) : Unit = { + health = Math.max(0, Health - dam) + } + + def Repair(rep : Int) : Unit = { + health = Math.min(Health + rep, 100) + } + + /** + * Process some `TransactionType` action requested by the user. + * @param player the player + * @param msg the original packet carrying the request + * @return an actionable message that explains what resulted from interacting with this `Terminal` + */ + def Request(player : Player, msg : ItemTransactionMessage) : Terminal.Exchange = { + msg.transaction_type match { + case TransactionType.Buy => + tdef.Buy(player, msg) + + case TransactionType.Sell => + tdef.Sell(player, msg) + + case TransactionType.InfantryLoadout => + tdef.InfantryLoadout(player, msg) + + case _ => + Terminal.NoDeal() + } + } + + def Definition : TerminalDefinition = tdef +} + +object Terminal { + /** + * Entry message into this `Terminal` that carries the request. + * Accessing an option in a `Terminal` normally always results in this message. + * @param player the player who sent this request message + * @param msg the original packet carrying the request + */ + final case class Request(player : Player, msg : ItemTransactionMessage) + + /** + * A basic `Trait` connecting all of the actionable `Terminal` response messages. + */ + sealed trait Exchange + + /** + * Message that carries the result of the processed request message back to the original user (`player`). + * @param player the player who sent this request message + * @param msg the original packet carrying the request + * @param response the result of the processed request + */ + final case class TerminalMessage(player : Player, msg : ItemTransactionMessage, response : Exchange) + + /** + * No action will result from interacting with this `Terminal`. + * A result of a processed request. + */ + final case class NoDeal() extends Exchange + /** + * The `Player` exo-suit will be changed to the prescribed one. + * The subtype will be important if the user is swapping to an `ExoSuitType.MAX` exo-suit. + * A result of a processed request. + * @param exosuit the type of exo-suit + * @param subtype the exo-suit subtype, if any + */ + final case class BuyExosuit(exosuit : ExoSuitType.Value, subtype : Int = 0) extends Exchange + /** + * A single piece of `Equipment` has been selected and will be given to the `Player`. + * The `Player` must decide what to do with it once it is in their control. + * A result of a processed request. + * @param item the `Equipment` being given to the player + */ + final case class BuyEquipment(item : Equipment) extends Exchange + /** + * A roundabout message oft-times. + * Most `Terminals` should always allow `Player`s to dispose of some piece of `Equipment`. + * A result of a processed request. + */ + //TODO if there are exceptions, find them + final case class SellEquipment() extends Exchange + /** + * Recover a former exo-suit and `Equipment` configuration that the `Player` possessed. + * A result of a processed request. + * @param exosuit the type of exo-suit + * @param subtype the exo-suit subtype, if any + * @param holsters the contents of the `Player`'s holsters + * @param inventory the contents of the `Player`'s inventory + */ + final case class InfantryLoadout(exosuit : ExoSuitType.Value, subtype : Int = 0, holsters : List[InventoryItem], inventory : List[InventoryItem]) extends Exchange + + import net.psforever.packet.game.PlanetSideGUID + def apply(guid : PlanetSideGUID, tdef : TerminalDefinition) : Terminal = { + val obj = new Terminal(tdef) + obj.GUID = guid + obj + } +} diff --git a/common/src/main/scala/net/psforever/objects/terminals/TerminalControl.scala b/common/src/main/scala/net/psforever/objects/terminals/TerminalControl.scala new file mode 100644 index 000000000..7e9a71cfb --- /dev/null +++ b/common/src/main/scala/net/psforever/objects/terminals/TerminalControl.scala @@ -0,0 +1,34 @@ +// Copyright (c) 2017 PSForever +package net.psforever.objects.terminals + +import akka.actor.Actor + +/** + * An `Actor` that handles messages being dispatched to a specific `Terminal`.
+ *
+ * For now, the only important message being managed is `Terminal.Request`. + * @param term the `Terminal` object being governed + */ +class TerminalControl(term : Terminal) extends Actor { + def receive : Receive = { + case Terminal.Request(player, msg) => + sender ! Terminal.TerminalMessage(player, msg, term.Request(player, msg)) + + case TemporaryTerminalMessages.Convert(fact) => + term.Convert(fact) + + case TemporaryTerminalMessages.Hacked(fact) => + term.HackedBy(fact) + + case TemporaryTerminalMessages.Damaged(dam) => + term.Damaged(dam) + + case TemporaryTerminalMessages.Repaired(rep) => + term.Repair(rep) + + case _ => + sender ! Terminal.NoDeal() + } + + override def toString : String = term.Definition.Name +} diff --git a/common/src/main/scala/net/psforever/objects/terminals/TerminalDefinition.scala b/common/src/main/scala/net/psforever/objects/terminals/TerminalDefinition.scala new file mode 100644 index 000000000..a82fb739d --- /dev/null +++ b/common/src/main/scala/net/psforever/objects/terminals/TerminalDefinition.scala @@ -0,0 +1,264 @@ +// Copyright (c) 2017 PSForever +package net.psforever.objects.terminals + +import net.psforever.objects._ +import net.psforever.objects.definition._ +import net.psforever.objects.equipment.Equipment +import net.psforever.packet.game.ItemTransactionMessage +import net.psforever.types.ExoSuitType + +import scala.collection.immutable.HashMap + +/** + * The definition for any `Terminal`. + * @param objectId the object's identifier number + */ +abstract class TerminalDefinition(objectId : Int) extends ObjectDefinition(objectId) { + Name = "terminal" + + /** + * The unimplemented functionality for this `Terminal`'s `TransactionType.Buy` activity. + */ + def Buy(player : Player, msg : ItemTransactionMessage) : Terminal.Exchange + + /** + * The unimplemented functionality for this `Terminal`'s `TransactionType.Sell` activity. + */ + def Sell(player: Player, msg : ItemTransactionMessage) : Terminal.Exchange + + /** + * The unimplemented functionality for this `Terminal`'s `TransactionType.InfantryLoadout` activity. + */ + def InfantryLoadout(player : Player, msg : ItemTransactionMessage) : Terminal.Exchange + + /** + * A `Map` of information for changing exo-suits. + * key - an identification string sent by the client + * value - a `Tuple` containing exo-suit specifications + */ + protected val suits : Map[String, (ExoSuitType.Value, Int)] = Map( + "standard_issue_armor" -> (ExoSuitType.Standard, 0), + "lite_armor" -> (ExoSuitType.Agile, 0), + "med_armor" -> (ExoSuitType.Reinforced, 0) + //TODO max and infiltration suit + ) + + import net.psforever.objects.GlobalDefinitions._ + /** + * A `Map` of operations for producing the `AmmoBox` `Equipment` for infantry-held weaponry. + * key - an identification string sent by the client + * value - a curried function that builds the object + */ + protected val infantryAmmunition : HashMap[String, ()=>Equipment] = HashMap( + "9mmbullet" -> MakeAmmoBox(bullet_9mm), + "9mmbullet_AP" -> MakeAmmoBox(bullet_9mm_AP), + "shotgun_shell" -> MakeAmmoBox(shotgun_shell), + "shotgun_shell_AP" -> MakeAmmoBox(shotgun_shell_AP), + "energy_cell" -> MakeAmmoBox(energy_cell), + "anniversary_ammo" -> MakeAmmoBox(anniversary_ammo), //10mm multi-phase + "rocket" -> MakeAmmoBox(rocket), + "frag_cartridge" -> MakeAmmoBox(frag_cartridge), + "jammer_cartridge" -> MakeAmmoBox(jammer_cartridge), + "plasma_cartridge" -> MakeAmmoBox(plasma_cartridge), + "ancient_ammo_combo" -> MakeAmmoBox(ancient_ammo_combo), + "maelstrom_ammo" -> MakeAmmoBox(maelstrom_ammo), + "striker_missile_ammo" -> MakeAmmoBox(striker_missile_ammo), + "hunter_seeker_missile" -> MakeAmmoBox(hunter_seeker_missile), //phoenix missile + "lancer_cartridge" -> MakeAmmoBox(lancer_cartridge), + "bolt" -> MakeAmmoBox(bolt), + "oicw_ammo" -> MakeAmmoBox(oicw_ammo), //scorpion missile + "flamethrower_ammo" -> MakeAmmoBox(flamethrower_ammo) + ) + + /** + * A `Map` of operations for producing the `AmmoBox` `Equipment` for infantry-held utilities. + * key - an identification string sent by the client + * value - a curried function that builds the object + */ + protected val supportAmmunition : HashMap[String, ()=>Equipment] = HashMap( + "health_canister" -> MakeAmmoBox(health_canister), + "armor_canister" -> MakeAmmoBox(armor_canister), + "upgrade_canister" -> MakeAmmoBox(upgrade_canister) + ) + + /** + * A `Map` of operations for producing the `AmmoBox` `Equipment` for vehicle-mounted weaponry. + * key - an identification string sent by the client + * value - a curried function that builds the object + */ + protected val vehicleAmmunition : HashMap[String, ()=>Equipment] = HashMap( + "35mmbullet" -> MakeAmmoBox(bullet_35mm), + "hellfire_ammo" -> MakeAmmoBox(hellfire_ammo), + "liberator_bomb" -> MakeAmmoBox(liberator_bomb), + "25mmbullet" -> MakeAmmoBox(bullet_25mm), + "75mmbullet" -> MakeAmmoBox(bullet_75mm), + "heavy_grenade_mortar" -> MakeAmmoBox(heavy_grenade_mortar), + "reaver_rocket" -> MakeAmmoBox(reaver_rocket), + "20mmbullet" -> MakeAmmoBox(bullet_20mm), + "12mmbullet" -> MakeAmmoBox(bullet_12mm), + "wasp_rocket_ammo" -> MakeAmmoBox(wasp_rocket_ammo), + "wasp_gun_ammo" -> MakeAmmoBox(wasp_gun_ammo), + "aphelion_laser_ammo" -> MakeAmmoBox(aphelion_laser_ammo), + "aphelion_immolation_cannon_ammo" -> MakeAmmoBox(aphelion_immolation_cannon_ammo), + "aphelion_plasma_rocket_ammo" -> MakeAmmoBox(aphelion_plasma_rocket_ammo), + "aphelion_ppa_ammo" -> MakeAmmoBox(aphelion_ppa_ammo), + "aphelion_starfire_ammo" -> MakeAmmoBox(aphelion_starfire_ammo), + "skyguard_flak_cannon_ammo" -> MakeAmmoBox(skyguard_flak_cannon_ammo), + "flux_cannon_thresher_battery" -> MakeAmmoBox(flux_cannon_thresher_battery), + "fluxpod_ammo" -> MakeAmmoBox(fluxpod_ammo), + "pulse_battery" -> MakeAmmoBox(pulse_battery), + "heavy_rail_beam_battery" -> MakeAmmoBox(heavy_rail_beam_battery), + "15mmbullet" -> MakeAmmoBox(bullet_15mm), + "colossus_100mm_cannon_ammo" -> MakeAmmoBox(colossus_100mm_cannon_ammo), + "colossus_burster_ammo" -> MakeAmmoBox(colossus_burster_ammo), + "colossus_cluster_bomb_ammo" -> MakeAmmoBox(colossus_cluster_bomb_ammo), + "colossus_chaingun_ammo" -> MakeAmmoBox(colossus_chaingun_ammo), + "colossus_tank_cannon_ammo" -> MakeAmmoBox(colossus_tank_cannon_ammo), + "105mmbullet" -> MakeAmmoBox(bullet_105mm), + "gauss_cannon_ammo" -> MakeAmmoBox(gauss_cannon_ammo), + "peregrine_dual_machine_gun_ammo" -> MakeAmmoBox(peregrine_dual_machine_gun_ammo), + "peregrine_mechhammer_ammo" -> MakeAmmoBox(peregrine_mechhammer_ammo), + "peregrine_particle_cannon_ammo" -> MakeAmmoBox(peregrine_particle_cannon_ammo), + "peregrine_rocket_pod_ammo" -> MakeAmmoBox(peregrine_rocket_pod_ammo), + "peregrine_sparrow_ammo" -> MakeAmmoBox(peregrine_sparrow_ammo), + "150mmbullet" -> MakeAmmoBox(bullet_150mm) + ) + + /** + * A `Map` of operations for producing the `Tool` `Equipment` for infantry weapons. + * key - an identification string sent by the client + * value - a curried function that builds the object + */ + protected val infantryWeapons : HashMap[String, ()=>Equipment] = HashMap( + "ilc9" -> MakeTool(ilc9, bullet_9mm), + "repeater" -> MakeTool(repeater, bullet_9mm), + "isp" -> MakeTool(isp, shotgun_shell), //amp + "beamer" -> MakeTool(beamer, energy_cell), + "suppressor" -> MakeTool(suppressor, bullet_9mm), + "anniversary_guna" -> MakeTool(anniversary_guna, anniversary_ammo), //tr stinger + "anniversary_gun" -> MakeTool(anniversary_gun, anniversary_ammo), //nc spear + "anniversary_gunb" -> MakeTool(anniversary_gunb, anniversary_ammo), //vs eraser + "cycler" -> MakeTool(cycler, bullet_9mm), + "gauss" -> MakeTool(gauss, bullet_9mm), + "pulsar" -> MakeTool(pulsar, energy_cell), + "punisher" -> MakeTool(punisher, List(bullet_9mm, rocket)), + "flechette" -> MakeTool(flechette, shotgun_shell), + "spiker" -> MakeTool(spiker, ancient_ammo_combo), + "frag_grenade" -> MakeTool(frag_grenade, frag_grenade_ammo), + "jammer_grenade" -> MakeTool(jammer_grenade, jammer_grenade_ammo), + "plasma_grenade" -> MakeTool(plasma_grenade, plasma_grenade_ammo), + "katana" -> MakeTool(katana, melee_ammo), + "chainblade" -> MakeTool(chainblade, melee_ammo), + "magcutter" -> MakeTool(magcutter, melee_ammo), + "forceblade" -> MakeTool(forceblade, melee_ammo), + "mini_chaingun" -> MakeTool(mini_chaingun, bullet_9mm), + "r_shotgun" -> MakeTool(r_shotgun, shotgun_shell), //jackhammer + "lasher" -> MakeTool(lasher, energy_cell), + "maelstrom" -> MakeTool(maelstrom, maelstrom_ammo), + "striker" -> MakeTool(striker, striker_missile_ammo), + "hunterseeker" -> MakeTool(hunterseeker, hunter_seeker_missile), //phoenix + "lancer" -> MakeTool(lancer, lancer_cartridge), + "phoenix" -> MakeTool(phoenix, phoenix_missile), //decimator + "rocklet" -> MakeTool(rocklet, rocket), + "thumper" -> MakeTool(thumper, frag_cartridge), + "radiator" -> MakeTool(radiator, ancient_ammo_combo), + "heavy_sniper" -> MakeTool(heavy_sniper, bolt), //hsr + "bolt_driver" -> MakeTool(bolt_driver, bolt), + "oicw" -> MakeTool(oicw, oicw_ammo), //scorpion + "flamethrower" -> MakeTool(flamethrower, flamethrower_ammo) + ) + + /** + * A `Map` of operations for producing the `Tool` `Equipment` for utilities. + * key - an identification string sent by the client + * value - a curried function that builds the object + */ + protected val supportWeapons : HashMap[String, ()=>Equipment] = HashMap( + "medkit" -> MakeKit(medkit), + "super_medkit" -> MakeKit(super_medkit), + "super_armorkit" -> MakeKit(super_armorkit), + "super_staminakit" -> MakeKit(super_staminakit), + "medicalapplicator" -> MakeTool(medicalapplicator, health_canister), + "bank" -> MakeTool(bank, armor_canister), + "nano_dispenser" -> MakeTool(nano_dispenser, armor_canister), + //TODO "ace" -> MakeConstructionItem(ace), + //TODO "advanced_ace" -> MakeConstructionItem(advanced_ace), + "remote_electronics_kit" -> MakeSimpleItem(remote_electronics_kit), + "trek" -> MakeTool(trek, trek_ammo), + "command_detonater" -> MakeSimpleItem(command_detonater), + "flail_targeting_laser" -> MakeSimpleItem(flail_targeting_laser) + ) + + /** + * Create a new `Tool` from provided `EquipmentDefinition` objects. + * @param tdef the `ToolDefinition` objects + * @param adef an `AmmoBoxDefinition` object + * @return a partial function that, when called, creates the piece of `Equipment` + */ + protected def MakeTool(tdef : ToolDefinition, adef : AmmoBoxDefinition)() : Tool = MakeTool(tdef, List(adef)) + + /** + * Create a new `Tool` from provided `EquipmentDefinition` objects. + * Only use this function to create default `Tools` with the default parameters. + * For example, loadouts can retain `Tool` information that utilizes alternate, valid ammunition types; + * and, this method function will not construct a complete object if provided that information. + * @param tdef the `ToolDefinition` objects + * @param adefs a `List` of `AmmoBoxDefinition` objects + * @return a curried function that, when called, creates the piece of `Equipment` + * @see `GlobalDefinitions` + * @see `OrderTerminalDefinition.BuildSimplifiedPattern` + */ + protected def MakeTool(tdef : ToolDefinition, adefs : List[AmmoBoxDefinition])() : Tool = { + val obj = Tool(tdef) + (0 until obj.MaxAmmoSlot).foreach(index => { + val aType = adefs(index) + val ammo = MakeAmmoBox(aType, Some(obj.Definition.FireModes(index).Magazine)) //make internal magazine, full + (obj.AmmoSlots(index).Box = ammo) match { + case Some(_) => ; //this means it worked + case None => + org.log4s.getLogger("TerminalDefinition").warn(s"plans do not match definition: trying to feed ${ammo.AmmoType} ammunition into Tool (${obj.Definition.ObjectId} @ $index)") + } + }) + obj + } + + /** + * Create a new `AmmoBox` from provided `EquipmentDefinition` objects. + * @param adef the `AmmoBoxDefinition` object + * @param capacity optional number of rounds in this `AmmoBox`, deviating from the `EquipmentDefinition`; + * necessary for constructing the magazine (`AmmoSlot`) of `Tool`s + * @return a curried function that, when called, creates the piece of `Equipment` + * @see `GlobalDefinitions` + */ + protected def MakeAmmoBox(adef : AmmoBoxDefinition, capacity : Option[Int] = None)() : AmmoBox = { + val obj = AmmoBox(adef) + if(capacity.isDefined) { + obj.Capacity = capacity.get + } + obj + } + + /** + * Create a new `Kit` from provided `EquipmentDefinition` objects. + * @param kdef the `KitDefinition` object + * @return a curried function that, when called, creates the piece of `Equipment` + * @see `GlobalDefinitions` + */ + protected def MakeKit(kdef : KitDefinition)() : Kit = Kit(kdef) + + /** + * Create a new `SimpleItem` from provided `EquipmentDefinition` objects. + * @param sdef the `SimpleItemDefinition` object + * @return a curried function that, when called, creates the piece of `Equipment` + * @see `GlobalDefinitions` + */ + protected def MakeSimpleItem(sdef : SimpleItemDefinition)() : SimpleItem = SimpleItem(sdef) + + /** + * Create a new `ConstructionItem` from provided `EquipmentDefinition` objects. + * @param cdef the `ConstructionItemDefinition` object + * @return a curried function that, when called, creates the piece of `Equipment` + * @see `GlobalDefinitions` + */ + protected def MakeConstructionItem(cdef : ConstructionItemDefinition)() : ConstructionItem = ConstructionItem(cdef) +} diff --git a/common/src/main/scala/net/psforever/objects/vehicles/ANTResourceUtility.scala b/common/src/main/scala/net/psforever/objects/vehicles/ANTResourceUtility.scala new file mode 100644 index 000000000..b7a32caa1 --- /dev/null +++ b/common/src/main/scala/net/psforever/objects/vehicles/ANTResourceUtility.scala @@ -0,0 +1,31 @@ +// Copyright (c) 2017 PSForever +package net.psforever.objects.vehicles + +import net.psforever.objects.Vehicle + +/** + * A `Utility` designed to simulate the NTU-distributive functions of an ANT. + * @param objectId the object id that is associated with this sort of `Utility` + * @param vehicle the `Vehicle` to which this `Utility` is attached + */ +class ANTResourceUtility(objectId : Int, vehicle : Vehicle) extends Utility(objectId, vehicle) { + private var currentNTU : Int = 0 + + def NTU : Int = currentNTU + + def NTU_=(ntu : Int) : Int = { + currentNTU = ntu + currentNTU = math.max(math.min(currentNTU, MaxNTU), 0) + NTU + } + + def MaxNTU : Int = ANTResourceUtility.MaxNTU +} + +object ANTResourceUtility { + private val MaxNTU : Int = 300 //TODO what should this value be? + + def apply(objectId : Int, vehicle : Vehicle) : ANTResourceUtility = { + new ANTResourceUtility(objectId, vehicle) + } +} \ No newline at end of file diff --git a/common/src/main/scala/net/psforever/objects/vehicles/Seat.scala b/common/src/main/scala/net/psforever/objects/vehicles/Seat.scala new file mode 100644 index 000000000..263fa2e22 --- /dev/null +++ b/common/src/main/scala/net/psforever/objects/vehicles/Seat.scala @@ -0,0 +1,134 @@ +// Copyright (c) 2017 PSForever +package net.psforever.objects.vehicles + +import net.psforever.objects.definition.SeatDefinition +import net.psforever.objects.{Player, Vehicle} +import net.psforever.packet.game.PlanetSideGUID +import net.psforever.types.PlanetSideEmpire + +/** + * Server-side support for a slot that infantry players can occupy, ostensibly called a "seat" and treated like a "seat." + * (Players can sit in it.) + * @param seatDef the Definition that constructs this item and maintains some of its immutable fields + * @param vehicle the vehicle where this seat is installed + */ +class Seat(private val seatDef : SeatDefinition, private val vehicle : Vehicle) { + private var occupant : Option[PlanetSideGUID] = None + private var lockState : VehicleLockState.Value = VehicleLockState.Empire + + /** + * The faction association of this `Seat` is tied directly to the connected `Vehicle`. + * @return the faction association + */ + def Faction : PlanetSideEmpire.Value = { + vehicle.Faction + } + + /** + * Is this seat occupied? + * @return the GUID of the player sitting in this seat, or `None` if it is left vacant + */ + def Occupant : Option[PlanetSideGUID] = { + this.occupant + } + + /** + * The player is trying to sit down. + * Seats are exclusive positions that can only hold one occupant at a time. + * @param player the player who wants to sit, or `None` if the occupant is getting up + * @return the GUID of the player sitting in this seat, or `None` if it is left vacant + */ + def Occupant_=(player : Option[Player]) : Option[PlanetSideGUID] = { + if(player.isDefined) { + if(this.occupant.isEmpty) { + this.occupant = Some(player.get.GUID) + } + } + else { + this.occupant = None + } + this.occupant + } + + /** + * Is this seat occupied? + * @return `true`, if it is occupied; `false`, otherwise + */ + def isOccupied : Boolean = { + this.occupant.isDefined + } + + def SeatLockState : VehicleLockState.Value = { + this.lockState + } + + def SeatLockState_=(lockState : VehicleLockState.Value) : VehicleLockState.Value = { + this.lockState = lockState + SeatLockState + } + + def ArmorRestriction : SeatArmorRestriction.Value = { + seatDef.ArmorRestriction + } + + def Bailable : Boolean = { + seatDef.Bailable + } + + def ControlledWeapon : Option[Int] = { + seatDef.ControlledWeapon + } + + /** + * Given a player, can they access this `Seat` under its current restrictions and permissions. + * @param player the player who wants to sit + * @return `true` if the player can sit down in this `Seat`; `false`, otherwise + */ + def CanUseSeat(player : Player) : Boolean = { + var access : Boolean = false + val owner : Option[PlanetSideGUID] = vehicle.Owner + lockState match { + case VehicleLockState.Locked => + access = owner.isEmpty || (owner.isDefined && player.GUID == owner.get) + case VehicleLockState.Group => + access = Faction == player.Faction //TODO this is not correct + case VehicleLockState.Empire => + access = Faction == player.Faction + } + access + } + + /** + * Override the string representation to provide additional information. + * @return the string output + */ + override def toString : String = { + Seat.toString(this) + } +} + +object Seat { + /** + * Overloaded constructor. + * @param vehicle the vehicle where this seat is installed + * @return a `Seat` object + */ + def apply(seatDef : SeatDefinition, vehicle : Vehicle) : Seat = { + new Seat(seatDef, vehicle) + } + + /** + * Provide a fixed string representation. + * @return the string output + */ + def toString(obj : Seat) : String = { + val weaponStr = if(obj.ControlledWeapon.isDefined) { " (gunner)" } else { "" } + val seatStr = if(obj.isOccupied) { + "occupied by %d".format(obj.Occupant.get.guid) + } + else { + "unoccupied" + } + s"{Seat$weaponStr: $seatStr}" + } +} diff --git a/common/src/main/scala/net/psforever/objects/vehicles/SeatArmorRestriction.scala b/common/src/main/scala/net/psforever/objects/vehicles/SeatArmorRestriction.scala new file mode 100644 index 000000000..391b3d86e --- /dev/null +++ b/common/src/main/scala/net/psforever/objects/vehicles/SeatArmorRestriction.scala @@ -0,0 +1,18 @@ +// Copyright (c) 2017 PSForever +package net.psforever.objects.vehicles + +/** + * An `Enumeration` of exo-suit-based seat access restrictions.
+ *
+ * The default value is `NoMax` as that is the most common seat. + * `NoReinforcedOrMax` is next most common. + * `MaxOnly` is a rare seat restriction found in pairs on Galaxies and on the large "Ground Transport" vehicles. + */ +object SeatArmorRestriction extends Enumeration { + type Type = Value + + val MaxOnly, + NoMax, + NoReinforcedOrMax + = Value +} diff --git a/common/src/main/scala/net/psforever/objects/vehicles/Utility.scala b/common/src/main/scala/net/psforever/objects/vehicles/Utility.scala new file mode 100644 index 000000000..172d7ed1e --- /dev/null +++ b/common/src/main/scala/net/psforever/objects/vehicles/Utility.scala @@ -0,0 +1,104 @@ +// Copyright (c) 2017 PSForever +package net.psforever.objects.vehicles + +import net.psforever.objects.entity.IdentifiableEntity +import net.psforever.objects.Vehicle +import net.psforever.packet.game.PlanetSideGUID +import net.psforever.types.PlanetSideEmpire + +import scala.annotation.switch + +/** + * A `Utility` represents an unknown but functional entity that is attached to a `Vehicle` and is not a weapon or a seat. + * This is a placeholder solution until a better system is established. + * @param objectId the object id that is associated with this sort of `Utility` + * @param vehicle the `Vehicle` to which this `Utility` is attached + */ +class Utility(val objectId : Int, vehicle : Vehicle) extends IdentifiableEntity { + private var active : Boolean = false + + /** + * The faction association of this `Utility` is tied directly to the connected `Vehicle`. + * @return the faction association + */ + def Faction : PlanetSideEmpire.Value = { + vehicle.Faction + } + + /** + * An "active" `Utility` can be used by players; an "inactive" one can not be used in its current state. + * @return whether this `Utility` is active. + */ + def ActiveState : Boolean = { + this.active + } + + /** + * Change the "active" state of this `Utility`. + * @param state the new active state + * @return the current active state after being changed + */ + def ActiveState_=(state : Boolean) : Boolean = { + this.active = state + state + } + + /** + * Override the string representation to provide additional information. + * @return the string output + */ + override def toString : String = { + Utility.toString(this) + } +} + +object Utility { + /** + * An overloaded constructor. + * @param objectId the object id the is associated with this sort of `Utility` + * @param vehicle the `Vehicle` to which this `Utility` is attached + * @return a `Utility` object + */ + def apply(objectId : Int, vehicle : Vehicle) : Utility = { + new Utility(objectId, vehicle) + } + + /** + * An overloaded constructor. + * @param objectId the object id the is associated with this sort of `Utility` + * @param vehicle the `Vehicle` to which this `Utility` is attached + * @return a `Utility` object + */ + def apply(guid : PlanetSideGUID, objectId : Int, vehicle : Vehicle) : Utility = { + val obj = new Utility(objectId, vehicle) + obj.GUID = guid + obj + } + + /** + * Create one of a specific type of utilities. + * @param objectId the object id that is associated with this sort of `Utility` + * @param vehicle the `Vehicle` to which this `Utility` is attached + * @return a permitted `Utility` object + */ + def Select(objectId : Int, vehicle : Vehicle) : Utility = { + (objectId : @switch) match { + case 60 => //this is the object id of an ANT + ANTResourceUtility(objectId, vehicle) + + case 49 | 519 | 613 | 614 => //ams parts + Utility(objectId, vehicle) + + case _ => + throw new IllegalArgumentException(s"the requested objectID #$objectId is not accepted as a valid Utility") + } + } + + /** + * Provide a fixed string representation. + * @return the string output + */ + def toString(obj : Utility) : String = { + s"{utility-${obj.objectId}}" + } +} diff --git a/common/src/main/scala/net/psforever/objects/vehicles/VehicleLockState.scala b/common/src/main/scala/net/psforever/objects/vehicles/VehicleLockState.scala new file mode 100644 index 000000000..661567019 --- /dev/null +++ b/common/src/main/scala/net/psforever/objects/vehicles/VehicleLockState.scala @@ -0,0 +1,14 @@ +// Copyright (c) 2017 PSForever +package net.psforever.objects.vehicles + +/** + * An `Enumeration` of various access states for vehicle components, such as the seats and the trunk. + */ +object VehicleLockState extends Enumeration { + type Type = Value + + val Empire, //owner's whole faction + Group, //owner's squad/platoon only + Locked //owner only + = Value +} diff --git a/common/src/main/scala/net/psforever/packet/GamePacketOpcode.scala b/common/src/main/scala/net/psforever/packet/GamePacketOpcode.scala index a502732a5..02b1db55a 100644 --- a/common/src/main/scala/net/psforever/packet/GamePacketOpcode.scala +++ b/common/src/main/scala/net/psforever/packet/GamePacketOpcode.scala @@ -429,7 +429,7 @@ object GamePacketOpcode extends Enumeration { case 0x5b => noDecoder(OrbitalShuttleTimeMsg) case 0x5c => noDecoder(AIDamage) case 0x5d => game.DeployObjectMessage.decode - case 0x5e => noDecoder(FavoritesRequest) + case 0x5e => game.FavoritesRequest.decode case 0x5f => noDecoder(FavoritesResponse) // OPCODES 0x60-6f diff --git a/common/src/main/scala/net/psforever/packet/game/AvatarImplantMessage.scala b/common/src/main/scala/net/psforever/packet/game/AvatarImplantMessage.scala index b87428fb2..424a14daf 100644 --- a/common/src/main/scala/net/psforever/packet/game/AvatarImplantMessage.scala +++ b/common/src/main/scala/net/psforever/packet/game/AvatarImplantMessage.scala @@ -1,49 +1,16 @@ // Copyright (c) 2017 PSForever package net.psforever.packet.game -import net.psforever.packet.{GamePacketOpcode, Marshallable, PacketHelpers, PlanetSideGamePacket} +import net.psforever.packet.{GamePacketOpcode, Marshallable, PlanetSideGamePacket} +import net.psforever.types.ImplantType import scodec.Codec import scodec.codecs._ -/** - * An `Enumeration` of the available implants. - */ -object ImplantType extends Enumeration { - type Type = Value - val AdvancedRegen, - Targeting, - AudioAmplifier, - DarklightVision, - MeleeBooster, - PersonalShield, - RangeMagnifier, - Unknown7, - SilentRun, - Surge = Value - - implicit val codec = PacketHelpers.createEnumerationCodec(this, uint4L) -} - /** * Change the state of the implant.
- * Write better comments. *
- * Implant:
- * ` - * 00 - Regeneration (advanced_regen)
- * 01 - Enhanced Targeting (targeting)
- * 02 - Audio Amplifier (audio_amplifier)
- * 03 - Darklight Vision (darklight_vision)
- * 04 - Melee Booster (melee_booster)
- * 05 - Personal Shield (personal_shield)
- * 06 - Range Magnifier (range_magnifier)
- * 07 - `None`
- * 08 - Sensor Shield (silent_run)
- * 09 - Surge (surge)
- * ` - *
- * Exploration
- * Where is Second Wind (second_wind)? + * The implant Second Wind is technically an invalid `ImplantType` for this packet. + * This owes to the unique activation trigger for that implant - a near-death experience of ~0HP. * @param player_guid the player * @param unk1 na * @param unk2 na diff --git a/common/src/main/scala/net/psforever/packet/game/BeginZoningMessage.scala b/common/src/main/scala/net/psforever/packet/game/BeginZoningMessage.scala index bcdf964c8..f4cc6df2e 100644 --- a/common/src/main/scala/net/psforever/packet/game/BeginZoningMessage.scala +++ b/common/src/main/scala/net/psforever/packet/game/BeginZoningMessage.scala @@ -5,7 +5,8 @@ import net.psforever.packet.{GamePacketOpcode, Marshallable, PacketHelpers, Plan import scodec.Codec /** - * Dispatched by the client after the current map has been fully loaded locally and its objects are ready to be initialized.
+ * Dispatched by the client after the current map has been fully loaded locally and its objects are ready to be initialized. + * This packet is a direct response to `LoadMapMessage`.
*
* When the server receives the packet, for each object on that map, it sends the packets to the client:
* - `SetEmpireMessage`
diff --git a/common/src/main/scala/net/psforever/packet/game/FavoritesRequest.scala b/common/src/main/scala/net/psforever/packet/game/FavoritesRequest.scala new file mode 100644 index 000000000..3d9af5689 --- /dev/null +++ b/common/src/main/scala/net/psforever/packet/game/FavoritesRequest.scala @@ -0,0 +1,38 @@ +// Copyright (c) 2017 PSForever +package net.psforever.packet.game + +import net.psforever.packet.{GamePacketOpcode, Marshallable, PacketHelpers, PlanetSideGamePacket} +import scodec.Codec +import scodec.codecs._ + +object FavoritesAction extends Enumeration { + type Type = Value + + val Unknown, + Save, + Delete = Value + + implicit val codec = PacketHelpers.createEnumerationCodec(this, uint2L) +} + +final case class FavoritesRequest(player_guid : PlanetSideGUID, + unk : Int, + action : FavoritesAction.Value, + line : Int, + label : Option[String]) + extends PlanetSideGamePacket { + type Packet = FavoritesRequest + def opcode = GamePacketOpcode.FavoritesRequest + def encode = FavoritesRequest.encode(this) +} + +object FavoritesRequest extends Marshallable[FavoritesRequest] { + implicit val codec : Codec[FavoritesRequest] = ( + ("player_guid" | PlanetSideGUID.codec) :: + ("unk" | uint2L) :: + (("action" | FavoritesAction.codec) >>:~ { action => + ("line" | uint4L) :: + conditional(action == FavoritesAction.Save, "label" | PacketHelpers.encodedWideString) + }) + ).as[FavoritesRequest] +} diff --git a/common/src/main/scala/net/psforever/packet/game/PlanetsideAttributeMessage.scala b/common/src/main/scala/net/psforever/packet/game/PlanetsideAttributeMessage.scala index f2faa5aad..125e521de 100644 --- a/common/src/main/scala/net/psforever/packet/game/PlanetsideAttributeMessage.scala +++ b/common/src/main/scala/net/psforever/packet/game/PlanetsideAttributeMessage.scala @@ -48,7 +48,7 @@ import scodec.codecs._ * 25 : BFR Anti Infantry
* 26 : ?! Removed Cert ?
* 27 : ?! Removed Cert ?
- * 28 : Reinforced ExoSuit
+ * 28 : Reinforced ExoSuitDefinition
* 29 : Infiltration Suit
* 30 : MAX (Burster)
* 31 : MAX (Dual-Cycler)
diff --git a/common/src/main/scala/net/psforever/packet/game/objectcreate/InventoryData.scala b/common/src/main/scala/net/psforever/packet/game/objectcreate/InventoryData.scala index ef4fa92e6..f21590b4b 100644 --- a/common/src/main/scala/net/psforever/packet/game/objectcreate/InventoryData.scala +++ b/common/src/main/scala/net/psforever/packet/game/objectcreate/InventoryData.scala @@ -1,7 +1,7 @@ // Copyright (c) 2017 PSForever package net.psforever.packet.game.objectcreate -import InventoryItem._ +import InventoryItemData._ import net.psforever.packet.PacketHelpers import scodec.Codec import scodec.codecs._ @@ -19,7 +19,7 @@ import shapeless.{::, HNil} *
* Inventories are usually prefaced with a single bit value not accounted for here to switch them "on." * @param contents the items in the inventory - * @see `InventoryItem` + * @see `InventoryItemData` */ final case class InventoryData(contents : List[InventoryItem] = List.empty) extends StreamBitSize { override def bitsize : Long = { @@ -57,10 +57,10 @@ object InventoryData { /** * A `Codec` for `0x17` `ObjectCreateMessage` data. */ - val codec : Codec[InventoryData] = codec(InventoryItem.codec) + val codec : Codec[InventoryData] = codec(InventoryItemData.codec) /** * A `Codec` for `0x18` `ObjectCreateDetailedMessage` data. */ - val codec_detailed : Codec[InventoryData] = codec(InventoryItem.codec_detailed) + val codec_detailed : Codec[InventoryData] = codec(InventoryItemData.codec_detailed) } diff --git a/common/src/main/scala/net/psforever/packet/game/objectcreate/InventoryItem.scala b/common/src/main/scala/net/psforever/packet/game/objectcreate/InventoryItemData.scala similarity index 82% rename from common/src/main/scala/net/psforever/packet/game/objectcreate/InventoryItem.scala rename to common/src/main/scala/net/psforever/packet/game/objectcreate/InventoryItemData.scala index 4b6a0191b..b07e3500a 100644 --- a/common/src/main/scala/net/psforever/packet/game/objectcreate/InventoryItem.scala +++ b/common/src/main/scala/net/psforever/packet/game/objectcreate/InventoryItemData.scala @@ -5,21 +5,21 @@ import net.psforever.packet.game.PlanetSideGUID import scodec.Codec /** - * Mask the use of `InternalSlot` using a fake class called an `InventoryItem`. + * Mask the use of `InternalSlot` using a fake class called an `InventoryItemData`. */ -object InventoryItem { +object InventoryItemData { /** - * Constructor for creating an `InventoryItem`. + * Constructor for creating an `InventoryItemData`. * @param guid the GUID this object will be assigned * @param slot a parent-defined slot identifier that explains where the child is to be attached to the parent * @param obj the data used as representation of the object to be constructed - * @return an `InventoryItem` object + * @return an `InventoryItemData` object */ def apply(objClass : Int, guid : PlanetSideGUID, slot : Int, obj : ConstructorData) : InventoryItem = InternalSlot(objClass, guid, slot, obj) /** - * Alias `InventoryItem` to `InternalSlot`. + * Alias `InventoryItemData` to `InternalSlot`. */ type InventoryItem = InternalSlot diff --git a/common/src/main/scala/net/psforever/packet/game/objectcreate/MountItem.scala b/common/src/main/scala/net/psforever/packet/game/objectcreate/MountItem.scala index 0af87b305..922bade80 100644 --- a/common/src/main/scala/net/psforever/packet/game/objectcreate/MountItem.scala +++ b/common/src/main/scala/net/psforever/packet/game/objectcreate/MountItem.scala @@ -13,7 +13,7 @@ object MountItem { * @param guid the GUID this object will be assigned * @param slot a parent-defined slot identifier that explains where the child is to be attached to the parent * @param obj the data used as representation of the object to be constructed - * @return an `InventoryItem` object + * @return an `InventoryItemData` object */ def apply(objClass : Int, guid : PlanetSideGUID, slot : Int, obj : ConstructorData) : MountItem = InternalSlot(objClass, guid, slot, obj) diff --git a/common/src/main/scala/net/psforever/packet/game/objectcreate/ObjectClass.scala b/common/src/main/scala/net/psforever/packet/game/objectcreate/ObjectClass.scala index 030def3d9..84fd562d2 100644 --- a/common/src/main/scala/net/psforever/packet/game/objectcreate/ObjectClass.scala +++ b/common/src/main/scala/net/psforever/packet/game/objectcreate/ObjectClass.scala @@ -275,7 +275,7 @@ object ObjectClass { final val bank = 132 final val nano_dispenser = 577 final val command_detonater = 213 - final val laze_pointer = 297 + final val flail_targeting_laser = 297 //ace deployables final val ace = 32 final val advanced_ace = 39 @@ -612,7 +612,7 @@ object ObjectClass { case ObjectClass.applicator => ConstructorData.genericCodec(DetailedWeaponData.codec, "tool") case ObjectClass.bank => ConstructorData.genericCodec(DetailedWeaponData.codec, "tool") case ObjectClass.command_detonater => ConstructorData.genericCodec(DetailedCommandDetonaterData.codec, "tool") - case ObjectClass.laze_pointer => ConstructorData.genericCodec(DetailedWeaponData.codec, "tool") + case ObjectClass.flail_targeting_laser => ConstructorData.genericCodec(DetailedCommandDetonaterData.codec, "tool") case ObjectClass.medicalapplicator => ConstructorData.genericCodec(DetailedWeaponData.codec, "tool") case ObjectClass.nano_dispenser => ConstructorData.genericCodec(DetailedWeaponData.codec, "tool") case ObjectClass.remote_electronics_kit => ConstructorData.genericCodec(DetailedREKData.codec, "tool") @@ -894,7 +894,7 @@ object ObjectClass { case ObjectClass.applicator => ConstructorData.genericCodec(WeaponData.codec, "tool") case ObjectClass.bank => ConstructorData.genericCodec(WeaponData.codec, "tool") case ObjectClass.command_detonater => ConstructorData.genericCodec(CommandDetonaterData.codec, "tool") - case ObjectClass.laze_pointer => ConstructorData.genericCodec(WeaponData.codec, "tool") + case ObjectClass.flail_targeting_laser => ConstructorData.genericCodec(CommandDetonaterData.codec, "tool") case ObjectClass.medicalapplicator => ConstructorData.genericCodec(WeaponData.codec, "tool") case ObjectClass.nano_dispenser => ConstructorData.genericCodec(WeaponData.codec, "tool") case ObjectClass.remote_electronics_kit => ConstructorData.genericCodec(REKData.codec, "tool") @@ -1117,7 +1117,7 @@ object ObjectClass { case ObjectClass.applicator => DroppedItemData.genericCodec(WeaponData.codec, "tool") case ObjectClass.bank => DroppedItemData.genericCodec(WeaponData.codec, "tool") case ObjectClass.command_detonater => DroppedItemData.genericCodec(CommandDetonaterData.codec, "tool") - case ObjectClass.laze_pointer => DroppedItemData.genericCodec(WeaponData.codec, "tool") + case ObjectClass.flail_targeting_laser => DroppedItemData.genericCodec(CommandDetonaterData.codec, "tool") case ObjectClass.medicalapplicator => DroppedItemData.genericCodec(WeaponData.codec, "tool") case ObjectClass.nano_dispenser => DroppedItemData.genericCodec(WeaponData.codec, "tool") case ObjectClass.remote_electronics_kit => DroppedItemData.genericCodec(REKData.codec, " tool") diff --git a/common/src/main/scala/net/psforever/types/ImplantType.scala b/common/src/main/scala/net/psforever/types/ImplantType.scala new file mode 100644 index 000000000..3449c0e45 --- /dev/null +++ b/common/src/main/scala/net/psforever/types/ImplantType.scala @@ -0,0 +1,38 @@ +// Copyright (c) 2017 PSForever +package net.psforever.types + +import net.psforever.packet.PacketHelpers +import scodec.codecs._ + +/** + * An `Enumeration` of the available implants.
+ *
+ * Implant:
+ * ` + * 00 - Regeneration (advanced_regen)
+ * 01 - Enhanced Targeting (targeting)
+ * 02 - Audio Amplifier (audio_amplifier)
+ * 03 - Darklight Vision (darklight_vision)
+ * 04 - Melee Booster (melee_booster)
+ * 05 - Personal Shield (personal_shield)
+ * 06 - Range Magnifier (range_magnifier)
+ * 07 - Second Wind `(na)`
+ * 08 - Sensor Shield (silent_run)
+ * 09 - Surge (surge)
+ * ` + */ +object ImplantType extends Enumeration { + type Type = Value + val AdvancedRegen, + Targeting, + AudioAmplifier, + DarklightVision, + MeleeBooster, + PersonalShield, + RangeMagnifier, + SecondWind, //technically + SilentRun, + Surge = Value + + implicit val codec = PacketHelpers.createEnumerationCodec(this, uint4L) +} diff --git a/common/src/main/scala/net/psforever/types/TransactionType.scala b/common/src/main/scala/net/psforever/types/TransactionType.scala index dd2c0cc72..bf76e0257 100644 --- a/common/src/main/scala/net/psforever/types/TransactionType.scala +++ b/common/src/main/scala/net/psforever/types/TransactionType.scala @@ -12,7 +12,7 @@ object TransactionType extends Enumeration { Sell, // or forget on certif term Unk4, Unk5, - Infantry_Loadout, + InfantryLoadout, Unk7 = Value diff --git a/common/src/test/scala/game/AvatarImplantMessageTest.scala b/common/src/test/scala/game/AvatarImplantMessageTest.scala index 3c2ace41d..166632534 100644 --- a/common/src/test/scala/game/AvatarImplantMessageTest.scala +++ b/common/src/test/scala/game/AvatarImplantMessageTest.scala @@ -4,6 +4,7 @@ package game import org.specs2.mutable._ import net.psforever.packet._ import net.psforever.packet.game._ +import net.psforever.types.ImplantType import scodec.bits._ class AvatarImplantMessageTest extends Specification { diff --git a/common/src/test/scala/game/FavoritesRequestTest.scala b/common/src/test/scala/game/FavoritesRequestTest.scala new file mode 100644 index 000000000..23adfccc1 --- /dev/null +++ b/common/src/test/scala/game/FavoritesRequestTest.scala @@ -0,0 +1,33 @@ +// Copyright (c) 2017 PSForever +package game + +import org.specs2.mutable._ +import net.psforever.packet._ +import net.psforever.packet.game._ +import scodec.bits._ + +class FavoritesRequestTest extends Specification { + val stringInfantry = hex"5E 4B00 1187 4500 7800 6100 6D00 7000 6C00 6500" + + "decode (for infantry)" in { + PacketCoding.DecodePacket(stringInfantry).require match { + case FavoritesRequest(player_guid, unk, action, line, label) => + player_guid mustEqual PlanetSideGUID(75) + unk mustEqual 0 + action mustEqual FavoritesAction.Save + line mustEqual 1 + label.isDefined mustEqual true + label.get mustEqual "Example" + case _ => + ko + } + } + + "encode (for infantry)" in { + val msg = FavoritesRequest(PlanetSideGUID(75), 0, FavoritesAction.Save, 1, Some("Example")) + val pkt = PacketCoding.EncodePacket(msg).require.toByteVector + + pkt mustEqual stringInfantry + } +} + diff --git a/common/src/test/scala/game/ObjectCreateDetailedMessageTest.scala b/common/src/test/scala/game/ObjectCreateDetailedMessageTest.scala index f818ac402..ee9e38d19 100644 --- a/common/src/test/scala/game/ObjectCreateDetailedMessageTest.scala +++ b/common/src/test/scala/game/ObjectCreateDetailedMessageTest.scala @@ -391,16 +391,16 @@ class ObjectCreateDetailedMessageTest extends Specification { false, RibbonBars() ) - val inv = InventoryItem(ObjectClass.beamer, PlanetSideGUID(76), 0, DetailedWeaponData(4, 8, ObjectClass.energy_cell, PlanetSideGUID(77), 0, DetailedAmmoBoxData(8, 16))) :: - InventoryItem(ObjectClass.suppressor, PlanetSideGUID(78), 2, DetailedWeaponData(4, 8, ObjectClass.bullet_9mm, PlanetSideGUID(79), 0, DetailedAmmoBoxData(8, 25))) :: - InventoryItem(ObjectClass.forceblade, PlanetSideGUID(80), 4, DetailedWeaponData(4, 8, ObjectClass.melee_ammo, PlanetSideGUID(81), 0, DetailedAmmoBoxData(8, 1))) :: - InventoryItem(ObjectClass.locker_container, PlanetSideGUID(82), 5, DetailedAmmoBoxData(8, 1)) :: - InventoryItem(ObjectClass.bullet_9mm, PlanetSideGUID(83), 6, DetailedAmmoBoxData(8, 50)) :: - InventoryItem(ObjectClass.bullet_9mm, PlanetSideGUID(84), 9, DetailedAmmoBoxData(8, 50)) :: - InventoryItem(ObjectClass.bullet_9mm, PlanetSideGUID(85), 12, DetailedAmmoBoxData(8, 50)) :: - InventoryItem(ObjectClass.bullet_9mm_AP, PlanetSideGUID(86), 33, DetailedAmmoBoxData(8, 50)) :: - InventoryItem(ObjectClass.energy_cell, PlanetSideGUID(87), 36, DetailedAmmoBoxData(8, 50)) :: - InventoryItem(ObjectClass.remote_electronics_kit, PlanetSideGUID(88), 39, DetailedREKData(8)) :: + val inv = InventoryItemData(ObjectClass.beamer, PlanetSideGUID(76), 0, DetailedWeaponData(4, 8, ObjectClass.energy_cell, PlanetSideGUID(77), 0, DetailedAmmoBoxData(8, 16))) :: + InventoryItemData(ObjectClass.suppressor, PlanetSideGUID(78), 2, DetailedWeaponData(4, 8, ObjectClass.bullet_9mm, PlanetSideGUID(79), 0, DetailedAmmoBoxData(8, 25))) :: + InventoryItemData(ObjectClass.forceblade, PlanetSideGUID(80), 4, DetailedWeaponData(4, 8, ObjectClass.melee_ammo, PlanetSideGUID(81), 0, DetailedAmmoBoxData(8, 1))) :: + InventoryItemData(ObjectClass.locker_container, PlanetSideGUID(82), 5, DetailedAmmoBoxData(8, 1)) :: + InventoryItemData(ObjectClass.bullet_9mm, PlanetSideGUID(83), 6, DetailedAmmoBoxData(8, 50)) :: + InventoryItemData(ObjectClass.bullet_9mm, PlanetSideGUID(84), 9, DetailedAmmoBoxData(8, 50)) :: + InventoryItemData(ObjectClass.bullet_9mm, PlanetSideGUID(85), 12, DetailedAmmoBoxData(8, 50)) :: + InventoryItemData(ObjectClass.bullet_9mm_AP, PlanetSideGUID(86), 33, DetailedAmmoBoxData(8, 50)) :: + InventoryItemData(ObjectClass.energy_cell, PlanetSideGUID(87), 36, DetailedAmmoBoxData(8, 50)) :: + InventoryItemData(ObjectClass.remote_electronics_kit, PlanetSideGUID(88), 39, DetailedREKData(8)) :: Nil val obj = DetailedCharacterData( app, diff --git a/common/src/test/scala/game/ObjectCreateMessageTest.scala b/common/src/test/scala/game/ObjectCreateMessageTest.scala index 7b779f8bc..af4d8df26 100644 --- a/common/src/test/scala/game/ObjectCreateMessageTest.scala +++ b/common/src/test/scala/game/ObjectCreateMessageTest.scala @@ -1076,9 +1076,9 @@ class ObjectCreateMessageTest extends Specification { "encode (locker container)" in { val obj = LockerContainerData( InventoryData( - InventoryItem(ObjectClass.nano_dispenser, PlanetSideGUID(2935), 0, WeaponData(0x6, 0x0, ObjectClass.armor_canister, PlanetSideGUID(3426), 0, AmmoBoxData())) :: - InventoryItem(ObjectClass.armor_canister, PlanetSideGUID(4090), 45, AmmoBoxData()) :: - InventoryItem(ObjectClass.armor_canister, PlanetSideGUID(3326), 78, AmmoBoxData()) :: + InventoryItemData(ObjectClass.nano_dispenser, PlanetSideGUID(2935), 0, WeaponData(0x6, 0x0, ObjectClass.armor_canister, PlanetSideGUID(3426), 0, AmmoBoxData())) :: + InventoryItemData(ObjectClass.armor_canister, PlanetSideGUID(4090), 45, AmmoBoxData()) :: + InventoryItemData(ObjectClass.armor_canister, PlanetSideGUID(3326), 78, AmmoBoxData()) :: Nil ) ) @@ -1127,11 +1127,11 @@ class ObjectCreateMessageTest extends Specification { Some(ImplantEffects.NoEffects), Some(Cosmetics(true, true, true, true, false)), InventoryData( - InventoryItem(ObjectClass.plasma_grenade, PlanetSideGUID(3662), 0, WeaponData(0, 0, ObjectClass.plasma_grenade_ammo, PlanetSideGUID(3751), 0, AmmoBoxData())) :: - InventoryItem(ObjectClass.bank, PlanetSideGUID(3908), 1, WeaponData(0, 0, 1, ObjectClass.armor_canister, PlanetSideGUID(4143), 0, AmmoBoxData())) :: - InventoryItem(ObjectClass.mini_chaingun, PlanetSideGUID(4164), 2, WeaponData(0, 0, ObjectClass.bullet_9mm, PlanetSideGUID(3728), 0, AmmoBoxData())) :: - InventoryItem(ObjectClass.phoenix, PlanetSideGUID(3603), 3, WeaponData(0, 0, ObjectClass.phoenix_missile, PlanetSideGUID(3056), 0, AmmoBoxData())) :: - InventoryItem(ObjectClass.chainblade, PlanetSideGUID(4088), 4, WeaponData(0, 0, 1, ObjectClass.melee_ammo, PlanetSideGUID(3279), 0, AmmoBoxData())) :: + InventoryItemData(ObjectClass.plasma_grenade, PlanetSideGUID(3662), 0, WeaponData(0, 0, ObjectClass.plasma_grenade_ammo, PlanetSideGUID(3751), 0, AmmoBoxData())) :: + InventoryItemData(ObjectClass.bank, PlanetSideGUID(3908), 1, WeaponData(0, 0, 1, ObjectClass.armor_canister, PlanetSideGUID(4143), 0, AmmoBoxData())) :: + InventoryItemData(ObjectClass.mini_chaingun, PlanetSideGUID(4164), 2, WeaponData(0, 0, ObjectClass.bullet_9mm, PlanetSideGUID(3728), 0, AmmoBoxData())) :: + InventoryItemData(ObjectClass.phoenix, PlanetSideGUID(3603), 3, WeaponData(0, 0, ObjectClass.phoenix_missile, PlanetSideGUID(3056), 0, AmmoBoxData())) :: + InventoryItemData(ObjectClass.chainblade, PlanetSideGUID(4088), 4, WeaponData(0, 0, 1, ObjectClass.melee_ammo, PlanetSideGUID(3279), 0, AmmoBoxData())) :: Nil ), DrawnSlot.Rifle1 diff --git a/common/src/test/scala/objects/ConverterTest.scala b/common/src/test/scala/objects/ConverterTest.scala new file mode 100644 index 000000000..07af93659 --- /dev/null +++ b/common/src/test/scala/objects/ConverterTest.scala @@ -0,0 +1,200 @@ +// Copyright (c) 2017 PSForever +package objects + +import net.psforever.objects.definition.converter.{ACEConverter, REKConverter} +import net.psforever.objects._ +import net.psforever.objects.definition._ +import net.psforever.objects.equipment.CItem.{DeployedItem, Unit} +import net.psforever.objects.equipment._ +import net.psforever.objects.inventory.InventoryTile +import net.psforever.packet.game.PlanetSideGUID +import net.psforever.packet.game.objectcreate._ +import net.psforever.types.{CharacterGender, PlanetSideEmpire, Vector3} +import org.specs2.mutable.Specification + +import scala.util.Success + +class ConverterTest extends Specification { + "AmmoBox" should { + val bullet_9mm = AmmoBoxDefinition(28) + bullet_9mm.Capacity = 50 + + "convert to packet" in { + val obj = AmmoBox(bullet_9mm) + obj.Definition.Packet.DetailedConstructorData(obj) match { + case Success(pkt) => + pkt mustEqual DetailedAmmoBoxData(8, 50) + case _ => + ko + } + obj.Definition.Packet.ConstructorData(obj) match { + case Success(pkt) => + pkt mustEqual AmmoBoxData() + case _ => + ko + } + } + } + + "Tool" should { + "convert to packet" in { + val tdef = ToolDefinition(1076) + tdef.Size = EquipmentSize.Rifle + tdef.AmmoTypes += Ammo.shotgun_shell + tdef.AmmoTypes += Ammo.shotgun_shell_AP + tdef.FireModes += new FireModeDefinition + tdef.FireModes.head.AmmoTypeIndices += 0 + tdef.FireModes.head.AmmoTypeIndices += 1 + tdef.FireModes.head.AmmoSlotIndex = 0 + val obj : Tool = Tool(tdef) + val box = AmmoBox(PlanetSideGUID(90), new AmmoBoxDefinition(Ammo.shotgun_shell.id)) + obj.AmmoSlots.head.Box = box + obj.AmmoSlots.head.Magazine = 30 + + obj.Definition.Packet.DetailedConstructorData(obj) match { + case Success(pkt) => + pkt mustEqual DetailedWeaponData(4,8, Ammo.shotgun_shell.id, PlanetSideGUID(90), 0, DetailedAmmoBoxData(8, 30)) + case _ => + ko + } + obj.Definition.Packet.ConstructorData(obj) match { + case Success(pkt) => + pkt mustEqual WeaponData(4,8, 0, Ammo.shotgun_shell.id, PlanetSideGUID(90), 0, AmmoBoxData()) + case _ => + ko + } + } + } + + "Kit" should { + "convert to packet" in { + val kdef = KitDefinition(Kits.medkit) + val obj = Kit(PlanetSideGUID(90), kdef) + obj.Definition.Packet.DetailedConstructorData(obj) match { + case Success(pkt) => + pkt mustEqual DetailedAmmoBoxData(0, 1) + case _ => + ko + } + obj.Definition.Packet.ConstructorData(obj) match { + case Success(pkt) => + pkt mustEqual AmmoBoxData() + case _ => + ko + } + } + + "ConstructionItem" should { + "convert to packet" in { + val cdef = ConstructionItemDefinition(Unit.advanced_ace) + cdef.Modes += DeployedItem.tank_traps + cdef.Modes += DeployedItem.portable_manned_turret_tr + cdef.Modes += DeployedItem.deployable_shield_generator + cdef.Tile = InventoryTile.Tile63 + cdef.Packet = new ACEConverter() + val obj = ConstructionItem(PlanetSideGUID(90), cdef) + obj.Definition.Packet.DetailedConstructorData(obj) match { + case Success(pkt) => + pkt mustEqual DetailedACEData(0) + case _ => + ko + } + obj.Definition.Packet.ConstructorData(obj) match { + case Success(pkt) => + pkt mustEqual ACEData(0,0) + case _ => + ko + } + } + } + } + + "SimpleItem" should { + "convert to packet" in { + val sdef = SimpleItemDefinition(SItem.remote_electronics_kit) + sdef.Packet = new REKConverter() + val obj = SimpleItem(PlanetSideGUID(90), sdef) + obj.Definition.Packet.DetailedConstructorData(obj) match { + case Success(pkt) => + pkt mustEqual DetailedREKData(8) + case _ => + ko + } + obj.Definition.Packet.ConstructorData(obj) match { + case Success(pkt) => + pkt mustEqual REKData(8,0) + case _ => + ko + } + } + } + + "Player" should { + "convert to packet" in { + /* + Create an AmmoBoxDefinition with which to build two AmmoBoxes + Create a ToolDefinition with which to create a Tool + Load one of the AmmoBoxes into that Tool + Create a Player + Give the Player's Holster (2) the Tool + Place the remaining AmmoBox into the Player's inventory in the third slot (8) + */ + val bullet_9mm = AmmoBoxDefinition(28) + bullet_9mm.Capacity = 50 + val box1 = AmmoBox(PlanetSideGUID(90), bullet_9mm) + val box2 = AmmoBox(PlanetSideGUID(91), bullet_9mm) + val tdef = ToolDefinition(1076) + tdef.Name = "sample_weapon" + tdef.Size = EquipmentSize.Rifle + tdef.AmmoTypes += Ammo.bullet_9mm + tdef.FireModes += new FireModeDefinition + tdef.FireModes.head.AmmoTypeIndices += 0 + tdef.FireModes.head.AmmoSlotIndex = 0 + tdef.FireModes.head.Magazine = 18 + val tool = Tool(PlanetSideGUID(92), tdef) + tool.AmmoSlots.head.Box = box1 + val obj = Player(PlanetSideGUID(93), "Chord", PlanetSideEmpire.TR, CharacterGender.Male, 0, 5) + obj.Slot(2).Equipment = tool + obj.Inventory += 8 -> box2 + + obj.Definition.Packet.DetailedConstructorData(obj).isSuccess mustEqual true + ok //TODO write more of this test + } + } + + "Vehicle" should { + "convert to packet" in { + val hellfire_ammo = AmmoBoxDefinition(Ammo.hellfire_ammo.id) + + val fury_weapon_systema_def = ToolDefinition(ObjectClass.fury_weapon_systema) + fury_weapon_systema_def.Size = EquipmentSize.VehicleWeapon + fury_weapon_systema_def.AmmoTypes += Ammo.hellfire_ammo + fury_weapon_systema_def.FireModes += new FireModeDefinition + fury_weapon_systema_def.FireModes.head.AmmoTypeIndices += 0 + fury_weapon_systema_def.FireModes.head.AmmoSlotIndex = 0 + fury_weapon_systema_def.FireModes.head.Magazine = 2 + + val fury_def = VehicleDefinition(ObjectClass.fury) + fury_def.Seats += 0 -> new SeatDefinition() + fury_def.Seats(0).Bailable = true + fury_def.Seats(0).ControlledWeapon = Some(1) + fury_def.MountPoints += 0 -> 0 + fury_def.MountPoints += 2 -> 0 + fury_def.Weapons += 1 -> fury_weapon_systema_def + fury_def.TrunkSize = InventoryTile(11, 11) + fury_def.TrunkOffset = 30 + + val hellfire_ammo_box = AmmoBox(PlanetSideGUID(432), hellfire_ammo) + + val fury = Vehicle(PlanetSideGUID(413), fury_def) + fury.Faction = PlanetSideEmpire.VS + fury.Position = Vector3(3674.8438f, 2732f, 91.15625f) + fury.Orientation = Vector3(0.0f, 0.0f, 90.0f) + fury.WeaponControlledFromSeat(0).get.GUID = PlanetSideGUID(400) + fury.WeaponControlledFromSeat(0).get.AmmoSlots.head.Box = hellfire_ammo_box + + fury.Definition.Packet.ConstructorData(fury).isSuccess mustEqual true + ok //TODO write more of this test + } + } +} \ No newline at end of file diff --git a/common/src/test/scala/objects/EntityTest.scala b/common/src/test/scala/objects/EntityTest.scala new file mode 100644 index 000000000..68cac3ee8 --- /dev/null +++ b/common/src/test/scala/objects/EntityTest.scala @@ -0,0 +1,83 @@ +// Copyright (c) 2017 PSForever +package objects + +import net.psforever.objects.PlanetSideGameObject +import net.psforever.objects.definition.ObjectDefinition +import net.psforever.objects.entity.NoGUIDException +import net.psforever.packet.game.PlanetSideGUID +import net.psforever.types.Vector3 +import org.specs2.mutable._ + +class EntityTest extends Specification { + //both WorldEntity and IdentifiableEntity are components of PlanetSideGameObject + private class EntityTestClass extends PlanetSideGameObject { + def Definition : ObjectDefinition = new ObjectDefinition(0) { } + } + + "SimpleWorldEntity" should { + "construct" in { + new EntityTestClass() + ok + } + + "initialize" in { + val obj : EntityTestClass = new EntityTestClass() + obj.Position mustEqual Vector3(0f, 0f, 0f) + obj.Orientation mustEqual Vector3(0f, 0f, 0f) + obj.Velocity mustEqual Vector3(0f, 0f, 0f) + } + + "mutate and access" in { + val obj : EntityTestClass = new EntityTestClass + obj.Position = Vector3(1f, 1f, 1f) + obj.Orientation = Vector3(2f, 2f, 2f) + obj.Velocity = Vector3(3f, 3f, 3f) + + obj.Position mustEqual Vector3(1f, 1f, 1f) + obj.Orientation mustEqual Vector3(2f, 2f, 2f) + obj.Velocity mustEqual Vector3(3f, 3f, 3f) + } + + "clamp Orientation" in { + val obj : EntityTestClass = new EntityTestClass + obj.Orientation = Vector3(-1f, 361f, -0f) + obj.Orientation mustEqual Vector3(359f, 1f, 0f) + } + } + + "IdentifiableEntity" should { + "construct" in { + new EntityTestClass() + ok + } + + "error while unset" in { + val obj : EntityTestClass = new EntityTestClass + obj.GUID must throwA[NoGUIDException] + } + + "work after mutation" in { + val obj : EntityTestClass = new EntityTestClass + obj.GUID = PlanetSideGUID(1051) + obj.GUID mustEqual PlanetSideGUID(1051) + } + + "work after multiple mutations" in { + val obj : EntityTestClass = new EntityTestClass + obj.GUID = PlanetSideGUID(1051) + obj.GUID mustEqual PlanetSideGUID(1051) + obj.GUID = PlanetSideGUID(30052) + obj.GUID mustEqual PlanetSideGUID(30052) + obj.GUID = PlanetSideGUID(62) + obj.GUID mustEqual PlanetSideGUID(62) + } + + "invalidate and resume error" in { + val obj : EntityTestClass = new EntityTestClass + obj.GUID = PlanetSideGUID(1051) + obj.GUID mustEqual PlanetSideGUID(1051) + obj.Invalidate() + obj.GUID must throwA[NoGUIDException] + } + } +} diff --git a/common/src/test/scala/objects/EquipmentTest.scala b/common/src/test/scala/objects/EquipmentTest.scala new file mode 100644 index 000000000..284d0b2b7 --- /dev/null +++ b/common/src/test/scala/objects/EquipmentTest.scala @@ -0,0 +1,258 @@ +// Copyright (c) 2017 PSForever +package objects + +import net.psforever.objects._ +import net.psforever.objects.definition._ +import net.psforever.objects.equipment.CItem.{DeployedItem, Unit} +import net.psforever.objects.equipment._ +import net.psforever.objects.inventory.InventoryTile +import net.psforever.objects.GlobalDefinitions._ +import org.specs2.mutable._ + +class EquipmentTest extends Specification { + + "AmmoBox" should { + "define" in { + val obj = AmmoBoxDefinition(86) + obj.Capacity = 300 + obj.Tile = InventoryTile.Tile44 + + obj.AmmoType mustEqual Ammo.aphelion_immolation_cannon_ammo + obj.Capacity mustEqual 300 + obj.Tile.width mustEqual InventoryTile.Tile44.width + obj.Tile.height mustEqual InventoryTile.Tile44.height + obj.ObjectId mustEqual 86 + } + + "construct" in { + val obj = AmmoBox(bullet_9mm) + obj.AmmoType mustEqual Ammo.bullet_9mm + obj.Capacity mustEqual 50 + } + + "construct (2)" in { + val obj = AmmoBox(bullet_9mm, 150) + obj.AmmoType mustEqual Ammo.bullet_9mm + obj.Capacity mustEqual 150 + } + + "vary capacity" in { + val obj = AmmoBox(bullet_9mm, 0) + obj.Capacity mustEqual 1 //can not be initialized to 0 + obj.Capacity = 75 + obj.Capacity mustEqual 75 + } + + "limit capacity" in { + val obj = AmmoBox(bullet_9mm) + obj.Capacity mustEqual 50 + obj.Capacity = -1 + obj.Capacity mustEqual 0 + obj.Capacity = 65536 + obj.Capacity mustEqual 65535 + } + } + + "Tool" should { + "define" in { + val obj = ToolDefinition(1076) + obj.Name = "sample_weapon" + obj.Size = EquipmentSize.Rifle + obj.AmmoTypes += Ammo.shotgun_shell + obj.AmmoTypes += Ammo.shotgun_shell_AP + obj.FireModes += new FireModeDefinition + obj.FireModes.head.AmmoTypeIndices += 0 + obj.FireModes.head.AmmoTypeIndices += 1 + obj.FireModes.head.AmmoSlotIndex = 0 + obj.FireModes.head.Magazine = 18 + obj.FireModes.head.ResetAmmoIndexOnSwap = true + obj.FireModes += new FireModeDefinition + obj.FireModes(1).AmmoTypeIndices += 0 + obj.FireModes(1).AmmoTypeIndices += 1 + obj.FireModes(1).AmmoSlotIndex = 1 + obj.FireModes(1).Chamber = 3 + obj.FireModes(1).Magazine = 18 + obj.Tile = InventoryTile.Tile93 + obj.ObjectId mustEqual 1076 + obj.Name mustEqual "sample_weapon" + obj.AmmoTypes.head mustEqual Ammo.shotgun_shell + obj.AmmoTypes(1) mustEqual Ammo.shotgun_shell_AP + obj.FireModes.head.AmmoTypeIndices.head mustEqual 0 + obj.FireModes.head.AmmoTypeIndices(1) mustEqual 1 + obj.FireModes.head.AmmoSlotIndex mustEqual 0 + obj.FireModes.head.Chamber mustEqual 1 + obj.FireModes.head.Magazine mustEqual 18 + obj.FireModes.head.ResetAmmoIndexOnSwap mustEqual true + obj.FireModes(1).AmmoTypeIndices.head mustEqual 0 + obj.FireModes(1).AmmoTypeIndices(1) mustEqual 1 + obj.FireModes(1).AmmoSlotIndex mustEqual 1 + obj.FireModes(1).Chamber mustEqual 3 + obj.FireModes(1).Magazine mustEqual 18 + obj.FireModes(1).ResetAmmoIndexOnSwap mustEqual false + obj.Tile.width mustEqual InventoryTile.Tile93.width + obj.Tile.height mustEqual InventoryTile.Tile93.height + } + + "construct" in { + val obj : Tool = Tool(fury_weapon_systema) + obj.Definition.ObjectId mustEqual fury_weapon_systema.ObjectId + } + + "fire mode" in { + //explanation: fury_weapon_systema has one fire mode and that fire mode is our only option + val obj : Tool = Tool(fury_weapon_systema) + obj.Magazine = obj.MaxMagazine + obj.Magazine mustEqual obj.Definition.FireModes.head.Magazine + //fmode = 0 + obj.FireModeIndex mustEqual 0 + obj.FireMode.Magazine mustEqual 2 + obj.AmmoType mustEqual Ammo.hellfire_ammo + //fmode -> 1 (0) + obj.FireModeIndex = 1 + obj.FireModeIndex mustEqual 0 + obj.FireMode.Magazine mustEqual 2 + obj.AmmoType mustEqual Ammo.hellfire_ammo + } + + "multiple fire modes" in { + //explanation: sample_weapon has two fire modes; adjusting the FireMode changes between them + val tdef = ToolDefinition(1076) + tdef.Size = EquipmentSize.Rifle + tdef.AmmoTypes += Ammo.shotgun_shell + tdef.AmmoTypes += Ammo.shotgun_shell_AP + tdef.FireModes += new FireModeDefinition + tdef.FireModes.head.AmmoTypeIndices += 0 + tdef.FireModes.head.AmmoSlotIndex = 0 + tdef.FireModes.head.Magazine = 9 + tdef.FireModes += new FireModeDefinition + tdef.FireModes(1).AmmoTypeIndices += 1 + tdef.FireModes(1).AmmoSlotIndex = 1 + tdef.FireModes(1).Magazine = 18 + val obj : Tool = Tool(tdef) + //fmode = 0 + obj.FireModeIndex mustEqual 0 + obj.FireMode.Magazine mustEqual 9 + obj.AmmoType mustEqual Ammo.shotgun_shell + //fmode -> 1 + obj.NextFireMode + obj.FireModeIndex mustEqual 1 + obj.FireMode.Magazine mustEqual 18 + obj.AmmoType mustEqual Ammo.shotgun_shell_AP + //fmode -> 0 + obj.NextFireMode + obj.FireModeIndex mustEqual 0 + obj.FireMode.Magazine mustEqual 9 + obj.AmmoType mustEqual Ammo.shotgun_shell + } + + "multiple types of ammunition" in { + //explanation: obj has one fire mode and two ammunitions; adjusting the AmmoType changes between them + val tdef = ToolDefinition(1076) + tdef.Size = EquipmentSize.Rifle + tdef.AmmoTypes += Ammo.shotgun_shell + tdef.AmmoTypes += Ammo.shotgun_shell_AP + tdef.FireModes += new FireModeDefinition + tdef.FireModes.head.AmmoTypeIndices += 0 + tdef.FireModes.head.AmmoTypeIndices += 1 + tdef.FireModes.head.AmmoSlotIndex = 0 + val obj : Tool = Tool(tdef) + //ammo = 0 + obj.AmmoTypeIndex mustEqual 0 + obj.AmmoType mustEqual Ammo.shotgun_shell + //ammo -> 1 + obj.NextAmmoType + obj.AmmoTypeIndex mustEqual 1 + obj.AmmoType mustEqual Ammo.shotgun_shell_AP + //ammo -> 2 (0) + obj.NextAmmoType + obj.AmmoTypeIndex mustEqual 0 + obj.AmmoType mustEqual Ammo.shotgun_shell + } + } + + "Kit" should { + "define" in { + val sample = KitDefinition(Kits.medkit) + sample.ObjectId mustEqual medkit.ObjectId + sample.Tile.width mustEqual medkit.Tile.width + sample.Tile.height mustEqual medkit.Tile.height + } + + "construct" in { + val obj : Kit = Kit(medkit) + obj.Definition.ObjectId mustEqual medkit.ObjectId + } + } + + "ConstructionItem" should { + val advanced_ace_tr = ConstructionItemDefinition(39) + advanced_ace_tr.Modes += DeployedItem.tank_traps + advanced_ace_tr.Modes += DeployedItem.portable_manned_turret_tr + advanced_ace_tr.Modes += DeployedItem.deployable_shield_generator + advanced_ace_tr.Tile = InventoryTile.Tile63 + + "define" in { + val sample = ConstructionItemDefinition(Unit.advanced_ace) + sample.Modes += DeployedItem.tank_traps + sample.Modes += DeployedItem.portable_manned_turret_tr + sample.Modes += DeployedItem.deployable_shield_generator + sample.Tile = InventoryTile.Tile63 + sample.Modes.head mustEqual DeployedItem.tank_traps + sample.Modes(1) mustEqual DeployedItem.portable_manned_turret_tr + sample.Modes(2) mustEqual DeployedItem.deployable_shield_generator + sample.Tile.width mustEqual InventoryTile.Tile63.width + sample.Tile.height mustEqual InventoryTile.Tile63.height + } + + "construct" in { + val obj : ConstructionItem = ConstructionItem(advanced_ace_tr) + obj.Definition.ObjectId mustEqual advanced_ace_tr.ObjectId + } + + "fire mode" in { + //explanation: router_telepad has one fire mode and that fire mode is our only option + val router_telepad : ConstructionItemDefinition = ConstructionItemDefinition(Unit.router_telepad) + router_telepad.Modes += DeployedItem.router_telepad_deployable + val obj : ConstructionItem = ConstructionItem(router_telepad) + //fmode = 0 + obj.FireModeIndex mustEqual 0 + obj.FireMode mustEqual DeployedItem.router_telepad_deployable + //fmode -> 1 (0) + obj.FireModeIndex = 1 + obj.FireModeIndex mustEqual 0 + obj.FireMode mustEqual DeployedItem.router_telepad_deployable + } + + "multiple fire modes" in { + //explanation: advanced_ace_tr has three fire modes; adjusting the FireMode changes between them + val obj : ConstructionItem = ConstructionItem(advanced_ace_tr) + //fmode = 0 + obj.FireModeIndex mustEqual 0 + obj.FireMode mustEqual DeployedItem.tank_traps + //fmode -> 1 + obj.NextFireMode + obj.FireModeIndex mustEqual 1 + obj.FireMode mustEqual DeployedItem.portable_manned_turret_tr + //fmode -> 2 + obj.NextFireMode + obj.FireModeIndex mustEqual 2 + obj.FireMode mustEqual DeployedItem.deployable_shield_generator + //fmode -> 0 + obj.NextFireMode + obj.FireModeIndex mustEqual 0 + obj.FireMode mustEqual DeployedItem.tank_traps + } + } + + "SimpleItem" should { + "define" in { + val sample = SimpleItemDefinition(SItem.remote_electronics_kit) + sample.ObjectId mustEqual remote_electronics_kit.ObjectId + } + + "construct" in { + val obj : SimpleItem = SimpleItem(remote_electronics_kit) + obj.Definition.ObjectId mustEqual remote_electronics_kit.ObjectId + } + } +} diff --git a/common/src/test/scala/objects/ImplantTest.scala b/common/src/test/scala/objects/ImplantTest.scala new file mode 100644 index 000000000..e1f47936b --- /dev/null +++ b/common/src/test/scala/objects/ImplantTest.scala @@ -0,0 +1,76 @@ +// Copyright (c) 2017 PSForever +package objects + +import net.psforever.objects.Implant +import net.psforever.objects.definition.{ImplantDefinition, Stance} +import net.psforever.types.{ExoSuitType, ImplantType} +import org.specs2.mutable._ + +class ImplantTest extends Specification { + val sample = new ImplantDefinition(8) //variant of sensor shield/silent run + sample.Initialization = 90000 //1:30 + sample.ActivationCharge = 3 + sample.DurationChargeBase = 1 + sample.DurationChargeByExoSuit += ExoSuitType.Agile -> 2 + sample.DurationChargeByExoSuit += ExoSuitType.Reinforced -> 2 + sample.DurationChargeByExoSuit += ExoSuitType.Standard -> 1 + sample.DurationChargeByStance += Stance.Running -> 1 + + "define" in { + sample.Initialization mustEqual 90000 + sample.ActivationCharge mustEqual 3 + sample.DurationChargeBase mustEqual 1 + sample.DurationChargeByExoSuit(ExoSuitType.Agile) mustEqual 2 + sample.DurationChargeByExoSuit(ExoSuitType.Reinforced) mustEqual 2 + sample.DurationChargeByExoSuit(ExoSuitType.Standard) mustEqual 1 + sample.DurationChargeByExoSuit(ExoSuitType.Infiltration) mustEqual 0 //default value + sample.DurationChargeByStance(Stance.Running) mustEqual 1 + sample.DurationChargeByStance(Stance.Crouching) mustEqual 0 //default value + sample.Type mustEqual ImplantType.SilentRun + } + + "construct" in { + val obj = new Implant(sample) + obj.Definition.Type mustEqual sample.Type + obj.Active mustEqual false + obj.Ready mustEqual false + obj.Timer mustEqual 0 + } + + "reset/init their timer" in { + val obj = new Implant(sample) + obj.Timer mustEqual 0 + obj.Reset() + obj.Timer mustEqual 90000 + } + + "reset/init their readiness condition" in { + val obj = new Implant(sample) + obj.Ready mustEqual false + obj.Timer = 0 + obj.Ready mustEqual true + obj.Reset() + obj.Ready mustEqual false + } + + "not activate until they are ready" in { + val obj = new Implant(sample) + obj.Active = true + obj.Active mustEqual false + obj.Timer = 0 + obj.Active = true + obj.Active mustEqual true + } + + "not cost energy while not active" in { + val obj = new Implant(sample) + obj.Charge(ExoSuitType.Reinforced, Stance.Running) mustEqual 0 + } + + "cost energy while active" in { + val obj = new Implant(sample) + obj.Timer = 0 + obj.Active = true + obj.Charge(ExoSuitType.Reinforced, Stance.Running) mustEqual 4 + } +} diff --git a/common/src/test/scala/objects/InventoryTest.scala b/common/src/test/scala/objects/InventoryTest.scala new file mode 100644 index 000000000..52698fa5e --- /dev/null +++ b/common/src/test/scala/objects/InventoryTest.scala @@ -0,0 +1,215 @@ +// Copyright (c) 2017 PSForever +package objects + +import net.psforever.objects.{AmmoBox, SimpleItem} +import net.psforever.objects.definition.SimpleItemDefinition +import net.psforever.objects.inventory.{GridInventory, InventoryItem, InventoryTile} +import net.psforever.objects.GlobalDefinitions._ +import net.psforever.packet.game.PlanetSideGUID +import org.specs2.mutable._ + +import scala.collection.mutable.ListBuffer +import scala.util.Success + +class InventoryTest extends Specification { + val bullet9mmBox1 = AmmoBox(PlanetSideGUID(1), bullet_9mm) + val bullet9mmBox2 = AmmoBox(PlanetSideGUID(2), bullet_9mm) + + "GridInventory" should { + "construct" in { + val obj : GridInventory = GridInventory() + obj.TotalCapacity mustEqual 1 + obj.Capacity mustEqual 1 + } + + "resize" in { + val obj : GridInventory = GridInventory(9, 6) + obj.TotalCapacity mustEqual 54 + obj.Capacity mustEqual 54 + obj.Size mustEqual 0 + } + + "insert item" in { + val obj : GridInventory = GridInventory(9, 6) + obj.CheckCollisions(23, bullet9mmBox1) mustEqual Success(Nil) + obj += 2 -> bullet9mmBox1 + obj.TotalCapacity mustEqual 54 + obj.Capacity mustEqual 45 + obj.Size mustEqual 1 + obj.hasItem(PlanetSideGUID(1)) mustEqual Some(bullet9mmBox1) + obj.Clear() + obj.Size mustEqual 0 + } + + "check for collision with inventory border" in { + val obj : GridInventory = GridInventory(3, 3) + //safe + obj.CheckCollisionsAsList(0, 3, 3) mustEqual Success(Nil) + //right + obj.CheckCollisionsAsList(-1, 3, 3).isFailure mustEqual true + //left + obj.CheckCollisionsAsList(1, 3, 3).isFailure mustEqual true + //bottom + obj.CheckCollisionsAsList(3, 3, 3).isFailure mustEqual true + } + + "check for item collision (right insert)" in { + val obj : GridInventory = GridInventory(9, 6) + obj += 0 -> bullet9mmBox1 + obj.Capacity mustEqual 45 + val w = bullet9mmBox2.Tile.width + val h = bullet9mmBox2.Tile.height + obj.CheckCollisionsAsList(0, w, h) mustEqual Success(1 :: Nil) + obj.CheckCollisionsAsList(1, w, h) mustEqual Success(1 :: Nil) + obj.CheckCollisionsAsList(2, w, h) mustEqual Success(1 :: Nil) + obj.CheckCollisionsAsList(3, w, h) mustEqual Success(Nil) + obj.CheckCollisionsAsGrid(0, w, h) mustEqual Success(1 :: Nil) + obj.CheckCollisionsAsGrid(1, w, h) mustEqual Success(1 :: Nil) + obj.CheckCollisionsAsGrid(2, w, h) mustEqual Success(1 :: Nil) + obj.CheckCollisionsAsGrid(3, w, h) mustEqual Success(Nil) + obj.Clear() + ok + } + + "check for item collision (left insert)" in { + val obj : GridInventory = GridInventory(9, 6) + obj += 3 -> bullet9mmBox1 + obj.Capacity mustEqual 45 + val w = bullet9mmBox2.Tile.width + val h = bullet9mmBox2.Tile.height + obj.CheckCollisionsAsList(3, w, h) mustEqual Success(1 :: Nil) + obj.CheckCollisionsAsList(2, w, h) mustEqual Success(1 :: Nil) + obj.CheckCollisionsAsList(1, w, h) mustEqual Success(1 :: Nil) + obj.CheckCollisionsAsList(0, w, h) mustEqual Success(Nil) + obj.CheckCollisionsAsGrid(3, w, h) mustEqual Success(1 :: Nil) + obj.CheckCollisionsAsGrid(2, w, h) mustEqual Success(1 :: Nil) + obj.CheckCollisionsAsGrid(1, w, h) mustEqual Success(1 :: Nil) + obj.CheckCollisionsAsGrid(0, w, h) mustEqual Success(Nil) + obj.Clear() + ok + } + + "check for item collision (below insert)" in { + val obj : GridInventory = GridInventory(9, 6) + obj += 0 -> bullet9mmBox1 + obj.Capacity mustEqual 45 + val w = bullet9mmBox2.Tile.width + val h = bullet9mmBox2.Tile.height + obj.CheckCollisionsAsList(0, w, h) mustEqual Success(1 :: Nil) + obj.CheckCollisionsAsList(9, w, h) mustEqual Success(1 :: Nil) + obj.CheckCollisionsAsList(18, w, h) mustEqual Success(1 :: Nil) + obj.CheckCollisionsAsList(27, w, h) mustEqual Success(Nil) + obj.CheckCollisionsAsGrid(0, w, h) mustEqual Success(1 :: Nil) + obj.CheckCollisionsAsGrid(9, w, h) mustEqual Success(1 :: Nil) + obj.CheckCollisionsAsGrid(18, w, h) mustEqual Success(1 :: Nil) + obj.CheckCollisionsAsGrid(27, w, h) mustEqual Success(Nil) + obj.Clear() + ok + } + + "check for item collision (above insert)" in { + val obj : GridInventory = GridInventory(9, 6) + obj += 27 -> bullet9mmBox1 + obj.Capacity mustEqual 45 + val w = bullet9mmBox2.Tile.width + val h = bullet9mmBox2.Tile.height + obj.CheckCollisionsAsList(27, w, h) mustEqual Success(1 :: Nil) + obj.CheckCollisionsAsList(19, w, h) mustEqual Success(1 :: Nil) + obj.CheckCollisionsAsList(9, w, h) mustEqual Success(1 :: Nil) + obj.CheckCollisionsAsList(0, w, h) mustEqual Success(Nil) + obj.CheckCollisionsAsGrid(27, w, h) mustEqual Success(1 :: Nil) + obj.CheckCollisionsAsGrid(19, w, h) mustEqual Success(1 :: Nil) + obj.CheckCollisionsAsGrid(9, w, h) mustEqual Success(1 :: Nil) + obj.CheckCollisionsAsGrid(0, w, h) mustEqual Success(Nil) + obj.Clear() + ok + } + + "block insertion if item collision" in { + val obj : GridInventory = GridInventory(9, 6) + obj += 0 -> bullet9mmBox1 + obj.Capacity mustEqual 45 + obj.hasItem(PlanetSideGUID(1)) mustEqual Some(bullet9mmBox1) + obj += 2 -> bullet9mmBox2 + obj.hasItem(PlanetSideGUID(2)) mustEqual None + obj.Clear() + ok + } + + "remove item" in { + val obj : GridInventory = GridInventory(9, 6) + obj += 0 -> bullet9mmBox1 + obj.hasItem(PlanetSideGUID(1)) mustEqual Some(bullet9mmBox1) + obj -= PlanetSideGUID(1) + obj.hasItem(PlanetSideGUID(1)) mustEqual None + obj.Clear() + ok + } + + "unblock insertion on item removal" in { + val obj : GridInventory = GridInventory(9, 6) + obj.CheckCollisions(23, bullet9mmBox1) mustEqual Success(Nil) + obj += 23 -> bullet9mmBox1 + obj.hasItem(PlanetSideGUID(1)) mustEqual Some(bullet9mmBox1) + obj.CheckCollisions(23, bullet9mmBox1) mustEqual Success(1 :: Nil) + obj -= PlanetSideGUID(1) + obj.hasItem(PlanetSideGUID(1)) mustEqual None + obj.CheckCollisions(23, bullet9mmBox1) mustEqual Success(Nil) + obj.Clear() + ok + } + + "attempt to fit an item" in { + val sampleDef22 = new SimpleItemDefinition(149) + sampleDef22.Tile = InventoryTile.Tile22 + val sampleDef33 = new SimpleItemDefinition(149) + sampleDef33.Tile = InventoryTile.Tile33 + val sampleDef63 = new SimpleItemDefinition(149) + sampleDef63.Tile = InventoryTile.Tile63 + + val obj : GridInventory = GridInventory(9, 9) + obj += 0 -> SimpleItem(PlanetSideGUID(0), sampleDef22) + obj += 20 -> SimpleItem(PlanetSideGUID(1), sampleDef63) + obj += 56 -> SimpleItem(PlanetSideGUID(2), sampleDef33) + obj.Fit(InventoryTile.Tile33) match { + case Some(x) => + x mustEqual 50 + case None => + ko + } + ok + } + + "attempt to fit all the items" in { + val sampleDef1 = new SimpleItemDefinition(149) + sampleDef1.Tile = InventoryTile.Tile22 + val sampleDef2 = new SimpleItemDefinition(149) + sampleDef2.Tile = InventoryTile.Tile33 + val sampleDef3 = new SimpleItemDefinition(149) + sampleDef3.Tile = InventoryTile.Tile42 + val sampleDef4 = new SimpleItemDefinition(149) + sampleDef4.Tile = InventoryTile.Tile63 + + val list : ListBuffer[InventoryItem] = ListBuffer() + list += new InventoryItem(SimpleItem(PlanetSideGUID(0), sampleDef2), -1) + list += new InventoryItem(SimpleItem(PlanetSideGUID(1), sampleDef3), -1) + list += new InventoryItem(SimpleItem(PlanetSideGUID(2), sampleDef1), -1) + list += new InventoryItem(SimpleItem(PlanetSideGUID(3), sampleDef4), -1) + list += new InventoryItem(SimpleItem(PlanetSideGUID(4), sampleDef1), -1) + list += new InventoryItem(SimpleItem(PlanetSideGUID(5), sampleDef4), -1) + list += new InventoryItem(SimpleItem(PlanetSideGUID(6), sampleDef2), -1) + list += new InventoryItem(SimpleItem(PlanetSideGUID(7), sampleDef3), -1) + val obj : GridInventory = GridInventory(9, 9) + + val (elements, out) = GridInventory.recoverInventory(list.toList, obj) + elements.length mustEqual 6 + out.length mustEqual 2 + elements.foreach(item => { + obj.Insert(item.start, item.obj) mustEqual true + }) + out.head.Definition.Tile mustEqual InventoryTile.Tile22 //did not fit + out(1).Definition.Tile mustEqual InventoryTile.Tile22 //did not fit + ok + } + } +} diff --git a/common/src/test/scala/objects/NumberPoolActorTest.scala b/common/src/test/scala/objects/NumberPoolActorTest.scala new file mode 100644 index 000000000..f0f710dd0 --- /dev/null +++ b/common/src/test/scala/objects/NumberPoolActorTest.scala @@ -0,0 +1,73 @@ +// Copyright (c) 2017 PSForever +package objects + +import akka.actor.{ActorSystem, Props} +import akka.pattern.ask +import akka.util.Timeout + +import scala.concurrent.duration._ +import com.typesafe.config.ConfigFactory +import net.psforever.objects.entity.IdentifiableEntity +import net.psforever.objects.guid.NumberPoolHub +import net.psforever.objects.guid.actor._ + +import scala.collection.JavaConverters._ +import net.psforever.objects.guid.pool.ExclusivePool +import net.psforever.objects.guid.selector.RandomSelector +import net.psforever.objects.guid.source.LimitedNumberSource +import org.specs2.mutable.Specification + +import scala.concurrent.Await +import scala.util.{Failure, Try} +import scala.concurrent.ExecutionContext.Implicits.global + +class NumberPoolActorTest extends Specification { + val config : java.util.Map[String,Object] = Map( + "akka.loggers" -> List("akka.event.slf4j.Slf4jLogger").asJava, + "akka.loglevel" -> "INFO", + "akka.logging-filter" -> "akka.event.slf4j.Slf4jLoggingFilter" + ).asJava + implicit val timeout = Timeout(100 milliseconds) + + class TestEntity extends IdentifiableEntity + + "NumberPoolActor" in { + val system : akka.actor.ActorSystem = ActorSystem("ActorTest", ConfigFactory.parseMap(config)) + val pool = new ExclusivePool((25 to 50).toList) + pool.Selector = new RandomSelector + val poolActor = system.actorOf(Props(classOf[NumberPoolActor], pool), name = "poolActor") + val future = (poolActor ? NumberPoolActor.GetAnyNumber()).mapTo[Try[Int]] + future.onComplete(value => { + system.terminate + value.foreach { + case Failure(_) => + ko + case _ => ; + } + }) + Await.result(system.whenTerminated, Duration.Inf) + ok + } + + "NumberPoolAccessorActor" in { + /* + Notes: + Receiver sets resultObject.complete to true and shuts down the ActorSystem. + If Receiver never gets the appropriate message, Await.result will timeout (and the exception will be caught safely). + */ + val system : akka.actor.ActorSystem = ActorSystem("ActorTest", ConfigFactory.parseMap(config)) + val hub = new NumberPoolHub(new LimitedNumberSource(51)) + val pool = hub.AddPool("test", (25 to 50).toList) + pool.Selector = new RandomSelector + val poolActor = system.actorOf(Props(classOf[NumberPoolActor], pool), name = "poolActor") + val poolAccessor = system.actorOf(Props(classOf[NumberPoolAccessorActor], hub, pool, poolActor), name = "accessor") + val resultObject = new ResolutionObject + resultObject.complete mustEqual false + val receiver = system.actorOf(Props(classOf[Receiver], system, resultObject), "receiver") + + val obj : TestEntity = new TestEntity + poolAccessor ! Register(obj, receiver) + try { Await.result(system.whenTerminated, 5 seconds) } catch { case _ : Exception => ; } + resultObject.complete mustEqual true + } +} diff --git a/common/src/test/scala/objects/NumberPoolHubTest.scala b/common/src/test/scala/objects/NumberPoolHubTest.scala new file mode 100644 index 000000000..10738a182 --- /dev/null +++ b/common/src/test/scala/objects/NumberPoolHubTest.scala @@ -0,0 +1,285 @@ +// Copyright (c) 2017 PSForever +package objects + +import net.psforever.objects.entity.IdentifiableEntity +import net.psforever.objects.guid.NumberPoolHub +import net.psforever.objects.guid.selector.RandomSelector +import net.psforever.objects.guid.source.LimitedNumberSource +import net.psforever.packet.game.PlanetSideGUID +import org.specs2.mutable.Specification + +import scala.util.Success + +class NumberPoolHubTest extends Specification { + val numberList = 0 :: 1 :: 2 :: 3 :: 5 :: 8 :: 13 :: 21 :: Nil + val numberList1 = 0 :: 1 :: 2 :: 3 :: 5 :: Nil + val numberList2 = 8 :: 13 :: 21 :: 34 :: Nil + val numberSet1 = numberList1.toSet + val numberSet2 = numberList2.toSet + class EntityTestClass extends IdentifiableEntity + + "NumberPoolHub" should { + "construct" in { + new NumberPoolHub(new LimitedNumberSource(51)) + ok + } + + "get a pool" in { + val obj = new NumberPoolHub(new LimitedNumberSource(51)) + obj.GetPool("generic").isDefined mustEqual true //default pool + } + + "add a pool" in { + val obj = new NumberPoolHub(new LimitedNumberSource(51)) + obj.Numbers.isEmpty mustEqual true + obj.AddPool("fibonacci", numberList) + obj.Numbers.toSet.equals(numberList.toSet) mustEqual true + val pool = obj.GetPool("fibonacci") + pool.isDefined mustEqual true + pool.get.Numbers.equals(numberList) + } + + "enumerate the content of all pools" in { + val obj = new NumberPoolHub(new LimitedNumberSource(51)) + obj.AddPool("fibonacci1", numberList1) + obj.AddPool("fibonacci2", numberList2) + numberSet1.intersect(obj.Numbers.toSet) mustEqual numberSet1 + numberSet2.intersect(obj.Numbers.toSet) mustEqual numberSet2 + obj.Numbers.toSet.diff(numberSet1) mustEqual numberSet2 + } + + "remove a pool" in { + val obj = new NumberPoolHub(new LimitedNumberSource(51)) + obj.Numbers.isEmpty mustEqual true + obj.AddPool("fibonacci", numberList) + obj.Numbers.toSet.equals(numberList.toSet) mustEqual true + obj.RemovePool("fibonacci").toSet.equals(numberList.toSet) mustEqual true + obj.Numbers.isEmpty mustEqual true + obj.GetPool("fibonacci") mustEqual None + } + + "block removing the default 'generic' pool" in { + val obj = new NumberPoolHub(new LimitedNumberSource(51)) + obj.RemovePool("generic") must throwA[IllegalArgumentException] + } + + "block adding pools that use already-included numbers" in { + val obj = new NumberPoolHub(new LimitedNumberSource(51)) + obj.AddPool("fibonacci1", numberList) + val numberList4 = 3 :: 7 :: 21 :: 34 :: 45 :: Nil + obj.AddPool("fibonacci2", numberList4) must throwA[IllegalArgumentException] + } + + "enumerate only the content of all current pools" in { + val obj = new NumberPoolHub(new LimitedNumberSource(51)) + obj.AddPool("fibonacci1", numberList1) + obj.AddPool("fibonacci2", numberList2) + numberSet1.intersect(obj.Numbers.toSet) mustEqual numberSet1 + numberSet2.intersect(obj.Numbers.toSet) mustEqual numberSet2 + obj.RemovePool("fibonacci1") + numberSet1.intersect(obj.Numbers.toSet) mustEqual Set() //no intersect + numberSet2.intersect(obj.Numbers.toSet) mustEqual numberSet2 + } + + "register an object to a pool" in { + val hub = new NumberPoolHub(new LimitedNumberSource(51)) + hub.AddPool("fibonacci", numberList) + val obj = new EntityTestClass() + obj.GUID must throwA[Exception] + hub.register(obj, "fibonacci") match { + case Success(number) => + obj.GUID mustEqual PlanetSideGUID(number) + case _ => + ko + } + } + + "lookup a registered object" in { + val hub = new NumberPoolHub(new LimitedNumberSource(51)) + hub.AddPool("fibonacci", numberList) + val obj = new EntityTestClass() + hub.register(obj, "fibonacci") match { + case Success(number) => + val objFromNumber = hub(number) + objFromNumber mustEqual Some(obj) + case _ => + ko + } + } + + "lookup the pool of a(n unassigned) number" in { + val hub = new NumberPoolHub(new LimitedNumberSource(51)) + hub.AddPool("fibonacci1", numberList1) + hub.AddPool("fibonacci2", numberList2) + hub.WhichPool(13) mustEqual Some("fibonacci2") + } + + "lookup the pool of a registered object" in { + val hub = new NumberPoolHub(new LimitedNumberSource(51)) + hub.AddPool("fibonacci", numberList1) + val obj = new EntityTestClass() + hub.register(obj, "fibonacci") + hub.WhichPool(obj) mustEqual Some("fibonacci") + } + + "register an object to a specific, unused number; it is assigned to pool 'generic'" in { + val hub = new NumberPoolHub(new LimitedNumberSource(51)) + hub.AddPool("fibonacci", numberList1) + val obj = new EntityTestClass() + obj.GUID must throwA[Exception] + hub.register(obj, 44) match { + case Success(number) => + obj.GUID mustEqual PlanetSideGUID(number) + hub.WhichPool(obj) mustEqual Some("generic") + case _ => + ko + } + } + + "register an object to a specific, pooled number" in { + val hub = new NumberPoolHub(new LimitedNumberSource(51)) + hub.AddPool("fibonacci", numberList) + val obj = new EntityTestClass() + obj.GUID must throwA[Exception] + hub.register(obj, 13) match { + case Success(number) => + obj.GUID mustEqual PlanetSideGUID(number) + hub.WhichPool(obj) mustEqual Some("fibonacci") + case _ => + ko + } + } + + "register an object without extra specifications; it is assigned to pool 'generic'" in { + val hub = new NumberPoolHub(new LimitedNumberSource(51)) + val obj = new EntityTestClass() + hub.register(obj) + hub.WhichPool(obj) mustEqual Some("generic") + } + + "unregister an object" in { + val hub = new NumberPoolHub(new LimitedNumberSource(51)) + hub.AddPool("fibonacci", numberList) + val obj = new EntityTestClass() + hub.register(obj, "fibonacci") + hub.WhichPool(obj) mustEqual Some("fibonacci") + try { obj.GUID } catch { case _ : Exception => ko } //passes + + hub.unregister(obj) + hub.WhichPool(obj) mustEqual None + obj.GUID must throwA[Exception] //fails + } + + "not register an object to a different pool" in { + val hub = new NumberPoolHub(new LimitedNumberSource(51)) + hub.AddPool("fibonacci1", numberList1) + hub.AddPool("fibonacci2", numberList2) + val obj = new EntityTestClass() + hub.register(obj, "fibonacci1") + hub.register(obj, "fibonacci2") + hub.WhichPool(obj).contains("fibonacci1") mustEqual true + } + + "fail to unregister an object that is not registered to this hub" in { + val hub1 = new NumberPoolHub(new LimitedNumberSource(51)) + val hub2 = new NumberPoolHub(new LimitedNumberSource(51)) + hub1.AddPool("fibonacci", numberList) + hub2.AddPool("fibonacci", numberList) + val obj = new EntityTestClass() + hub1.register(obj, "fibonacci") + hub2.unregister(obj) must throwA[Exception] + } + + "pre-register a specific, unused number" in { + val hub = new NumberPoolHub(new LimitedNumberSource(51)) + hub.register(13) match { + case Success(_) => + ok + case _ => + ko + } + } + + "pre-register a specific, pooled number" in { + val hub = new NumberPoolHub(new LimitedNumberSource(51)) + hub.AddPool("fibonacci", numberList) + hub.register(13) match { + case Success(key) => + key.GUID mustEqual 13 + case _ => + ko + } + } + + "pre-register a number from a known pool" in { + val hub = new NumberPoolHub(new LimitedNumberSource(51)) + hub.AddPool("fibonacci", numberList).Selector = new RandomSelector + hub.register("fibonacci") match { + case Success(key) => + numberList.contains(key.GUID) mustEqual true + case _ => + ko + } + } + + "unregister a number" in { + val hub = new NumberPoolHub(new LimitedNumberSource(51)) + hub.AddPool("fibonacci", numberList).Selector = new RandomSelector //leave this tagged on + val obj = new EntityTestClass() + hub.register(13) match { + case Success(key) => + key.Object = obj + case _ => + ko + } + hub.WhichPool(obj) mustEqual Some("fibonacci") + hub.unregister(13) match { + case Success(thing) => + thing mustEqual Some(obj) + thing.get.GUID must throwA[Exception] + case _ => + ko + } + } + + "not affect the hidden restricted pool by adding a new pool" in { + val src = new LimitedNumberSource(51) + src.Restrict(4) + src.Restrict(8) //in fibonacci + src.Restrict(10) + src.Restrict(12) + val hub = new NumberPoolHub(src) + hub.AddPool("fibonacci", numberList) must throwA[IllegalArgumentException] + } + + "not register an object to a number belonging to the restricted pool" in { + val src = new LimitedNumberSource(51) + src.Restrict(4) + val hub = new NumberPoolHub(src) + val obj = new EntityTestClass() + hub.register(obj, 4).isFailure mustEqual true + } + + "not register an object to the restricted pool directly" in { + val src = new LimitedNumberSource(51) +// src.Restrict(4) + val hub = new NumberPoolHub(src) + val obj = new EntityTestClass() + hub.register(obj, "").isFailure mustEqual true //the empty string represents the restricted pool + } + + "not register a number belonging to the restricted pool" in { + val src = new LimitedNumberSource(51) + src.Restrict(4) + val hub = new NumberPoolHub(src) + hub.register(4).isFailure mustEqual true + } + + "not unregister a number belonging to the restricted pool" in { + val src = new LimitedNumberSource(51) + src.Restrict(4) + val hub = new NumberPoolHub(src) + hub.unregister(4).isFailure mustEqual true + } + } +} diff --git a/common/src/test/scala/objects/NumberPoolTest.scala b/common/src/test/scala/objects/NumberPoolTest.scala new file mode 100644 index 000000000..a8bbda2b4 --- /dev/null +++ b/common/src/test/scala/objects/NumberPoolTest.scala @@ -0,0 +1,194 @@ +// Copyright (c) 2017 PSForever +package objects + +import net.psforever.objects.guid.pool.{ExclusivePool, GenericPool, SimplePool} +import net.psforever.objects.guid.selector.SpecificSelector +import org.specs2.mutable.Specification + +import scala.collection.mutable +import scala.collection.mutable.ListBuffer +import scala.util.Success + +class NumberPoolTest extends Specification { + "SimplePool" should { + "construct" in { + new SimplePool(0 :: 1 :: 2 :: Nil) + ok + } + + "get a number" in { + val obj = new SimplePool((0 to 10).toList) + obj.Get() match { + case Success(number) => + (-1 < number && number < 11) mustEqual true + case _ => + ko + } + } + + "return a number" in { + //returning a number for a SimplePool is actually just a way of checking that the number is in the "pool" at all + val obj = new SimplePool((0 to 10).toList) + obj.Get() match { + case Success(number) => + obj.Return(number) mustEqual true + obj.Return(11) mustEqual false + obj.Return(number) mustEqual true + case _ => + ko + } + } + + "numbers remain available" in { + val obj = new SimplePool((0 to 10).toList) + obj.Selector = new SpecificSelector + obj.Selector.asInstanceOf[SpecificSelector].SelectionIndex = 8 + obj.Get() mustEqual Success(8) + obj.Get() mustEqual Success(8) //compare to how SpecificSelector works otherwise - it would be an invalid return + } + } + + "ExclusivePool" should { + "construct" in { + new ExclusivePool(0 :: 1 :: 2 :: Nil) + ok + } + + "get a number" in { + val obj = new ExclusivePool((0 to 10).toList) + obj.Get() match { + case Success(number) => + (-1 < number && number < 11) mustEqual true + case _ => + ko + } + } + + "get all the numbers" in { + val range = 0 to 10 + val obj = new ExclusivePool((0 to 10).toList) + range.foreach(_ => { + obj.Get() match { + case Success(number) => + (-1 < number && number < 11) mustEqual true + case _ => + ko + } + }) + ok + } + + "return a number" in { + val obj = new ExclusivePool((0 to 10).toList) + obj.Get() match { + case Success(number) => + try { obj.Return(number) mustEqual true } catch { case _ : Exception => ko } + case _ => + ko + } + } + + "return all the numbers" in { + val range = 0 to 10 + val obj = new ExclusivePool((0 to 10).toList) + val list : ListBuffer[Int] = ListBuffer[Int]() + range.foreach(_ => { + obj.Get() match { + case Success(number) => + list += number + case _ => + } + }) + list.foreach(number => { + try { obj.Return(number) mustEqual true } catch { case _ : Exception => ko } + }) + ok + } + } + + "GenericPool" should { + "construct" in { + new GenericPool(mutable.LongMap[String](), 11) + ok + } + + "get a provided number" in { + val map = mutable.LongMap[String]() + val obj = new GenericPool(map, 11) + obj.Numbers.isEmpty mustEqual true + obj.Selector.asInstanceOf[SpecificSelector].SelectionIndex = 5 + obj.Get() match { + case Success(number) => + number mustEqual 5 + map.contains(5) mustEqual true + map(5) mustEqual "generic" + obj.Numbers.contains(5) mustEqual true + case _ => + ko + } + } + + "return a number" in { + val map = mutable.LongMap[String]() + val obj = new GenericPool(map, 11) + obj.Selector.asInstanceOf[SpecificSelector].SelectionIndex = 5 + obj.Get() + map.get(5) mustEqual Some("generic") + obj.Numbers.contains(5) mustEqual true + obj.Return(5) mustEqual true + map.get(5) mustEqual None + obj.Numbers.isEmpty mustEqual true + } + + "block on numbers that are already defined" in { + val map = mutable.LongMap[String]() + map += 5L -> "test" //5 is defined + val obj = new GenericPool(map, 11) + obj.Numbers.isEmpty mustEqual true + obj.Selector.asInstanceOf[SpecificSelector].SelectionIndex = 5 //5 is requested + obj.Get() match { + case Success(_) => + ko + case _ => + obj.Numbers.isEmpty mustEqual true + } + } + + "get a free number on own if none provided" in { + val map = mutable.LongMap[String]() + val obj = new GenericPool(map, 11) + obj.Get() match { + case Success(number) => + number mustEqual 5 + case _ => + ko + } + } + + "get a free number that is not already defined" in { + val map = mutable.LongMap[String]() + map += 5L -> "test" //5 is defined; think, -1 :: 5 :: 11 + val obj = new GenericPool(map, 11) + obj.Get() match { + case Success(number) => + number mustEqual 2 // think, -1 :: 2 :: 5 :: 11 + case _ => ko + } + + } + + "get a free number that represents half of the largest delta" in { + val map = mutable.LongMap[String]() + map += 5L -> "test" //5 is defined; think, -1 :: 5 :: 11 + map += 4L -> "test" //4 is defined; think, -1 :: 4 :: 5 :: 11 + val obj = new GenericPool(map, 11) + obj.Get() match { + case Success(number) => + number mustEqual 8 // think, -1 :: 4 :: 5 :: 8 :: 11 + case _ => + ko + } + } + } +} + diff --git a/common/src/test/scala/objects/NumberSelectorTest.scala b/common/src/test/scala/objects/NumberSelectorTest.scala new file mode 100644 index 000000000..463f96548 --- /dev/null +++ b/common/src/test/scala/objects/NumberSelectorTest.scala @@ -0,0 +1,326 @@ +// Copyright (c) 2017 PSForever +package objects + +import net.psforever.objects.guid.selector.{RandomSequenceSelector, _} +import org.specs2.mutable.Specification + +class NumberSelectorTest extends Specification { + def randArrayGen(n : Int = 26) : Array[Int] = { + val obj = Array.ofDim[Int](n) + (0 to 25).foreach(x => { obj(x) = x } ) + obj + } + + "RandomSequenceSelector" should { + "construct" in { + new RandomSequenceSelector + ok + } + + "get a number" in { + val obj = new RandomSequenceSelector + obj.Get(randArrayGen()) mustNotEqual -1 + } + + "return a number" in { + val obj = new RandomSequenceSelector + val ary = randArrayGen() + val number = obj.Get(ary) + number mustNotEqual -1 + ary.head mustEqual -1 //regardless of which number we actually got, the head of the array is now -1 + obj.Return(number, ary) + ary.head mustEqual number //the returned number is at the head of the array + } + + "get all numbers" in { + val n = 26 + val obj = new RandomSequenceSelector + val ary = randArrayGen(n) + (0 until n).foreach(_ => { obj.Get(ary) mustNotEqual -1 } ) + ok + } + + "return all numbers" in { + val n = 26 + val obj = new RandomSequenceSelector + val ary1 = randArrayGen(n) + val ary2 = randArrayGen(n) + (0 until n).foreach(index => { ary2(index) = obj.Get(ary1) } ) //move numbers from ary1 to ary2 + ary2.toSet.diff(ary1.toSet).size mustEqual n //no numbers between ary2 and ary1 match + (0 until n).foreach(index => { obj.Return(ary2(index), ary1) mustEqual true } ) //return numbers from ary2 to ary1 + ary2.toSet.diff(ary1.toSet).size mustEqual 0 //no difference in the content between ary2 and ary1 + } + + "gets invalid index when exhausted" in { + val n = 26 + val obj = new RandomSequenceSelector + val ary = randArrayGen(n) + (0 until n).foreach(_ => { obj.Get(ary) mustNotEqual -1 } ) + obj.Get(ary) mustEqual -1 + } + + "format an array" in { + val ary = Array[Int](1, -1, 5, 3, -1, 2) + (new RandomSequenceSelector).Format(ary) + ary mustEqual Array[Int](-1, -1, 1, 5, 3, 2) + } + } + + "RandomSelector" should { + "construct" in { + new RandomSelector + ok + } + + "get a number" in { + val obj = new RandomSelector + obj.Get(randArrayGen()) mustNotEqual -1 + } + + "return a number" in { + val obj = new RandomSelector + val ary = randArrayGen() + val number = obj.Get(ary) + number mustNotEqual -1 + ary.head mustEqual -1 //regardless of which number we actually got, the head of the array is now -1 + obj.Return(number, ary) + ary.head mustEqual number //the returned number is at the head of the array + } + + "get all numbers" in { + val n = 26 + val obj = new RandomSelector + val ary = randArrayGen(n) + (0 until n).foreach(_ => { obj.Get(ary) mustNotEqual -1 } ) + ok + } + + "return all numbers" in { + val n = 26 + val obj = new RandomSelector + val ary1 = randArrayGen(n) + val ary2 = randArrayGen(n) + (0 until n).foreach(index => { ary2(index) = obj.Get(ary1) } ) //move numbers from ary1 to ary2 + ary2.toSet.diff(ary1.toSet).size mustEqual n //no numbers between ary2 and ary1 match + (0 until n).foreach(index => { obj.Return(ary2(index), ary1) mustEqual true } ) //return numbers from ary2 to ary1 + ary2.toSet.diff(ary1.toSet).size mustEqual 0 //no difference in the content between ary2 and ary1 + } + + "gets invalid index when exhausted" in { + val n = 26 + val obj = new RandomSelector + val ary = randArrayGen(n) + (0 until n).foreach(_ => { obj.Get(ary) mustNotEqual -1 } ) + obj.Get(ary) mustEqual -1 + } + + "format an array" in { + val ary = Array[Int](1, -1, 5, 3, -1, 2) + (new RandomSelector).Format(ary) + ary mustEqual Array[Int](-1, -1, 1, 5, 3, 2) + } + } + + "StrictInOrderSelector" should { + "construct" in { + new StrictInOrderSelector + ok + } + + "get a number" in { + val obj = new StrictInOrderSelector + obj.Get(randArrayGen()) mustNotEqual -1 + } + + "return a number" in { + val obj = new StrictInOrderSelector + val ary = randArrayGen() + val number = obj.Get(ary) + number mustNotEqual -1 + ary.head mustEqual -1 //regardless of which number we actually got, the head of the array is now -1 + obj.Return(number, ary) + ary.head mustEqual number //the returned number is at the head of the array + } + + "get all numbers" in { + val n = 26 + val obj = new StrictInOrderSelector + val ary = randArrayGen() + (0 until n).foreach(_ => { obj.Get(ary) mustNotEqual -1 } ) + ok + } + + "return all numbers" in { + val n = 26 + val obj = new StrictInOrderSelector + val ary1 = randArrayGen(n) + val ary2 = randArrayGen(n) + (0 until n).foreach(index => { ary2(index) = obj.Get(ary1) } ) //move numbers from ary1 to ary2 + ary2.toSet.diff(ary1.toSet).size mustEqual n //no numbers between ary2 and ary1 match + (0 until n).foreach(index => { obj.Return(ary2(index), ary1) mustEqual true } ) //return numbers from ary2 to ary1 + ary2.toSet.diff(ary1.toSet).size mustEqual 0 //no difference in the content between ary2 and ary1 + } + + "gets invalid index when exhausted" in { + val n = 26 + val obj = new StrictInOrderSelector + val ary = randArrayGen(n) + (0 until n).foreach(_ => { obj.Get(ary) mustNotEqual -1 } ) + obj.Get(ary) mustEqual -1 + } + + "wait until number is available" in { + val n = 26 + val obj = new StrictInOrderSelector + val ary = randArrayGen(n) + (0 until n).foreach(_ => { obj.Get(ary) mustNotEqual -1 } ) + obj.Get(ary) mustEqual -1 + obj.Return(1, ary) //return a number that isn't the one StrictOrder is waiting on + obj.Get(ary) mustEqual -1 + obj.Return(0, ary) //return the number StrictOrder wants + obj.Get(ary) mustEqual 0 + obj.Get(ary) mustEqual 1 + } + + "format an array" in { + val ary = Array[Int](1, -1, 5, 3, -1, 2) + (new StrictInOrderSelector).Format(ary) + ary mustEqual Array[Int](-1, 1, 2, 3, -1, 5) + } + } + + "OpportunisticSelector" should { + "construct" in { + new OpportunisticSelector + ok + } + + "get a number" in { + val obj = new OpportunisticSelector + obj.Get(randArrayGen()) mustNotEqual -1 + } + + "return a number" in { + val obj = new OpportunisticSelector + val ary = randArrayGen() + val number = obj.Get(ary) + number mustNotEqual -1 + ary.head mustEqual -1 //regardless of which number we actually got, the head of the array is now -1 + obj.Return(number, ary) + ary.head mustEqual number //the returned number is at the head of the array + } + + "get all numbers" in { + val obj = new OpportunisticSelector + val ary = randArrayGen() + (0 to 25).foreach(_ => { obj.Get(ary) mustNotEqual -1 } ) + ok + } + + "return all numbers" in { + val n = 26 + val obj = new OpportunisticSelector + val ary1 = randArrayGen(n) + val ary2 = randArrayGen(n) + (0 until n).foreach(index => { ary2(index) = obj.Get(ary1) } ) //move numbers from ary1 to ary2 + ary2.toSet.diff(ary1.toSet).size mustEqual n //no numbers between ary2 and ary1 match + (0 until n).foreach(index => { obj.Return(ary2(index), ary1) mustEqual true } ) //return numbers from ary2 to ary1 + ary2.toSet.diff(ary1.toSet).size mustEqual 0 //no difference in the content between ary2 and ary1 + } + + "gets invalid index when exhausted" in { + val n = 26 + val obj = new OpportunisticSelector + val ary = randArrayGen(n) + (0 until n).foreach(_ => { obj.Get(ary) mustNotEqual -1 } ) + obj.Get(ary) mustEqual -1 + } + + "format an array" in { + val ary = Array[Int](1, -1, 5, 3, -1, 2) + (new OpportunisticSelector).Format(ary) + ary mustEqual Array[Int](-1, -1, 1, 5, 3, 2) + } + } + + "SpecificSelector" should { + "construct" in { + new SpecificSelector + ok + } + + "get a number" in { + val obj = new SpecificSelector + val ary = randArrayGen() + obj.SelectionIndex = 5 + obj.Get(ary) mustEqual 5 + obj.Get(ary) mustEqual -1 //now that 5 has been selected, the selector will only get a -1 from that position + } + + "return a number" in { + val obj = new SpecificSelector + val ary = randArrayGen() + obj.SelectionIndex = 5 + val number = obj.Get(ary) + number mustEqual 5 + obj.Get(ary) mustEqual -1 + obj.Return(number, ary) + obj.Get(ary) mustEqual number //the returned number is at the head of the array + } + + "return a number (2)" in { + val obj = new SpecificSelector + val ary = randArrayGen() + obj.SelectionIndex = 5 + val number = obj.Get(ary) + number mustEqual 5 + obj.Get(ary) mustEqual -1 + ary(number) mustEqual -1 + + obj.SelectionIndex = 10 //even if we move the selection index, the number will return to its last position + obj.Return(number, ary) + ary(number) mustEqual number //the returned number at the original index + obj.Get(ary) mustEqual 10 //of course, with the selection index changed, we will not get the same position next time + } + + "get all numbers" in { + val n = 26 + val obj = new SpecificSelector + val ary = randArrayGen(n) + (0 until n).foreach(i => { + obj.SelectionIndex = i + obj.Get(ary) mustEqual i + }) + ok + } + + "return all numbers" in { + val n = 26 + val obj = new SpecificSelector + val ary1 = randArrayGen(n) + val ary2 = randArrayGen(n) + (0 until n).foreach(index => { + obj.SelectionIndex = index + ary2(index) = obj.Get(ary1) + }) //move numbers from ary1 to ary2 + ary2.toSet.diff(ary1.toSet).size mustEqual n //no numbers between ary2 and ary1 match + (0 until n).foreach(index => { obj.Return(ary2(index), ary1) mustEqual true } ) //return numbers from ary2 to ary1 + ary2.toSet.diff(ary1.toSet).size mustEqual 0 //no difference in the content between ary2 and ary1 + } + + "gets invalid index when exhausted" in { + val obj = new SpecificSelector + val ary = randArrayGen() + obj.SelectionIndex = 5 + obj.Get(ary) mustEqual 5 + obj.Get(ary) mustEqual -1 //yes, it really is that simple + } + + "format an array" in { + val ary = Array[Int](1, -1, 5, 3, -1, 2) + (new SpecificSelector).Format(ary) + ary mustEqual Array[Int](-1, 1, 2, 3, -1, 5) + } + } +} + diff --git a/common/src/test/scala/objects/NumberSourceTest.scala b/common/src/test/scala/objects/NumberSourceTest.scala new file mode 100644 index 000000000..dc4660617 --- /dev/null +++ b/common/src/test/scala/objects/NumberSourceTest.scala @@ -0,0 +1,359 @@ +// Copyright (c) 2017 PSForever +package objects + +import net.psforever.objects.guid.key.{LoanedKey, SecureKey} +import net.psforever.objects.guid.AvailabilityPolicy +import org.specs2.mutable.Specification + +class NumberSourceTest extends Specification { + import net.psforever.objects.entity.IdentifiableEntity + private class TestClass extends IdentifiableEntity + + "MaxNumberSource" should { + import net.psforever.objects.guid.source.MaxNumberSource + "construct" in { + val obj = MaxNumberSource() + obj.Size mustEqual Int.MaxValue + obj.CountAvailable mustEqual Int.MaxValue + obj.CountUsed mustEqual 0 + } + + "get a number" in { + val obj = MaxNumberSource() + val result : Option[LoanedKey] = obj.Available(5) + result.isDefined mustEqual true + result.get.GUID mustEqual 5 + result.get.Policy mustEqual AvailabilityPolicy.Leased + result.get.Object mustEqual None + obj.Size mustEqual Int.MaxValue + obj.CountAvailable mustEqual Int.MaxValue - 1 + obj.CountUsed mustEqual 1 + } + + "assign the number" in { + val obj = MaxNumberSource() + val result : Option[LoanedKey] = obj.Available(5) + result.isDefined mustEqual true + result.get.Object = new TestClass() + ok + } + + "return a number (unused)" in { + val obj = MaxNumberSource() + val result : Option[LoanedKey] = obj.Available(5) + result.isDefined mustEqual true + result.get.GUID mustEqual 5 + obj.CountUsed mustEqual 1 + val ret = obj.Return(result.get) + ret mustEqual None + obj.CountUsed mustEqual 0 + } + + "return a number (assigned)" in { + val obj = MaxNumberSource() + val test = new TestClass() + val result : Option[LoanedKey] = obj.Available(5) + result.isDefined mustEqual true + result.get.GUID mustEqual 5 + result.get.Object = test + obj.CountUsed mustEqual 1 + val ret = obj.Return(result.get) + ret mustEqual Some(test) + obj.CountUsed mustEqual 0 + } + + "restrict a number (unassigned)" in { + val obj = MaxNumberSource() + val result : Option[LoanedKey] = obj.Restrict(5) + result.isDefined mustEqual true + result.get.GUID mustEqual 5 + result.get.Policy mustEqual AvailabilityPolicy.Restricted + result.get.Object mustEqual None + } + + "restrict a number (assigned + multiple assignments)" in { + val obj = MaxNumberSource() + val test1 = new TestClass() + val test2 = new TestClass() + val result : Option[LoanedKey] = obj.Restrict(5) + result.get.GUID mustEqual 5 + result.get.Policy mustEqual AvailabilityPolicy.Restricted + result.get.Object mustEqual None + result.get.Object = None //assignment 1 + result.get.Object mustEqual None //still unassigned + result.get.Object = test1 //assignment 2 + result.get.Object mustEqual Some(test1) + result.get.Object = test2 //assignment 3 + result.get.Object mustEqual Some(test1) //same as above + } + + "return a restricted number (correctly fail)" in { + val obj = MaxNumberSource() + val test = new TestClass() + val result : Option[LoanedKey] = obj.Restrict(5) + result.get.GUID mustEqual 5 + result.get.Policy mustEqual AvailabilityPolicy.Restricted + result.get.Object = test + + obj.Return(5) + val result2 : Option[SecureKey] = obj.Get(5) + result2.get.GUID mustEqual 5 + result2.get.Policy mustEqual AvailabilityPolicy.Restricted + result2.get.Object mustEqual Some(test) + } + + "restrict a previously-assigned number" in { + val obj = MaxNumberSource() + val test = new TestClass() + val result1 : Option[LoanedKey] = obj.Available(5) + result1.isDefined mustEqual true + result1.get.Policy mustEqual AvailabilityPolicy.Leased + result1.get.Object = test + val result2 : Option[LoanedKey] = obj.Restrict(5) + result2.isDefined mustEqual true + result2.get.Policy mustEqual AvailabilityPolicy.Restricted + result2.get.Object mustEqual Some(test) + } + + "check a number (not previously gotten)" in { + val obj = MaxNumberSource() + val result2 : Option[SecureKey] = obj.Get(5) + result2.get.GUID mustEqual 5 + result2.get.Policy mustEqual AvailabilityPolicy.Available + result2.get.Object mustEqual None + } + + "check a number (previously gotten)" in { + val obj = MaxNumberSource() + val result : Option[LoanedKey] = obj.Available(5) + result.isDefined mustEqual true + result.get.GUID mustEqual 5 + result.get.Policy mustEqual AvailabilityPolicy.Leased + result.get.Object mustEqual None + val result2 : Option[SecureKey] = obj.Get(5) + result2.get.GUID mustEqual 5 + result2.get.Policy mustEqual AvailabilityPolicy.Leased + result2.get.Object mustEqual None + } + + "check a number (assigned)" in { + val obj = MaxNumberSource() + val result : Option[LoanedKey] = obj.Available(5) + result.isDefined mustEqual true + result.get.GUID mustEqual 5 + result.get.Policy mustEqual AvailabilityPolicy.Leased + result.get.Object = new TestClass() + val result2 : Option[SecureKey] = obj.Get(5) + result2.get.GUID mustEqual 5 + result2.get.Policy mustEqual AvailabilityPolicy.Leased + result2.get.Object mustEqual result.get.Object + } + + "check a number (assigned and returned)" in { + val obj = MaxNumberSource() + val test = new TestClass() + val result : Option[LoanedKey] = obj.Available(5) + result.get.Policy mustEqual AvailabilityPolicy.Leased + result.get.Object = test + val result2 : Option[SecureKey] = obj.Get(5) + result2.get.Policy mustEqual AvailabilityPolicy.Leased + result2.get.Object.get === test + obj.Return(5) mustEqual Some(test) + val result3 : Option[SecureKey] = obj.Get(5) + result3.get.Policy mustEqual AvailabilityPolicy.Available + result3.get.Object mustEqual None + } + + "clear" in { + val obj = MaxNumberSource() + val test1 = new TestClass() + val test2 = new TestClass() + obj.Available(5) //no assignment + obj.Available(10).get.Object = test1 + obj.Available(15).get.Object = test2 + obj.Restrict(15) + obj.Restrict(20).get.Object = test1 + obj.CountUsed mustEqual 4 + + val list : List[IdentifiableEntity] = obj.Clear() + obj.CountUsed mustEqual 0 + list.size mustEqual 3 + list.count(obj => { obj == test1 }) mustEqual 2 + list.count(obj => { obj == test2 }) mustEqual 1 + } + } + + "LimitedNumberSource" should { + import net.psforever.objects.guid.source.LimitedNumberSource + "construct" in { + val obj = LimitedNumberSource(25) + obj.Size mustEqual 26 + obj.CountAvailable mustEqual 26 + obj.CountUsed mustEqual 0 + } + + "get a number" in { + val obj = LimitedNumberSource(25) + val result : Option[LoanedKey] = obj.Available(5) + result.isDefined mustEqual true + result.get.GUID mustEqual 5 + result.get.Policy mustEqual AvailabilityPolicy.Leased + result.get.Object mustEqual None + obj.Size mustEqual 26 + obj.CountAvailable mustEqual 25 + obj.CountUsed mustEqual 1 + } + + "assign the number" in { + val obj = LimitedNumberSource(25) + val result : Option[LoanedKey] = obj.Available(5) + result.isDefined mustEqual true + result.get.Object = new TestClass() + ok + } + + "return a number (unused)" in { + val obj = LimitedNumberSource(25) + val result : Option[LoanedKey] = obj.Available(5) + result.isDefined mustEqual true + result.get.GUID mustEqual 5 + obj.CountUsed mustEqual 1 + val ret = obj.Return(result.get) + ret mustEqual None + obj.CountUsed mustEqual 0 + } + + "return a number (assigned)" in { + val obj = LimitedNumberSource(25) + val test = new TestClass() + val result : Option[LoanedKey] = obj.Available(5) + result.isDefined mustEqual true + result.get.GUID mustEqual 5 + result.get.Object = test + obj.CountUsed mustEqual 1 + val ret = obj.Return(result.get) + ret mustEqual Some(test) + obj.CountUsed mustEqual 0 + } + + "restrict a number (unassigned)" in { + val obj = LimitedNumberSource(25) + val result : Option[LoanedKey] = obj.Restrict(5) + result.isDefined mustEqual true + result.get.GUID mustEqual 5 + result.get.Policy mustEqual AvailabilityPolicy.Restricted + result.get.Object mustEqual None + } + + "restrict a number (assigned + multiple assignments)" in { + val obj = LimitedNumberSource(25) + val test1 = new TestClass() + val test2 = new TestClass() + val result : Option[LoanedKey] = obj.Restrict(5) + result.get.GUID mustEqual 5 + result.get.Policy mustEqual AvailabilityPolicy.Restricted + result.get.Object mustEqual None + result.get.Object = None //assignment 1 + result.get.Object mustEqual None //still unassigned + result.get.Object = test1 //assignment 2 + result.get.Object mustEqual Some(test1) + result.get.Object = test2 //assignment 3 + result.get.Object mustEqual Some(test1) //same as above + } + + "return a restricted number (correctly fail)" in { + val obj = LimitedNumberSource(25) + val test = new TestClass() + val result : Option[LoanedKey] = obj.Restrict(5) + result.get.GUID mustEqual 5 + result.get.Policy mustEqual AvailabilityPolicy.Restricted + result.get.Object = test + + obj.Return(5) + val result2 : Option[SecureKey] = obj.Get(5) + result2.get.GUID mustEqual 5 + result2.get.Policy mustEqual AvailabilityPolicy.Restricted + result2.get.Object mustEqual Some(test) + } + + "restrict a previously-assigned number" in { + val obj = LimitedNumberSource(25) + val test = new TestClass() + val result1 : Option[LoanedKey] = obj.Available(5) + result1.isDefined mustEqual true + result1.get.Policy mustEqual AvailabilityPolicy.Leased + result1.get.Object = test + val result2 : Option[LoanedKey] = obj.Restrict(5) + result2.isDefined mustEqual true + result2.get.Policy mustEqual AvailabilityPolicy.Restricted + result2.get.Object mustEqual Some(test) + } + + "check a number (not previously gotten)" in { + val obj = LimitedNumberSource(25) + val result2 : Option[SecureKey] = obj.Get(5) + result2.get.GUID mustEqual 5 + result2.get.Policy mustEqual AvailabilityPolicy.Available + result2.get.Object mustEqual None + } + + "check a number (previously gotten)" in { + val obj = LimitedNumberSource(25) + val result : Option[LoanedKey] = obj.Available(5) + result.isDefined mustEqual true + result.get.GUID mustEqual 5 + result.get.Policy mustEqual AvailabilityPolicy.Leased + result.get.Object mustEqual None + val result2 : Option[SecureKey] = obj.Get(5) + result2.get.GUID mustEqual 5 + result2.get.Policy mustEqual AvailabilityPolicy.Leased + result2.get.Object mustEqual None + } + + "check a number (assigned)" in { + val obj = LimitedNumberSource(25) + val result : Option[LoanedKey] = obj.Available(5) + result.isDefined mustEqual true + result.get.GUID mustEqual 5 + result.get.Policy mustEqual AvailabilityPolicy.Leased + result.get.Object = new TestClass() + val result2 : Option[SecureKey] = obj.Get(5) + result2.get.GUID mustEqual 5 + result2.get.Policy mustEqual AvailabilityPolicy.Leased + result2.get.Object mustEqual result.get.Object + } + + "check a number (assigned and returned)" in { + val obj = LimitedNumberSource(25) + val test = new TestClass() + val result : Option[LoanedKey] = obj.Available(5) + result.get.Policy mustEqual AvailabilityPolicy.Leased + result.get.Object = test + val result2 : Option[SecureKey] = obj.Get(5) + result2.get.Policy mustEqual AvailabilityPolicy.Leased + result2.get.Object.get mustEqual test + obj.Return(5) mustEqual Some(test) + val result3 : Option[SecureKey] = obj.Get(5) + result3.get.Policy mustEqual AvailabilityPolicy.Available + result3.get.Object mustEqual None + } + + "clear" in { + val obj = LimitedNumberSource(25) + val test1 = new TestClass() + val test2 = new TestClass() + obj.Available(5) //no assignment + obj.Available(10).get.Object = test1 + obj.Available(15).get.Object = test2 + obj.Restrict(15) + obj.Restrict(20).get.Object = test1 + obj.CountUsed mustEqual 4 + + val list : List[IdentifiableEntity] = obj.Clear() + obj.CountUsed mustEqual 0 + list.size mustEqual 3 + list.count(obj => obj == test1) mustEqual 2 + list.count(obj => obj == test2) mustEqual 1 + } + } +} diff --git a/common/src/test/scala/objects/PlayerTest.scala b/common/src/test/scala/objects/PlayerTest.scala new file mode 100644 index 000000000..fd2d3fc5d --- /dev/null +++ b/common/src/test/scala/objects/PlayerTest.scala @@ -0,0 +1,153 @@ +// Copyright (c) 2017 PSForever +package objects + +import net.psforever.objects.{Implant, Player, SimpleItem} +import net.psforever.objects.definition.{ImplantDefinition, SimpleItemDefinition} +import net.psforever.objects.equipment.EquipmentSize +import net.psforever.types.{CharacterGender, ExoSuitType, ImplantType, PlanetSideEmpire} +import org.specs2.mutable._ + +class PlayerTest extends Specification { + "construct" in { + val obj = new Player("Chord", PlanetSideEmpire.TR, CharacterGender.Male, 0, 5) + obj.isAlive mustEqual false + } + + "(re)spawn" in { + val obj = new Player("Chord", PlanetSideEmpire.TR, CharacterGender.Male, 0, 5) + obj.isAlive mustEqual false + obj.Health mustEqual 0 + obj.Stamina mustEqual 0 + obj.Armor mustEqual 0 + obj.Spawn + obj.isAlive mustEqual true + obj.Health mustEqual obj.MaxHealth + obj.Stamina mustEqual obj.MaxStamina + obj.Armor mustEqual obj.MaxArmor + } + + "init (Standard Exo-Suit)" in { + val obj = new Player("Chord", PlanetSideEmpire.TR, CharacterGender.Male, 0, 5) + obj.ExoSuit mustEqual ExoSuitType.Standard + obj.Slot(0).Size mustEqual EquipmentSize.Pistol + obj.Slot(1).Size mustEqual EquipmentSize.Blocked + obj.Slot(2).Size mustEqual EquipmentSize.Rifle + obj.Slot(3).Size mustEqual EquipmentSize.Blocked + obj.Slot(4).Size mustEqual EquipmentSize.Melee + obj.Inventory.Width mustEqual 9 + obj.Inventory.Height mustEqual 6 + obj.Inventory.Offset mustEqual 6 + } + + "die" in { + val obj = new Player("Chord", PlanetSideEmpire.TR, CharacterGender.Male, 0, 5) + obj.Spawn + obj.Armor = 35 //50 -> 35 + obj.isAlive mustEqual true + obj.Health mustEqual obj.MaxHealth + obj.Stamina mustEqual obj.MaxStamina + obj.Armor mustEqual 35 + obj.Die + obj.isAlive mustEqual false + obj.Health mustEqual 0 + obj.Stamina mustEqual 0 + obj.Armor mustEqual 35 + } + + "draw equipped holsters only" in { + val wep = SimpleItem(SimpleItemDefinition(149)) + val obj = new Player("Chord", PlanetSideEmpire.TR, CharacterGender.Male, 0, 5) + obj.Slot(1).Size = EquipmentSize.Pistol + obj.Slot(1).Equipment = wep + obj.DrawnSlot mustEqual Player.HandsDownSlot + obj.DrawnSlot = 0 + obj.DrawnSlot mustEqual Player.HandsDownSlot + obj.DrawnSlot = 1 + obj.DrawnSlot mustEqual 1 + } + + "remember the last drawn holster" in { + val wep1 = SimpleItem(SimpleItemDefinition(149)) + val wep2 = SimpleItem(SimpleItemDefinition(149)) + val obj = new Player("Chord", PlanetSideEmpire.TR, CharacterGender.Male, 0, 5) + obj.Slot(0).Size = EquipmentSize.Pistol + obj.Slot(0).Equipment = wep1 + obj.Slot(1).Size = EquipmentSize.Pistol + obj.Slot(1).Equipment = wep2 + obj.DrawnSlot mustEqual Player.HandsDownSlot //default value + obj.LastDrawnSlot mustEqual 0 //default value + + obj.DrawnSlot = 1 + obj.DrawnSlot mustEqual 1 + obj.LastDrawnSlot mustEqual 0 //default value; sorry + + obj.DrawnSlot = 0 + obj.DrawnSlot mustEqual 0 + obj.LastDrawnSlot mustEqual 1 + + obj.DrawnSlot = Player.HandsDownSlot + obj.DrawnSlot mustEqual Player.HandsDownSlot + obj.LastDrawnSlot mustEqual 0 + + obj.DrawnSlot = 1 + obj.DrawnSlot mustEqual 1 + obj.LastDrawnSlot mustEqual 0 + + obj.DrawnSlot = 0 + obj.DrawnSlot mustEqual 0 + obj.LastDrawnSlot mustEqual 1 + + obj.DrawnSlot = 1 + obj.DrawnSlot mustEqual 1 + obj.LastDrawnSlot mustEqual 0 + + obj.DrawnSlot = Player.HandsDownSlot + obj.DrawnSlot mustEqual Player.HandsDownSlot + obj.LastDrawnSlot mustEqual 1 + } + + "install no implants until a slot is unlocked" in { + val testplant : Implant = Implant(ImplantDefinition(1)) + val obj = new Player("Chord", PlanetSideEmpire.TR, CharacterGender.Male, 0, 5) + obj.Implants(0).Unlocked mustEqual false + obj.Implant(0) mustEqual None + obj.InstallImplant(testplant) + obj.Implant(0) mustEqual None + obj.Implant(ImplantType(1)) mustEqual None + + obj.Implants(0).Unlocked = true + obj.InstallImplant(testplant) + obj.Implant(0) mustEqual Some(testplant.Definition.Type) + obj.Implant(ImplantType(1)) mustEqual Some(testplant) + } + + "uninstall implants" in { + val testplant : Implant = Implant(ImplantDefinition(1)) + val obj = new Player("Chord", PlanetSideEmpire.TR, CharacterGender.Male, 0, 5) + obj.Implants(0).Unlocked = true + obj.InstallImplant(testplant) + obj.Implant(ImplantType(1)) mustEqual Some(testplant) + + obj.UninstallImplant(ImplantType(1)) + obj.Implant(0) mustEqual None + obj.Implant(ImplantType(1)) mustEqual None + } + + "administrate" in { + val obj = new Player("Chord", PlanetSideEmpire.TR, CharacterGender.Male, 0, 5) + obj.Admin mustEqual false + Player.Administrate(obj, true) + obj.Admin mustEqual true + Player.Administrate(obj, false) + obj.Admin mustEqual false + } + + "spectate" in { + val obj = new Player("Chord", PlanetSideEmpire.TR, CharacterGender.Male, 0, 5) + obj.Spectator mustEqual false + Player.Spectate(obj, true) + obj.Spectator mustEqual true + Player.Spectate(obj, false) + obj.Spectator mustEqual false + } +} diff --git a/common/src/test/scala/objects/Receiver.scala b/common/src/test/scala/objects/Receiver.scala new file mode 100644 index 000000000..bb1894ec5 --- /dev/null +++ b/common/src/test/scala/objects/Receiver.scala @@ -0,0 +1,28 @@ +// Copyright (c) 2017 PSForever +package objects + +import akka.actor.{Actor, ActorSystem} +import net.psforever.objects.entity.IdentifiableEntity + +import scala.util.{Failure, Success} + +class ResolutionObject { + var complete = false +} + +/** + * This is for file NumberPoolActorTest, for its tests. + * Attempting to define this class in the aforementioned file causes a "can not find constructor" issue. + */ +class Receiver(private val system : ActorSystem, result : ResolutionObject) extends Actor { + def receive : Receive = { + case Success(objct : IdentifiableEntity) => + objct.GUID //this will throw a NoGUIDException if it fails + result.complete = true + system.terminate() + case Failure(ex) => + org.log4s.getLogger.error(s"object did not register - ${ex.getMessage}") + system.terminate() + } +} +//TODO Look into whether that was a legitimate issue or whether I (the user) was in error during Actor initialization later. diff --git a/pslogin/src/main/scala/AvatarService.scala b/pslogin/src/main/scala/AvatarService.scala new file mode 100644 index 000000000..377a92496 --- /dev/null +++ b/pslogin/src/main/scala/AvatarService.scala @@ -0,0 +1,228 @@ +// Copyright (c) 2016 PSForever.net to present +import akka.actor.Actor +import akka.event.{ActorEventBus, SubchannelClassification} +import akka.util.Subclassification +import net.psforever.objects.equipment.Equipment +import net.psforever.packet.game.objectcreate.ConstructorData +import net.psforever.types.ExoSuitType +import net.psforever.packet.game.{PlanetSideGUID, PlayerStateMessageUpstream} +import net.psforever.types.Vector3 + +sealed trait Action + +sealed trait Response + +final case class Join(channel : String) +final case class Leave() +final case class LeaveAll() + +object AvatarAction { + final case class ArmorChanged(player_guid : PlanetSideGUID, suit : ExoSuitType.Value, subtype : Int) extends Action + //final case class DropItem(pos : Vector3, orient : Vector3, item : PlanetSideGUID) extends Action + final case class EquipmentInHand(player_guid : PlanetSideGUID, slot : Int, item : Equipment) extends Action + final case class EquipmentOnGround(player_guid : PlanetSideGUID, pos : Vector3, orient : Vector3, item : Equipment) extends Action + final case class LoadPlayer(player_guid : PlanetSideGUID, pdata : ConstructorData) extends Action +// final case class LoadMap(msg : PlanetSideGUID) extends Action +// final case class unLoadMap(msg : PlanetSideGUID) extends Action + final case class ObjectDelete(player_guid : PlanetSideGUID, item_guid : PlanetSideGUID, unk : Int = 0) extends Action + final case class ObjectHeld(player_guid : PlanetSideGUID, slot : Int) extends Action + final case class PlanetsideAttribute(player_guid : PlanetSideGUID, attribute_type : Int, attribute_value : Long) extends Action + final case class PlayerState(player_guid : PlanetSideGUID, msg : PlayerStateMessageUpstream, spectator : Boolean, weaponInHand : Boolean) extends Action + final case class Reload(player_guid : PlanetSideGUID, mag : Int) extends Action +// final case class PlayerStateShift(killer : PlanetSideGUID, victim : PlanetSideGUID) extends Action +// final case class DestroyDisplay(killer : PlanetSideGUID, victim : PlanetSideGUID) extends Action +// final case class HitHintReturn(killer : PlanetSideGUID, victim : PlanetSideGUID) extends Action +// final case class ChangeWeapon(unk1 : Int, sessionId : Long) extends Action +} + +object AvatarServiceResponse { + final case class ArmorChanged(suit : ExoSuitType.Value, subtype : Int) extends Response + //final case class DropItem(pos : Vector3, orient : Vector3, item : PlanetSideGUID) extends Response + final case class EquipmentInHand(slot : Int, item : Equipment) extends Response + final case class EquipmentOnGround(pos : Vector3, orient : Vector3, item : Equipment) extends Response + final case class LoadPlayer(pdata : ConstructorData) extends Response +// final case class unLoadMap() extends Response +// final case class LoadMap() extends Response + final case class ObjectDelete(item_guid : PlanetSideGUID, unk : Int) extends Response + final case class ObjectHeld(slot : Int) extends Response + final case class PlanetSideAttribute(attribute_type : Int, attribute_value : Long) extends Response + final case class PlayerState(msg : PlayerStateMessageUpstream, spectator : Boolean, weaponInHand : Boolean) extends Response + final case class Reload(mag : Int) extends Response +// final case class PlayerStateShift(itemID : PlanetSideGUID) extends Response +// final case class DestroyDisplay(itemID : PlanetSideGUID) extends Response +// final case class HitHintReturn(itemID : PlanetSideGUID) extends Response +// final case class ChangeWeapon(facingYaw : Int) extends Response +} + +final case class AvatarServiceMessage(forChannel : String, actionMessage : Action) + +final case class AvatarServiceResponse(toChannel : String, avatar_guid : PlanetSideGUID, replyMessage : Response) + +/* + /avatar/ + */ + +class AvatarEventBus extends ActorEventBus with SubchannelClassification { + type Event = AvatarServiceResponse + type Classifier = String + + protected def classify(event: Event): Classifier = event.toChannel + + protected def subclassification = new Subclassification[Classifier] { + def isEqual(x: Classifier, y: Classifier) = x == y + def isSubclass(x: Classifier, y: Classifier) = x.startsWith(y) + } + + protected def publish(event: Event, subscriber: Subscriber): Unit = { + subscriber ! event + } +} + +class AvatarService extends Actor { + //import AvatarServiceResponse._ + private [this] val log = org.log4s.getLogger + + override def preStart = { + log.info("Starting...") + } + + val AvatarEvents = new AvatarEventBus + + /*val channelMap = Map( + AvatarMessageType.CMT_OPEN -> AvatarPath("local") + )*/ + + def receive = { + case Join(channel) => + val path = "/Avatar/" + channel + val who = sender() + + log.info(s"$who has joined $path") + + AvatarEvents.subscribe(who, path) + case Leave() => + AvatarEvents.unsubscribe(sender()) + case LeaveAll() => + AvatarEvents.unsubscribe(sender()) + + case AvatarServiceMessage(forChannel, action) => + action match { + case AvatarAction.ArmorChanged(player_guid, suit, subtype) => + AvatarEvents.publish( + AvatarServiceResponse("/Avatar/" + forChannel, player_guid, AvatarServiceResponse.ArmorChanged(suit, subtype)) + ) + case AvatarAction.EquipmentInHand(player_guid, slot, obj) => + AvatarEvents.publish( + AvatarServiceResponse("/Avatar/" + forChannel, player_guid, AvatarServiceResponse.EquipmentInHand(slot, obj)) + ) + case AvatarAction.EquipmentOnGround(player_guid, pos, orient, obj) => + AvatarEvents.publish( + AvatarServiceResponse("/Avatar/" + forChannel, player_guid, AvatarServiceResponse.EquipmentOnGround(pos, orient, obj)) + ) + case AvatarAction.LoadPlayer(player_guid, pdata) => + AvatarEvents.publish( + AvatarServiceResponse("/Avatar/" + forChannel, player_guid, AvatarServiceResponse.LoadPlayer(pdata)) + ) + case AvatarAction.ObjectDelete(player_guid, item_guid, unk) => + AvatarEvents.publish( + AvatarServiceResponse("/Avatar/" + forChannel, player_guid, AvatarServiceResponse.ObjectDelete(item_guid, unk)) + ) + case AvatarAction.ObjectHeld(player_guid, slot) => + AvatarEvents.publish( + AvatarServiceResponse("/Avatar/" + forChannel, player_guid, AvatarServiceResponse.ObjectHeld(slot)) + ) + case AvatarAction.PlanetsideAttribute(guid, attribute_type, attribute_value) => + AvatarEvents.publish( + AvatarServiceResponse("/Avatar/" + forChannel, guid, AvatarServiceResponse.PlanetSideAttribute(attribute_type, attribute_value)) + ) + case AvatarAction.PlayerState(guid, msg, spectator, weapon) => + AvatarEvents.publish( + AvatarServiceResponse("/Avatar/" + forChannel, guid, AvatarServiceResponse.PlayerState(msg, spectator, weapon)) + ) + case AvatarAction.Reload(player_guid, mag) => + AvatarEvents.publish( + AvatarServiceResponse("/Avatar/" + forChannel, player_guid, AvatarServiceResponse.Reload(mag)) + ) + case _ => ; + } + + /* + case AvatarService.PlayerStateMessage(msg) => + // log.info(s"NEW: ${m}") + val playerOpt: Option[PlayerAvatar] = PlayerMasterList.getPlayer(msg.avatar_guid) + if (playerOpt.isDefined) { + val player: PlayerAvatar = playerOpt.get + AvatarEvents.publish(AvatarMessage("/Avatar/" + player.continent, msg.avatar_guid, + AvatarServiceReply.PlayerStateMessage(msg.pos, msg.vel, msg.facingYaw, msg.facingPitch, msg.facingYawUpper, msg.is_crouching, msg.is_jumping, msg.jump_thrust, msg.is_cloaked) + )) + + } + case AvatarService.LoadMap(msg) => + val playerOpt: Option[PlayerAvatar] = PlayerMasterList.getPlayer(msg.guid) + if (playerOpt.isDefined) { + val player: PlayerAvatar = playerOpt.get + AvatarEvents.publish(AvatarMessage("/Avatar/" + player.continent, PlanetSideGUID(msg.guid), + AvatarServiceReply.LoadMap() + )) + } + case AvatarService.unLoadMap(msg) => + val playerOpt: Option[PlayerAvatar] = PlayerMasterList.getPlayer(msg.guid) + if (playerOpt.isDefined) { + val player: PlayerAvatar = playerOpt.get + AvatarEvents.publish(AvatarMessage("/Avatar/" + player.continent, PlanetSideGUID(msg.guid), + AvatarServiceReply.unLoadMap() + )) + } + case AvatarService.ObjectHeld(msg) => + val playerOpt: Option[PlayerAvatar] = PlayerMasterList.getPlayer(msg.guid) + if (playerOpt.isDefined) { + val player: PlayerAvatar = playerOpt.get + AvatarEvents.publish(AvatarMessage("/Avatar/" + player.continent, PlanetSideGUID(msg.guid), + AvatarServiceReply.ObjectHeld() + )) + } + case AvatarService.PlanetsideAttribute(guid, attribute_type, attribute_value) => + val playerOpt: Option[PlayerAvatar] = PlayerMasterList.getPlayer(guid) + if (playerOpt.isDefined) { + val player: PlayerAvatar = playerOpt.get + AvatarEvents.publish(AvatarMessage("/Avatar/" + player.continent, guid, + AvatarServiceReply.PlanetSideAttribute(attribute_type, attribute_value) + )) + } + case AvatarService.PlayerStateShift(killer, guid) => + val playerOpt: Option[PlayerAvatar] = PlayerMasterList.getPlayer(guid) + if (playerOpt.isDefined) { + val player: PlayerAvatar = playerOpt.get + AvatarEvents.publish(AvatarMessage("/Avatar/" + player.continent, guid, + AvatarServiceReply.PlayerStateShift(killer) + )) + } + case AvatarService.DestroyDisplay(killer, victim) => + val playerOpt: Option[PlayerAvatar] = PlayerMasterList.getPlayer(victim) + if (playerOpt.isDefined) { + val player: PlayerAvatar = playerOpt.get + AvatarEvents.publish(AvatarMessage("/Avatar/" + player.continent, victim, + AvatarServiceReply.DestroyDisplay(killer) + )) + } + case AvatarService.HitHintReturn(source_guid,victim_guid) => + val playerOpt: Option[PlayerAvatar] = PlayerMasterList.getPlayer(source_guid) + if (playerOpt.isDefined) { + val player: PlayerAvatar = playerOpt.get + AvatarEvents.publish(AvatarMessage("/Avatar/" + player.continent, victim_guid, + AvatarServiceReply.DestroyDisplay(source_guid) + )) + } + case AvatarService.ChangeWeapon(unk1, sessionId) => + val playerOpt: Option[PlayerAvatar] = PlayerMasterList.getPlayer(sessionId) + if (playerOpt.isDefined) { + val player: PlayerAvatar = playerOpt.get + AvatarEvents.publish(AvatarMessage("/Avatar/" + player.continent, PlanetSideGUID(player.guid), + AvatarServiceReply.ChangeWeapon(unk1) + )) + } + */ + case msg => + log.info(s"Unhandled message $msg from $sender") + } +} diff --git a/pslogin/src/main/scala/PsLogin.scala b/pslogin/src/main/scala/PsLogin.scala index 4745594d8..fefc32c80 100644 --- a/pslogin/src/main/scala/PsLogin.scala +++ b/pslogin/src/main/scala/PsLogin.scala @@ -4,6 +4,7 @@ import java.io.File import java.util.Locale import akka.actor.{ActorSystem, Props} +import akka.routing.RandomPool import ch.qos.logback.classic.LoggerContext import ch.qos.logback.classic.joran.JoranConfigurator import ch.qos.logback.core.joran.spi.JoranException @@ -11,6 +12,10 @@ import ch.qos.logback.core.status._ import ch.qos.logback.core.util.StatusPrinter import com.typesafe.config.ConfigFactory import net.psforever.crypto.CryptoInterface +import net.psforever.objects.guid.{NumberPoolHub, TaskResolver} +import net.psforever.objects.guid.actor.{NumberPoolAccessorActor, NumberPoolActor} +import net.psforever.objects.guid.selector.RandomSelector +import net.psforever.objects.guid.source.LimitedNumberSource import org.slf4j import org.fusesource.jansi.Ansi._ import org.fusesource.jansi.Ansi.Color._ @@ -25,7 +30,7 @@ object PsLogin { var args : Array[String] = Array() var config : java.util.Map[String,Object] = null - var system : akka.actor.ActorSystem = null + implicit var system : akka.actor.ActorSystem = null var loginRouter : akka.actor.Props = null var worldRouter : akka.actor.Props = null var loginListener : akka.actor.ActorRef = null @@ -195,6 +200,25 @@ object PsLogin { ) */ + val serviceManager = ServiceManager.boot + + //experimental guid code + val hub = new NumberPoolHub(new LimitedNumberSource(65536)) + val pool1 = hub.AddPool("test1", (400 to 599).toList) + val poolActor1 = system.actorOf(Props(classOf[NumberPoolActor], pool1), name = "poolActor1") + pool1.Selector = new RandomSelector + val pool2 = hub.AddPool("test2", (600 to 799).toList) + val poolActor2 = system.actorOf(Props(classOf[NumberPoolActor], pool2), name = "poolActor2") + pool2.Selector = new RandomSelector + + serviceManager ! ServiceManager.Register(Props(classOf[NumberPoolAccessorActor], hub, pool1, poolActor1), "accessor1") + serviceManager ! ServiceManager.Register(Props(classOf[NumberPoolAccessorActor], hub, pool2, poolActor2), "accessor2") + + //task resolver + serviceManager ! ServiceManager.Register(RandomPool(50).props(Props[TaskResolver]), "taskResolver") + + serviceManager ! ServiceManager.Register(Props[AvatarService], "avatar") + /** Create two actors for handling the login and world server endpoints */ loginRouter = Props(new SessionRouter("Login", loginTemplate)) worldRouter = Props(new SessionRouter("World", worldTemplate)) diff --git a/pslogin/src/main/scala/ServiceManager.scala b/pslogin/src/main/scala/ServiceManager.scala new file mode 100644 index 000000000..40f2c4c0f --- /dev/null +++ b/pslogin/src/main/scala/ServiceManager.scala @@ -0,0 +1,64 @@ +// Copyright (c) 2017 PSForever +import akka.actor.{Actor, ActorIdentity, ActorRef, ActorSystem, Identify, Props} + +import scala.collection.mutable + +object ServiceManager { + var serviceManager = Actor.noSender + + def boot(implicit system : ActorSystem) = { + serviceManager = system.actorOf(Props[ServiceManager], "service") + serviceManager + } + + case class Register(props : Props, name : String) + case class Lookup(name : String) + case class LookupResult(request : String, endpoint : ActorRef) +} + +class ServiceManager extends Actor { + import ServiceManager._ + private [this] val log = org.log4s.getLogger + + var nextLookupId : Long = 0 + val lookups : mutable.LongMap[RequestEntry] = mutable.LongMap() + + override def preStart = { + log.info("Starting...") + } + + def receive = { + case Register(props, name) => + log.info(s"Registered $name service") + context.actorOf(props, name) + case Lookup(name) => + context.actorSelection(name) ! Identify(nextLookupId) + lookups += nextLookupId -> RequestEntry(name, sender()) + nextLookupId += 1 + + case ActorIdentity(id, Some(ref)) => + val idNumber = id.asInstanceOf[Long] + lookups.get(idNumber) match { + case Some(RequestEntry(name, sender)) => + sender ! LookupResult(name, ref) + lookups.remove(idNumber) + case _ => + //TODO something + } + + case ActorIdentity(id, None) => + val idNumber = id.asInstanceOf[Long] + lookups.get(idNumber) match { + case Some(RequestEntry(name, _)) => + log.error(s"request #$idNumber for service `$name` came back empty; it may not exist") + lookups.remove(idNumber) + case _ => + //TODO something + } + + case default => + log.error(s"invalid message received - $default") + } + + protected case class RequestEntry(request : String, responder : ActorRef) +} diff --git a/pslogin/src/main/scala/WorldSessionActor.scala b/pslogin/src/main/scala/WorldSessionActor.scala index f3940e9f5..ae61a4cf3 100644 --- a/pslogin/src/main/scala/WorldSessionActor.scala +++ b/pslogin/src/main/scala/WorldSessionActor.scala @@ -1,29 +1,59 @@ // Copyright (c) 2017 PSForever +import java.util.concurrent.atomic.AtomicInteger + import akka.actor.{Actor, ActorRef, Cancellable, MDCContextAware} import net.psforever.packet.{PlanetSideGamePacket, _} import net.psforever.packet.control._ -import net.psforever.packet.game._ +import net.psforever.packet.game.{ObjectCreateDetailedMessage, _} import scodec.Attempt.{Failure, Successful} import scodec.bits._ import org.log4s.MDC import MDCContextAware.Implicits._ +import ServiceManager.Lookup +import net.psforever.objects._ +import net.psforever.objects.entity.IdentifiableEntity +import net.psforever.objects.equipment._ +import net.psforever.objects.guid.{Task, TaskResolver} +import net.psforever.objects.guid.actor.{Register, Unregister} +import net.psforever.objects.inventory.{GridInventory, InventoryItem} +import net.psforever.objects.terminals.{OrderTerminalDefinition, Terminal} import net.psforever.packet.game.objectcreate._ import net.psforever.types._ +import scala.annotation.tailrec + class WorldSessionActor extends Actor with MDCContextAware { private[this] val log = org.log4s.getLogger - private case class PokeClient() + private final case class PokeClient() + private final case class ServerLoaded() + private final case class PlayerLoaded(tplayer : Player) + private final case class ListAccountCharacters() + private final case class SetCurrentAvatar(tplayer : Player) + private final case class Continent_GiveItemFromGround(tplyaer : Player, item : Option[Equipment]) //TODO wrong place, move later var sessionId : Long = 0 var leftRef : ActorRef = ActorRef.noSender var rightRef : ActorRef = ActorRef.noSender + var avatarService = Actor.noSender + var accessor = Actor.noSender + var taskResolver = Actor.noSender - var clientKeepAlive : Cancellable = null + var clientKeepAlive : Cancellable = WorldSessionActor.DefaultCancellable override def postStop() = { if(clientKeepAlive != null) clientKeepAlive.cancel() + + avatarService ! Leave() + LivePlayerList.Remove(sessionId) match { + case Some(tplayer) => + val guid = tplayer.GUID + avatarService ! AvatarServiceMessage(tplayer.Continent, AvatarAction.ObjectDelete(guid, guid)) + taskResolver ! UnregisterAvatar(tplayer) + //TODO normally, the actual player avatar persists a minute or so after the user disconnects + case None => ; + } } def receive = Initializing @@ -34,6 +64,10 @@ class WorldSessionActor extends Actor with MDCContextAware { leftRef = sender() rightRef = right.asInstanceOf[ActorRef] + ServiceManager.serviceManager ! Lookup("avatar") + ServiceManager.serviceManager ! Lookup("accessor1") + ServiceManager.serviceManager ! Lookup("taskResolver") + context.become(Started) case _ => log.error("Unknown message") @@ -41,14 +75,415 @@ class WorldSessionActor extends Actor with MDCContextAware { } def Started : Receive = { + case ServiceManager.LookupResult("avatar", endpoint) => + avatarService = endpoint + log.info("ID: " + sessionId + " Got avatar service " + endpoint) + case ServiceManager.LookupResult("accessor1", endpoint) => + accessor = endpoint + log.info("ID: " + sessionId + " Got guid service " + endpoint) + case ServiceManager.LookupResult("taskResolver", endpoint) => + taskResolver = endpoint + log.info("ID: " + sessionId + " Got task resolver service " + endpoint) + case ctrl @ ControlPacket(_, _) => handlePktContainer(ctrl) case game @ GamePacket(_, _, _) => handlePktContainer(game) // temporary hack to keep the client from disconnecting case PokeClient() => - sendResponse(PacketCoding.CreateGamePacket(0, KeepAliveMessage(0))) - case default => failWithError(s"Invalid packet class received: $default") + sendResponse(PacketCoding.CreateGamePacket(0, KeepAliveMessage())) + + case AvatarServiceResponse(_, guid, reply) => + reply match { + case AvatarServiceResponse.ArmorChanged(suit, subtype) => + if(player.GUID != guid) { + sendResponse(PacketCoding.CreateGamePacket(0, ArmorChangedMessage(guid, suit, subtype))) + } + + case AvatarServiceResponse.EquipmentInHand(slot, item) => + if(player.GUID != guid) { + val definition = item.Definition + sendResponse( + PacketCoding.CreateGamePacket(0, + ObjectCreateMessage( + definition.ObjectId, + item.GUID, + ObjectCreateMessageParent(guid, slot), + definition.Packet.ConstructorData(item).get + ) + ) + ) + } + + case AvatarServiceResponse.EquipmentOnGround(pos, orient, item) => + if(player.GUID != guid) { + val definition = item.Definition + sendResponse( + PacketCoding.CreateGamePacket(0, + ObjectCreateMessage( + definition.ObjectId, + item.GUID, + DroppedItemData(PlacementData(pos, Vector3(0f, 0f, orient.z)), definition.Packet.ConstructorData(item).get) + ) + ) + ) + } + + case AvatarServiceResponse.LoadPlayer(pdata) => + if(player.GUID != guid) { + sendResponse( + PacketCoding.CreateGamePacket( + 0, + ObjectCreateMessage(ObjectClass.avatar, guid, pdata) + ) + ) + } + + case AvatarServiceResponse.ObjectDelete(item_guid, unk) => + if(player.GUID != guid) { + sendResponse(PacketCoding.CreateGamePacket(0, ObjectDeleteMessage(item_guid, unk))) + } + + case AvatarServiceResponse.ObjectHeld(slot) => + if(player.GUID != guid) { + sendResponse(PacketCoding.CreateGamePacket(0, ObjectHeldMessage(guid, slot, true))) + } + + case AvatarServiceResponse.PlanetSideAttribute(attribute_type, attribute_value) => + if(player.GUID != guid) { + sendResponse(PacketCoding.CreateGamePacket(0, PlanetsideAttributeMessage(guid, attribute_type, attribute_value))) + } + + case AvatarServiceResponse.PlayerState(msg, spectating, weaponInHand) => + if(player.GUID != guid) { + val now = System.currentTimeMillis() + val (location, time, distanceSq) : (Vector3, Long, Float) = if(spectating) { + (Vector3(2, 2, 2), 0L, 0f) + } + else { + val before = player.lastSeenStreamMessage(guid.guid) + val dist = WorldSessionActor.DistanceSquared(player.Position, msg.pos) + (msg.pos, now - before, dist) + } + + if(spectating || + ((distanceSq < 900 || weaponInHand) && time > 200) || + (distanceSq < 10000 && time > 500) || + (distanceSq < 160000 && (msg.is_jumping || time < 200)) || + (distanceSq < 160000 && msg.vel.isEmpty && time > 2000) || + (distanceSq < 160000 && time > 1000) || + (distanceSq > 160000 && time > 5000)) + { + sendResponse( + PacketCoding.CreateGamePacket(0, + PlayerStateMessage( + guid, + location, + msg.vel, + msg.facingYaw, + msg.facingPitch, + msg.facingYawUpper, + 0, + msg.is_crouching, + msg.is_jumping, + msg.jump_thrust, + msg.is_cloaked + ) + ) + ) + player.lastSeenStreamMessage(guid.guid) = now + } + } + + case AvatarServiceResponse.Reload(mag) => + if(player.GUID != guid) { + sendResponse(PacketCoding.CreateGamePacket(0, ReloadMessage(guid, mag, 0))) + } + + case _ => ; + } + + case Terminal.TerminalMessage(tplayer, msg, order) => + order match { + case Terminal.BuyExosuit(exosuit, subtype) => + if(tplayer.ExoSuit == exosuit) { //just refresh armor points + //we should never actually reach this point through conventional in-game methods + sendResponse(PacketCoding.CreateGamePacket(0, ItemTransactionResultMessage (msg.terminal_guid, TransactionType.Buy, true))) + tplayer.Armor = tplayer.MaxArmor + sendResponse(PacketCoding.CreateGamePacket(0, PlanetsideAttributeMessage(tplayer.GUID, 4, tplayer.Armor))) + avatarService ! AvatarServiceMessage(tplayer.Continent, AvatarAction.PlanetsideAttribute(tplayer.GUID, 4, tplayer.Armor)) + } + else { //load a complete new exo-suit and shuffle the inventory around + //TODO if we're transitioning into a MAX suit, the subtype dictates the type of arm(s) if the holster list is empty + //save inventory before it gets cleared (empty holsters) + sendResponse(PacketCoding.CreateGamePacket(0, ItemTransactionResultMessage (msg.terminal_guid, TransactionType.Buy, true))) + val beforeHolsters = clearHolsters(tplayer.Holsters().iterator) + val beforeInventory = tplayer.Inventory.Clear() + //change suit (clear inventory and change holster sizes; note: holsters must be empty before this point) + Player.SuitSetup(tplayer, exosuit) + tplayer.Armor = tplayer.MaxArmor + //delete everything + (beforeHolsters ++ beforeInventory).foreach({ elem => + sendResponse(PacketCoding.CreateGamePacket(0, ObjectDeleteMessage(elem.obj.GUID, 0))) + }) + beforeHolsters.foreach({ elem => + avatarService ! AvatarServiceMessage(tplayer.Continent, AvatarAction.ObjectDelete(tplayer.GUID, elem.obj.GUID)) + }) + //report change + sendResponse(PacketCoding.CreateGamePacket(0, ArmorChangedMessage(tplayer.GUID, exosuit, subtype))) + avatarService ! AvatarServiceMessage(player.Continent, AvatarAction.ArmorChanged(tplayer.GUID, exosuit, subtype)) + sendResponse(PacketCoding.CreateGamePacket(0, PlanetsideAttributeMessage(tplayer.GUID, 4, tplayer.Armor))) + avatarService ! AvatarServiceMessage(player.Continent, AvatarAction.PlanetsideAttribute(tplayer.GUID, 4, tplayer.Armor)) + //fill holsters + val (afterHolsters, toInventory) = beforeHolsters.partition(elem => elem.obj.Size == tplayer.Slot(elem.start).Size) + afterHolsters.foreach({elem => tplayer.Slot(elem.start).Equipment = elem.obj }) + val finalInventory = fillEmptyHolsters(tplayer.Holsters().iterator, toInventory ++ beforeInventory) + //draw holsters + (0 until 5).foreach({index => + tplayer.Slot(index).Equipment match { + case Some(obj) => + val definition = obj.Definition + sendResponse( + PacketCoding.CreateGamePacket(0, + ObjectCreateDetailedMessage( + definition.ObjectId, + obj.GUID, + ObjectCreateMessageParent(tplayer.GUID, index), + definition.Packet.DetailedConstructorData(obj).get + ) + ) + ) + avatarService ! AvatarServiceMessage(player.Continent, AvatarAction.EquipmentInHand(player.GUID, index, obj)) + case None => ; + } + }) + //re-draw equipment held in free hand + tplayer.FreeHand.Equipment match { + case Some(item) => + val definition = item.Definition + sendResponse( + PacketCoding.CreateGamePacket(0, + ObjectCreateDetailedMessage( + definition.ObjectId, + item.GUID, + ObjectCreateMessageParent(tplayer.GUID, Player.FreeHandSlot), + definition.Packet.DetailedConstructorData(item).get + ) + ) + ) + case None => ; + } + //put items back into inventory + val (stow, drop) = GridInventory.recoverInventory(finalInventory, tplayer.Inventory) + stow.foreach(elem => { + tplayer.Inventory.Insert(elem.start, elem.obj) + val obj = elem.obj + val definition = obj.Definition + sendResponse( + PacketCoding.CreateGamePacket(0, + ObjectCreateDetailedMessage( + definition.ObjectId, + obj.GUID, + ObjectCreateMessageParent(tplayer.GUID, elem.start), + definition.Packet.DetailedConstructorData(obj).get + ) + ) + ) + }) + //drop items on ground + val pos = tplayer.Position + val orient = tplayer.Orientation + drop.foreach(obj => { + obj.Position = pos + obj.Orientation = orient + val definition = obj.Definition + sendResponse( + PacketCoding.CreateGamePacket(0, + ObjectCreateMessage( + definition.ObjectId, + obj.GUID, + DroppedItemData(PlacementData(pos, Vector3(0f, 0f, orient.z)), definition.Packet.ConstructorData(obj).get) + ) + ) + ) + avatarService ! AvatarServiceMessage(tplayer.Continent, AvatarAction.EquipmentOnGround(tplayer.GUID, pos, orient, obj)) + }) + } + + case Terminal.BuyEquipment(item) => ; + tplayer.Fit(item) match { + case Some(index) => + sendResponse(PacketCoding.CreateGamePacket(0, ItemTransactionResultMessage (msg.terminal_guid, TransactionType.Buy, true))) + PutEquipmentInSlot(tplayer, item, index) + case None => + sendResponse(PacketCoding.CreateGamePacket(0, ItemTransactionResultMessage (msg.terminal_guid, TransactionType.Buy, false))) + } + + case Terminal.SellEquipment() => + tplayer.FreeHand.Equipment match { + case Some(item) => + if(item.GUID == msg.item_guid) { + sendResponse(PacketCoding.CreateGamePacket(0, ItemTransactionResultMessage (msg.terminal_guid, TransactionType.Sell, true))) + RemoveEquipmentFromSlot(tplayer, item, Player.FreeHandSlot) + } + case None => + sendResponse(PacketCoding.CreateGamePacket(0, ItemTransactionResultMessage (msg.terminal_guid, TransactionType.Sell, false))) + } + + case Terminal.InfantryLoadout(exosuit, subtype, holsters, inventory) => + //TODO optimizations against replacing Equipment with the exact same Equipment and potentially for recycling existing Equipment + log.info(s"$tplayer wants to change equipment loadout to their option #${msg.unk1 + 1}") + sendResponse(PacketCoding.CreateGamePacket(0, ItemTransactionResultMessage (msg.terminal_guid, TransactionType.InfantryLoadout, true))) + val beforeHolsters = clearHolsters(tplayer.Holsters().iterator) + val beforeInventory = tplayer.Inventory.Clear() + val beforeFreeHand = tplayer.FreeHand.Equipment + //change suit (clear inventory and change holster sizes; note: holsters must be empty before this point) + Player.SuitSetup(tplayer, exosuit) + tplayer.Armor = tplayer.MaxArmor + //delete everything + beforeHolsters.foreach({ elem => + avatarService ! AvatarServiceMessage(tplayer.Continent, AvatarAction.ObjectDelete(tplayer.GUID, elem.obj.GUID)) + }) + (beforeHolsters ++ beforeInventory).foreach({ elem => + sendResponse(PacketCoding.CreateGamePacket(0, ObjectDeleteMessage(elem.obj.GUID, 0))) + taskResolver ! UnregisterEquipment(elem.obj) + }) + //report change + sendResponse(PacketCoding.CreateGamePacket(0, ArmorChangedMessage(tplayer.GUID, exosuit, 0))) + avatarService ! AvatarServiceMessage(tplayer.Continent, AvatarAction.ArmorChanged(tplayer.GUID, exosuit, subtype)) + sendResponse(PacketCoding.CreateGamePacket(0, PlanetsideAttributeMessage(tplayer.GUID, 4, tplayer.Armor))) + avatarService ! AvatarServiceMessage(tplayer.Continent, AvatarAction.PlanetsideAttribute(tplayer.GUID, 4, tplayer.Armor)) + //re-draw equipment held in free hand + beforeFreeHand match { + //TODO was any previous free hand item deleted? + case Some(item) => + tplayer.FreeHand.Equipment = beforeFreeHand + val definition = item.Definition + sendResponse( + PacketCoding.CreateGamePacket(0, + ObjectCreateDetailedMessage( + definition.ObjectId, + item.GUID, + ObjectCreateMessageParent(tplayer.GUID, Player.FreeHandSlot), + definition.Packet.DetailedConstructorData(item).get + ) + ) + ) + case None => ; + } + //draw holsters + holsters.foreach(entry => { + PutEquipmentInSlot(tplayer, entry.obj, entry.start) + }) + //put items into inventory + inventory.foreach(entry => { + PutEquipmentInSlot(tplayer, entry.obj, entry.start) + }) + //TODO drop items on ground + + case Terminal.NoDeal() => + log.warn(s"$tplayer made a request but the terminal rejected the order $msg") + sendResponse(PacketCoding.CreateGamePacket(0, ItemTransactionResultMessage(msg.terminal_guid, msg.transaction_type, false))) + } + + case ListAccountCharacters => + val gen : AtomicInteger = new AtomicInteger(1) + + //load characters + SetCharacterSelectScreenGUID(player, gen) + val health = player.Health + val stamina = player.Stamina + val armor = player.Armor + player.Spawn + sendResponse(PacketCoding.CreateGamePacket(0, + ObjectCreateMessage(ObjectClass.avatar, player.GUID, player.Definition.Packet.ConstructorData(player).get) + )) + if(health > 0) { //player can not be dead; stay spawned as alive + player.Health = health + player.Stamina = stamina + player.Armor = armor + } + sendResponse(PacketCoding.CreateGamePacket(0, CharacterInfoMessage(15,PlanetSideZoneID(10000), 41605313, player.GUID, false, 6404428))) + RemoveCharacterSelectScreenGUID(player) + + sendResponse(PacketCoding.CreateGamePacket(0, CharacterInfoMessage(0, PlanetSideZoneID(1), 0, PlanetSideGUID(0), true, 0))) + + case PlayerLoaded(tplayer) => + log.info(s"Player $tplayer has been loaded") + //init for whole server + //... + sendResponse( + PacketCoding.CreateGamePacket(0, + BuildingInfoUpdateMessage( + PlanetSideGUID(6), //Ceryshen + PlanetSideGUID(2), //Anguta + 8, //80% NTU + true, //Base hacked + PlanetSideEmpire.NC, //Base hacked by NC + 600000, //10 minutes remaining for hack + PlanetSideEmpire.VS, //Base owned by VS + 0, //!! Field != 0 will cause malformed packet. See class def. + None, + PlanetSideGeneratorState.Critical, //Generator critical + true, //Respawn tubes destroyed + true, //Force dome active + 16, //Tech plant lattice benefit + 0, + Nil, //!! Field > 0 will cause malformed packet. See class def. + 0, + false, + 8, //!! Field != 8 will cause malformed packet. See class def. + None, + true, //Boosted spawn room pain field + true //Boosted generator room pain field + ) + ) + ) + sendResponse(PacketCoding.CreateGamePacket(0, ContinentalLockUpdateMessage(PlanetSideGUID(13), PlanetSideEmpire.VS))) // "The VS have captured the VS Sanctuary." + sendResponse(PacketCoding.CreateGamePacket(0, BroadcastWarpgateUpdateMessage(PlanetSideGUID(13), PlanetSideGUID(1), false, false, true))) // VS Sanctuary: Inactive Warpgate -> Broadcast Warpgate + //LoadMapMessage -> BeginZoningMessage + sendResponse(PacketCoding.CreateGamePacket(0, LoadMapMessage("map13","home3",40100,25,true,3770441820L))) //VS Sanctuary + //load the now-registered player + tplayer.Spawn + sendResponse(PacketCoding.CreateGamePacket(0, + ObjectCreateDetailedMessage(ObjectClass.avatar, tplayer.GUID, tplayer.Definition.Packet.DetailedConstructorData(tplayer).get) + )) + avatarService ! AvatarServiceMessage(tplayer.Continent, AvatarAction.LoadPlayer(tplayer.GUID, tplayer.Definition.Packet.ConstructorData(tplayer).get)) + log.debug(s"ObjectCreateDetailedMessage: ${tplayer.Definition.Packet.DetailedConstructorData(tplayer).get}") + + case SetCurrentAvatar(tplayer) => + //avatar-specific + val guid = tplayer.GUID + LivePlayerList.Assign(sessionId, guid) + sendResponse(PacketCoding.CreateGamePacket(0, SetCurrentAvatarMessage(guid,0,0))) + sendResponse(PacketCoding.CreateGamePacket(0, CreateShortcutMessage(guid, 1, 0, true, Shortcut.MEDKIT))) + + //temporary location + case Continent_GiveItemFromGround(tplayer, item) => + item match { + case Some(obj) => + val obj_guid = obj.GUID + tplayer.Fit(obj) match { + case Some(slot) => + PickupItemFromGround(obj_guid) + tplayer.Slot(slot).Equipment = item + sendResponse(PacketCoding.CreateGamePacket(0, ObjectAttachMessage(tplayer.GUID, obj_guid, slot))) + avatarService ! AvatarServiceMessage(tplayer.Continent, AvatarAction.ObjectDelete(tplayer.GUID, obj_guid)) + if(-1 < slot && slot < 5) { + avatarService ! AvatarServiceMessage(tplayer.Continent, AvatarAction.EquipmentInHand(tplayer.GUID, slot, obj)) + } + case None => + DropItemOnGround(obj, obj.Position, obj.Orientation) //restore + } + case None => ; + } + + case WorldSessionActor.ResponseToSelf(pkt) => + log.info(s"Received a direct message: $pkt") + sendResponse(pkt) + + case default => + failWithError(s"Invalid packet class received: $default") } def handlePkt(pkt : PlanetSidePacket) : Unit = pkt match { @@ -106,160 +541,184 @@ class WorldSessionActor extends Actor with MDCContextAware { } } - //val objectHex = hex"18 57 0C 00 00 BC 84 B0 06 C2 D7 65 53 5C A1 60 00 01 34 40 00 09 70 49 00 6C 00 6C 00 6C 00 49 00 49 00 49 00 6C 00 6C 00 6C 00 49 00 6C 00 49 00 6C 00 6C 00 49 00 6C 00 6C 00 6C 00 49 00 6C 00 6C 00 49 00 84 52 70 76 1E 80 80 00 00 00 00 00 3F FF C0 00 00 00 20 00 00 0F F6 A7 03 FF FF FF FF FF FF FF FF FF FF FF FF FF FF FF FD 90 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 01 90 01 90 00 64 00 00 01 00 7E C8 00 C8 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 01 C0 00 42 C5 46 86 C7 00 00 00 80 00 00 12 40 78 70 65 5F 73 61 6E 63 74 75 61 72 79 5F 68 65 6C 70 90 78 70 65 5F 74 68 5F 66 69 72 65 6D 6F 64 65 73 8B 75 73 65 64 5F 62 65 61 6D 65 72 85 6D 61 70 31 33 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 01 0A 23 02 60 04 04 40 00 00 10 00 06 02 08 14 D0 08 0C 80 00 02 00 02 6B 4E 00 82 88 00 00 02 00 00 C0 41 C0 9E 01 01 90 00 00 64 00 44 2A 00 10 91 00 00 00 40 00 18 08 38 94 40 20 32 00 00 00 80 19 05 48 02 17 20 00 00 08 00 70 29 80 43 64 00 00 32 00 0E 05 40 08 9C 80 00 06 40 01 C0 AA 01 19 90 00 00 C8 00 3A 15 80 28 72 00 00 19 00 04 0A B8 05 26 40 00 03 20 06 C2 58 00 A7 88 00 00 02 00 00 80 00 00" - //currently, the character's starting BEP is discarded due to unknown bit format - val app = CharacterAppearanceData( - PlacementData( - Vector3(3674.8438f, 2726.789f, 91.15625f), - Vector3(0f, 0f, 90f) - ), - BasicCharacterData( - "IlllIIIlllIlIllIlllIllI", - PlanetSideEmpire.VS, - CharacterGender.Female, - 41, - 1 - ), - 3, - false, - false, - ExoSuitType.Standard, - "", - 0, - false, - 0, 181, - true, - GrenadeState.None, - false, - false, - false, - RibbonBars() - ) - val inv = InventoryItem(ObjectClass.beamer, PlanetSideGUID(76), 0, DetailedWeaponData(4, 8, ObjectClass.energy_cell, PlanetSideGUID(77), 0, DetailedAmmoBoxData(8, 16))) :: - InventoryItem(ObjectClass.suppressor, PlanetSideGUID(78), 2, DetailedWeaponData(4, 8, ObjectClass.bullet_9mm, PlanetSideGUID(79), 0, DetailedAmmoBoxData(8, 25))) :: - InventoryItem(ObjectClass.forceblade, PlanetSideGUID(80), 4, DetailedWeaponData(4, 8, ObjectClass.melee_ammo, PlanetSideGUID(81), 0, DetailedAmmoBoxData(8, 1))) :: - InventoryItem(ObjectClass.locker_container, PlanetSideGUID(82), 5, DetailedAmmoBoxData(8, 1)) :: - InventoryItem(ObjectClass.bullet_9mm, PlanetSideGUID(83), 6, DetailedAmmoBoxData(8, 50)) :: - InventoryItem(ObjectClass.bullet_9mm, PlanetSideGUID(84), 9, DetailedAmmoBoxData(8, 50)) :: - InventoryItem(ObjectClass.bullet_9mm, PlanetSideGUID(85), 12, DetailedAmmoBoxData(8, 50)) :: - InventoryItem(ObjectClass.bullet_9mm_AP, PlanetSideGUID(86), 33, DetailedAmmoBoxData(8, 50)) :: - InventoryItem(ObjectClass.energy_cell, PlanetSideGUID(87), 36, DetailedAmmoBoxData(8, 50)) :: - InventoryItem(ObjectClass.remote_electronics_kit, PlanetSideGUID(88), 39, DetailedREKData(8)) :: - Nil - val obj = DetailedCharacterData( - app, - 100, 100, - 50, - 1, 7, 7, - 100, 100, - 28, 4, 44, 84, 104, 1900, - "xpe_sanctuary_help" :: "xpe_th_firemodes" :: "used_beamer" :: "map13" :: Nil, - List.empty, - InventoryData(inv), - DrawnSlot.None - ) - val objectHex = ObjectCreateDetailedMessage(ObjectClass.avatar, PlanetSideGUID(75), obj) + val terminal = Terminal(PlanetSideGUID(55000), new OrderTerminalDefinition) + + import net.psforever.objects.GlobalDefinitions._ + //this part is created by the player (should be in case of ConnectToWorldRequestMessage, maybe) + val energy_cell_box1 = AmmoBox(energy_cell) + val energy_cell_box2 = AmmoBox(energy_cell, 16) + val bullet_9mm_box1 = AmmoBox(bullet_9mm) + val bullet_9mm_box2 = AmmoBox(bullet_9mm) + val bullet_9mm_box3 = AmmoBox(bullet_9mm) + val bullet_9mm_box4 = AmmoBox(bullet_9mm, 25) + val bullet_9mm_AP_box = AmmoBox(bullet_9mm_AP) + val melee_ammo_box = AmmoBox(melee_ammo) + val + beamer1 = Tool(beamer) + beamer1.AmmoSlots.head.Box = energy_cell_box2 + val + suppressor1 = Tool(suppressor) + suppressor1.AmmoSlots.head.Box = bullet_9mm_box4 + val + forceblade1 = Tool(forceblade) + forceblade1.AmmoSlots.head.Box = melee_ammo_box + val rek = SimpleItem(remote_electronics_kit) + val lockerContainer = LockerContainer() + val + player = Player("IlllIIIlllIlIllIlllIllI", PlanetSideEmpire.VS, CharacterGender.Female, 41, 1) + player.Position = Vector3(3674.8438f, 2726.789f, 91.15625f) + player.Orientation = Vector3(0f, 0f, 90f) + player.Continent = "home3" + player.Slot(0).Equipment = beamer1 + player.Slot(2).Equipment = suppressor1 + player.Slot(4).Equipment = forceblade1 + player.Slot(5).Equipment = lockerContainer + player.Slot(6).Equipment = bullet_9mm_box1 + player.Slot(9).Equipment = bullet_9mm_box2 + player.Slot(12).Equipment = bullet_9mm_box3 + player.Slot(33).Equipment = bullet_9mm_AP_box + player.Slot(36).Equipment = energy_cell_box1 + player.Slot(39).Equipment = rek + + //for player2 + val energy_cell_box3 = AmmoBox(PlanetSideGUID(187), energy_cell) + val energy_cell_box4 = AmmoBox(PlanetSideGUID(177), energy_cell, 16) + val bullet_9mm_box5 = AmmoBox(PlanetSideGUID(183), bullet_9mm) + val bullet_9mm_box6 = AmmoBox(PlanetSideGUID(184), bullet_9mm) + val bullet_9mm_box7 = AmmoBox(PlanetSideGUID(185), bullet_9mm) + val bullet_9mm_box8 = AmmoBox(PlanetSideGUID(179), bullet_9mm, 25) + val bullet_9mm_AP_box2 = AmmoBox(PlanetSideGUID(186), bullet_9mm_AP) + val melee_ammo_box2 = AmmoBox(PlanetSideGUID(181), melee_ammo) + + val + beamer2 = Tool(PlanetSideGUID(176), beamer) + beamer2.AmmoSlots.head.Box = energy_cell_box4 + val + suppressor2 = Tool(PlanetSideGUID(178), suppressor) + suppressor2.AmmoSlots.head.Box = bullet_9mm_box8 + val + forceblade2 = Tool(PlanetSideGUID(180), forceblade) + forceblade2.AmmoSlots.head.Box = melee_ammo_box2 + val + rek2 = SimpleItem(PlanetSideGUID(188), remote_electronics_kit) + val + lockerContainer2 = LockerContainer(PlanetSideGUID(182)) + val + player2 = Player(PlanetSideGUID(275), "Doppelganger", PlanetSideEmpire.NC, CharacterGender.Female, 41, 1) + player2.Position = Vector3(3680f, 2726.789f, 91.15625f) + player2.Orientation = Vector3(0f, 0f, 0f) + player2.Continent = "home3" + player2.Slot(0).Equipment = beamer2 + player2.Slot(2).Equipment = suppressor2 + player2.Slot(4).Equipment = forceblade2 + player2.Slot(5).Equipment = lockerContainer2 + player2.Slot(6).Equipment = bullet_9mm_box5 + player2.Slot(9).Equipment = bullet_9mm_box6 + player2.Slot(12).Equipment = bullet_9mm_box7 + player2.Slot(33).Equipment = bullet_9mm_AP_box2 + player2.Slot(36).Equipment = energy_cell_box3 + player2.Slot(39).Equipment = rek2 + player2.Spawn + + val hellfire_ammo_box = AmmoBox(PlanetSideGUID(432), hellfire_ammo) + + val + fury1 = Vehicle(PlanetSideGUID(313), fury) + fury1.Faction = PlanetSideEmpire.VS + fury1.Position = Vector3(3674.8438f, 2732f, 91.15625f) + fury1.Orientation = Vector3(0.0f, 0.0f, 90.0f) + fury1.WeaponControlledFromSeat(0).get.GUID = PlanetSideGUID(300) + fury1.WeaponControlledFromSeat(0).get.AmmoSlots.head.Box = hellfire_ammo_box + + val object2Hex = ObjectCreateMessage(ObjectClass.avatar, PlanetSideGUID(275), player2.Definition.Packet.ConstructorData(player2).get) + val furyHex = ObjectCreateMessage(ObjectClass.fury, PlanetSideGUID(313), fury1.Definition.Packet.ConstructorData(fury1).get) def handleGamePkt(pkt : PlanetSideGamePacket) = pkt match { case ConnectToWorldRequestMessage(server, token, majorVersion, minorVersion, revision, buildDate, unk) => - val clientVersion = s"Client Version: $majorVersion.$minorVersion.$revision, $buildDate" - log.info(s"New world login to $server with Token:$token. $clientVersion") + self ! ListAccountCharacters - // ObjectCreateMessage - sendResponse(PacketCoding.CreateGamePacket(0, objectHex)) - // XXX: hard coded message - sendRawResponse(hex"14 0F 00 00 00 10 27 00 00 C1 D8 7A 02 4B 00 26 5C B0 80 00 ") + case msg @ CharacterCreateRequestMessage(name, head, voice, gender, empire) => + log.info("Handling " + msg) + sendResponse(PacketCoding.CreateGamePacket(0, ActionResultMessage(true, None))) + self ! ListAccountCharacters - // NOTE: PlanetSideZoneID just chooses the background - sendResponse(PacketCoding.CreateGamePacket(0, - CharacterInfoMessage(0, PlanetSideZoneID(1), 0, PlanetSideGUID(0), true, 0))) case msg @ CharacterRequestMessage(charId, action) => log.info("Handling " + msg) - action match { case CharacterRequestAction.Delete => sendResponse(PacketCoding.CreateGamePacket(0, ActionResultMessage(false, Some(1)))) case CharacterRequestAction.Select => - objectHex match { - case obj @ ObjectCreateDetailedMessage(len, cls, guid, _, _) => - log.debug("Object: " + obj) - // LoadMapMessage 13714 in mossy .gcap - // XXX: hardcoded shit - sendResponse(PacketCoding.CreateGamePacket(0, LoadMapMessage("map13","home3",40100,25,true,3770441820L))) //VS Sanctuary - sendResponse(PacketCoding.CreateGamePacket(0, ZonePopulationUpdateMessage(PlanetSideGUID(13), 414, 138, 0, 138, 0, 138, 0, 138, 0))) - sendResponse(PacketCoding.CreateGamePacket(0, objectHex)) + LivePlayerList.Add(sessionId, player) + //check can spawn on last continent/location from player + //if yes, get continent guid accessors + //if no, get sanctuary guid accessors and reset the player's expectations + taskResolver ! RegisterAvatar(player) - // These object_guids are specfic to VS Sanc - sendResponse(PacketCoding.CreateGamePacket(0, SetEmpireMessage(PlanetSideGUID(2), PlanetSideEmpire.VS))) //HART building C - sendResponse(PacketCoding.CreateGamePacket(0, SetEmpireMessage(PlanetSideGUID(29), PlanetSideEmpire.NC))) //South Villa Gun Tower - - sendResponse(PacketCoding.CreateGamePacket(0, TimeOfDayMessage(1191182336))) - sendResponse(PacketCoding.CreateGamePacket(0, ContinentalLockUpdateMessage(PlanetSideGUID(13), PlanetSideEmpire.VS))) // "The VS have captured the VS Sanctuary." - sendResponse(PacketCoding.CreateGamePacket(0, BroadcastWarpgateUpdateMessage(PlanetSideGUID(13), PlanetSideGUID(1), false, false, true))) // VS Sanctuary: Inactive Warpgate -> Broadcast Warpgate - - sendResponse(PacketCoding.CreateGamePacket(0,BuildingInfoUpdateMessage( - PlanetSideGUID(6), //Ceryshen - PlanetSideGUID(2), //Anguta - 8, //80% NTU - true, //Base hacked - PlanetSideEmpire.NC, //Base hacked by NC - 600000, //10 minutes remaining for hack - PlanetSideEmpire.VS, //Base owned by VS - 0, //!! Field != 0 will cause malformed packet. See class def. - None, - PlanetSideGeneratorState.Critical, //Generator critical - true, //Respawn tubes destroyed - true, //Force dome active - 16, //Tech plant lattice benefit - 0, - Nil, //!! Field > 0 will cause malformed packet. See class def. - 0, - false, - 8, //!! Field != 8 will cause malformed packet. See class def. - None, - true, //Boosted spawn room pain field - true))) //Boosted generator room pain field - - sendResponse(PacketCoding.CreateGamePacket(0, SetCurrentAvatarMessage(guid,0,0))) - sendResponse(PacketCoding.CreateGamePacket(0, CreateShortcutMessage(guid, 1, 0, true, Shortcut.MEDKIT))) - sendResponse(PacketCoding.CreateGamePacket(0, ReplicationStreamMessage(5, Some(6), Vector(SquadListing())))) //clear squad list - - val fury = VehicleData( - CommonFieldData( - PlacementData(3674.8438f, 2732f, 91.15625f, 0.0f, 0.0f, 90.0f), - PlanetSideEmpire.VS, 4 - ), - 255, - MountItem(ObjectClass.fury_weapon_systema, PlanetSideGUID(400), 1, - WeaponData(0x6, 0x8, 0, ObjectClass.hellfire_ammo, PlanetSideGUID(432), 0, AmmoBoxData(0x8)) - ) - ) - sendResponse(PacketCoding.CreateGamePacket(0, ObjectCreateMessage(ObjectClass.fury, PlanetSideGUID(413), fury))) - - import scala.concurrent.duration._ - import scala.concurrent.ExecutionContext.Implicits.global - clientKeepAlive = context.system.scheduler.schedule(0 seconds, 500 milliseconds, self, PokeClient()) - } + import scala.concurrent.duration._ + import scala.concurrent.ExecutionContext.Implicits.global + clientKeepAlive = context.system.scheduler.schedule(0 seconds, 500 milliseconds, self, PokeClient()) case default => log.error("Unsupported " + default + " in " + msg) } - case msg @ CharacterCreateRequestMessage(name, head, voice, gender, empire) => - log.info("Handling " + msg) - - sendResponse(PacketCoding.CreateGamePacket(0, ActionResultMessage(true, None))) - sendResponse(PacketCoding.CreateGamePacket(0, - CharacterInfoMessage(0, PlanetSideZoneID(0), 0, PlanetSideGUID(0), true, 0))) case KeepAliveMessage(code) => sendResponse(PacketCoding.CreateGamePacket(0, KeepAliveMessage())) case msg @ BeginZoningMessage() => log.info("Reticulating splines ...") + //map-specific initializations (VS sanctuary) + sendResponse(PacketCoding.CreateGamePacket(0, SetEmpireMessage(PlanetSideGUID(2), PlanetSideEmpire.VS))) //HART building C + sendResponse(PacketCoding.CreateGamePacket(0, SetEmpireMessage(PlanetSideGUID(29), PlanetSideEmpire.NC))) //South Villa Gun Tower + sendResponse(PacketCoding.CreateGamePacket(0, object2Hex)) + //sendResponse(PacketCoding.CreateGamePacket(0, furyHex)) - case msg @ PlayerStateMessageUpstream(avatar_guid, pos, vel, yaw, pitch, yawUpper, seq_time, unk3, is_crouching, is_jumping, unk4, is_cloaking, unk5, unk6) => - //log.info("PlayerState: " + msg) + sendResponse(PacketCoding.CreateGamePacket(0, ZonePopulationUpdateMessage(PlanetSideGUID(13), 414, 138, 0, 138, 0, 138, 0, 138, 0))) + sendResponse(PacketCoding.CreateGamePacket(0, TimeOfDayMessage(1191182336))) + sendResponse(PacketCoding.CreateGamePacket(0, ReplicationStreamMessage(5, Some(6), Vector(SquadListing())))) //clear squad list + + //all players are part of the same zone right now, so don't expect much + val continent = player.Continent + val player_guid = player.GUID + LivePlayerList.WorldPopulation({ case (_, char : Player) => char.Continent == continent && char.HasGUID && char.GUID != player_guid}).foreach(char => { + sendResponse( + PacketCoding.CreateGamePacket(0, + ObjectCreateMessage(ObjectClass.avatar, char.GUID, char.Definition.Packet.ConstructorData(char).get) + ) + ) + }) + //all items are part of a single zone right now, so don't expect much + WorldSessionActor.equipmentOnGround.foreach(item => { + val definition = item.Definition + sendResponse( + PacketCoding.CreateGamePacket(0, + ObjectCreateMessage( + definition.ObjectId, + item.GUID, + DroppedItemData(PlacementData(item.Position, item.Orientation), definition.Packet.ConstructorData(item).get) + ) + ) + ) + }) + + avatarService ! Join("home3") + self ! SetCurrentAvatar(player) + + case msg @ PlayerStateMessageUpstream(avatar_guid, pos, vel, yaw, pitch, yaw_upper, seq_time, unk3, is_crouching, is_jumping, unk4, is_cloaking, unk5, unk6) => + player.Position = pos + player.Velocity = vel + player.Orientation = Vector3(player.Orientation.x, pitch, yaw) + player.FacingYawUpper = yaw_upper + player.Crouching = is_crouching + player.Jumping = is_jumping + + val wepInHand : Boolean = player.Slot(player.DrawnSlot).Equipment match { + case Some(item) => item.Definition == bolt_driver + case None => false + } + avatarService ! AvatarServiceMessage(player.Continent, AvatarAction.PlayerState(avatar_guid, msg, player.Spectator, wepInHand)) + //log.info("PlayerState: " + msg) case msg @ ChildObjectStateMessage(object_guid, pitch, yaw) => //log.info("ChildObjectState: " + msg) @@ -312,19 +771,50 @@ class WorldSessionActor extends Actor with MDCContextAware { case msg @ DropItemMessage(item_guid) => log.info("DropItem: " + msg) - //item dropped where you spawn in VS Sanctuary - sendResponse(PacketCoding.CreateGamePacket(0, ObjectDetachMessage(PlanetSideGUID(75), item_guid, app.pos.coord, 0, 0, 0))) + player.FreeHand.Equipment match { + case Some(item) => + if(item.GUID == item_guid) { + player.FreeHand.Equipment = None + DropItemOnGround(item, player.Position, player.Orientation) + sendResponse(PacketCoding.CreateGamePacket(0, ObjectDetachMessage(player.GUID, item.GUID, player.Position, 0f, 0f, player.Orientation.z))) + avatarService ! AvatarServiceMessage(player.Continent, AvatarAction.EquipmentOnGround(player.GUID, player.Position, player.Orientation, item)) + } + else { + log.warn(s"item in hand was ${item.GUID} but trying to drop $item_guid; nothing will be dropped") + } + case None => + log.error(s"$player wanted to drop an item, but it was not in hand") + } case msg @ PickupItemMessage(item_guid, player_guid, unk1, unk2) => log.info("PickupItem: " + msg) - sendResponse(PacketCoding.CreateGamePacket(0, PickupItemMessage(item_guid, player_guid, unk1, unk2))) - sendResponse(PacketCoding.CreateGamePacket(0, ObjectAttachMessage(player_guid, item_guid, 250))) // item on mouse + self ! Continent_GiveItemFromGround(player, PickupItemFromGround(item_guid)) case msg @ ReloadMessage(item_guid, ammo_clip, unk1) => log.info("Reload: " + msg) - sendResponse(PacketCoding.CreateGamePacket(0, ReloadMessage(item_guid, 123, unk1))) + val reloadValue = player.Slot(player.DrawnSlot).Equipment match { + case Some(item) => + item match { + case tool : Tool => + tool.FireMode.Magazine + case _ => + 0 + } + case None => + 0 + } + //TODO hunt for ammunition in inventory + if(reloadValue > 0) { + sendResponse(PacketCoding.CreateGamePacket(0, ReloadMessage(item_guid, reloadValue, unk1))) + } case msg @ ObjectHeldMessage(avatar_guid, held_holsters, unk1) => + val before = player.DrawnSlot + val after = player.DrawnSlot = held_holsters + if(before != after) { + val slot = if(after == Player.HandsDownSlot) { before } else { after } + avatarService ! AvatarServiceMessage(player.Continent, AvatarAction.ObjectHeld(player.GUID, slot)) + } log.info("ObjectHeld: " + msg) case msg @ AvatarJumpMessage(state) => @@ -349,17 +839,69 @@ class WorldSessionActor extends Actor with MDCContextAware { } case msg @ RequestDestroyMessage(object_guid) => - log.info("RequestDestroy: " + msg) // TODO: Make sure this is the correct response in all cases - sendResponse(PacketCoding.CreateGamePacket(0, ObjectDeleteMessage(object_guid, 0))) + player.Find(object_guid) match { + case Some(slot) => + taskResolver ! RemoveEquipmentFromSlot(player, player.Slot(slot).Equipment.get, slot) + log.info("RequestDestroy: " + msg) + case None => + sendResponse(PacketCoding.CreateGamePacket(0, ObjectDeleteMessage(object_guid, 0))) + log.warn(s"RequestDestroy: object $object_guid not found") + } case msg @ ObjectDeleteMessage(object_guid, unk1) => sendResponse(PacketCoding.CreateGamePacket(0, ObjectDeleteMessage(object_guid, 0))) log.info("ObjectDelete: " + msg) case msg @ MoveItemMessage(item_guid, avatar_guid_1, avatar_guid_2, dest, unk1) => - sendResponse(PacketCoding.CreateGamePacket(0, ObjectAttachMessage(avatar_guid_1,item_guid,dest))) - log.info("MoveItem: " + msg) + player.Find(item_guid) match { + case Some(index) => + val indexSlot = player.Slot(index) + val destSlot = player.Slot(dest) + val item = indexSlot.Equipment.get + val destItem = destSlot.Equipment + indexSlot.Equipment = None + destSlot.Equipment = None + + (destSlot.Equipment = item) match { + case Some(_) => //move item + log.info(s"MoveItem: $item_guid moved from $avatar_guid_1 @ $index to $avatar_guid_1 @ $dest") + //continue on to the code following the next match statement after resolving the match statement + destItem match { + case Some(item2) => //second item to swap? + (indexSlot.Equipment = destItem) match { + case Some(_) => //yes, swap + log.info(s"MoveItem: ${item2.GUID} swapped to $avatar_guid_1 @ $index") + //we must shuffle items around cleanly to avoid causing icons to "disappear" + if(index == Player.FreeHandSlot) { //temporarily put in safe location, A -> C + sendResponse(PacketCoding.CreateGamePacket(0, ObjectDetachMessage(player.GUID, item.GUID, Vector3(0f, 0f, 0f), 0f, 0f, 0f))) //ground + } + else { + sendResponse(PacketCoding.CreateGamePacket(0, ObjectAttachMessage(player.GUID, item.GUID, Player.FreeHandSlot))) //free hand + } + sendResponse(PacketCoding.CreateGamePacket(0, ObjectAttachMessage(player.GUID, item2.GUID, index))) //B -> A + if(0 <= index && index < 5) { + avatarService ! AvatarServiceMessage(player.Continent, AvatarAction.EquipmentInHand(player.GUID, index, item2)) + } + + case None => //can't complete the swap; drop the other item on the ground + sendResponse(PacketCoding.CreateGamePacket(0, ObjectDetachMessage(player.GUID, item2.GUID, player.Position, 0f, 0f, player.Orientation.z))) //ground + avatarService ! AvatarServiceMessage(player.Continent, AvatarAction.EquipmentOnGround(player.GUID, player.Position, player.Orientation, item2)) + } + + case None => ; //just move item over + } + sendResponse(PacketCoding.CreateGamePacket(0, ObjectAttachMessage(avatar_guid_1, item_guid, dest))) + if(0 <= dest && dest < 5) { + avatarService ! AvatarServiceMessage(player.Continent, AvatarAction.EquipmentInHand(player.GUID, dest, item)) + } + + case None => //restore original contents + indexSlot.Equipment = item + destSlot.Equipment = destItem + } + case None => ; + } case msg @ ChangeAmmoMessage(item_guid, unk1) => log.info("ChangeAmmo: " + msg) @@ -379,7 +921,7 @@ class WorldSessionActor extends Actor with MDCContextAware { sendResponse(PacketCoding.CreateGamePacket(0, GenericObjectStateMsg(object_guid, 16))) } - case msg @ UnuseItemMessage(player, item) => + case msg @ UnuseItemMessage(player_guid, item) => log.info("UnuseItem: " + msg) case msg @ DeployObjectMessage(guid, unk1, pos, roll, pitch, yaw, unk2) => @@ -389,12 +931,24 @@ class WorldSessionActor extends Actor with MDCContextAware { log.info("GenericObjectState: " + msg) case msg @ ItemTransactionMessage(terminal_guid, transaction_type, item_page, item_name, unk1, item_guid) => - if(transaction_type == TransactionType.Sell) { - sendResponse(PacketCoding.CreateGamePacket(0, ObjectDeleteMessage(item_guid, 0))) - sendResponse(PacketCoding.CreateGamePacket(0, ItemTransactionResultMessage(terminal_guid, transaction_type, true))) - } + terminal.Actor ! Terminal.Request(player, msg) log.info("ItemTransaction: " + msg) + case msg @ FavoritesRequest(player_guid, unk, action, line, label) => + if(player.GUID == player_guid) { + val name = label.getOrElse("missing_loadout_name") + action match { + case FavoritesAction.Unknown => ; + case FavoritesAction.Save => + player.SaveLoadout(name, line) + sendResponse(PacketCoding.CreateGamePacket(0, FavoritesMessage(0, player_guid, line, name))) + case FavoritesAction.Delete => + player.DeleteLoadout(line) + sendResponse(PacketCoding.CreateGamePacket(0, FavoritesMessage(0, player_guid, line, ""))) + } + } + log.info("FavoritesRequest: " + msg) + case msg @ WeaponDelayFireMessage(seq_time, weapon_guid) => log.info("WeaponDelayFire: " + msg) @@ -424,7 +978,7 @@ class WorldSessionActor extends Actor with MDCContextAware { sendResponse(PacketCoding.CreateGamePacket(0, msg)) //should be safe; replace with ObjectDetachMessage later log.info("DismountVehicleMsg: " + msg) - case msg @ DeployRequestMessage(player, entity, unk1, unk2, unk3, pos) => + case msg @ DeployRequestMessage(player_guid, entity, unk1, unk2, unk3, pos) => //if you try to deploy, can not undeploy log.info("DeployRequest: " + msg) @@ -456,7 +1010,7 @@ class WorldSessionActor extends Actor with MDCContextAware { case msg @ FriendsRequest(action, friend) => log.info("FriendsRequest: "+msg) - case msg @ HitHint(source, player) => + case msg @ HitHint(source, player_guid) => log.info("HitHint: "+msg) case msg @ WeaponDryFireMessage(weapon) => @@ -468,6 +1022,498 @@ class WorldSessionActor extends Actor with MDCContextAware { case default => log.error(s"Unhandled GamePacket $pkt") } + /** + * Iterate over a group of `EquipmentSlot`s, some of which may be occupied with an item. + * Remove any encountered items and add them to an output `List`. + * @param iter the `Iterator` of `EquipmentSlot`s + * @param index a number that equals the "current" holster slot (`EquipmentSlot`) + * @param list a persistent `List` of `Equipment` in the holster slots + * @return a `List` of `Equipment` in the holster slots + */ + @tailrec private def clearHolsters(iter : Iterator[EquipmentSlot], index : Int = 0, list : List[InventoryItem] = Nil) : List[InventoryItem] = { + if(!iter.hasNext) { + list + } + else { + val slot = iter.next + slot.Equipment match { + case Some(equipment) => + slot.Equipment = None + clearHolsters(iter, index + 1, InventoryItem(equipment, index) +: list) + case None => + clearHolsters(iter, index + 1, list) + } + } + } + + /** + * Iterate over a group of `EquipmentSlot`s, some of which may be occupied with an item. + * For any slots that are not yet occupied by an item, search through the `List` and find an item that fits in that slot. + * Add that item to the slot and remove it from the list. + * @param iter the `Iterator` of `EquipmentSlot`s + * @param list a `List` of all `Equipment` that is not yet assigned to a holster slot or an inventory slot + * @return the `List` of all `Equipment` not yet assigned to a holster slot or an inventory slot + */ + @tailrec private def fillEmptyHolsters(iter : Iterator[EquipmentSlot], list : List[InventoryItem]) : List[InventoryItem] = { + if(!iter.hasNext) { + list + } + else { + val slot = iter.next + if(slot.Equipment.isEmpty) { + list.find(item => item.obj.Size == slot.Size) match { + case Some(obj) => + val index = list.indexOf(obj) + slot.Equipment = obj.obj + fillEmptyHolsters(iter, list.take(index) ++ list.drop(index + 1)) + case None => + fillEmptyHolsters(iter, list) + } + } + else { + fillEmptyHolsters(iter, list) + } + } + } + + /** + * Iterate over a group of `EquipmentSlot`s, some of which may be occupied with an item. + * Use `func` on any discovered `Equipment` to transform items into tasking, and add the tasking to a `List`. + * @param iter the `Iterator` of `EquipmentSlot`s + * @param func the function used to build tasking from any discovered `Equipment` + * @param list a persistent `List` of `Equipment` tasking + * @return a `List` of `Equipment` tasking + */ + @tailrec private def recursiveHolsterTaskBuilding(iter : Iterator[EquipmentSlot], func : ((Equipment)=>TaskResolver.GiveTask), list : List[TaskResolver.GiveTask] = Nil) : List[TaskResolver.GiveTask] = { + if(!iter.hasNext) { + list + } + else { + iter.next.Equipment match { + case Some(item) => + recursiveHolsterTaskBuilding(iter, func, list :+ func(item)) + case None => + recursiveHolsterTaskBuilding(iter, func, list) + } + } + } + + /** + * Construct tasking that coordinates the following:
+ * 1) Accept a new piece of `Equipment` and register it with a globally unique identifier.
+ * 2) Once it is registered, give the `Equipment` to `target`. + * @param target what object will accept the new `Equipment` + * @param obj the new `Equipment` + * @param index the slot where the new `Equipment` will be placed + * @see `RegisterEquipment` + * @see `PutInSlot` + */ + private def PutEquipmentInSlot(target : Player, obj : Equipment, index : Int) : Unit = { + val regTask = RegisterEquipment(obj) + obj match { + case tool : Tool => + val linearToolTask = TaskResolver.GiveTask(regTask.task) +: regTask.subs + taskResolver ! TaskResolver.GiveTask(PutInSlot(target, tool, index).task, linearToolTask) + case _ => + taskResolver ! TaskResolver.GiveTask(PutInSlot(target, obj, index).task, List(regTask)) + } + } + + /** + * Construct tasking that coordinates the following:
+ * 1) Remove a new piece of `Equipment` from where it is currently stored.
+ * 2) Once it is removed, un-register the `Equipment`'s globally unique identifier. + * @param target the object that currently possesses the `Equipment` + * @param obj the `Equipment` + * @param index the slot from where the `Equipment` will be removed + * @see `UnregisterEquipment` + * @see `RemoveFromSlot` + */ + private def RemoveEquipmentFromSlot(target : Player, obj : Equipment, index : Int) : Unit = { + val regTask = UnregisterEquipment(obj) + //to avoid an error from a GUID-less object from being searchable, it is removed from the inventory first + obj match { + case _ : Tool => + taskResolver ! TaskResolver.GiveTask(regTask.task, RemoveFromSlot(target, obj, index) +: regTask.subs) + case _ => + taskResolver ! TaskResolver.GiveTask(regTask.task, RemoveFromSlot(target, obj, index) :: Nil) + } + } + + /** + * Construct tasking that registers an object with the a globally unique identifier selected from a pool of numbers. + * The object in question is not considered to have any form of internal complexity. + * @param obj the object being registered + * @return a `TaskResolver.GiveTask` message + */ + private def RegisterObjectTask(obj : IdentifiableEntity) : TaskResolver.GiveTask = { + TaskResolver.GiveTask( + new Task() { + private val localObject = obj + private val localAccessor = accessor + + override def isComplete : Task.Resolution.Value = { + try { + localObject.GUID + Task.Resolution.Success + } + catch { + case _ : Exception => + Task.Resolution.Incomplete + } + } + + def Execute(resolver : ActorRef) : Unit = { + localAccessor ! Register(localObject, resolver) + } + }) + } + + /** + * Construct tasking that registers an object that is an object of type `Tool`. + * `Tool` objects have internal structures called "ammo slots;" + * each ammo slot contains a register-able `AmmoBox` object. + * @param obj the object being registered + * @return a `TaskResolver.GiveTask` message + */ + private def RegisterTool(obj : Tool) : TaskResolver.GiveTask = { + val ammoTasks : List[TaskResolver.GiveTask] = (0 until obj.MaxAmmoSlot).map(ammoIndex => RegisterObjectTask(obj.AmmoSlots(ammoIndex).Box)).toList + TaskResolver.GiveTask(RegisterObjectTask(obj).task, ammoTasks) + } + + /** + * Construct tasking that registers an object, determining whether it is a complex object of type `Tool` or a more simple object type. + * @param obj the object being registered + * @return a `TaskResolver.GiveTask` message + */ + private def RegisterEquipment(obj : Equipment) : TaskResolver.GiveTask = { + obj match { + case tool : Tool => + RegisterTool(tool) + case _ => + RegisterObjectTask(obj) + } + } + + /** + * Construct tasking that gives the `Equipment` to `target`. + * @param target what object will accept the new `Equipment` + * @param obj the new `Equipment` + * @param index the slot where the new `Equipment` will be placed + * @return a `TaskResolver.GiveTask` message + */ + private def PutInSlot(target : Player, obj : Equipment, index : Int) : TaskResolver.GiveTask = { + TaskResolver.GiveTask( + new Task() { + private val localTarget = target + private val localIndex = index + private val localObject = obj + private val localAnnounce = self + + override def isComplete : Task.Resolution.Value = { + if(localTarget.Slot(localIndex).Equipment.contains(localObject)) { + Task.Resolution.Success + } + else { + Task.Resolution.Incomplete + } + } + + def Execute(resolver : ActorRef) : Unit = { + localTarget.Slot(localIndex).Equipment = localObject + resolver ! scala.util.Success(localObject) + } + + override def onSuccess() : Unit = { + val definition = localObject.Definition + localAnnounce ! WorldSessionActor.ResponseToSelf( + PacketCoding.CreateGamePacket(0, + ObjectCreateDetailedMessage( + definition.ObjectId, + localObject.GUID, + ObjectCreateMessageParent(localTarget.GUID, localIndex), + definition.Packet.DetailedConstructorData(localObject).get + ) + ) + ) + if(0 <= localIndex && localIndex < 5) { + avatarService ! AvatarServiceMessage(localTarget.Continent, AvatarAction.EquipmentInHand(localTarget.GUID, localIndex, localObject)) + } + } + }) + } + + /** + * Construct tasking that registers all aspects of a `Player` avatar. + * `Players` are complex objects that contain a variety of other register-able objects and each of these objects much be handled. + * @param tplayer the avatar `Player` + * @return a `TaskResolver.GiveTask` message + */ + private def RegisterAvatar(tplayer : Player) : TaskResolver.GiveTask = { + val holsterTasks = recursiveHolsterTaskBuilding(tplayer.Holsters().iterator, RegisterEquipment) + val fifthHolsterTask = tplayer.Slot(5).Equipment match { + case Some(item) => + RegisterEquipment(item) :: Nil + case None => + List.empty[TaskResolver.GiveTask]; + } + val inventoryTasks = tplayer.Inventory.Items.map({ case((_ : Int, entry : InventoryItem)) => RegisterEquipment(entry.obj)}) + TaskResolver.GiveTask( + new Task() { + private val localPlayer = tplayer + private val localAnnounce = self + + override def isComplete : Task.Resolution.Value = { + Task.Resolution.Incomplete + } + + def Execute(resolver : ActorRef) : Unit = { + localAnnounce ! PlayerLoaded(localPlayer) //alerts WSA + resolver ! scala.util.Success(localPlayer) + } + }, RegisterObjectTask(tplayer) +: (holsterTasks ++ fifthHolsterTask ++ inventoryTasks) + ) + } + + /** + * Construct tasking that un-registers an object. + * The object in question is not considered to have any form of internal complexity. + * @param obj the object being un-registered + * @return a `TaskResolver.GiveTask` message + */ + private def UnregisterObjectTask(obj : IdentifiableEntity) : TaskResolver.GiveTask = { + TaskResolver.GiveTask( + new Task() { + private val localObject = obj + private val localAccessor = accessor + + override def isComplete : Task.Resolution.Value = { + try { + localObject.GUID + Task.Resolution.Incomplete + } + catch { + case _ : Exception => + Task.Resolution.Success + } + } + + def Execute(resolver : ActorRef) : Unit = { + localAccessor ! Unregister(localObject, resolver) + } + } + ) + } + + /** + * Construct tasking that un-registers an object that is an object of type `Tool`. + * `Tool` objects have internal structures called "ammo slots;" + * each ammo slot contains a register-able `AmmoBox` object. + * @param obj the object being un-registered + * @return a `TaskResolver.GiveTask` message + */ + private def UnregisterTool(obj : Tool) : TaskResolver.GiveTask = { + val ammoTasks : List[TaskResolver.GiveTask] = (0 until obj.MaxAmmoSlot).map(ammoIndex => UnregisterObjectTask(obj.AmmoSlots(ammoIndex).Box)).toList + TaskResolver.GiveTask(UnregisterObjectTask(obj).task, ammoTasks) + } + + /** + * Construct tasking that un-registers an object, determining whether it is a complex object of type `Tool` or a more simple object type. + * @param obj the object being registered + * @return a `TaskResolver.GiveTask` message + */ + private def UnregisterEquipment(obj : Equipment) : TaskResolver.GiveTask = { + obj match { + case tool : Tool => + UnregisterTool(tool) + case _ => + UnregisterObjectTask(obj) + } + } + + /** + * Construct tasking that removes the `Equipment` to `target`. + * @param target what object that contains the `Equipment` + * @param obj the `Equipment` + * @param index the slot where the `Equipment` is stored + * @return a `TaskResolver.GiveTask` message + */ + private def RemoveFromSlot(target : Player, obj : Equipment, index : Int) : TaskResolver.GiveTask = { + TaskResolver.GiveTask( + new Task() { + private val localTarget = target + private val localIndex = index + private val localObject = obj + private val localObjectGUID = obj.GUID + private val localAnnounce = self //self may not be the same when it executes + + override def isComplete : Task.Resolution.Value = { + if(localTarget.Slot(localIndex).Equipment.contains(localObject)) { + Task.Resolution.Incomplete + } + else { + Task.Resolution.Success + } + } + + def Execute(resolver : ActorRef) : Unit = { + localTarget.Slot(localIndex).Equipment = None + resolver ! scala.util.Success(localObject) + } + + override def onSuccess() : Unit = { + localAnnounce ! WorldSessionActor.ResponseToSelf(PacketCoding.CreateGamePacket(0, ObjectDeleteMessage(localObjectGUID, 0))) + if(0 <= localIndex && localIndex < 5) { + avatarService ! AvatarServiceMessage(localTarget.Continent, AvatarAction.ObjectDelete(localTarget.GUID, localObjectGUID)) + } + } + }) + } + + /** + * Construct tasking that un-registers all aspects of a `Player` avatar. + * `Players` are complex objects that contain a variety of other register-able objects and each of these objects much be handled. + * @param tplayer the avatar `Player` + * @return a `TaskResolver.GiveTask` message + */ + private def UnregisterAvatar(tplayer : Player) : TaskResolver.GiveTask = { + val holsterTasks = recursiveHolsterTaskBuilding(tplayer.Holsters().iterator, UnregisterEquipment) + val inventoryTasks = tplayer.Inventory.Items.map({ case((_ : Int, entry : InventoryItem)) => UnregisterEquipment(entry.obj)}) + val fifthHolsterTask = tplayer.Slot(5).Equipment match { + case Some(item) => + UnregisterEquipment(item) :: Nil + case None => + List.empty[TaskResolver.GiveTask]; + } + TaskResolver.GiveTask(UnregisterObjectTask(tplayer).task, holsterTasks ++ fifthHolsterTask ++ inventoryTasks) + } + + /** + * After a client has connected to the server, their account is used to generate a list of characters. + * On the character selection screen, each of these characters is made to exist temporarily when one is selected. + * This "character select screen" is an isolated portion of the client, so it does not have any external constraints. + * Temporary global unique identifiers are assigned to the underlying `Player` objects so that they can be turned into packets. + * @param tplayer the `Player` object + * @param gen a constant source of incremental unique numbers + */ + private def SetCharacterSelectScreenGUID(tplayer : Player, gen : AtomicInteger) : Unit = { + tplayer.Holsters().foreach(holster => { + SetCharacterSelectScreenGUID_SelectEquipment(holster.Equipment, gen) + }) + tplayer.Inventory.Items.foreach({ case((_, entry : InventoryItem)) => + SetCharacterSelectScreenGUID_SelectEquipment(Some(entry.obj), gen) + }) + tplayer.Slot(5).Equipment.get.GUID = PlanetSideGUID(gen.getAndIncrement) + tplayer.GUID = PlanetSideGUID(gen.getAndIncrement) + } + + /** + * Assists in assigning temporary global unique identifiers. + * If the item is a `Tool`, handle the embedded `AmmoBox` objects in each ammunition slot. + * Whether or not, give the object itself a GUID as well. + * @param item the piece of `Equipment` + * @param gen a constant source of incremental unique numbers + */ + private def SetCharacterSelectScreenGUID_SelectEquipment(item : Option[Equipment], gen : AtomicInteger) : Unit = { + item match { + case Some(tool : Tool) => + tool.AmmoSlots.foreach(slot => { slot.Box.GUID = PlanetSideGUID(gen.getAndIncrement) }) + tool.GUID = PlanetSideGUID(gen.getAndIncrement) + case Some(item : Equipment) => + item.GUID = PlanetSideGUID(gen.getAndIncrement) + case None => ; + } + } + + /** + * After the user has selected a character to load from the "character select screen," + * the temporary global unique identifiers used for that screen are stripped from the underlying `Player` object that was selected. + * Characters that were not selected may be destroyed along with their temporary GUIDs. + * @param tplayer the `Player` object + */ + private def RemoveCharacterSelectScreenGUID(tplayer : Player) : Unit = { + tplayer.Holsters().foreach(holster => { + RemoveCharacterSelectScreenGUID_SelectEquipment(holster.Equipment) + }) + tplayer.Inventory.Items.foreach({ case((_, entry : InventoryItem)) => + RemoveCharacterSelectScreenGUID_SelectEquipment(Some(entry.obj)) + }) + tplayer.Slot(5).Equipment.get.Invalidate() + tplayer.Invalidate() + } + + /** + * Assists in stripping temporary global unique identifiers. + * If the item is a `Tool`, handle the embedded `AmmoBox` objects in each ammunition slot. + * Whether or not, remove the GUID from the object itself. + * @param item the piece of `Equipment` + */ + private def RemoveCharacterSelectScreenGUID_SelectEquipment(item : Option[Equipment]) : Unit = { + item match { + case Some(item : Tool) => + item.AmmoSlots.foreach(slot => { slot.Box.Invalidate() }) + item.Invalidate() + case Some(item : Equipment) => + item.Invalidate() + case None => ; + } + } + + /** + * Add an object to the local `List` of objects on the ground. + * @param item the `Equipment` to be dropped + * @param pos where the `item` will be dropped + * @param orient in what direction the item will face when dropped + * @return the global unique identifier of the object + */ + private def DropItemOnGround(item : Equipment, pos : Vector3, orient : Vector3) : PlanetSideGUID = { + item.Position = pos + item.Orientation = orient + WorldSessionActor.equipmentOnGround += item + item.GUID + } + + // private def FindItemOnGround(item_guid : PlanetSideGUID) : Option[Equipment] = { + // equipmentOnGround.find(item => item.GUID == item_guid) + // } + + /** + * Remove an object from the local `List` of objects on the ground. + * @param item_guid the `Equipment` to be picked up + * @return the object being picked up + */ + private def PickupItemFromGround(item_guid : PlanetSideGUID) : Option[Equipment] = { + recursiveFindItemOnGround(WorldSessionActor.equipmentOnGround.iterator, item_guid) match { + case Some(index) => + Some(WorldSessionActor.equipmentOnGround.remove(index)) + case None => + None + } + } + + /** + * Shift through objects on the ground to find the location of a specific item. + * @param iter an `Iterator` of `Equipment` + * @param item_guid the global unique identifier of the piece of `Equipment` being sought + * @param index the current position in the array-list structure used to create the `Iterator` + * @return the index of the object matching `item_guid`, if found; + * `None`, otherwise + */ + @tailrec private def recursiveFindItemOnGround(iter : Iterator[Equipment], item_guid : PlanetSideGUID, index : Int = 0) : Option[Int] = { + if(!iter.hasNext) { + None + } + else { + val item : Equipment = iter.next + if(item.GUID == item_guid) { + Some(index) + } + else { + recursiveFindItemOnGround(iter, item_guid, index + 1) + } + } + } + def failWithError(error : String) = { log.error(error) //sendResponse(PacketCoding.CreateControlPacket(ConnectionClose())) @@ -488,3 +1534,32 @@ class WorldSessionActor extends Actor with MDCContextAware { sendResponse(RawPacket(pkt)) } } + +object WorldSessionActor { + final case class ResponseToSelf(pkt : GamePacket) + + /** + * A placeholder `Cancellable` object. + */ + private final val DefaultCancellable = new Cancellable() { + def cancel : Boolean = true + def isCancelled() : Boolean = true + } + + //TODO this is a temporary local system; replace it in the future + //in the future, items dropped on the ground will be managed by a data structure on an external Actor representing the continent + //like so: WSA -> /GetItemOnGround/ -> continent -> /GiveItemFromGround/ -> WSA + import scala.collection.mutable.ListBuffer + private val equipmentOnGround : ListBuffer[Equipment] = ListBuffer[Equipment]() + + def Distance(pos1 : Vector3, pos2 : Vector3) : Float = { + math.sqrt(DistanceSquared(pos1, pos2)).toFloat + } + + def DistanceSquared(pos1 : Vector3, pos2 : Vector3) : Float = { + val dx : Float = pos1.x - pos2.x + val dy : Float = pos1.y - pos2.y + val dz : Float = pos1.z - pos2.z + (dx * dx) + (dy * dy) + (dz * dz) + } +}