From 5f3e7e5df8234a90aea92ccb18464bd77e6ec659 Mon Sep 17 00:00:00 2001 From: Fate-JH Date: Sun, 23 Sep 2018 08:00:58 -0400 Subject: [PATCH] Deployables (#230) * functions for certifcation ui updates (that don't work) * initialization of combat engineering deployables ui on load and certification change * representation classes for ACE and FDU; ability to pull ACE and FDU from equipment terminals * ammo change functionality and fire mode change functionality for ConstructionItems refactored from Tool operations and supported properly (switch between deployable options) * zone-specific structure for keeping track of deployables; abaility to dismiss deployables from the map screen (previous functionality); local client creation of explosive-type deployables * refactored MannedTurret into FacilityTurret and lesser traits to be used in the deployable spitfires and the OMFT's; all ACE deployables are available for placement; partial management of the construction items after the deployable is placed; boomers create boomer triggers * Avatar-specific storage for deployables and for updating UI elements * refactored quite a bit of code in WSA for the benefit of deployable management; refinements to deployable creation; server messages about deployable quantities; corrected the FDU encoding pattern; lots of work dedicated just to synchronizing BoomerTrigger objects * added RemoverActor for deployables and redistributed deconstruction functionality away from WSA to new DeployableRemover; added events to facilitate activities not inheritable with this model * refactored and distributed Deployables classes; copious amounts of testing and document-writing * boomers now explode from trigger; support for deployables being destroyed by weapon discharge, including individual health, soure identification, and damage model; shuffled deployable classes to build different hierarchy * sensor_shield was skipped by accident * identified stray object in Hanish, Ishundar, and added Irkalla, Ishundar's capture console; fixed issue with Warp command and 'Irkalla'; modified building amenity setup and setup testing in Zone; players load and die properly when seated in an omft; reserve ammunition in omft properly registered * added local service channel, capture consoles, fixed tests as much as posible * fixed LocalService tests by booting the ServiceManager; added avatar and local tests * a simple attempt to refactor Actor messages in a way that is acceptable to Travis CI * making the explosive deployables vanish upon explosion; sensor health bars are now supported --- .../scala/net/psforever/objects/Avatar.scala | 5 + .../psforever/objects/BoomerDeployable.scala | 24 + .../net/psforever/objects/BoomerTrigger.scala | 6 + .../psforever/objects/ConstructionItem.scala | 47 +- .../net/psforever/objects/Deployables.scala | 28 + .../objects/ExplosiveDeployable.scala | 16 + .../psforever/objects/GlobalDefinitions.scala | 318 +- .../psforever/objects/SensorDeployable.scala | 9 + .../objects/ShieldGeneratorDeployable.scala | 15 + .../scala/net/psforever/objects/Tool.scala | 5 +- .../psforever/objects/TrapDeployable.scala | 7 + .../psforever/objects/TurretDeployable.scala | 89 + .../objects/avatar/DeployableToolbox.scala | 668 +++ .../ballistics/ComplexDeployableSource.scala | 51 + .../objects/ballistics/DeployableSource.scala | 36 + .../objects/ballistics/SourceEntry.scala | 6 +- .../objects/ce/ComplexDeployable.scala | 27 + .../net/psforever/objects/ce/Deployable.scala | 151 + .../objects/ce/DeployableCategory.scala | 17 + .../psforever/objects/ce/DeployedItem.scala | 22 + .../objects/ce/SimpleDeployable.scala | 14 + .../objects/definition/BasicDefinition.scala | 10 + .../ConstructionItemDefinition.scala | 34 +- .../definition/DeployableDefinition.scala | 72 + .../converter/FieldTurretConverter.scala | 69 + .../definition/converter/SeatConverter.scala | 31 + .../converter/ShieldGeneratorConverter.scala | 52 + .../converter/SmallDeployableConverter.scala | 32 + .../converter/SmallTurretConverter.scala | 69 + .../definition/converter/TRAPConverter.scala | 54 + .../converter/VehicleConverter.scala | 32 +- .../psforever/objects/equipment/CItem.scala | 28 +- .../objects/equipment/RemoteUnit.scala | 24 + .../net/psforever/objects/guid/GUIDTask.scala | 17 +- .../psforever/objects/loadouts/Loadout.scala | 8 + .../mount/MountableBehavior.scala | 23 +- .../EquipmentTerminalDefinition.scala | 16 +- .../serverobject/turret/FacilityTurret.scala | 56 + ...trol.scala => FacilityTurretControl.scala} | 2 +- .../serverobject/turret/MannedTurret.scala | 224 - ...efinition.scala => TurretDefinition.scala} | 2 +- .../serverobject/turret/WeaponTurret.scala | 177 + .../psforever/objects/vehicles/Turrets.scala | 13 +- .../objects/vital/StandardDamages.scala | 6 + .../objects/vital/StandardResolutions.scala | 12 + .../psforever/objects/vital/Vitality.scala | 2 + .../resolution/DamageResistCalculations.scala | 17 +- .../resolution/ResolutionCalculations.scala | 48 +- .../net/psforever/objects/zones/Zone.scala | 115 +- .../psforever/objects/zones/ZoneActor.scala | 29 +- .../objects/zones/ZoneDeployableActor.scala | 204 + .../packet/game/DeployObjectMessage.scala | 27 +- .../packet/game/ObjectDeployedMessage.scala | 26 +- .../game/PlanetsideAttributeMessage.scala | 13 +- .../packet/game/TriggerEffectMessage.scala | 44 +- .../objectcreate/LargeDeployableData.scala | 71 + .../game/objectcreate/ObjectClass.scala | 4 +- .../game/objectcreate/ObjectCreateBase.scala | 4 +- .../OneMannedFieldTurretData.scala | 63 +- .../objectcreate/SmallDeployableData.scala | 49 +- .../game/objectcreate/SmallTurretData.scala | 44 +- .../packet/game/objectcreate/TRAPData.scala | 11 +- .../scala/net/psforever/types/Vector3.scala | 7 + .../main/scala/services/RemoverActor.scala | 13 +- .../scala/services/avatar/AvatarAction.scala | 8 +- .../services/avatar/AvatarResponse.scala | 6 +- .../scala/services/avatar/AvatarService.scala | 18 +- .../avatar/support/DroppedItemRemover.scala | 2 +- .../scala/services/galaxy/GalaxyAction.scala | 2 +- .../services/galaxy/GalaxyResponse.scala | 2 +- .../scala/services/local/LocalAction.scala | 9 +- .../scala/services/local/LocalResponse.scala | 9 +- .../scala/services/local/LocalService.scala | 141 +- .../services/local/LocalServiceMessage.scala | 4 + .../local/support/DeployableRemover.scala | 86 + .../vehicle/support/TurretUpgrader.scala | 10 +- common/src/test/scala/Vector3Test.scala | 8 + .../scala/game/DeployObjectMessageTest.scala | 12 +- .../game/ObjectDeployedMessageTest.scala | 6 +- .../scala/game/TriggerEffectMessageTest.scala | 45 +- .../AegisShieldGeneratorDataTest.scala | 12 +- .../OneMannedFieldTurretDataTest.scala | 40 +- .../SmallDeployableDataTest.scala | 24 +- .../objectcreate/SmallTurretDataTest.scala | 66 +- .../game/objectcreate/TRAPDataTest.scala | 17 +- .../test/scala/objects/ConverterTest.scala | 165 +- .../test/scala/objects/DeployableTest.scala | 330 ++ .../scala/objects/DeployableToolboxTest.scala | 1054 ++++ .../test/scala/objects/EquipmentTest.scala | 112 +- .../src/test/scala/objects/ExoSuitTest.scala | 3 - ...retTest.scala => FacilityTurretTest.scala} | 46 +- .../test/scala/objects/ResourceSiloTest.scala | 2 +- .../objects/ServerObjectBuilderTest.scala | 14 +- ...t.scala => GUIDTaskRegisterAmmoTest.scala} | 2 +- ...scala => GUIDTaskRegisterAvatarTest.scala} | 2 +- ...scala => GUIDTaskRegisterObjectTest.scala} | 2 +- ...scala => GUIDTaskRegisterPlayerTest.scala} | 2 +- ...t.scala => GUIDTaskRegisterToolTest.scala} | 2 +- .../guidtask/GUIDTaskRegisterTurretTest.scala | 27 + ...cala => GUIDTaskRegisterVehicleTest.scala} | 2 +- ...scala => GUIDTaskUnregisterAmmoTest.scala} | 2 +- ...ala => GUIDTaskUnregisterAvatarTest.scala} | 2 +- ...ala => GUIDTaskUnregisterObjectTest.scala} | 2 +- ...ala => GUIDTaskUnregisterPlayerTest.scala} | 2 +- ...scala => GUIDTaskUnregisterToolTest.scala} | 2 +- .../GUIDTaskUnregisterTurretTest.scala | 31 + ...la => GUIDTaskUnregisterVehicleTest.scala} | 2 +- .../number/UniqueNumberSystemTest.scala | 29 +- .../scala/service/AvatarServiceTest.scala | 62 + .../test/scala/service/LocalServiceTest.scala | 132 +- pslogin/src/main/scala/Maps.scala | 64 +- .../src/main/scala/WorldSessionActor.scala | 4428 +++++++++++------ pslogin/src/main/scala/csr/CSRZoneImpl.scala | 2 +- 113 files changed, 8155 insertions(+), 2312 deletions(-) create mode 100644 common/src/main/scala/net/psforever/objects/BoomerDeployable.scala create mode 100644 common/src/main/scala/net/psforever/objects/BoomerTrigger.scala create mode 100644 common/src/main/scala/net/psforever/objects/Deployables.scala create mode 100644 common/src/main/scala/net/psforever/objects/ExplosiveDeployable.scala create mode 100644 common/src/main/scala/net/psforever/objects/SensorDeployable.scala create mode 100644 common/src/main/scala/net/psforever/objects/ShieldGeneratorDeployable.scala create mode 100644 common/src/main/scala/net/psforever/objects/TrapDeployable.scala create mode 100644 common/src/main/scala/net/psforever/objects/TurretDeployable.scala create mode 100644 common/src/main/scala/net/psforever/objects/avatar/DeployableToolbox.scala create mode 100644 common/src/main/scala/net/psforever/objects/ballistics/ComplexDeployableSource.scala create mode 100644 common/src/main/scala/net/psforever/objects/ballistics/DeployableSource.scala create mode 100644 common/src/main/scala/net/psforever/objects/ce/ComplexDeployable.scala create mode 100644 common/src/main/scala/net/psforever/objects/ce/Deployable.scala create mode 100644 common/src/main/scala/net/psforever/objects/ce/DeployableCategory.scala create mode 100644 common/src/main/scala/net/psforever/objects/ce/DeployedItem.scala create mode 100644 common/src/main/scala/net/psforever/objects/ce/SimpleDeployable.scala create mode 100644 common/src/main/scala/net/psforever/objects/definition/DeployableDefinition.scala create mode 100644 common/src/main/scala/net/psforever/objects/definition/converter/FieldTurretConverter.scala create mode 100644 common/src/main/scala/net/psforever/objects/definition/converter/SeatConverter.scala create mode 100644 common/src/main/scala/net/psforever/objects/definition/converter/ShieldGeneratorConverter.scala create mode 100644 common/src/main/scala/net/psforever/objects/definition/converter/SmallDeployableConverter.scala create mode 100644 common/src/main/scala/net/psforever/objects/definition/converter/SmallTurretConverter.scala create mode 100644 common/src/main/scala/net/psforever/objects/definition/converter/TRAPConverter.scala create mode 100644 common/src/main/scala/net/psforever/objects/equipment/RemoteUnit.scala create mode 100644 common/src/main/scala/net/psforever/objects/serverobject/turret/FacilityTurret.scala rename common/src/main/scala/net/psforever/objects/serverobject/turret/{MannedTurretControl.scala => FacilityTurretControl.scala} (95%) delete mode 100644 common/src/main/scala/net/psforever/objects/serverobject/turret/MannedTurret.scala rename common/src/main/scala/net/psforever/objects/serverobject/turret/{MannedTurretDefinition.scala => TurretDefinition.scala} (94%) create mode 100644 common/src/main/scala/net/psforever/objects/serverobject/turret/WeaponTurret.scala create mode 100644 common/src/main/scala/net/psforever/objects/zones/ZoneDeployableActor.scala create mode 100644 common/src/main/scala/net/psforever/packet/game/objectcreate/LargeDeployableData.scala create mode 100644 common/src/main/scala/services/local/support/DeployableRemover.scala create mode 100644 common/src/test/scala/objects/DeployableTest.scala create mode 100644 common/src/test/scala/objects/DeployableToolboxTest.scala rename common/src/test/scala/objects/{MannedTurretTest.scala => FacilityTurretTest.scala} (79%) rename common/src/test/scala/objects/guidtask/{GUIDTaskRegister2Test.scala => GUIDTaskRegisterAmmoTest.scala} (91%) rename common/src/test/scala/objects/guidtask/{GUIDTaskRegister5Test.scala => GUIDTaskRegisterAvatarTest.scala} (96%) rename common/src/test/scala/objects/guidtask/{GUIDTaskRegister1Test.scala => GUIDTaskRegisterObjectTest.scala} (90%) rename common/src/test/scala/objects/guidtask/{GUIDTaskRegister6Test.scala => GUIDTaskRegisterPlayerTest.scala} (96%) rename common/src/test/scala/objects/guidtask/{GUIDTaskRegister3Test.scala => GUIDTaskRegisterToolTest.scala} (93%) create mode 100644 common/src/test/scala/objects/guidtask/GUIDTaskRegisterTurretTest.scala rename common/src/test/scala/objects/guidtask/{GUIDTaskRegister4Test.scala => GUIDTaskRegisterVehicleTest.scala} (95%) rename common/src/test/scala/objects/guidtask/{GUIDTaskUnregister2Test.scala => GUIDTaskUnregisterAmmoTest.scala} (92%) rename common/src/test/scala/objects/guidtask/{GUIDTaskUnregister5Test.scala => GUIDTaskUnregisterAvatarTest.scala} (96%) rename common/src/test/scala/objects/guidtask/{GUIDTaskUnregister1Test.scala => GUIDTaskUnregisterObjectTest.scala} (90%) rename common/src/test/scala/objects/guidtask/{GUIDTaskUnregister6Test.scala => GUIDTaskUnregisterPlayerTest.scala} (96%) rename common/src/test/scala/objects/guidtask/{GUIDTaskUnregister3Test.scala => GUIDTaskUnregisterToolTest.scala} (93%) create mode 100644 common/src/test/scala/objects/guidtask/GUIDTaskUnregisterTurretTest.scala rename common/src/test/scala/objects/guidtask/{GUIDTaskUnregister4Test.scala => GUIDTaskUnregisterVehicleTest.scala} (95%) diff --git a/common/src/main/scala/net/psforever/objects/Avatar.scala b/common/src/main/scala/net/psforever/objects/Avatar.scala index 8c2f4cd4..9452a6a6 100644 --- a/common/src/main/scala/net/psforever/objects/Avatar.scala +++ b/common/src/main/scala/net/psforever/objects/Avatar.scala @@ -1,6 +1,7 @@ // Copyright (c) 2017 PSForever package net.psforever.objects +import net.psforever.objects.avatar.DeployableToolbox import net.psforever.objects.definition.{AvatarDefinition, ImplantDefinition} import net.psforever.objects.equipment.EquipmentSize import net.psforever.objects.loadouts.Loadout @@ -37,6 +38,8 @@ class Avatar(val name : String, val faction : PlanetSideEmpire.Value, val sex : } } + private val deployables : DeployableToolbox = new DeployableToolbox + def BEP : Long = bep def BEP_=(battleExperiencePoints : Long) : Long = { @@ -177,6 +180,8 @@ class Avatar(val name : String, val faction : PlanetSideEmpire.Value, val sex : } } + def Deployables : DeployableToolbox = deployables + def Definition : AvatarDefinition = GlobalDefinitions.avatar /* diff --git a/common/src/main/scala/net/psforever/objects/BoomerDeployable.scala b/common/src/main/scala/net/psforever/objects/BoomerDeployable.scala new file mode 100644 index 00000000..f218da87 --- /dev/null +++ b/common/src/main/scala/net/psforever/objects/BoomerDeployable.scala @@ -0,0 +1,24 @@ +// Copyright (c) 2017 PSForever +package net.psforever.objects + +import net.psforever.objects.definition.DeployableDefinition + +class BoomerDeployable(cdef : DeployableDefinition) extends ExplosiveDeployable(cdef) { + private var trigger : Option[BoomerTrigger] = None + + def Trigger : Option[BoomerTrigger] = trigger + + def Trigger_=(item : BoomerTrigger) : Option[BoomerTrigger] = { + if(trigger.isEmpty) { //can only set trigger once + trigger = Some(item) + } + Trigger + } + + def Trigger_=(item : Option[BoomerTrigger]) : Option[BoomerTrigger] = { + if(item.isEmpty) { + trigger = None + } + Trigger + } +} diff --git a/common/src/main/scala/net/psforever/objects/BoomerTrigger.scala b/common/src/main/scala/net/psforever/objects/BoomerTrigger.scala new file mode 100644 index 00000000..dfc3fec3 --- /dev/null +++ b/common/src/main/scala/net/psforever/objects/BoomerTrigger.scala @@ -0,0 +1,6 @@ +// Copyright (c) 2017 PSForever +package net.psforever.objects + +import net.psforever.objects.equipment.RemoteUnit + +class BoomerTrigger extends SimpleItem(GlobalDefinitions.boomer_trigger) with RemoteUnit diff --git a/common/src/main/scala/net/psforever/objects/ConstructionItem.scala b/common/src/main/scala/net/psforever/objects/ConstructionItem.scala index 6d7b1e2b..fb8fb705 100644 --- a/common/src/main/scala/net/psforever/objects/ConstructionItem.scala +++ b/common/src/main/scala/net/psforever/objects/ConstructionItem.scala @@ -1,26 +1,61 @@ // Copyright (c) 2017 PSForever package net.psforever.objects -import net.psforever.objects.definition.ConstructionItemDefinition -import net.psforever.objects.equipment.{CItem, Equipment, FireModeSwitch} +import net.psforever.objects.ce.DeployedItem +import net.psforever.objects.definition.{ConstructionFireMode, ConstructionItemDefinition} +import net.psforever.objects.equipment.{Equipment, FireModeSwitch} +import net.psforever.types.CertificationType -class ConstructionItem(private val cItemDef : ConstructionItemDefinition) extends Equipment with FireModeSwitch[CItem.DeployedItem.Value] { +/** + * A type of `Equipment` that can be wielded and applied to the game world to produce other game objects.
+ *
+ * Functionally, `ConstructionItem` objects resemble `Tool` objects that have fire mode state and alternate "ammunition." + * Very much unlike `Tool` object counterparts, however, + * the alternate "ammunition" is also a type of fire mode state + * maintained in a two-dimensional grid of related states. + * These states represent output products called deployables or, in the common vernacular, CE. + * Also unlike `Tool` objects, whose ammunition is always available even when drawing the weapon is not permitted, + * the different states are not all available if just the equipment itself is available. + * Parameters along with these CE states + * indicate whether the current output product is something the player is permitted to utilize. + * @param cItemDef the `ObjectDefinition` that constructs this item and maintains some of its immutable fields + */ +class ConstructionItem(private val cItemDef : ConstructionItemDefinition) extends Equipment + with FireModeSwitch[ConstructionFireMode] { private var fireModeIndex : Int = 0 + private var ammoTypeIndex : Int = 0 def FireModeIndex : Int = fireModeIndex def FireModeIndex_=(index : Int) : Int = { - fireModeIndex = index % cItemDef.Modes.length + fireModeIndex = index % Definition.Modes.length FireModeIndex } - def FireMode : CItem.DeployedItem.Value = cItemDef.Modes(fireModeIndex) + def FireMode : ConstructionFireMode = Definition.Modes(fireModeIndex) - def NextFireMode : CItem.DeployedItem.Value = { + def NextFireMode : ConstructionFireMode = { FireModeIndex = FireModeIndex + 1 + ammoTypeIndex = 0 FireMode } + def AmmoTypeIndex : Int = ammoTypeIndex + + def AmmoTypeIndex_=(index : Int) : Int = { + ammoTypeIndex = index % FireMode.Deployables.length + AmmoTypeIndex + } + + def AmmoType : DeployedItem.Value = FireMode.Deployables(ammoTypeIndex) + + def NextAmmoType : DeployedItem.Value = { + AmmoTypeIndex = AmmoTypeIndex + 1 + FireMode.Deployables(ammoTypeIndex) + } + + def ModePermissions : Set[CertificationType.Value] = FireMode.Permissions(ammoTypeIndex) + def Definition : ConstructionItemDefinition = cItemDef } diff --git a/common/src/main/scala/net/psforever/objects/Deployables.scala b/common/src/main/scala/net/psforever/objects/Deployables.scala new file mode 100644 index 00000000..eea8be37 --- /dev/null +++ b/common/src/main/scala/net/psforever/objects/Deployables.scala @@ -0,0 +1,28 @@ +// Copyright (c) 2017 PSForever +package net.psforever.objects + +import net.psforever.objects.ce.{Deployable, DeployedItem} + +object Deployables { + object Make { + def apply(item : DeployedItem.Value) : ()=>PlanetSideGameObject with Deployable = cemap(item) + + private val cemap : Map[DeployedItem.Value, ()=>PlanetSideGameObject with Deployable] = Map( + DeployedItem.boomer -> { ()=> new BoomerDeployable(GlobalDefinitions.boomer) }, + DeployedItem.he_mine -> { ()=> new ExplosiveDeployable(GlobalDefinitions.he_mine) }, + DeployedItem.jammer_mine -> { ()=> new ExplosiveDeployable(GlobalDefinitions.jammer_mine) }, + DeployedItem.spitfire_turret -> { ()=> new TurretDeployable(GlobalDefinitions.spitfire_turret) }, + DeployedItem.spitfire_cloaked -> { ()=> new TurretDeployable(GlobalDefinitions.spitfire_cloaked) }, + DeployedItem.spitfire_aa -> { ()=> new TurretDeployable(GlobalDefinitions.spitfire_aa) }, + DeployedItem.motionalarmsensor -> { ()=> new SensorDeployable(GlobalDefinitions.motionalarmsensor) }, + DeployedItem.sensor_shield -> { ()=> new SensorDeployable(GlobalDefinitions.sensor_shield) }, + DeployedItem.tank_traps -> { ()=> new TrapDeployable(GlobalDefinitions.tank_traps) }, + DeployedItem.portable_manned_turret -> { ()=> new TurretDeployable(GlobalDefinitions.portable_manned_turret) }, + DeployedItem.portable_manned_turret -> { ()=> new TurretDeployable(GlobalDefinitions.portable_manned_turret) }, + DeployedItem.portable_manned_turret_nc -> { ()=> new TurretDeployable(GlobalDefinitions.portable_manned_turret_nc) }, + DeployedItem.portable_manned_turret_tr -> { ()=> new TurretDeployable(GlobalDefinitions.portable_manned_turret_tr) }, + DeployedItem.portable_manned_turret_vs -> { ()=> new TurretDeployable(GlobalDefinitions.portable_manned_turret_vs) }, + DeployedItem.deployable_shield_generator -> { ()=> new ShieldGeneratorDeployable(GlobalDefinitions.deployable_shield_generator) } + ).withDefaultValue( { ()=> new ExplosiveDeployable(GlobalDefinitions.boomer) } ) + } +} diff --git a/common/src/main/scala/net/psforever/objects/ExplosiveDeployable.scala b/common/src/main/scala/net/psforever/objects/ExplosiveDeployable.scala new file mode 100644 index 00000000..9d03bf74 --- /dev/null +++ b/common/src/main/scala/net/psforever/objects/ExplosiveDeployable.scala @@ -0,0 +1,16 @@ +// Copyright (c) 2017 PSForever +package net.psforever.objects + +import net.psforever.objects.ce.SimpleDeployable +import net.psforever.objects.definition.DeployableDefinition + +class ExplosiveDeployable(cdef : DeployableDefinition) extends SimpleDeployable(cdef) { + private var exploded : Boolean = false + + def Exploded : Boolean = exploded + + def Exploded_=(fuse : Boolean) : Boolean = { + exploded = fuse + Exploded + } +} diff --git a/common/src/main/scala/net/psforever/objects/GlobalDefinitions.scala b/common/src/main/scala/net/psforever/objects/GlobalDefinitions.scala index 46049f60..6515c43d 100644 --- a/common/src/main/scala/net/psforever/objects/GlobalDefinitions.scala +++ b/common/src/main/scala/net/psforever/objects/GlobalDefinitions.scala @@ -2,9 +2,9 @@ package net.psforever.objects import net.psforever.objects.ballistics.Projectiles +import net.psforever.objects.ce.{DeployableCategory, DeployedItem} import net.psforever.objects.definition._ import net.psforever.objects.definition.converter._ -import net.psforever.objects.equipment.CItem.DeployedItem import net.psforever.objects.equipment._ import net.psforever.objects.inventory.InventoryTile import net.psforever.objects.serverobject.doors.DoorDefinition @@ -15,10 +15,10 @@ import net.psforever.objects.serverobject.pad.VehicleSpawnPadDefinition import net.psforever.objects.serverobject.terminals._ import net.psforever.objects.serverobject.tube.SpawnTubeDefinition import net.psforever.objects.serverobject.resourcesilo.ResourceSiloDefinition -import net.psforever.objects.serverobject.turret.{MannedTurretDefinition, TurretUpgrade} +import net.psforever.objects.serverobject.turret.{TurretDefinition, TurretUpgrade} import net.psforever.objects.vehicles.{DestroyedVehicle, SeatArmorRestriction, UtilityType} -import net.psforever.objects.vital.DamageType -import net.psforever.types.PlanetSideEmpire +import net.psforever.objects.vital.{DamageType, StandardResolutions} +import net.psforever.types.{CertificationType, PlanetSideEmpire} import scala.collection.mutable import scala.concurrent.duration._ @@ -497,6 +497,12 @@ object GlobalDefinitions { val bullet_150mm = AmmoBoxDefinition(Ammo.bullet_150mm) val phalanx_ammo = AmmoBoxDefinition(Ammo.phalanx_ammo) + + val spitfire_ammo = AmmoBoxDefinition(Ammo.spitfire_ammo) + + val spitfire_aa_ammo = AmmoBoxDefinition(Ammo.spitfire_aa_ammo) + + val energy_gun_ammo = AmmoBoxDefinition(Ammo.energy_gun_ammo) init_ammo() val chainblade = ToolDefinition(ObjectClass.chainblade) @@ -622,6 +628,8 @@ object GlobalDefinitions { val bank = ToolDefinition(ObjectClass.bank) + val boomer_trigger = SimpleItemDefinition(SItem.boomer_trigger) + val remote_electronics_kit = SimpleItemDefinition(SItem.remote_electronics_kit) val trek = ToolDefinition(ObjectClass.trek) @@ -630,9 +638,9 @@ object GlobalDefinitions { val command_detonater = SimpleItemDefinition(SItem.command_detonater) - val ace = ConstructionItemDefinition(CItem.Unit.ace) + val ace = ConstructionItemDefinition(CItem.ace) - val advanced_ace = ConstructionItemDefinition(CItem.Unit.advanced_ace) + val advanced_ace = ConstructionItemDefinition(CItem.advanced_ace) val fury_weapon_systema = ToolDefinition(ObjectClass.fury_weapon_systema) @@ -737,6 +745,18 @@ object GlobalDefinitions { val phalanx_avcombo = ToolDefinition(ObjectClass.phalanx_avcombo) val phalanx_flakcombo = ToolDefinition(ObjectClass.phalanx_flakcombo) + + val spitfire_weapon = ToolDefinition(ObjectClass.spitfire_weapon) + + val spitfire_aa_weapon = ToolDefinition(ObjectClass.spitfire_aa_weapon) + + val energy_gun = ToolDefinition(ObjectClass.energy_gun) + + val energy_gun_nc = ToolDefinition(ObjectClass.energy_gun_nc) + + val energy_gun_tr = ToolDefinition(ObjectClass.energy_gun_tr) + + val energy_gun_vs = ToolDefinition(ObjectClass.energy_gun_vs) init_tools() /* @@ -809,6 +829,38 @@ object GlobalDefinitions { val phantasm = VehicleDefinition(ObjectClass.phantasm) init_vehicles() + /* + combat engineering deployables + */ + val boomer = DeployableDefinition(DeployedItem.boomer) + + val he_mine = DeployableDefinition(DeployedItem.he_mine) + + val jammer_mine = DeployableDefinition(DeployedItem.jammer_mine) + + val spitfire_turret = TurretDeployableDefinition(DeployedItem.spitfire_turret) + + val spitfire_cloaked = TurretDeployableDefinition(DeployedItem.spitfire_cloaked) + + val spitfire_aa = TurretDeployableDefinition(DeployedItem.spitfire_aa) + + val motionalarmsensor = DeployableDefinition(DeployedItem.motionalarmsensor) + + val sensor_shield = DeployableDefinition(DeployedItem.sensor_shield) + + val tank_traps = DeployableDefinition(DeployedItem.tank_traps) + + val portable_manned_turret = TurretDeployableDefinition(DeployedItem.portable_manned_turret) + + val portable_manned_turret_nc = TurretDeployableDefinition(DeployedItem.portable_manned_turret_nc) + + val portable_manned_turret_tr = TurretDeployableDefinition(DeployedItem.portable_manned_turret_tr) + + val portable_manned_turret_vs = TurretDeployableDefinition(DeployedItem.portable_manned_turret_vs) + + val deployable_shield_generator = new ShieldGeneratorDefinition + init_deployables() + /* Miscellaneous */ @@ -868,7 +920,7 @@ object GlobalDefinitions { val secondary_capture = new CaptureTerminalDefinition(751) // Tower CC - val manned_turret = new MannedTurretDefinition(480) { + val manned_turret = new TurretDefinition(480) { Name = "manned_turret" MaxHealth = 3600 Weapons += 1 -> new mutable.HashMap() @@ -1084,6 +1136,15 @@ object GlobalDefinitions { } } + def PortableMannedTurret(faction :PlanetSideEmpire.Value) : TurretDeployableDefinition = { + faction match { + case PlanetSideEmpire.TR => portable_manned_turret_tr + case PlanetSideEmpire.NC => portable_manned_turret_nc + case PlanetSideEmpire.VS => portable_manned_turret_vs + case PlanetSideEmpire.NEUTRAL => portable_manned_turret + } + } + /** * Using the definition for a piece of `Equipment` determine if it is a grenade-type weapon. * Only the normal grenades count; the grenade packs are excluded. @@ -1541,8 +1602,16 @@ object GlobalDefinitions { bullet_150mm.Tile = InventoryTile.Tile44 phalanx_ammo.Name = "phalanx_ammo" - phalanx_ammo.Capacity = 4000 //sufficient for a reload phalanx_ammo.Size = EquipmentSize.Inventory + + spitfire_ammo.Name = "spitfire_ammo" + spitfire_ammo.Size = EquipmentSize.Inventory + + spitfire_aa_ammo.Name = "spitfire_aa_ammo" + spitfire_aa_ammo.Size = EquipmentSize.Inventory + + energy_gun_ammo.Name = "energy_gun_ammo" + energy_gun_ammo.Size = EquipmentSize.Inventory } /** @@ -3938,6 +4007,10 @@ object GlobalDefinitions { remote_electronics_kit.Packet = new REKConverter remote_electronics_kit.Tile = InventoryTile.Tile33 + boomer_trigger.Name = "boomer_trigger" + boomer_trigger.Packet = new BoomerTriggerConverter + boomer_trigger.Tile = InventoryTile.Tile22 + trek.Name = "trek" trek.Size = EquipmentSize.Pistol trek.AmmoTypes += trek_ammo @@ -3961,21 +4034,30 @@ object GlobalDefinitions { command_detonater.Tile = InventoryTile.Tile33 ace.Name = "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.Size = EquipmentSize.Pistol + ace.Modes += new ConstructionFireMode + ace.Modes.head.Item(DeployedItem.boomer -> Set(CertificationType.CombatEngineering)) + ace.Modes += new ConstructionFireMode + ace.Modes(1).Item(DeployedItem.he_mine -> Set(CertificationType.CombatEngineering)) + ace.Modes(1).Item(DeployedItem.jammer_mine -> Set(CertificationType.AssaultEngineering)) + ace.Modes += new ConstructionFireMode + ace.Modes(2).Item(DeployedItem.spitfire_turret -> Set(CertificationType.CombatEngineering)) + ace.Modes(2).Item(DeployedItem.spitfire_cloaked -> Set(CertificationType.FortificationEngineering)) + ace.Modes(2).Item(DeployedItem.spitfire_aa -> Set(CertificationType.FortificationEngineering)) + ace.Modes += new ConstructionFireMode + ace.Modes(3).Item(DeployedItem.motionalarmsensor -> Set(CertificationType.CombatEngineering)) + ace.Modes(3).Item(DeployedItem.sensor_shield -> Set(CertificationType.AdvancedHacking, CertificationType.CombatEngineering)) ace.Tile = InventoryTile.Tile33 advanced_ace.Name = "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 + advanced_ace.Size = EquipmentSize.Rifle + advanced_ace.Modes += new ConstructionFireMode + advanced_ace.Modes.head.Item(DeployedItem.tank_traps -> Set(CertificationType.FortificationEngineering)) + advanced_ace.Modes += new ConstructionFireMode + advanced_ace.Modes(1).Item(DeployedItem.portable_manned_turret -> Set(CertificationType.AssaultEngineering)) + advanced_ace.Modes += new ConstructionFireMode + advanced_ace.Modes(2).Item(DeployedItem.deployable_shield_generator -> Set(CertificationType.AssaultEngineering)) + advanced_ace.Tile = InventoryTile.Tile93 fury_weapon_systema.Name = "fury_weapon_systema" fury_weapon_systema.Size = EquipmentSize.VehicleWeapon @@ -4510,6 +4592,61 @@ object GlobalDefinitions { phalanx_flakcombo.FireModes(1).ProjectileTypeIndices += 1 phalanx_flakcombo.FireModes(1).AmmoSlotIndex = 0 phalanx_flakcombo.FireModes(1).Magazine = 4000 + + spitfire_weapon.Name = "spitfire_weapon" + spitfire_weapon.Size = EquipmentSize.BaseTurretWeapon + spitfire_weapon.AmmoTypes += spitfire_ammo + spitfire_weapon.ProjectileTypes += spitfire_ammo_projectile + spitfire_weapon.FireModes += new InfiniteFireModeDefinition + spitfire_weapon.FireModes.head.AmmoTypeIndices += 0 + spitfire_weapon.FireModes.head.AmmoSlotIndex = 0 + spitfire_weapon.FireModes.head.Magazine = 4000 + + spitfire_aa_weapon.Name = "spitfire_aa_weapon" + spitfire_aa_weapon.Size = EquipmentSize.BaseTurretWeapon + spitfire_aa_weapon.AmmoTypes += spitfire_aa_ammo + spitfire_aa_weapon.ProjectileTypes += spitfire_aa_ammo_projectile + spitfire_aa_weapon.FireModes += new InfiniteFireModeDefinition + spitfire_aa_weapon.FireModes.head.AmmoTypeIndices += 0 + spitfire_aa_weapon.FireModes.head.AmmoSlotIndex = 0 + spitfire_aa_weapon.FireModes.head.Magazine = 4000 + + energy_gun.Name = "energy_gun" + energy_gun.Size = EquipmentSize.BaseTurretWeapon + energy_gun.AmmoTypes += energy_gun_ammo + energy_gun.ProjectileTypes += bullet_9mm_projectile //fallback + energy_gun.FireModes += new FireModeDefinition + energy_gun.FireModes.head.AmmoTypeIndices += 0 + energy_gun.FireModes.head.AmmoSlotIndex = 0 + energy_gun.FireModes.head.Magazine = 4000 + + energy_gun_nc.Name = "energy_gun_nc" + energy_gun_nc.Size = EquipmentSize.BaseTurretWeapon + energy_gun_nc.AmmoTypes += energy_gun_ammo + energy_gun_nc.ProjectileTypes += energy_gun_nc_projectile + energy_gun_nc.FireModes += new PelletFireModeDefinition + energy_gun_nc.FireModes.head.AmmoTypeIndices += 0 + energy_gun_nc.FireModes.head.AmmoSlotIndex = 0 + energy_gun_nc.FireModes.head.Magazine = 35 + energy_gun_nc.FireModes.head.Chamber = 9 + + energy_gun_tr.Name = "energy_gun_tr" + energy_gun_tr.Size = EquipmentSize.BaseTurretWeapon + energy_gun_tr.AmmoTypes += energy_gun_ammo + energy_gun_tr.ProjectileTypes += energy_gun_tr_projectile + energy_gun_tr.FireModes += new FireModeDefinition + energy_gun_tr.FireModes.head.AmmoTypeIndices += 0 + energy_gun_tr.FireModes.head.AmmoSlotIndex = 0 + energy_gun_tr.FireModes.head.Magazine = 200 + + energy_gun_vs.Name = "energy_gun_vs" + energy_gun_vs.Size = EquipmentSize.BaseTurretWeapon + energy_gun_vs.AmmoTypes += energy_gun_ammo + energy_gun_vs.ProjectileTypes += energy_gun_tr_projectile + energy_gun_vs.FireModes += new FireModeDefinition + energy_gun_vs.FireModes.head.AmmoTypeIndices += 0 + energy_gun_vs.FireModes.head.AmmoSlotIndex = 0 + energy_gun_vs.FireModes.head.Magazine = 100 } /** @@ -5238,4 +5375,145 @@ object GlobalDefinitions { phantasm.Packet = variantConverter phantasm.DestroyedModel = None //the adb calls out a phantasm_destroyed but no such asset exists } + + /** + * Initialize `Deployable` globals. + */ + private def init_deployables() : Unit = { + boomer.Name = "boomer" + boomer.Descriptor = "Boomers" + boomer.MaxHealth = 100 + boomer.DeployCategory = DeployableCategory.Boomers + boomer.DeployTime = Duration.create(1000, "ms") + boomer.Model = StandardResolutions.SimpleDeployables + + he_mine.Name = "he_mine" + he_mine.Descriptor = "Mines" + he_mine.MaxHealth = 100 + he_mine.DeployCategory = DeployableCategory.Mines + he_mine.DeployTime = Duration.create(1000, "ms") + he_mine.Model = StandardResolutions.SimpleDeployables + + jammer_mine.Name = "jammer_mine" + jammer_mine.Descriptor = "JammerMines" + jammer_mine.MaxHealth = 100 + jammer_mine.DeployCategory = DeployableCategory.Mines + jammer_mine.DeployTime = Duration.create(1000, "ms") + jammer_mine.Model = StandardResolutions.SimpleDeployables + + spitfire_turret.Name = "spitfire_turret" + spitfire_turret.Descriptor= "Spitfires" + spitfire_turret.MaxHealth = 100 + spitfire_turret.Weapons += 1 -> new mutable.HashMap() + spitfire_turret.Weapons(1) += TurretUpgrade.None -> spitfire_weapon + spitfire_turret.ReserveAmmunition = false + spitfire_turret.DeployCategory = DeployableCategory.SmallTurrets + spitfire_turret.DeployTime = Duration.create(5000, "ms") + spitfire_turret.Model = StandardResolutions.ComplexDeployables + + spitfire_cloaked.Name = "spitfire_cloaked" + spitfire_cloaked.Descriptor= "CloakingSpitfires" + spitfire_cloaked.MaxHealth = 100 + spitfire_cloaked.Weapons += 1 -> new mutable.HashMap() + spitfire_cloaked.Weapons(1) += TurretUpgrade.None -> spitfire_weapon + spitfire_cloaked.ReserveAmmunition = false + spitfire_cloaked.DeployCategory = DeployableCategory.SmallTurrets + spitfire_cloaked.DeployTime = Duration.create(5000, "ms") + spitfire_cloaked.Model = StandardResolutions.ComplexDeployables + + spitfire_aa.Name = "spitfire_aa" + spitfire_aa.Descriptor= "FlakSpitfires" + spitfire_aa.MaxHealth = 100 + spitfire_aa.Weapons += 1 -> new mutable.HashMap() + spitfire_aa.Weapons(1) += TurretUpgrade.None -> spitfire_aa_weapon + spitfire_aa.ReserveAmmunition = false + spitfire_aa.DeployCategory = DeployableCategory.SmallTurrets + spitfire_aa.DeployTime = Duration.create(5000, "ms") + spitfire_aa.Model = StandardResolutions.ComplexDeployables + + motionalarmsensor.Name = "motionalarmsensor" + motionalarmsensor.Descriptor = "MotionSensors" + motionalarmsensor.MaxHealth = 100 + motionalarmsensor.DeployCategory = DeployableCategory.Sensors + motionalarmsensor.DeployTime = Duration.create(1000, "ms") + motionalarmsensor.Model = StandardResolutions.SimpleDeployables + + sensor_shield.Name = "sensor_shield" + sensor_shield.Descriptor = "SensorShields" + sensor_shield.MaxHealth = 100 + sensor_shield.DeployCategory = DeployableCategory.Sensors + sensor_shield.DeployTime = Duration.create(5000, "ms") + sensor_shield.Model = StandardResolutions.SimpleDeployables + + tank_traps.Name = "tank_traps" + tank_traps.Descriptor = "TankTraps" + tank_traps.MaxHealth = 5000 + tank_traps.Packet = new TRAPConverter + tank_traps.DeployCategory = DeployableCategory.TankTraps + tank_traps.DeployTime = Duration.create(6000, "ms") + tank_traps.Model = StandardResolutions.SimpleDeployables + + val fieldTurretConverter = new FieldTurretConverter + portable_manned_turret.Name = "portable_manned_turret" + portable_manned_turret.Descriptor = "FieldTurrets" + portable_manned_turret.MaxHealth = 1000 + portable_manned_turret.MountPoints += 1 -> 0 + portable_manned_turret.MountPoints += 2 -> 0 + portable_manned_turret.Weapons += 1 -> new mutable.HashMap() + portable_manned_turret.Weapons(1) += TurretUpgrade.None -> energy_gun + portable_manned_turret.ReserveAmmunition = true + portable_manned_turret.FactionLocked = true + portable_manned_turret.Packet = fieldTurretConverter + portable_manned_turret.DeployCategory = DeployableCategory.FieldTurrets + portable_manned_turret.DeployTime = Duration.create(6000, "ms") + portable_manned_turret.Model = StandardResolutions.ComplexDeployables + + portable_manned_turret_nc.Name = "portable_manned_turret_nc" + portable_manned_turret_nc.Descriptor = "FieldTurrets" + portable_manned_turret_nc.MaxHealth = 1000 + portable_manned_turret_nc.MountPoints += 1 -> 0 + portable_manned_turret_nc.MountPoints += 2 -> 0 + portable_manned_turret_nc.Weapons += 1 -> new mutable.HashMap() + portable_manned_turret_nc.Weapons(1) += TurretUpgrade.None -> energy_gun_nc + portable_manned_turret_nc.ReserveAmmunition = true + portable_manned_turret_nc.FactionLocked = true + portable_manned_turret_nc.Packet = fieldTurretConverter + portable_manned_turret_nc.DeployCategory = DeployableCategory.FieldTurrets + portable_manned_turret_nc.DeployTime = Duration.create(6000, "ms") + portable_manned_turret_nc.Model = StandardResolutions.ComplexDeployables + + portable_manned_turret_tr.Name = "portable_manned_turret_tr" + portable_manned_turret_tr.Descriptor = "FieldTurrets" + portable_manned_turret_tr.MaxHealth = 1000 + portable_manned_turret_tr.MountPoints += 1 -> 0 + portable_manned_turret_tr.MountPoints += 2 -> 0 + portable_manned_turret_tr.Weapons += 1 -> new mutable.HashMap() + portable_manned_turret_tr.Weapons(1) += TurretUpgrade.None -> energy_gun_tr + portable_manned_turret_tr.ReserveAmmunition = true + portable_manned_turret_tr.FactionLocked = true + portable_manned_turret_tr.Packet = fieldTurretConverter + portable_manned_turret_tr.DeployCategory = DeployableCategory.FieldTurrets + portable_manned_turret_tr.DeployTime = Duration.create(6000, "ms") + portable_manned_turret_tr.Model = StandardResolutions.ComplexDeployables + + portable_manned_turret_vs.Name = "portable_manned_turret_vs" + portable_manned_turret_vs.Descriptor = "FieldTurrets" + portable_manned_turret_vs.MaxHealth = 1000 + portable_manned_turret_vs.MountPoints += 1 -> 0 + portable_manned_turret_vs.MountPoints += 2 -> 0 + portable_manned_turret_vs.Weapons += 1 -> new mutable.HashMap() + portable_manned_turret_vs.Weapons(1) += TurretUpgrade.None -> energy_gun_vs + portable_manned_turret_vs.ReserveAmmunition = true + portable_manned_turret_vs.FactionLocked = true + portable_manned_turret_vs.Packet = fieldTurretConverter + portable_manned_turret_vs.DeployCategory = DeployableCategory.FieldTurrets + portable_manned_turret_vs.DeployTime = Duration.create(6000, "ms") + portable_manned_turret_vs.Model = StandardResolutions.ComplexDeployables + + deployable_shield_generator.Name = "deployable_shield_generator" + deployable_shield_generator.Descriptor = "ShieldGenerators" + deployable_shield_generator.MaxHealth = 1700 + deployable_shield_generator.DeployTime = Duration.create(6000, "ms") + deployable_shield_generator.Model = StandardResolutions.ComplexDeployables + } } diff --git a/common/src/main/scala/net/psforever/objects/SensorDeployable.scala b/common/src/main/scala/net/psforever/objects/SensorDeployable.scala new file mode 100644 index 00000000..eccfcc7e --- /dev/null +++ b/common/src/main/scala/net/psforever/objects/SensorDeployable.scala @@ -0,0 +1,9 @@ +// Copyright (c) 2017 PSForever +package net.psforever.objects + +import net.psforever.objects.ce.SimpleDeployable +import net.psforever.objects.definition.DeployableDefinition +import net.psforever.objects.serverobject.hackable.Hackable + +class SensorDeployable(cdef : DeployableDefinition) extends SimpleDeployable(cdef) + with Hackable diff --git a/common/src/main/scala/net/psforever/objects/ShieldGeneratorDeployable.scala b/common/src/main/scala/net/psforever/objects/ShieldGeneratorDeployable.scala new file mode 100644 index 00000000..bbdedd91 --- /dev/null +++ b/common/src/main/scala/net/psforever/objects/ShieldGeneratorDeployable.scala @@ -0,0 +1,15 @@ +// Copyright (c) 2017 PSForever +package net.psforever.objects + +import net.psforever.objects.ce.{ComplexDeployable, DeployableCategory} +import net.psforever.objects.definition.DeployableDefinition +import net.psforever.objects.definition.converter.ShieldGeneratorConverter +import net.psforever.objects.serverobject.hackable.Hackable + +class ShieldGeneratorDeployable(cdef : ShieldGeneratorDefinition) extends ComplexDeployable(cdef) + with Hackable + +class ShieldGeneratorDefinition extends DeployableDefinition(240) { + Packet = new ShieldGeneratorConverter + DeployCategory = DeployableCategory.ShieldGenerators +} diff --git a/common/src/main/scala/net/psforever/objects/Tool.scala b/common/src/main/scala/net/psforever/objects/Tool.scala index e925f513..3faff440 100644 --- a/common/src/main/scala/net/psforever/objects/Tool.scala +++ b/common/src/main/scala/net/psforever/objects/Tool.scala @@ -8,7 +8,7 @@ import net.psforever.objects.ballistics.Projectiles import scala.annotation.tailrec /** - * A type of utility that can be wielded and loaded with certain other game elements.
+ * A type of `Equipment` 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," @@ -17,7 +17,8 @@ import scala.annotation.tailrec * 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] { +class Tool(private val toolDef : ToolDefinition) extends Equipment + with FireModeSwitch[FireModeDefinition] { /** index of the current fire mode on the `ToolDefinition`'s list of fire modes */ private var fireModeIndex : Int = 0 /** current ammunition slot being used by this fire mode */ diff --git a/common/src/main/scala/net/psforever/objects/TrapDeployable.scala b/common/src/main/scala/net/psforever/objects/TrapDeployable.scala new file mode 100644 index 00000000..488c4bc6 --- /dev/null +++ b/common/src/main/scala/net/psforever/objects/TrapDeployable.scala @@ -0,0 +1,7 @@ +// Copyright (c) 2017 PSForever +package net.psforever.objects + +import net.psforever.objects.ce.SimpleDeployable +import net.psforever.objects.definition.DeployableDefinition + +class TrapDeployable(cdef : DeployableDefinition) extends SimpleDeployable(cdef) diff --git a/common/src/main/scala/net/psforever/objects/TurretDeployable.scala b/common/src/main/scala/net/psforever/objects/TurretDeployable.scala new file mode 100644 index 00000000..c72d59cd --- /dev/null +++ b/common/src/main/scala/net/psforever/objects/TurretDeployable.scala @@ -0,0 +1,89 @@ +// Copyright (c) 2017 PSForever +package net.psforever.objects + +import akka.actor.{Actor, ActorContext, Props} +import net.psforever.objects.ce.{Deployable, DeployedItem} +import net.psforever.objects.definition.{BaseDeployableDefinition, DeployableDefinition} +import net.psforever.objects.definition.converter.SmallTurretConverter +import net.psforever.objects.serverobject.PlanetSideServerObject +import net.psforever.objects.serverobject.affinity.{FactionAffinity, FactionAffinityBehavior} +import net.psforever.objects.serverobject.hackable.Hackable +import net.psforever.objects.serverobject.mount.MountableBehavior +import net.psforever.objects.serverobject.turret.{TurretDefinition, WeaponTurret} + +class TurretDeployable(tdef : TurretDeployableDefinition) extends PlanetSideServerObject + with Deployable + with WeaponTurret + with Hackable { + private var shields : Int = 0 + + WeaponTurret.LoadDefinition(this) //calls the equivalent of Health = Definition.MaxHealth + + def MaxHealth : Int = Definition.MaxHealth + + def Shields : Int = shields + + def Shields_=(toShields : Int) : Int = { + shields = math.min(math.max(0, toShields), MaxShields) + Shields + } + + def MaxShields : Int = { + 0//Definition.MaxShields + } + + def MountPoints : Map[Int, Int] = Definition.MountPoints.toMap + + //override to clarify inheritance conflict + override def Health : Int = super[Deployable].Health + //override to clarify inheritance conflict + override def Health_=(toHealth : Int) : Int = super[Deployable].Health_=(toHealth) + + override def Definition = tdef +} + +class TurretDeployableDefinition(private val objectId : Int) extends TurretDefinition(objectId) + with BaseDeployableDefinition { + private val item = DeployedItem(objectId) //let throw NoSuchElementException + Name = "turret_deployable" + Packet = new SmallTurretConverter + + def Item : DeployedItem.Value = item + + //override to clarify inheritance conflict + override def MaxHealth : Int = super[BaseDeployableDefinition].MaxHealth + //override to clarify inheritance conflict + override def MaxHealth_=(max : Int) : Int = super[BaseDeployableDefinition].MaxHealth_=(max) + + override def Initialize(obj : PlanetSideServerObject with Deployable, context : ActorContext) = { + obj.Actor = context.actorOf(Props(classOf[TurretControl], obj), s"${obj.Definition.Name}_${obj.GUID.guid}") + } + + override def Uninitialize(obj : PlanetSideServerObject with Deployable, context : ActorContext) = { + DeployableDefinition.SimpleUninitialize(obj, context) + } +} + +object TurretDeployableDefinition { + def apply(dtype : DeployedItem.Value) : TurretDeployableDefinition = { + new TurretDeployableDefinition(dtype.id) + } +} + +/** control actors */ + +class TurretControl(turret : TurretDeployable) extends Actor + with FactionAffinityBehavior.Check + with MountableBehavior.Mount + with MountableBehavior.Dismount { + def MountableObject = turret //do not add type! + + def FactionObject : FactionAffinity = turret + + def receive : Receive = checkBehavior + .orElse(dismountBehavior) + .orElse(turretMountBehavior) + .orElse { + case _ => ; + } +} diff --git a/common/src/main/scala/net/psforever/objects/avatar/DeployableToolbox.scala b/common/src/main/scala/net/psforever/objects/avatar/DeployableToolbox.scala new file mode 100644 index 00000000..972fc9e0 --- /dev/null +++ b/common/src/main/scala/net/psforever/objects/avatar/DeployableToolbox.scala @@ -0,0 +1,668 @@ +// Copyright (c) 2017 PSForever +package net.psforever.objects.avatar + +import net.psforever.objects.PlanetSideGameObject +import net.psforever.objects.ce.{Deployable, DeployableCategory, DeployedItem} +import net.psforever.packet.game.PlanetSideGUID +import net.psforever.types.CertificationType + +import scala.collection.mutable + +/** + * A class that keeps track - "manages" - deployables that are owned by the avatar.
+ *
+ * Deployables belong to the Engineering certification line of certifications. + * `CombatEngineering` and above certifications include permissions for different types of deployables, + * and one unique type of deployable is available through the `GroundSupport` + * and one that also requires `AdvancedHacking`. + * (They are collectively called "ce" for that reason.) + * Not only does the level of certification change the maximum number of deployables that can be managed by type + * but it also influences the maximum number of deployables that can be managed by category. + * Individual deployables are counted by type and category individually in special data structures + * to avoid having to probe the primary list of deployable references whenever a question of quantity is asked. + * As deployables are added and removed, and tracked certifications are added and removed, + * these structures are updated to reflect proper count. + */ +class DeployableToolbox { + /** + * a map of bins for keeping track of the quantities of deployables in a category + * keys: categories, values: quantity storage object + */ + private val categoryCounts = DeployableCategory.values.toSeq.map(value => { value -> new DeployableToolbox.Bin }).toMap + //) + /** + * a map of bins for keeping track of the quantities of individual deployables + * keys: deployable types, values: quantity storage object + */ + private val deployableCounts = DeployedItem.values.toSeq.map(value => { value -> new DeployableToolbox.Bin }).toMap + /** + * a map of tracked/owned individual deployables + * keys: categories, values: deployable objects + */ + private val deployableLists = + DeployableCategory.values.toSeq.map(value => { value -> mutable.ListBuffer[DeployableToolbox.AcceptableDeployable]() }).toMap + /** + * can only be initialized once + * set during the `Initialization` method primarily, and in `Add` and in `Remove` if not + */ + private var initialized : Boolean = false + + /** + * Set up the initial deployable counts by providing certification values to be used in category and unit selection. + * @param certifications a group of certifications for the initial values + * @return `true`, if this is the first time and actual "initialization" is performed; + * `false`, otherwise + */ + def Initialize(certifications : Set[CertificationType.Value]) : Boolean = { + if(!initialized) { + DeployableToolbox.Initialize(deployableCounts, categoryCounts, certifications) + initialized = true + true + } + else { + false + } + } + + /** + * Change the count of deployable units that can be tracked by providing a new certification. + * If the given certification is already factored into the quantities, no changes will occur. + * @param certification the new certification + * @param certificationSet the group of previous certifications being tracked; + * occasionally, important former certification values are required for additional configuration; + * the new certification should already have been added to this group + */ + def AddToDeployableQuantities(certification : CertificationType.Value, certificationSet : Set[CertificationType.Value]) : Unit = { + initialized = true + DeployableToolbox.AddToDeployableQuantities(deployableCounts, categoryCounts, certification, certificationSet) + } + + /** + * Change the count of deployable units that can be tracked + * by designating a certification whose deployables will be removed. + * If the given certification is already factored out of the quantities, no changes will occur. + * @param certification the old certification + * @param certificationSet the group of previous certifications being tracked; + * occasionally, important former certification values are required for additional configuration; + * the new certification should already have been excluded from this group + */ + def RemoveFromDeployableQuantities(certification : CertificationType.Value, certificationSet : Set[CertificationType.Value]) : Unit = { + initialized = true + DeployableToolbox.RemoveFromDeployablesQuantities(deployableCounts, categoryCounts, certification, certificationSet) + } + + /** + * Determine if the given deployable can be managed by this toolbox. + * @see `Valid` + * @see `Available` + * @see `Contains` + * @param obj the deployable + * @return `true`, if it can be managed under the current conditions; + * `false`, otherwise + */ + def Accept(obj : DeployableToolbox.AcceptableDeployable) : Boolean = { + Valid(obj) && Available(obj) && !Contains(obj) + } + + /** + * Determine if the given deployable can be managed by this toolbox + * by testing if the specific deployable maximum and the deployable category maximum is non-zero + * @param obj the deployable + * @return `true`, if both category maximum and deployable type maximum are positive non-zero integers; + * `false`, otherwise + */ + def Valid(obj : DeployableToolbox.AcceptableDeployable) : Boolean = { + deployableCounts(DeployableToolbox.UnifiedType(obj.Definition.Item)).Max > 0 && + categoryCounts(obj.Definition.DeployCategory).Max > 0 + } + + /** + * Determine if the given deployable can be managed by this toolbox + * by testing if the specific deployable list and the deployable category list have available slots. + * In this case, a "slot" is merely the difference between the current count is less than the maximum count. + * @param obj the deployable + * @return `true`, if the deployable can be added to the support lists and counted; + * `false`, otherwise + */ + def Available(obj : DeployableToolbox.AcceptableDeployable) : Boolean = { + deployableCounts(DeployableToolbox.UnifiedType(obj.Definition.Item)).Available && + categoryCounts(obj.Definition.DeployCategory).Available + } + + /** + * Check if this deployable is already being managed by the toolbox + * by determining whether or not it is already being managed by this toolbox. + * @param obj the deployable + * @return `true`, if the deployable can be found in one of the lists; + * `false`, otherwise + */ + def Contains(obj : DeployableToolbox.AcceptableDeployable) : Boolean = { + deployableLists(obj.Definition.DeployCategory).contains(obj) + } + + /** + * Manage the provided deployable.
+ *
+ * Although proper testing should be performed prior to attempting to add the deployable to this toolbox, + * three tests are administered to determine whether space is available prior to insertion. + * The first two tests check for available space in the category count and in the unit count + * and the third test checks whether the deployable is already being managed by this toolbox. + * No changes should occur if the deployable is not properly added. + * @param obj the deployable + * @return `true`, if the deployable is added; + * `false`, otherwise + */ + def Add(obj : DeployableToolbox.AcceptableDeployable) : Boolean = { + val category = obj.Definition.DeployCategory + val dCategory = categoryCounts(category) + val dType = deployableCounts(DeployableToolbox.UnifiedType(obj.Definition.Item)) + val dList = deployableLists(category) + if(dCategory.Available() && dType.Available() && !dList.contains(obj)) { + dCategory.Current += 1 + dType.Current += 1 + dList += obj + true + } + else { + false + } + } + + /** + * Stop managing the provided deployable.
+ *
+ * Although proper testing should be performed prior to attempting to remove the deployable to this toolbox, + * a single test is administered to determine whether the removal can take place. + * If the deployable is found to currently being managed by this toolbox, then it is properly removed. + * No changes should occur if the deployable is not properly removed. + * @param obj the deployable + * @return `true`, if the deployable is added; + * `false`, otherwise + */ + def Remove(obj : DeployableToolbox.AcceptableDeployable) : Boolean = { + val category = obj.Definition.DeployCategory + val deployables = deployableLists(category) + if(deployables.contains(obj)) { + categoryCounts(category).Current -= 1 + deployableCounts(DeployableToolbox.UnifiedType(obj.Definition.Item)).Current -= 1 + deployables -= obj + true + } + else { + false + } + } + + /** + * Remove the first managed deployable that matches the same type of deployable as the example. + * The explicit tests is defined to find the first deployable whose type matches. + * @param obj the example deployable + * @return any deployable that is found + */ + def DisplaceFirst(obj : DeployableToolbox.AcceptableDeployable) : Option[DeployableToolbox.AcceptableDeployable] = { + DisplaceFirst(obj, { d => d.Definition.Item == obj.Definition.Item }) + } + + /** + * Remove the first managed deployable that satisfies a test and belongs to the same category as the example. + * The test in question is used to pinpoint the first qualifying deployable; + * but, if the test fails to find any valid targets, + * the first deployable in the list of managed deployables for that category is selected to be removed. + * The only test performed is whether there is any valid deployable managed for the category. + * @param obj the example deployable + * @param rule the testing rule for determining a valid deployable + * @return any deployable that is found + */ + def DisplaceFirst(obj : DeployableToolbox.AcceptableDeployable, rule : (Deployable)=> Boolean) : Option[DeployableToolbox.AcceptableDeployable] = { + val definition = obj.Definition + val category = definition.DeployCategory + val categoryList = deployableLists(category) + if(categoryList.nonEmpty) { + val found = categoryList.find(rule) match { + case Some(target) => + categoryList.remove(categoryList.indexOf(target)) + case None => + categoryList.remove(0) + } + categoryCounts(category).Current -= 1 + deployableCounts(DeployableToolbox.UnifiedType(found.Definition.Item)).Current -= 1 + Some(found) + } + else { + None + } + } + + /** + * Remove the first managed deployable from a category. + * The only test performed is whether there is any valid deployable managed for the category. + * @param category the target category + * @return any deployable that is found + */ + def DisplaceFirst(category : DeployableCategory.Value) : Option[DeployableToolbox.AcceptableDeployable] = { + val categoryList = deployableLists(category) + if(categoryList.nonEmpty) { + val found = categoryList.remove(0) + categoryCounts(category).Current -= 1 + deployableCounts(DeployableToolbox.UnifiedType(found.Definition.Item)).Current -= 1 + Some(found) + } + else { + None + } + } + + /** + * Reference all managed deployables of the same type as an example deployable. + * @param filter the example deployable + * @return a list of globally unique identifiers that should be valid for the current zone + */ + def Deployables(filter : DeployableToolbox.AcceptableDeployable) : List[PlanetSideGUID] = { + Deployables(filter.Definition.Item) + } + + /** + * Reference all managed deployables of the same type. + * @param filter the type of deployable + * @return a list of globally unique identifiers that should be valid for the current zone + */ + def Deployables(filter : DeployedItem.Value) : List[PlanetSideGUID] = { + deployableLists(Deployable.Category.Of(filter)) + .filter(entry => { entry.Definition.Item == filter }) + .map(_.GUID).toList + } + + /** + * Reference all managed deployables in the same category as an example deployable. + * @param filter the example deployable + * @return a list of globally unique identifiers that should be valid for the current zone + */ + def Category(filter : DeployableToolbox.AcceptableDeployable) : List[PlanetSideGUID] = { + Category(filter.Definition.DeployCategory) + } + + /** + * Reference all managed deployables in the same category. + * @param filter the type of deployable + * @return a list of globally unique identifiers that should be valid for the current zone + */ + def Category(filter : DeployableCategory.Value) : List[PlanetSideGUID] = { + deployableLists(filter).map(_.GUID).toList + } + + /** + * Check the current capacity for the same type of deployable as the example. + * @param item the example deployable + * @return the current quantity of deployables and the maximum number + */ + def CountDeployable(item : DeployedItem.Value) : (Int, Int) = { + val dType = deployableCounts(DeployableToolbox.UnifiedType(item)) + (dType.Current, dType.Max) + } + + /** + * Check the current capacity for the same category of deployable as the example. + * @param item the example deployable + * @return the current quantity of deployables and the maximum number + */ + def CountCategory(item : DeployedItem.Value) : (Int, Int) = { + val dCat = categoryCounts(Deployable.Category.Of(DeployableToolbox.UnifiedType(item))) + (dCat.Current, dCat.Max) + } + + def UpdateUIElement(entry : DeployedItem.Value) : List[(Int,Int,Int,Int)] = { + val toEntry = DeployableToolbox.UnifiedType(entry) + val (curr, max) = Deployable.UI(toEntry) + val dType = deployableCounts(toEntry) + List((curr, dType.Current, max, dType.Max)) + } + + def UpdateUI() : List[(Int,Int,Int,Int)] = DeployedItem.values flatMap UpdateUIElement toList + + def UpdateUI(entry : CertificationType.Value) : List[(Int,Int,Int,Int)] = { + import CertificationType._ + entry match { + case AdvancedHacking => + UpdateUIElement(DeployedItem.sensor_shield) + + case CombatEngineering => + List( + DeployedItem.boomer, DeployedItem.he_mine, DeployedItem.spitfire_turret, DeployedItem.motionalarmsensor + ) flatMap UpdateUIElement + + case AssaultEngineering => + List( + DeployedItem.jammer_mine, DeployedItem.portable_manned_turret, DeployedItem.deployable_shield_generator + ) flatMap UpdateUIElement + + case FortificationEngineering => + List( + DeployedItem.boomer, + DeployedItem.he_mine, + DeployedItem.spitfire_turret, DeployedItem.spitfire_cloaked, DeployedItem.spitfire_aa, + DeployedItem.motionalarmsensor, + DeployedItem.tank_traps + ) flatMap UpdateUIElement + + case AdvancedEngineering => + List(AssaultEngineering, FortificationEngineering) flatMap UpdateUI + + case _ => + Nil + } + } + + def UpdateUI(certifications : List[CertificationType.Value]) : List[(Int,Int,Int,Int)] = { + certifications flatMap UpdateUI + } + + /** + * Remove all managed deployables that are the same type. + * @param item the deployable type + * @return a list of globally unique identifiers that should be valid for the current zone + */ + def ClearDeployable(item : DeployedItem.Value) : List[PlanetSideGUID] = { + val uitem = DeployableToolbox.UnifiedType(item) + val category = Deployable.Category.Of(uitem) + val categoryList = deployableLists(category) + val (out, in) = categoryList.partition(_.Definition.Item == item) + + categoryList.clear() + categoryList ++= in + categoryCounts(category).Current = in.size + deployableCounts(uitem).Current = 0 + out.map(_.GUID).toList + } + + /** + * Remove all managed deployables that belong to the same category. + * @param item the deployable type belonging to a category + * @return a list of globally unique identifiers that should be valid for the current zone + */ + def ClearCategory(item : DeployedItem.Value) : List[PlanetSideGUID] = { + val category = Deployable.Category.Of(DeployableToolbox.UnifiedType(item)) + val out = deployableLists(category).map(_.GUID).toList + deployableLists(category).clear() + categoryCounts(category).Current = 0 + (Deployable.Category.Includes(category) map DeployableToolbox.UnifiedType toSet) + .foreach({item : DeployedItem.Value => deployableCounts(item).Current = 0 }) + out + } + + /** + * Remove all managed deployables. + * @return a list of globally unique identifiers that should be valid for the current zone + */ + def Clear() : List[PlanetSideGUID] = { + val out = deployableLists.values.flatten.map(_.GUID).toList + deployableLists.values.foreach(_.clear()) + deployableCounts.values.foreach(_.Current = 0) + categoryCounts.values.foreach(_.Current = 0) + out + } +} + +object DeployableToolbox { + /** + * A `type` intended to properly define the minimum acceptable conditions for a `Deployable` object. + */ + type AcceptableDeployable = PlanetSideGameObject with Deployable + + /** + * An internal class to keep track of the quantity of deployables managed for a certain set of criteria. + * There are deployable numbers organized by deploybale type and by deployable category. + */ + private class Bin { + /** the maximum number of deployables for this criteria that can be managed */ + private var max : Int = 0 + /** the current number of deployables for this criteria that are being managed */ + private var current : Int = 0 + + def Current : Int = current + + def Current_=(curr : Int) : Int = { + current = curr + Current + } + + def Max : Int = max + + def Max_=(mx : Int) : Int = { + max = mx + Max + } + + def Available() : Boolean = current < max + } + + /** + * Some deployable types, though unique themselves, + * resolve to the same deployable type for the purposes of categorization. + * @param item the type of deployable + * @return the corrected deployable type + */ + def UnifiedType(item : DeployedItem.Value) : DeployedItem.Value = item match { + case DeployedItem.portable_manned_turret_nc | DeployedItem.portable_manned_turret_tr | DeployedItem.portable_manned_turret_vs => + DeployedItem.portable_manned_turret + case _ => + item + } + + /** + * Hardcoded maximum values for the category and type initialization. + * @param counts a reference to the type `Bin` object + * @param categories a reference to the category `Bin` object + * @param certifications a group of certifications for the initial values + */ + private def Initialize(counts : Map[DeployedItem.Value, DeployableToolbox.Bin], categories : Map[DeployableCategory.Value, DeployableToolbox.Bin], certifications : Set[CertificationType.Value]) : Unit = { + import CertificationType._ + if(certifications.contains(AdvancedEngineering)) { + counts(DeployedItem.boomer).Max = 25 + counts(DeployedItem.he_mine).Max = 25 + counts(DeployedItem.jammer_mine).Max = 20 + counts(DeployedItem.spitfire_turret).Max = 15 + counts(DeployedItem.spitfire_cloaked).Max = 5 + counts(DeployedItem.spitfire_aa).Max = 5 + counts(DeployedItem.motionalarmsensor).Max = 25 + counts(DeployedItem.tank_traps).Max = 5 + counts(DeployedItem.portable_manned_turret).Max = 1 //the below turret types are unified + //counts(DeployedItem.portable_manned_turret_nc).Max = 1 + //counts(DeployedItem.portable_manned_turret_tr).Max = 1 + //counts(DeployedItem.portable_manned_turret_vs).Max = 1 + counts(DeployedItem.deployable_shield_generator).Max = 1 + categories(DeployableCategory.Boomers).Max = 25 + categories(DeployableCategory.Mines).Max = 25 + categories(DeployableCategory.SmallTurrets).Max = 15 + categories(DeployableCategory.Sensors).Max = 25 + categories(DeployableCategory.TankTraps).Max = 5 + categories(DeployableCategory.FieldTurrets).Max = 1 + categories(DeployableCategory.ShieldGenerators).Max = 1 + + if(certifications.contains(AdvancedHacking)) { + counts(DeployedItem.sensor_shield).Max = 25 + } + } + else if(certifications.contains(CombatEngineering)) { + if(certifications.contains(AssaultEngineering)) { + counts(DeployedItem.jammer_mine).Max = 20 + counts(DeployedItem.portable_manned_turret).Max = 1 //the below turret types are unified + //counts(DeployedItem.portable_manned_turret_nc).Max = 1 + //counts(DeployedItem.portable_manned_turret_tr).Max = 1 + //counts(DeployedItem.portable_manned_turret_vs).Max = 1 + counts(DeployedItem.deployable_shield_generator).Max = 1 + categories(DeployableCategory.FieldTurrets).Max = 1 + categories(DeployableCategory.ShieldGenerators).Max = 1 + } + if(certifications.contains(FortificationEngineering)) { + counts(DeployedItem.boomer).Max = 25 + counts(DeployedItem.he_mine).Max = 25 + counts(DeployedItem.spitfire_turret).Max = 15 + counts(DeployedItem.spitfire_cloaked).Max = 5 + counts(DeployedItem.spitfire_aa).Max = 5 + counts(DeployedItem.motionalarmsensor).Max = 25 + counts(DeployedItem.tank_traps).Max = 5 + categories(DeployableCategory.Boomers).Max = 25 + categories(DeployableCategory.Mines).Max = 25 + categories(DeployableCategory.SmallTurrets).Max = 15 + categories(DeployableCategory.Sensors).Max = 25 + categories(DeployableCategory.TankTraps).Max = 5 + } + else { + counts(DeployedItem.boomer).Max = 20 + counts(DeployedItem.he_mine).Max = 20 + counts(DeployedItem.spitfire_turret).Max = 10 + counts(DeployedItem.motionalarmsensor).Max = 20 + categories(DeployableCategory.Boomers).Max = 20 + categories(DeployableCategory.Mines).Max = 20 + categories(DeployableCategory.SmallTurrets).Max = 10 + categories(DeployableCategory.Sensors).Max = 20 + } + + if(certifications.contains(AdvancedHacking)) { + counts(DeployedItem.sensor_shield).Max = 20 + } + } + if(certifications.contains(CertificationType.GroundSupport)) { + counts(DeployedItem.router_telepad_deployable).Max = 1 + categories(DeployableCategory.Telepads).Max = 1 + } + } + + /** + * Hardcoded maximum values for the category and type initialization upon providing a new certification. + * @param counts a reference to the type `Bin` object + * @param categories a reference to the category `Bin` object + * @param certification the new certification + * @param certificationSet the group of previous certifications being tracked + */ + def AddToDeployableQuantities(counts : Map[DeployedItem.Value, DeployableToolbox.Bin], categories : Map[DeployableCategory.Value, DeployableToolbox.Bin], certification : CertificationType.Value, certificationSet : Set[CertificationType.Value]) : Unit = { + import CertificationType._ + if(certificationSet contains certification) { + certification match { + case AdvancedHacking => + if(certificationSet contains CombatEngineering) { + counts(DeployedItem.sensor_shield).Max = 20 + } + + case CombatEngineering => + counts(DeployedItem.boomer).Max = 20 + counts(DeployedItem.he_mine).Max = 20 + counts(DeployedItem.spitfire_turret).Max = 10 + counts(DeployedItem.motionalarmsensor).Max = 20 + categories(DeployableCategory.Boomers).Max = 20 + categories(DeployableCategory.Mines).Max = 20 + categories(DeployableCategory.SmallTurrets).Max = 10 + categories(DeployableCategory.Sensors).Max = 20 + if(certificationSet contains AdvancedHacking) { + counts(DeployedItem.sensor_shield).Max = 20 + } + + case AssaultEngineering => + counts(DeployedItem.jammer_mine).Max = 20 + counts(DeployedItem.portable_manned_turret).Max = 1 //the below turret types are unified + //counts(DeployedItem.portable_manned_turret_nc).Max = 1 + //counts(DeployedItem.portable_manned_turret_tr).Max = 1 + //counts(DeployedItem.portable_manned_turret_vs).Max = 1 + counts(DeployedItem.deployable_shield_generator).Max = 1 + categories(DeployableCategory.FieldTurrets).Max = 1 + categories(DeployableCategory.ShieldGenerators).Max = 1 + + case FortificationEngineering => + counts(DeployedItem.boomer).Max = 25 + counts(DeployedItem.he_mine).Max = 25 + counts(DeployedItem.spitfire_turret).Max = 15 + counts(DeployedItem.motionalarmsensor).Max = 25 + counts(DeployedItem.spitfire_cloaked).Max = 5 + counts(DeployedItem.spitfire_aa).Max = 5 + counts(DeployedItem.tank_traps).Max = 5 + categories(DeployableCategory.Boomers).Max = 25 + categories(DeployableCategory.Mines).Max = 25 + categories(DeployableCategory.SmallTurrets).Max = 15 + categories(DeployableCategory.Sensors).Max = 25 + categories(DeployableCategory.TankTraps).Max = 5 + + case AdvancedEngineering => + if(!certificationSet.contains(AssaultEngineering)) { + AddToDeployableQuantities(counts, categories, AssaultEngineering, certificationSet ++ Set(AssaultEngineering)) + } + if(!certificationSet.contains(FortificationEngineering)) { + AddToDeployableQuantities(counts, categories, FortificationEngineering, certificationSet ++ Set(FortificationEngineering)) + } + + case GroundSupport => + counts(DeployedItem.router_telepad_deployable).Max = 1024 + categories(DeployableCategory.Telepads).Max = 1024 + + case _ => ; + } + } + } + + /** + * Hardcoded zero'd values for the category and type initialization upon ignoring a previous certification. + * @param counts a reference to the type `Bin` object + * @param categories a reference to the category `Bin` object + * @param certification the new certification + * @param certificationSet the group of previous certifications being tracked + */ + def RemoveFromDeployablesQuantities(counts : Map[DeployedItem.Value, DeployableToolbox.Bin], categories : Map[DeployableCategory.Value, DeployableToolbox.Bin], certification : CertificationType.Value, certificationSet : Set[CertificationType.Value]) : Unit = { + import CertificationType._ + if(!certificationSet.contains(certification)) { + certification match { + case AdvancedHacking => + counts(DeployedItem.sensor_shield).Max = 0 + + case CombatEngineering => + counts(DeployedItem.boomer).Max = 0 + counts(DeployedItem.he_mine).Max = 0 + counts(DeployedItem.spitfire_turret).Max = 0 + counts(DeployedItem.motionalarmsensor).Max = 0 + counts(DeployedItem.sensor_shield).Max = 0 + categories(DeployableCategory.Boomers).Max = 0 + categories(DeployableCategory.Mines).Max = 0 + categories(DeployableCategory.SmallTurrets).Max = 0 + categories(DeployableCategory.Sensors).Max = 0 + + case AssaultEngineering => + counts(DeployedItem.jammer_mine).Max = 0 + counts(DeployedItem.portable_manned_turret).Max = 0 //the below turret types are unified + //counts(DeployedItem.portable_manned_turret_nc).Max = 0 + //counts(DeployedItem.portable_manned_turret_tr).Max = 0 + //counts(DeployedItem.portable_manned_turret_vs).Max = 0 + counts(DeployedItem.deployable_shield_generator).Max = 0 + categories(DeployableCategory.Sensors).Max = if(certificationSet contains CombatEngineering) 20 else 0 + categories(DeployableCategory.FieldTurrets).Max = 0 + categories(DeployableCategory.ShieldGenerators).Max = 0 + + case FortificationEngineering => + val ce : Int = if(certificationSet contains CombatEngineering) 1 else 0 //true = 1, false = 0 + counts(DeployedItem.boomer).Max = ce * 20 + counts(DeployedItem.he_mine).Max = ce * 20 + counts(DeployedItem.spitfire_turret).Max = ce * 10 + counts(DeployedItem.motionalarmsensor).Max = ce * 20 + counts(DeployedItem.spitfire_cloaked).Max = 0 + counts(DeployedItem.spitfire_aa).Max = 0 + counts(DeployedItem.tank_traps).Max = 0 + categories(DeployableCategory.Boomers).Max = ce * 20 + categories(DeployableCategory.Mines).Max = ce * 20 + categories(DeployableCategory.SmallTurrets).Max = ce * 10 + categories(DeployableCategory.Sensors).Max = ce * 20 + categories(DeployableCategory.TankTraps).Max = 0 + + case AdvancedEngineering => + if(!certificationSet.contains(AssaultEngineering)) { + RemoveFromDeployablesQuantities(counts, categories, AssaultEngineering, certificationSet) + } + if(!certificationSet.contains(FortificationEngineering)) { + RemoveFromDeployablesQuantities(counts, categories, FortificationEngineering, certificationSet) + } + + case GroundSupport => + counts(DeployedItem.router_telepad_deployable).Max = 0 + categories(DeployableCategory.Telepads).Max = 0 + + case _ => ; + } + } + } +} diff --git a/common/src/main/scala/net/psforever/objects/ballistics/ComplexDeployableSource.scala b/common/src/main/scala/net/psforever/objects/ballistics/ComplexDeployableSource.scala new file mode 100644 index 00000000..32e86ecd --- /dev/null +++ b/common/src/main/scala/net/psforever/objects/ballistics/ComplexDeployableSource.scala @@ -0,0 +1,51 @@ +// Copyright (c) 2017 PSForever +package net.psforever.objects.ballistics + +import net.psforever.objects.TurretDeployable +import net.psforever.objects.ce.ComplexDeployable +import net.psforever.objects.definition.{BaseDeployableDefinition, ObjectDefinition} +import net.psforever.types.{PlanetSideEmpire, Vector3} + +final case class ComplexDeployableSource(obj_def : ObjectDefinition with BaseDeployableDefinition, + faction : PlanetSideEmpire.Value, + health : Int, + shields : Int, + ownerName : String, + position : Vector3, + orientation : Vector3) extends SourceEntry { + override def Name = SourceEntry.NameFormat(obj_def.Name) + override def Faction = faction + def Definition : ObjectDefinition with BaseDeployableDefinition = obj_def + def Health = health + def Shields = shields + def OwnerName = ownerName + def Position = position + def Orientation = orientation + def Velocity = None +} + +object ComplexDeployableSource { + def apply(obj : ComplexDeployable) : ComplexDeployableSource = { + ComplexDeployableSource( + obj.Definition, + obj.Faction, + obj.Health, + obj.Shields, + obj.OwnerName.getOrElse(""), + obj.Position, + obj.Orientation + ) + } + + def apply(obj : TurretDeployable) : ComplexDeployableSource = { + ComplexDeployableSource( + obj.Definition, + obj.Faction, + obj.Health, + obj.Shields, + obj.OwnerName.getOrElse(""), + obj.Position, + obj.Orientation + ) + } +} diff --git a/common/src/main/scala/net/psforever/objects/ballistics/DeployableSource.scala b/common/src/main/scala/net/psforever/objects/ballistics/DeployableSource.scala new file mode 100644 index 00000000..bf504009 --- /dev/null +++ b/common/src/main/scala/net/psforever/objects/ballistics/DeployableSource.scala @@ -0,0 +1,36 @@ +// Copyright (c) 2017 PSForever +package net.psforever.objects.ballistics + +import net.psforever.objects.PlanetSideGameObject +import net.psforever.objects.ce.Deployable +import net.psforever.objects.definition.{BaseDeployableDefinition, ObjectDefinition} +import net.psforever.types.{PlanetSideEmpire, Vector3} + +final case class DeployableSource(obj_def : ObjectDefinition with BaseDeployableDefinition, + faction : PlanetSideEmpire.Value, + health : Int, + ownerName : String, + position : Vector3, + orientation : Vector3) extends SourceEntry { + override def Name = SourceEntry.NameFormat(obj_def.Name) + override def Faction = faction + def Definition : ObjectDefinition with BaseDeployableDefinition = obj_def + def Health = health + def OwnerName = ownerName + def Position = position + def Orientation = orientation + def Velocity = None +} + +object DeployableSource { + def apply(obj : PlanetSideGameObject with Deployable) : DeployableSource = { + DeployableSource( + obj.Definition, + obj.Faction, + obj.Health, + obj.OwnerName.getOrElse(""), + obj.Position, + obj.Orientation + ) + } +} diff --git a/common/src/main/scala/net/psforever/objects/ballistics/SourceEntry.scala b/common/src/main/scala/net/psforever/objects/ballistics/SourceEntry.scala index 5c2eed85..ccc32092 100644 --- a/common/src/main/scala/net/psforever/objects/ballistics/SourceEntry.scala +++ b/common/src/main/scala/net/psforever/objects/ballistics/SourceEntry.scala @@ -1,8 +1,9 @@ // Copyright (c) 2017 PSForever package net.psforever.objects.ballistics +import net.psforever.objects.ce.{ComplexDeployable, SimpleDeployable} import net.psforever.objects.definition.ObjectDefinition -import net.psforever.objects.{PlanetSideGameObject, Player, Vehicle} +import net.psforever.objects.{PlanetSideGameObject, Player, TurretDeployable, Vehicle} import net.psforever.objects.entity.WorldEntity import net.psforever.objects.serverobject.affinity.FactionAffinity import net.psforever.types.{PlanetSideEmpire, Vector3} @@ -29,6 +30,9 @@ object SourceEntry { target match { case obj : Player => PlayerSource(obj) case obj : Vehicle => VehicleSource(obj) + case obj : ComplexDeployable => ComplexDeployableSource(obj) + case obj : TurretDeployable => ComplexDeployableSource(obj) + case obj : SimpleDeployable => DeployableSource(obj) case _ => ObjectSource(target) } } diff --git a/common/src/main/scala/net/psforever/objects/ce/ComplexDeployable.scala b/common/src/main/scala/net/psforever/objects/ce/ComplexDeployable.scala new file mode 100644 index 00000000..5dec6c65 --- /dev/null +++ b/common/src/main/scala/net/psforever/objects/ce/ComplexDeployable.scala @@ -0,0 +1,27 @@ +// Copyright (c) 2017 PSForever +package net.psforever.objects.ce + +import net.psforever.objects.definition.DeployableDefinition +import net.psforever.objects.serverobject.PlanetSideServerObject + +abstract class ComplexDeployable(cdef : DeployableDefinition) extends PlanetSideServerObject + with Deployable { + Health = Definition.MaxHealth + + def MaxHealth : Int = Definition.MaxHealth + + private var shields : Int = 0 + + def Shields : Int = shields + + def Shields_=(toShields : Int) : Int = { + shields = math.min(math.max(0, toShields), MaxShields) + Shields + } + + def MaxShields : Int = { + 0//Definition.MaxShields + } + + def Definition = cdef +} diff --git a/common/src/main/scala/net/psforever/objects/ce/Deployable.scala b/common/src/main/scala/net/psforever/objects/ce/Deployable.scala new file mode 100644 index 00000000..d04c6b01 --- /dev/null +++ b/common/src/main/scala/net/psforever/objects/ce/Deployable.scala @@ -0,0 +1,151 @@ +// Copyright (c) 2017 PSForever +package net.psforever.objects.ce + +import net.psforever.objects._ +import net.psforever.objects.definition.{BaseDeployableDefinition, ObjectDefinition} +import net.psforever.objects.serverobject.affinity.FactionAffinity +import net.psforever.objects.vital.{DamageResistanceModel, Vitality} +import net.psforever.packet.game.{DeployableIcon, PlanetSideGUID} +import net.psforever.types.PlanetSideEmpire + +trait Deployable extends FactionAffinity + with Vitality { + this : PlanetSideGameObject => + private var health : Int = 1 + private var faction : PlanetSideEmpire.Value = PlanetSideEmpire.NEUTRAL + private var owner : Option[PlanetSideGUID] = None + private var ownerName : Option[String] = None + + def Health : Int = health + + def Health_=(toHealth : Int) : Int = { + health = math.min(math.max(0, toHealth), MaxHealth) + Health + } + + def MaxHealth : Int + + def Faction : PlanetSideEmpire.Value = faction + + override def Faction_=(toFaction : PlanetSideEmpire.Value) : PlanetSideEmpire.Value = { + faction = toFaction + Faction + } + + def Owner : Option[PlanetSideGUID] = owner + + def Owner_=(owner : PlanetSideGUID) : Option[PlanetSideGUID] = Owner_=(Some(owner)) + + def Owner_=(owner : Player) : Option[PlanetSideGUID] = Owner_=(Some(owner.GUID)) + + def Owner_=(owner : Option[PlanetSideGUID]) : Option[PlanetSideGUID] = { + owner match { + case Some(_) => + this.owner = owner + case None => + this.owner = None + } + Owner + } + + def OwnerName : Option[String] = ownerName + + def OwnerName_=(owner : String) : Option[String] = OwnerName_=(Some(owner)) + + def OwnerName_=(owner : Player) : Option[String] = OwnerName_=(Some(owner.Name)) + + def OwnerName_=(owner : Option[String]) : Option[String] = { + owner match { + case Some(_) => + ownerName = owner + case None => + ownerName = None + } + OwnerName + } + + def DamageModel : DamageResistanceModel = Definition.asInstanceOf[DamageResistanceModel] + + def Definition : ObjectDefinition with BaseDeployableDefinition +} + +object Deployable { + object Category { + def Of(item : DeployedItem.Value) : DeployableCategory.Value = deployablesToCategories(item) + + def Includes(category : DeployableCategory.Value) : List[DeployedItem.Value] = { + (for { + (ce, cat) <- deployablesToCategories + if cat == category + } yield ce) toList + } + + def OfAll() : Map[DeployedItem.Value, DeployableCategory.Value] = deployablesToCategories + + private val deployablesToCategories : Map[DeployedItem.Value, DeployableCategory.Value] = Map( + DeployedItem.boomer -> DeployableCategory.Boomers, + DeployedItem.he_mine -> DeployableCategory.Mines, + DeployedItem.jammer_mine -> DeployableCategory.Mines, + DeployedItem.spitfire_turret -> DeployableCategory.SmallTurrets, + DeployedItem.motionalarmsensor -> DeployableCategory.Sensors, + DeployedItem.spitfire_cloaked -> DeployableCategory.SmallTurrets, + DeployedItem.spitfire_aa -> DeployableCategory.SmallTurrets, + DeployedItem.deployable_shield_generator -> DeployableCategory.ShieldGenerators, + DeployedItem.tank_traps -> DeployableCategory.TankTraps, + DeployedItem.portable_manned_turret -> DeployableCategory.FieldTurrets, + DeployedItem.portable_manned_turret_nc -> DeployableCategory.FieldTurrets, + DeployedItem.portable_manned_turret_tr -> DeployableCategory.FieldTurrets, + DeployedItem.portable_manned_turret_vs -> DeployableCategory.FieldTurrets, + DeployedItem.sensor_shield -> DeployableCategory.Sensors, + DeployedItem.router_telepad_deployable -> DeployableCategory.Telepads + ) + } + + object Icon { + def apply(item : DeployedItem.Value) : DeployableIcon.Value = ceicon(item.id) + + private val ceicon : Map[Int, DeployableIcon.Value] = Map( + DeployedItem.boomer.id -> DeployableIcon.Boomer, + DeployedItem.he_mine.id -> DeployableIcon.HEMine, + DeployedItem.jammer_mine.id -> DeployableIcon.DisruptorMine, + DeployedItem.spitfire_turret.id -> DeployableIcon.SpitfireTurret, + DeployedItem.spitfire_cloaked.id -> DeployableIcon.ShadowTurret, + DeployedItem.spitfire_aa.id -> DeployableIcon.CerebusTurret, + DeployedItem.motionalarmsensor.id -> DeployableIcon.MotionAlarmSensor, + DeployedItem.sensor_shield.id -> DeployableIcon.SensorDisruptor, + DeployedItem.tank_traps.id -> DeployableIcon.TRAP, + DeployedItem.portable_manned_turret.id -> DeployableIcon.FieldTurret, + DeployedItem.portable_manned_turret_tr.id -> DeployableIcon.FieldTurret, + DeployedItem.portable_manned_turret_nc.id -> DeployableIcon.FieldTurret, + DeployedItem.portable_manned_turret_vs.id -> DeployableIcon.FieldTurret, + DeployedItem.deployable_shield_generator.id -> DeployableIcon.AegisShieldGenerator + ).withDefaultValue(DeployableIcon.Boomer) + } + + object UI { + def apply(item : DeployedItem.Value) : (Int, Int) = planetsideAttribute(item) + + /** + * The attribute values to be invoked in `PlanetsideAttributeMessage` packets + * in reference to a particular combat engineering deployable element on the UI. + * The first number is for the actual count field. + * The second number is for the maximum count field. + */ + private val planetsideAttribute : Map[DeployedItem.Value, (Int, Int)] = Map( + DeployedItem.boomer -> (94, 83), + DeployedItem.he_mine -> (95, 84), + DeployedItem.jammer_mine -> (96, 85), + DeployedItem.spitfire_turret -> (97, 86), + DeployedItem.motionalarmsensor -> (98, 87), + DeployedItem.spitfire_cloaked -> (99, 88), + DeployedItem.spitfire_aa -> (100, 89), + DeployedItem.deployable_shield_generator -> (101, 90), + DeployedItem.tank_traps -> (102, 91), + DeployedItem.portable_manned_turret -> (103, 92), + DeployedItem.portable_manned_turret_nc -> (103, 92), + DeployedItem.portable_manned_turret_tr -> (103, 92), + DeployedItem.portable_manned_turret_vs -> (103, 92), + DeployedItem.sensor_shield -> (104, 93) + ).withDefaultValue((0,0)) + } +} diff --git a/common/src/main/scala/net/psforever/objects/ce/DeployableCategory.scala b/common/src/main/scala/net/psforever/objects/ce/DeployableCategory.scala new file mode 100644 index 00000000..72f66e55 --- /dev/null +++ b/common/src/main/scala/net/psforever/objects/ce/DeployableCategory.scala @@ -0,0 +1,17 @@ +// Copyright (c) 2017 PSForever +package net.psforever.objects.ce + +object DeployableCategory extends Enumeration { + type Type = Value + + val + Boomers, + Mines, + SmallTurrets, + Sensors, + TankTraps, + FieldTurrets, + ShieldGenerators, + Telepads + = Value +} diff --git a/common/src/main/scala/net/psforever/objects/ce/DeployedItem.scala b/common/src/main/scala/net/psforever/objects/ce/DeployedItem.scala new file mode 100644 index 00000000..2c5eb553 --- /dev/null +++ b/common/src/main/scala/net/psforever/objects/ce/DeployedItem.scala @@ -0,0 +1,22 @@ +// Copyright (c) 2017 PSForever +package net.psforever.objects.ce + +object DeployedItem extends Enumeration { + type Type = Value + + 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/ce/SimpleDeployable.scala b/common/src/main/scala/net/psforever/objects/ce/SimpleDeployable.scala new file mode 100644 index 00000000..38200737 --- /dev/null +++ b/common/src/main/scala/net/psforever/objects/ce/SimpleDeployable.scala @@ -0,0 +1,14 @@ +// Copyright (c) 2017 PSForever +package net.psforever.objects.ce + +import net.psforever.objects.PlanetSideGameObject +import net.psforever.objects.definition.DeployableDefinition + +abstract class SimpleDeployable(cdef : DeployableDefinition) extends PlanetSideGameObject + with Deployable { + Health = Definition.MaxHealth + + def MaxHealth : Int = Definition.MaxHealth + + def Definition = cdef +} diff --git a/common/src/main/scala/net/psforever/objects/definition/BasicDefinition.scala b/common/src/main/scala/net/psforever/objects/definition/BasicDefinition.scala index f7a7b61b..4de5b0cd 100644 --- a/common/src/main/scala/net/psforever/objects/definition/BasicDefinition.scala +++ b/common/src/main/scala/net/psforever/objects/definition/BasicDefinition.scala @@ -3,6 +3,7 @@ package net.psforever.objects.definition abstract class BasicDefinition { private var name : String = "definition" + private var descriptor : Option[String] = None def Name : String = name @@ -10,4 +11,13 @@ abstract class BasicDefinition { this.name = name Name } + + def Descriptor : String = descriptor.getOrElse(Name) + + def Descriptor_=(description : String) : String = Descriptor_=(Some(description)) + + def Descriptor_=(description : Option[String]) : String = { + descriptor = description + Descriptor + } } diff --git a/common/src/main/scala/net/psforever/objects/definition/ConstructionItemDefinition.scala b/common/src/main/scala/net/psforever/objects/definition/ConstructionItemDefinition.scala index 4a3eb9bf..7370b006 100644 --- a/common/src/main/scala/net/psforever/objects/definition/ConstructionItemDefinition.scala +++ b/common/src/main/scala/net/psforever/objects/definition/ConstructionItemDefinition.scala @@ -1,15 +1,19 @@ // Copyright (c) 2017 PSForever package net.psforever.objects.definition +import net.psforever.objects.ce.DeployedItem +import net.psforever.objects.definition.converter.ACEConverter import net.psforever.objects.equipment.CItem +import net.psforever.types.CertificationType 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() + CItem(objectId) //let throw NoSuchElementException + private val modes : ListBuffer[ConstructionFireMode] = ListBuffer() + Packet = new ACEConverter - def Modes : ListBuffer[CItem.DeployedItem.Value] = modes + def Modes : ListBuffer[ConstructionFireMode] = modes } object ConstructionItemDefinition { @@ -17,7 +21,29 @@ object ConstructionItemDefinition { new ConstructionItemDefinition(objectId) } - def apply(cItem : CItem.Unit.Value) : ConstructionItemDefinition = { + def apply(cItem : CItem.Value) : ConstructionItemDefinition = { new ConstructionItemDefinition(cItem.id) } } + +class ConstructionFireMode { + private val deployables : ListBuffer[DeployedItem.Value] = ListBuffer.empty + private val permissions : ListBuffer[Set[CertificationType.Value]] = ListBuffer.empty + + def Permissions : ListBuffer[Set[CertificationType.Value]] = permissions + + def Deployables : ListBuffer[DeployedItem.Value] = deployables + + def Item(deployable : DeployedItem.Value) : ListBuffer[DeployedItem.Value] = { + deployables += deployable + permissions += Set.empty[CertificationType.Value] + deployables + } + + def Item(deployPair : (DeployedItem.Value, Set[CertificationType.Value])) : ListBuffer[DeployedItem.Value] = { + val (deployable, permission) = deployPair + deployables += deployable + permissions += permission + deployables + } +} diff --git a/common/src/main/scala/net/psforever/objects/definition/DeployableDefinition.scala b/common/src/main/scala/net/psforever/objects/definition/DeployableDefinition.scala new file mode 100644 index 00000000..48fe1586 --- /dev/null +++ b/common/src/main/scala/net/psforever/objects/definition/DeployableDefinition.scala @@ -0,0 +1,72 @@ +// Copyright (c) 2017 PSForever +package net.psforever.objects.definition + +import akka.actor.{ActorContext, ActorRef} +import net.psforever.objects.PlanetSideGameObject +import net.psforever.objects.ce.{Deployable, DeployableCategory, DeployedItem} +import net.psforever.objects.definition.converter.SmallDeployableConverter +import net.psforever.objects.serverobject.PlanetSideServerObject +import net.psforever.objects.vital.{DamageResistanceModel, NoResistanceSelection, StandardDeployableDamage} + +import scala.concurrent.duration._ + +trait BaseDeployableDefinition extends DamageResistanceModel { + private var category : DeployableCategory.Value = DeployableCategory.Boomers + private var deployTime : Long = (1 second).toMillis //ms + private var maxHealth : Int = 1 + Damage = StandardDeployableDamage + Resistance = NoResistanceSelection + + def Item : DeployedItem.Value + + def MaxHealth : Int = maxHealth + + def MaxHealth_=(toHealth : Int) : Int = { + maxHealth = toHealth + MaxHealth + } + + def DeployCategory : DeployableCategory.Value = category + + def DeployCategory_=(cat : DeployableCategory.Value) : DeployableCategory.Value = { + category = cat + DeployCategory + } + + def DeployTime : Long = deployTime + + def DeployTime_=(time : FiniteDuration) : Long = DeployTime_=(time.toMillis) + + def DeployTime_=(time: Long) : Long = { + deployTime = time + DeployTime + } + + def Initialize(obj : PlanetSideGameObject with Deployable, context : ActorContext) : Unit = { } + + def Initialize(obj : PlanetSideServerObject with Deployable, context : ActorContext) : Unit = { } + + def Uninitialize(obj : PlanetSideGameObject with Deployable, context : ActorContext) : Unit = { } + + def Uninitialize(obj : PlanetSideServerObject with Deployable, context : ActorContext) : Unit = { } +} + +class DeployableDefinition(private val objectId : Int) extends ObjectDefinition(objectId) + with BaseDeployableDefinition { + private val item = DeployedItem(objectId) //let throw NoSuchElementException + Packet = new SmallDeployableConverter + + def Item : DeployedItem.Value = item +} + +object DeployableDefinition { + def apply(item : DeployedItem.Value) : DeployableDefinition = + new DeployableDefinition(item.id) + + def SimpleUninitialize(obj : PlanetSideGameObject, context : ActorContext) : Unit = { } + + def SimpleUninitialize(obj : PlanetSideServerObject, context : ActorContext) : Unit = { + obj.Actor ! akka.actor.PoisonPill + obj.Actor = ActorRef.noSender + } +} diff --git a/common/src/main/scala/net/psforever/objects/definition/converter/FieldTurretConverter.scala b/common/src/main/scala/net/psforever/objects/definition/converter/FieldTurretConverter.scala new file mode 100644 index 00000000..0139069d --- /dev/null +++ b/common/src/main/scala/net/psforever/objects/definition/converter/FieldTurretConverter.scala @@ -0,0 +1,69 @@ +// Copyright (c) 2017 PSForever +package net.psforever.objects.definition.converter + +import net.psforever.objects.equipment.Equipment +import net.psforever.objects.serverobject.turret.WeaponTurret +import net.psforever.objects.TurretDeployable +import net.psforever.packet.game.PlanetSideGUID +import net.psforever.packet.game.objectcreate._ + +import scala.util.{Failure, Success, Try} + +class FieldTurretConverter extends ObjectCreateConverter[TurretDeployable]() { + override def ConstructorData(obj : TurretDeployable) : Try[OneMannedFieldTurretData] = { + val health = 255 * obj.Health / obj.MaxHealth //TODO not precise + if(health > 3) { + Success( + OneMannedFieldTurretData( + SmallDeployableData( + PlacementData(obj.Position, obj.Orientation), + obj.Faction, + bops = false, + destroyed = false, + unk1 = 0, + obj.Jammered, + unk2 = false, + obj.Owner match { + case Some(owner) => owner + case None => PlanetSideGUID(0) + } + ), + health, + Some(InventoryData(FieldTurretConverter.MakeMountings(obj))) + ) + ) + } + else { + Success( + OneMannedFieldTurretData( + SmallDeployableData( + PlacementData(obj.Position, obj.Orientation), + obj.Faction, + bops = false, + destroyed = true, + unk1 = 0, + jammered = false, + unk2 = false, + owner_guid = PlanetSideGUID(0) + ), + 0, + None + ) + ) + } + } + + override def DetailedConstructorData(obj : TurretDeployable) : Try[OneMannedFieldTurretData] = + Failure(new Exception("converter should not be used to generate detailed OneMannedFieldTurretData")) +} + +object FieldTurretConverter { + private def MakeMountings(obj : WeaponTurret) : List[InventoryItemData.InventoryItem] = { + obj.Weapons.map({ + case((index, slot)) => + val equip : Equipment = slot.Equipment.get + val equipDef = equip.Definition + InventoryItemData(equipDef.ObjectId, equip.GUID, index, equipDef.Packet.ConstructorData(equip).get) + }).toList + } +} diff --git a/common/src/main/scala/net/psforever/objects/definition/converter/SeatConverter.scala b/common/src/main/scala/net/psforever/objects/definition/converter/SeatConverter.scala new file mode 100644 index 00000000..5b7af1f2 --- /dev/null +++ b/common/src/main/scala/net/psforever/objects/definition/converter/SeatConverter.scala @@ -0,0 +1,31 @@ +// Copyright (c) 2017 PSForever +package net.psforever.objects.definition.converter + +import net.psforever.objects.Player +import net.psforever.objects.vehicles.Seat +import net.psforever.packet.game.objectcreate.{InventoryItemData, ObjectClass, PlayerData, VehicleData} + +object SeatConverter { + def MakeSeat(player : Player, offset : Long) : PlayerData = { + VehicleData.PlayerData( + AvatarConverter.MakeAppearanceData(player), + AvatarConverter.MakeCharacterData(player), + AvatarConverter.MakeInventoryData(player), + AvatarConverter.GetDrawnSlot(player), + offset + ) + } + + //TODO do not use for now; causes seat access permission issues with many passengers; may not mesh with workflows; GUID requirements + def MakeSeats(seats : Map[Int, Seat], initialOffset : Long) : List[InventoryItemData.InventoryItem] = { + var offset = initialOffset + seats + .filter({ case (_, seat) => seat.isOccupied }) + .map({case (index, seat) => + val player = seat.Occupant.get + val entry = InventoryItemData(ObjectClass.avatar, player.GUID, index, SeatConverter.MakeSeat(player, offset)) + offset += entry.bitsize + entry + }).toList + } +} diff --git a/common/src/main/scala/net/psforever/objects/definition/converter/ShieldGeneratorConverter.scala b/common/src/main/scala/net/psforever/objects/definition/converter/ShieldGeneratorConverter.scala new file mode 100644 index 00000000..559d4152 --- /dev/null +++ b/common/src/main/scala/net/psforever/objects/definition/converter/ShieldGeneratorConverter.scala @@ -0,0 +1,52 @@ +// Copyright (c) 2017 PSForever +package net.psforever.objects.definition.converter + +import net.psforever.objects.ShieldGeneratorDeployable +import net.psforever.packet.game.PlanetSideGUID +import net.psforever.packet.game.objectcreate._ + +import scala.util.{Failure, Success, Try} + +class ShieldGeneratorConverter extends ObjectCreateConverter[ShieldGeneratorDeployable]() { + override def ConstructorData(obj : ShieldGeneratorDeployable) : Try[AegisShieldGeneratorData] = { + val health = 255 * obj.Health / obj.MaxHealth //TODO not precise + if(health > 0) { + Success( + AegisShieldGeneratorData( + CommonFieldData( + PlacementData(obj.Position, obj.Orientation), + obj.Faction, + bops = false, + destroyed = false, + unk = 0, + jammered = false, + obj.Owner match { + case Some(owner) => owner + case None => PlanetSideGUID(0) + } + ), + health + ) + ) + } + else { + Success( + AegisShieldGeneratorData( + CommonFieldData( + PlacementData(obj.Position, obj.Orientation), + obj.Faction, + bops = false, + destroyed = true, + unk = 0, + jammered = false, + player_guid = PlanetSideGUID(0) + ), + 0 + ) + ) + } + } + + override def DetailedConstructorData(obj : ShieldGeneratorDeployable) : Try[AegisShieldGeneratorData] = + Failure(new Exception("converter should not be used to generate detailed ShieldGeneratorDdata")) +} diff --git a/common/src/main/scala/net/psforever/objects/definition/converter/SmallDeployableConverter.scala b/common/src/main/scala/net/psforever/objects/definition/converter/SmallDeployableConverter.scala new file mode 100644 index 00000000..bd327b21 --- /dev/null +++ b/common/src/main/scala/net/psforever/objects/definition/converter/SmallDeployableConverter.scala @@ -0,0 +1,32 @@ +// Copyright (c) 2017 PSForever +package net.psforever.objects.definition.converter + +import net.psforever.objects.ce.Deployable +import net.psforever.objects.PlanetSideGameObject +import net.psforever.packet.game.PlanetSideGUID +import net.psforever.packet.game.objectcreate.{PlacementData, SmallDeployableData} + +import scala.util.{Failure, Success, Try} + +class SmallDeployableConverter extends ObjectCreateConverter[PlanetSideGameObject with Deployable]() { + override def ConstructorData(obj : PlanetSideGameObject with Deployable) : Try[SmallDeployableData] = { + Success( + SmallDeployableData( + PlacementData(obj.Position, obj.Orientation), + obj.Faction, + bops = false, + destroyed = false, + unk1 = 0, + jammered = false, + unk2 = false, + obj.Owner match { + case Some(owner) => owner + case None => PlanetSideGUID(0) + } + ) + ) + } + + override def DetailedConstructorData(obj : PlanetSideGameObject with Deployable) : Try[SmallDeployableData] = + Failure(new Exception("converter should not be used to generate detailed SmallDeployableData")) +} \ No newline at end of file diff --git a/common/src/main/scala/net/psforever/objects/definition/converter/SmallTurretConverter.scala b/common/src/main/scala/net/psforever/objects/definition/converter/SmallTurretConverter.scala new file mode 100644 index 00000000..25e74d52 --- /dev/null +++ b/common/src/main/scala/net/psforever/objects/definition/converter/SmallTurretConverter.scala @@ -0,0 +1,69 @@ +// Copyright (c) 2017 PSForever +package net.psforever.objects.definition.converter + +import net.psforever.objects.equipment.Equipment +import net.psforever.objects.TurretDeployable +import net.psforever.objects.serverobject.turret.WeaponTurret +import net.psforever.packet.game.PlanetSideGUID +import net.psforever.packet.game.objectcreate._ + +import scala.util.{Failure, Success, Try} + +class SmallTurretConverter extends ObjectCreateConverter[TurretDeployable]() { + override def ConstructorData(obj : TurretDeployable) : Try[SmallTurretData] = { + val health = 255 * obj.Health / obj.MaxHealth //TODO not precise + if(health > 0) { + Success( + SmallTurretData( + SmallDeployableData( + PlacementData(obj.Position, obj.Orientation), + obj.Faction, + bops = false, + destroyed = false, + unk1 = 0, + obj.Jammered, + unk2 = false, + obj.Owner match { + case Some(owner) => owner + case None => PlanetSideGUID(0) + } + ), + health, + Some(InventoryData(SmallTurretConverter.MakeMountings(obj))) + ) + ) + } + else { + Success( + SmallTurretData( + SmallDeployableData( + PlacementData(obj.Position, obj.Orientation), + obj.Faction, + bops = false, + destroyed = true, + unk1 = 0, + jammered = false, + unk2 = false, + owner_guid = PlanetSideGUID(0) + ), + 0, + None + ) + ) + } + } + + override def DetailedConstructorData(obj : TurretDeployable) : Try[SmallTurretData] = + Failure(new Exception("converter should not be used to generate detailed SmallTurretData")) +} + +object SmallTurretConverter { + private def MakeMountings(obj : WeaponTurret) : List[InventoryItemData.InventoryItem] = { + obj.Weapons.map({ + case((index, slot)) => + val equip : Equipment = slot.Equipment.get + val equipDef = equip.Definition + InventoryItemData(equipDef.ObjectId, equip.GUID, index, equipDef.Packet.ConstructorData(equip).get) + }).toList + } +} diff --git a/common/src/main/scala/net/psforever/objects/definition/converter/TRAPConverter.scala b/common/src/main/scala/net/psforever/objects/definition/converter/TRAPConverter.scala new file mode 100644 index 00000000..3bcfa303 --- /dev/null +++ b/common/src/main/scala/net/psforever/objects/definition/converter/TRAPConverter.scala @@ -0,0 +1,54 @@ +// Copyright (c) 2017 PSForever +package net.psforever.objects.definition.converter + +import net.psforever.objects.TrapDeployable +import net.psforever.packet.game.PlanetSideGUID +import net.psforever.packet.game.objectcreate._ + +import scala.util.{Failure, Success, Try} + +class TRAPConverter extends ObjectCreateConverter[TrapDeployable]() { + override def ConstructorData(obj : TrapDeployable) : Try[TRAPData] = { + val health = 255 * obj.Health / obj.MaxHealth //TODO not precise + if(health > 0) { + Success( + TRAPData( + SmallDeployableData( + PlacementData(obj.Position, obj.Orientation), + obj.Faction, + bops = false, + destroyed = false, + unk1 = 0, + jammered = false, + unk2 = false, + obj.Owner match { + case Some(owner) => owner + case None => PlanetSideGUID(0) + } + ), + health + ) + ) + } + else { + Success( + TRAPData( + SmallDeployableData( + PlacementData(obj.Position, obj.Orientation), + obj.Faction, + bops = false, + destroyed = true, + unk1 = 0, + jammered = false, + unk2 = false, + owner_guid = PlanetSideGUID(0) + ), + 0 + ) + ) + } + } + + override def DetailedConstructorData(obj : TrapDeployable) : Try[TRAPData] = + Failure(new Exception("converter should not be used to generate detailed TRAPData")) +} 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 index 91b4a734..0a11eba0 100644 --- a/common/src/main/scala/net/psforever/objects/definition/converter/VehicleConverter.scala +++ b/common/src/main/scala/net/psforever/objects/definition/converter/VehicleConverter.scala @@ -15,7 +15,7 @@ class VehicleConverter extends ObjectCreateConverter[Vehicle]() { override def ConstructorData(obj : Vehicle) : Try[VehicleData] = { val health = 255 * obj.Health / obj.MaxHealth //TODO not precise - if(health > 3) { //active + if(health > 0) { //active Success( VehicleData( PlacementData(obj.Position, obj.Orientation, obj.Velocity), @@ -72,39 +72,11 @@ class VehicleConverter extends ObjectCreateConverter[Vehicle]() { val offset : Long = VehicleData.InitialStreamLengthToSeatEntries(obj.Velocity.nonEmpty, SpecificFormatModifier) obj.Seats(0).Occupant match { case Some(player) => - val mountedPlayer = VehicleData.PlayerData( - AvatarConverter.MakeAppearanceData(player), - AvatarConverter.MakeCharacterData(player), - AvatarConverter.MakeInventoryData(player), - AvatarConverter.GetDrawnSlot(player), - offset - ) - List(InventoryItemData(ObjectClass.avatar, player.GUID, 0, mountedPlayer)) + List(InventoryItemData(ObjectClass.avatar, player.GUID, 0, SeatConverter.MakeSeat(player, offset))) case None => Nil } } - - //TODO do not use for now; causes vehicle access permission issues; may not mesh with workflows; player GUID requirements -// private def MakeSeats(obj : Vehicle) : List[InventoryItemData.InventoryItem] = { -// var offset : Long = VehicleData.InitialStreamLengthToSeatEntries(obj.Velocity.nonEmpty, SpecificFormatModifier) -// obj.Seats -// .filter({ case (_, seat) => seat.isOccupied }) -// .map({case (index, seat) => -// val player = seat.Occupant.get -// val mountedPlayer = VehicleData.PlayerData( -// AvatarConverter.MakeAppearanceData(player), -// AvatarConverter.MakeCharacterData(player), -// AvatarConverter.MakeInventoryData(player), -// AvatarConverter.GetDrawnSlot(player), -// offset -// ) -// val entry = InventoryItemData(ObjectClass.avatar, player.GUID, index, mountedPlayer) -// //println(s"seat 0 offset: $offset, size: ${entry.bitsize}, pad: ${mountedPlayer.basic_appearance.NamePadding}") -// offset += entry.bitsize -// entry -// }).toList -// } private def MakeMountings(obj : Vehicle) : List[InventoryItemData.InventoryItem] = { obj.Weapons.map({ diff --git a/common/src/main/scala/net/psforever/objects/equipment/CItem.scala b/common/src/main/scala/net/psforever/objects/equipment/CItem.scala index 0582bfde..0989207a 100644 --- a/common/src/main/scala/net/psforever/objects/equipment/CItem.scala +++ b/common/src/main/scala/net/psforever/objects/equipment/CItem.scala @@ -1,28 +1,8 @@ // 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) - } +object CItem extends Enumeration { + final val ace = Value(32) + final val advanced_ace = Value(39) //fdu + final val router_telepad = Value(743) } diff --git a/common/src/main/scala/net/psforever/objects/equipment/RemoteUnit.scala b/common/src/main/scala/net/psforever/objects/equipment/RemoteUnit.scala new file mode 100644 index 00000000..1e49b64d --- /dev/null +++ b/common/src/main/scala/net/psforever/objects/equipment/RemoteUnit.scala @@ -0,0 +1,24 @@ +// Copyright (c) 2017 PSForever +package net.psforever.objects.equipment + +import net.psforever.packet.game.PlanetSideGUID + +trait RemoteUnit { + private var companion : Option[PlanetSideGUID] = None + + def Companion : Option[PlanetSideGUID] = companion + + def Companion_=(guid : PlanetSideGUID) : Option[PlanetSideGUID] = { + if(companion.isEmpty) { + companion = Some(guid) + } + Companion + } + + def Companion_=(guid : Option[Any]) : Option[PlanetSideGUID] = { + if(guid.isEmpty) { + companion = None + } + Companion + } +} diff --git a/common/src/main/scala/net/psforever/objects/guid/GUIDTask.scala b/common/src/main/scala/net/psforever/objects/guid/GUIDTask.scala index af5c2db4..dd104f5a 100644 --- a/common/src/main/scala/net/psforever/objects/guid/GUIDTask.scala +++ b/common/src/main/scala/net/psforever/objects/guid/GUIDTask.scala @@ -4,8 +4,9 @@ package net.psforever.objects.guid import akka.actor.ActorRef import net.psforever.objects.entity.IdentifiableEntity import net.psforever.objects.equipment.Equipment -import net.psforever.objects.{EquipmentSlot, LockerContainer, Player, Tool, Vehicle} +import net.psforever.objects._ import net.psforever.objects.inventory.Container +import net.psforever.objects.serverobject.turret.WeaponTurret import scala.annotation.tailrec @@ -191,6 +192,13 @@ object GUIDTask { TaskResolver.GiveTask(RegisterObjectTask(vehicle).task, weaponTasks ++ utilTasks ++ inventoryTasks) } + def RegisterDeployableTurret(obj : PlanetSideGameObject with WeaponTurret)(implicit guid : ActorRef) : TaskResolver.GiveTask = { + TaskResolver.GiveTask( + RegisterObjectTask(obj).task, + VisibleSlotTaskBuilding(obj.Weapons.values, GUIDTask.RegisterEquipment) ++ RegisterInventory(obj) + ) + } + /** * Construct tasking that unregisters an object from a globally unique identifier system.
*
@@ -329,6 +337,13 @@ object GUIDTask { TaskResolver.GiveTask(UnregisterObjectTask(vehicle).task, weaponTasks ++ utilTasks ++ inventoryTasks) } + def UnregisterDeployableTurret(obj : PlanetSideGameObject with WeaponTurret)(implicit guid : ActorRef) : TaskResolver.GiveTask = { + TaskResolver.GiveTask( + UnregisterObjectTask(obj).task, + VisibleSlotTaskBuilding(obj.Weapons.values, GUIDTask.UnregisterEquipment) ++ UnregisterInventory(obj) + ) + } + /** * Construct tasking that allocates work upon encountered `Equipment` objects * in reference to a globally unique identifier system of a pool of numbers. diff --git a/common/src/main/scala/net/psforever/objects/loadouts/Loadout.scala b/common/src/main/scala/net/psforever/objects/loadouts/Loadout.scala index 82cab3d4..5bcf4125 100644 --- a/common/src/main/scala/net/psforever/objects/loadouts/Loadout.scala +++ b/common/src/main/scala/net/psforever/objects/loadouts/Loadout.scala @@ -99,6 +99,12 @@ object Loadout { * @param definition the `ConstructionItemDefinition` that describes this future object */ final case class ShorthandConstructionItem(definition : ConstructionItemDefinition) extends Simplification + /** + * The simplified form of a `BoomerTrigger`, a unique kind of `SimpleItem`. + * @param definition the `SimpleItemDefinition` that describes this future object; + * actually ignored, but retained for function definition consistency + */ + final case class ShorthandTriggerItem(definition : SimpleItemDefinition) extends Simplification /** * The simplified form of a `SimpleItem`. * @param definition the `SimpleItemDefinition` that describes this future object @@ -223,6 +229,8 @@ object Loadout { ShorthandAmmoBox(obj.Definition, obj.Capacity) case obj : ConstructionItem => ShorthandConstructionItem(obj.Definition) + case obj : BoomerTrigger => + ShorthandTriggerItem(obj.Definition) case obj : SimpleItem => ShorthandSimpleItem(obj.Definition) case obj : Kit => diff --git a/common/src/main/scala/net/psforever/objects/serverobject/mount/MountableBehavior.scala b/common/src/main/scala/net/psforever/objects/serverobject/mount/MountableBehavior.scala index 33f8aa3f..4cca26e8 100644 --- a/common/src/main/scala/net/psforever/objects/serverobject/mount/MountableBehavior.scala +++ b/common/src/main/scala/net/psforever/objects/serverobject/mount/MountableBehavior.scala @@ -2,8 +2,10 @@ package net.psforever.objects.serverobject.mount import akka.actor.Actor +import net.psforever.objects.PlanetSideGameObject import net.psforever.objects.entity.{Identifiable, WorldEntity} import net.psforever.objects.serverobject.affinity.FactionAffinity +import net.psforever.objects.serverobject.turret.TurretDefinition import net.psforever.types.Vector3 object MountableBehavior { @@ -16,7 +18,7 @@ object MountableBehavior { trait Mount { this : Actor => - def MountableObject : Mountable with Identifiable with WorldEntity with FactionAffinity + def MountableObject : PlanetSideGameObject with Mountable with FactionAffinity val mountBehavior : Receive = { case Mountable.TryMount(user, seat_num) => @@ -34,6 +36,25 @@ object MountableBehavior { sender ! Mountable.MountMessages(user, Mountable.CanNotMount(obj, seat_num)) } } + + val turretMountBehavior : Receive = { + case Mountable.TryMount(user, seat_num) => + val obj = MountableObject + val definition = obj.Definition.asInstanceOf[TurretDefinition] + obj.Seat(seat_num) match { + case Some(seat) => + if((!definition.FactionLocked || user.Faction == obj.Faction) && + (seat.Occupant = user).contains(user)) { + user.VehicleSeated = obj.GUID + sender ! Mountable.MountMessages(user, Mountable.CanMount(obj, seat_num)) + } + else { + sender ! Mountable.MountMessages(user, Mountable.CanNotMount(obj, seat_num)) + } + case None => + sender ! Mountable.MountMessages(user, Mountable.CanNotMount(obj, seat_num)) + } + } } /** diff --git a/common/src/main/scala/net/psforever/objects/serverobject/terminals/EquipmentTerminalDefinition.scala b/common/src/main/scala/net/psforever/objects/serverobject/terminals/EquipmentTerminalDefinition.scala index 9067ae34..ceaf90c1 100644 --- a/common/src/main/scala/net/psforever/objects/serverobject/terminals/EquipmentTerminalDefinition.scala +++ b/common/src/main/scala/net/psforever/objects/serverobject/terminals/EquipmentTerminalDefinition.scala @@ -205,8 +205,8 @@ object EquipmentTerminalDefinition { "medicalapplicator" -> MakeTool(medicalapplicator), "bank" -> MakeTool(bank, armor_canister), "nano_dispenser" -> MakeTool(nano_dispenser), - //TODO "ace" -> MakeConstructionItem(ace), - //TODO "advanced_ace" -> MakeConstructionItem(advanced_ace), + "ace" -> MakeConstructionItem(ace), + "advanced_ace" -> MakeConstructionItem(advanced_ace), "remote_electronics_kit" -> MakeSimpleItem(remote_electronics_kit), "trek" -> MakeTool(trek), "command_detonater" -> MakeSimpleItem(command_detonater), @@ -309,6 +309,15 @@ object EquipmentTerminalDefinition { */ private def MakeKit(kdef : KitDefinition)() : Kit = Kit(kdef) + /** + * Create a new `BoomerTrigger`, a unique kind of `SimpleItem`. + * @param sdef the `SimpleItemDefinition` object; + * actually ignored, but retained for function definition consistency + * @return a curried function that, when called, creates the piece of `Equipment` + * @see `GlobalDefinitions` + */ + private def MakeTriggerItem(sdef : SimpleItemDefinition)() : SimpleItem = new BoomerTrigger + /** * Create a new `SimpleItem` from provided `EquipmentDefinition` objects. * @param sdef the `SimpleItemDefinition` object @@ -356,6 +365,9 @@ object EquipmentTerminalDefinition { case obj : ShorthandConstructionItem => MakeConstructionItem(obj.definition) + case obj : ShorthandTriggerItem => + MakeTriggerItem(obj.definition) + case obj : ShorthandSimpleItem => MakeSimpleItem(obj.definition) diff --git a/common/src/main/scala/net/psforever/objects/serverobject/turret/FacilityTurret.scala b/common/src/main/scala/net/psforever/objects/serverobject/turret/FacilityTurret.scala new file mode 100644 index 00000000..7cf7dc3b --- /dev/null +++ b/common/src/main/scala/net/psforever/objects/serverobject/turret/FacilityTurret.scala @@ -0,0 +1,56 @@ +// Copyright (c) 2017 PSForever +package net.psforever.objects.serverobject.turret + +import net.psforever.objects.serverobject.structures.Amenity + +class FacilityTurret(tDef : TurretDefinition) extends Amenity + with WeaponTurret { + /** some turrets can be updated; they all start without updates */ + private var upgradePath : TurretUpgrade.Value = TurretUpgrade.None + + WeaponTurret.LoadDefinition(this) + + def MaxHealth : Int = Definition.MaxHealth + + def MountPoints : Map[Int, Int] = Definition.MountPoints.toMap + + def Upgrade : TurretUpgrade.Value = upgradePath + + def Upgrade_=(upgrade : TurretUpgrade.Value) : TurretUpgrade.Value = { + upgradePath = upgrade + //upgrade each weapon as long as that weapon has a valid option for that upgrade + Definition.Weapons.foreach({ case(index, upgradePaths) => + if(upgradePaths.contains(upgrade)) { + weapons(index).Equipment.get.asInstanceOf[TurretWeapon].Upgrade = upgrade + } + }) + Upgrade + } + + def Definition : TurretDefinition = tDef +} + +object FacilityTurret { + /** + * Overloaded constructor. + * @param tDef the `ObjectDefinition` that constructs this object and maintains some of its immutable fields + * @return a `FacilityTurret` object + */ + def apply(tDef : TurretDefinition) : FacilityTurret = { + new FacilityTurret(tDef) + } + + import akka.actor.ActorContext + /** + * Instantiate and configure a `FacilityTurret` object + * @param id the unique id that will be assigned to this entity + * @param context a context to allow the object to properly set up `ActorSystem` functionality + * @return the `MannedTurret` object + */ + def Constructor(tdef : TurretDefinition)(id : Int, context : ActorContext) : FacilityTurret = { + import akka.actor.Props + val obj = FacilityTurret(tdef) + obj.Actor = context.actorOf(Props(classOf[FacilityTurretControl], obj), s"${tdef.Name}_$id") + obj + } +} diff --git a/common/src/main/scala/net/psforever/objects/serverobject/turret/MannedTurretControl.scala b/common/src/main/scala/net/psforever/objects/serverobject/turret/FacilityTurretControl.scala similarity index 95% rename from common/src/main/scala/net/psforever/objects/serverobject/turret/MannedTurretControl.scala rename to common/src/main/scala/net/psforever/objects/serverobject/turret/FacilityTurretControl.scala index 51f89f4e..557980a1 100644 --- a/common/src/main/scala/net/psforever/objects/serverobject/turret/MannedTurretControl.scala +++ b/common/src/main/scala/net/psforever/objects/serverobject/turret/FacilityTurretControl.scala @@ -13,7 +13,7 @@ import net.psforever.objects.serverobject.affinity.{FactionAffinity, FactionAffi * and faction-blind cavern sentry turrets. * @param turret the `MannedTurret` object being governed */ -class MannedTurretControl(turret : MannedTurret) extends Actor +class FacilityTurretControl(turret : FacilityTurret) extends Actor with FactionAffinityBehavior.Check with MountableBehavior.Dismount { def MountableObject = turret //do not add type! diff --git a/common/src/main/scala/net/psforever/objects/serverobject/turret/MannedTurret.scala b/common/src/main/scala/net/psforever/objects/serverobject/turret/MannedTurret.scala deleted file mode 100644 index 70624b4e..00000000 --- a/common/src/main/scala/net/psforever/objects/serverobject/turret/MannedTurret.scala +++ /dev/null @@ -1,224 +0,0 @@ -// Copyright (c) 2017 PSForever -package net.psforever.objects.serverobject.turret - -import net.psforever.objects.{EquipmentSlot, Player} -import net.psforever.objects.definition.SeatDefinition -import net.psforever.objects.equipment.Equipment -import net.psforever.objects.inventory.{Container, GridInventory} -import net.psforever.objects.serverobject.affinity.FactionAffinity -import net.psforever.objects.serverobject.mount.Mountable -import net.psforever.objects.serverobject.structures.Amenity -import net.psforever.objects.serverobject.turret.MannedTurret.MannedTurretWeapon -import net.psforever.objects.vehicles.{MountedWeapons, Seat => Chair} - -class MannedTurret(tDef : MannedTurretDefinition) extends Amenity - with FactionAffinity - with Mountable - with MountedWeapons - with Container { - private var health : Int = 1 - private var jammered : Boolean = false - /** manned turrets have just one seat; this is just standard interface */ - private val seats : Map[Int, Chair] = Map(0 -> Chair(new SeatDefinition() { ControlledWeapon = Some(1) })) - /** manned turrets have just one weapon; this is just standard interface */ - private var weapons : Map[Int, EquipmentSlot] = Map.empty - /** may or may not have inaccessible inventory space - * see `ReserveAmmunition` in the definition */ - private val inventory : GridInventory = new GridInventory() { - import net.psforever.packet.game.PlanetSideGUID - override def Remove(index : Int) : Boolean = false - override def Remove(guid : PlanetSideGUID) : Boolean = false - } - /** some turrets can be updated; they all start without updates */ - private var upgradePath : TurretUpgrade.Value = TurretUpgrade.None - - MannedTurret.LoadDefinition(this) - - def Health : Int = { - health - } - - def Health_=(toHealth : Int) : Int = { - health = toHealth - health - } - - def MaxHealth : Int = { - Definition.MaxHealth - } - - def Seats : Map[Int, Chair] = seats - - def Seat(seatNum : Int) : Option[Chair] = seats.get(seatNum) - - /** - * 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] = { - Definition.MountPoints.get(mountPoint) - } - - def MountPoints : Map[Int, Int] = Definition.MountPoints.toMap - - def PassengerInSeat(user : Player) : Option[Int] = { - if(seats(0).Occupant.contains(user)) { - Some(0) - } - else { - None - } - } - - def Weapons : Map[Int, EquipmentSlot] = weapons.filter({ case(index, _) => index < 2 }) - - def ControlledWeapon(wepNumber : Int) : Option[Equipment] = { - if(VisibleSlots.contains(wepNumber)) { - weapons(wepNumber).Equipment - } - else { - None - } - } - - def Inventory : GridInventory = inventory - - def VisibleSlots : Set[Int] = Set(1) - - def Upgrade : TurretUpgrade.Value = upgradePath - - def Upgrade_=(upgrade : TurretUpgrade.Value) : TurretUpgrade.Value = { - upgradePath = upgrade - //upgrade each weapon as long as that weapon has a valid option for that upgrade - Definition.Weapons.foreach({ case(index, upgradePaths) => - if(upgradePaths.contains(upgrade)) { - weapons(index).Equipment.get.asInstanceOf[MannedTurretWeapon].Upgrade = upgrade - } - }) - Upgrade - } - - def Jammered : Boolean = jammered - - def Jammered_=(jamState : Boolean) : Boolean = { - jammered = jamState - Jammered - } - - def Definition : MannedTurretDefinition = tDef -} - -object MannedTurret { - /** - * Overloaded constructor. - * @param tDef the `ObjectDefinition` that constructs this object and maintains some of its immutable fields - * @return a `MannedTurret` object - */ - def apply(tDef : MannedTurretDefinition) : MannedTurret = { - new MannedTurret(tDef) - } - - /** - * Use the `*Definition` that was provided to this object to initialize its fields and settings. - * @param turret the `MannedTurret` being initialized - * @see `{object}.LoadDefinition` - */ - def LoadDefinition(turret : MannedTurret) : MannedTurret = { - import net.psforever.objects.equipment.EquipmentSize.BaseTurretWeapon - val tdef : MannedTurretDefinition = turret.Definition - //general stuff - turret.Health = tdef.MaxHealth - //create weapons; note the class - turret.weapons = tdef.Weapons.map({case (num, upgradePaths) => - val slot = EquipmentSlot(BaseTurretWeapon) - slot.Equipment = new MannedTurretWeapon(tdef, upgradePaths.toMap) - num -> slot - }).toMap - //special inventory ammunition object(s) - if(tdef.ReserveAmmunition) { - val allAmmunitionTypes = tdef.Weapons.values.flatMap{ _.values.flatMap { _.AmmoTypes } }.toSet - if(allAmmunitionTypes.nonEmpty) { - turret.inventory.Resize(allAmmunitionTypes.size, 1) - var i : Int = 0 - allAmmunitionTypes.foreach(ammotype => { - turret.inventory.InsertQuickly(i, new TurretAmmoBox(ammotype)) - i += 1 - }) - } - } - turret - } - - import net.psforever.objects.definition.ToolDefinition - import net.psforever.objects.Tool - /** - * A stateful weapon that is mounted in `MannedTurrets` - * and may maintains a group of upgraded forms that can by swapped - * without reconstructing the weapon object itself or managing object registration. - * @param mdef the turret's definition - * @param udefs a map of turret upgrades to tool definitions that would be constructed by this weapon - * @param default the default upgrade state; - * defaults to `None` - */ - private class MannedTurretWeapon(mdef : MannedTurretDefinition, udefs : Map[TurretUpgrade.Value, ToolDefinition], default : TurretUpgrade.Value = TurretUpgrade.None) - extends Tool(udefs(default)) { - private var upgradePath : TurretUpgrade.Value = default - - def Upgrade : TurretUpgrade.Value = { - /* - Must check `not null` due to how this object's `Definition` will be called during `Tool`'s constructor - before the internal value can be set to default value `None` - */ - Option(upgradePath) match { - case Some(value) => - value - case None => - default - } - } - - def Upgrade_=(upgrade : TurretUpgrade.Value) : TurretUpgrade.Value = { - if(udefs.contains(upgrade)) { - val beforeUpgrade = upgradePath - upgradePath = upgrade - if(beforeUpgrade != upgradePath) { - Tool.LoadDefinition(this) //rebuild weapon internal structure - FireModeIndex = 0 //reset fire mode; this option is always valid - } - } - Upgrade - } - - override def Definition = udefs(Upgrade) - } - - import net.psforever.objects.definition.AmmoBoxDefinition - import net.psforever.objects.AmmoBox - - /** - * A special type of ammunition box contained within a `MannedTurret` for the purposes of infinite reloads. - * The original quantity of ammunition does not change. - * @param adef ammunition definition - */ - private class TurretAmmoBox(private val adef : AmmoBoxDefinition) extends AmmoBox(adef, Some(65535)) { - import net.psforever.objects.inventory.InventoryTile - override def Tile = InventoryTile.Tile11 - - override def Capacity_=(toCapacity : Int) = Capacity - } - - import akka.actor.ActorContext - /** - * Instantiate an configure a `MannedTurret` object - * @param id the unique id that will be assigned to this entity - * @param context a context to allow the object to properly set up `ActorSystem` functionality - * @return the `MannedTurret` object - */ - def Constructor(tdef : MannedTurretDefinition)(id : Int, context : ActorContext) : MannedTurret = { - import akka.actor.Props - val obj = MannedTurret(tdef) - obj.Actor = context.actorOf(Props(classOf[MannedTurretControl], obj), s"${tdef.Name}_$id") - obj - } -} diff --git a/common/src/main/scala/net/psforever/objects/serverobject/turret/MannedTurretDefinition.scala b/common/src/main/scala/net/psforever/objects/serverobject/turret/TurretDefinition.scala similarity index 94% rename from common/src/main/scala/net/psforever/objects/serverobject/turret/MannedTurretDefinition.scala rename to common/src/main/scala/net/psforever/objects/serverobject/turret/TurretDefinition.scala index 4c01105e..487f1320 100644 --- a/common/src/main/scala/net/psforever/objects/serverobject/turret/MannedTurretDefinition.scala +++ b/common/src/main/scala/net/psforever/objects/serverobject/turret/TurretDefinition.scala @@ -10,7 +10,7 @@ import scala.collection.mutable * The definition for any `MannedTurret`. * @param objectId the object's identifier number */ -class MannedTurretDefinition(private val objectId : Int) extends ObjectDefinition(objectId) { +class TurretDefinition(private val objectId : Int) extends ObjectDefinition(objectId) { Turrets(objectId) //let throw NoSuchElementException private var maxHealth : Int = 100 diff --git a/common/src/main/scala/net/psforever/objects/serverobject/turret/WeaponTurret.scala b/common/src/main/scala/net/psforever/objects/serverobject/turret/WeaponTurret.scala new file mode 100644 index 00000000..bdf777cb --- /dev/null +++ b/common/src/main/scala/net/psforever/objects/serverobject/turret/WeaponTurret.scala @@ -0,0 +1,177 @@ +// Copyright (c) 2017 PSForever +package net.psforever.objects.serverobject.turret + +import net.psforever.objects.definition.{AmmoBoxDefinition, SeatDefinition, ToolDefinition} +import net.psforever.objects._ +import net.psforever.objects.equipment.Equipment +import net.psforever.objects.inventory.{Container, GridInventory} +import net.psforever.objects.serverobject.affinity.FactionAffinity +import net.psforever.objects.serverobject.mount.Mountable +import net.psforever.objects.vehicles.{MountedWeapons, Seat => Chair} + +trait WeaponTurret extends FactionAffinity + with Mountable + with MountedWeapons + with Container { + this : PlanetSideGameObject => + + private var health : Int = 1 + private var jammered : Boolean = false + /** manned turrets have just one seat; this is just standard interface */ + protected val seats : Map[Int, Chair] = Map(0 -> Chair(new SeatDefinition() { ControlledWeapon = Some(1) })) + /** turrets have just one weapon; this is just standard interface */ + protected var weapons : Map[Int, EquipmentSlot] = Map.empty + /** may or may not have inaccessible inventory space + * see `ReserveAmmunition` in the definition */ + protected val inventory : GridInventory = new GridInventory() { + import net.psforever.packet.game.PlanetSideGUID + override def Remove(index : Int) : Boolean = false + override def Remove(guid : PlanetSideGUID) : Boolean = false + } + + def Health : Int = { + health + } + + def Health_=(toHealth : Int) : Int = { + health = toHealth + health + } + + def MaxHealth : Int + + def Inventory : GridInventory = inventory + + def VisibleSlots : Set[Int] = Set(1) + + def Weapons : Map[Int, EquipmentSlot] = weapons + + def MountPoints : Map[Int, Int] + + def Seats : Map[Int, Chair] = seats + + def Seat(seatNum : Int) : Option[Chair] = seats.get(seatNum) + + /** + * 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] = { + MountPoints.get(mountPoint) + } + + def PassengerInSeat(user : Player) : Option[Int] = { + if(seats(0).Occupant.contains(user)) { + Some(0) + } + else { + None + } + } + + def ControlledWeapon(wepNumber : Int) : Option[Equipment] = { + if(VisibleSlots.contains(wepNumber)) { + weapons(wepNumber).Equipment + } + else { + None + } + } + + def Jammered : Boolean = jammered + + def Jammered_=(jamState : Boolean) : Boolean = { + jammered = jamState + Jammered + } + + def Definition : TurretDefinition +} + +object WeaponTurret { + /** + * Use the `*Definition` that was provided to this object to initialize its fields and settings. + * @see `{object}.LoadDefinition` + * @param turret the `MannedTurret` being initialized + */ + def LoadDefinition(turret : WeaponTurret) : WeaponTurret = { + LoadDefinition(turret, turret.Definition) + } + + /** + * Use the `*Definition` that was provided to this object to initialize its fields and settings. + * A default definition is provided to be used. + * @see `{object}.LoadDefinition` + * @param turret the `MannedTurret` being initialized + * @param tdef the object definition + */ + def LoadDefinition(turret : WeaponTurret, tdef : TurretDefinition) : WeaponTurret = { + import net.psforever.objects.equipment.EquipmentSize.BaseTurretWeapon + //general stuff + turret.Health = tdef.MaxHealth + //create weapons; note the class + turret.weapons = tdef.Weapons.map({case (num, upgradePaths) => + val slot = EquipmentSlot(BaseTurretWeapon) + slot.Equipment = new TurretWeapon(tdef, upgradePaths.toMap) + num -> slot + }).toMap + //special inventory ammunition object(s) + if(tdef.ReserveAmmunition) { + val allAmmunitionTypes = tdef.Weapons.values.flatMap{ _.values.flatMap { _.AmmoTypes } }.toSet + if(allAmmunitionTypes.nonEmpty) { + turret.inventory.Resize(allAmmunitionTypes.size, 1) + var i : Int = 0 + allAmmunitionTypes.foreach(ammotype => { + turret.inventory.InsertQuickly(i, new TurretAmmoBox(ammotype)) + i += 1 + }) + } + } + turret + } +} + +class TurretWeapon(mdef : TurretDefinition, udefs : Map[TurretUpgrade.Value, ToolDefinition], default : TurretUpgrade.Value = TurretUpgrade.None) + extends Tool(udefs(default)) { + private var upgradePath : TurretUpgrade.Value = default + + def Upgrade : TurretUpgrade.Value = { + /* + Must check `not null` due to how this object's `Definition` will be called during `Tool`'s constructor + before the internal value can be set to default value `None` + */ + Option(upgradePath) match { + case Some(value) => + value + case None => + default + } + } + + def Upgrade_=(upgrade : TurretUpgrade.Value) : TurretUpgrade.Value = { + if(udefs.contains(upgrade)) { + val beforeUpgrade = upgradePath + upgradePath = upgrade + if(beforeUpgrade != upgradePath) { + Tool.LoadDefinition(this) //rebuild weapon internal structure + FireModeIndex = 0 //reset fire mode; this option is always valid + } + } + Upgrade + } + + override def Definition = udefs(Upgrade) +} + +/** + * A special type of ammunition box contained within a `MannedTurret` for the purposes of infinite reloads. + * The original quantity of ammunition does not change. + * @param adef ammunition definition + */ +class TurretAmmoBox(private val adef : AmmoBoxDefinition) extends AmmoBox(adef, Some(65535)) { + import net.psforever.objects.inventory.InventoryTile + override def Tile = InventoryTile.Tile11 + + override def Capacity_=(toCapacity : Int) = Capacity +} diff --git a/common/src/main/scala/net/psforever/objects/vehicles/Turrets.scala b/common/src/main/scala/net/psforever/objects/vehicles/Turrets.scala index 74293d46..66c7d8c4 100644 --- a/common/src/main/scala/net/psforever/objects/vehicles/Turrets.scala +++ b/common/src/main/scala/net/psforever/objects/vehicles/Turrets.scala @@ -2,9 +2,16 @@ package net.psforever.objects.vehicles /** - * An `Enumeration` of all the turret type objectss in the game, paired with their object id as the `Value`. + * An `Enumeration` of all the turret type objects in the game, paired with their object id as the `Value`. */ object Turrets extends Enumeration { - val manned_turret = Value(480) - val vanu_sentry_turret = Value(943) + final val manned_turret = Value(480) + 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 spitfire_aa = Value(819) + final val spitfire_cloaked = Value(825) + final val spitfire_turret = Value(826) + final val vanu_sentry_turret = Value(943) } diff --git a/common/src/main/scala/net/psforever/objects/vital/StandardDamages.scala b/common/src/main/scala/net/psforever/objects/vital/StandardDamages.scala index 9c7ebbae..740fe438 100644 --- a/common/src/main/scala/net/psforever/objects/vital/StandardDamages.scala +++ b/common/src/main/scala/net/psforever/objects/vital/StandardDamages.scala @@ -111,3 +111,9 @@ object StandardAircraftDamage extends DamageSelection { def Splash = AircraftSplashDamage.Calculate def Lash = AircraftLashDamage.Calculate } + +object StandardDeployableDamage extends DamageSelection { + def Direct = VehicleHitDamage.Calculate + def Splash = VehicleSplashDamage.Calculate + def Lash = NoDamage.Calculate +} diff --git a/common/src/main/scala/net/psforever/objects/vital/StandardResolutions.scala b/common/src/main/scala/net/psforever/objects/vital/StandardResolutions.scala index 6a25af3e..37a5d3a5 100644 --- a/common/src/main/scala/net/psforever/objects/vital/StandardResolutions.scala +++ b/common/src/main/scala/net/psforever/objects/vital/StandardResolutions.scala @@ -23,9 +23,21 @@ object VehicleResolutions extends DamageResistCalculations( ResolutionCalculations.VehicleApplication ) +object SimpleDeployableResolutions extends DamageResistCalculations( + ResolutionCalculations.VehicleDamageAfterResist, + ResolutionCalculations.SimpleDeployableApplication +) + +object ComplexDeployableResolutions extends DamageResistCalculations( + ResolutionCalculations.VehicleDamageAfterResist, + ResolutionCalculations.ComplexDeployableApplication +) + object StandardResolutions extends ResolutionSelection { def Infantry : ResolutionCalculations.Form = InfantryResolutions.Calculate def Max : ResolutionCalculations.Form = MaxResolutions.Calculate def Vehicle : ResolutionCalculations.Form = VehicleResolutions.Calculate def Aircraft : ResolutionCalculations.Form = VehicleResolutions.Calculate + def SimpleDeployables : ResolutionCalculations.Form = SimpleDeployableResolutions.Calculate + def ComplexDeployables : ResolutionCalculations.Form = ComplexDeployableResolutions.Calculate } diff --git a/common/src/main/scala/net/psforever/objects/vital/Vitality.scala b/common/src/main/scala/net/psforever/objects/vital/Vitality.scala index a9052212..f5945790 100644 --- a/common/src/main/scala/net/psforever/objects/vital/Vitality.scala +++ b/common/src/main/scala/net/psforever/objects/vital/Vitality.scala @@ -102,6 +102,8 @@ object Vitality { */ final case class Damage(func : (Any)=>Unit) + final case class DamageOn(obj : Vitality, func : (Any)=>Unit) + /** * Report that a vitals object must be updated due to damage. * @param obj the vital object diff --git a/common/src/main/scala/net/psforever/objects/vital/resolution/DamageResistCalculations.scala b/common/src/main/scala/net/psforever/objects/vital/resolution/DamageResistCalculations.scala index e46fb46a..4d434092 100644 --- a/common/src/main/scala/net/psforever/objects/vital/resolution/DamageResistCalculations.scala +++ b/common/src/main/scala/net/psforever/objects/vital/resolution/DamageResistCalculations.scala @@ -18,10 +18,23 @@ abstract class DamageResistCalculations[A](calcFunc : (ResolvedProjectile)=>((In applyFunc : (A, ResolvedProjectile)=>ResolutionCalculations.Output) extends ResolutionCalculations { def Calculate(damages : ProjectileCalculations.Form, resistances : ProjectileCalculations.Form, data : ResolvedProjectile) : ResolutionCalculations.Output = { + val modDam = Sample(damages, resistances, data) + applyFunc(modDam, data) + } + + /** + * An intermediate step of the normal `Calculate` operation that retrieves the damage values in their transitory form. + * @param damages the function that calculations raw damage values + * @param resistances the function that calculates resistance values + * @param data a historical projectile interaction; + * the origin of the data used to extract damage and resistance values + * @return the transitory form of the modified damage(s); + * usually, a single `Int` value or a tuple of `Int` values + */ + def Sample(damages : ProjectileCalculations.Form, resistances : ProjectileCalculations.Form, data : ResolvedProjectile) : A = { val dam : Int = damages(data) val res : Int = resistances(data) val mod = calcFunc(data) - val modDam = mod(dam, res) - applyFunc(modDam, data) + mod(dam, res) } } diff --git a/common/src/main/scala/net/psforever/objects/vital/resolution/ResolutionCalculations.scala b/common/src/main/scala/net/psforever/objects/vital/resolution/ResolutionCalculations.scala index 3b143ba0..4524c759 100644 --- a/common/src/main/scala/net/psforever/objects/vital/resolution/ResolutionCalculations.scala +++ b/common/src/main/scala/net/psforever/objects/vital/resolution/ResolutionCalculations.scala @@ -1,8 +1,9 @@ // Copyright (c) 2017 PSForever package net.psforever.objects.vital.resolution -import net.psforever.objects.{Player, Vehicle} +import net.psforever.objects.{Player, TurretDeployable, Vehicle} import net.psforever.objects.ballistics.{PlayerSource, ResolvedProjectile} +import net.psforever.objects.ce.{ComplexDeployable, SimpleDeployable} import net.psforever.objects.vital.projectile.ProjectileCalculations /** @@ -157,4 +158,49 @@ object ResolutionCalculations { } case _ => ; } + + def SimpleDeployableApplication(damage : Int, data : ResolvedProjectile)(target : Any) : Unit = target match { + case ce : SimpleDeployable => + if(ce.Health > 0) { + ce.Health -= damage + ce.History(data) + } + case _ => + } + + def ComplexDeployableApplication(damage : Int, data : ResolvedProjectile)(target : Any) : Unit = target match { + case ce : ComplexDeployable => + if(ce.Shields > 0) { + if(damage > ce.Shields) { + ce.Health -= (damage - ce.Shields) + ce.Shields = 0 + } + else { + ce.Shields -= damage + } + ce.History(data) + } + else if(ce.Health > 0) { + ce.Health -= damage + ce.History(data) + } + + case ce : TurretDeployable => + if(ce.Shields > 0) { + if(damage > ce.Shields) { + ce.Health -= (damage - ce.Shields) + ce.Shields = 0 + } + else { + ce.Shields -= damage + } + ce.History(data) + } + else if(ce.Health > 0) { + ce.Health -= damage + ce.History(data) + } + + case _ => ; + } } diff --git a/common/src/main/scala/net/psforever/objects/zones/Zone.scala b/common/src/main/scala/net/psforever/objects/zones/Zone.scala index b08c9c3d..1690ff21 100644 --- a/common/src/main/scala/net/psforever/objects/zones/Zone.scala +++ b/common/src/main/scala/net/psforever/objects/zones/Zone.scala @@ -5,14 +5,16 @@ import akka.actor.{ActorContext, ActorRef, Props} import akka.routing.RandomPool import net.psforever.objects.ballistics.Projectile import net.psforever.objects._ +import net.psforever.objects.ce.Deployable import net.psforever.objects.equipment.Equipment import net.psforever.objects.guid.NumberPoolHub import net.psforever.objects.guid.actor.UniqueNumberSystem import net.psforever.objects.guid.selector.RandomSelector import net.psforever.objects.guid.source.LimitedNumberSource +import net.psforever.objects.inventory.Container import net.psforever.objects.serverobject.structures.{Amenity, Building} import net.psforever.objects.serverobject.tube.SpawnTube -import net.psforever.objects.serverobject.turret.MannedTurret +import net.psforever.objects.serverobject.turret.FacilityTurret import net.psforever.packet.game.PlanetSideGUID import net.psforever.types.Vector3 @@ -54,6 +56,10 @@ class Zone(private val zoneId : String, zoneMap : ZoneMap, zoneNumber : Int) { /** Used by the `Zone` to coordinate `Equipment` dropping and collection requests. */ private var ground : ActorRef = ActorRef.noSender /** */ + private val constructions : ListBuffer[PlanetSideGameObject with Deployable] = ListBuffer[PlanetSideGameObject with Deployable]() + /** */ + private var deployables : ActorRef = ActorRef.noSender + /** */ private var transport : ActorRef = ActorRef.noSender /** */ private val players : TrieMap[Avatar, Option[Player]] = TrieMap[Avatar, Option[Player]]() @@ -91,6 +97,7 @@ class Zone(private val zoneId : String, zoneMap : ZoneMap, zoneNumber : Int) { SetupNumberPools() accessor = context.actorOf(RandomPool(25).props(Props(classOf[UniqueNumberSystem], guid, UniqueNumberSystem.AllocateNumberPoolActors(guid))), s"$Id-uns") ground = context.actorOf(Props(classOf[ZoneGroundActor], this, equipmentOnGround), s"$Id-ground") + deployables = context.actorOf(Props(classOf[ZoneDeployableActor], this, constructions), s"$Id-deployables") transport = context.actorOf(Props(classOf[ZoneVehicleActor], this, vehicles), s"$Id-vehicles") population = context.actorOf(Props(classOf[ZonePopulationActor], this, players, corpses), s"$Id-players") @@ -253,6 +260,8 @@ class Zone(private val zoneId : String, zoneMap : ZoneMap, zoneNumber : Int) { */ def EquipmentOnGround : List[Equipment] = equipmentOnGround.toList + def DeployableList : List[PlanetSideGameObject with Deployable] = constructions.toList + def Vehicles : List[Vehicle] = vehicles.toList def Players : List[Avatar] = players.keys.toList @@ -271,6 +280,8 @@ class Zone(private val zoneId : String, zoneMap : ZoneMap, zoneNumber : Int) { */ def Ground : ActorRef = ground + def Deployables : ActorRef = deployables + def Transport : ActorRef = transport def Population : ActorRef = population @@ -290,7 +301,7 @@ class Zone(private val zoneId : String, zoneMap : ZoneMap, zoneNumber : Int) { //turret to weapon Map.TurretToWeapon.foreach({ case ((turret_guid, weapon_guid)) => ((GUID(turret_guid) match { - case Some(obj : MannedTurret) => + case Some(obj : FacilityTurret) => Some(obj) case _ => ; None @@ -322,7 +333,11 @@ class Zone(private val zoneId : String, zoneMap : ZoneMap, zoneNumber : Int) { private def AssignAmenities() : Unit = { Map.ObjectToBuilding.foreach({ case(object_guid, building_id) => - buildings(building_id).Amenities = guid(object_guid).get.asInstanceOf[Amenity] + (buildings.get(building_id), guid(object_guid)) match { + case (Some(building), Some(amenity)) => + building.Amenities = amenity.asInstanceOf[Amenity] + case (None, _) | (_, None) => ; //let ZoneActor's sanity check catch this error + } }) } @@ -386,6 +401,17 @@ object Zone { /** Default value, non-zone area. */ final val Nowhere : Zone = new Zone("nowhere", new ZoneMap("nowhere"), 99) + /** + * Overloaded constructor. + * @param id the privileged name that can be used as the second parameter in the packet `LoadMapMessage` + * @param map the map of server objects upon which this `Zone` is based + * @param number the numerical index of the `Zone` as it is recognized in a variety of packets + * @return a `Zone` object + */ + def apply(id : String, map : ZoneMap, number : Int) : Zone = { + new Zone(id, map, number) + } + /** * Message to initialize the `Zone`. * @see `Zone.Init(implicit ActorContext)` @@ -494,6 +520,14 @@ object Zone { final case class RemoveItem(item_guid : PlanetSideGUID) } + object Deployable { + final case class Build(obj : PlanetSideGameObject with Deployable, withTool : ConstructionItem) + final case class DeployableIsBuilt(obj : PlanetSideGameObject with Deployable, withTool : ConstructionItem) + + final case class Dismiss(obj : PlanetSideGameObject with Deployable) + final case class DeployableIsDismissed(obj : PlanetSideGameObject with Deployable) + } + object Vehicle { final case class Spawn(vehicle : Vehicle) @@ -512,14 +546,71 @@ object Zone { */ final case class ClientInitialization(zone : Zone) - /** - * Overloaded constructor. - * @param id the privileged name that can be used as the second parameter in the packet `LoadMapMessage` - * @param map the map of server objects upon which this `Zone` is based - * @param number the numerical index of the `Zone` as it is recognized in a variety of packets - * @return a `Zone` object - */ - def apply(id : String, map : ZoneMap, number : Int) : Zone = { - new Zone(id, map, number) + object EquipmentIs { + /** + * Tha base `trait` connecting all `Equipment` object location tokens. + */ + sealed trait ItemLocation + /** + * The target item is contained within another object. + * @see `GridInventory`
+ * `Container` + * @param obj the containing object + * @param index the slot where the target is located + */ + final case class InContainer(obj : Container, index : Int) extends ItemLocation + /** + * The target item is found on the Ground. + * @see `ZoneGroundActor` + */ + final case class OnGround() extends ItemLocation + /** + * The target item exists but could not be found belonging to any expected region of the location. + */ + final case class Orphaned() extends ItemLocation + + /** + * An exhaustive search of the provided zone is conducted in search of the target `Equipment` object + * and a token that qualifies the current location of the object in the zone is returned. + * The following groups of objects are searched: + * the inventories of all players and all corpses, + * all vehicles trunks, + * the lockers of all players and corpses; + * and, if still not found, the ground is scoured too. + * @see `ItemLocation`
+ * `LockerContainer` + * @param equipment the target object + * @param guid that target object's globally unique identifier + * @param continent the zone whose objects to search + * @return a token that explains where the object is, if it is found in this zone; + * `None` is the token that is used to indicate not having been found + */ + def Where(equipment : Equipment, guid : PlanetSideGUID, continent : Zone) : Option[Zone.EquipmentIs.ItemLocation] = { + continent.GUID(guid) match { + case Some(_) => + ((continent.LivePlayers ++ continent.Corpses).find(_.Find(guid).nonEmpty) match { + case Some(tplayer) => Some((tplayer, tplayer.Find(guid))) + case _ => None + }).orElse(continent.Vehicles.find(_.Find(guid).nonEmpty) match { + case Some(vehicle) => Some((vehicle, vehicle.Find(guid))) + case _ => None + }).orElse(continent.Players.find(_.Locker.Find(guid).nonEmpty) match { + case Some(avatar) => Some((avatar.Locker, avatar.Locker.Find(guid))) + case _ => None + }) match { + case Some((obj, Some(index))) => + Some(Zone.EquipmentIs.InContainer(obj, index)) + case _ => + continent.EquipmentOnGround.find(_.GUID == guid) match { + case Some(_) => + Some(Zone.EquipmentIs.OnGround()) + case None => + Some(Zone.EquipmentIs.Orphaned()) + } + } + case None => + None + } + } } } diff --git a/common/src/main/scala/net/psforever/objects/zones/ZoneActor.scala b/common/src/main/scala/net/psforever/objects/zones/ZoneActor.scala index acd4c6f4..327eb6c2 100644 --- a/common/src/main/scala/net/psforever/objects/zones/ZoneActor.scala +++ b/common/src/main/scala/net/psforever/objects/zones/ZoneActor.scala @@ -56,6 +56,13 @@ class ZoneActor(zone : Zone) extends Actor { case msg @ Zone.Ground.PickupItem => zone.Ground forward msg + //frwd to Deployable Actor + case msg @ Zone.Deployable.Build => + zone.Deployables forward msg + + case msg @ Zone.Deployable.Dismiss => + zone.Deployables forward msg + //frwd to Vehicle Actor case msg @ Zone.Vehicle.Spawn => zone.Transport forward msg @@ -142,17 +149,21 @@ class ZoneActor(zone : Zone) extends Actor { val errors = new AtomicInteger(0) val validateObject : (Int, (PlanetSideGameObject)=>Boolean, String) => Boolean = ValidateObject(guid, slog, errors) - //check base to object associations - map.ObjectToBuilding.foreach({ case((object_guid, building_id)) => + //check bases + map.ObjectToBuilding.values.toSet[Int].foreach(building_id => if(zone.Building(building_id).isEmpty) { - slog.error(s"expected a building at id #$building_id") + slog.error(s"expected a building for id #$building_id") errors.incrementAndGet() } + ) + + //check base to object associations + map.ObjectToBuilding.keys.foreach(object_guid => if(guid(object_guid).isEmpty) { slog.error(s"expected object id $object_guid to exist, but it did not") errors.incrementAndGet() } - }) + ) //check door to lock association map.DoorToLock.foreach({ case((door_guid, lock_guid)) => @@ -174,8 +185,8 @@ class ZoneActor(zone : Zone) extends Actor { //check manned turret to weapon association map.TurretToWeapon.foreach({ case ((turret_guid, weapon_guid)) => - validateObject(turret_guid, MannedTurretCheck, "manned turret mount") - if(validateObject(weapon_guid, WeaponCheck, "manned turret weapon")) { + validateObject(turret_guid, FacilityTurretCheck, "facility turret mount") + if(validateObject(weapon_guid, WeaponCheck, "facility turret weapon")) { if(guid(weapon_guid).get.asInstanceOf[Tool].AmmoSlots.count(!_.Box.HasGUID) > 0) { slog.error(s"expected weapon $weapon_guid has an unregistered ammunition unit") errors.incrementAndGet() @@ -247,9 +258,9 @@ object ZoneActor { obj.isInstanceOf[VehicleSpawnPad] } - def MannedTurretCheck(obj : PlanetSideGameObject) : Boolean = { - import net.psforever.objects.serverobject.turret.MannedTurret - obj.isInstanceOf[MannedTurret] + def FacilityTurretCheck(obj : PlanetSideGameObject) : Boolean = { + import net.psforever.objects.serverobject.turret.FacilityTurret + obj.isInstanceOf[FacilityTurret] } def WeaponCheck(obj : PlanetSideGameObject) : Boolean = { diff --git a/common/src/main/scala/net/psforever/objects/zones/ZoneDeployableActor.scala b/common/src/main/scala/net/psforever/objects/zones/ZoneDeployableActor.scala new file mode 100644 index 00000000..593d1847 --- /dev/null +++ b/common/src/main/scala/net/psforever/objects/zones/ZoneDeployableActor.scala @@ -0,0 +1,204 @@ +// Copyright (c) 2017 PSForever +package net.psforever.objects.zones + +import akka.actor.Actor +import net.psforever.objects.ce.Deployable +import net.psforever.objects.serverobject.PlanetSideServerObject +import net.psforever.objects.PlanetSideGameObject + +import scala.annotation.tailrec +import scala.collection.mutable.ListBuffer + +/** + * na + * @param zone the `Zone` object + */ +class ZoneDeployableActor(zone : Zone, deployableList : ListBuffer[PlanetSideGameObject with Deployable]) extends Actor { + import ZoneDeployableActor._ + + def receive : Receive = { + case Zone.Deployable.Build(obj, tool) => + if(DeployableBuild(obj, deployableList)) { + obj match { + case o : PlanetSideServerObject => + obj.Definition.Initialize(o, context) + case _ => + obj.Definition.Initialize(obj, context) + } + sender ! Zone.Deployable.DeployableIsBuilt(obj, tool) + } + + case Zone.Deployable.Dismiss(obj) => + if(DeployableDismiss(obj, deployableList)) { + obj match { + case o : PlanetSideServerObject => + obj.Definition.Uninitialize(o, context) + case _ => + obj.Definition.Uninitialize(obj, context) + } + sender ! Zone.Deployable.DeployableIsDismissed(obj) + } + + case _ => ; + } +} + +object ZoneDeployableActor { + def DeployableBuild(obj : PlanetSideGameObject with Deployable, deployableList : ListBuffer[PlanetSideGameObject with Deployable]) : Boolean = { + deployableList.find(d => d == obj) match { + case Some(_) => + false + case None => + deployableList += obj + true + } + } + + + def DeployableDismiss(obj : PlanetSideGameObject with Deployable, deployableList : ListBuffer[PlanetSideGameObject with Deployable]) : Boolean = { + recursiveFindDeployable(deployableList.iterator, obj) match { + case None => + false + case Some(index) => + deployableList.remove(index) + true + } + } + + @tailrec final def recursiveFindDeployable(iter : Iterator[PlanetSideGameObject with Deployable], target : PlanetSideGameObject with Deployable, index : Int = 0) : Option[Int] = { + if(!iter.hasNext) { + None + } + else { + if(iter.next == target) { + Some(index) + } + else { + recursiveFindDeployable(iter, target, index + 1) + } + } + } + +// /** +// * Add an `avatar` as the key of an `Avatar` to `Player` object pair in the given collection. +// * @param avatar an `Avatar` object +// * @param playerMap the mapping of `Avatar` objects to `Player` objects +// * @return true, if the mapping is for a new key; +// * false, if the key already exists +// */ +// def PopulationJoin(avatar : Avatar, playerMap : TrieMap[Avatar, Option[Player]]) : Boolean = { +// playerMap.get(avatar) match { +// case Some(_) => +// false +// case None => +// playerMap += avatar -> None +// true +// } +// } +// /** +// * Remove an `avatar` from the key of an `Avatar` to `Player` object pair in the given collection. +// * If a `Player` object is associated at the time, return it safely. +// * @param avatar an `Avatar` object +// * @param playerMap the mapping of `Avatar` objects to `Player` objects +// * @return any `Player` object that was associated at the time the `avatar` was removed +// */ +// def PopulationLeave(avatar : Avatar, playerMap : TrieMap[Avatar, Option[Player]]) : Option[Player] = { +// playerMap.remove(avatar) match { +// case None => +// None +// case Some(tplayer) => +// tplayer +// } +// } +// +// /** +// * Associate a `Player` object as a value to an existing `Avatar` object that will be its key. +// * Do not overwrite players that are already associated. +// * @param avatar an `Avatar` object +// * @param player a `Player` object +// * @param playerMap the mapping of `Avatar` objects to `Player` objects +// * @return the `Player` object that is associated with the `Avatar` key +// */ +// def PopulationSpawn(avatar : Avatar, player : Player, playerMap : TrieMap[Avatar, Option[Player]]) : Option[Player] = { +// playerMap.get(avatar) match { +// case None => +// None +// case Some(tplayer) => +// tplayer match { +// case Some(aplayer) => +// Some(aplayer) +// case None => +// playerMap(avatar) = Some(player) +// Some(player) +// } +// } +// } +// +// /** +// * Disassociate a `Player` object from an existing `Avatar` object that was be its key. +// * @param avatar an `Avatar` object +// * @param playerMap the mapping of `Avatar` objects to `Player` objects +// * @return any `Player` object that is associated at the time +// */ +// def PopulationRelease(avatar : Avatar, playerMap : TrieMap[Avatar, Option[Player]]) : Option[Player] = { +// playerMap.get(avatar) match { +// case None => +// None +// case Some(tplayer) => +// playerMap(avatar) = None +// tplayer +// } +// } +// +// /** +// * If the given `player` passes a condition check, add it to the list. +// * @param player a `Player` object +// * @param corpseList a list of `Player` objects +// * @return true, if the `player` was added to the list; +// * false, otherwise +// */ +// def CorpseAdd(player : Player, corpseList : ListBuffer[Player]) : Boolean = { +// if(player.isBackpack) { +// corpseList += player +// true +// } +// else { +// false +// } +// } +// +// /** +// * Remove the given `player` from the list. +// * @param player a `Player` object +// * @param corpseList a list of `Player` objects +// */ +// def CorpseRemove(player : Player, corpseList : ListBuffer[Player]) : Unit = { +// recursiveFindCorpse(corpseList.iterator, player) match { +// case None => ; +// case Some(index) => +// corpseList.remove(index) +// } +// } +// +// /** +// * A recursive function that finds and removes a specific player from a list of players. +// * @param iter an `Iterator` of `Player` objects +// * @param player the target `Player` +// * @param index the index of the discovered `Player` object +// * @return the index of the `Player` object in the list to be removed; +// * `None`, otherwise +// */ +// @tailrec final def recursiveFindCorpse(iter : Iterator[Player], player : Player, index : Int = 0) : Option[Int] = { +// if(!iter.hasNext) { +// None +// } +// else { +// if(iter.next == player) { +// Some(index) +// } +// else { +// recursiveFindCorpse(iter, player, index + 1) +// } +// } +// } +} diff --git a/common/src/main/scala/net/psforever/packet/game/DeployObjectMessage.scala b/common/src/main/scala/net/psforever/packet/game/DeployObjectMessage.scala index f29219cd..caaf749e 100644 --- a/common/src/main/scala/net/psforever/packet/game/DeployObjectMessage.scala +++ b/common/src/main/scala/net/psforever/packet/game/DeployObjectMessage.scala @@ -2,9 +2,10 @@ package net.psforever.packet.game import net.psforever.packet.{GamePacketOpcode, Marshallable, PlanetSideGamePacket} -import net.psforever.types.Vector3 +import net.psforever.types.{Angular, Vector3} import scodec.Codec import scodec.codecs._ +import shapeless.{::, HNil} /** * Dispatched from the client to request that an object be deployed.
@@ -15,17 +16,13 @@ import scodec.codecs._ * @param object_guid the object * @param unk1 na * @param pos the location where the object is to be deployed - * @param roll the amount of roll that affects orientation - * @param pitch the amount of pitch that affects orientation - * @param yaw the amount of yaw that affects orientation + * @param orient the angle of orientation * @param unk2 na */ final case class DeployObjectMessage(object_guid : PlanetSideGUID, unk1 : Long, pos : Vector3, - roll : Int, - pitch : Int, - yaw : Int, + orient : Vector3, unk2 : Long) extends PlanetSideGamePacket { type Packet = DeployObjectMessage @@ -38,9 +35,19 @@ object DeployObjectMessage extends Marshallable[DeployObjectMessage] { ("object_guid" | PlanetSideGUID.codec) :: ("unk1" | uint32L) :: ("pos" | Vector3.codec_pos) :: - ("roll" | uint8L) :: - ("pitch" | uint8L) :: - ("yaw" | uint8L) :: + (("roll" | Angular.codec_roll) :: + ("pitch" | Angular.codec_pitch) :: + ("yaw" | Angular.codec_yaw()) + ).xmap[Vector3] ( + { + case x :: y :: z :: HNil => + Vector3(x, y, z) + }, + { + case Vector3(x, y, z) => + x :: y :: z :: HNil + } + ) :: ("unk2" | uint32L) ).as[DeployObjectMessage] } diff --git a/common/src/main/scala/net/psforever/packet/game/ObjectDeployedMessage.scala b/common/src/main/scala/net/psforever/packet/game/ObjectDeployedMessage.scala index 9b6e237a..7e9dec6a 100644 --- a/common/src/main/scala/net/psforever/packet/game/ObjectDeployedMessage.scala +++ b/common/src/main/scala/net/psforever/packet/game/ObjectDeployedMessage.scala @@ -9,7 +9,7 @@ import shapeless.{::, HNil} /** * An `Enumeration` for the forms of the event chat message produced by this packet. */ -object DeploymentOutcome extends Enumeration(1) { +object DeployOutcome extends Enumeration(1) { type Type = Value val Failure = Value(2) @@ -47,7 +47,7 @@ object DeploymentOutcome extends Enumeration(1) { */ final case class ObjectDeployedMessage(unk : Int, desc : String, - action : DeploymentOutcome.Value, + action : DeployOutcome.Value, count : Long, max : Long) extends PlanetSideGamePacket { @@ -65,13 +65,31 @@ object ObjectDeployedMessage extends Marshallable[ObjectDeployedMessage] { * @param max the maximum number of this type of object that can be deployed * @return an `ObjectDeployedMessage` object */ - def apply(desc : String, action : DeploymentOutcome.Value, count : Long, max : Long) : ObjectDeployedMessage = + def apply(desc : String, action : DeployOutcome.Value, count : Long, max : Long) : ObjectDeployedMessage = new ObjectDeployedMessage(0, desc, action, count, max) + /** + * na + * @param desc descriptive text of what kind of object is being deployed + * @param count the number of this type of object deployed + * @param max the maximum number of this type of object that can be deployed + * @return an `ObjectDeployedMessage` object + */ + def Success(desc : String, count : Int, max : Int) : ObjectDeployedMessage = + new ObjectDeployedMessage(0, desc, DeployOutcome.Success, count, max) + + /** + * na + * @param desc descriptive text of what kind of object failed to be deployed + * @return an `ObjectDeployedMessage` object + */ + def Failure(desc : String) : ObjectDeployedMessage = + new ObjectDeployedMessage(0, desc, DeployOutcome.Failure, 0, 0) + implicit val codec : Codec[ObjectDeployedMessage] = ( ("unk" | uint16L) :: ("desc" | PacketHelpers.encodedString) :: - ("action" | DeploymentOutcome.codec) :: + ("action" | DeployOutcome.codec) :: ("count" | uint32L) :: ("max" | uint32L) ).xmap[ObjectDeployedMessage] ( 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 c4b6640e..5288d97b 100644 --- a/common/src/main/scala/net/psforever/packet/game/PlanetsideAttributeMessage.scala +++ b/common/src/main/scala/net/psforever/packet/game/PlanetsideAttributeMessage.scala @@ -13,7 +13,8 @@ import scodec.codecs._ * `67 - ???`
*
* Global (GUID=0)
- * `82 - ???` + * `75 - Russian client region check` (value checks with bitmask `& 8`)
+ * `82 - ???`
* `83 - max boomers`
* `84 - max he mines`
* `85 - max disruptor mines`
@@ -114,10 +115,10 @@ import scodec.codecs._ * `36 - CR. Value is the CR`
* `43 - Info on avatar name : 0 = Nothing, 1 = "(LD)" message`
* `45 - NTU charge bar 0-10, 5 = 50% full. Seems to apply to both ANT and NTU Silo (possibly siphons?)`
- * 47 - Sets base NTU level to CRITICAL. MUST use base modelId not base GUID - * 48 - Set to 1 to send base power loss message & turns on red warning lights throughout base. MUST use base modelId not base GUID - * 49 - Vehicle texture effects state? (>0 turns on ANT panel glow or ntu silo panel glow + orbs) (bit?) - * `52 - Vehicle particle effects? (>0 turns on orbs going towards ANT. Doesn't affect silo) (bit?) + * `47 - Sets base NTU level to CRITICAL. MUST use base modelId not base GUID`
+ * `48 - Set to 1 to send base power loss message & turns on red warning lights throughout base. MUST use base modelId not base GUID`
+ * `49 - Vehicle texture effects state? (>0 turns on ANT panel glow or ntu silo panel glow + orbs) (bit?)`
+ * `52 - Vehicle particle effects? (>0 turns on orbs going towards ANT. Doesn't affect silo) (bit?)`
* `53 - LFS. Value is 1 to flag LFS`
* `54 - Player "Aura". Values can be expressed in the first byte's lower nibble:`
* - 0 is nothing
@@ -151,7 +152,7 @@ import scodec.codecs._ * `79 - ???`
* `80 - Damage vehicle (unknown value)`
* `81 - ???`
- * `113 - `Vehicle capacitor - e.g. Leviathan EMP charge` + * `113 - Vehicle capacitor - e.g. Leviathan EMP charge` * * @param player_guid the player * @param attribute_type na diff --git a/common/src/main/scala/net/psforever/packet/game/TriggerEffectMessage.scala b/common/src/main/scala/net/psforever/packet/game/TriggerEffectMessage.scala index a8c2cc9b..3fe3a6f2 100644 --- a/common/src/main/scala/net/psforever/packet/game/TriggerEffectMessage.scala +++ b/common/src/main/scala/net/psforever/packet/game/TriggerEffectMessage.scala @@ -2,9 +2,10 @@ package net.psforever.packet.game import net.psforever.packet.{GamePacketOpcode, Marshallable, PacketHelpers, PlanetSideGamePacket} -import net.psforever.types.Vector3 +import net.psforever.types.{Angular, Vector3} import scodec.Codec import scodec.codecs._ +import shapeless.{::, HNil} /** * na @@ -19,14 +20,11 @@ final case class TriggeredEffect(unk1 : Boolean, * Activate an effect that is not directly associated with an existing game object. * Without a game object from which to inherit position and orientation, those explicit parameters must be provided. * @param pos the position in the game world - * @param roll the amount of roll that affects orientation - * @param pitch the amount of pitch that affects orientation - * @param yaw the amount of yaw that affects orientation + * @param orient the angle of orientation */ +//TODO must find proper North-corrective angle for orientation final case class TriggeredEffectLocation(pos : Vector3, - roll : Int, - pitch : Int, - yaw : Int) + orient : Vector3) /** * Dispatched by the server to cause a client to display a special graphical effect.
@@ -38,14 +36,14 @@ final case class TriggeredEffectLocation(pos : Vector3, * For example, the effect "on" will only work on objects that accept "on" normally, like a deployed `motionalarmsensor`. * The effect "spawn_object_effect" can be applied anywhere in the environment; * but, it can not be activated in conjunction with an existing object. - * @param obj an object that accepts the effect + * @param object_guid an object that accepts the effect * @param effect the name of the effect * @param unk na; * when activating an effect on an existing object * @param location an optional position where the effect will be displayed; * when activating an effect independently */ -final case class TriggerEffectMessage(obj : PlanetSideGUID, +final case class TriggerEffectMessage(object_guid : PlanetSideGUID, effect : String, unk : Option[TriggeredEffect] = None, location : Option[TriggeredEffectLocation] = None @@ -56,6 +54,15 @@ final case class TriggerEffectMessage(obj : PlanetSideGUID, } object TriggerEffectMessage extends Marshallable[TriggerEffectMessage] { + def apply(object_guid : PlanetSideGUID, effect : String) : TriggerEffectMessage = + TriggerEffectMessage(object_guid, effect, None, None) + + def apply(object_guid : PlanetSideGUID, effect : String, unk1 : Boolean, unk2 : Long) : TriggerEffectMessage = + TriggerEffectMessage(object_guid, effect, Some(TriggeredEffect(unk1, unk2)), None) + + def apply(effect : String, position : Vector3, orientation : Vector3) : TriggerEffectMessage = + TriggerEffectMessage(PlanetSideGUID(0), effect, None, Some(TriggeredEffectLocation(position, orientation))) + /** * A `Codec` for `TriggeredEffect` data. */ @@ -69,15 +76,24 @@ object TriggerEffectMessage extends Marshallable[TriggerEffectMessage] { */ private val effect_location_codec : Codec[TriggeredEffectLocation] = ( ("pos" | Vector3.codec_pos) :: - ("roll" | uint8L) :: - ("pitch" | uint8L) :: - ("yaw" | uint8L) + (("roll" | Angular.codec_roll) :: + ("pitch" | Angular.codec_pitch) :: + ("yaw" | Angular.codec_yaw())).xmap[Vector3] ( + { + case x :: y :: z :: HNil => + Vector3(x, y, z) + }, + { + case Vector3(x, y, z) => + x :: y :: z :: HNil + } + ) ).as[TriggeredEffectLocation] implicit val codec : Codec[TriggerEffectMessage] = ( - ("obj" | PlanetSideGUID.codec) >>:~ { obj => + ("object_guid" | PlanetSideGUID.codec) >>:~ { guid => ("effect" | PacketHelpers.encodedString) :: optional(bool, "unk" | effect_codec) :: - conditional(obj.guid == 0, "location" | effect_location_codec) + conditional(guid.guid == 0, "location" | effect_location_codec) }).as[TriggerEffectMessage] } diff --git a/common/src/main/scala/net/psforever/packet/game/objectcreate/LargeDeployableData.scala b/common/src/main/scala/net/psforever/packet/game/objectcreate/LargeDeployableData.scala new file mode 100644 index 00000000..e2a63545 --- /dev/null +++ b/common/src/main/scala/net/psforever/packet/game/objectcreate/LargeDeployableData.scala @@ -0,0 +1,71 @@ +// Copyright (c) 2017 PSForever +package net.psforever.packet.game.objectcreate + +import net.psforever.packet.Marshallable +import scodec.codecs._ +import scodec.{Attempt, Codec, Err} +import shapeless.{::, HNil} + +/** + * A representation of the Spitfire-based small turrets deployed using an adaptive construction engine.
+ *
+ * The turret may contain substructure defining a weapon is a turret weapon contained within the turret itself. + * Furthermore, that turret-like weapon is loaded with turret-like ammunition. + * In other words, this outer turret can be considered a weapons platform for the inner turret weapon.
+ *
+ * If the turret has no `health`, it is rendered as destroyed. + * If the turret has no internal weapon, it is safest rendered as destroyed. + * @param deploy data common to objects spawned by the (advanced) adaptive construction engine + * @param health the amount of health the object has, as a percentage of a filled bar + * @param internals data regarding the mounted weapon + */ +final case class LargeDeployableData(deploy : SmallDeployableData, + health : Int, + internals : Option[InventoryData] = None + ) extends ConstructorData { + override def bitsize : Long = { + val deploySize = deploy.bitsize + val internalSize = internals match { + case Some(inv) => + inv.bitsize + case None => + 0 + } + 22L + deploySize + internalSize //8u + 7u + 4u + 2u + 1u + } +} + +object LargeDeployableData extends Marshallable[LargeDeployableData] { + implicit val codec : Codec[LargeDeployableData] = ( + ("deploy" | SmallDeployableData.codec) :: + ("health" | uint8L) :: + uintL(7) :: + uint4L :: + uint2L :: + optional(bool, "internals" | InventoryData.codec) + ).exmap[LargeDeployableData] ( + { + case deploy :: health :: 0 :: 0xF :: 0 :: internals :: HNil => + val (newHealth, newInternals) = if(health == 0 || internals.isEmpty || internals.get.contents.isEmpty) { + (0, None) + } + else { + (health, internals) + } + Attempt.successful(LargeDeployableData(deploy, newHealth, newInternals)) + + case _ => + Attempt.failure(Err("invalid large deployable data format")) + }, + { + case LargeDeployableData(deploy, health, internals) => + val (newHealth, newInternals) = if(health == 0 || internals.isEmpty || internals.get.contents.isEmpty) { + (0, None) + } + else { + (health, internals) + } + Attempt.successful(deploy :: newHealth :: 0 :: 0xF :: 0 :: newInternals :: HNil) + } + ) +} 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 11e92559..5f32d07f 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 @@ -955,7 +955,7 @@ object ObjectClass { case ObjectClass.trek => ConstructorData.genericCodec(WeaponData.codec, "tool") //ace deployables case ObjectClass.ace => ConstructorData.genericCodec(ACEData.codec, "ace") - case ObjectClass.advanced_ace => ConstructorData.genericCodec(CommandDetonaterData.codec, "advanced ace") + case ObjectClass.advanced_ace => ConstructorData.genericCodec(ACEData.codec, "advanced ace") case ObjectClass.boomer_trigger => ConstructorData.genericCodec(BoomerTriggerData.codec, "boomer trigger") //vehicles? case ObjectClass.orbital_shuttle => ConstructorData.genericCodec(OrbitalShuttleData.codec, "HART") @@ -1186,7 +1186,7 @@ object ObjectClass { case ObjectClass.trek => DroppedItemData.genericCodec(WeaponData.codec, "tool") //ace deployables case ObjectClass.ace => DroppedItemData.genericCodec(ACEData.codec, "ace") - case ObjectClass.advanced_ace => DroppedItemData.genericCodec(CommandDetonaterData.codec, "advanced ace") //todo temporary? + case ObjectClass.advanced_ace => DroppedItemData.genericCodec(ACEData.codec, "advanced ace") //todo temporary? case ObjectClass.boomer_trigger => DroppedItemData.genericCodec(BoomerTriggerData.codec, "boomer trigger") case ObjectClass.boomer => ConstructorData.genericCodec(SmallDeployableData.codec, "ace deployable") case ObjectClass.he_mine => ConstructorData.genericCodec(SmallDeployableData.codec, "ace deployable") diff --git a/common/src/main/scala/net/psforever/packet/game/objectcreate/ObjectCreateBase.scala b/common/src/main/scala/net/psforever/packet/game/objectcreate/ObjectCreateBase.scala index b9c15dfe..71b77fa8 100644 --- a/common/src/main/scala/net/psforever/packet/game/objectcreate/ObjectCreateBase.scala +++ b/common/src/main/scala/net/psforever/packet/game/objectcreate/ObjectCreateBase.scala @@ -97,7 +97,7 @@ object ObjectCreateBase { } catch { case ex : Exception => - log.error(s"${ex.getClass.toString} - ${ex.toString}") + log.error(s"${ex.getClass.toString} - ${ex.toString} ($objectClass)") } out } @@ -125,7 +125,7 @@ object ObjectCreateBase { } catch { case ex : Exception => - log.error(s"${ex.getClass.toString} - ${ex.toString}") + log.error(s"${ex.getClass.toString} - ${ex.toString} ($objectClass)") } out } diff --git a/common/src/main/scala/net/psforever/packet/game/objectcreate/OneMannedFieldTurretData.scala b/common/src/main/scala/net/psforever/packet/game/objectcreate/OneMannedFieldTurretData.scala index 5708fd88..4b9c1586 100644 --- a/common/src/main/scala/net/psforever/packet/game/objectcreate/OneMannedFieldTurretData.scala +++ b/common/src/main/scala/net/psforever/packet/game/objectcreate/OneMannedFieldTurretData.scala @@ -21,14 +21,19 @@ import shapeless.{::, HNil} * @param health the amount of health the object has, as a percentage of a filled bar * @param internals data regarding the mountable weapon */ -final case class OneMannedFieldTurretData(deploy : CommonFieldData, +final case class OneMannedFieldTurretData(deploy : SmallDeployableData, health : Int, - internals : Option[InternalSlot] = None + internals : Option[InventoryData] = None ) extends ConstructorData { override def bitsize : Long = { val deploySize = deploy.bitsize - val internalSize = if(internals.isDefined) { CommonFieldData.internalWeapon_bitsize + internals.get.bitsize } else { 0L } - 38L + deploySize + internalSize //16u + 8u + 8u + 2u + 4u + val internalSize = internals match { + case Some(inv) => + inv.bitsize + case None => + 0 + } + 37L + deploySize + internalSize //16u + 1u + 8u + 5u + 4u + 2u + 1u } } @@ -40,7 +45,7 @@ object OneMannedFieldTurretData extends Marshallable[OneMannedFieldTurretData] { * @param internals data regarding the mountable weapon * @return a `OneMannedFieldTurretData` object */ - def apply(deploy : CommonFieldData, health : Int, internals : InternalSlot) : OneMannedFieldTurretData = + def apply(deploy : SmallDeployableData, health : Int, internals : InventoryData) : OneMannedFieldTurretData = new OneMannedFieldTurretData(deploy, health, Some(internals)) /** @@ -122,40 +127,44 @@ object OneMannedFieldTurretData extends Marshallable[OneMannedFieldTurretData] { ) implicit val codec : Codec[OneMannedFieldTurretData] = ( - ("deploy" | CommonFieldData.codec) :: - bool :: - PlanetSideGUID.codec :: //hoist/extract with the CommonFieldData above + ("deploy" | SmallDeployableData.codec) :: + PlanetSideGUID.codec :: //hoist/extract with the deploy.owner_guid in field above bool :: ("health" | uint8L) :: - uint2L :: - uint8L :: - bool :: - optional(bool, "internals" | CommonFieldData.internalWeaponCodec) + uint(5) :: + uint4 :: + uint2 :: + optional(bool, "internals" | InventoryData.codec) ).exmap[OneMannedFieldTurretData] ( { - case deploy :: false :: player :: false :: health :: 0 :: 0x1E :: false :: internals :: HNil => - var newHealth : Int = health - var newInternals : Option[InternalSlot] = internals - if(health == 0 || internals.isEmpty) { - newHealth = 0 - newInternals = None + case deploy :: player :: false :: health :: 0 :: 0xF :: 0 :: internals :: HNil => + val (newHealth, newInternals) = if(health == 0 || internals.isEmpty || internals.get.contents.isEmpty) { + (0, None) } - val newDeploy = CommonFieldData(deploy.pos, deploy.faction, deploy.unk, player) - Attempt.successful(OneMannedFieldTurretData(newDeploy, newHealth, newInternals)) + else { + (health, internals) + } + Attempt.successful( + OneMannedFieldTurretData( + SmallDeployableData(deploy.pos, deploy.faction, deploy.bops, deploy.destroyed, deploy.unk1, deploy.jammered, deploy.unk2, player), + newHealth, + newInternals + ) + ) case _ => Attempt.failure(Err("invalid omft data format")) }, { case OneMannedFieldTurretData(deploy, health, internals) => - var newHealth : Int = health - var newInternals : Option[InternalSlot] = internals - if(health == 0 || internals.isEmpty) { - newHealth = 0 - newInternals = None + val (newHealth, newInternals) = if(health == 0 || internals.isEmpty || internals.get.contents.isEmpty) { + (0, None) } - val newDeploy = CommonFieldData(deploy.pos, deploy.faction, deploy.unk) - Attempt.successful(newDeploy :: false :: deploy.player_guid :: false :: newHealth :: 0 :: 0x1E :: false :: newInternals :: HNil) + else { + (health, internals) + } + val newDeploy = SmallDeployableData(deploy.pos, deploy.faction, deploy.bops, deploy.destroyed, deploy.unk1, deploy.jammered, deploy.unk2, PlanetSideGUID(0)) + Attempt.successful(newDeploy :: deploy.owner_guid :: false :: newHealth :: 0 :: 0xF :: 0 :: newInternals :: HNil) case _ => Attempt.failure(Err("invalid omft data format")) diff --git a/common/src/main/scala/net/psforever/packet/game/objectcreate/SmallDeployableData.scala b/common/src/main/scala/net/psforever/packet/game/objectcreate/SmallDeployableData.scala index 14432434..c6ac9cd2 100644 --- a/common/src/main/scala/net/psforever/packet/game/objectcreate/SmallDeployableData.scala +++ b/common/src/main/scala/net/psforever/packet/game/objectcreate/SmallDeployableData.scala @@ -2,33 +2,42 @@ package net.psforever.packet.game.objectcreate import net.psforever.packet.Marshallable -import scodec.{Attempt, Codec, Err} +import net.psforever.packet.game.PlanetSideGUID +import net.psforever.types.PlanetSideEmpire +import scodec.Codec import scodec.codecs._ -import shapeless.{::, HNil} /** * A representation of simple objects that are spawned by the adaptive construction engine. - * @param deploy data common to objects spawned by the (advanced) adaptive construction engine + * //@param deploy data common to objects spawned by the (advanced) adaptive construction engine */ -final case class SmallDeployableData(deploy : CommonFieldData) extends ConstructorData { - override def bitsize : Long = deploy.bitsize + 1L +final case class SmallDeployableData(pos : PlacementData, + faction : PlanetSideEmpire.Value, + bops : Boolean, + destroyed : Boolean, + unk1 : Int, + jammered : Boolean, + unk2 : Boolean, + owner_guid : PlanetSideGUID) extends ConstructorData { + override def bitsize : Long = { + val posSize = pos.bitsize + 24 + posSize + } } object SmallDeployableData extends Marshallable[SmallDeployableData] { - implicit val codec : Codec[SmallDeployableData] = ( - ("deploy" | CommonFieldData.codec) :: - bool - ).exmap[SmallDeployableData] ( - { - case deploy :: false :: HNil => - Attempt.successful(SmallDeployableData(deploy)) + def apply(pos : PlacementData, faction : PlanetSideEmpire.Value, unk1 : Int, jammered : Boolean, unk2 : Boolean) : SmallDeployableData = { + SmallDeployableData(pos, faction, false, false, unk1, jammered, unk2, PlanetSideGUID(0)) + } - case _ => - Attempt.failure(Err("invalid small deployable data format")) - }, - { - case SmallDeployableData(deploy) => - Attempt.successful(deploy :: false :: HNil) - } - ) + implicit val codec : Codec[SmallDeployableData] = ( + ("pos" | PlacementData.codec) :: + ("faction" | PlanetSideEmpire.codec) :: + ("bops" | bool) :: + ("destroyed" | bool) :: + ("unk1" | uint2L) :: //3 - na, 2 - common, 1 - na, 0 - common? + ("jammered" | bool) :: + ("unk2" | bool) :: + ("owner_guid" | PlanetSideGUID.codec) + ).as[SmallDeployableData] } diff --git a/common/src/main/scala/net/psforever/packet/game/objectcreate/SmallTurretData.scala b/common/src/main/scala/net/psforever/packet/game/objectcreate/SmallTurretData.scala index 4edd4800..464e2e02 100644 --- a/common/src/main/scala/net/psforever/packet/game/objectcreate/SmallTurretData.scala +++ b/common/src/main/scala/net/psforever/packet/game/objectcreate/SmallTurretData.scala @@ -20,14 +20,19 @@ import shapeless.{::, HNil} * @param health the amount of health the object has, as a percentage of a filled bar * @param internals data regarding the mounted weapon */ -final case class SmallTurretData(deploy : CommonFieldData, +final case class SmallTurretData(deploy : SmallDeployableData, health : Int, - internals : Option[InternalSlot] = None + internals : Option[InventoryData] = None ) extends ConstructorData { override def bitsize : Long = { val deploySize = deploy.bitsize - val internalSize = if(internals.isDefined) { CommonFieldData.internalWeapon_bitsize + internals.get.bitsize } else { 0L } - 23L + deploySize + internalSize //1u + 8u + 7u + 4u + 2u + 1u + val internalSize = internals match { + case Some(inv) => + inv.bitsize + case None => + 0 + } + 22L + deploySize + internalSize //8u + 7u + 4u + 2u + 1u } } @@ -39,7 +44,7 @@ object SmallTurretData extends Marshallable[SmallTurretData] { * @param internals data regarding the mounted weapon * @return a `SmallTurretData` object */ - def apply(deploy : CommonFieldData, health : Int, internals : InternalSlot) : SmallTurretData = + def apply(deploy : SmallDeployableData, health : Int, internals : InventoryData) : SmallTurretData = new SmallTurretData(deploy, health, Some(internals)) /** @@ -81,21 +86,20 @@ object SmallTurretData extends Marshallable[SmallTurretData] { ) implicit val codec : Codec[SmallTurretData] = ( - ("deploy" | CommonFieldData.codec) :: - bool :: + ("deploy" | SmallDeployableData.codec) :: ("health" | uint8L) :: uintL(7) :: uint4L :: uint2L :: - optional(bool, "internals" | CommonFieldData.internalWeaponCodec) + optional(bool, "internals" | InventoryData.codec) ).exmap[SmallTurretData] ( { - case deploy :: false :: health :: 0 :: 0xF :: 0 :: internals :: HNil => - var newHealth : Int = health - var newInternals : Option[InternalSlot] = internals - if(health == 0 || internals.isEmpty) { - newHealth = 0 - newInternals = None + case deploy :: health :: 0 :: 0xF :: 0 :: internals :: HNil => + val (newHealth, newInternals) = if(health == 0 || internals.isEmpty || internals.get.contents.isEmpty) { + (0, None) + } + else { + (health, internals) } Attempt.successful(SmallTurretData(deploy, newHealth, newInternals)) @@ -104,13 +108,13 @@ object SmallTurretData extends Marshallable[SmallTurretData] { }, { case SmallTurretData(deploy, health, internals) => - var newHealth : Int = health - var newInternals : Option[InternalSlot] = internals - if(health == 0 || internals.isEmpty) { - newHealth = 0 - newInternals = None + val (newHealth, newInternals) = if(health == 0 || internals.isEmpty || internals.get.contents.isEmpty) { + (0, None) } - Attempt.successful(deploy :: false :: newHealth :: 0 :: 0xF :: 0 :: newInternals :: HNil) + else { + (health, internals) + } + Attempt.successful(deploy :: newHealth :: 0 :: 0xF :: 0 :: newInternals :: HNil) } ) } diff --git a/common/src/main/scala/net/psforever/packet/game/objectcreate/TRAPData.scala b/common/src/main/scala/net/psforever/packet/game/objectcreate/TRAPData.scala index 3c5fcba7..8128965b 100644 --- a/common/src/main/scala/net/psforever/packet/game/objectcreate/TRAPData.scala +++ b/common/src/main/scala/net/psforever/packet/game/objectcreate/TRAPData.scala @@ -12,25 +12,24 @@ import shapeless.{::, HNil} * @param deploy data common to objects spawned by the (advanced) adaptive construction engine * @param health the amount of health the object has, as a percentage of a filled bar */ -final case class TRAPData(deploy : CommonFieldData, +final case class TRAPData(deploy : SmallDeployableData, health : Int ) extends ConstructorData { override def bitsize : Long = { - 23L + deploy.bitsize //8u + 7u + 4u + 3u + 1u + 22L + deploy.bitsize //8u + 7u + 4u + 3u } } object TRAPData extends Marshallable[TRAPData] { implicit val codec : Codec[TRAPData] = ( - ("deploy" | CommonFieldData.codec) :: - bool :: + ("deploy" | SmallDeployableData.codec) :: ("health" | uint8L) :: uint(7) :: uint4L :: uint(3) ).exmap[TRAPData] ( { - case deploy :: false :: health :: 0 :: 15 :: 0 :: HNil => + case deploy :: health :: 0 :: 15 :: 0 :: HNil => Attempt.successful(TRAPData(deploy, health)) case _ => @@ -38,7 +37,7 @@ object TRAPData extends Marshallable[TRAPData] { }, { case TRAPData(deploy, health) => - Attempt.successful(deploy :: false :: health :: 0 :: 15 :: 0 :: HNil) + Attempt.successful(deploy :: health :: 0 :: 15 :: 0 :: HNil) } ) } diff --git a/common/src/main/scala/net/psforever/types/Vector3.scala b/common/src/main/scala/net/psforever/types/Vector3.scala index e501d635..1d135af6 100644 --- a/common/src/main/scala/net/psforever/types/Vector3.scala +++ b/common/src/main/scala/net/psforever/types/Vector3.scala @@ -69,6 +69,13 @@ object Vector3 { ("z" | floatL) ).as[Vector3] + /** + * A common vector object that only concerns itself with rotation around the world-up axis. + * @param yaw the angle of rotation + * @return a `Vector3` object + */ + def z(yaw : Float) : Vector3 = Vector3(0, 0, yaw) + /** * Calculate the actual distance between two points. * @param pos1 the first point diff --git a/common/src/main/scala/services/RemoverActor.scala b/common/src/main/scala/services/RemoverActor.scala index e096a872..703b53c2 100644 --- a/common/src/main/scala/services/RemoverActor.scala +++ b/common/src/main/scala/services/RemoverActor.scala @@ -48,7 +48,7 @@ abstract class RemoverActor extends SupportActor[RemoverActor.Entry] { */ var secondHeap : List[RemoverActor.Entry] = List() - private var taskResolver : ActorRef = Actor.noSender + protected var taskResolver : ActorRef = Actor.noSender val sameEntryComparator = new SimilarityComparator[RemoverActor.Entry]() { def Test(entry1 : RemoverActor.Entry, entry2 : RemoverActor.Entry) : Boolean = { @@ -101,7 +101,16 @@ abstract class RemoverActor extends SupportActor[RemoverActor.Entry] { val entry = RemoverActor.Entry(obj, zone, duration.getOrElse(FirstStandardDuration).toNanos) if(InclusionTest(entry) && !secondHeap.exists(test => sameEntryComparator.Test(test, entry) )) { InitialJob(entry) - if(firstHeap.isEmpty) { + if(entry.duration == 0) { + //skip the first queue altogether + FirstJob(entry) + secondHeap = secondHeap ++ List(RepackageEntry(entry)) + if(secondHeap.size == 1) { + import scala.concurrent.ExecutionContext.Implicits.global + secondTask = context.system.scheduler.scheduleOnce(SecondStandardDuration, self, RemoverActor.TryDelete()) + } + } + else if(firstHeap.isEmpty) { //we were the only entry so the event must be started from scratch firstHeap = List(entry) trace(s"a remover task has been added: $entry") diff --git a/common/src/main/scala/services/avatar/AvatarAction.scala b/common/src/main/scala/services/avatar/AvatarAction.scala index 310bad7e..b22bbeff 100644 --- a/common/src/main/scala/services/avatar/AvatarAction.scala +++ b/common/src/main/scala/services/avatar/AvatarAction.scala @@ -2,6 +2,7 @@ package services.avatar import net.psforever.objects.ballistics.SourceEntry +import net.psforever.objects.ce.Deployable import net.psforever.objects.{PlanetSideGameObject, Player} import net.psforever.objects.equipment.Equipment import net.psforever.objects.inventory.Container @@ -9,7 +10,7 @@ import net.psforever.objects.zones.Zone import net.psforever.packet.PlanetSideGamePacket import net.psforever.packet.game.{PlanetSideGUID, PlayerStateMessageUpstream} import net.psforever.packet.game.objectcreate.{ConstructorData, ObjectCreateMessageParent} -import net.psforever.types.{ExoSuitType, Vector3} +import net.psforever.types.{ExoSuitType, PlanetSideEmpire, Vector3} import scala.concurrent.duration.FiniteDuration @@ -23,20 +24,23 @@ object AvatarAction { final case class ChangeFireState_Stop(player_guid : PlanetSideGUID, weapon_guid : PlanetSideGUID) extends Action final case class ConcealPlayer(player_guid : PlanetSideGUID) extends Action final case class Damage(player_guid : PlanetSideGUID, target : Player, resolution_function : (Any)=>Unit) extends Action + final case class DeployItem(player_guid : PlanetSideGUID, item : PlanetSideGameObject with Deployable) extends Action final case class Destroy(victim : PlanetSideGUID, killer : PlanetSideGUID, weapon : PlanetSideGUID, pos : Vector3) extends Action final case class DestroyDisplay(killer : SourceEntry, victim : SourceEntry, method : Int, unk : Int = 121) extends Action final case class DropItem(player_guid : PlanetSideGUID, item : Equipment, zone : Zone) extends Action final case class EquipmentInHand(player_guid : PlanetSideGUID, target_guid : PlanetSideGUID, slot : Int, item : Equipment) extends Action final case class HitHint(source_guid : PlanetSideGUID, player_guid : PlanetSideGUID) extends Action - final case class LoadPlayer(player_guid : PlanetSideGUID, object_id : Int, target_guid : PlanetSideGUID, cdata : ConstructorData, pdata : Option[ObjectCreateMessageParent]) extends Action final case class KilledWhileInVehicle(player_guid : PlanetSideGUID) extends Action + final case class LoadPlayer(player_guid : PlanetSideGUID, object_id : Int, target_guid : PlanetSideGUID, cdata : ConstructorData, pdata : Option[ObjectCreateMessageParent]) 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 PickupItem(player_guid : PlanetSideGUID, zone : Zone, target : PlanetSideGameObject with Container, slot : Int, item : Equipment, unk : Int = 0) extends Action + final case class PutDownFDU(player_guid : PlanetSideGUID) extends Action final case class Release(player : Player, zone : Zone, time : Option[FiniteDuration] = None) extends Action final case class Reload(player_guid : PlanetSideGUID, weapon_guid : PlanetSideGUID) extends Action + final case class SetEmpire(player_guid : PlanetSideGUID, object_guid : PlanetSideGUID, faction : PlanetSideEmpire.Value) extends Action final case class StowEquipment(player_guid : PlanetSideGUID, target_guid : PlanetSideGUID, slot : Int, item : Equipment) extends Action final case class WeaponDryFire(player_guid : PlanetSideGUID, weapon_guid : PlanetSideGUID) extends Action diff --git a/common/src/main/scala/services/avatar/AvatarResponse.scala b/common/src/main/scala/services/avatar/AvatarResponse.scala index 03647d5d..a35c5ef6 100644 --- a/common/src/main/scala/services/avatar/AvatarResponse.scala +++ b/common/src/main/scala/services/avatar/AvatarResponse.scala @@ -5,9 +5,9 @@ import net.psforever.objects.Player import net.psforever.objects.ballistics.SourceEntry import net.psforever.objects.equipment.Equipment import net.psforever.packet.PlanetSideGamePacket -import net.psforever.packet.game.{ObjectCreateMessage, PlanetSideGUID, PlayerStateMessageUpstream} +import net.psforever.packet.game._ import net.psforever.packet.game.objectcreate.ConstructorData -import net.psforever.types.{ExoSuitType, Vector3} +import net.psforever.types.{ExoSuitType, PlanetSideEmpire, Vector3} object AvatarResponse { trait Response @@ -30,8 +30,10 @@ object AvatarResponse { 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 PutDownFDU(target_guid : PlanetSideGUID) extends Response final case class Release(player : Player) extends Response final case class Reload(weapon_guid : PlanetSideGUID) extends Response + final case class SetEmpire(object_guid : PlanetSideGUID, faction : PlanetSideEmpire.Value) extends Response final case class StowEquipment(target_guid : PlanetSideGUID, slot : Int, item : Equipment) extends Response final case class WeaponDryFire(weapon_guid : PlanetSideGUID) extends Response diff --git a/common/src/main/scala/services/avatar/AvatarService.scala b/common/src/main/scala/services/avatar/AvatarService.scala index c703b360..2b7e5081 100644 --- a/common/src/main/scala/services/avatar/AvatarService.scala +++ b/common/src/main/scala/services/avatar/AvatarService.scala @@ -10,7 +10,6 @@ import services.{GenericEventBus, RemoverActor, Service} class AvatarService extends Actor { private val undertaker : ActorRef = context.actorOf(Props[CorpseRemovalActor], "corpse-removal-agent") private val janitor = context.actorOf(Props[DroppedItemRemover], "item-remover-agent") - //undertaker ! "startup" private [this] val log = org.log4s.getLogger @@ -69,6 +68,14 @@ class AvatarService extends Actor { AvatarEvents.publish( AvatarServiceResponse(s"/$forChannel/Avatar", player_guid, AvatarResponse.DamageResolution(target, resolution_function)) ) + case AvatarAction.DeployItem(player_guid, item) => + val definition = item.Definition + val objectData = definition.Packet.ConstructorData(item).get + AvatarEvents.publish( + AvatarServiceResponse(s"/$forChannel/Avatar", player_guid, + AvatarResponse.DropItem(ObjectCreateMessage(definition.ObjectId, item.GUID, objectData)) + ) + ) case AvatarAction.Destroy(victim, killer, weapon, pos) => AvatarEvents.publish( AvatarServiceResponse(s"/$forChannel/Avatar", victim, AvatarResponse.Destroy(victim, killer, weapon, pos)) @@ -148,6 +155,10 @@ class AvatarService extends Actor { } }) ) + case AvatarAction.PutDownFDU(player_guid) => + AvatarEvents.publish( + AvatarServiceResponse(s"/$forChannel/Avatar", player_guid, AvatarResponse.PutDownFDU(player_guid)) + ) case AvatarAction.Release(player, zone, time) => undertaker forward RemoverActor.AddTask(player, zone, time) AvatarEvents.publish( @@ -157,6 +168,10 @@ class AvatarService extends Actor { AvatarEvents.publish( AvatarServiceResponse(s"/$forChannel/Avatar", player_guid, AvatarResponse.Reload(weapon_guid)) ) + case AvatarAction.SetEmpire(player_guid, target_guid, faction) => + AvatarEvents.publish( + AvatarServiceResponse(s"/$forChannel/Avatar", player_guid, AvatarResponse.SetEmpire(target_guid, faction)) + ) case AvatarAction.StowEquipment(player_guid, target_guid, slot, obj) => AvatarEvents.publish( AvatarServiceResponse(s"/$forChannel/Avatar", player_guid, AvatarResponse.StowEquipment(target_guid, slot, obj)) @@ -177,6 +192,7 @@ class AvatarService extends Actor { case AvatarServiceMessage.Corpse(msg) => undertaker forward msg + //message to Janitor case AvatarServiceMessage.Ground(msg) => janitor forward msg diff --git a/common/src/main/scala/services/avatar/support/DroppedItemRemover.scala b/common/src/main/scala/services/avatar/support/DroppedItemRemover.scala index ce91fdf8..295536bc 100644 --- a/common/src/main/scala/services/avatar/support/DroppedItemRemover.scala +++ b/common/src/main/scala/services/avatar/support/DroppedItemRemover.scala @@ -21,7 +21,7 @@ class DroppedItemRemover extends RemoverActor { def FirstJob(entry : RemoverActor.Entry) : Unit = { import net.psforever.objects.zones.Zone - entry.zone.Ground ! Zone.Ground.PickupItem(entry.obj.GUID) + entry.zone.Ground ! Zone.Ground.RemoveItem(entry.obj.GUID) context.parent ! AvatarServiceMessage(entry.zone.Id, AvatarAction.ObjectDelete(Service.defaultPlayerGUID, entry.obj.GUID)) } diff --git a/common/src/main/scala/services/galaxy/GalaxyAction.scala b/common/src/main/scala/services/galaxy/GalaxyAction.scala index 7bb9c794..34c0b4de 100644 --- a/common/src/main/scala/services/galaxy/GalaxyAction.scala +++ b/common/src/main/scala/services/galaxy/GalaxyAction.scala @@ -1,7 +1,7 @@ // Copyright (c) 2017 PSForever package services.galaxy -import net.psforever.packet.game.{BuildingInfoUpdateMessage} +import net.psforever.packet.game.BuildingInfoUpdateMessage object GalaxyAction { trait Action diff --git a/common/src/main/scala/services/galaxy/GalaxyResponse.scala b/common/src/main/scala/services/galaxy/GalaxyResponse.scala index 8026085f..42ece3a5 100644 --- a/common/src/main/scala/services/galaxy/GalaxyResponse.scala +++ b/common/src/main/scala/services/galaxy/GalaxyResponse.scala @@ -1,7 +1,7 @@ // Copyright (c) 2017 PSForever package services.galaxy -import net.psforever.packet.game.{BuildingInfoUpdateMessage} +import net.psforever.packet.game.BuildingInfoUpdateMessage object GalaxyResponse { trait Response diff --git a/common/src/main/scala/services/local/LocalAction.scala b/common/src/main/scala/services/local/LocalAction.scala index b20c5143..4dc109d4 100644 --- a/common/src/main/scala/services/local/LocalAction.scala +++ b/common/src/main/scala/services/local/LocalAction.scala @@ -1,17 +1,21 @@ // Copyright (c) 2017 PSForever package services.local +import net.psforever.objects.PlanetSideGameObject +import net.psforever.objects.ce.Deployable import net.psforever.objects.serverobject.PlanetSideServerObject import net.psforever.objects.serverobject.doors.Door import net.psforever.objects.serverobject.hackable.Hackable import net.psforever.objects.serverobject.terminals.CaptureTerminal import net.psforever.objects.zones.Zone -import net.psforever.packet.game.{PlanetSideGUID, TriggeredSound} +import net.psforever.packet.game._ import net.psforever.types.{PlanetSideEmpire, Vector3} object LocalAction { trait Action + final case class AlertDestroyDeployable(player_guid : PlanetSideGUID, obj : PlanetSideGameObject with Deployable) extends Action + final case class DeployableMapIcon(player_guid : PlanetSideGUID, behavior : DeploymentAction.Value, deployInfo : DeployableInfo) extends Action final case class DoorOpens(player_guid : PlanetSideGUID, continent : Zone, door : Door) extends Action final case class DoorCloses(player_guid : PlanetSideGUID, door_guid : PlanetSideGUID) extends Action final case class HackClear(player_guid : PlanetSideGUID, target : PlanetSideServerObject, unk1 : Long, unk2 : Long = 8L) extends Action @@ -19,6 +23,9 @@ object LocalAction { final case class ClearTemporaryHack(player_guid: PlanetSideGUID, target: PlanetSideServerObject with Hackable) extends Action final case class HackCaptureTerminal(player_guid : PlanetSideGUID, continent : Zone, target : CaptureTerminal, unk1 : Long, unk2 : Long = 8L, isResecured : Boolean) extends Action final case class ProximityTerminalEffect(player_guid : PlanetSideGUID, object_guid : PlanetSideGUID, effectState : Boolean) extends Action + final case class TriggerEffect(player_guid : PlanetSideGUID, effect : String, target : PlanetSideGUID) extends Action + final case class TriggerEffectInfo(player_guid : PlanetSideGUID, effect : String, target : PlanetSideGUID, unk1 : Boolean, unk2 : Long) extends Action + final case class TriggerEffectLocation(player_guid : PlanetSideGUID, effect : String, pos : Vector3, orient : Vector3) extends Action final case class TriggerSound(player_guid : PlanetSideGUID, sound : TriggeredSound.Value, pos : Vector3, unk : Int, volume : Float) extends Action final case class SetEmpire(object_guid: PlanetSideGUID, empire: PlanetSideEmpire.Value) extends Action } diff --git a/common/src/main/scala/services/local/LocalResponse.scala b/common/src/main/scala/services/local/LocalResponse.scala index c2d997a5..7963a1af 100644 --- a/common/src/main/scala/services/local/LocalResponse.scala +++ b/common/src/main/scala/services/local/LocalResponse.scala @@ -1,18 +1,25 @@ // Copyright (c) 2017 PSForever package services.local -import net.psforever.packet.game.{PlanetSideGUID, TriggeredSound} +import net.psforever.objects.ce.Deployable +import net.psforever.objects.PlanetSideGameObject +import net.psforever.packet.game._ import net.psforever.types.{PlanetSideEmpire, Vector3} object LocalResponse { trait Response + final case class AlertDestroyDeployable(obj : PlanetSideGameObject with Deployable) extends Response + final case class DeployableMapIcon(action : DeploymentAction.Value, deployInfo : DeployableInfo) extends Response final case class DoorOpens(door_guid : PlanetSideGUID) extends Response final case class DoorCloses(door_guid : PlanetSideGUID) extends Response + final case class EliminateDeployable(obj : PlanetSideGameObject with Deployable, object_guid : PlanetSideGUID, pos : Vector3) extends Response final case class HackClear(target_guid : PlanetSideGUID, unk1 : Long, unk2 : Long) extends Response final case class HackObject(target_guid : PlanetSideGUID, unk1 : Long, unk2 : Long) extends Response final case class HackCaptureTerminal(target_guid : PlanetSideGUID, unk1 : Long, unk2 : Long, isResecured: Boolean) extends Response + final case class ObjectDelete(item_guid : PlanetSideGUID, unk : Int) extends Response final case class ProximityTerminalEffect(object_guid : PlanetSideGUID, effectState : Boolean) extends Response + final case class TriggerEffect(target: PlanetSideGUID, effect: String, effectInfo: Option[TriggeredEffect] = None, triggeredLocation: Option[TriggeredEffectLocation] = None) extends Response final case class TriggerSound(sound : TriggeredSound.Value, pos : Vector3, unk : Int, volume : Float) extends Response final case class SetEmpire(object_guid: PlanetSideGUID, empire: PlanetSideEmpire.Value) extends Response } diff --git a/common/src/main/scala/services/local/LocalService.scala b/common/src/main/scala/services/local/LocalService.scala index be44d0d8..8f5c5e21 100644 --- a/common/src/main/scala/services/local/LocalService.scala +++ b/common/src/main/scala/services/local/LocalService.scala @@ -2,28 +2,30 @@ package services.local import akka.actor.{Actor, ActorRef, Props} -import net.psforever.objects.serverobject.CommonMessages +import net.psforever.objects.ce.Deployable +import net.psforever.objects.serverobject.resourcesilo.ResourceSilo +import net.psforever.objects.serverobject.structures.Building +import net.psforever.objects.serverobject.terminals.CaptureTerminal import net.psforever.objects.zones.{InterstellarCluster, Zone} -import net.psforever.objects.zones.InterstellarCluster.GetWorld -import services.local.support.{DoorCloseActor, HackCaptureActor, HackClearActor} +import net.psforever.objects.{BoomerDeployable, GlobalDefinitions, PlanetSideGameObject, TurretDeployable} +import net.psforever.packet.game.{PlanetSideGUID, TriggeredEffect, TriggeredEffectLocation} +import net.psforever.objects.vital.Vitality +import net.psforever.types.Vector3 +import services.local.support.{DeployableRemover, DoorCloseActor, HackClearActor, HackCaptureActor} +import services.vehicle.{VehicleAction, VehicleServiceMessage} import services.{GenericEventBus, Service, ServiceManager} -import services.local.support.{DoorCloseActor, HackClearActor} import scala.util.Success import scala.concurrent.duration._ import akka.pattern.ask -import net.psforever.objects.GlobalDefinitions -import net.psforever.objects.serverobject.hackable.Hackable -import net.psforever.objects.serverobject.resourcesilo.ResourceSilo -import net.psforever.objects.serverobject.structures.{Amenity, Building} -import net.psforever.objects.serverobject.terminals.CaptureTerminal -import net.psforever.packet.game.PlanetSideGUID import services.ServiceManager.Lookup +import scala.concurrent.duration.Duration class LocalService extends Actor { private val doorCloser = context.actorOf(Props[DoorCloseActor], "local-door-closer") private val hackClearer = context.actorOf(Props[HackClearActor], "local-hack-clearer") private val hackCapturer = context.actorOf(Props[HackCaptureActor], "local-hack-capturer") + private val engineer = context.actorOf(Props[DeployableRemover], "deployable-remover-agent") private [this] val log = org.log4s.getLogger var cluster : ActorRef = Actor.noSender @@ -59,6 +61,14 @@ class LocalService extends Actor { case LocalServiceMessage(forChannel, action) => action match { + case LocalAction.AlertDestroyDeployable(_, obj) => + LocalEvents.publish( + LocalServiceResponse(s"/$forChannel/Local", Service.defaultPlayerGUID, LocalResponse.AlertDestroyDeployable(obj)) + ) + case LocalAction.DeployableMapIcon(player_guid, behavior, deployInfo) => + LocalEvents.publish( + LocalServiceResponse(s"/$forChannel/Local", player_guid, LocalResponse.DeployableMapIcon(behavior, deployInfo)) + ) case LocalAction.DoorOpens(player_guid, zone, door) => doorCloser ! DoorCloseActor.DoorIsOpen(door, zone) LocalEvents.publish( @@ -77,7 +87,7 @@ class LocalService extends Actor { LocalEvents.publish( LocalServiceResponse(s"/$forChannel/Local", player_guid, LocalResponse.HackObject(target.GUID, unk1, unk2)) ) - case LocalAction.ClearTemporaryHack(player_guid, target) => + case LocalAction.ClearTemporaryHack(_, target) => hackClearer ! HackClearActor.ObjectIsResecured(target) case LocalAction.HackCaptureTerminal(player_guid, zone, target, unk1, unk2, isResecured) => @@ -101,14 +111,26 @@ class LocalService extends Actor { LocalEvents.publish( LocalServiceResponse(s"/$forChannel/Local", player_guid, LocalResponse.ProximityTerminalEffect(object_guid, effectState)) ) + case LocalAction.SetEmpire(object_guid, empire) => + LocalEvents.publish( + LocalServiceResponse(s"/$forChannel/Local", Service.defaultPlayerGUID, LocalResponse.SetEmpire(object_guid, empire)) + ) + case LocalAction.TriggerEffect(player_guid, effect, target) => + LocalEvents.publish( + LocalServiceResponse(s"/$forChannel/Local", player_guid, LocalResponse.TriggerEffect(target, effect)) + ) + case LocalAction.TriggerEffectLocation(player_guid, effect, pos, orient) => + LocalEvents.publish( + LocalServiceResponse(s"/$forChannel/Local", player_guid, LocalResponse.TriggerEffect(PlanetSideGUID(0), effect, None, Some(TriggeredEffectLocation(pos, orient)))) + ) + case LocalAction.TriggerEffectInfo(player_guid, effect, target, unk1, unk2) => + LocalEvents.publish( + LocalServiceResponse(s"/$forChannel/Local", player_guid, LocalResponse.TriggerEffect(target, effect, Some(TriggeredEffect(unk1, unk2)))) + ) case LocalAction.TriggerSound(player_guid, sound, pos, unk, volume) => LocalEvents.publish( LocalServiceResponse(s"/$forChannel/Local", player_guid, LocalResponse.TriggerSound(sound, pos, unk, volume)) ) - case LocalAction.SetEmpire(object_guid, empire) => - LocalEvents.publish( - LocalServiceResponse(s"/$forChannel/Local", PlanetSideGUID(-1), LocalResponse.SetEmpire(object_guid, empire)) - ) case _ => ; } @@ -120,21 +142,21 @@ class LocalService extends Actor { //response from HackClearActor case HackClearActor.ClearTheHack(target_guid, zone_id, unk1, unk2) => - log.warn(s"Clearing hack for ${target_guid}") + log.warn(s"Clearing hack for $target_guid") LocalEvents.publish( LocalServiceResponse(s"/$zone_id/Local", Service.defaultPlayerGUID, LocalResponse.HackClear(target_guid, unk1, unk2)) ) - case HackCaptureActor.HackTimeoutReached(capture_terminal_guid, zone_id, unk1, unk2, hackedByFaction) => + case HackCaptureActor.HackTimeoutReached(capture_terminal_guid, zone_id, _, _, hackedByFaction) => import scala.concurrent.ExecutionContext.Implicits.global ask(cluster, InterstellarCluster.GetWorld(zone_id))(1 seconds).onComplete { - case Success(InterstellarCluster.GiveWorld(zoneId, zone)) => + case Success(InterstellarCluster.GiveWorld(_, zone)) => val terminal = zone.asInstanceOf[Zone].GUID(capture_terminal_guid).get.asInstanceOf[CaptureTerminal] val building = terminal.Owner.asInstanceOf[Building] // todo: Move this to a function for Building var ntuLevel = 0 - building.Amenities.filter(x => (x.Definition == GlobalDefinitions.resource_silo)).headOption.asInstanceOf[Option[ResourceSilo]] match { + building.Amenities.find(_.Definition == GlobalDefinitions.resource_silo).asInstanceOf[Option[ResourceSilo]] match { case Some(obj: ResourceSilo) => ntuLevel = obj.CapacitorDisplay.toInt case _ => @@ -143,7 +165,7 @@ class LocalService extends Actor { } if(ntuLevel > 0) { - log.info(s"Setting base ${building.ModelId} as owned by ${hackedByFaction}") + log.info(s"Setting base ${building.ModelId} as owned by $hackedByFaction") building.Faction = hackedByFaction self ! LocalServiceMessage(zone.Id, LocalAction.SetEmpire(PlanetSideGUID(building.ModelId), hackedByFaction)) @@ -160,9 +182,86 @@ class LocalService extends Actor { case scala.util.Failure(_) => log.warn(s"LocalService Failed to get zone when hack timeout was reached") } + case HackCaptureActor.GetHackTimeRemainingNanos(capture_console_guid) => hackCapturer forward HackCaptureActor.GetHackTimeRemainingNanos(capture_console_guid) + + //message to Engineer + case LocalServiceMessage.Deployables(msg) => + engineer forward msg + + //message(s) from Engineer + case msg @ DeployableRemover.EliminateDeployable(obj : TurretDeployable, guid, pos, zone) => + val seats = obj.Seats.values + if(seats.count(_.isOccupied) > 0) { + val wasKickedByDriver = false //TODO yeah, I don't know + seats.foreach(seat => { + seat.Occupant match { + case Some(tplayer) => + seat.Occupant = None + tplayer.VehicleSeated = None + zone.VehicleEvents ! VehicleServiceMessage(zone.Id, VehicleAction.KickPassenger(tplayer.GUID, 4, wasKickedByDriver, obj.GUID)) + case None => ; + } + }) + import scala.concurrent.ExecutionContext.Implicits.global + context.system.scheduler.scheduleOnce(Duration.create(2, "seconds"), self, msg) + } + else { + EliminateDeployable(obj, guid, pos, zone.Id) + } + + case DeployableRemover.EliminateDeployable(obj : BoomerDeployable, guid, pos, zone) => + EliminateDeployable(obj, guid, pos, zone.Id) + obj.Trigger match { + case Some(trigger) => + log.warn(s"LocalService: deconstructing boomer in ${zone.Id}, but trigger@${trigger.GUID.guid} still exists") + case _ => ; + } + + case DeployableRemover.EliminateDeployable(obj, guid, pos, zone) => + EliminateDeployable(obj, guid, pos, zone.Id) + + case DeployableRemover.DeleteTrigger(trigger_guid, zone) => + LocalEvents.publish( + LocalServiceResponse(s"/${zone.Id}/Local", Service.defaultPlayerGUID, LocalResponse.ObjectDelete(trigger_guid, 0)) + ) + + //synchronized damage calculations + case Vitality.DamageOn(target : Deployable, func) => + func(target) + sender ! Vitality.DamageResolution(target) + case msg => - log.info(s"Unhandled message $msg from $sender") + log.warn(s"Unhandled message $msg from $sender") + } + + /** + * Common behavior for distributing information about a deployable's destruction or deconstruction.
+ *
+ * The primary distribution task instructs all clients to eliminate the target deployable. + * This is a cosmetic exercise as the deployable should already be unregistered from its zone and + * functionally removed from its zone's list of deployable objects by external operations. + * The other distribution is a targeted message sent to the former owner of the deployable + * if he still exists on the server + * to clean up any leftover ownership-specific knowledge about the deployable. + * @see `DeployableRemover` + * @param obj the deployable object + * @param guid the deployable objects globally unique identifier; + * may be a former identifier + * @param position the deployable's position + * @param zoneId the zone where the deployable is currently placed + */ + def EliminateDeployable(obj : PlanetSideGameObject with Deployable, guid : PlanetSideGUID, position : Vector3, zoneId : String) : Unit = { + LocalEvents.publish( + LocalServiceResponse(s"/$zoneId/Local", Service.defaultPlayerGUID, LocalResponse.EliminateDeployable(obj, guid, position)) + ) + obj.OwnerName match { + case Some(name) => + LocalEvents.publish( + LocalServiceResponse(s"/$name/Local", Service.defaultPlayerGUID, LocalResponse.AlertDestroyDeployable(obj)) + ) + case None => ; + } } } diff --git a/common/src/main/scala/services/local/LocalServiceMessage.scala b/common/src/main/scala/services/local/LocalServiceMessage.scala index fc6dd20a..4bbe7291 100644 --- a/common/src/main/scala/services/local/LocalServiceMessage.scala +++ b/common/src/main/scala/services/local/LocalServiceMessage.scala @@ -2,3 +2,7 @@ package services.local final case class LocalServiceMessage(forChannel : String, actionMessage : LocalAction.Action) + +object LocalServiceMessage { + final case class Deployables(msg : Any) +} diff --git a/common/src/main/scala/services/local/support/DeployableRemover.scala b/common/src/main/scala/services/local/support/DeployableRemover.scala new file mode 100644 index 00000000..30bd8677 --- /dev/null +++ b/common/src/main/scala/services/local/support/DeployableRemover.scala @@ -0,0 +1,86 @@ +// Copyright (c) 2017 PSForever +package services.local.support + +import net.psforever.objects.ce.Deployable +import net.psforever.objects.guid.{GUIDTask, TaskResolver} +import net.psforever.objects.zones.Zone +import net.psforever.objects.{BoomerDeployable, PlanetSideGameObject, TurretDeployable} +import net.psforever.packet.game.PlanetSideGUID +import net.psforever.types.Vector3 +import services.RemoverActor + +import scala.concurrent.duration._ + +class DeployableRemover extends RemoverActor { + final val FirstStandardDuration : FiniteDuration = 3 minutes + + final val SecondStandardDuration : FiniteDuration = 2 seconds + + def InclusionTest(entry : RemoverActor.Entry) : Boolean = entry.obj.isInstanceOf[Deployable] + + def InitialJob(entry : RemoverActor.Entry) : Unit = { } + + def FirstJob(entry : RemoverActor.Entry) : Unit = { + val obj = entry.obj + obj match { + case boomer : BoomerDeployable => + FirstJobBoomer(boomer, entry) + case _ => ; + } + entry.zone.Deployables ! Zone.Deployable.Dismiss(obj.asInstanceOf[PlanetSideGameObject with Deployable]) + } + + def FirstJobBoomer(obj : BoomerDeployable, entry : RemoverActor.Entry) : Unit = { + obj.Trigger match { + case Some(trigger) => + if(trigger.HasGUID) { + val guid = trigger.GUID + Zone.EquipmentIs.Where(trigger, guid, entry.zone) match { + case Some(Zone.EquipmentIs.InContainer(container, index)) => + container.Slot(index).Equipment = None + case Some(Zone.EquipmentIs.OnGround()) => + entry.zone.Ground ! Zone.Ground.RemoveItem(guid) + case _ => ; + } + context.parent ! DeployableRemover.DeleteTrigger(guid, entry.zone) + } + case None => ; + } + } + + override def SecondJob(entry : RemoverActor.Entry) : Unit = { + val obj = entry.obj.asInstanceOf[PlanetSideGameObject with Deployable] + info(s"Deleting a ${obj.Definition.Name} deployable") + context.parent ! DeployableRemover.EliminateDeployable(obj, obj.GUID, obj.Position, entry.zone) + super.SecondJob(entry) + } + + def ClearanceTest(entry : RemoverActor.Entry) : Boolean = !entry.zone.DeployableList.contains(entry.obj) + + def DeletionTask(entry : RemoverActor.Entry) : TaskResolver.GiveTask = { + entry.obj match { + case turret : TurretDeployable => + GUIDTask.UnregisterDeployableTurret(turret)(entry.zone.GUID) + case boomer : BoomerDeployable => + boomer.Trigger match { + case Some(trigger) => + boomer.Trigger = None + taskResolver ! GUIDTask.UnregisterObjectTask(trigger)(entry.zone.GUID) + case None => ; + } + GUIDTask.UnregisterObjectTask(boomer)(entry.zone.GUID) + case obj => + GUIDTask.UnregisterObjectTask(obj)(entry.zone.GUID) + } + } +} + +object DeployableRemover { + final case class EliminateDeployable(obj : PlanetSideGameObject with Deployable, + guid : PlanetSideGUID, + position : Vector3, + zone : Zone) + + final case class DeleteTrigger(trigger_guid : PlanetSideGUID, + zone : Zone) +} diff --git a/common/src/main/scala/services/vehicle/support/TurretUpgrader.scala b/common/src/main/scala/services/vehicle/support/TurretUpgrader.scala index 73613ade..c5b55eb1 100644 --- a/common/src/main/scala/services/vehicle/support/TurretUpgrader.scala +++ b/common/src/main/scala/services/vehicle/support/TurretUpgrader.scala @@ -4,7 +4,7 @@ package services.vehicle.support import akka.actor.{Actor, ActorRef, Cancellable} import net.psforever.objects.{AmmoBox, DefaultCancellable, PlanetSideGameObject, Tool} import net.psforever.objects.guid.{GUIDTask, Task, TaskResolver} -import net.psforever.objects.serverobject.turret.{MannedTurret, TurretUpgrade} +import net.psforever.objects.serverobject.turret.{FacilityTurret, TurretUpgrade} import net.psforever.objects.vehicles.MountedWeapons import net.psforever.objects.zones.Zone import net.psforever.packet.game.PlanetSideGUID @@ -50,7 +50,7 @@ class TurretUpgrader extends SupportActor[TurretUpgrader.Entry] { def CreateEntry(obj : PlanetSideGameObject, zone : Zone, upgrade : TurretUpgrade.Value, duration : Long) = TurretUpgrader.Entry(obj, zone, upgrade, duration) - def InclusionTest(entry : TurretUpgrader.Entry) : Boolean = entry.obj.isInstanceOf[MannedTurret] + def InclusionTest(entry : TurretUpgrader.Entry) : Boolean = entry.obj.isInstanceOf[FacilityTurret] def receive : Receive = { case Service.Startup() => @@ -157,7 +157,7 @@ class TurretUpgrader extends SupportActor[TurretUpgrader.Entry] { * @param entry na */ def UpgradeTurretAmmo(entry : TurretUpgrader.Entry) : Unit = { - val target = entry.obj.asInstanceOf[MannedTurret] + val target = entry.obj.asInstanceOf[FacilityTurret] val zone = entry.zone val zoneId = zone.Id val upgrade = entry.upgrade @@ -222,7 +222,7 @@ class TurretUpgrader extends SupportActor[TurretUpgrader.Entry] { * @param entry na */ def FinishUpgradingTurret(entry : TurretUpgrader.Entry)() : Unit = { - val target = entry.obj.asInstanceOf[MannedTurret] + val target = entry.obj.asInstanceOf[FacilityTurret] val zone = entry.zone info(s"Wall turret finished ${target.Upgrade} upgrade") val targetGUID = target.GUID @@ -250,7 +250,7 @@ object TurretUpgrader extends SupportActorCaseConversions { */ case class Entry(_obj : PlanetSideGameObject, _zone : Zone, upgrade : TurretUpgrade.Value, _duration : Long) extends SupportActor.Entry(_obj, _zone, _duration) - final case class AddTask(turret : MannedTurret, zone : Zone, upgrade : TurretUpgrade.Value, duration : Option[FiniteDuration] = None) + final case class AddTask(turret : FacilityTurret, zone : Zone, upgrade : TurretUpgrade.Value, duration : Option[FiniteDuration] = None) final case class Downgrade() diff --git a/common/src/test/scala/Vector3Test.scala b/common/src/test/scala/Vector3Test.scala index 26fcddd5..6bf392e8 100644 --- a/common/src/test/scala/Vector3Test.scala +++ b/common/src/test/scala/Vector3Test.scala @@ -12,6 +12,14 @@ class Vector3Test extends Specification { vec.z mustEqual 3.9f } + "isolate x,y components" in { + vec.xy mustEqual Vector3(1.3f, -2.6f, 0) + } + + "promote float values into a specific z-format" in { + Vector3.z(3.9f) mustEqual Vector3(0, 0, 3.9f) + } + "calculate magnitude (like a vector) 1" in { val obj = Vector3(2.0f, 0.0f, 0.0f) Vector3.Magnitude(obj) mustEqual 2.0f diff --git a/common/src/test/scala/game/DeployObjectMessageTest.scala b/common/src/test/scala/game/DeployObjectMessageTest.scala index 23ec4ccb..99d89180 100644 --- a/common/src/test/scala/game/DeployObjectMessageTest.scala +++ b/common/src/test/scala/game/DeployObjectMessageTest.scala @@ -12,15 +12,11 @@ class DeployObjectMessageTest extends Specification { "decode" in { PacketCoding.DecodePacket(string).require match { - case DeployObjectMessage(guid, unk1, pos, roll, pitch, yaw, unk2) => + case DeployObjectMessage(guid, unk1, pos, orient, unk2) => guid mustEqual PlanetSideGUID(2932) unk1 mustEqual 1000L - pos.x mustEqual 5769.297f - pos.y mustEqual 3192.8594f - pos.z mustEqual 97.96875f - roll mustEqual 0 - pitch mustEqual 0 - yaw mustEqual 63 + pos mustEqual Vector3(5769.297f, 3192.8594f, 97.96875f) + orient mustEqual Vector3.z(272.8125f) unk2 mustEqual 1L case _ => ko @@ -28,7 +24,7 @@ class DeployObjectMessageTest extends Specification { } "encode" in { - val msg = DeployObjectMessage(PlanetSideGUID(2932), 1000L, Vector3(5769.297f, 3192.8594f, 97.96875f), 0, 0, 63, 1L) + val msg = DeployObjectMessage(PlanetSideGUID(2932), 1000L, Vector3(5769.297f, 3192.8594f, 97.96875f), Vector3.z(272.8125f), 1L) val pkt = PacketCoding.EncodePacket(msg).require.toByteVector pkt mustEqual string diff --git a/common/src/test/scala/game/ObjectDeployedMessageTest.scala b/common/src/test/scala/game/ObjectDeployedMessageTest.scala index 88c9c0b7..0678da13 100644 --- a/common/src/test/scala/game/ObjectDeployedMessageTest.scala +++ b/common/src/test/scala/game/ObjectDeployedMessageTest.scala @@ -11,10 +11,10 @@ class ObjectDeployedMessageTest extends Specification { "decode" in { PacketCoding.DecodePacket(string_boomer).require match { - case ObjectDeployedMessage(unk : Int, desc : String, act : DeploymentOutcome.Value, count : Long, max : Long) => + case ObjectDeployedMessage(unk : Int, desc : String, act : DeployOutcome.Value, count : Long, max : Long) => unk mustEqual 0 desc mustEqual "boomer" - act mustEqual DeploymentOutcome.Success + act mustEqual DeployOutcome.Success count mustEqual 1 max mustEqual 25 case _ => @@ -23,7 +23,7 @@ class ObjectDeployedMessageTest extends Specification { } "encode" in { - val msg = ObjectDeployedMessage("boomer", DeploymentOutcome.Success, 1, 25) + val msg = ObjectDeployedMessage("boomer", DeployOutcome.Success, 1, 25) val pkt = PacketCoding.EncodePacket(msg).require.toByteVector pkt mustEqual string_boomer diff --git a/common/src/test/scala/game/TriggerEffectMessageTest.scala b/common/src/test/scala/game/TriggerEffectMessageTest.scala index 9e3e6faa..2acc8e3c 100644 --- a/common/src/test/scala/game/TriggerEffectMessageTest.scala +++ b/common/src/test/scala/game/TriggerEffectMessageTest.scala @@ -10,6 +10,7 @@ import scodec.bits._ class TriggerEffectMessageTest extends Specification { val string_motionalarmsensor = hex"51 970B 82 6F6E FA00C00000" val string_boomer = hex"51 0000 93 737061776E5F6F626A6563745F656666656374 417BB2CB3B4F8E00000000" + val string_boomer_explode = hex"51 DF09 8F 6465746F6E6174655F626F6F6D6572 00" "decode (motion alarm sensor)" in { PacketCoding.DecodePacket(string_motionalarmsensor).require match { @@ -32,42 +33,44 @@ class TriggerEffectMessageTest extends Specification { effect mustEqual "spawn_object_effect" unk.isDefined mustEqual false location.isDefined mustEqual true - location.get.pos.x mustEqual 3567.0156f - location.get.pos.y mustEqual 3278.6953f - location.get.pos.z mustEqual 114.484375f - location.get.roll mustEqual 0 - location.get.pitch mustEqual 0 - location.get.yaw mustEqual 0 + location.get.pos mustEqual Vector3(3567.0156f, 3278.6953f, 114.484375f) + location.get.orient mustEqual Vector3(0, 0, 90) + case _ => + ko + } + } + + "decode (boomer explode)" in { + PacketCoding.DecodePacket(string_boomer_explode).require match { + case TriggerEffectMessage(guid, effect, unk, location) => + guid mustEqual PlanetSideGUID(2527) + effect mustEqual "detonate_boomer" + unk.isDefined mustEqual false + location.isDefined mustEqual false case _ => ko } } "encode (motion alarm sensor)" in { - val msg = TriggerEffectMessage( - PlanetSideGUID(2967), - "on", - Some(TriggeredEffect(true, 1000L)), - None - ) + val msg = TriggerEffectMessage(PlanetSideGUID(2967), "on", true, 1000L) val pkt = PacketCoding.EncodePacket(msg).require.toByteVector pkt mustEqual string_motionalarmsensor } "encode (boomer)" in { - val msg = TriggerEffectMessage( - PlanetSideGUID(0), - "spawn_object_effect", - None, - Some(TriggeredEffectLocation( - Vector3(3567.0156f, 3278.6953f, 114.484375f), - 0, 0, 0 - )) - ) + val msg = TriggerEffectMessage("spawn_object_effect", Vector3(3567.0156f, 3278.6953f, 114.484375f), Vector3(0, 0, 90)) val pkt = PacketCoding.EncodePacket(msg).require.toByteVector pkt mustEqual string_boomer } + + "encode (boomer explode)" in { + val msg = TriggerEffectMessage(PlanetSideGUID(2527), "detonate_boomer") + val pkt = PacketCoding.EncodePacket(msg).require.toByteVector + + pkt mustEqual string_boomer_explode + } } diff --git a/common/src/test/scala/game/objectcreate/AegisShieldGeneratorDataTest.scala b/common/src/test/scala/game/objectcreate/AegisShieldGeneratorDataTest.scala index 8b2bae09..8772daf2 100644 --- a/common/src/test/scala/game/objectcreate/AegisShieldGeneratorDataTest.scala +++ b/common/src/test/scala/game/objectcreate/AegisShieldGeneratorDataTest.scala @@ -4,7 +4,7 @@ package game.objectcreate import net.psforever.packet.PacketCoding import net.psforever.packet.game.{ObjectCreateMessage, PlanetSideGUID} import net.psforever.packet.game.objectcreate._ -import net.psforever.types.PlanetSideEmpire +import net.psforever.types.{PlanetSideEmpire, Vector3} import org.specs2.mutable._ import scodec.bits._ @@ -22,12 +22,8 @@ class AegisShieldGeneratorDataTest extends Specification { data.isDefined mustEqual true data.get.isInstanceOf[AegisShieldGeneratorData] mustEqual true val aegis = data.get.asInstanceOf[AegisShieldGeneratorData] - aegis.deploy.pos.coord.x mustEqual 3571.2266f - aegis.deploy.pos.coord.y mustEqual 3278.0938f - aegis.deploy.pos.coord.z mustEqual 114.0f - aegis.deploy.pos.orient.x mustEqual 0f - aegis.deploy.pos.orient.y mustEqual 0f - aegis.deploy.pos.orient.z mustEqual 90.0f + aegis.deploy.pos.coord mustEqual Vector3(3571.2266f, 3278.0938f, 114.0f) + aegis.deploy.pos.orient mustEqual Vector3(0, 0, 90) aegis.deploy.faction mustEqual PlanetSideEmpire.VS aegis.deploy.unk mustEqual 2 aegis.health mustEqual 255 @@ -40,7 +36,7 @@ class AegisShieldGeneratorDataTest extends Specification { "encode" in { val obj = AegisShieldGeneratorData( CommonFieldData( - PlacementData(3571.2266f, 3278.0938f, 114.0f, 0f, 0f, 90.0f), + PlacementData(Vector3(3571.2266f, 3278.0938f, 114.0f), Vector3(0, 0, 90)), PlanetSideEmpire.VS, 2, PlanetSideGUID(2366) ), 255 diff --git a/common/src/test/scala/game/objectcreate/OneMannedFieldTurretDataTest.scala b/common/src/test/scala/game/objectcreate/OneMannedFieldTurretDataTest.scala index 4847bb15..e7815d72 100644 --- a/common/src/test/scala/game/objectcreate/OneMannedFieldTurretDataTest.scala +++ b/common/src/test/scala/game/objectcreate/OneMannedFieldTurretDataTest.scala @@ -4,7 +4,7 @@ package game.objectcreate import net.psforever.packet.PacketCoding import net.psforever.packet.game.{ObjectCreateMessage, PlanetSideGUID} import net.psforever.packet.game.objectcreate._ -import net.psforever.types.PlanetSideEmpire +import net.psforever.types.{PlanetSideEmpire, Vector3} import org.specs2.mutable._ import scodec.bits._ @@ -22,23 +22,19 @@ class OneMannedFieldTurretDataTest extends Specification { data.isDefined mustEqual true data.get.isInstanceOf[OneMannedFieldTurretData] mustEqual true val omft = data.get.asInstanceOf[OneMannedFieldTurretData] - omft.deploy.pos.coord.x mustEqual 3567.1406f - omft.deploy.pos.coord.y mustEqual 2988.0078f - omft.deploy.pos.coord.z mustEqual 71.84375f - omft.deploy.pos.orient.x mustEqual 0f - omft.deploy.pos.orient.y mustEqual 0f - omft.deploy.pos.orient.z mustEqual 185.625f + omft.deploy.pos.coord mustEqual Vector3(3567.1406f, 2988.0078f, 71.84375f) + omft.deploy.pos.orient mustEqual Vector3(0, 0, 185.625f) omft.deploy.faction mustEqual PlanetSideEmpire.VS - omft.deploy.unk mustEqual 2 - omft.deploy.player_guid mustEqual PlanetSideGUID(2502) + omft.deploy.unk1 mustEqual 2 + omft.deploy.owner_guid mustEqual PlanetSideGUID(2502) omft.health mustEqual 255 omft.internals.isDefined mustEqual true - val internals = omft.internals.get - internals.objectClass mustEqual ObjectClass.energy_gun_vs - internals.guid mustEqual PlanetSideGUID(2615) - internals.parentSlot mustEqual 1 - internals.obj.isInstanceOf[WeaponData] mustEqual true - val wep = internals.obj.asInstanceOf[WeaponData] + val internals = omft.internals.get.contents + internals.head.objectClass mustEqual ObjectClass.energy_gun_vs + internals.head.guid mustEqual PlanetSideGUID(2615) + internals.head.parentSlot mustEqual 1 + internals.head.obj.isInstanceOf[WeaponData] mustEqual true + val wep = internals.head.obj.asInstanceOf[WeaponData] wep.unk1 mustEqual 0x6 wep.unk2 mustEqual 0x8 wep.fire_mode mustEqual 0 @@ -55,20 +51,16 @@ class OneMannedFieldTurretDataTest extends Specification { "encode (orion)" in { val obj = OneMannedFieldTurretData( - CommonFieldData( - PlacementData(3567.1406f, 2988.0078f, 71.84375f, 0f, 0f, 185.625f), - PlanetSideEmpire.VS, 2, PlanetSideGUID(2502) + SmallDeployableData( + PlacementData(Vector3(3567.1406f, 2988.0078f, 71.84375f), Vector3(0, 0, 185.625f)), + PlanetSideEmpire.VS, false, false, 2, false, false, PlanetSideGUID(2502) ), 255, - OneMannedFieldTurretData.orion(PlanetSideGUID(2615), 0x6, 0x8, PlanetSideGUID(2510), 8) + InventoryData(List(OneMannedFieldTurretData.orion(PlanetSideGUID(2615), 0x6, 0x8, PlanetSideGUID(2510), 8))) ) val msg = ObjectCreateMessage(ObjectClass.portable_manned_turret_vs, PlanetSideGUID(2916), obj) val pkt = PacketCoding.EncodePacket(msg).require.toByteVector - val pkt_bitv = pkt.toBitVector - val ori_bitv = string_orion.toBitVector - pkt_bitv.take(189) mustEqual ori_bitv.take(189) - pkt_bitv.drop(200) mustEqual ori_bitv.drop(200) - //TODO work on OneMannedFieldTurretData to make this pass as a single stream + pkt mustEqual string_orion } "avenger" in { diff --git a/common/src/test/scala/game/objectcreate/SmallDeployableDataTest.scala b/common/src/test/scala/game/objectcreate/SmallDeployableDataTest.scala index eb3ce947..6ce53364 100644 --- a/common/src/test/scala/game/objectcreate/SmallDeployableDataTest.scala +++ b/common/src/test/scala/game/objectcreate/SmallDeployableDataTest.scala @@ -4,7 +4,7 @@ package game.objectcreate import net.psforever.packet.PacketCoding import net.psforever.packet.game.{ObjectCreateMessage, PlanetSideGUID} import net.psforever.packet.game.objectcreate._ -import net.psforever.types.PlanetSideEmpire +import net.psforever.types.{PlanetSideEmpire, Vector3} import org.specs2.mutable._ import scodec.bits._ @@ -22,14 +22,14 @@ class SmallDeployableDataTest extends Specification { data.isDefined mustEqual true data.get.isInstanceOf[SmallDeployableData] mustEqual true val boomer = data.get.asInstanceOf[SmallDeployableData] - boomer.deploy.pos.coord.x mustEqual 4704.172f - boomer.deploy.pos.coord.y mustEqual 5546.4375f - boomer.deploy.pos.coord.z mustEqual 82.234375f - boomer.deploy.pos.orient.x mustEqual 0f - boomer.deploy.pos.orient.y mustEqual 0f - boomer.deploy.pos.orient.z mustEqual 272.8125f - boomer.deploy.unk mustEqual 0 - boomer.deploy.player_guid mustEqual PlanetSideGUID(4145) + boomer.pos.coord.x mustEqual 4704.172f + boomer.pos.coord.y mustEqual 5546.4375f + boomer.pos.coord.z mustEqual 82.234375f + boomer.pos.orient.x mustEqual 0f + boomer.pos.orient.y mustEqual 0f + boomer.pos.orient.z mustEqual 272.8125f + boomer.unk1 mustEqual 0 + boomer.owner_guid mustEqual PlanetSideGUID(8290) case _ => ko } @@ -37,10 +37,8 @@ class SmallDeployableDataTest extends Specification { "encode (boomer)" in { val obj = SmallDeployableData( - CommonFieldData( - PlacementData(4704.172f, 5546.4375f, 82.234375f, 0f, 0f, 272.8125f), - PlanetSideEmpire.TR, 0, PlanetSideGUID(4145) - ) + PlacementData(Vector3(4704.172f, 5546.4375f, 82.234375f), Vector3.z(272.8125f)), + PlanetSideEmpire.TR, false, false, 0, false, false, PlanetSideGUID(8290) ) val msg = ObjectCreateMessage(ObjectClass.boomer, PlanetSideGUID(3840), obj) val pkt = PacketCoding.EncodePacket(msg).require.toByteVector diff --git a/common/src/test/scala/game/objectcreate/SmallTurretDataTest.scala b/common/src/test/scala/game/objectcreate/SmallTurretDataTest.scala index a9ecdbac..ea0124d3 100644 --- a/common/src/test/scala/game/objectcreate/SmallTurretDataTest.scala +++ b/common/src/test/scala/game/objectcreate/SmallTurretDataTest.scala @@ -3,8 +3,8 @@ package game.objectcreate import net.psforever.packet.PacketCoding import net.psforever.packet.game.{ObjectCreateMessage, PlanetSideGUID} -import net.psforever.packet.game.objectcreate._ -import net.psforever.types.PlanetSideEmpire +import net.psforever.packet.game.objectcreate.{SmallDeployableData, _} +import net.psforever.types.{PlanetSideEmpire, Vector3} import org.specs2.mutable._ import scodec.bits._ @@ -23,16 +23,12 @@ class SmallTurretDataTest extends Specification { data.isDefined mustEqual true data.get.isInstanceOf[SmallTurretData] mustEqual true val turret = data.get.asInstanceOf[SmallTurretData] - turret.deploy.pos.coord.x mustEqual 4577.7812f - turret.deploy.pos.coord.y mustEqual 5624.828f - turret.deploy.pos.coord.z mustEqual 72.046875f - turret.deploy.pos.orient.x mustEqual 0f - turret.deploy.pos.orient.y mustEqual 2.8125f - turret.deploy.pos.orient.z mustEqual 264.375f + turret.deploy.pos.coord mustEqual Vector3(4577.7812f, 5624.828f, 72.046875f) + turret.deploy.pos.orient mustEqual Vector3(0, 2.8125f, 264.375f) turret.deploy.faction mustEqual PlanetSideEmpire.NC turret.deploy.destroyed mustEqual true - turret.deploy.unk mustEqual 2 - turret.deploy.player_guid mustEqual PlanetSideGUID(3871) + turret.deploy.unk1 mustEqual 2 + turret.deploy.owner_guid mustEqual PlanetSideGUID(7742) turret.health mustEqual 0 turret.internals.isDefined mustEqual false case _ => @@ -50,23 +46,19 @@ class SmallTurretDataTest extends Specification { data.isDefined mustEqual true data.get.isInstanceOf[SmallTurretData] mustEqual true val turret = data.get.asInstanceOf[SmallTurretData] - turret.deploy.pos.coord.x mustEqual 4527.633f - turret.deploy.pos.coord.y mustEqual 6271.3594f - turret.deploy.pos.coord.z mustEqual 70.265625f - turret.deploy.pos.orient.x mustEqual 0f - turret.deploy.pos.orient.y mustEqual 0f - turret.deploy.pos.orient.z mustEqual 154.6875f + turret.deploy.pos.coord mustEqual Vector3(4527.633f, 6271.3594f, 70.265625f) + turret.deploy.pos.orient mustEqual Vector3(0, 0, 154.6875f) turret.deploy.faction mustEqual PlanetSideEmpire.VS - turret.deploy.unk mustEqual 2 - turret.deploy.player_guid mustEqual PlanetSideGUID(4232) + turret.deploy.unk1 mustEqual 2 + turret.deploy.owner_guid mustEqual PlanetSideGUID(8208) turret.health mustEqual 255 turret.internals.isDefined mustEqual true - val internals = turret.internals.get - internals.objectClass mustEqual ObjectClass.spitfire_weapon - internals.guid mustEqual PlanetSideGUID(3064) - internals.parentSlot mustEqual 0 - internals.obj.isInstanceOf[WeaponData] mustEqual true - val wep = internals.obj.asInstanceOf[WeaponData] + val internals = turret.internals.get.contents + internals.head.objectClass mustEqual ObjectClass.spitfire_weapon + internals.head.guid mustEqual PlanetSideGUID(3064) + internals.head.parentSlot mustEqual 0 + internals.head.obj.isInstanceOf[WeaponData] mustEqual true + val wep = internals.head.obj.asInstanceOf[WeaponData] wep.unk1 mustEqual 0x6 wep.unk2 mustEqual 0x8 wep.fire_mode mustEqual 0 @@ -83,37 +75,29 @@ class SmallTurretDataTest extends Specification { "encode (spitfire, short)" in { val obj = SmallTurretData( - CommonFieldData( - PlacementData(4577.7812f, 5624.828f, 72.046875f, 0f, 2.8125f, 264.375f), - PlanetSideEmpire.NC, true, 2, PlanetSideGUID(3871) + SmallDeployableData( + PlacementData(Vector3(4577.7812f, 5624.828f, 72.046875f), Vector3(0, 2.8125f, 264.375f)), + PlanetSideEmpire.NC, false, true, 2, false, false, PlanetSideGUID(7742) ), 255 //sets to 0 ) val msg = ObjectCreateMessage(ObjectClass.spitfire_turret, PlanetSideGUID(4208), obj) val pkt = PacketCoding.EncodePacket(msg).require.toByteVector - val pkt_bitv = pkt.toBitVector - val ori_bitv = string_spitfire_short.toBitVector - pkt_bitv.take(173) mustEqual ori_bitv.take(173) - pkt_bitv.drop(185) mustEqual ori_bitv.drop(185) - //TODO work on SmallTurretData to make this pass as a single stream + pkt mustEqual string_spitfire_short } "encode (spitfire)" in { val obj = SmallTurretData( - CommonFieldData( - PlacementData(4527.633f, 6271.3594f, 70.265625f, 0f, 0f, 154.6875f), - PlanetSideEmpire.VS, 2, PlanetSideGUID(4232) + SmallDeployableData( + PlacementData(Vector3(4527.633f, 6271.3594f, 70.265625f), Vector3(0, 0, 154.6875f)), + PlanetSideEmpire.VS, false, false, 2, false, true, PlanetSideGUID(8208) ), 255, - SmallTurretData.spitfire(PlanetSideGUID(3064), 0x6, 0x8, PlanetSideGUID(3694), 8) + InventoryData(List(SmallTurretData.spitfire(PlanetSideGUID(3064), 0x6, 0x8, PlanetSideGUID(3694), 8))) ) val msg = ObjectCreateMessage(ObjectClass.spitfire_turret, PlanetSideGUID(4265), obj) val pkt = PacketCoding.EncodePacket(msg).require.toByteVector - val pkt_bitv = pkt.toBitVector - val ori_bitv = string_spitfire.toBitVector - pkt_bitv.take(173) mustEqual ori_bitv.take(173) - pkt_bitv.drop(185) mustEqual ori_bitv.drop(185) - //TODO work on SmallTurretData to make this pass as a single stream + pkt mustEqual string_spitfire } } } diff --git a/common/src/test/scala/game/objectcreate/TRAPDataTest.scala b/common/src/test/scala/game/objectcreate/TRAPDataTest.scala index 88fdf57a..b8b89b6a 100644 --- a/common/src/test/scala/game/objectcreate/TRAPDataTest.scala +++ b/common/src/test/scala/game/objectcreate/TRAPDataTest.scala @@ -4,7 +4,7 @@ package game.objectcreate import net.psforever.packet.PacketCoding import net.psforever.packet.game.{ObjectCreateMessage, PlanetSideGUID} import net.psforever.packet.game.objectcreate._ -import net.psforever.types.PlanetSideEmpire +import net.psforever.types.{PlanetSideEmpire, Vector3} import org.specs2.mutable._ import scodec.bits._ @@ -22,16 +22,11 @@ class TRAPDataTest extends Specification { data.isDefined mustEqual true data.get.isInstanceOf[TRAPData] mustEqual true val trap = data.get.asInstanceOf[TRAPData] - trap.deploy.pos.coord.x mustEqual 3572.4453f - trap.deploy.pos.coord.y mustEqual 3277.9766f - trap.deploy.pos.coord.z mustEqual 114.0f - trap.deploy.pos.orient.x mustEqual 0f - trap.deploy.pos.orient.y mustEqual 0f - trap.deploy.pos.orient.z mustEqual 90.0f + trap.deploy.pos.coord mustEqual Vector3(3572.4453f, 3277.9766f, 114.0f) + trap.deploy.pos.orient mustEqual Vector3(0, 0, 90) trap.deploy.faction mustEqual PlanetSideEmpire.VS - trap.deploy.unk mustEqual 2 + trap.deploy.owner_guid mustEqual PlanetSideGUID(4748) trap.health mustEqual 255 - trap.deploy.player_guid mustEqual PlanetSideGUID(2502) case _ => ko } @@ -39,9 +34,9 @@ class TRAPDataTest extends Specification { "encode" in { val obj = TRAPData( - CommonFieldData( + SmallDeployableData( PlacementData(3572.4453f, 3277.9766f, 114.0f, 0f, 0f, 90.0f), - PlanetSideEmpire.VS, 2, PlanetSideGUID(2502) + PlanetSideEmpire.VS, false, false, 2, false, true, PlanetSideGUID(4748) ), 255 ) diff --git a/common/src/test/scala/objects/ConverterTest.scala b/common/src/test/scala/objects/ConverterTest.scala index 7021914f..72e32608 100644 --- a/common/src/test/scala/objects/ConverterTest.scala +++ b/common/src/test/scala/objects/ConverterTest.scala @@ -1,15 +1,13 @@ // Copyright (c) 2017 PSForever package objects -import net.psforever.objects.definition.converter.{ACEConverter, CharacterSelectConverter, DestroyedVehicleConverter, REKConverter} +import net.psforever.objects.definition.converter.{CharacterSelectConverter, DestroyedVehicleConverter, 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.objects.serverobject.terminals.Terminal import net.psforever.objects.serverobject.tube.SpawnTube -import net.psforever.objects.vehicles.DestroyedVehicle import net.psforever.packet.game.PlanetSideGUID import net.psforever.packet.game.objectcreate._ import net.psforever.types.{CharacterGender, CharacterVoice, PlanetSideEmpire, Vector3} @@ -109,13 +107,7 @@ class ConverterTest extends Specification { "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(cdef) + val obj = ConstructionItem(GlobalDefinitions.ace) obj.GUID = PlanetSideGUID(90) obj.Definition.Packet.DetailedConstructorData(obj) match { case Success(pkt) => @@ -123,6 +115,7 @@ class ConverterTest extends Specification { case _ => ko } + obj.Definition.Packet.ConstructorData(obj) match { case Success(pkt) => pkt mustEqual ACEData(0,0) @@ -154,6 +147,158 @@ class ConverterTest extends Specification { } } + "BoomerTrigger" should { + "convert" in { + val obj = new BoomerTrigger + obj.GUID = PlanetSideGUID(90) + obj.Definition.Packet.DetailedConstructorData(obj) match { + case Success(pkt) => + pkt mustEqual DetailedBoomerTriggerData() + case _ => + ko + } + obj.Definition.Packet.ConstructorData(obj) match { + case Success(pkt) => + pkt mustEqual BoomerTriggerData() + case _ => + ko + } + } + } + + "SmallDeployable" should { + "convert" in { + val obj = new SensorDeployable(GlobalDefinitions.motionalarmsensor) + obj.Faction = PlanetSideEmpire.TR + obj.Definition.Packet.DetailedConstructorData(obj).isFailure mustEqual true + + obj.Definition.Packet.ConstructorData(obj) match { + case Success(pkt) => + pkt mustEqual SmallDeployableData( + PlacementData(Vector3.Zero, Vector3.Zero), + PlanetSideEmpire.TR, + 0, + false, + false + ) + case _ => + ko + } + } + } + + "SmallTurret" should { + "convert" in { + val obj = new TurretDeployable(GlobalDefinitions.spitfire_turret) + obj.Faction = PlanetSideEmpire.TR + obj.GUID = PlanetSideGUID(90) + obj.Weapons(1).Equipment.get.GUID = PlanetSideGUID(91) + obj.Weapons(1).Equipment.get.asInstanceOf[Tool].AmmoSlot.Box.GUID = PlanetSideGUID(92) + obj.Definition.Packet.DetailedConstructorData(obj).isFailure mustEqual true + + obj.Definition.Packet.ConstructorData(obj) match { + case Success(pkt) => + pkt mustEqual SmallTurretData( + SmallDeployableData( + PlacementData(Vector3.Zero, Vector3.Zero), + PlanetSideEmpire.TR, + 0, + false, + false + ), + 255, + InventoryData( + List(InternalSlot(ObjectClass.spitfire_weapon, PlanetSideGUID(91), 1, + WeaponData(4, 8, ObjectClass.spitfire_ammo, PlanetSideGUID(92), 0, AmmoBoxData())) + ) + ) + ) + case _ => + ko + } + } + } + + "FieldTurret" should { + "convert" in { + val obj = new TurretDeployable(GlobalDefinitions.portable_manned_turret_tr) + obj.Faction = PlanetSideEmpire.TR + obj.GUID = PlanetSideGUID(90) + obj.Weapons(1).Equipment.get.GUID = PlanetSideGUID(91) + obj.Weapons(1).Equipment.get.asInstanceOf[Tool].AmmoSlot.Box.GUID = PlanetSideGUID(92) + obj.Definition.Packet.DetailedConstructorData(obj).isFailure mustEqual true + + obj.Definition.Packet.ConstructorData(obj) match { + case Success(pkt) => + pkt mustEqual OneMannedFieldTurretData( + SmallDeployableData( + PlacementData(Vector3.Zero, Vector3.Zero), + PlanetSideEmpire.TR, + 0, + false, + false + ), + 255, + InventoryData( + List(InternalSlot(ObjectClass.energy_gun_tr, PlanetSideGUID(91), 1, + WeaponData(4, 8, ObjectClass.energy_gun_ammo, PlanetSideGUID(92), 0, AmmoBoxData())) + ) + ) + ) + case _ => + ko + } + } + } + + "TRAP" should { + "convert" in { + val obj = new TrapDeployable(GlobalDefinitions.tank_traps) + obj.Faction = PlanetSideEmpire.TR + obj.GUID = PlanetSideGUID(90) + obj.Definition.Packet.DetailedConstructorData(obj).isFailure mustEqual true + + obj.Definition.Packet.ConstructorData(obj) match { + case Success(pkt) => + pkt mustEqual TRAPData( + SmallDeployableData( + PlacementData(Vector3.Zero, Vector3.Zero), + PlanetSideEmpire.TR, + 0, + false, + false + ), + 255 + ) + case _ => + ko + } + } + } + + "ShieldGenerator" should { + "convert" in { + val obj = new ShieldGeneratorDeployable(GlobalDefinitions.deployable_shield_generator) + obj.Faction = PlanetSideEmpire.TR + obj.GUID = PlanetSideGUID(90) + obj.Definition.Packet.DetailedConstructorData(obj).isFailure mustEqual true + + obj.Definition.Packet.ConstructorData(obj) match { + case Success(pkt) => + pkt mustEqual AegisShieldGeneratorData( + CommonFieldData( + PlacementData(Vector3.Zero, Vector3.Zero), + PlanetSideEmpire.TR, + 0 + ), + 255 + ) + case _ => + ko + } + } + } + "Player" should { val avatar = Avatar("Chord", PlanetSideEmpire.TR, CharacterGender.Male, 0, CharacterVoice.Voice5) val obj : Player = { diff --git a/common/src/test/scala/objects/DeployableTest.scala b/common/src/test/scala/objects/DeployableTest.scala new file mode 100644 index 00000000..e79ac0d9 --- /dev/null +++ b/common/src/test/scala/objects/DeployableTest.scala @@ -0,0 +1,330 @@ +// Copyright (c) 2017 PSForever +package objects + +import akka.actor.{Actor, ActorRef, Props} +import base.ActorTest +import net.psforever.objects.ce.DeployedItem +import net.psforever.objects.serverobject.mount.Mountable +import net.psforever.objects.{TurretDeployable, _} +import net.psforever.packet.game.PlanetSideGUID +import net.psforever.types.{CharacterGender, CharacterVoice, PlanetSideEmpire} +import org.specs2.mutable.Specification + +import scala.concurrent.duration._ + +class DeployableTest extends Specification { + "Deployable" should { + "know its owner by GUID" in { + val obj = new ExplosiveDeployable(GlobalDefinitions.he_mine) + obj.Owner mustEqual None + obj.Owner = PlanetSideGUID(10) + obj.Owner mustEqual Some(PlanetSideGUID(10)) + } + + "know its owner by GUID" in { + val obj = new ExplosiveDeployable(GlobalDefinitions.he_mine) + obj.OwnerName mustEqual None + obj.OwnerName = "TestCharacter" + obj.OwnerName mustEqual Some("TestCharacter") + } + + "know its faction allegiance" in { + val obj = new ExplosiveDeployable(GlobalDefinitions.he_mine) + obj.Faction mustEqual PlanetSideEmpire.NEUTRAL + obj.Faction = PlanetSideEmpire.TR + obj.Faction mustEqual PlanetSideEmpire.TR + } + } +} + +class SensorDeployableTest extends Specification { + "SensorDeployable" should { + "construct" in { + new SensorDeployable(GlobalDefinitions.motionalarmsensor) + ok + } + } +} + +class ExplosiveDeployableTest extends Specification { + "ExplosiveDeployable" should { + "construct" in { + val obj = new ExplosiveDeployable(GlobalDefinitions.he_mine) + obj.Exploded mustEqual false + } + + "explode" in { + val obj = new ExplosiveDeployable(GlobalDefinitions.he_mine) + obj.Exploded mustEqual false + obj.Exploded = true + obj.Exploded mustEqual true + } + } +} + +class BoomerDeployableTest extends Specification { + "BoomerDeployable" should { + "construct" in { + val obj = new BoomerDeployable(GlobalDefinitions.boomer) + obj.Exploded mustEqual false + obj.Trigger mustEqual None + } + + "explode" in { + val obj = new BoomerDeployable(GlobalDefinitions.boomer) + obj.Exploded mustEqual false + obj.Exploded = true + obj.Exploded mustEqual true + } + + "manage its trigger" in { + val obj = new BoomerDeployable(GlobalDefinitions.boomer) + obj.Trigger mustEqual None + + val trigger = new BoomerTrigger + obj.Trigger = trigger + obj.Trigger mustEqual Some(trigger) + + obj.Trigger = None + obj.Trigger mustEqual None + } + } +} + +class TrapDeployableTest extends Specification { + "SensorDeployable" should { + "construct" in { + val obj = new TrapDeployable(GlobalDefinitions.tank_traps) + obj.Health mustEqual GlobalDefinitions.tank_traps.MaxHealth + } + + "update health values" in { + val obj = new TrapDeployable(GlobalDefinitions.tank_traps) + obj.Health mustEqual GlobalDefinitions.tank_traps.MaxHealth + obj.Health = 0 + obj.Health mustEqual 0 + } + } +} + +class TurretDeployableTest extends Specification { + "TurretDeployable" should { + "define (valid turret objects)" in { + List( + DeployedItem.spitfire_turret.id, DeployedItem.spitfire_cloaked.id, DeployedItem.spitfire_aa.id, + DeployedItem.portable_manned_turret.id, DeployedItem.portable_manned_turret_tr.id, + DeployedItem.portable_manned_turret_nc.id, DeployedItem.portable_manned_turret_vs.id + ).foreach(id => { + try { new TurretDeployableDefinition(id) } catch { case _ : Exception => ko } + }) + ok + } + + "define (invalid object)" in { + new TurretDeployableDefinition(5) must throwA[NoSuchElementException] //wrong object id altogether + } + + "construct" in { + val obj = new TurretDeployable(GlobalDefinitions.spitfire_turret) + obj.Health mustEqual obj.MaxHealth + } + + "update health values" in { + val obj = new TurretDeployable(GlobalDefinitions.spitfire_turret) + obj.Health mustEqual GlobalDefinitions.spitfire_turret.MaxHealth + obj.Health = 0 + obj.Health mustEqual 0 + } + + "may have mount point" in { + new TurretDeployable(GlobalDefinitions.spitfire_turret).MountPoints mustEqual Map() + new TurretDeployable(GlobalDefinitions.portable_manned_turret_vs).MountPoints mustEqual Map(1 -> 0, 2 -> 0) + } + } +} + +class ShieldGeneratorDeployableTest extends Specification { + "ShieldGeneratorDeployable" should { + "construct" in { + val obj = new ShieldGeneratorDeployable(GlobalDefinitions.deployable_shield_generator) + obj.Health mustEqual obj.MaxHealth + } + + "update health values" in { + val obj = new ShieldGeneratorDeployable(GlobalDefinitions.deployable_shield_generator) + obj.Health mustEqual GlobalDefinitions.deployable_shield_generator.MaxHealth + obj.Health = 0 + obj.Health mustEqual 0 + } + } +} + +class TurretControlConstructTest extends ActorTest { + "TurretControl" should { + "construct" in { + val obj = new TurretDeployable(GlobalDefinitions.spitfire_turret) + system.actorOf(Props(classOf[TurretControl], obj), s"${obj.Definition.Name}_test") + } + } +} + +class TurretControlInitializeTest extends ActorTest { + "TurretControl" should { + "initialize" in { + val obj = new TurretDeployable(GlobalDefinitions.spitfire_turret) + obj.GUID = PlanetSideGUID(1) + assert(obj.Actor == ActorRef.noSender) + val init = system.actorOf(Props(classOf[DeployableTest.TurretInitializer], obj), "init_turret_test") + init ! "initialize" + expectNoMsg(200 milliseconds) + assert(obj.Actor != ActorRef.noSender) + } + } +} + +class TurretControlUninitializeTest extends ActorTest { + "TurretControl" should { + "uninitialize" in { + val obj = new TurretDeployable(GlobalDefinitions.spitfire_turret) + val init = system.actorOf(Props(classOf[DeployableTest.TurretInitializer], obj), "init_turret_test") + obj.GUID = PlanetSideGUID(1) + init ! "initialize" + expectNoMsg(200 milliseconds) + assert(obj.Actor != ActorRef.noSender) + + init ! "uninitialize" + expectNoMsg(200 milliseconds) + assert(obj.Actor == ActorRef.noSender) + } + } +} + +class TurretControlMountTest extends ActorTest { + "TurretControl" should { + "control mounting" in { + val obj = new TurretDeployable(GlobalDefinitions.portable_manned_turret_tr) { GUID = PlanetSideGUID(1) } + obj.Faction = PlanetSideEmpire.TR + obj.Actor = system.actorOf(Props(classOf[TurretControl], obj), s"${obj.Definition.Name}_test") + + assert(obj.Seats(0).Occupant.isEmpty) + val player1 = Player(Avatar("test1", PlanetSideEmpire.TR, CharacterGender.Male, 0, CharacterVoice.Mute)) + obj.Actor ! Mountable.TryMount(player1, 0) + val reply1a = receiveOne(200 milliseconds) + assert(reply1a.isInstanceOf[Mountable.MountMessages]) + val reply1b = reply1a.asInstanceOf[Mountable.MountMessages] + assert(reply1b.player == player1) + assert(reply1b.response.isInstanceOf[Mountable.CanMount]) + assert(obj.Seats(0).Occupant.contains(player1)) + } + } +} + +class TurretControlBlockMountTest extends ActorTest { + "TurretControl" should { + "block mounting by others if already mounted by someone" in { + val obj = new TurretDeployable(GlobalDefinitions.portable_manned_turret_tr) { GUID = PlanetSideGUID(1) } + obj.Faction = PlanetSideEmpire.TR + obj.Actor = system.actorOf(Props(classOf[TurretControl], obj), s"${obj.Definition.Name}_test") + + assert(obj.Seats(0).Occupant.isEmpty) + val player1 = Player(Avatar("test1", PlanetSideEmpire.TR, CharacterGender.Male, 0, CharacterVoice.Mute)) + obj.Actor ! Mountable.TryMount(player1, 0) + val reply1a = receiveOne(200 milliseconds) + assert(reply1a.isInstanceOf[Mountable.MountMessages]) + val reply1b = reply1a.asInstanceOf[Mountable.MountMessages] + assert(reply1b.player == player1) + assert(reply1b.response.isInstanceOf[Mountable.CanMount]) + assert(obj.Seats(0).Occupant.contains(player1)) + + val player2 = Player(Avatar("test2", PlanetSideEmpire.TR, CharacterGender.Male, 0, CharacterVoice.Mute)) + obj.Actor ! Mountable.TryMount(player2, 0) + val reply2a = receiveOne(200 milliseconds) + assert(reply2a.isInstanceOf[Mountable.MountMessages]) + val reply2b = reply2a.asInstanceOf[Mountable.MountMessages] + assert(reply2b.player == player2) + assert(reply2b.response.isInstanceOf[Mountable.CanNotMount]) + } + } +} + +class TurretControlBlockBetrayalMountTest extends ActorTest { + "TurretControl" should { + "block mounting by players of another faction" in { + val obj = new TurretDeployable(GlobalDefinitions.portable_manned_turret_tr) { GUID = PlanetSideGUID(1) } + obj.Faction = PlanetSideEmpire.TR + obj.Actor = system.actorOf(Props(classOf[TurretControl], obj), s"${obj.Definition.Name}_test") + + assert(obj.Seats(0).Occupant.isEmpty) + val player = Player(Avatar("test", PlanetSideEmpire.VS, CharacterGender.Male, 0, CharacterVoice.Mute)) + obj.Actor ! Mountable.TryMount(player, 0) + val reply1a = receiveOne(200 milliseconds) + assert(reply1a.isInstanceOf[Mountable.MountMessages]) + val reply1b = reply1a.asInstanceOf[Mountable.MountMessages] + assert(reply1b.player == player) + assert(reply1b.response.isInstanceOf[Mountable.CanNotMount]) + assert(obj.Seats(0).Occupant.isEmpty) + } + } +} + +class TurretControlDismountTest extends ActorTest { + "TurretControl" should { + "control dismounting" in { + val obj = new TurretDeployable(GlobalDefinitions.portable_manned_turret_tr) { GUID = PlanetSideGUID(1) } + obj.Faction = PlanetSideEmpire.TR + obj.Actor = system.actorOf(Props(classOf[TurretControl], obj), s"${obj.Definition.Name}_test") + + assert(obj.Seats(0).Occupant.isEmpty) + val player = Player(Avatar("test", PlanetSideEmpire.TR, CharacterGender.Male, 0, CharacterVoice.Mute)) + obj.Actor ! Mountable.TryMount(player, 0) + val reply1a = receiveOne(200 milliseconds) + assert(reply1a.isInstanceOf[Mountable.MountMessages]) + val reply1b = reply1a.asInstanceOf[Mountable.MountMessages] + assert(reply1b.player == player) + assert(reply1b.response.isInstanceOf[Mountable.CanMount]) + assert(obj.Seats(0).Occupant.contains(player)) + + obj.Actor ! Mountable.TryDismount(player, 0) + val reply2a = receiveOne(200 milliseconds) + assert(reply2a.isInstanceOf[Mountable.MountMessages]) + val reply2b = reply2a.asInstanceOf[Mountable.MountMessages] + assert(reply2b.player == player) + assert(reply2b.response.isInstanceOf[Mountable.CanDismount]) + assert(obj.Seats(0).Occupant.isEmpty) + } + } +} + +class TurretControlBetrayalMountTest extends ActorTest { + "TurretControl" should { + "allow all allegiances" in { + val obj = new TurretDeployable( + new TurretDeployableDefinition(685) { FactionLocked = false } //required (defaults to true) + ) { GUID = PlanetSideGUID(1) } + obj.Faction = PlanetSideEmpire.TR + obj.Actor = system.actorOf(Props(classOf[TurretControl], obj), s"${obj.Definition.Name}_test") + + assert(obj.Seats(0).Occupant.isEmpty) + val player = Player(Avatar("test", PlanetSideEmpire.NC, CharacterGender.Male, 0, CharacterVoice.Mute)) + assert(player.Faction != obj.Faction) + obj.Actor ! Mountable.TryMount(player, 0) + val reply1a = receiveOne(200 milliseconds) + assert(reply1a.isInstanceOf[Mountable.MountMessages]) + val reply1b = reply1a.asInstanceOf[Mountable.MountMessages] + assert(reply1b.player == player) + assert(reply1b.response.isInstanceOf[Mountable.CanMount]) + assert(obj.Seats(0).Occupant.contains(player)) + } + } +} + +object DeployableTest { + class TurretInitializer(obj : TurretDeployable) extends Actor { + def receive : Receive = { + case "initialize" => + obj.Definition.Initialize(obj, context) + case "uninitialize" => + obj.Definition.Uninitialize(obj, context) + } + } +} diff --git a/common/src/test/scala/objects/DeployableToolboxTest.scala b/common/src/test/scala/objects/DeployableToolboxTest.scala new file mode 100644 index 00000000..afc96888 --- /dev/null +++ b/common/src/test/scala/objects/DeployableToolboxTest.scala @@ -0,0 +1,1054 @@ +// Copyright (c) 2017 PSForever +package objects + +import net.psforever.objects._ +import net.psforever.objects.avatar.DeployableToolbox +import net.psforever.objects.ce.{DeployableCategory, DeployedItem} +import net.psforever.packet.game.PlanetSideGUID +import net.psforever.types.CertificationType._ +import org.specs2.mutable.Specification + +class DeployableToolboxTest extends Specification { + "DeployableToolbbox" should { + "construct" in { + new DeployableToolbox //should just construct without issue + ok + } + + "initialization (default)" in { + val obj = new DeployableToolbox + obj.Initialize(Set()) + val list = obj.UpdateUI() + list.size mustEqual DeployedItem.values.size - 3 //extra field turrets + list.foreach({case(_,curr,_,max) => + curr mustEqual 0 + max mustEqual 0 + }) + ok + } + + "initialization (CombatEngineering)" in { + val obj = new DeployableToolbox + obj.Initialize(Set(CombatEngineering)) + obj.CountDeployable(DeployedItem.boomer)._2 mustEqual 20 + obj.CountDeployable(DeployedItem.he_mine)._2 mustEqual 20 + obj.CountDeployable(DeployedItem.spitfire_turret)._2 mustEqual 10 + obj.CountDeployable(DeployedItem.motionalarmsensor)._2 mustEqual 20 + obj.CountDeployable(DeployedItem.jammer_mine)._2 mustEqual 0 + obj.CountDeployable(DeployedItem.spitfire_cloaked)._2 mustEqual 0 + obj.CountDeployable(DeployedItem.spitfire_aa)._2 mustEqual 0 + obj.CountDeployable(DeployedItem.sensor_shield)._2 mustEqual 0 + obj.CountDeployable(DeployedItem.tank_traps)._2 mustEqual 0 + obj.CountDeployable(DeployedItem.portable_manned_turret)._2 mustEqual 0 + obj.CountDeployable(DeployedItem.portable_manned_turret_nc)._2 mustEqual 0 + obj.CountDeployable(DeployedItem.portable_manned_turret_tr)._2 mustEqual 0 + obj.CountDeployable(DeployedItem.portable_manned_turret_vs)._2 mustEqual 0 + obj.CountDeployable(DeployedItem.deployable_shield_generator)._2 mustEqual 0 + obj.CountDeployable(DeployedItem.router_telepad_deployable)._2 mustEqual 0 + } + + "initialization (AssaultEngineering)" in { + val obj = new DeployableToolbox + obj.Initialize(Set(CombatEngineering, AssaultEngineering)) + obj.CountDeployable(DeployedItem.boomer)._2 mustEqual 20 + obj.CountDeployable(DeployedItem.he_mine)._2 mustEqual 20 + obj.CountDeployable(DeployedItem.spitfire_turret)._2 mustEqual 10 + obj.CountDeployable(DeployedItem.motionalarmsensor)._2 mustEqual 20 + obj.CountDeployable(DeployedItem.jammer_mine)._2 mustEqual 20 + obj.CountDeployable(DeployedItem.spitfire_cloaked)._2 mustEqual 0 + obj.CountDeployable(DeployedItem.spitfire_aa)._2 mustEqual 0 + obj.CountDeployable(DeployedItem.sensor_shield)._2 mustEqual 0 + obj.CountDeployable(DeployedItem.tank_traps)._2 mustEqual 0 + obj.CountDeployable(DeployedItem.portable_manned_turret)._2 mustEqual 1 + obj.CountDeployable(DeployedItem.portable_manned_turret_nc)._2 mustEqual 1 + obj.CountDeployable(DeployedItem.portable_manned_turret_tr)._2 mustEqual 1 + obj.CountDeployable(DeployedItem.portable_manned_turret_vs)._2 mustEqual 1 + obj.CountDeployable(DeployedItem.deployable_shield_generator)._2 mustEqual 1 + obj.CountDeployable(DeployedItem.router_telepad_deployable)._2 mustEqual 0 + } + + "initialization (FortificationEngineering)" in { + val obj = new DeployableToolbox + obj.Initialize(Set(CombatEngineering, FortificationEngineering)) + obj.CountDeployable(DeployedItem.boomer)._2 mustEqual 25 + obj.CountDeployable(DeployedItem.he_mine)._2 mustEqual 25 + obj.CountDeployable(DeployedItem.spitfire_turret)._2 mustEqual 15 + obj.CountDeployable(DeployedItem.motionalarmsensor)._2 mustEqual 25 + obj.CountDeployable(DeployedItem.jammer_mine)._2 mustEqual 0 + obj.CountDeployable(DeployedItem.spitfire_cloaked)._2 mustEqual 5 + obj.CountDeployable(DeployedItem.spitfire_aa)._2 mustEqual 5 + obj.CountDeployable(DeployedItem.sensor_shield)._2 mustEqual 0 + obj.CountDeployable(DeployedItem.tank_traps)._2 mustEqual 5 + obj.CountDeployable(DeployedItem.portable_manned_turret)._2 mustEqual 0 + obj.CountDeployable(DeployedItem.portable_manned_turret_nc)._2 mustEqual 0 + obj.CountDeployable(DeployedItem.portable_manned_turret_tr)._2 mustEqual 0 + obj.CountDeployable(DeployedItem.portable_manned_turret_vs)._2 mustEqual 0 + obj.CountDeployable(DeployedItem.deployable_shield_generator)._2 mustEqual 0 + obj.CountDeployable(DeployedItem.router_telepad_deployable)._2 mustEqual 0 + } + + "initialization (AdvancedEngineering)" in { + val obj = new DeployableToolbox + obj.Initialize(Set(AdvancedEngineering)) + obj.CountDeployable(DeployedItem.boomer)._2 mustEqual 25 + obj.CountDeployable(DeployedItem.he_mine)._2 mustEqual 25 + obj.CountDeployable(DeployedItem.spitfire_turret)._2 mustEqual 15 + obj.CountDeployable(DeployedItem.motionalarmsensor)._2 mustEqual 25 + obj.CountDeployable(DeployedItem.jammer_mine)._2 mustEqual 20 + obj.CountDeployable(DeployedItem.spitfire_cloaked)._2 mustEqual 5 + obj.CountDeployable(DeployedItem.spitfire_aa)._2 mustEqual 5 + obj.CountDeployable(DeployedItem.sensor_shield)._2 mustEqual 0 + obj.CountDeployable(DeployedItem.tank_traps)._2 mustEqual 5 + obj.CountDeployable(DeployedItem.portable_manned_turret)._2 mustEqual 1 + obj.CountDeployable(DeployedItem.portable_manned_turret_nc)._2 mustEqual 1 + obj.CountDeployable(DeployedItem.portable_manned_turret_tr)._2 mustEqual 1 + obj.CountDeployable(DeployedItem.portable_manned_turret_vs)._2 mustEqual 1 + obj.CountDeployable(DeployedItem.deployable_shield_generator)._2 mustEqual 1 + obj.CountDeployable(DeployedItem.router_telepad_deployable)._2 mustEqual 0 + } + + "initialization (AdvancedHacking)" in { + val obj = new DeployableToolbox + obj.Initialize(Set(CombatEngineering, AdvancedHacking)) + obj.CountDeployable(DeployedItem.boomer)._2 mustEqual 20 + obj.CountDeployable(DeployedItem.he_mine)._2 mustEqual 20 + obj.CountDeployable(DeployedItem.spitfire_turret)._2 mustEqual 10 + obj.CountDeployable(DeployedItem.motionalarmsensor)._2 mustEqual 20 + obj.CountDeployable(DeployedItem.jammer_mine)._2 mustEqual 0 + obj.CountDeployable(DeployedItem.spitfire_cloaked)._2 mustEqual 0 + obj.CountDeployable(DeployedItem.spitfire_aa)._2 mustEqual 0 + obj.CountDeployable(DeployedItem.sensor_shield)._2 mustEqual 20 + obj.CountDeployable(DeployedItem.tank_traps)._2 mustEqual 0 + obj.CountDeployable(DeployedItem.portable_manned_turret)._2 mustEqual 0 + obj.CountDeployable(DeployedItem.portable_manned_turret_nc)._2 mustEqual 0 + obj.CountDeployable(DeployedItem.portable_manned_turret_tr)._2 mustEqual 0 + obj.CountDeployable(DeployedItem.portable_manned_turret_vs)._2 mustEqual 0 + obj.CountDeployable(DeployedItem.deployable_shield_generator)._2 mustEqual 0 + obj.CountDeployable(DeployedItem.router_telepad_deployable)._2 mustEqual 0 + } + + "initialization (without CombatEngineering)" in { + val obj = new DeployableToolbox + obj.Initialize(Set(AssaultEngineering, FortificationEngineering, AdvancedHacking)) + obj.CountDeployable(DeployedItem.boomer)._2 mustEqual 0 + obj.CountDeployable(DeployedItem.he_mine)._2 mustEqual 0 + obj.CountDeployable(DeployedItem.spitfire_turret)._2 mustEqual 0 + obj.CountDeployable(DeployedItem.motionalarmsensor)._2 mustEqual 0 + obj.CountDeployable(DeployedItem.jammer_mine)._2 mustEqual 0 + obj.CountDeployable(DeployedItem.spitfire_cloaked)._2 mustEqual 0 + obj.CountDeployable(DeployedItem.spitfire_aa)._2 mustEqual 0 + obj.CountDeployable(DeployedItem.sensor_shield)._2 mustEqual 0 + obj.CountDeployable(DeployedItem.tank_traps)._2 mustEqual 0 + obj.CountDeployable(DeployedItem.portable_manned_turret)._2 mustEqual 0 + obj.CountDeployable(DeployedItem.portable_manned_turret_nc)._2 mustEqual 0 + obj.CountDeployable(DeployedItem.portable_manned_turret_tr)._2 mustEqual 0 + obj.CountDeployable(DeployedItem.portable_manned_turret_vs)._2 mustEqual 0 + obj.CountDeployable(DeployedItem.deployable_shield_generator)._2 mustEqual 0 + obj.CountDeployable(DeployedItem.router_telepad_deployable)._2 mustEqual 0 + } + + "initialization (GroundSupport)" in { + val obj = new DeployableToolbox + obj.Initialize(Set(GroundSupport)) + obj.CountDeployable(DeployedItem.boomer)._2 mustEqual 0 + obj.CountDeployable(DeployedItem.he_mine)._2 mustEqual 0 + obj.CountDeployable(DeployedItem.spitfire_turret)._2 mustEqual 0 + obj.CountDeployable(DeployedItem.motionalarmsensor)._2 mustEqual 0 + obj.CountDeployable(DeployedItem.jammer_mine)._2 mustEqual 0 + obj.CountDeployable(DeployedItem.spitfire_cloaked)._2 mustEqual 0 + obj.CountDeployable(DeployedItem.spitfire_aa)._2 mustEqual 0 + obj.CountDeployable(DeployedItem.sensor_shield)._2 mustEqual 0 + obj.CountDeployable(DeployedItem.tank_traps)._2 mustEqual 0 + obj.CountDeployable(DeployedItem.portable_manned_turret)._2 mustEqual 0 + obj.CountDeployable(DeployedItem.portable_manned_turret_nc)._2 mustEqual 0 + obj.CountDeployable(DeployedItem.portable_manned_turret_tr)._2 mustEqual 0 + obj.CountDeployable(DeployedItem.portable_manned_turret_vs)._2 mustEqual 0 + obj.CountDeployable(DeployedItem.deployable_shield_generator)._2 mustEqual 0 + } + + "can not initialize twice" in { + val obj = new DeployableToolbox + obj.Initialize(Set()) mustEqual true + obj.CountDeployable(DeployedItem.boomer)._2 mustEqual 0 + obj.CountDeployable(DeployedItem.he_mine)._2 mustEqual 0 + obj.CountDeployable(DeployedItem.spitfire_turret)._2 mustEqual 0 + obj.CountDeployable(DeployedItem.motionalarmsensor)._2 mustEqual 0 + obj.CountDeployable(DeployedItem.jammer_mine)._2 mustEqual 0 + obj.CountDeployable(DeployedItem.spitfire_cloaked)._2 mustEqual 0 + obj.CountDeployable(DeployedItem.spitfire_aa)._2 mustEqual 0 + obj.CountDeployable(DeployedItem.sensor_shield)._2 mustEqual 0 + obj.CountDeployable(DeployedItem.tank_traps)._2 mustEqual 0 + obj.CountDeployable(DeployedItem.portable_manned_turret)._2 mustEqual 0 + obj.CountDeployable(DeployedItem.portable_manned_turret_nc)._2 mustEqual 0 + obj.CountDeployable(DeployedItem.portable_manned_turret_tr)._2 mustEqual 0 + obj.CountDeployable(DeployedItem.portable_manned_turret_vs)._2 mustEqual 0 + obj.CountDeployable(DeployedItem.deployable_shield_generator)._2 mustEqual 0 + obj.CountDeployable(DeployedItem.router_telepad_deployable)._2 mustEqual 0 + + obj.Initialize(Set(AdvancedEngineering)) mustEqual false + obj.CountDeployable(DeployedItem.boomer)._2 mustEqual 0 + obj.CountDeployable(DeployedItem.he_mine)._2 mustEqual 0 + obj.CountDeployable(DeployedItem.spitfire_turret)._2 mustEqual 0 + obj.CountDeployable(DeployedItem.motionalarmsensor)._2 mustEqual 0 + obj.CountDeployable(DeployedItem.jammer_mine)._2 mustEqual 0 + obj.CountDeployable(DeployedItem.spitfire_cloaked)._2 mustEqual 0 + obj.CountDeployable(DeployedItem.spitfire_aa)._2 mustEqual 0 + obj.CountDeployable(DeployedItem.sensor_shield)._2 mustEqual 0 + obj.CountDeployable(DeployedItem.tank_traps)._2 mustEqual 0 + obj.CountDeployable(DeployedItem.portable_manned_turret)._2 mustEqual 0 + obj.CountDeployable(DeployedItem.portable_manned_turret_nc)._2 mustEqual 0 + obj.CountDeployable(DeployedItem.portable_manned_turret_tr)._2 mustEqual 0 + obj.CountDeployable(DeployedItem.portable_manned_turret_vs)._2 mustEqual 0 + obj.CountDeployable(DeployedItem.deployable_shield_generator)._2 mustEqual 0 + obj.CountDeployable(DeployedItem.router_telepad_deployable)._2 mustEqual 0 + } + + "uninitialized fields can not accept deployables" in { + val obj = new DeployableToolbox + obj.CountDeployable(DeployedItem.boomer)._2 mustEqual 0 + val boomer = new BoomerDeployable(GlobalDefinitions.boomer) + obj.Accept(boomer) mustEqual false + obj.Add(boomer) mustEqual false + obj.CountDeployable(DeployedItem.boomer)._2 mustEqual 0 + } + + "only initialized fields can accept deployables" in { + val obj = new DeployableToolbox + obj.CountDeployable(DeployedItem.boomer)._2 mustEqual 0 + obj.Initialize(Set(CombatEngineering)) + obj.CountDeployable(DeployedItem.boomer)._2 mustEqual 20 + val boomer = new BoomerDeployable(GlobalDefinitions.boomer) + obj.Accept(boomer) mustEqual true + obj.Add(boomer) mustEqual true + obj.CountDeployable(DeployedItem.boomer).productIterator.toList mustEqual List(1, 20) + } + + "change accessible fields by adding by certification type (CombatEngineering ...)" in { + val obj = new DeployableToolbox + obj.Initialize(Set()) + obj.CountDeployable(DeployedItem.boomer)._2 mustEqual 0 + obj.CountDeployable(DeployedItem.he_mine)._2 mustEqual 0 + obj.CountDeployable(DeployedItem.spitfire_turret)._2 mustEqual 0 + obj.CountDeployable(DeployedItem.motionalarmsensor)._2 mustEqual 0 + obj.CountDeployable(DeployedItem.jammer_mine)._2 mustEqual 0 + obj.CountDeployable(DeployedItem.spitfire_cloaked)._2 mustEqual 0 + obj.CountDeployable(DeployedItem.spitfire_aa)._2 mustEqual 0 + obj.CountDeployable(DeployedItem.sensor_shield)._2 mustEqual 0 + obj.CountDeployable(DeployedItem.tank_traps)._2 mustEqual 0 + obj.CountDeployable(DeployedItem.portable_manned_turret)._2 mustEqual 0 + obj.CountDeployable(DeployedItem.portable_manned_turret_nc)._2 mustEqual 0 + obj.CountDeployable(DeployedItem.portable_manned_turret_tr)._2 mustEqual 0 + obj.CountDeployable(DeployedItem.portable_manned_turret_vs)._2 mustEqual 0 + obj.CountDeployable(DeployedItem.deployable_shield_generator)._2 mustEqual 0 + obj.CountDeployable(DeployedItem.router_telepad_deployable)._2 mustEqual 0 + + obj.AddToDeployableQuantities( + CombatEngineering, + Set(CombatEngineering) + ) + obj.CountDeployable(DeployedItem.boomer)._2 mustEqual 20 + obj.CountDeployable(DeployedItem.he_mine)._2 mustEqual 20 + obj.CountDeployable(DeployedItem.spitfire_turret)._2 mustEqual 10 + obj.CountDeployable(DeployedItem.motionalarmsensor)._2 mustEqual 20 + obj.CountDeployable(DeployedItem.jammer_mine)._2 mustEqual 0 + obj.CountDeployable(DeployedItem.spitfire_cloaked)._2 mustEqual 0 + obj.CountDeployable(DeployedItem.spitfire_aa)._2 mustEqual 0 + obj.CountDeployable(DeployedItem.sensor_shield)._2 mustEqual 0 + obj.CountDeployable(DeployedItem.tank_traps)._2 mustEqual 0 + obj.CountDeployable(DeployedItem.portable_manned_turret)._2 mustEqual 0 + obj.CountDeployable(DeployedItem.portable_manned_turret_nc)._2 mustEqual 0 + obj.CountDeployable(DeployedItem.portable_manned_turret_tr)._2 mustEqual 0 + obj.CountDeployable(DeployedItem.portable_manned_turret_vs)._2 mustEqual 0 + obj.CountDeployable(DeployedItem.deployable_shield_generator)._2 mustEqual 0 + obj.CountDeployable(DeployedItem.router_telepad_deployable)._2 mustEqual 0 + + obj.AddToDeployableQuantities( + FortificationEngineering, + Set(CombatEngineering, FortificationEngineering) + ) + obj.CountDeployable(DeployedItem.boomer)._2 mustEqual 25 + obj.CountDeployable(DeployedItem.he_mine)._2 mustEqual 25 + obj.CountDeployable(DeployedItem.spitfire_turret)._2 mustEqual 15 + obj.CountDeployable(DeployedItem.motionalarmsensor)._2 mustEqual 25 + obj.CountDeployable(DeployedItem.jammer_mine)._2 mustEqual 0 + obj.CountDeployable(DeployedItem.spitfire_cloaked)._2 mustEqual 5 + obj.CountDeployable(DeployedItem.spitfire_aa)._2 mustEqual 5 + obj.CountDeployable(DeployedItem.sensor_shield)._2 mustEqual 0 + obj.CountDeployable(DeployedItem.tank_traps)._2 mustEqual 5 + obj.CountDeployable(DeployedItem.portable_manned_turret)._2 mustEqual 0 + obj.CountDeployable(DeployedItem.portable_manned_turret_nc)._2 mustEqual 0 + obj.CountDeployable(DeployedItem.portable_manned_turret_tr)._2 mustEqual 0 + obj.CountDeployable(DeployedItem.portable_manned_turret_vs)._2 mustEqual 0 + obj.CountDeployable(DeployedItem.deployable_shield_generator)._2 mustEqual 0 + obj.CountDeployable(DeployedItem.router_telepad_deployable)._2 mustEqual 0 + + obj.AddToDeployableQuantities( + AssaultEngineering, + Set(CombatEngineering, FortificationEngineering, AssaultEngineering) + ) + obj.CountDeployable(DeployedItem.boomer)._2 mustEqual 25 + obj.CountDeployable(DeployedItem.he_mine)._2 mustEqual 25 + obj.CountDeployable(DeployedItem.spitfire_turret)._2 mustEqual 15 + obj.CountDeployable(DeployedItem.motionalarmsensor)._2 mustEqual 25 + obj.CountDeployable(DeployedItem.jammer_mine)._2 mustEqual 20 + obj.CountDeployable(DeployedItem.spitfire_cloaked)._2 mustEqual 5 + obj.CountDeployable(DeployedItem.spitfire_aa)._2 mustEqual 5 + obj.CountDeployable(DeployedItem.sensor_shield)._2 mustEqual 0 + obj.CountDeployable(DeployedItem.tank_traps)._2 mustEqual 5 + obj.CountDeployable(DeployedItem.portable_manned_turret)._2 mustEqual 1 + obj.CountDeployable(DeployedItem.portable_manned_turret_nc)._2 mustEqual 1 + obj.CountDeployable(DeployedItem.portable_manned_turret_tr)._2 mustEqual 1 + obj.CountDeployable(DeployedItem.portable_manned_turret_vs)._2 mustEqual 1 + obj.CountDeployable(DeployedItem.deployable_shield_generator)._2 mustEqual 1 + obj.CountDeployable(DeployedItem.router_telepad_deployable)._2 mustEqual 0 + + obj.AddToDeployableQuantities( + AssaultEngineering, + Set(CombatEngineering, FortificationEngineering, AssaultEngineering) + ) + obj.CountDeployable(DeployedItem.boomer)._2 mustEqual 25 + obj.CountDeployable(DeployedItem.he_mine)._2 mustEqual 25 + obj.CountDeployable(DeployedItem.spitfire_turret)._2 mustEqual 15 + obj.CountDeployable(DeployedItem.motionalarmsensor)._2 mustEqual 25 + obj.CountDeployable(DeployedItem.jammer_mine)._2 mustEqual 20 + obj.CountDeployable(DeployedItem.spitfire_cloaked)._2 mustEqual 5 + obj.CountDeployable(DeployedItem.spitfire_aa)._2 mustEqual 5 + obj.CountDeployable(DeployedItem.sensor_shield)._2 mustEqual 0 + obj.CountDeployable(DeployedItem.tank_traps)._2 mustEqual 5 + obj.CountDeployable(DeployedItem.portable_manned_turret)._2 mustEqual 1 + obj.CountDeployable(DeployedItem.portable_manned_turret_nc)._2 mustEqual 1 + obj.CountDeployable(DeployedItem.portable_manned_turret_tr)._2 mustEqual 1 + obj.CountDeployable(DeployedItem.portable_manned_turret_vs)._2 mustEqual 1 + obj.CountDeployable(DeployedItem.deployable_shield_generator)._2 mustEqual 1 + obj.CountDeployable(DeployedItem.router_telepad_deployable)._2 mustEqual 0 + + obj.AddToDeployableQuantities( + AdvancedHacking, + Set(CombatEngineering, FortificationEngineering, AssaultEngineering, AdvancedHacking) + ) + obj.CountDeployable(DeployedItem.boomer)._2 mustEqual 25 + obj.CountDeployable(DeployedItem.he_mine)._2 mustEqual 25 + obj.CountDeployable(DeployedItem.spitfire_turret)._2 mustEqual 15 + obj.CountDeployable(DeployedItem.motionalarmsensor)._2 mustEqual 25 + obj.CountDeployable(DeployedItem.jammer_mine)._2 mustEqual 20 + obj.CountDeployable(DeployedItem.spitfire_cloaked)._2 mustEqual 5 + obj.CountDeployable(DeployedItem.spitfire_aa)._2 mustEqual 5 + obj.CountDeployable(DeployedItem.sensor_shield)._2 mustEqual 20 + obj.CountDeployable(DeployedItem.tank_traps)._2 mustEqual 5 + obj.CountDeployable(DeployedItem.portable_manned_turret)._2 mustEqual 1 + obj.CountDeployable(DeployedItem.portable_manned_turret_nc)._2 mustEqual 1 + obj.CountDeployable(DeployedItem.portable_manned_turret_tr)._2 mustEqual 1 + obj.CountDeployable(DeployedItem.portable_manned_turret_vs)._2 mustEqual 1 + obj.CountDeployable(DeployedItem.deployable_shield_generator)._2 mustEqual 1 + obj.CountDeployable(DeployedItem.router_telepad_deployable)._2 mustEqual 0 + } + + "change accessible fields by adding by certification type (GroundSupport)" in { + val obj = new DeployableToolbox + obj.Initialize(Set()) + obj.CountDeployable(DeployedItem.boomer)._2 mustEqual 0 + obj.CountDeployable(DeployedItem.he_mine)._2 mustEqual 0 + obj.CountDeployable(DeployedItem.spitfire_turret)._2 mustEqual 0 + obj.CountDeployable(DeployedItem.motionalarmsensor)._2 mustEqual 0 + obj.CountDeployable(DeployedItem.jammer_mine)._2 mustEqual 0 + obj.CountDeployable(DeployedItem.spitfire_cloaked)._2 mustEqual 0 + obj.CountDeployable(DeployedItem.spitfire_aa)._2 mustEqual 0 + obj.CountDeployable(DeployedItem.sensor_shield)._2 mustEqual 0 + obj.CountDeployable(DeployedItem.tank_traps)._2 mustEqual 0 + obj.CountDeployable(DeployedItem.portable_manned_turret)._2 mustEqual 0 + obj.CountDeployable(DeployedItem.portable_manned_turret_nc)._2 mustEqual 0 + obj.CountDeployable(DeployedItem.portable_manned_turret_tr)._2 mustEqual 0 + obj.CountDeployable(DeployedItem.portable_manned_turret_vs)._2 mustEqual 0 + obj.CountDeployable(DeployedItem.deployable_shield_generator)._2 mustEqual 0 + obj.CountDeployable(DeployedItem.router_telepad_deployable)._2 mustEqual 0 + + obj.AddToDeployableQuantities( + GroundSupport, + Set(GroundSupport) + ) + obj.CountDeployable(DeployedItem.boomer)._2 mustEqual 0 + obj.CountDeployable(DeployedItem.he_mine)._2 mustEqual 0 + obj.CountDeployable(DeployedItem.spitfire_turret)._2 mustEqual 0 + obj.CountDeployable(DeployedItem.motionalarmsensor)._2 mustEqual 0 + obj.CountDeployable(DeployedItem.jammer_mine)._2 mustEqual 0 + obj.CountDeployable(DeployedItem.spitfire_cloaked)._2 mustEqual 0 + obj.CountDeployable(DeployedItem.spitfire_aa)._2 mustEqual 0 + obj.CountDeployable(DeployedItem.sensor_shield)._2 mustEqual 0 + obj.CountDeployable(DeployedItem.tank_traps)._2 mustEqual 0 + obj.CountDeployable(DeployedItem.portable_manned_turret)._2 mustEqual 0 + obj.CountDeployable(DeployedItem.portable_manned_turret_nc)._2 mustEqual 0 + obj.CountDeployable(DeployedItem.portable_manned_turret_tr)._2 mustEqual 0 + obj.CountDeployable(DeployedItem.portable_manned_turret_vs)._2 mustEqual 0 + obj.CountDeployable(DeployedItem.deployable_shield_generator)._2 mustEqual 0 + obj.CountDeployable(DeployedItem.router_telepad_deployable)._2 mustEqual 1024 + } + + "change accessible fields by adding by certification type (AdvancedEngineering)" in { + val obj = new DeployableToolbox + obj.Initialize(Set(CombatEngineering)) + obj.CountDeployable(DeployedItem.boomer)._2 mustEqual 20 + obj.CountDeployable(DeployedItem.he_mine)._2 mustEqual 20 + obj.CountDeployable(DeployedItem.spitfire_turret)._2 mustEqual 10 + obj.CountDeployable(DeployedItem.motionalarmsensor)._2 mustEqual 20 + obj.CountDeployable(DeployedItem.jammer_mine)._2 mustEqual 0 + obj.CountDeployable(DeployedItem.spitfire_cloaked)._2 mustEqual 0 + obj.CountDeployable(DeployedItem.spitfire_aa)._2 mustEqual 0 + obj.CountDeployable(DeployedItem.sensor_shield)._2 mustEqual 0 + obj.CountDeployable(DeployedItem.tank_traps)._2 mustEqual 0 + obj.CountDeployable(DeployedItem.portable_manned_turret)._2 mustEqual 0 + obj.CountDeployable(DeployedItem.portable_manned_turret_nc)._2 mustEqual 0 + obj.CountDeployable(DeployedItem.portable_manned_turret_tr)._2 mustEqual 0 + obj.CountDeployable(DeployedItem.portable_manned_turret_vs)._2 mustEqual 0 + obj.CountDeployable(DeployedItem.deployable_shield_generator)._2 mustEqual 0 + obj.CountDeployable(DeployedItem.router_telepad_deployable)._2 mustEqual 0 + + obj.AddToDeployableQuantities( + AdvancedEngineering, + Set(CombatEngineering, AdvancedEngineering) + ) + obj.CountDeployable(DeployedItem.boomer)._2 mustEqual 25 + obj.CountDeployable(DeployedItem.he_mine)._2 mustEqual 25 + obj.CountDeployable(DeployedItem.spitfire_turret)._2 mustEqual 15 + obj.CountDeployable(DeployedItem.motionalarmsensor)._2 mustEqual 25 + obj.CountDeployable(DeployedItem.jammer_mine)._2 mustEqual 20 + obj.CountDeployable(DeployedItem.spitfire_cloaked)._2 mustEqual 5 + obj.CountDeployable(DeployedItem.spitfire_aa)._2 mustEqual 5 + obj.CountDeployable(DeployedItem.sensor_shield)._2 mustEqual 0 + obj.CountDeployable(DeployedItem.tank_traps)._2 mustEqual 5 + obj.CountDeployable(DeployedItem.portable_manned_turret)._2 mustEqual 1 + obj.CountDeployable(DeployedItem.portable_manned_turret_nc)._2 mustEqual 1 + obj.CountDeployable(DeployedItem.portable_manned_turret_tr)._2 mustEqual 1 + obj.CountDeployable(DeployedItem.portable_manned_turret_vs)._2 mustEqual 1 + obj.CountDeployable(DeployedItem.deployable_shield_generator)._2 mustEqual 1 + obj.CountDeployable(DeployedItem.router_telepad_deployable)._2 mustEqual 0 + } + + "change accessible fields by removing by certification types (all)" in { + val obj = new DeployableToolbox + obj.Initialize(Set(CombatEngineering, AssaultEngineering, FortificationEngineering, AdvancedHacking, GroundSupport)) + obj.CountDeployable(DeployedItem.boomer)._2 mustEqual 25 + obj.CountDeployable(DeployedItem.he_mine)._2 mustEqual 25 + obj.CountDeployable(DeployedItem.spitfire_turret)._2 mustEqual 15 + obj.CountDeployable(DeployedItem.motionalarmsensor)._2 mustEqual 25 + obj.CountDeployable(DeployedItem.jammer_mine)._2 mustEqual 20 + obj.CountDeployable(DeployedItem.spitfire_cloaked)._2 mustEqual 5 + obj.CountDeployable(DeployedItem.spitfire_aa)._2 mustEqual 5 + obj.CountDeployable(DeployedItem.sensor_shield)._2 mustEqual 20 + obj.CountDeployable(DeployedItem.tank_traps)._2 mustEqual 5 + obj.CountDeployable(DeployedItem.portable_manned_turret)._2 mustEqual 1 + obj.CountDeployable(DeployedItem.portable_manned_turret_nc)._2 mustEqual 1 + obj.CountDeployable(DeployedItem.portable_manned_turret_tr)._2 mustEqual 1 + obj.CountDeployable(DeployedItem.portable_manned_turret_vs)._2 mustEqual 1 + obj.CountDeployable(DeployedItem.deployable_shield_generator)._2 mustEqual 1 + obj.CountDeployable(DeployedItem.router_telepad_deployable)._2 mustEqual 1 + + obj.RemoveFromDeployableQuantities( + GroundSupport, + Set(CombatEngineering, AssaultEngineering, FortificationEngineering, AdvancedHacking) + ) + obj.CountDeployable(DeployedItem.boomer)._2 mustEqual 25 + obj.CountDeployable(DeployedItem.he_mine)._2 mustEqual 25 + obj.CountDeployable(DeployedItem.spitfire_turret)._2 mustEqual 15 + obj.CountDeployable(DeployedItem.motionalarmsensor)._2 mustEqual 25 + obj.CountDeployable(DeployedItem.jammer_mine)._2 mustEqual 20 + obj.CountDeployable(DeployedItem.spitfire_cloaked)._2 mustEqual 5 + obj.CountDeployable(DeployedItem.spitfire_aa)._2 mustEqual 5 + obj.CountDeployable(DeployedItem.sensor_shield)._2 mustEqual 20 + obj.CountDeployable(DeployedItem.tank_traps)._2 mustEqual 5 + obj.CountDeployable(DeployedItem.portable_manned_turret)._2 mustEqual 1 + obj.CountDeployable(DeployedItem.portable_manned_turret_nc)._2 mustEqual 1 + obj.CountDeployable(DeployedItem.portable_manned_turret_tr)._2 mustEqual 1 + obj.CountDeployable(DeployedItem.portable_manned_turret_vs)._2 mustEqual 1 + obj.CountDeployable(DeployedItem.deployable_shield_generator)._2 mustEqual 1 + obj.CountDeployable(DeployedItem.router_telepad_deployable)._2 mustEqual 0 + + obj.RemoveFromDeployableQuantities( + AdvancedHacking, + Set(CombatEngineering, AssaultEngineering, FortificationEngineering) + ) + obj.CountDeployable(DeployedItem.boomer)._2 mustEqual 25 + obj.CountDeployable(DeployedItem.he_mine)._2 mustEqual 25 + obj.CountDeployable(DeployedItem.spitfire_turret)._2 mustEqual 15 + obj.CountDeployable(DeployedItem.motionalarmsensor)._2 mustEqual 25 + obj.CountDeployable(DeployedItem.jammer_mine)._2 mustEqual 20 + obj.CountDeployable(DeployedItem.spitfire_cloaked)._2 mustEqual 5 + obj.CountDeployable(DeployedItem.spitfire_aa)._2 mustEqual 5 + obj.CountDeployable(DeployedItem.sensor_shield)._2 mustEqual 0 + obj.CountDeployable(DeployedItem.tank_traps)._2 mustEqual 5 + obj.CountDeployable(DeployedItem.portable_manned_turret)._2 mustEqual 1 + obj.CountDeployable(DeployedItem.portable_manned_turret_nc)._2 mustEqual 1 + obj.CountDeployable(DeployedItem.portable_manned_turret_tr)._2 mustEqual 1 + obj.CountDeployable(DeployedItem.portable_manned_turret_vs)._2 mustEqual 1 + obj.CountDeployable(DeployedItem.deployable_shield_generator)._2 mustEqual 1 + obj.CountDeployable(DeployedItem.router_telepad_deployable)._2 mustEqual 0 + + obj.RemoveFromDeployableQuantities( + FortificationEngineering, + Set(CombatEngineering, AssaultEngineering) + ) + obj.CountDeployable(DeployedItem.boomer)._2 mustEqual 20 + obj.CountDeployable(DeployedItem.he_mine)._2 mustEqual 20 + obj.CountDeployable(DeployedItem.spitfire_turret)._2 mustEqual 10 + obj.CountDeployable(DeployedItem.motionalarmsensor)._2 mustEqual 20 + obj.CountDeployable(DeployedItem.jammer_mine)._2 mustEqual 20 + obj.CountDeployable(DeployedItem.spitfire_cloaked)._2 mustEqual 0 + obj.CountDeployable(DeployedItem.spitfire_aa)._2 mustEqual 0 + obj.CountDeployable(DeployedItem.sensor_shield)._2 mustEqual 0 + obj.CountDeployable(DeployedItem.tank_traps)._2 mustEqual 0 + obj.CountDeployable(DeployedItem.portable_manned_turret)._2 mustEqual 1 + obj.CountDeployable(DeployedItem.portable_manned_turret_nc)._2 mustEqual 1 + obj.CountDeployable(DeployedItem.portable_manned_turret_tr)._2 mustEqual 1 + obj.CountDeployable(DeployedItem.portable_manned_turret_vs)._2 mustEqual 1 + obj.CountDeployable(DeployedItem.deployable_shield_generator)._2 mustEqual 1 + obj.CountDeployable(DeployedItem.router_telepad_deployable)._2 mustEqual 0 + + obj.RemoveFromDeployableQuantities( + AssaultEngineering, + Set(CombatEngineering) + ) + obj.CountDeployable(DeployedItem.boomer)._2 mustEqual 20 + obj.CountDeployable(DeployedItem.he_mine)._2 mustEqual 20 + obj.CountDeployable(DeployedItem.spitfire_turret)._2 mustEqual 10 + obj.CountDeployable(DeployedItem.motionalarmsensor)._2 mustEqual 20 + obj.CountDeployable(DeployedItem.jammer_mine)._2 mustEqual 0 + obj.CountDeployable(DeployedItem.spitfire_cloaked)._2 mustEqual 0 + obj.CountDeployable(DeployedItem.spitfire_aa)._2 mustEqual 0 + obj.CountDeployable(DeployedItem.sensor_shield)._2 mustEqual 0 + obj.CountDeployable(DeployedItem.tank_traps)._2 mustEqual 0 + obj.CountDeployable(DeployedItem.portable_manned_turret)._2 mustEqual 0 + obj.CountDeployable(DeployedItem.portable_manned_turret_nc)._2 mustEqual 0 + obj.CountDeployable(DeployedItem.portable_manned_turret_tr)._2 mustEqual 0 + obj.CountDeployable(DeployedItem.portable_manned_turret_vs)._2 mustEqual 0 + obj.CountDeployable(DeployedItem.deployable_shield_generator)._2 mustEqual 0 + obj.CountDeployable(DeployedItem.router_telepad_deployable)._2 mustEqual 0 + + obj.RemoveFromDeployableQuantities( + CombatEngineering, + Set() + ) + obj.CountDeployable(DeployedItem.boomer)._2 mustEqual 0 + obj.CountDeployable(DeployedItem.he_mine)._2 mustEqual 0 + obj.CountDeployable(DeployedItem.spitfire_turret)._2 mustEqual 0 + obj.CountDeployable(DeployedItem.motionalarmsensor)._2 mustEqual 0 + obj.CountDeployable(DeployedItem.jammer_mine)._2 mustEqual 0 + obj.CountDeployable(DeployedItem.spitfire_cloaked)._2 mustEqual 0 + obj.CountDeployable(DeployedItem.spitfire_aa)._2 mustEqual 0 + obj.CountDeployable(DeployedItem.sensor_shield)._2 mustEqual 0 + obj.CountDeployable(DeployedItem.tank_traps)._2 mustEqual 0 + obj.CountDeployable(DeployedItem.portable_manned_turret)._2 mustEqual 0 + obj.CountDeployable(DeployedItem.portable_manned_turret_nc)._2 mustEqual 0 + obj.CountDeployable(DeployedItem.portable_manned_turret_tr)._2 mustEqual 0 + obj.CountDeployable(DeployedItem.portable_manned_turret_vs)._2 mustEqual 0 + obj.CountDeployable(DeployedItem.deployable_shield_generator)._2 mustEqual 0 + obj.CountDeployable(DeployedItem.router_telepad_deployable)._2 mustEqual 0 + } + + "change accessible fields by removing by certification type (AdvancedEngineering)" in { + val obj = new DeployableToolbox + obj.Initialize(Set(CombatEngineering, AdvancedEngineering)) + obj.CountDeployable(DeployedItem.boomer)._2 mustEqual 25 + obj.CountDeployable(DeployedItem.he_mine)._2 mustEqual 25 + obj.CountDeployable(DeployedItem.spitfire_turret)._2 mustEqual 15 + obj.CountDeployable(DeployedItem.motionalarmsensor)._2 mustEqual 25 + obj.CountDeployable(DeployedItem.jammer_mine)._2 mustEqual 20 + obj.CountDeployable(DeployedItem.spitfire_cloaked)._2 mustEqual 5 + obj.CountDeployable(DeployedItem.spitfire_aa)._2 mustEqual 5 + obj.CountDeployable(DeployedItem.sensor_shield)._2 mustEqual 0 + obj.CountDeployable(DeployedItem.tank_traps)._2 mustEqual 5 + obj.CountDeployable(DeployedItem.portable_manned_turret)._2 mustEqual 1 + obj.CountDeployable(DeployedItem.portable_manned_turret_nc)._2 mustEqual 1 + obj.CountDeployable(DeployedItem.portable_manned_turret_tr)._2 mustEqual 1 + obj.CountDeployable(DeployedItem.portable_manned_turret_vs)._2 mustEqual 1 + obj.CountDeployable(DeployedItem.deployable_shield_generator)._2 mustEqual 1 + obj.CountDeployable(DeployedItem.router_telepad_deployable)._2 mustEqual 0 + + obj.RemoveFromDeployableQuantities( + AdvancedEngineering, + Set(CombatEngineering) + ) + obj.CountDeployable(DeployedItem.boomer)._2 mustEqual 20 + obj.CountDeployable(DeployedItem.he_mine)._2 mustEqual 20 + obj.CountDeployable(DeployedItem.spitfire_turret)._2 mustEqual 10 + obj.CountDeployable(DeployedItem.motionalarmsensor)._2 mustEqual 20 + obj.CountDeployable(DeployedItem.jammer_mine)._2 mustEqual 0 + obj.CountDeployable(DeployedItem.spitfire_cloaked)._2 mustEqual 0 + obj.CountDeployable(DeployedItem.spitfire_aa)._2 mustEqual 0 + obj.CountDeployable(DeployedItem.sensor_shield)._2 mustEqual 0 + obj.CountDeployable(DeployedItem.tank_traps)._2 mustEqual 0 + obj.CountDeployable(DeployedItem.portable_manned_turret)._2 mustEqual 0 + obj.CountDeployable(DeployedItem.portable_manned_turret_nc)._2 mustEqual 0 + obj.CountDeployable(DeployedItem.portable_manned_turret_tr)._2 mustEqual 0 + obj.CountDeployable(DeployedItem.portable_manned_turret_vs)._2 mustEqual 0 + obj.CountDeployable(DeployedItem.deployable_shield_generator)._2 mustEqual 0 + obj.CountDeployable(DeployedItem.router_telepad_deployable)._2 mustEqual 0 + } + + "can not remove deployables from an unpopulated field" in { + val obj = new DeployableToolbox + obj.Initialize(Set(CombatEngineering)) + val boomer = new BoomerDeployable(GlobalDefinitions.boomer) + obj.CountDeployable(DeployedItem.boomer)._2 mustEqual 20 + obj.Remove(boomer) mustEqual false + obj.CountDeployable(DeployedItem.boomer)._2 mustEqual 20 + } + + "can remove a deployable from a field it populates" in { + val obj = new DeployableToolbox + obj.Initialize(Set(CombatEngineering)) + val boomer = new BoomerDeployable(GlobalDefinitions.boomer) + obj.CountDeployable(DeployedItem.boomer)._2 mustEqual 20 + obj.Add(boomer) mustEqual true + obj.CountDeployable(DeployedItem.boomer).productIterator.toList mustEqual List(1,20) + obj.CountDeployable(DeployedItem.boomer).productIterator.toList mustEqual List(1, 20) + obj.Remove(boomer) mustEqual true + obj.CountDeployable(DeployedItem.boomer)._2 mustEqual 20 + } + + "can not remove a deployable from a field it does not populate" in { + val obj = new DeployableToolbox + obj.Initialize(Set(CombatEngineering)) + val boomer1 = new BoomerDeployable(GlobalDefinitions.boomer) + obj.CountDeployable(DeployedItem.boomer)._2 mustEqual 20 + obj.Add(boomer1) mustEqual true + obj.CountDeployable(DeployedItem.boomer).productIterator.toList mustEqual List(1, 20) + val boomer2 = new BoomerDeployable(GlobalDefinitions.boomer) + obj.Remove(boomer2) mustEqual false + obj.CountDeployable(DeployedItem.boomer).productIterator.toList mustEqual List(1, 20) + } + + "changing accessible fields by removing by certification type does not invalidate existing deployables" in { + val obj = new DeployableToolbox + obj.Initialize(Set(CombatEngineering)) + obj.Add(new BoomerDeployable(GlobalDefinitions.boomer)) + obj.Add(new BoomerDeployable(GlobalDefinitions.boomer)) + obj.Add(new BoomerDeployable(GlobalDefinitions.boomer)) + obj.CountDeployable(DeployedItem.boomer).productIterator.toList mustEqual List(3, 20) + + obj.RemoveFromDeployableQuantities( + CombatEngineering, + Set() + ) + obj.CountDeployable(DeployedItem.boomer).productIterator.toList mustEqual List(3, 0) + } + + "can not add the same deployable multiple times" in { + val obj = new DeployableToolbox + obj.Initialize(Set(CombatEngineering)) + val boomer = new BoomerDeployable(GlobalDefinitions.boomer) + obj.Add(boomer) mustEqual true + obj.Add(boomer) mustEqual false + } + + "can not add more deployables to a field than is allowed by its current type maximum" in { + val obj = new DeployableToolbox + obj.Initialize(Set(CombatEngineering)) + obj.CountDeployable(DeployedItem.he_mine)._2 mustEqual 20 + (1 to 19).foreach(_ => { + val o1 = new ExplosiveDeployable(GlobalDefinitions.he_mine) + obj.Accept(o1)mustEqual true + obj.Add(o1) mustEqual true + }) + obj.CountDeployable(DeployedItem.he_mine).productIterator.toList mustEqual List(19, 20) + val o2 = new BoomerDeployable(GlobalDefinitions.he_mine) + obj.Accept(o2) mustEqual true + obj.Add(o2) mustEqual true + obj.CountDeployable(DeployedItem.he_mine).productIterator.toList mustEqual List(20, 20) + //fail extra + val o3 = new BoomerDeployable(GlobalDefinitions.he_mine) + obj.Accept(o3) mustEqual false + obj.Add(o3) mustEqual false + obj.CountDeployable(DeployedItem.he_mine).productIterator.toList mustEqual List(20, 20) + //remove old + obj.Remove(o2) mustEqual true + obj.Accept(o2) mustEqual true + obj.Accept(o3) mustEqual true + obj.CountDeployable(DeployedItem.he_mine).productIterator.toList mustEqual List(19, 20) + //add extra + obj.Add(o3) mustEqual true + obj.CountDeployable(DeployedItem.he_mine).productIterator.toList mustEqual List(20, 20) + } + + "some deployables can share a category and a category maximum" in { + val obj = new DeployableToolbox + obj.Initialize(Set(CombatEngineering, AdvancedHacking)) + obj.CountDeployable(DeployedItem.motionalarmsensor)._2 mustEqual 20 + obj.CountDeployable(DeployedItem.sensor_shield)._2 mustEqual 20 + (1 to 10).foreach(_ => { + val o = new SensorDeployable(GlobalDefinitions.motionalarmsensor) + obj.Accept(o)mustEqual true + obj.Add(o) mustEqual true + }) + (1 to 10).foreach(_ => { + val o = new SensorDeployable(GlobalDefinitions.sensor_shield) + obj.Accept(o)mustEqual true + obj.Add(o) mustEqual true + }) + obj.CountDeployable(DeployedItem.motionalarmsensor).productIterator.toList mustEqual List(10, 20) + obj.CountDeployable(DeployedItem.sensor_shield).productIterator.toList mustEqual List(10, 20) + + val o1 = new SensorDeployable(GlobalDefinitions.motionalarmsensor) + obj.Accept(o1)mustEqual false + obj.Add(o1) mustEqual false + val o2 = new SensorDeployable(GlobalDefinitions.sensor_shield) + obj.Accept(o2)mustEqual false + obj.Add(o2) mustEqual false + } + + "remove the first deployable type from a category" in { + val obj = new DeployableToolbox + obj.Initialize(Set(CombatEngineering, AdvancedHacking)) + obj.CountDeployable(DeployedItem.motionalarmsensor)._2 mustEqual 20 + obj.CountDeployable(DeployedItem.sensor_shield)._2 mustEqual 20 + val o1 = new SensorDeployable(GlobalDefinitions.motionalarmsensor) + obj.Accept(o1)mustEqual true + obj.Add(o1) mustEqual true + val o2 = new SensorDeployable(GlobalDefinitions.sensor_shield) + obj.Accept(o2)mustEqual true + obj.Add(o2) mustEqual true + obj.CountDeployable(DeployedItem.motionalarmsensor).productIterator.toList mustEqual List(1, 20) + obj.CountDeployable(DeployedItem.sensor_shield).productIterator.toList mustEqual List(1, 20) + + val o3 = new SensorDeployable(GlobalDefinitions.sensor_shield) + val displaced1a = obj.DisplaceFirst(o3) //remove the first sensor_shield's deployable + displaced1a.nonEmpty mustEqual true + displaced1a.get.Definition == o3.Definition mustEqual true + obj.CountDeployable(DeployedItem.motionalarmsensor).productIterator.toList mustEqual List(1, 20) + obj.CountDeployable(DeployedItem.sensor_shield).productIterator.toList mustEqual List(0, 20) + //test: add o2 again and try to remove the motionalarmsensor + obj.Add(o2) + val displaced1b = obj.DisplaceFirst(o3, {(d) => d.Definition.Item != DeployedItem.sensor_shield}) //remove the first sensor_shield's deployable + displaced1b.nonEmpty mustEqual true + displaced1b.get.Definition == o3.Definition mustEqual false + displaced1b.get.Definition mustEqual GlobalDefinitions.motionalarmsensor + obj.CountDeployable(DeployedItem.motionalarmsensor).productIterator.toList mustEqual List(0, 20) + obj.CountDeployable(DeployedItem.sensor_shield).productIterator.toList mustEqual List(1, 20) + + val o4 = new SensorDeployable(GlobalDefinitions.motionalarmsensor) + val displaced2 = obj.DisplaceFirst(o3) //remove the first deployable of motionalarmsensor's category + displaced2.nonEmpty mustEqual true + displaced2.get.Definition == o4.Definition mustEqual false + displaced2.get.Definition mustEqual GlobalDefinitions.sensor_shield //removed deployable is a sensor_shield type + obj.CountDeployable(DeployedItem.motionalarmsensor)._2 mustEqual 20 + obj.CountDeployable(DeployedItem.sensor_shield)._2 mustEqual 20 + + obj.DisplaceFirst(o3) mustEqual None //we can not remove anymore deployables because the category is empty + obj.DisplaceFirst(o4) mustEqual None //likewise + } + + "remove the first deployable type from a category" in { + val obj = new DeployableToolbox + obj.Initialize(Set(CombatEngineering, AdvancedHacking)) + val o1 = new SensorDeployable(GlobalDefinitions.sensor_shield) + val o2 = new SensorDeployable(GlobalDefinitions.motionalarmsensor) + val o3 = new SensorDeployable(GlobalDefinitions.sensor_shield) + obj.Add(o3) mustEqual true //0 + obj.Add(o1) mustEqual true //1 + obj.Add(o2) mustEqual true //2 + + val displaced1 = obj.DisplaceFirst(DeployableCategory.Sensors) + displaced1.nonEmpty mustEqual true + displaced1.get.Definition == GlobalDefinitions.sensor_shield mustEqual true + displaced1.get == o3 mustEqual true + + val displaced2 = obj.DisplaceFirst(DeployableCategory.Sensors) + displaced2.nonEmpty mustEqual true + displaced2.get.Definition == GlobalDefinitions.sensor_shield mustEqual true + displaced2.get == o1 mustEqual true + } + + "counting deployables will give the type number; counting categories gives all deployables in that category" in { + val obj = new DeployableToolbox + obj.Initialize(Set(CombatEngineering, AdvancedHacking)) + obj.Add(new SensorDeployable(GlobalDefinitions.sensor_shield)) + obj.Add(new SensorDeployable(GlobalDefinitions.motionalarmsensor)) + obj.Add(new SensorDeployable(GlobalDefinitions.sensor_shield)) + obj.CountDeployable(DeployedItem.motionalarmsensor).productIterator.toList mustEqual List(1, 20) + obj.CountDeployable(DeployedItem.sensor_shield).productIterator.toList mustEqual List(2, 20) + obj.CountCategory(DeployedItem.motionalarmsensor).productIterator.toList mustEqual List(3, 20) + obj.CountCategory(DeployedItem.sensor_shield).productIterator.toList mustEqual List(3, 20) + } + + "get a list of GUIDs when the category contents are tested" in { + val obj = new DeployableToolbox + obj.Initialize(Set(CombatEngineering, AdvancedHacking)) + obj.CountDeployable(DeployedItem.motionalarmsensor)._2 mustEqual 20 + obj.CountDeployable(DeployedItem.sensor_shield)._2 mustEqual 20 + val o1 = new SensorDeployable(GlobalDefinitions.motionalarmsensor) + o1.GUID = PlanetSideGUID(1) + obj.Add(o1) mustEqual true + val o3 = new SensorDeployable(GlobalDefinitions.sensor_shield) + o3.GUID = PlanetSideGUID(3) + obj.Add(o3) mustEqual true + val o4 = new SensorDeployable(GlobalDefinitions.motionalarmsensor) + o4.GUID = PlanetSideGUID(4) + obj.Add(o4) mustEqual true + val o2 = new SensorDeployable(GlobalDefinitions.sensor_shield) + o2.GUID = PlanetSideGUID(2) + obj.Add(o2) mustEqual true + obj.CountDeployable(DeployedItem.motionalarmsensor).productIterator.toList mustEqual List(2, 20) + obj.CountDeployable(DeployedItem.sensor_shield).productIterator.toList mustEqual List(2, 20) + + val test1 = new SensorDeployable(GlobalDefinitions.motionalarmsensor) + val test2 = new SensorDeployable(GlobalDefinitions.sensor_shield) + obj.Category(test1) mustEqual List(PlanetSideGUID(1),PlanetSideGUID(3),PlanetSideGUID(4),PlanetSideGUID(2)) + obj.Category(test1) mustEqual obj.Category(test2) + } + + "get a list of GUIDs when the types are tested" in { + val obj = new DeployableToolbox + obj.Initialize(Set(CombatEngineering, AdvancedHacking)) + obj.CountDeployable(DeployedItem.motionalarmsensor)._2 mustEqual 20 + obj.CountDeployable(DeployedItem.sensor_shield)._2 mustEqual 20 + val o1 = new SensorDeployable(GlobalDefinitions.motionalarmsensor) + o1.GUID = PlanetSideGUID(1) + obj.Add(o1) mustEqual true + val o3 = new SensorDeployable(GlobalDefinitions.sensor_shield) + o3.GUID = PlanetSideGUID(3) + obj.Add(o3) mustEqual true + val o4 = new SensorDeployable(GlobalDefinitions.motionalarmsensor) + o4.GUID = PlanetSideGUID(4) + obj.Add(o4) mustEqual true + val o2 = new SensorDeployable(GlobalDefinitions.sensor_shield) + o2.GUID = PlanetSideGUID(2) + obj.Add(o2) mustEqual true + obj.CountDeployable(DeployedItem.motionalarmsensor).productIterator.toList mustEqual List(2, 20) + obj.CountDeployable(DeployedItem.sensor_shield).productIterator.toList mustEqual List(2, 20) + + val test1 = new SensorDeployable(GlobalDefinitions.motionalarmsensor) + val test2 = new SensorDeployable(GlobalDefinitions.sensor_shield) + obj.Deployables(test1) mustEqual List(PlanetSideGUID(1),PlanetSideGUID(4)) + obj.Deployables(test2) mustEqual List(PlanetSideGUID(3),PlanetSideGUID(2)) + } + + "three tests: 'contains' detects same deployable" in { + val obj = new DeployableToolbox + obj.Initialize(Set(CombatEngineering)) + + val boomer1 = new BoomerDeployable(GlobalDefinitions.boomer) + val boomer2 = new BoomerDeployable(GlobalDefinitions.boomer) + obj.Contains(boomer1) mustEqual false + obj.Contains(boomer2) mustEqual false + obj.Add(boomer1) + obj.Contains(boomer1) mustEqual true + obj.Contains(boomer2) mustEqual false + } + + "three tests: 'valid' tests whether deployable type can be accepted" in { + val obj = new DeployableToolbox + obj.Initialize(Set(CombatEngineering)) + + val cerebus = new TurretDeployable(GlobalDefinitions.spitfire_aa) //cerebus turret + obj.Valid(cerebus) mustEqual false + obj.CountDeployable(DeployedItem.spitfire_aa).productIterator.toList mustEqual List(0,0) + + obj.AddToDeployableQuantities(AdvancedEngineering, Set(CombatEngineering, AdvancedEngineering)) + obj.Valid(cerebus) mustEqual true + obj.CountDeployable(DeployedItem.spitfire_aa).productIterator.toList mustEqual List(0,5) + } + + "three tests: 'available' tests whether there is enough space to add more deployables of a type" in { + val obj = new DeployableToolbox + obj.Initialize(Set(CombatEngineering)) + obj.CountDeployable(DeployedItem.boomer).productIterator.toList mustEqual List(0,20) + + (1 to 20).foreach(_ => { + val boomer = new BoomerDeployable(GlobalDefinitions.boomer) + obj.Available(boomer) mustEqual true + obj.Add(boomer) mustEqual true + }) + obj.CountDeployable(DeployedItem.boomer).productIterator.toList mustEqual List(20,20) + val boomer = new BoomerDeployable(GlobalDefinitions.boomer) + obj.Available(boomer) mustEqual false + obj.Add(boomer) mustEqual false + } + + "three tests: 'accept' ensures that all three of the previous tests are passable" in { + val obj = new DeployableToolbox + val boomer = new BoomerDeployable(GlobalDefinitions.boomer) + + obj.Initialize(Set()) + obj.CountDeployable(DeployedItem.boomer).productIterator.toList mustEqual List(0,0) + obj.Accept(boomer) mustEqual false + obj.Available(boomer) mustEqual false + obj.Contains(boomer) mustEqual false //false is being passable + obj.Valid(boomer) mustEqual false + + obj.AddToDeployableQuantities(CombatEngineering, Set(CombatEngineering)) + obj.CountDeployable(DeployedItem.boomer).productIterator.toList mustEqual List(0,20) + obj.Accept(boomer) mustEqual true + obj.Available(boomer) mustEqual true //true is being passable + obj.Contains(boomer) mustEqual false //false is being passable + obj.Valid(boomer) mustEqual true //true is being passable + + obj.Add(boomer) + obj.CountDeployable(DeployedItem.boomer).productIterator.toList mustEqual List(1,20) + obj.Accept(boomer) mustEqual false + obj.Available(boomer) mustEqual true //true is being passable + obj.Contains(boomer) mustEqual true + obj.Valid(boomer) mustEqual true //true is being passable + + (1 to 20).foreach(_ => { obj.Add(new BoomerDeployable(GlobalDefinitions.boomer)) }) + obj.CountDeployable(DeployedItem.boomer).productIterator.toList mustEqual List(20,20) + obj.Accept(boomer) mustEqual false + obj.Available(boomer) mustEqual false + obj.Contains(boomer) mustEqual true + obj.Valid(boomer) mustEqual true //true is being passable + + obj.RemoveFromDeployableQuantities(CombatEngineering, Set()) + obj.CountDeployable(DeployedItem.boomer).productIterator.toList mustEqual List(20,0) + obj.Accept(boomer) mustEqual false + obj.Available(boomer) mustEqual false + obj.Contains(boomer) mustEqual true + obj.Valid(boomer) mustEqual false + } + + "UI elements report individual deployable types for a certification type" in { + val obj = new DeployableToolbox + obj.Initialize(Set(CombatEngineering, AdvancedEngineering, AdvancedHacking)) + + val list1 = obj.UpdateUI(CombatEngineering) + list1.head.productIterator.toList mustEqual List(94, 0, 83, 25) + list1(1).productIterator.toList mustEqual List(95, 0, 84, 25) + list1(2).productIterator.toList mustEqual List(97, 0, 86, 15) + list1(3).productIterator.toList mustEqual List(98, 0, 87, 25) + + val list2 = obj.UpdateUI(AdvancedHacking) + list2.head.productIterator.toList mustEqual List(104, 0, 93, 25) + + val list3 = obj.UpdateUI(AssaultEngineering) + list3.head.productIterator.toList mustEqual List(96, 0, 85, 20) + list3(1).productIterator.toList mustEqual List(103, 0, 92, 1) + list3(2).productIterator.toList mustEqual List(101, 0, 90, 1) + + val list4 = obj.UpdateUI(FortificationEngineering) + list4.head.productIterator.toList mustEqual List(94, 0, 83, 25) + list4(1).productIterator.toList mustEqual List(95, 0, 84, 25) + list4(2).productIterator.toList mustEqual List(97, 0, 86, 15) + list4(3).productIterator.toList mustEqual List(99, 0, 88, 5) + list4(4).productIterator.toList mustEqual List(100, 0, 89, 5) + list4(5).productIterator.toList mustEqual List(98, 0, 87, 25) + list4(6).productIterator.toList mustEqual List(102, 0, 91, 5) + + obj.UpdateUI(AdvancedEngineering).flatMap(tuple => tuple.productIterator.toList) mustEqual + obj.UpdateUI(AssaultEngineering).flatMap(tuple => tuple.productIterator.toList) ++ + obj.UpdateUI(FortificationEngineering).flatMap(tuple => tuple.productIterator.toList) + + obj.UpdateUI(MediumAssault) mustEqual Nil + } + + "UI elements report individual deployable types for a combination of certification types" in { + val obj = new DeployableToolbox + obj.Initialize(Set(CombatEngineering, AdvancedEngineering, AdvancedHacking)) + + obj.UpdateUI( + List(AssaultEngineering, FortificationEngineering) + ).flatMap(tuple => tuple.productIterator.toList) mustEqual + obj.UpdateUI(AdvancedEngineering).flatMap(tuple => tuple.productIterator.toList) + } + + "all of the one manned field turrets use the same category and the same UI elements" in { + val obj = new DeployableToolbox + //initial state + (obj.UpdateUIElement(DeployedItem.portable_manned_turret) ++ + obj.UpdateUIElement(DeployedItem.portable_manned_turret_nc) ++ + obj.UpdateUIElement(DeployedItem.portable_manned_turret_tr) ++ + obj.UpdateUIElement(DeployedItem.portable_manned_turret_vs) + ).toSet mustEqual Set((103, 0, 92, 0)) //note: four elements become one common element + + //initialized state + obj.Initialize(Set(CombatEngineering, AdvancedEngineering)) + (obj.UpdateUIElement(DeployedItem.portable_manned_turret) ++ + obj.UpdateUIElement(DeployedItem.portable_manned_turret_nc) ++ + obj.UpdateUIElement(DeployedItem.portable_manned_turret_tr) ++ + obj.UpdateUIElement(DeployedItem.portable_manned_turret_vs) + ).toSet mustEqual Set((103, 0, 92, 1)) + + //portable_manned_turret_vs added + val obj1 = new TurretDeployable(GlobalDefinitions.portable_manned_turret_vs) + obj1.Definition.Item mustEqual DeployedItem.portable_manned_turret_vs + obj1.Definition.DeployCategory mustEqual DeployableCategory.FieldTurrets + obj1.GUID = PlanetSideGUID(1) + obj.Add(obj1) mustEqual true + (obj.UpdateUIElement(DeployedItem.portable_manned_turret) ++ + obj.UpdateUIElement(DeployedItem.portable_manned_turret_nc) ++ + obj.UpdateUIElement(DeployedItem.portable_manned_turret_tr) ++ + obj.UpdateUIElement(DeployedItem.portable_manned_turret_vs) + ).toSet mustEqual Set((103, 1, 92, 1)) + + //portable_manned_turret_nc fails to add + val obj2 = new TurretDeployable(GlobalDefinitions.portable_manned_turret_nc) + obj2.Definition.Item mustEqual DeployedItem.portable_manned_turret_nc + obj2.Definition.DeployCategory mustEqual DeployableCategory.FieldTurrets + obj2.GUID = PlanetSideGUID(2) + obj.Add(obj2) mustEqual false + obj.Category(DeployableCategory.FieldTurrets).contains(PlanetSideGUID(1)) mustEqual true //included + obj.Category(DeployableCategory.FieldTurrets).contains(PlanetSideGUID(2)) mustEqual false + + //swap turrets + obj.Remove(obj1) mustEqual true + obj.Add(obj2) mustEqual true + (obj.UpdateUIElement(DeployedItem.portable_manned_turret) ++ + obj.UpdateUIElement(DeployedItem.portable_manned_turret_nc) ++ + obj.UpdateUIElement(DeployedItem.portable_manned_turret_tr) ++ + obj.UpdateUIElement(DeployedItem.portable_manned_turret_vs) + ).toSet mustEqual Set((103, 1, 92, 1)) + obj.Category(DeployableCategory.FieldTurrets).contains(PlanetSideGUID(1)) mustEqual false + obj.Category(DeployableCategory.FieldTurrets).contains(PlanetSideGUID(2)) mustEqual true //included + } + + "clear all deployables, return their GUIDs" in { + val obj = new DeployableToolbox + obj.Initialize(Set(CombatEngineering)) + + obj.Add(new SensorDeployable(GlobalDefinitions.motionalarmsensor) { GUID = PlanetSideGUID(1) }) + obj.Add(new BoomerDeployable(GlobalDefinitions.boomer) { GUID = PlanetSideGUID(2) }) + obj.Add(new BoomerDeployable(GlobalDefinitions.he_mine) { GUID = PlanetSideGUID(3) }) + obj.CountDeployable(DeployedItem.boomer).productIterator.toList mustEqual List(1, 20) + obj.CountDeployable(DeployedItem.he_mine).productIterator.toList mustEqual List(1, 20) + obj.CountDeployable(DeployedItem.motionalarmsensor).productIterator.toList mustEqual List(1, 20) + + obj.Clear().toSet mustEqual Set(PlanetSideGUID(1), PlanetSideGUID(2), PlanetSideGUID(3)) + obj.CountDeployable(DeployedItem.boomer).productIterator.toList mustEqual List(0, 20) + obj.CountDeployable(DeployedItem.he_mine).productIterator.toList mustEqual List(0, 20) + obj.CountDeployable(DeployedItem.motionalarmsensor).productIterator.toList mustEqual List(0, 20) + } + + "clear all deployables of a certain type" in { + val obj = new DeployableToolbox + obj.Initialize(Set(CombatEngineering, AdvancedHacking)) + + obj.Add(new ExplosiveDeployable(GlobalDefinitions.he_mine) { GUID = PlanetSideGUID(1) }) + obj.Add(new SensorDeployable(GlobalDefinitions.motionalarmsensor) { GUID = PlanetSideGUID(3) }) + obj.Add(new SensorDeployable(GlobalDefinitions.sensor_shield) { GUID = PlanetSideGUID(4) }) + obj.CountDeployable(DeployedItem.he_mine).productIterator.toList mustEqual List(1, 20) + obj.CountDeployable(DeployedItem.motionalarmsensor).productIterator.toList mustEqual List(1, 20) + obj.CountDeployable(DeployedItem.sensor_shield).productIterator.toList mustEqual List(1, 20) + obj.CountCategory(DeployedItem.motionalarmsensor).productIterator.toList mustEqual List(2, 20) + + obj.ClearDeployable(DeployedItem.motionalarmsensor).toSet mustEqual Set(PlanetSideGUID(3)) + obj.CountDeployable(DeployedItem.he_mine).productIterator.toList mustEqual List(1, 20) + obj.CountDeployable(DeployedItem.motionalarmsensor).productIterator.toList mustEqual List(0, 20) + obj.CountDeployable(DeployedItem.sensor_shield).productIterator.toList mustEqual List(1, 20) + obj.CountCategory(DeployedItem.motionalarmsensor).productIterator.toList mustEqual List(1, 20) + } + + "clear all deployables of a certain category" in { + val obj = new DeployableToolbox + obj.Initialize(Set(CombatEngineering, AdvancedHacking)) + + obj.Add(new ExplosiveDeployable(GlobalDefinitions.he_mine) { GUID = PlanetSideGUID(1) }) + obj.Add(new SensorDeployable(GlobalDefinitions.motionalarmsensor) { GUID = PlanetSideGUID(3) }) + obj.Add(new SensorDeployable(GlobalDefinitions.sensor_shield) { GUID = PlanetSideGUID(4) }) + obj.CountDeployable(DeployedItem.he_mine).productIterator.toList mustEqual List(1, 20) + obj.CountDeployable(DeployedItem.motionalarmsensor).productIterator.toList mustEqual List(1, 20) + obj.CountDeployable(DeployedItem.sensor_shield).productIterator.toList mustEqual List(1, 20) + obj.CountCategory(DeployedItem.motionalarmsensor).productIterator.toList mustEqual List(2, 20) + + obj.ClearCategory(DeployedItem.motionalarmsensor).toSet mustEqual Set(PlanetSideGUID(3), PlanetSideGUID(4)) + obj.CountDeployable(DeployedItem.he_mine).productIterator.toList mustEqual List(1, 20) + obj.CountDeployable(DeployedItem.motionalarmsensor).productIterator.toList mustEqual List(0, 20) + obj.CountDeployable(DeployedItem.sensor_shield).productIterator.toList mustEqual List(0, 20) + obj.CountCategory(DeployedItem.motionalarmsensor).productIterator.toList mustEqual List(0, 20) + } + } +} diff --git a/common/src/test/scala/objects/EquipmentTest.scala b/common/src/test/scala/objects/EquipmentTest.scala index caf8431b..6c638c94 100644 --- a/common/src/test/scala/objects/EquipmentTest.scala +++ b/common/src/test/scala/objects/EquipmentTest.scala @@ -2,11 +2,13 @@ package objects import net.psforever.objects._ -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 net.psforever.objects.ce.DeployedItem import net.psforever.objects.definition._ +import net.psforever.packet.game.PlanetSideGUID +import net.psforever.types.CertificationType import org.specs2.mutable._ class EquipmentTest extends Specification { @@ -329,62 +331,57 @@ class EquipmentTest extends Specification { } "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 + val obj : ConstructionItem = ConstructionItem(GlobalDefinitions.ace) + obj.Definition.ObjectId mustEqual GlobalDefinitions.ace.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 + "fire modes" in { + val obj : ConstructionItem = ConstructionItem(GlobalDefinitions.ace) + obj.AmmoType mustEqual DeployedItem.boomer + obj.NextFireMode + obj.AmmoType mustEqual DeployedItem.he_mine + obj.NextFireMode + obj.AmmoType mustEqual DeployedItem.spitfire_turret + obj.NextFireMode + obj.AmmoType mustEqual DeployedItem.motionalarmsensor + obj.NextFireMode + obj.AmmoType mustEqual DeployedItem.boomer } - "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 + "ammo types" in { + val obj : ConstructionItem = ConstructionItem(GlobalDefinitions.ace) obj.NextFireMode - obj.FireModeIndex mustEqual 1 - obj.FireMode mustEqual DeployedItem.portable_manned_turret_tr - //fmode -> 2 + obj.AmmoType mustEqual DeployedItem.he_mine + obj.NextAmmoType + obj.AmmoType mustEqual DeployedItem.jammer_mine + obj.NextAmmoType + obj.AmmoType mustEqual DeployedItem.he_mine + } + + "when switching fire modes, ammo mode resets to the first entry" in { + val obj : ConstructionItem = ConstructionItem(GlobalDefinitions.ace) obj.NextFireMode - obj.FireModeIndex mustEqual 2 - obj.FireMode mustEqual DeployedItem.deployable_shield_generator - //fmode -> 0 + obj.AmmoType mustEqual DeployedItem.he_mine + obj.NextAmmoType + obj.AmmoType mustEqual DeployedItem.jammer_mine + obj.NextFireMode //spitfire_turret + obj.NextFireMode //motionalarmsensor + obj.NextFireMode //boomer obj.NextFireMode - obj.FireModeIndex mustEqual 0 - obj.FireMode mustEqual DeployedItem.tank_traps + obj.AmmoType mustEqual DeployedItem.he_mine + } + + "qualify certifications that must be met before ammo types may be used" in { + val obj : ConstructionItem = ConstructionItem(GlobalDefinitions.ace) + obj.AmmoType mustEqual DeployedItem.boomer + obj.ModePermissions mustEqual Set(CertificationType.CombatEngineering) + obj.NextFireMode + obj.AmmoType mustEqual DeployedItem.he_mine + obj.ModePermissions mustEqual Set(CertificationType.CombatEngineering) + obj.NextAmmoType + obj.AmmoType mustEqual DeployedItem.jammer_mine + obj.ModePermissions mustEqual Set(CertificationType.AssaultEngineering) } } @@ -399,4 +396,21 @@ class EquipmentTest extends Specification { obj.Definition.ObjectId mustEqual remote_electronics_kit.ObjectId } } + + "BoomerTrigger" should { + "construct" in { + val obj : BoomerTrigger = new BoomerTrigger + obj.Definition.ObjectId mustEqual boomer_trigger.ObjectId + obj.Companion mustEqual None + } + + "boomer trigger has a companion object referenced by GUID" in { + val obj : BoomerTrigger = new BoomerTrigger + obj.Companion mustEqual None + obj.Companion = PlanetSideGUID(1) + obj.Companion.contains(PlanetSideGUID(1)) mustEqual true + obj.Companion = None + obj.Companion mustEqual None + } + } } diff --git a/common/src/test/scala/objects/ExoSuitTest.scala b/common/src/test/scala/objects/ExoSuitTest.scala index 484c748c..ba9a9123 100644 --- a/common/src/test/scala/objects/ExoSuitTest.scala +++ b/common/src/test/scala/objects/ExoSuitTest.scala @@ -2,11 +2,8 @@ package objects import net.psforever.objects._ -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 net.psforever.objects.definition._ import net.psforever.types.ExoSuitType import org.specs2.mutable._ diff --git a/common/src/test/scala/objects/MannedTurretTest.scala b/common/src/test/scala/objects/FacilityTurretTest.scala similarity index 79% rename from common/src/test/scala/objects/MannedTurretTest.scala rename to common/src/test/scala/objects/FacilityTurretTest.scala index 2c85cc9a..cb91dda0 100644 --- a/common/src/test/scala/objects/MannedTurretTest.scala +++ b/common/src/test/scala/objects/FacilityTurretTest.scala @@ -7,7 +7,7 @@ import net.psforever.objects.{Avatar, GlobalDefinitions, Player, Tool} import net.psforever.objects.definition.ToolDefinition import net.psforever.objects.serverobject.mount.Mountable import net.psforever.objects.serverobject.structures.{Building, StructureType} -import net.psforever.objects.serverobject.turret.{MannedTurret, MannedTurretControl, MannedTurretDefinition, TurretUpgrade} +import net.psforever.objects.serverobject.turret._ import net.psforever.objects.zones.Zone import net.psforever.packet.game.PlanetSideGUID import net.psforever.types.{CharacterGender, CharacterVoice, PlanetSideEmpire} @@ -16,10 +16,10 @@ import org.specs2.mutable.Specification import scala.collection.mutable import scala.concurrent.duration._ -class MannedTurretTest extends Specification { - "MannedTurretTest" should { +class FacilityTurretTest extends Specification { + "FacilityTurretTest" should { "define" in { - val obj = new MannedTurretDefinition(480) + val obj = new TurretDefinition(480) obj.Weapons mustEqual mutable.HashMap.empty[TurretUpgrade.Value, ToolDefinition] obj.ReserveAmmunition mustEqual false obj.FactionLocked mustEqual true @@ -28,7 +28,7 @@ class MannedTurretTest extends Specification { } "construct" in { - val obj = MannedTurret(GlobalDefinitions.manned_turret) + val obj = FacilityTurret(GlobalDefinitions.manned_turret) obj.Weapons.size mustEqual 1 obj.Weapons(1).Equipment match { case Some(tool : Tool) => @@ -51,7 +51,7 @@ class MannedTurretTest extends Specification { } "upgrade to a different weapon" in { - val obj = MannedTurret(GlobalDefinitions.manned_turret) + val obj = FacilityTurret(GlobalDefinitions.manned_turret) obj.Upgrade = TurretUpgrade.None obj.Weapons(1).Equipment match { case Some(tool : Tool) => @@ -87,26 +87,26 @@ class MannedTurretTest extends Specification { } } -class MannedTurretControl1Test extends ActorTest { - "MannedTurretControl" should { +class FacilityTurretControl1Test extends ActorTest { + "FacilityTurretControl" should { "construct" in { - val obj = MannedTurret(GlobalDefinitions.manned_turret) - obj.Actor = system.actorOf(Props(classOf[MannedTurretControl], obj), "turret-control") + val obj = FacilityTurret(GlobalDefinitions.manned_turret) + obj.Actor = system.actorOf(Props(classOf[FacilityTurretControl], obj), "turret-control") assert(obj.Actor != ActorRef.noSender) } } } -class MannedTurretControl2Test extends ActorTest { +class FacilityTurretControl2Test extends ActorTest { val player = Player(Avatar("", PlanetSideEmpire.TR, CharacterGender.Male, 0, CharacterVoice.Mute)) - val obj = MannedTurret(GlobalDefinitions.manned_turret) + val obj = FacilityTurret(GlobalDefinitions.manned_turret) obj.GUID = PlanetSideGUID(1) - obj.Actor = system.actorOf(Props(classOf[MannedTurretControl], obj), "turret-control") + obj.Actor = system.actorOf(Props(classOf[FacilityTurretControl], obj), "turret-control") val bldg = Building(0, Zone.Nowhere, StructureType.Building) bldg.Amenities = obj bldg.Faction = PlanetSideEmpire.TR - "MannedTurretControl" should { + "FacilityTurretControl" should { "seat on faction affiliation when FactionLock is true" in { assert(player.Faction == PlanetSideEmpire.TR) assert(obj.Faction == PlanetSideEmpire.TR) @@ -124,15 +124,15 @@ class MannedTurretControl2Test extends ActorTest { } } -class MannedTurretControl3Test extends ActorTest { +class FacilityTurretControl3Test extends ActorTest { val player = Player(Avatar("", PlanetSideEmpire.TR, CharacterGender.Male, 0, CharacterVoice.Mute)) - val obj = MannedTurret(GlobalDefinitions.manned_turret) + val obj = FacilityTurret(GlobalDefinitions.manned_turret) obj.GUID = PlanetSideGUID(1) - obj.Actor = system.actorOf(Props(classOf[MannedTurretControl], obj), "turret-control") + obj.Actor = system.actorOf(Props(classOf[FacilityTurretControl], obj), "turret-control") val bldg = Building(0, Zone.Nowhere, StructureType.Building) bldg.Amenities = obj - "MannedTurretControl" should { + "FacilityTurretControl" should { "block seating on mismatched faction affiliation when FactionLock is true" in { assert(player.Faction == PlanetSideEmpire.TR) assert(obj.Faction == PlanetSideEmpire.NEUTRAL) @@ -150,17 +150,17 @@ class MannedTurretControl3Test extends ActorTest { } } -class MannedTurretControl4Test extends ActorTest { +class FacilityTurretControl4Test extends ActorTest { val player = Player(Avatar("", PlanetSideEmpire.TR, CharacterGender.Male, 0, CharacterVoice.Mute)) - val objDef = new MannedTurretDefinition(480) + val objDef = new TurretDefinition(480) objDef.FactionLocked = false - val obj = MannedTurret(objDef) + val obj = FacilityTurret(objDef) obj.GUID = PlanetSideGUID(1) - obj.Actor = system.actorOf(Props(classOf[MannedTurretControl], obj), "turret-control") + obj.Actor = system.actorOf(Props(classOf[FacilityTurretControl], obj), "turret-control") val bldg = Building(0, Zone.Nowhere, StructureType.Building) bldg.Amenities = obj - "MannedTurretControl" should { + "FacilityTurretControl" should { "seating even with mismatched faction affiliation when FactionLock is false" in { assert(player.Faction == PlanetSideEmpire.TR) assert(obj.Faction == PlanetSideEmpire.NEUTRAL) diff --git a/common/src/test/scala/objects/ResourceSiloTest.scala b/common/src/test/scala/objects/ResourceSiloTest.scala index 62bd3908..e5cc2e51 100644 --- a/common/src/test/scala/objects/ResourceSiloTest.scala +++ b/common/src/test/scala/objects/ResourceSiloTest.scala @@ -191,7 +191,7 @@ class ResourceSiloControlUpdate1Test extends ActorTest { .actionMessage.asInstanceOf[AvatarAction.PlanetsideAttribute].attribute_value == 0) val reply4 = probe1.receiveOne(500 milliseconds) - assert(obj.LowNtuWarningOn == false) + assert(!obj.LowNtuWarningOn) assert(reply4.isInstanceOf[AvatarServiceMessage]) assert(reply4.asInstanceOf[AvatarServiceMessage].forChannel == "nowhere") assert(reply4.asInstanceOf[AvatarServiceMessage] diff --git a/common/src/test/scala/objects/ServerObjectBuilderTest.scala b/common/src/test/scala/objects/ServerObjectBuilderTest.scala index b59bd70c..220a7ce5 100644 --- a/common/src/test/scala/objects/ServerObjectBuilderTest.scala +++ b/common/src/test/scala/objects/ServerObjectBuilderTest.scala @@ -246,20 +246,20 @@ class SpawnTubeObjectBuilderTest extends ActorTest { } } -class MannedTurretObjectBuilderTest extends ActorTest { +class FacilityTurretObjectBuilderTest extends ActorTest { import net.psforever.objects.GlobalDefinitions.manned_turret - import net.psforever.objects.serverobject.turret.MannedTurret - "MannedTurretObjectBuilder" should { + import net.psforever.objects.serverobject.turret.FacilityTurret + "FacilityTurretObjectBuilder" should { "build" in { val hub = ServerObjectBuilderTest.NumberPoolHub val actor = system.actorOf(Props(classOf[ServerObjectBuilderTest.BuilderTestActor], ServerObjectBuilder(1, - MannedTurret.Constructor(manned_turret)), hub), "spawn-tube") + FacilityTurret.Constructor(manned_turret)), hub), "spawn-tube") actor ! "!" val reply = receiveOne(Duration.create(1000, "ms")) - assert(reply.isInstanceOf[MannedTurret]) - assert(reply.asInstanceOf[MannedTurret].HasGUID) - assert(reply.asInstanceOf[MannedTurret].GUID == PlanetSideGUID(1)) + assert(reply.isInstanceOf[FacilityTurret]) + assert(reply.asInstanceOf[FacilityTurret].HasGUID) + assert(reply.asInstanceOf[FacilityTurret].GUID == PlanetSideGUID(1)) assert(reply == hub(1).get) } } diff --git a/common/src/test/scala/objects/guidtask/GUIDTaskRegister2Test.scala b/common/src/test/scala/objects/guidtask/GUIDTaskRegisterAmmoTest.scala similarity index 91% rename from common/src/test/scala/objects/guidtask/GUIDTaskRegister2Test.scala rename to common/src/test/scala/objects/guidtask/GUIDTaskRegisterAmmoTest.scala index 78ccdad2..69902bea 100644 --- a/common/src/test/scala/objects/guidtask/GUIDTaskRegister2Test.scala +++ b/common/src/test/scala/objects/guidtask/GUIDTaskRegisterAmmoTest.scala @@ -5,7 +5,7 @@ import base.ActorTest import net.psforever.objects._ import net.psforever.objects.guid.{GUIDTask, TaskResolver} -class GUIDTaskRegister2Test extends ActorTest { +class GUIDTaskRegisterAmmoTest extends ActorTest { "RegisterEquipment -> RegisterObjectTask" in { val (_, uns, taskResolver, probe) = GUIDTaskTest.CommonTestSetup val obj = AmmoBox(GlobalDefinitions.energy_cell) diff --git a/common/src/test/scala/objects/guidtask/GUIDTaskRegister5Test.scala b/common/src/test/scala/objects/guidtask/GUIDTaskRegisterAvatarTest.scala similarity index 96% rename from common/src/test/scala/objects/guidtask/GUIDTaskRegister5Test.scala rename to common/src/test/scala/objects/guidtask/GUIDTaskRegisterAvatarTest.scala index ad33f006..4fdd378f 100644 --- a/common/src/test/scala/objects/guidtask/GUIDTaskRegister5Test.scala +++ b/common/src/test/scala/objects/guidtask/GUIDTaskRegisterAvatarTest.scala @@ -6,7 +6,7 @@ import net.psforever.objects._ import net.psforever.objects.guid.{GUIDTask, TaskResolver} import net.psforever.types.{CharacterGender, CharacterVoice, PlanetSideEmpire} -class GUIDTaskRegister5Test extends ActorTest { +class GUIDTaskRegisterAvatarTest extends ActorTest { "RegisterAvatar" in { val (_, uns, taskResolver, probe) = GUIDTaskTest.CommonTestSetup val obj = Player(Avatar("test", PlanetSideEmpire.TR, CharacterGender.Male, 0, CharacterVoice.Mute)) diff --git a/common/src/test/scala/objects/guidtask/GUIDTaskRegister1Test.scala b/common/src/test/scala/objects/guidtask/GUIDTaskRegisterObjectTest.scala similarity index 90% rename from common/src/test/scala/objects/guidtask/GUIDTaskRegister1Test.scala rename to common/src/test/scala/objects/guidtask/GUIDTaskRegisterObjectTest.scala index 19338c49..cd1e5c28 100644 --- a/common/src/test/scala/objects/guidtask/GUIDTaskRegister1Test.scala +++ b/common/src/test/scala/objects/guidtask/GUIDTaskRegisterObjectTest.scala @@ -4,7 +4,7 @@ package objects.guidtask import base.ActorTest import net.psforever.objects.guid.{GUIDTask, TaskResolver} -class GUIDTaskRegister1Test extends ActorTest { +class GUIDTaskRegisterObjectTest extends ActorTest { "RegisterObjectTask" in { val (_, uns, taskResolver, probe) = GUIDTaskTest.CommonTestSetup val obj = new GUIDTaskTest.TestObject diff --git a/common/src/test/scala/objects/guidtask/GUIDTaskRegister6Test.scala b/common/src/test/scala/objects/guidtask/GUIDTaskRegisterPlayerTest.scala similarity index 96% rename from common/src/test/scala/objects/guidtask/GUIDTaskRegister6Test.scala rename to common/src/test/scala/objects/guidtask/GUIDTaskRegisterPlayerTest.scala index 72f1a3e3..0171d8d6 100644 --- a/common/src/test/scala/objects/guidtask/GUIDTaskRegister6Test.scala +++ b/common/src/test/scala/objects/guidtask/GUIDTaskRegisterPlayerTest.scala @@ -6,7 +6,7 @@ import net.psforever.objects._ import net.psforever.objects.guid.{GUIDTask, TaskResolver} import net.psforever.types.{CharacterGender, CharacterVoice, PlanetSideEmpire} -class GUIDTaskRegister6Test extends ActorTest { +class GUIDTaskRegisterPlayerTest extends ActorTest { "RegisterPlayer" in { val (_, uns, taskResolver, probe) = GUIDTaskTest.CommonTestSetup val obj = Player(Avatar("test", PlanetSideEmpire.TR, CharacterGender.Male, 0, CharacterVoice.Mute)) diff --git a/common/src/test/scala/objects/guidtask/GUIDTaskRegister3Test.scala b/common/src/test/scala/objects/guidtask/GUIDTaskRegisterToolTest.scala similarity index 93% rename from common/src/test/scala/objects/guidtask/GUIDTaskRegister3Test.scala rename to common/src/test/scala/objects/guidtask/GUIDTaskRegisterToolTest.scala index 2350563b..3da02707 100644 --- a/common/src/test/scala/objects/guidtask/GUIDTaskRegister3Test.scala +++ b/common/src/test/scala/objects/guidtask/GUIDTaskRegisterToolTest.scala @@ -5,7 +5,7 @@ import base.ActorTest import net.psforever.objects._ import net.psforever.objects.guid.{GUIDTask, TaskResolver} -class GUIDTaskRegister3Test extends ActorTest { +class GUIDTaskRegisterToolTest extends ActorTest { "RegisterEquipment -> RegisterTool" in { val (_, uns, taskResolver, probe) = GUIDTaskTest.CommonTestSetup val obj = Tool(GlobalDefinitions.beamer) diff --git a/common/src/test/scala/objects/guidtask/GUIDTaskRegisterTurretTest.scala b/common/src/test/scala/objects/guidtask/GUIDTaskRegisterTurretTest.scala new file mode 100644 index 00000000..59b381da --- /dev/null +++ b/common/src/test/scala/objects/guidtask/GUIDTaskRegisterTurretTest.scala @@ -0,0 +1,27 @@ +// Copyright (c) 2017 PSForever +package objects.guidtask + +import base.ActorTest +import net.psforever.objects._ +import net.psforever.objects.guid.{GUIDTask, TaskResolver} + +class GUIDTaskRegisterTurretTest extends ActorTest { + "RegisterDeployableTurret" in { + val (_, uns, taskResolver, probe) = GUIDTaskTest.CommonTestSetup + val obj = new TurretDeployable(GlobalDefinitions.portable_manned_turret_vs) + val obj_wep = obj.Weapons(1).Equipment.get + val obj_ammo = obj_wep.asInstanceOf[Tool].AmmoSlot.Box + val obj_res = obj.Inventory.Items.map(_.obj) + + assert(!obj.HasGUID) + assert(!obj_wep.HasGUID) + assert(!obj_ammo.HasGUID) + obj_res.foreach(box => !box.HasGUID) + taskResolver ! TaskResolver.GiveTask(new GUIDTaskTest.RegisterTestTask(probe.ref), List(GUIDTask.RegisterDeployableTurret(obj)(uns))) + probe.expectMsg(scala.util.Success) + assert(obj.HasGUID) + assert(obj_wep.HasGUID) + assert(obj_ammo.HasGUID) + obj_res.foreach(box => box.HasGUID) + } +} diff --git a/common/src/test/scala/objects/guidtask/GUIDTaskRegister4Test.scala b/common/src/test/scala/objects/guidtask/GUIDTaskRegisterVehicleTest.scala similarity index 95% rename from common/src/test/scala/objects/guidtask/GUIDTaskRegister4Test.scala rename to common/src/test/scala/objects/guidtask/GUIDTaskRegisterVehicleTest.scala index 64c7f66d..841b2b71 100644 --- a/common/src/test/scala/objects/guidtask/GUIDTaskRegister4Test.scala +++ b/common/src/test/scala/objects/guidtask/GUIDTaskRegisterVehicleTest.scala @@ -5,7 +5,7 @@ import base.ActorTest import net.psforever.objects._ import net.psforever.objects.guid.{GUIDTask, TaskResolver} -class GUIDTaskRegister4Test extends ActorTest { +class GUIDTaskRegisterVehicleTest extends ActorTest { "RegisterVehicle" in { val (_, uns, taskResolver, probe) = GUIDTaskTest.CommonTestSetup val obj = Vehicle(GlobalDefinitions.fury) diff --git a/common/src/test/scala/objects/guidtask/GUIDTaskUnregister2Test.scala b/common/src/test/scala/objects/guidtask/GUIDTaskUnregisterAmmoTest.scala similarity index 92% rename from common/src/test/scala/objects/guidtask/GUIDTaskUnregister2Test.scala rename to common/src/test/scala/objects/guidtask/GUIDTaskUnregisterAmmoTest.scala index 6b4bfb1a..77ea65dd 100644 --- a/common/src/test/scala/objects/guidtask/GUIDTaskUnregister2Test.scala +++ b/common/src/test/scala/objects/guidtask/GUIDTaskUnregisterAmmoTest.scala @@ -5,7 +5,7 @@ import base.ActorTest import net.psforever.objects._ import net.psforever.objects.guid.{GUIDTask, TaskResolver} -class GUIDTaskUnregister2Test extends ActorTest { +class GUIDTaskUnregisterAmmoTest extends ActorTest { "UnregisterEquipment -> UnregisterObjectTask" in { val (guid, uns, taskResolver, probe) = GUIDTaskTest.CommonTestSetup val obj = AmmoBox(GlobalDefinitions.energy_cell) diff --git a/common/src/test/scala/objects/guidtask/GUIDTaskUnregister5Test.scala b/common/src/test/scala/objects/guidtask/GUIDTaskUnregisterAvatarTest.scala similarity index 96% rename from common/src/test/scala/objects/guidtask/GUIDTaskUnregister5Test.scala rename to common/src/test/scala/objects/guidtask/GUIDTaskUnregisterAvatarTest.scala index 561f67a4..6bddc5be 100644 --- a/common/src/test/scala/objects/guidtask/GUIDTaskUnregister5Test.scala +++ b/common/src/test/scala/objects/guidtask/GUIDTaskUnregisterAvatarTest.scala @@ -6,7 +6,7 @@ import net.psforever.objects._ import net.psforever.objects.guid.{GUIDTask, TaskResolver} import net.psforever.types.{CharacterGender, CharacterVoice, PlanetSideEmpire} -class GUIDTaskUnregister5Test extends ActorTest { +class GUIDTaskUnregisterAvatarTest extends ActorTest { "UnregisterAvatar" in { val (guid, uns, taskResolver, probe) = GUIDTaskTest.CommonTestSetup val obj = Player(Avatar("test", PlanetSideEmpire.TR, CharacterGender.Male, 0, CharacterVoice.Mute)) diff --git a/common/src/test/scala/objects/guidtask/GUIDTaskUnregister1Test.scala b/common/src/test/scala/objects/guidtask/GUIDTaskUnregisterObjectTest.scala similarity index 90% rename from common/src/test/scala/objects/guidtask/GUIDTaskUnregister1Test.scala rename to common/src/test/scala/objects/guidtask/GUIDTaskUnregisterObjectTest.scala index e26629b6..9f91dc7b 100644 --- a/common/src/test/scala/objects/guidtask/GUIDTaskUnregister1Test.scala +++ b/common/src/test/scala/objects/guidtask/GUIDTaskUnregisterObjectTest.scala @@ -4,7 +4,7 @@ package objects.guidtask import base.ActorTest import net.psforever.objects.guid.{GUIDTask, TaskResolver} -class GUIDTaskUnregister1Test extends ActorTest { +class GUIDTaskUnregisterObjectTest extends ActorTest { "UnregisterObjectTask" in { val (guid, uns, taskResolver, probe) = GUIDTaskTest.CommonTestSetup val obj = new GUIDTaskTest.TestObject diff --git a/common/src/test/scala/objects/guidtask/GUIDTaskUnregister6Test.scala b/common/src/test/scala/objects/guidtask/GUIDTaskUnregisterPlayerTest.scala similarity index 96% rename from common/src/test/scala/objects/guidtask/GUIDTaskUnregister6Test.scala rename to common/src/test/scala/objects/guidtask/GUIDTaskUnregisterPlayerTest.scala index 59891d6f..3a4bf21a 100644 --- a/common/src/test/scala/objects/guidtask/GUIDTaskUnregister6Test.scala +++ b/common/src/test/scala/objects/guidtask/GUIDTaskUnregisterPlayerTest.scala @@ -6,7 +6,7 @@ import net.psforever.objects._ import net.psforever.objects.guid.{GUIDTask, TaskResolver} import net.psforever.types.{CharacterGender, CharacterVoice, PlanetSideEmpire} -class GUIDTaskUnregister6Test extends ActorTest { +class GUIDTaskUnregisterPlayerTest extends ActorTest { "UnregisterPlayer" in { val (guid, uns, taskResolver, probe) = GUIDTaskTest.CommonTestSetup val obj = Player(Avatar("test", PlanetSideEmpire.TR, CharacterGender.Male, 0, CharacterVoice.Mute)) diff --git a/common/src/test/scala/objects/guidtask/GUIDTaskUnregister3Test.scala b/common/src/test/scala/objects/guidtask/GUIDTaskUnregisterToolTest.scala similarity index 93% rename from common/src/test/scala/objects/guidtask/GUIDTaskUnregister3Test.scala rename to common/src/test/scala/objects/guidtask/GUIDTaskUnregisterToolTest.scala index 6da3e31a..49352784 100644 --- a/common/src/test/scala/objects/guidtask/GUIDTaskUnregister3Test.scala +++ b/common/src/test/scala/objects/guidtask/GUIDTaskUnregisterToolTest.scala @@ -5,7 +5,7 @@ import base.ActorTest import net.psforever.objects._ import net.psforever.objects.guid.{GUIDTask, TaskResolver} -class GUIDTaskUnregister3Test extends ActorTest { +class GUIDTaskUnregisterToolTest extends ActorTest { "UnregisterEquipment -> UnregisterTool" in { val (guid, uns, taskResolver, probe) = GUIDTaskTest.CommonTestSetup val obj = Tool(GlobalDefinitions.beamer) diff --git a/common/src/test/scala/objects/guidtask/GUIDTaskUnregisterTurretTest.scala b/common/src/test/scala/objects/guidtask/GUIDTaskUnregisterTurretTest.scala new file mode 100644 index 00000000..35821890 --- /dev/null +++ b/common/src/test/scala/objects/guidtask/GUIDTaskUnregisterTurretTest.scala @@ -0,0 +1,31 @@ +// Copyright (c) 2017 PSForever +package objects.guidtask + +import base.ActorTest +import net.psforever.objects._ +import net.psforever.objects.guid.{GUIDTask, TaskResolver} + +class GUIDTaskUnregisterTurretTest extends ActorTest { + "UnregisterDeployableTurret" in { + val (guid, uns, taskResolver, probe) = GUIDTaskTest.CommonTestSetup + val obj = new TurretDeployable(GlobalDefinitions.portable_manned_turret_vs) + val obj_wep = obj.Weapons(1).Equipment.get + val obj_ammo = obj_wep.asInstanceOf[Tool].AmmoSlot.Box + val obj_res = obj.Inventory.Items.map(_.obj) + guid.register(obj, "dynamic") + guid.register(obj_wep, "dynamic") + guid.register(obj_ammo, "dynamic") + obj_res.foreach(box => guid.register(box, "dynamic")) + + assert(obj.HasGUID) + assert(obj_wep.HasGUID) + assert(obj_ammo.HasGUID) + obj_res.foreach(box => box.HasGUID) + taskResolver ! TaskResolver.GiveTask(new GUIDTaskTest.RegisterTestTask(probe.ref), List(GUIDTask.UnregisterDeployableTurret(obj)(uns))) + probe.expectMsg(scala.util.Success) + assert(!obj.HasGUID) + assert(!obj_wep.HasGUID) + assert(!obj_ammo.HasGUID) + obj_res.foreach(box => !box.HasGUID) + } +} diff --git a/common/src/test/scala/objects/guidtask/GUIDTaskUnregister4Test.scala b/common/src/test/scala/objects/guidtask/GUIDTaskUnregisterVehicleTest.scala similarity index 95% rename from common/src/test/scala/objects/guidtask/GUIDTaskUnregister4Test.scala rename to common/src/test/scala/objects/guidtask/GUIDTaskUnregisterVehicleTest.scala index 5c1df0a4..6e3d9121 100644 --- a/common/src/test/scala/objects/guidtask/GUIDTaskUnregister4Test.scala +++ b/common/src/test/scala/objects/guidtask/GUIDTaskUnregisterVehicleTest.scala @@ -5,7 +5,7 @@ import base.ActorTest import net.psforever.objects._ import net.psforever.objects.guid.{GUIDTask, TaskResolver} -class GUIDTaskUnregister4Test extends ActorTest { +class GUIDTaskUnregisterVehicleTest extends ActorTest { "RegisterVehicle" in { val (guid, uns, taskResolver, probe) = GUIDTaskTest.CommonTestSetup val obj = Vehicle(GlobalDefinitions.fury) diff --git a/common/src/test/scala/objects/number/UniqueNumberSystemTest.scala b/common/src/test/scala/objects/number/UniqueNumberSystemTest.scala index 31a11655..17e7efe6 100644 --- a/common/src/test/scala/objects/number/UniqueNumberSystemTest.scala +++ b/common/src/test/scala/objects/number/UniqueNumberSystemTest.scala @@ -9,7 +9,7 @@ import net.psforever.objects.guid.actor.{NumberPoolActor, Register, UniqueNumber import net.psforever.objects.guid.selector.RandomSelector import net.psforever.objects.guid.source.LimitedNumberSource -import scala.concurrent.duration.Duration +import scala.concurrent.duration._ import scala.util.{Failure, Success} class AllocateNumberPoolActors extends ActorTest { @@ -309,6 +309,33 @@ class UniqueNumberSystemTest9 extends ActorTest() { } } +class UniqueNumberSystemTestA extends ActorTest { + class EntityTestClass extends IdentifiableEntity + + "UniqueNumberSystem" should { + "remain consistent between registrations" in { + val src : LimitedNumberSource = LimitedNumberSource(10) + val guid : NumberPoolHub = new NumberPoolHub(src) + guid.AddPool("pool1", (0 until 10).toList).Selector = new RandomSelector + val uns = system.actorOf(Props(classOf[UniqueNumberSystem], guid, UniqueNumberSystemTest.AllocateNumberPoolActors(guid)), "uns") + expectNoMsg(Duration.create(200, "ms")) + + assert(src.CountUsed == 0) + (0 to 4).foreach(i => { assert(guid.register(new EntityTestClass(), i).isSuccess) }) + assert(src.CountUsed == 5) + + (0 to 5).foreach(_ => { uns ! Register(new EntityTestClass(), "pool1") }) + assert(receiveOne(200 milliseconds).isInstanceOf[Success[_]]) //6th + assert(receiveOne(200 milliseconds).isInstanceOf[Success[_]]) //7th + assert(receiveOne(200 milliseconds).isInstanceOf[Success[_]]) //8th + assert(receiveOne(200 milliseconds).isInstanceOf[Success[_]]) //9th + assert(receiveOne(200 milliseconds).isInstanceOf[Success[_]]) //10th + assert(receiveOne(200 milliseconds).isInstanceOf[Failure[_]]) //no more + assert(src.CountUsed == 10) + } + } +} + object UniqueNumberSystemTest { /** * @see `UniqueNumberSystem.AllocateNumberPoolActors(NumberPoolHub)(implicit ActorContext)` diff --git a/common/src/test/scala/service/AvatarServiceTest.scala b/common/src/test/scala/service/AvatarServiceTest.scala index f4dc9c00..64e6ebd2 100644 --- a/common/src/test/scala/service/AvatarServiceTest.scala +++ b/common/src/test/scala/service/AvatarServiceTest.scala @@ -122,6 +122,29 @@ class EquipmentInHandTest extends ActorTest { } } +class DeployItemTest extends ActorTest { + ServiceManager.boot(system) ! ServiceManager.Register(RandomPool(1).props(Props[TaskResolver]), "taskResolver") + val service = system.actorOf(Props[AvatarService], "deploy-item-test-service") + val objDef = GlobalDefinitions.motionalarmsensor + val obj = new SensorDeployable(objDef) + obj.Position = Vector3(1,2,3) + obj.Orientation = Vector3(4,5,6) + obj.GUID = PlanetSideGUID(40) + val pkt = ObjectCreateMessage( + objDef.ObjectId, + obj.GUID, + objDef.Packet.ConstructorData(obj).get + ) + + "AvatarService" should { + "pass DeployItem" in { + service ! Service.Join("test") + service ! AvatarServiceMessage("test", AvatarAction.DeployItem(PlanetSideGUID(10), obj)) + expectMsg(AvatarServiceResponse("/test/Avatar", PlanetSideGUID(10), AvatarResponse.DropItem(pkt))) + } + } +} + class DroptItemTest extends ActorTest { ServiceManager.boot(system) ! ServiceManager.Register(RandomPool(1).props(Props[TaskResolver]), "taskResolver") val service = system.actorOf(Props[AvatarService], "release-test-service") @@ -211,6 +234,18 @@ class ObjectHeldTest extends ActorTest { } } +class PutDownFDUTest extends ActorTest { + "AvatarService" should { + "pass PutDownFDU" in { + ServiceManager.boot(system) + val service = system.actorOf(Props[AvatarService], AvatarServiceTest.TestName) + service ! Service.Join("test") + service ! AvatarServiceMessage("test", AvatarAction.PutDownFDU(PlanetSideGUID(10))) + expectMsg(AvatarServiceResponse("/test/Avatar", PlanetSideGUID(10), AvatarResponse.PutDownFDU(PlanetSideGUID(10)))) + } + } +} + class PlanetsideAttributeTest extends ActorTest { "AvatarService" should { "pass PlanetsideAttribute" in { @@ -342,6 +377,33 @@ class ChangeFireStateStopTest extends ActorTest { } } +class DamageTest extends ActorTest { + val test_func_ref : (Any)=>Unit = { + def test_func(o : Any) : Unit = { } + test_func + } + val player = Player(Avatar("TestCharacter", PlanetSideEmpire.VS, CharacterGender.Female, 1, CharacterVoice.Voice1)) + + "AvatarService" should { + "pass Damage" in { + ServiceManager.boot(system) + val service = system.actorOf(Props[AvatarService], AvatarServiceTest.TestName) + service ! Service.Join("test") + service ! AvatarServiceMessage("test", AvatarAction.Damage(PlanetSideGUID(10), player, test_func_ref)) + val msg = receiveOne(1 seconds) + assert(msg.isInstanceOf[AvatarServiceResponse]) + assert(msg.asInstanceOf[AvatarServiceResponse].toChannel == "/test/Avatar") + assert(msg.asInstanceOf[AvatarServiceResponse].avatar_guid == PlanetSideGUID(10)) + assert(msg.asInstanceOf[AvatarServiceResponse].replyMessage + .isInstanceOf[AvatarResponse.DamageResolution]) + assert(msg.asInstanceOf[AvatarServiceResponse].replyMessage + .asInstanceOf[AvatarResponse.DamageResolution].target == player) + assert(msg.asInstanceOf[AvatarServiceResponse].replyMessage + .asInstanceOf[AvatarResponse.DamageResolution].resolution_function == test_func_ref) + } + } +} + class WeaponDryFireTest extends ActorTest { "AvatarService" should { "pass WeaponDryFire" in { diff --git a/common/src/test/scala/service/LocalServiceTest.scala b/common/src/test/scala/service/LocalServiceTest.scala index 0819252d..55001453 100644 --- a/common/src/test/scala/service/LocalServiceTest.scala +++ b/common/src/test/scala/service/LocalServiceTest.scala @@ -1,27 +1,32 @@ +// Copyright (c) 2017 PSForever package service -// Copyright (c) 2017 PSForever import akka.actor.Props import base.ActorTest +import net.psforever.objects.{GlobalDefinitions, SensorDeployable} import net.psforever.objects.serverobject.PlanetSideServerObject -import net.psforever.packet.game.PlanetSideGUID +import net.psforever.packet.game._ import net.psforever.types.{PlanetSideEmpire, Vector3} -import services.Service +import services.{Service, ServiceManager} import services.local._ class LocalService1Test extends ActorTest { + ServiceManager.boot(system) + "LocalService" should { "construct" in { - system.actorOf(Props[LocalService], "service") + system.actorOf(Props[LocalService], "l_service") assert(true) } } } class LocalService2Test extends ActorTest { + ServiceManager.boot(system) + "LocalService" should { "subscribe" in { - val service = system.actorOf(Props[LocalService], "service") + val service = system.actorOf(Props[LocalService], "l_service") service ! Service.Join("test") assert(true) } @@ -29,9 +34,11 @@ class LocalService2Test extends ActorTest { } class LocalService3Test extends ActorTest { + ServiceManager.boot(system) + "LocalService" should { "subscribe to a specific channel" in { - val service = system.actorOf(Props[LocalService], "service") + val service = system.actorOf(Props[LocalService], "l_service") service ! Service.Join("test") service ! Service.Leave() assert(true) @@ -40,9 +47,11 @@ class LocalService3Test extends ActorTest { } class LocalService4Test extends ActorTest { + ServiceManager.boot(system) + "LocalService" should { "subscribe" in { - val service = system.actorOf(Props[LocalService], "service") + val service = system.actorOf(Props[LocalService], "l_service") service ! Service.Join("test") service ! Service.LeaveAll() assert(true) @@ -51,9 +60,11 @@ class LocalService4Test extends ActorTest { } class LocalService5Test extends ActorTest { + ServiceManager.boot(system) + "LocalService" should { "pass an unhandled message" in { - val service = system.actorOf(Props[LocalService], "service") + val service = system.actorOf(Props[LocalService], "l_service") service ! Service.Join("test") service ! "hello" expectNoMsg() @@ -61,10 +72,50 @@ class LocalService5Test extends ActorTest { } } +class AlertDestroyDeployableTest extends ActorTest { + ServiceManager.boot(system) + val obj = new SensorDeployable(GlobalDefinitions.motionalarmsensor) + + "LocalService" should { + "pass AlertDestroyDeployable" in { + val service = system.actorOf(Props[LocalService], "l_service") + service ! Service.Join("test") + service ! LocalServiceMessage("test", LocalAction.AlertDestroyDeployable(PlanetSideGUID(10), obj)) + expectMsg(LocalServiceResponse("/test/Local", PlanetSideGUID(0), LocalResponse.AlertDestroyDeployable(obj))) + } + } +} + +class DeployableMapIconTest extends ActorTest { + ServiceManager.boot(system) + + "LocalService" should { + "pass DeployableMapIcon" in { + val service = system.actorOf(Props[LocalService], "l_service") + service ! Service.Join("test") + service ! LocalServiceMessage("test", + LocalAction.DeployableMapIcon( + PlanetSideGUID(10), + DeploymentAction.Build, + DeployableInfo(PlanetSideGUID(40), DeployableIcon.Boomer, Vector3(1,2,3), PlanetSideGUID(11)) + ) + ) + expectMsg(LocalServiceResponse("/test/Local", PlanetSideGUID(10), + LocalResponse.DeployableMapIcon( + DeploymentAction.Build, + DeployableInfo(PlanetSideGUID(40), DeployableIcon.Boomer, Vector3(1,2,3), PlanetSideGUID(11)) + ) + )) + } + } +} + class DoorClosesTest extends ActorTest { + ServiceManager.boot(system) + "LocalService" should { "pass DoorCloses" in { - val service = system.actorOf(Props[LocalService], "service") + val service = system.actorOf(Props[LocalService], "l_service") service ! Service.Join("test") service ! LocalServiceMessage("test", LocalAction.DoorCloses(PlanetSideGUID(10), PlanetSideGUID(40))) expectMsg(LocalServiceResponse("/test/Local", PlanetSideGUID(10), LocalResponse.DoorCloses(PlanetSideGUID(40)))) @@ -73,6 +124,7 @@ class DoorClosesTest extends ActorTest { } class HackClearTest extends ActorTest { + ServiceManager.boot(system) val obj = new PlanetSideServerObject() { def Faction = PlanetSideEmpire.NEUTRAL def Definition = null @@ -81,7 +133,7 @@ class HackClearTest extends ActorTest { "LocalService" should { "pass HackClear" in { - val service = system.actorOf(Props[LocalService], "service") + val service = system.actorOf(Props[LocalService], "l_service") service ! Service.Join("test") service ! LocalServiceMessage("test", LocalAction.HackClear(PlanetSideGUID(10), obj, 0L, 1000L)) expectMsg(LocalServiceResponse("/test/Local", PlanetSideGUID(10), LocalResponse.HackClear(PlanetSideGUID(40), 0L, 1000L))) @@ -90,9 +142,11 @@ class HackClearTest extends ActorTest { } class ProximityTerminalEffectTest extends ActorTest { + ServiceManager.boot(system) + "LocalService" should { "pass ProximityTerminalEffect" in { - val service = system.actorOf(Props[LocalService], "service") + val service = system.actorOf(Props[LocalService], "l_service") service ! Service.Join("test") service ! LocalServiceMessage("test", LocalAction.ProximityTerminalEffect(PlanetSideGUID(10), PlanetSideGUID(40), true)) expectMsg(LocalServiceResponse("/test/Local", PlanetSideGUID(10), LocalResponse.ProximityTerminalEffect(PlanetSideGUID(40), true))) @@ -100,12 +154,66 @@ class ProximityTerminalEffectTest extends ActorTest { } } +class SetEmpireTest extends ActorTest { + ServiceManager.boot(system) + val obj = new SensorDeployable(GlobalDefinitions.motionalarmsensor) + + "LocalService" should { + "pass SetEmpire" in { + val service = system.actorOf(Props[LocalService], "l_service") + service ! Service.Join("test") + service ! LocalServiceMessage("test", LocalAction.SetEmpire(PlanetSideGUID(10), PlanetSideEmpire.TR)) + expectMsg(LocalServiceResponse("/test/Local", PlanetSideGUID(0), LocalResponse.SetEmpire(PlanetSideGUID(10), PlanetSideEmpire.TR))) + } + } +} + +class TriggerEffectTest extends ActorTest { + ServiceManager.boot(system) + + "LocalService" should { + "pass TriggerEffect (1)" in { + val service = system.actorOf(Props[LocalService], "l_service") + service ! Service.Join("test") + service ! LocalServiceMessage("test", LocalAction.TriggerEffect(PlanetSideGUID(10), "on", PlanetSideGUID(40))) + expectMsg(LocalServiceResponse("/test/Local", PlanetSideGUID(10), LocalResponse.TriggerEffect(PlanetSideGUID(40), "on", None, None))) + } + } +} + +class TriggerEffectInfoTest extends ActorTest { + ServiceManager.boot(system) + + "LocalService" should { + "pass TriggerEffect (2)" in { + val service = system.actorOf(Props[LocalService], "l_service") + service ! Service.Join("test") + service ! LocalServiceMessage("test", LocalAction.TriggerEffectInfo(PlanetSideGUID(10), "on", PlanetSideGUID(40), true, 1000)) + expectMsg(LocalServiceResponse("/test/Local", PlanetSideGUID(10), LocalResponse.TriggerEffect(PlanetSideGUID(40), "on", Some(TriggeredEffect(true, 1000)), None))) + } + } +} + +class TriggerEffectLocationTest extends ActorTest { + ServiceManager.boot(system) + + "LocalService" should { + "pass TriggerEffect (3)" in { + val service = system.actorOf(Props[LocalService], "l_service") + service ! Service.Join("test") + service ! LocalServiceMessage("test", LocalAction.TriggerEffectLocation(PlanetSideGUID(10), "spawn_object_failed_effect", Vector3(1.1f, 2.2f, 3.3f), Vector3(4.4f, 5.5f, 6.6f))) + expectMsg(LocalServiceResponse("/test/Local", PlanetSideGUID(10), LocalResponse.TriggerEffect(PlanetSideGUID(0), "spawn_object_failed_effect", None, Some(TriggeredEffectLocation(Vector3(1.1f, 2.2f, 3.3f), Vector3(4.4f, 5.5f, 6.6f)))))) + } + } +} + class TriggerSoundTest extends ActorTest { import net.psforever.packet.game.TriggeredSound + ServiceManager.boot(system) "LocalService" should { "pass TriggerSound" in { - val service = system.actorOf(Props[LocalService], "service") + val service = system.actorOf(Props[LocalService], "l_service") service ! Service.Join("test") service ! LocalServiceMessage("test", LocalAction.TriggerSound(PlanetSideGUID(10), TriggeredSound.LockedOut, Vector3(1.1f, 2.2f, 3.3f), 0, 0.75f)) expectMsg(LocalServiceResponse("/test/Local", PlanetSideGUID(10), LocalResponse.TriggerSound(TriggeredSound.LockedOut, Vector3(1.1f, 2.2f, 3.3f), 0, 0.75f))) diff --git a/pslogin/src/main/scala/Maps.scala b/pslogin/src/main/scala/Maps.scala index 4263cbff..68163d7d 100644 --- a/pslogin/src/main/scala/Maps.scala +++ b/pslogin/src/main/scala/Maps.scala @@ -13,7 +13,7 @@ import net.psforever.objects.serverobject.terminals.{ProximityTerminal, Terminal import net.psforever.objects.serverobject.terminals.{CaptureTerminal, ProximityTerminal, Terminal} import net.psforever.objects.serverobject.tube.SpawnTube import net.psforever.objects.serverobject.resourcesilo.ResourceSilo -import net.psforever.objects.serverobject.turret.MannedTurret +import net.psforever.objects.serverobject.turret.FacilityTurret import net.psforever.types.Vector3 object Maps { @@ -46,7 +46,7 @@ object Maps { def Building5() : Unit = { // Akkan Dropship Center, Ishundar (ID: 24) LocalBuilding(5, FoundationBuilder(Building.Structure(StructureType.Facility, Vector3(2698.6406f, 4336.914f, 52.046875f)))) //todo ? change ? - + //LocalObject(220, CaptureTerminal.Constructor(capture_terminal)) //Akkan IDs for courtyard/lobby/dish/bunkers, no access to tower or basement with exception of stairs and backdoor areas only, can't proceed past it into basement) //Akkan Doors @@ -123,6 +123,7 @@ object Maps { // LocalObject(1901, ProximityTerminal.Constructor(medical_terminal)) //Akkan ObjectToBuilding + //ObjectToBuilding(220, 5) ObjectToBuilding(281, 5) ObjectToBuilding(282, 5) ObjectToBuilding(283, 5) @@ -477,7 +478,7 @@ object Maps { ObjectToBuilding(923, 10) ObjectToBuilding(932, 10) ObjectToBuilding(933, 10) - ObjectToBuilding(959, 10) + //ObjectToBuilding(959, 10) //TODO LLU socket ObjectToBuilding(971, 10) ObjectToBuilding(1105, 10) @@ -554,6 +555,7 @@ object Maps { def Building11() : Unit = { // Irkalla LocalBuilding(11, FoundationBuilder(Building.Structure(StructureType.Facility, Vector3(4812f, 5212f, 0)))) // Todo change pos + LocalObject(227, CaptureTerminal.Constructor(capture_terminal)) LocalObject(562, Door.Constructor) LocalObject(563, Door.Constructor) LocalObject(566, Door.Constructor) @@ -632,6 +634,8 @@ object Maps { // ObjectToBuilding(1912, 11) // ObjectToBuilding(1913, 11) + ObjectToBuilding(227, 11) + ObjectToBuilding(562, 11) ObjectToBuilding(563, 11) ObjectToBuilding(566, 11) @@ -722,6 +726,7 @@ object Maps { def Building25() : Unit = { // Gate Outpost Watch Tower (North of Forseral Warpgate), Ishundar (ID: 74) LocalBuilding(25, FoundationBuilder(Building.Structure(StructureType.Tower, Vector3(5404f, 4222f, 0)))) // TODO loc + LocalObject(2797, CaptureTerminal.Constructor(secondary_capture)) LocalObject(2973, Door.Constructor) LocalObject(2974, Door.Constructor) LocalObject(590, Door.Constructor(Vector3(5405.992f, 4220.1797f, 39.1875f), Vector3(0, 0, 180))) @@ -742,6 +747,7 @@ object Maps { LocalObject(2749, SpawnTube.Constructor(respawn_tube_tower, Vector3(5404.8125f, 4215.7344f, 29.484375f), Vector3(0, 0, 90))) LocalObject(2750, SpawnTube.Constructor(respawn_tube_tower, Vector3(5404.7656f, 4232.1562f, 29.484375f), Vector3(0, 0, 90))) + ObjectToBuilding(2797, 25) ObjectToBuilding(2973, 25) ObjectToBuilding(2974, 25) ObjectToBuilding(590, 25) @@ -814,6 +820,7 @@ object Maps { } def Building34() : Unit = { // SE Hanish Gun Tower (ID: 60) LocalBuilding(34, FoundationBuilder(Building.Structure(StructureType.Tower, Vector3(4422f, 4852f, 0)))) // TODO loc + LocalObject(2789, CaptureTerminal.Constructor(secondary_capture)) LocalObject(2951, Door.Constructor) LocalObject(2952, Door.Constructor) LocalObject(525, Door.Constructor(Vector3(4423.9766f, 4850.164f, 86.203125f), Vector3(0, 0, 180))) @@ -830,6 +837,7 @@ object Maps { LocalObject(2727, SpawnTube.Constructor(respawn_tube_tower, Vector3(4422.8203f, 4845.711f, 76.4375f), Vector3(0, 0, 90))) LocalObject(2728, SpawnTube.Constructor(respawn_tube_tower, Vector3(4422.7344f, 4862.1406f, 76.4375f), Vector3(0, 0, 90))) + ObjectToBuilding(2789, 34) ObjectToBuilding(2951, 34) ObjectToBuilding(2952, 34) ObjectToBuilding(525, 34) @@ -851,8 +859,10 @@ object Maps { DoorToLock(527, 1150) DoorToLock(528, 1151) } + def Building35() : Unit = { // NE Akkan Watch Tower, Ishundar (ID: 69) LocalBuilding(35, FoundationBuilder(Building.Structure(StructureType.Tower, Vector3(3096f, 5037f, 0)))) // TODO loc + LocalObject(2779, CaptureTerminal.Constructor(secondary_capture)) LocalObject(2917, Door.Constructor) LocalObject(2918, Door.Constructor) LocalObject(414, Door.Constructor(Vector3(3098.0f, 5032.1484f, 81.9375f), Vector3(0, 0, 180))) @@ -873,6 +883,7 @@ object Maps { LocalObject(2693, SpawnTube.Constructor(respawn_tube_tower, Vector3(3096.6562f, 5027.742f, 72.1875f), Vector3(0, 0, 90))) LocalObject(2694, SpawnTube.Constructor(respawn_tube_tower, Vector3(3096.7812f, 5044.1562f, 72.1875f), Vector3(0, 0, 90))) + ObjectToBuilding(2779, 35) ObjectToBuilding(2917, 35) ObjectToBuilding(2918, 35) ObjectToBuilding(414, 35) @@ -900,8 +911,10 @@ object Maps { DoorToLock(418, 1063) DoorToLock(419, 1064) } + def Building36() : Unit = { // West Girru Air Tower, Ishundar (ID: 83) LocalBuilding(36, FoundationBuilder(Building.Structure(StructureType.Tower, Vector3(3748f, 6042f, 0)))) // TODO loc + LocalObject(2785, CaptureTerminal.Constructor(secondary_capture)) LocalObject(2935, Door.Constructor) LocalObject(2936, Door.Constructor) LocalObject(477, Door.Constructor(Vector3(3750.0f, 6040.164f, 56.203125f), Vector3(0, 0, 180))) @@ -918,6 +931,7 @@ object Maps { LocalObject(2711, SpawnTube.Constructor(respawn_tube_tower, Vector3(3748.7266f, 6035.7344f, 46.453125f), Vector3(0, 0, 90))) LocalObject(2712, SpawnTube.Constructor(respawn_tube_tower, Vector3(3748.6328f, 6052.125f, 46.453125f), Vector3(0, 0, 90))) + ObjectToBuilding(2785, 36) ObjectToBuilding(2935, 36) ObjectToBuilding(2936, 36) ObjectToBuilding(477, 36) @@ -939,8 +953,10 @@ object Maps { DoorToLock(479, 1109) DoorToLock(480, 1110) } + def Building55() : Unit = { // South Irkalla Air Tower, Ishundar (ID: 86) LocalBuilding(55, FoundationBuilder(Building.Structure(StructureType.Tower, Vector3(4894f, 4935f, 0)))) // TODO loc + LocalObject(2795, CaptureTerminal.Constructor(secondary_capture)) LocalObject(2969, Door.Constructor) LocalObject(2970, Door.Constructor) LocalObject(581, Door.Constructor(Vector3(4896.0156f, 4932.125f, 67.75f), Vector3(0, 0, 180))) @@ -957,6 +973,7 @@ object Maps { LocalObject(2745, SpawnTube.Constructor(respawn_tube_tower, Vector3(4894.7734f, 4927.742f, 57.984375f), Vector3(0, 0, 90))) LocalObject(2746, SpawnTube.Constructor(respawn_tube_tower, Vector3(4894.7734f, 4944.117f, 57.984375f), Vector3(0, 0, 90))) + ObjectToBuilding(2795, 55) ObjectToBuilding(2969, 55) ObjectToBuilding(2970, 55) ObjectToBuilding(581, 55) @@ -978,8 +995,10 @@ object Maps { DoorToLock(583, 1197) DoorToLock(584, 1198) } + def Building56() : Unit = { // SW Hanish Air Tower, Ishundar (ID: 82) LocalBuilding(56, FoundationBuilder(Building.Structure(StructureType.Tower, Vector3(3590f, 5290f, 0)))) // TODO loc + LocalObject(2784, CaptureTerminal.Constructor(secondary_capture)) LocalObject(2932, Door.Constructor) LocalObject(2933, Door.Constructor) LocalObject(466, Door.Constructor(Vector3(3592.0f, 5286.1562f, 69.390625f), Vector3(0, 0, 180))) @@ -996,6 +1015,7 @@ object Maps { LocalObject(2708, SpawnTube.Constructor(respawn_tube_tower, Vector3(3590.9062f, 5281.742f, 59.6875f), Vector3(0, 0, 90))) LocalObject(2709, SpawnTube.Constructor(respawn_tube_tower, Vector3(3590.836f, 5298.1484f, 59.6875f), Vector3(0, 0, 90))) + ObjectToBuilding(2784, 56) ObjectToBuilding(2932, 56) ObjectToBuilding(2933, 56) ObjectToBuilding(466, 56) @@ -1017,8 +1037,10 @@ object Maps { DoorToLock(468, 1101) DoorToLock(469, 1102) } + def Building59() : Unit = { // Gate Outpost Watch Tower (South of Cyssor Warpgate), Ishundar (ID: 73) LocalBuilding(59, FoundationBuilder(Building.Structure(StructureType.Tower, Vector3(4668f, 6625f, 0)))) // TODO loc + LocalObject(2793, CaptureTerminal.Constructor(secondary_capture)) LocalObject(2959, Door.Constructor) LocalObject(2960, Door.Constructor) LocalObject(546, Door.Constructor(Vector3(4669.992f, 6620.1562f, 42.875f), Vector3(0, 0, 180))) @@ -1039,6 +1061,7 @@ object Maps { LocalObject(2735, SpawnTube.Constructor(respawn_tube_tower, Vector3(4668.7656f, 6615.7344f, 33.109375f), Vector3(0, 0, 90))) LocalObject(2736, SpawnTube.Constructor(respawn_tube_tower, Vector3(4668.742f, 6632.1562f, 33.109375f), Vector3(0, 0, 90))) + ObjectToBuilding(2793, 59) ObjectToBuilding(2959, 59) ObjectToBuilding(2960, 59) ObjectToBuilding(546, 59) @@ -1066,8 +1089,10 @@ object Maps { DoorToLock(550, 1169) DoorToLock(551, 1170) } + def Building65() : Unit = { // West Hanish Gun Tower, Ishundar (ID: 56) LocalBuilding(65, FoundationBuilder(Building.Structure(StructureType.Tower, Vector3(3012f, 5701f, 0)))) // TODO loc + LocalObject(2778, CaptureTerminal.Constructor(secondary_capture)) LocalObject(2914, Door.Constructor) LocalObject(2915, Door.Constructor) LocalObject(404, Door.Constructor(Vector3(3003.9688f, 5706.1484f, 56.3125f), Vector3(0, 0, 180))) @@ -1084,6 +1109,7 @@ object Maps { LocalObject(2690, SpawnTube.Constructor(respawn_tube_tower, Vector3(3022.711f, 5701.758f, 47f), Vector3(0, 0, 90))) LocalObject(2691, SpawnTube.Constructor(respawn_tube_tower, Vector3(3002.7188f, 5718.1562f, 47f), Vector3(0, 0, 90))) + ObjectToBuilding(2778, 65) ObjectToBuilding(2914, 65) ObjectToBuilding(2915, 65) ObjectToBuilding(404, 65) @@ -1162,6 +1188,7 @@ object Maps { def Building2() : Unit = { //Anguta LocalBuilding(2, FoundationBuilder(Building.Structure(StructureType.Facility, Vector3(3974.2344f, 4287.914f, 0)))) + LocalObject(180, CaptureTerminal.Constructor(capture_terminal)) LocalObject(222, Door.Constructor) //air term building, bay door LocalObject(370, Door.Constructor) //courtyard LocalObject(371, Door.Constructor) //courtyard @@ -1230,14 +1257,14 @@ object Maps { LocalObject(1186, Locker.Constructor) LocalObject(1187, Locker.Constructor) LocalObject(1188, Locker.Constructor) - LocalObject(1418, MannedTurret.Constructor(manned_turret)) - LocalObject(1419, MannedTurret.Constructor(manned_turret)) - LocalObject(1421, MannedTurret.Constructor(manned_turret)) - LocalObject(1426, MannedTurret.Constructor(manned_turret)) - LocalObject(1427, MannedTurret.Constructor(manned_turret)) - LocalObject(1428, MannedTurret.Constructor(manned_turret)) - LocalObject(1431, MannedTurret.Constructor(manned_turret)) - LocalObject(1432, MannedTurret.Constructor(manned_turret)) + LocalObject(1418, FacilityTurret.Constructor(manned_turret)) + LocalObject(1419, FacilityTurret.Constructor(manned_turret)) + LocalObject(1421, FacilityTurret.Constructor(manned_turret)) + LocalObject(1426, FacilityTurret.Constructor(manned_turret)) + LocalObject(1427, FacilityTurret.Constructor(manned_turret)) + LocalObject(1428, FacilityTurret.Constructor(manned_turret)) + LocalObject(1431, FacilityTurret.Constructor(manned_turret)) + LocalObject(1432, FacilityTurret.Constructor(manned_turret)) LocalObject(1492, ProximityTerminal.Constructor(medical_terminal)) //lobby LocalObject(1494, ProximityTerminal.Constructor(medical_terminal)) //kitchen LocalObject(1564, Terminal.Constructor(order_terminal)) @@ -1284,6 +1311,7 @@ object Maps { LocalObject(223, VehicleSpawnPad.Constructor(Vector3(4012.3594f, 4364.8047f, 271.90625f), Vector3(0f, 0f, 0f)) ) + ObjectToBuilding(180, 2) ObjectToBuilding(222, 2) ObjectToBuilding(223, 2) ObjectToBuilding(224, 2) @@ -1472,6 +1500,7 @@ object Maps { LocalObject(1563, Terminal.Constructor(order_terminal)) LocalObject(2138, SpawnTube.Constructor(respawn_tube_tower, Vector3(3870.9688f, 4505.7266f, 259.875f), Vector3(0, 0, 90))) LocalObject(2139, SpawnTube.Constructor(respawn_tube_tower, Vector3(3870.9688f, 4522.1562f, 259.875f), Vector3(0, 0, 90))) + LocalObject(2197, CaptureTerminal.Constructor(secondary_capture)) LocalObject(2315, Door.Constructor) //spawn tube door LocalObject(2316, Door.Constructor) //spawn tube door ObjectToBuilding(364, 48) @@ -1499,6 +1528,7 @@ object Maps { ObjectToBuilding(1563, 48) ObjectToBuilding(2138, 48) ObjectToBuilding(2139, 48) + ObjectToBuilding(2197, 48) ObjectToBuilding(2315, 48) ObjectToBuilding(2316, 48) DoorToLock(364, 857) @@ -1528,8 +1558,8 @@ object Maps { LocalObject(1226, Locker.Constructor) LocalObject(1227, Locker.Constructor) LocalObject(1228, Locker.Constructor) - LocalObject(1440, MannedTurret.Constructor(manned_turret)) - LocalObject(1442, MannedTurret.Constructor(manned_turret)) + LocalObject(1440, FacilityTurret.Constructor(manned_turret)) + LocalObject(1442, FacilityTurret.Constructor(manned_turret)) LocalObject(1591, Terminal.Constructor(order_terminal)) LocalObject(1592, Terminal.Constructor(order_terminal)) LocalObject(1593, Terminal.Constructor(order_terminal)) @@ -1539,6 +1569,7 @@ object Maps { LocalObject(1850, Terminal.Constructor(pad_landing)) //air pad N LocalObject(2156, SpawnTube.Constructor(respawn_tube_tower, Vector3(4364.633f, 3994.125f, 228.1875f), Vector3(0, 0, 90))) LocalObject(2157, SpawnTube.Constructor(respawn_tube_tower, Vector3(4364.633f, 3977.7266f, 228.1875f), Vector3(0, 0, 90))) + LocalObject(2203, CaptureTerminal.Constructor(secondary_capture)) LocalObject(2333, Door.Constructor) //spawn tube door LocalObject(2334, Door.Constructor) //spawn tube door ObjectToBuilding(430, 49) @@ -1568,6 +1599,7 @@ object Maps { ObjectToBuilding(1850, 49) ObjectToBuilding(2156, 49) ObjectToBuilding(2157, 49) + ObjectToBuilding(2203, 49) ObjectToBuilding(2333, 49) ObjectToBuilding(2334, 49) DoorToLock(430, 906) @@ -1811,8 +1843,9 @@ object Maps { LocalObject(557, IFFLock.Constructor) LocalObject(558, IFFLock.Constructor) LocalObject(559, IFFLock.Constructor) - LocalObject(670, MannedTurret.Constructor(manned_turret)) - LocalObject(671, MannedTurret.Constructor(manned_turret)) + LocalObject(670, FacilityTurret.Constructor(manned_turret)) + LocalObject(671, FacilityTurret.Constructor(manned_turret)) + LocalObject(1019, CaptureTerminal.Constructor(secondary_capture)) ObjectToBuilding(330, 29) ObjectToBuilding(331, 29) ObjectToBuilding(332, 29) @@ -1823,6 +1856,7 @@ object Maps { ObjectToBuilding(559, 29) ObjectToBuilding(670, 29) ObjectToBuilding(671, 29) + ObjectToBuilding(1019, 29) DoorToLock(330, 558) DoorToLock(331, 559) DoorToLock(332, 556) diff --git a/pslogin/src/main/scala/WorldSessionActor.scala b/pslogin/src/main/scala/WorldSessionActor.scala index 0e680447..38d9e644 100644 --- a/pslogin/src/main/scala/WorldSessionActor.scala +++ b/pslogin/src/main/scala/WorldSessionActor.scala @@ -5,20 +5,21 @@ import java.util.concurrent.atomic.AtomicInteger import akka.actor.{Actor, ActorRef, Cancellable, MDCContextAware} import net.psforever.packet._ import net.psforever.packet.control._ -import net.psforever.packet.game.{BattleDiagramAction, _} +import net.psforever.packet.game.{BattleDiagramAction, ObjectDetachMessage, _} import scodec.Attempt.{Failure, Successful} import scodec.bits._ -import org.log4s.MDC +import org.log4s.{Logger, MDC} import MDCContextAware.Implicits._ import csr.{CSRWarp, CSRZone, Traveler} import net.psforever.objects.GlobalDefinitions._ import services.ServiceManager.Lookup import net.psforever.objects._ -import net.psforever.objects.avatar.Certification +import net.psforever.objects.avatar.{Certification, DeployableToolbox} import net.psforever.objects.ballistics._ -import net.psforever.objects.definition.ToolDefinition +import net.psforever.objects.ce.{ComplexDeployable, Deployable, DeployedItem, SimpleDeployable} +import net.psforever.objects.definition.{ConstructionFireMode, DeployableDefinition, ObjectDefinition, ToolDefinition} import net.psforever.objects.definition.converter.{CorpseConverter, DestroyedVehicleConverter} -import net.psforever.objects.equipment._ +import net.psforever.objects.equipment.{CItem, _} import net.psforever.objects.loadouts._ import net.psforever.objects.guid.{GUIDTask, Task, TaskResolver} import net.psforever.objects.inventory.{Container, GridInventory, InventoryItem} @@ -38,7 +39,7 @@ import net.psforever.objects.serverobject.structures.{Building, StructureType, W import net.psforever.objects.serverobject.terminals._ import net.psforever.objects.serverobject.terminals.Terminal.TerminalMessage import net.psforever.objects.serverobject.tube.SpawnTube -import net.psforever.objects.serverobject.turret.{MannedTurret, TurretUpgrade} +import net.psforever.objects.serverobject.turret.{FacilityTurret, TurretUpgrade, WeaponTurret} import net.psforever.objects.vehicles.{AccessPermissionGroup, Cargo, Utility, VehicleLockState, _} import net.psforever.objects.vital._ import net.psforever.objects.zones.{InterstellarCluster, Zone} @@ -61,9 +62,10 @@ import akka.pattern.ask import services.local.support.HackCaptureActor class WorldSessionActor extends Actor with MDCContextAware { - import WorldSessionActor._ - private[this] val log = org.log4s.getLogger + import WorldSessionActor._ + + private[this] val log = org.log4s.getLogger var sessionId : Long = 0 var leftRef : ActorRef = ActorRef.noSender var rightRef : ActorRef = ActorRef.noSender @@ -86,21 +88,20 @@ class WorldSessionActor extends Actor with MDCContextAware { var usingMedicalTerminal : Option[PlanetSideGUID] = None var usingProximityTerminal : Set[PlanetSideGUID] = Set.empty var delayedProximityTerminalResets : Map[PlanetSideGUID, Cancellable] = Map.empty - var controlled : Option[Int] = None //keep track of avatar's ServerVehicleOverride state + var controlled : Option[Int] = None + //keep track of avatar's ServerVehicleOverride state var traveler : Traveler = null var deadState : DeadState.Value = DeadState.Dead var whenUsedLastKit : Long = 0 val projectiles : Array[Option[Projectile]] = Array.fill[Option[Projectile]](Projectile.RangeUID - Projectile.BaseUID)(None) - + var drawDeloyableIcon : PlanetSideGameObject with Deployable => Unit = RedrawDeployableIcons var amsSpawnPoint : Option[SpawnTube] = None - var clientKeepAlive : Cancellable = DefaultCancellable.obj var progressBarUpdate : Cancellable = DefaultCancellable.obj var reviveTimer : Cancellable = DefaultCancellable.obj var respawnTimer : Cancellable = DefaultCancellable.obj var cargoMountTimer : Cancellable = DefaultCancellable.obj var cargoDismountTimer : Cancellable = DefaultCancellable.obj - var antChargingTick : Cancellable = DefaultCancellable.obj var antDischargingTick : Cancellable = DefaultCancellable.obj @@ -110,7 +111,8 @@ class WorldSessionActor extends Actor with MDCContextAware { * @param b `true` or `false` (or `null`) * @return 1 for `true`; 0 for `false` */ - implicit def boolToInt(b : Boolean) : Int = if(b) 1 else 0 + implicit def boolToInt(b : Boolean) : Int = if(b) 1 + else 0 override def postStop() = { //TODO normally, player avatar persists a minute or so after disconnect; we are subject to the SessionReaper @@ -122,22 +124,31 @@ class WorldSessionActor extends Actor with MDCContextAware { vehicleService ! Service.Leave() avatarService ! Service.Leave() galaxyService ! Service.Leave() - LivePlayerList.Remove(sessionId) if(player != null && player.HasGUID) { val player_guid = player.GUID //proximity vehicle terminals must be considered too - delayedProximityTerminalResets.foreach({case(_, task) => task.cancel}) + delayedProximityTerminalResets.foreach({ case (_, task) => task.cancel }) usingProximityTerminal.foreach(term_guid => { continent.GUID(term_guid) match { case Some(obj : ProximityTerminal) => - if(obj.NumberUsers > 0 && obj.RemoveUser(player_guid) == 0) { //refer to ProximityTerminalControl when modernizng + if(obj.NumberUsers > 0 && obj.RemoveUser(player_guid) == 0) { + //refer to ProximityTerminalControl when modernizng localService ! LocalServiceMessage(continent.Id, LocalAction.ProximityTerminalEffect(player_guid, term_guid, false)) } case _ => ; } }) - + //handle orphaned deployables + DisownDeployables() + //clean up boomer triggers + val equipment = ( + (player.Holsters() + .zipWithIndex + .map({ case ((slot, index)) => (index, slot.Equipment) }) + .collect { case ((index, Some(obj))) => InventoryItem(obj, index) } + ) ++ player.Inventory.Items) + .filterNot({ case InventoryItem(obj, _) => obj.isInstanceOf[BoomerTrigger] }) //TODO final character save before doing any of this continent.Population ! Zone.Population.Release(avatar) if(player.isAlive) { @@ -150,6 +161,7 @@ class WorldSessionActor extends Actor with MDCContextAware { } avatarService ! AvatarServiceMessage(continent.Id, AvatarAction.ObjectDelete(player_guid, player_guid)) taskResolver ! GUIDTask.UnregisterAvatar(player)(continent.GUID) + //TODO normally, the actual player avatar persists a minute or so after the user disconnects } else if(continent.LivePlayers.contains(player) && !continent.Corpses.contains(player)) { //player disconnected while waiting for a revive @@ -163,7 +175,8 @@ class WorldSessionActor extends Actor with MDCContextAware { avatarService ! AvatarServiceMessage(continent.Id, AvatarAction.Release(player, continent)) taskResolver ! GUIDTask.UnregisterLocker(player.Locker)(continent.GUID) //rest of player will be cleaned up with corpses } - else { //no items in inventory; leave no corpse + else { + //no items in inventory; leave no corpse val player_guid = player.GUID player.Position = Vector3.Zero //save character before doing this avatarService ! AvatarServiceMessage(continent.Id, AvatarAction.ObjectDelete(player_guid, player_guid)) @@ -178,7 +191,6 @@ class WorldSessionActor extends Actor with MDCContextAware { DismountVehicleOnLogOut() } } - DisownVehicle() continent.Population ! Zone.Population.Leave(avatar) } @@ -258,291 +270,16 @@ class WorldSessionActor extends Actor with MDCContextAware { handleControlPkt(ctrl) case GamePacket(_, _, pkt) => handleGamePkt(pkt) - // temporary hack to keep the client from disconnecting - //it's been a "temporary hack" since 2016 :P + // temporary hack to keep the client from disconnecting + //it's been a "temporary hack" since 2016 :P case PokeClient() => sendResponse(KeepAliveMessage()) - case CheckCargoDismount(vehicle_guid, cargo_vehicle_guid, cargo_mountpoint, iteration) => - val vehicle = continent.GUID(vehicle_guid.guid).get.asInstanceOf[Vehicle] - val cargo_vehicle = continent.GUID(cargo_vehicle_guid).get.asInstanceOf[Vehicle] + case AvatarServiceResponse(toChannel, guid, reply) => + HandleAvatarServiceResponse(toChannel, guid, reply) - val distance = Vector3.Distance(vehicle.Position, cargo_vehicle.Position) - - log.info(s"Dismount distance ${distance}") - if(distance > 15 || iteration > 20) { - // Vehicle has moved far enough away - close the cargo door - log.info("Vehicle is far enough away or disembark timed out - closing cargo door and returning full control to driver") - - StartBundlingPackets() - - // Return control of vehicle to driver - DriverVehicleControl(vehicle) - - val cargoStatusMessage = CargoMountPointStatusMessage(cargo_vehicle_guid, PlanetSideGUID(0), PlanetSideGUID(0), vehicle_guid, cargo_mountpoint, CargoStatus.Empty, 0) - log.warn(cargoStatusMessage.toString) - // Do NOT send this packet back to the client directly. If you do and then send it again to all clients in the zone (including the client again) - // The client will get stuck in a state where the player cannot dismount as it thinks it is always trying to remount the cargo hold - avatarService ! AvatarServiceMessage(continent.Id, AvatarAction.SendResponse(player.GUID, cargoStatusMessage)) - - StopBundlingPackets() - - cargoMountTimer.cancel() - cargoDismountTimer.cancel() - } else { - // Not far enough away - rescheduling check - import scala.concurrent.ExecutionContext.Implicits.global - cargoDismountTimer = context.system.scheduler.scheduleOnce(250 milliseconds, self, CheckCargoDismount(vehicle_guid, cargo_vehicle.GUID, cargo_mountpoint, iteration = iteration + 1)) - } - case CheckCargoMounting(vehicle_guid, cargo_vehicle_guid, cargo_mountpoint, iteration) => - val vehicle = continent.GUID(vehicle_guid.guid).get.asInstanceOf[Vehicle] - val cargo_vehicle = continent.GUID(cargo_vehicle_guid.guid).get.asInstanceOf[Vehicle] - - val distance = Vector3.Distance(vehicle.Position, cargo_vehicle.Position) - log.warn(s"Mount distance ${distance}") - if(distance <= 8) { - // Vehicle is close enough that it should be within the cargo bay. Mount it. - log.info("Mounting vehicle cargo") - cargoMountTimer.cancel() - cargoDismountTimer.cancel() - - val vehicle = continent.GUID(vehicle_guid).get.asInstanceOf[Vehicle] - - StartBundlingPackets() - vehicleService ! VehicleServiceMessage(s"${vehicle.Actor}", VehicleAction.SendResponse(PlanetSideGUID(0), PlanetsideAttributeMessage(cargo_vehicle_guid, 0, cargo_vehicle.Health))) - vehicleService ! VehicleServiceMessage(s"${vehicle.Actor}", VehicleAction.SendResponse(PlanetSideGUID(0), PlanetsideAttributeMessage(cargo_vehicle_guid, 68, cargo_vehicle.Shields))) - - val attachMessage = ObjectAttachMessage(cargo_vehicle_guid, vehicle_guid, cargo_mountpoint) - log.warn(attachMessage.toString) - sendResponse(attachMessage) - - // This is required for when DismountVehicleCargoMsg is sent as the cargo_vehicle_guid isn't sent as a parameter - vehicle.MountedIn = cargo_vehicle_guid - cargo_vehicle.CargoHold(cargo_mountpoint).get.Occupant = vehicle - - val orientation = if(vehicle.Definition == GlobalDefinitions.router) { - // mount router "sideways" in a lodestar - //todo: BFRs will likely also need this set - 1 - } else { - 0 - } - - val cargoStatusMessage = CargoMountPointStatusMessage(cargo_vehicle_guid, vehicle_guid, vehicle_guid, PlanetSideGUID(0), cargo_mountpoint, CargoStatus.Occupied, orientation) - log.warn(cargoStatusMessage.toString) - sendResponse(cargoStatusMessage) - - StopBundlingPackets() - - avatarService ! AvatarServiceMessage(continent.Id, AvatarAction.SendResponse(player.GUID, attachMessage)) - avatarService ! AvatarServiceMessage(continent.Id, AvatarAction.SendResponse(player.GUID, cargoStatusMessage)) - } else if (distance > 25 || iteration >= 15) { - // Vehicle is too far away. Abort mounting. - log.info("Vehicle is too far away or didn't mount within allocated time. Aborting cargo mount.") - - val cargoStatusMessage = CargoMountPointStatusMessage(cargo_vehicle_guid, PlanetSideGUID(0), PlanetSideGUID(0), vehicle_guid, cargo_mountpoint, CargoStatus.Empty, 0) - log.warn(cargoStatusMessage.toString) - // Do NOT send this packet back to the client directly. If you do and then send it again to all clients in the zone (including the client again) - // The client will get stuck in a state where the player cannot dismount as it thinks it is always trying to remount the cargo hold - avatarService ! AvatarServiceMessage(continent.Id, AvatarAction.SendResponse(player.GUID, cargoStatusMessage)) - - - cargoMountTimer.cancel() - cargoDismountTimer.cancel() - } - else { - // Not close enough, far away enough or timeout not exceeded. Reschedule check - import scala.concurrent.ExecutionContext.Implicits.global - cargoMountTimer = context.system.scheduler.scheduleOnce(1 second, self, CheckCargoMounting(vehicle_guid, cargo_vehicle_guid, cargo_mountpoint, iteration = iteration + 1)) - } - - case AvatarServiceResponse(_, guid, reply) => - val tplayer_guid = if(player.HasGUID) { player.GUID} else { PlanetSideGUID(-1) } - reply match { - case AvatarResponse.SendResponse(msg) => - sendResponse(msg) - case AvatarResponse.ArmorChanged(suit, subtype) => - if(tplayer_guid != guid) { - sendResponse(ArmorChangedMessage(guid, suit, subtype)) - } - - case AvatarResponse.ChangeAmmo(weapon_guid, weapon_slot, previous_guid, ammo_id, ammo_guid, ammo_data) => - if(tplayer_guid != guid) { - sendResponse(ObjectDetachMessage(weapon_guid, previous_guid, Vector3.Zero, 0)) - sendResponse( - ObjectCreateMessage( - ammo_id, - ammo_guid, - ObjectCreateMessageParent(weapon_guid, weapon_slot), - ammo_data - ) - ) - sendResponse(ChangeAmmoMessage(weapon_guid, 1)) - } - - case AvatarResponse.ChangeFireMode(item_guid, mode) => - if(tplayer_guid != guid) { - sendResponse(ChangeFireModeMessage(item_guid, mode)) - } - - case AvatarResponse.ChangeFireState_Start(weapon_guid) => - if(tplayer_guid != guid) { - sendResponse(ChangeFireStateMessage_Start(weapon_guid)) - } - - case AvatarResponse.ChangeFireState_Stop(weapon_guid) => - if(tplayer_guid != guid) { - sendResponse(ChangeFireStateMessage_Stop(weapon_guid)) - } - - case AvatarResponse.ConcealPlayer() => - if(tplayer_guid != guid) { - sendResponse(GenericObjectActionMessage(guid, 36)) - } - - case AvatarResponse.DamageResolution(target, resolution_function) => - if(player.isAlive) { - resolution_function(target) - - val health = player.Health - val armor = player.Armor - val playerGUID = player.GUID - sendResponse(PlanetsideAttributeMessage(playerGUID, 0, health)) - sendResponse(PlanetsideAttributeMessage(playerGUID, 4, armor)) - avatarService ! AvatarServiceMessage(continent.Id, AvatarAction.PlanetsideAttribute(playerGUID, 0, health)) - avatarService ! AvatarServiceMessage(continent.Id, AvatarAction.PlanetsideAttribute(playerGUID, 4, armor)) - if(health == 0 && player.isAlive) { - KillPlayer(player) - } - } - - case AvatarResponse.Destroy(victim, killer, weapon, pos) => - // guid = victim // killer = killer ;) - sendResponse(DestroyMessage(victim, killer, weapon, pos)) - - case AvatarResponse.DestroyDisplay(killer, victim, method, unk) => - sendResponse(DestroyDisplayMessage(killer, victim, method, unk)) - - case AvatarResponse.DropItem(pkt) => - if(tplayer_guid != guid) { - sendResponse(pkt) - } - - case AvatarResponse.EquipmentInHand(pkt) => - if(tplayer_guid != guid) { - sendResponse(pkt) - } - - case AvatarResponse.HitHint(source_guid) => - sendResponse(HitHint(source_guid, guid)) - - case AvatarResponse.KilledWhileInVehicle() => - if(player.isAlive && player.VehicleSeated.nonEmpty) { - continent.GUID(player.VehicleSeated.get) match { - case Some(vehicle : Vehicle) => - if(vehicle.Health == 0) { - vehicle.LastShot match { - case Some(cause) => - player.History(cause) - case None => ; - } - KillPlayer(player) - } - case _ => ; - } - } - - case AvatarResponse.LoadPlayer(pkt) => - if(tplayer_guid != guid) { - sendResponse(pkt) - } - - case AvatarResponse.ObjectDelete(item_guid, unk) => - if(tplayer_guid != guid) { - sendResponse(ObjectDeleteMessage(item_guid, unk)) - } - - case AvatarResponse.ObjectHeld(slot) => - if(tplayer_guid != guid) { - sendResponse(ObjectHeldMessage(guid, slot, false)) - } - - case AvatarResponse.PlanetsideAttribute(attribute_type, attribute_value) => - if(tplayer_guid != guid) { - sendResponse(PlanetsideAttributeMessage(guid, attribute_type, attribute_value)) - } - - case AvatarResponse.PlayerState(msg, spectating, weaponInHand) => - if(tplayer_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 = Vector3.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)) || - ((msg.vel.isEmpty || Vector3.MagnitudeSquared(msg.vel.get).toInt == 0) && time > 2000) || - (time > 1000)) || - (distanceSq > 160000 && time > 5000)) - { - sendResponse( - PlayerStateMessage( - guid, - location, - msg.vel, - msg.facingYaw, - msg.facingPitch, - msg.facingYawUpper, - unk1 = 0, - msg.is_crouching, - msg.is_jumping, - msg.jump_thrust, - msg.is_cloaked - ) - ) - player.lastSeenStreamMessage(guid.guid) = now - } - } - - case AvatarResponse.Release(tplayer) => - if(tplayer_guid != guid) { - TurnPlayerIntoCorpse(tplayer) - } - - case AvatarResponse.Reload(item_guid) => - if(tplayer_guid != guid) { - sendResponse(ReloadMessage(item_guid, 1, 0)) - } - - case AvatarResponse.StowEquipment(target, slot, item) => - if(tplayer_guid != guid) { - val definition = item.Definition - sendResponse( - ObjectCreateDetailedMessage( - definition.ObjectId, - item.GUID, - ObjectCreateMessageParent(target, slot), - definition.Packet.DetailedConstructorData(item).get - ) - ) - } - - case AvatarResponse.WeaponDryFire(weapon_guid) => - if(tplayer_guid != guid) { - sendResponse(WeaponDryFireMessage(weapon_guid)) - } - - case _ => ; - } + case Door.DoorMessage(tplayer, msg, order) => + HandleDoorMessage(tplayer, msg, order) case GalaxyServiceResponse(_, reply) => reply match { @@ -550,229 +287,17 @@ class WorldSessionActor extends Actor with MDCContextAware { sendResponse(msg) } - case LocalServiceResponse(_, guid, reply) => - val tplayer_guid = if(player.HasGUID) { player.GUID} else { PlanetSideGUID(-1) } - reply match { - case LocalResponse.DoorOpens(door_guid) => - if(tplayer_guid != guid) { - sendResponse(GenericObjectStateMsg(door_guid, 16)) - } + case LocalServiceResponse(toChannel, guid, reply) => + HandleLocalServiceResponse(toChannel, guid, reply) - case LocalResponse.DoorCloses(door_guid) => //door closes for everyone - sendResponse(GenericObjectStateMsg(door_guid, 17)) + case Mountable.MountMessages(tplayer, reply) => + HandleMountMessages(tplayer, reply) - case LocalResponse.HackClear(target_guid, unk1, unk2) => - log.trace(s"Clearing hack for ${target_guid}") - // Reset hack state for all players - sendResponse(HackMessage(0, target_guid, guid, 0, unk1, HackState.HackCleared, unk2)) - // Set the object faction displayed back to it's original owner faction + case Terminal.TerminalMessage(tplayer, msg, order) => + HandleTerminalMessage(tplayer, msg, order) - continent.GUID(target_guid) match { - case Some(obj) => - sendResponse(SetEmpireMessage(target_guid, obj.asInstanceOf[FactionAffinity].Faction)) - case None => ; - } - - case LocalResponse.HackObject(target_guid, unk1, unk2) => - if(tplayer_guid != guid && continent.GUID(target_guid).get.asInstanceOf[Hackable].HackedBy.get._1.Faction != player.Faction) { - // If the player is not in the faction that hacked this object then send the packet that it's been hacked, so they can either unhack it or use the hacked object - // Don't send this to the faction that hacked the object, otherwise it will interfere with the new SetEmpireMessage QoL change that changes the object colour to their faction (but only visible to that faction) - sendResponse(HackMessage(0, target_guid, guid, 100, unk1, HackState.Hacked, unk2)) - } - - if(continent.GUID(target_guid).get.asInstanceOf[Hackable].HackedBy.get._1.Faction == player.Faction){ - // Make the hacked object look like it belongs to the hacking empire, but only for that empire's players (so that infiltrators on stealth missions won't be given away to opposing factions) - sendResponse(SetEmpireMessage(target_guid, player.Faction)) - } - case LocalResponse.HackCaptureTerminal(target_guid, unk1, unk2, isResecured) => - var value = 0L - - if(isResecured) { - value = 17039360L - } else { - import scala.concurrent.ExecutionContext.Implicits.global - val future = ask(localService, HackCaptureActor.GetHackTimeRemainingNanos(target_guid))(1 second) - val time = Await.result(future, 1 second).asInstanceOf[Long] // todo: blocking call. Not good. - val hack_time_remaining_ms = TimeUnit.MILLISECONDS.convert(time, TimeUnit.NANOSECONDS) - val deciseconds_remaining = (hack_time_remaining_ms / 100) - - val hacking_faction = continent.GUID(target_guid).get.asInstanceOf[Hackable].HackedBy.get._1.Faction - - // See PlanetSideAttributeMessage #20 documentation for an explanation of how the timer is calculated - val start_num = hacking_faction match { - case PlanetSideEmpire.TR => 65536L - case PlanetSideEmpire.NC => 131072L - case PlanetSideEmpire.VS => 196608L - } - - value = start_num + deciseconds_remaining - } - - sendResponse(PlanetsideAttributeMessage(target_guid, 20, value)) - case LocalResponse.ProximityTerminalEffect(object_guid, effectState) => - if(tplayer_guid != guid) { - sendResponse(ProximityTerminalUseMessage(PlanetSideGUID(0), object_guid, effectState)) - } - - case LocalResponse.TriggerSound(sound, pos, unk, volume) => - sendResponse(TriggerSoundMessage(sound, pos, unk, volume)) - - case LocalResponse.SetEmpire(object_guid, empire) => - sendResponse(SetEmpireMessage(object_guid, empire)) - case _ => ; - } - - case VehicleServiceResponse(_, guid, reply) => - val tplayer_guid = if(player.HasGUID) { player.GUID} else { PlanetSideGUID(0) } - reply match { - case VehicleResponse.Ownership(vehicle_guid) => - sendResponse(PlanetsideAttributeMessage(guid, 21, vehicle_guid.guid)) - - case VehicleResponse.AttachToRails(vehicle_guid, pad_guid) => - sendResponse(ObjectAttachMessage(pad_guid, vehicle_guid, 3)) - - case VehicleResponse.ChildObjectState(object_guid, pitch, yaw) => - if(tplayer_guid != guid) { - sendResponse(ChildObjectStateMessage(object_guid, pitch, yaw)) - } - - case VehicleResponse.ConcealPlayer(player_guid) => - //TODO this is the correct message; but, I don't know how to undo the effects of it - //sendResponse(GenericObjectActionMessage(player_guid, 36)) - sendResponse(PlanetsideAttributeMessage(player_guid, 29, 1)) - - case VehicleResponse.DismountVehicle(bailType, wasKickedByDriver) => - if(tplayer_guid != guid) { - sendResponse(DismountVehicleMsg(guid, bailType, wasKickedByDriver)) - } - - case VehicleResponse.DeployRequest(object_guid, state, unk1, unk2, pos) => - if(tplayer_guid != guid) { - sendResponse(DeployRequestMessage(guid, object_guid, state, unk1, unk2, pos)) - } - - case VehicleResponse.DetachFromRails(vehicle_guid, pad_guid, pad_position, pad_orientation_z) => - sendResponse(ObjectDetachMessage(pad_guid, vehicle_guid, pad_position + Vector3(0,0,0.5f), pad_orientation_z)) - - case VehicleResponse.EquipmentInSlot(pkt) => - if(tplayer_guid != guid) { - sendResponse(pkt) - } - - case VehicleResponse.HitHint(source_guid) => - sendResponse(HitHint(source_guid, player.GUID)) - - case VehicleResponse.InventoryState(obj, parent_guid, start, con_data) => - if(tplayer_guid != guid) { - //TODO prefer ObjectDetachMessage, but how to force ammo pools to update properly? - val obj_guid = obj.GUID - sendResponse(ObjectDeleteMessage(obj_guid, 0)) - sendResponse( - ObjectCreateDetailedMessage( - obj.Definition.ObjectId, - obj_guid, - ObjectCreateMessageParent(parent_guid, start), - con_data - ) - ) - } - - case msg @ VehicleResponse.KickPassenger(seat_num, wasKickedByDriver, vehicle_guid) => - // seat_num seems to be correct if passenger is kicked manually by driver, but always seems to return 4 if user is kicked by seat permissions - log.info(s"$msg") - sendResponse(DismountVehicleMsg(guid, BailType.Kicked, wasKickedByDriver)) - if(tplayer_guid == guid) { - continent.GUID(vehicle_guid) match { - case Some(obj : Vehicle) => - UnAccessContents(obj) - case _ => ; - } - } - - case VehicleResponse.InventoryState2(obj_guid, parent_guid, value) => - if(tplayer_guid != guid) { - sendResponse(InventoryStateMessage(obj_guid, 0, parent_guid, value)) - } - - case VehicleResponse.LoadVehicle(vehicle, vtype, vguid, vdata) => - //this is not be suitable for vehicles with people who are seated in it before it spawns (if that is possible) - if(tplayer_guid != guid) { - sendResponse(ObjectCreateMessage(vtype, vguid, vdata)) - ReloadVehicleAccessPermissions(vehicle) - } - - case VehicleResponse.MountVehicle(vehicle_guid, seat) => - if(tplayer_guid != guid) { - sendResponse(ObjectAttachMessage(vehicle_guid, guid, seat)) - } - - case VehicleResponse.PlanetsideAttribute(vehicle_guid, attribute_type, attribute_value) => - if(tplayer_guid != guid) { - sendResponse(PlanetsideAttributeMessage(vehicle_guid, attribute_type, attribute_value)) - } - - case VehicleResponse.ResetSpawnPad(pad_guid) => - sendResponse(GenericObjectActionMessage(pad_guid, 92)) - - case VehicleResponse.RevealPlayer(player_guid) => - //TODO see note in ConcealPlayer - sendResponse(PlanetsideAttributeMessage(player_guid, 29, 0)) - - case VehicleResponse.SeatPermissions(vehicle_guid, seat_group, permission) => - if(tplayer_guid != guid) { - sendResponse(PlanetsideAttributeMessage(vehicle_guid, seat_group, permission)) - } - - case VehicleResponse.StowEquipment(vehicle_guid, slot, item_type, item_guid, item_data) => - if(tplayer_guid != guid) { - //TODO prefer ObjectAttachMessage, but how to force ammo pools to update properly? - sendResponse( - ObjectCreateDetailedMessage(item_type, item_guid, ObjectCreateMessageParent(vehicle_guid, slot), item_data) - ) - } - - case VehicleResponse.UnloadVehicle(vehicle_guid) => - sendResponse(ObjectDeleteMessage(vehicle_guid, 0)) - - case VehicleResponse.UnstowEquipment(item_guid) => - if(tplayer_guid != guid) { - //TODO prefer ObjectDetachMessage, but how to force ammo pools to update properly? - sendResponse(ObjectDeleteMessage(item_guid, 0)) - } - - case VehicleResponse.VehicleState(vehicle_guid, unk1, pos, ang, vel, unk2, unk3, unk4, wheel_direction, unk5, unk6) => - if(tplayer_guid != guid) { - sendResponse(VehicleStateMessage(vehicle_guid, unk1, pos, ang, vel, unk2, unk3, unk4, wheel_direction, unk5, unk6)) - if(player.VehicleSeated.contains(vehicle_guid)) { - player.Position = pos - } - } - case VehicleResponse.SendResponse(msg) => - sendResponse(msg) - - case VehicleResponse.UpdateAmsSpawnPoint(list) => - if(player.isBackpack) { - //dismiss old ams spawn point - ClearCurrentAmsSpawnPoint() - //draw new ams spawn point - list - .filter(tube => tube.Faction == player.Faction) - .sortBy(tube => Vector3.DistanceSquared(tube.Position, player.Position)) - .headOption match { - case Some(tube) => - sendResponse( - BattleplanMessage(41378949, "ams", continent.Number, List(BattleDiagramAction(DiagramActionCode.StartDrawing))) - ) - sendResponse( - BattleplanMessage(41378949, "ams", continent.Number, List(BattleDiagramAction.drawString(tube.Position.x, tube.Position.y, 3, 0, "AMS"))) - ) - amsSpawnPoint = Some(tube) - case None => ; - } - } - - case _ => ; - } + case VehicleServiceResponse(toChannel, guid, reply) => + HandleVehicleServiceResponse(toChannel, guid, reply) case Deployment.CanDeploy(obj, state) => val vehicle_guid = obj.GUID @@ -830,565 +355,11 @@ class WorldSessionActor extends Actor with MDCContextAware { val silo_guid = msg.object_guid order match { case ResourceSilo.ChargeEvent() => - antChargingTick.cancel() // If an ANT is refilling a NTU silo it isn't in a warpgate, so disable NTU regeneration + antChargingTick.cancel() // If an ANT is refilling an NTU silo it isn't in a warpgate, so disable NTU regeneration antDischargingTick.cancel() - antDischargingTick = context.system.scheduler.scheduleOnce(1000 milliseconds, self, NtuDischarging(player, continent.GUID(vehicle_guid).get.asInstanceOf[Vehicle], silo_guid)) } - case Door.DoorMessage(tplayer, msg, order) => - val door_guid = msg.object_guid - order match { - case Door.OpenEvent() => - continent.GUID(door_guid) match { - case Some(door : Door) => - sendResponse(GenericObjectStateMsg(door_guid, 16)) - localService ! LocalServiceMessage(continent.Id, LocalAction.DoorOpens (tplayer.GUID, continent, door) ) - - case _ => - log.warn(s"door $door_guid wanted to be opened but could not be found") - } - - case Door.CloseEvent() => - sendResponse(GenericObjectStateMsg(door_guid, 17)) - localService ! LocalServiceMessage(continent.Id, LocalAction.DoorCloses(tplayer.GUID, door_guid)) - - case Door.NoEvent() => ; - } - - case Mountable.MountMessages(tplayer, reply) => - reply match { - case Mountable.CanMount(obj : ImplantTerminalMech, seat_num) => - MountingAction(tplayer, obj, seat_num) - sendResponse(PlanetsideAttributeMessage(obj.GUID, 0, 1000L)) //health of mech - - case Mountable.CanMount(obj : MannedTurret, seat_num) => - obj.WeaponControlledFromSeat(seat_num) match { - case Some(weapon : Tool) => - //update mounted weapon belonging to seat - weapon.AmmoSlots.foreach(slot => { //update the magazine(s) in the weapon, specifically - val magazine = slot.Box - sendResponse(InventoryStateMessage(magazine.GUID, weapon.GUID, magazine.Capacity.toLong)) - }) - case _ => ; //no weapons to update - } - sendResponse(PlanetsideAttributeMessage(obj.GUID, 0, obj.Health)) - MountingAction(tplayer, obj, seat_num) - - case Mountable.CanMount(obj : Vehicle, seat_num) => - val obj_guid : PlanetSideGUID = obj.GUID - val player_guid : PlanetSideGUID = tplayer.GUID - log.info(s"MountVehicleMsg: $player_guid mounts $obj_guid @ $seat_num") - vehicleService ! VehicleServiceMessage.Decon(RemoverActor.ClearSpecific(List(obj), continent)) //clear timer - PlayerActionsToCancel() - sendResponse(PlanetsideAttributeMessage(obj_guid, 0, obj.Health)) - sendResponse(PlanetsideAttributeMessage(obj_guid, 68, 0)) //shield health - sendResponse(PlanetsideAttributeMessage(obj_guid, 113, 0)) //capacitor - if(seat_num == 0) { //simplistic vehicle ownership management - obj.Owner match { - case Some(owner_guid) => - continent.GUID(owner_guid) match { - case Some(previous_owner : Player) => - if(previous_owner.VehicleOwned.contains(obj_guid)) { - previous_owner.VehicleOwned = None //simplistic ownership management, player loses vehicle ownership - } - case _ => ; - } - case None => ; - } - tplayer.VehicleOwned = Some(obj_guid) - obj.Owner = Some(tplayer.GUID) - } - obj.WeaponControlledFromSeat(seat_num) match { - case Some(weapon : Tool) => - //update mounted weapon belonging to seat - weapon.AmmoSlots.foreach(slot => { //update the magazine(s) in the weapon, specifically - val magazine = slot.Box - sendResponse(InventoryStateMessage(magazine.GUID, weapon.GUID, magazine.Capacity)) - }) - case _ => ; //no weapons to update - } - //sendResponse(PlanetsideAttributeMessage(obj.GUID, 0, obj.Health)) //TODO vehicle max health in definition - vehicleService ! VehicleServiceMessage.Decon(RemoverActor.ClearSpecific(List(obj), continent)) //clear timer - AccessContents(obj) - MountingAction(tplayer, obj, seat_num) - - case Mountable.CanMount(obj : Mountable, _) => - log.warn(s"MountVehicleMsg: $obj is some generic mountable object and nothing will happen") - - case Mountable.CanDismount(obj : ImplantTerminalMech, seat_num) => - DismountAction(tplayer, obj, seat_num) - - case Mountable.CanDismount(obj : MannedTurret, seat_num) => - DismountAction(tplayer, obj, seat_num) - - case Mountable.CanDismount(obj : Vehicle, seat_num) => - val player_guid : PlanetSideGUID = tplayer.GUID - if(player_guid == player.GUID) { - //disembarking self - TotalDriverVehicleControl(obj) - UnAccessContents(obj) - DismountAction(tplayer, obj, seat_num) - } - else { - vehicleService ! VehicleServiceMessage(continent.Id, VehicleAction.KickPassenger(player_guid, seat_num, true, obj.GUID)) - } - if(obj.Seats.values.count(_.isOccupied) == 0) { - vehicleService ! VehicleServiceMessage.Decon(RemoverActor.AddTask(obj, continent, obj.Definition.DeconstructionTime)) //start vehicle decay - } - - case Mountable.CanDismount(obj : Mountable, _) => - log.warn(s"DismountVehicleMsg: $obj is some generic mountable object and nothing will happen") - - case Mountable.CanNotMount(obj : Vehicle, seat_num) => - log.warn(s"MountVehicleMsg: $tplayer attempted to mount $obj's seat $seat_num, but was not allowed") - if(obj.SeatPermissionGroup(seat_num).contains(AccessPermissionGroup.Driver)) { - sendResponse(ChatMsg(ChatMessageType.CMT_OPEN, false, "", "You are not the driver of this vehicle.", None)) - } - - case Mountable.CanNotMount(obj : Mountable, seat_num) => - log.warn(s"MountVehicleMsg: $tplayer attempted to mount $obj's seat $seat_num, but was not allowed") - - case Mountable.CanNotDismount(obj, seat_num) => - log.warn(s"DismountVehicleMsg: $tplayer attempted to dismount $obj's seat $seat_num, but was not allowed") - } - - case Terminal.TerminalMessage(tplayer, msg, order) => - order match { - case Terminal.BuyExosuit(exosuit, subtype) => //refresh armor points - tplayer.History(HealFromExoSuitChange(PlayerSource(tplayer), exosuit)) - if(tplayer.ExoSuit == exosuit) { - if(exosuit == ExoSuitType.MAX) { - //special MAX case - clear any special state - player.UsingSpecial = SpecialExoSuitDefinition.Mode.Normal - player.ExoSuit = exosuit - if(Loadout.DetermineSubtype(tplayer) != subtype) { - //special MAX case - suit switching to a different MAX suit; we need to change the main weapon - sendResponse(ArmorChangedMessage(tplayer.GUID, exosuit, subtype)) - avatarService ! AvatarServiceMessage(player.Continent, AvatarAction.ArmorChanged(tplayer.GUID, exosuit, subtype)) - val arms = tplayer.Slot(0).Equipment.get - val putTask = PutEquipmentInSlot(tplayer, Tool(GlobalDefinitions.MAXArms(subtype, tplayer.Faction)), 0) - taskResolver ! DelayedObjectHeld(tplayer, 0, List(TaskResolver.GiveTask(putTask.task, putTask.subs :+ RemoveEquipmentFromSlot(tplayer, arms, 0)))) - } - } - //outside of the MAX condition above, we should seldom reach this point through conventional methods - tplayer.Armor = tplayer.MaxArmor - sendResponse(PlanetsideAttributeMessage(tplayer.GUID, 4, tplayer.Armor)) - avatarService ! AvatarServiceMessage(tplayer.Continent, AvatarAction.PlanetsideAttribute(tplayer.GUID, 4, tplayer.Armor)) - sendResponse(ItemTransactionResultMessage(msg.terminal_guid, TransactionType.Buy, true)) - } - else { - //load a complete new exo-suit and shuffle the inventory around - val originalSuit = tplayer.ExoSuit - //save inventory before it gets cleared (empty holsters) - val dropPred = DropPredicate(tplayer) - val (dropHolsters, beforeHolsters) = clearHolsters(tplayer.Holsters().iterator).partition(dropPred) - val (dropInventory, beforeInventory) = tplayer.Inventory.Clear().partition(dropPred) - //change suit (clear inventory and change holster sizes; note: holsters must be empty before this point) - tplayer.ExoSuit = exosuit - tplayer.Armor = tplayer.MaxArmor - //delete everything not dropped - (beforeHolsters ++ beforeInventory).foreach({ elem => - sendResponse(ObjectDeleteMessage(elem.obj.GUID, 0)) - }) - beforeHolsters.foreach({ elem => - avatarService ! AvatarServiceMessage(tplayer.Continent, AvatarAction.ObjectDelete(tplayer.GUID, elem.obj.GUID)) - }) - //report change - sendResponse(ArmorChangedMessage(tplayer.GUID, exosuit, subtype)) - avatarService ! AvatarServiceMessage(player.Continent, AvatarAction.ArmorChanged(tplayer.GUID, exosuit, subtype)) - sendResponse(PlanetsideAttributeMessage(tplayer.GUID, 4, tplayer.Armor)) - avatarService ! AvatarServiceMessage(player.Continent, AvatarAction.PlanetsideAttribute(tplayer.GUID, 4, tplayer.Armor)) - val finalInventory = if(exosuit == ExoSuitType.MAX) { - //MAX weapon to be placed in first pistol slot; slot to be drawn - taskResolver ! DelayedObjectHeld(tplayer, 0, List(PutEquipmentInSlot(tplayer, Tool(GlobalDefinitions.MAXArms(subtype, tplayer.Faction)), 0))) - //fill melee slot - fillEmptyHolsters(List(tplayer.Slot(4)).iterator, beforeHolsters) ++ beforeInventory - } - else { - //remove potential MAX weapon - val normalWeapons = if(originalSuit == ExoSuitType.MAX) { - val (maxWeapons, normalWeapons) = beforeHolsters.partition(elem => elem.obj.Size == EquipmentSize.Max) - maxWeapons.foreach(entry => { taskResolver ! GUIDTask.UnregisterEquipment(entry.obj)(continent.GUID) }) - normalWeapons - } - else { - tplayer.DrawnSlot = Player.HandsDownSlot - sendResponse(ObjectHeldMessage(tplayer.GUID, Player.HandsDownSlot, true)) - avatarService ! AvatarServiceMessage(tplayer.Continent, AvatarAction.ObjectHeld(tplayer.GUID, Player.HandsDownSlot)) - beforeHolsters - } - //fill holsters - val (afterHolsters, toInventory) = normalWeapons.partition(elem => elem.obj.Size == tplayer.Slot(elem.start).Size) - afterHolsters.foreach({elem => tplayer.Slot(elem.start).Equipment = elem.obj }) - fillEmptyHolsters(tplayer.Holsters().iterator, toInventory ++ beforeInventory) - } - //draw holsters - tplayer.VisibleSlots.foreach({index => - tplayer.Slot(index).Equipment match { - case Some(obj) => - val definition = obj.Definition - sendResponse( - ObjectCreateDetailedMessage( - definition.ObjectId, - obj.GUID, - ObjectCreateMessageParent(tplayer.GUID, index), - definition.Packet.DetailedConstructorData(obj).get - ) - ) - avatarService ! AvatarServiceMessage(player.Continent, AvatarAction.EquipmentInHand(player.GUID, 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( - 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( - 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 = Vector3(0,0, tplayer.Orientation.z) - ((dropHolsters ++ dropInventory).map(_.obj) ++ drop).foreach(obj => { - //TODO make a sound when dropping stuff - continent.Ground ! Zone.Ground.DropItem(obj, pos, orient) - }) - sendResponse(ItemTransactionResultMessage (msg.terminal_guid, TransactionType.Buy, true)) - } - - case Terminal.BuyEquipment(item) => ; - tplayer.Fit(item) match { - case Some(index) => - sendResponse(ItemTransactionResultMessage (msg.terminal_guid, TransactionType.Buy, true)) - taskResolver ! PutEquipmentInSlot(tplayer, item, index) - case None => - sendResponse(ItemTransactionResultMessage (msg.terminal_guid, TransactionType.Buy, false)) - } - - case Terminal.SellEquipment() => - tplayer.FreeHand.Equipment match { - case Some(item) => - if(item.GUID == msg.item_guid) { - sendResponse(ItemTransactionResultMessage (msg.terminal_guid, TransactionType.Sell, true)) - taskResolver ! RemoveEquipmentFromSlot(tplayer, item, Player.FreeHandSlot) - } - case None => - sendResponse(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(ItemTransactionResultMessage(msg.terminal_guid, TransactionType.Loadout, true)) - tplayer.History(HealFromExoSuitChange(PlayerSource(tplayer), exosuit)) - //ensure arm is down - tplayer.DrawnSlot = Player.HandsDownSlot - sendResponse(ObjectHeldMessage(tplayer.GUID, Player.HandsDownSlot, true)) - avatarService ! AvatarServiceMessage(tplayer.Continent, AvatarAction.ObjectHeld(tplayer.GUID, Player.HandsDownSlot)) - //load - val dropPred = DropPredicate(tplayer) - val (dropHolsters, beforeHolsters) = clearHolsters(tplayer.Holsters().iterator).partition(dropPred) - val (dropInventory, beforeInventory) = tplayer.Inventory.Clear().partition(dropPred) - val (_, afterHolsters) = holsters.partition(dropPred) //dropped items are lost - val (_, afterInventory) = inventory.partition(dropPred) //dropped items are lost - val beforeFreeHand = tplayer.FreeHand.Equipment - //change suit (clear inventory and change holster sizes; note: holsters must be empty before this point) - tplayer.ExoSuit = exosuit - tplayer.Armor = tplayer.MaxArmor - //delete everything (not dropped) - beforeHolsters.foreach({ elem => - avatarService ! AvatarServiceMessage(tplayer.Continent, AvatarAction.ObjectDelete(tplayer.GUID, elem.obj.GUID)) - }) - (beforeHolsters ++ beforeInventory).foreach({ elem => - sendResponse(ObjectDeleteMessage(elem.obj.GUID, 0)) - taskResolver ! GUIDTask.UnregisterEquipment(elem.obj)(continent.GUID) - }) - //report change - sendResponse(ArmorChangedMessage(tplayer.GUID, exosuit, subtype)) - avatarService ! AvatarServiceMessage(tplayer.Continent, AvatarAction.ArmorChanged(tplayer.GUID, exosuit, subtype)) - sendResponse(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 { - case Some(item) => - tplayer.FreeHand.Equipment = beforeFreeHand - val definition = item.Definition - sendResponse( - ObjectCreateDetailedMessage( - definition.ObjectId, - item.GUID, - ObjectCreateMessageParent(tplayer.GUID, Player.FreeHandSlot), - definition.Packet.DetailedConstructorData(item).get - ) - ) - case None => ; - } - //draw holsters - if(exosuit == ExoSuitType.MAX) { - tplayer.DrawnSlot = 0 - val (maxWeapons, otherWeapons) = afterHolsters.partition(entry => { entry.obj.Size == EquipmentSize.Max }) - taskResolver ! DelayedObjectHeld(tplayer, 0, List(PutEquipmentInSlot(tplayer, maxWeapons.head.obj, 0))) - otherWeapons - } - else { - afterHolsters - }.foreach(entry => { - taskResolver ! PutEquipmentInSlot(tplayer, entry.obj, entry.start) - }) - //put items into inventory - afterInventory.foreach(entry => { - taskResolver ! PutEquipmentInSlot(tplayer, entry.obj, entry.start) - }) - //drop stuff on ground - val pos = tplayer.Position - val orient = Vector3(0,0, tplayer.Orientation.z) - ((dropHolsters ++ dropInventory).map(_.obj)).foreach(obj => { - continent.Ground ! Zone.Ground.DropItem(obj, pos, orient) - }) - - case Terminal.VehicleLoadout(definition, weapons, inventory) => - log.info(s"$tplayer wants to change their vehicle equipment loadout to their option #${msg.unk1 + 1}") - FindLocalVehicle match { - case Some(vehicle) => - sendResponse(ItemTransactionResultMessage (msg.terminal_guid, TransactionType.Loadout, true)) - val (_, afterInventory) = inventory.partition( DropPredicate(tplayer) ) //dropped items are lost - //remove old inventory - val deleteEquipment : (Int,Equipment)=>Unit = DeleteEquipmentFromVehicle(vehicle) - vehicle.Inventory.Clear().foreach({ case InventoryItem(obj, index) => deleteEquipment(index, obj) }) - val stowEquipment : (Int,Equipment)=>TaskResolver.GiveTask = StowNewEquipmentInVehicle(vehicle) - (if(vehicle.Definition == definition) { - //vehicles are the same type; transfer over weapon ammo - //TODO ammo switching? no vehicle weapon does that currently but ... - //TODO want to completely swap weapons, but holster icon vanishes temporarily after swap - //TODO BFR arms must be swapped properly - val channel = s"${vehicle.Actor}" - weapons.foreach({ case InventoryItem(obj, index) => - val savedWeapon = obj.asInstanceOf[Tool] - val existingWeapon = vehicle.Weapons(index).Equipment.get.asInstanceOf[Tool] - (0 until existingWeapon.MaxAmmoSlot).foreach({ index => - val existingBox = existingWeapon.AmmoSlots(index).Box - existingBox.Capacity = savedWeapon.AmmoSlots(index).Box.Capacity - //use VehicleAction.InventoryState2; VehicleAction.InventoryState temporarily glitches ammo count in ui - vehicleService ! VehicleServiceMessage(channel, VehicleAction.InventoryState2(PlanetSideGUID(0), existingBox.GUID, existingWeapon.GUID, existingBox.Capacity)) - }) - }) - afterInventory - } - else { - //do not transfer over weapon ammo - if(vehicle.Definition.TrunkSize == definition.TrunkSize && vehicle.Definition.TrunkOffset == definition.TrunkOffset) { - afterInventory - } - else { - //accommodate as much of inventory as possible - val (stow, _) = GridInventory.recoverInventory(afterInventory, vehicle.Inventory) //dropped items can be forgotten - stow - } - }).foreach({ case InventoryItem(obj, index) => - taskResolver ! stowEquipment(index, obj) - }) - case None => - log.error(s"can not apply the loadout - can not find a vehicle") - sendResponse(ItemTransactionResultMessage (msg.terminal_guid, TransactionType.Loadout, false)) - } - - case Terminal.LearnCertification(cert) => - val name = tplayer.Name - if(!tplayer.Certifications.contains(cert)) { - val guid = tplayer.GUID - log.info(s"$name is learning the $cert certification for ${Certification.Cost.Of(cert)} points") - avatar.Certifications += cert - sendResponse(PlanetsideAttributeMessage(guid, 24, cert.id.toLong)) - - tplayer.Certifications.intersect(Certification.Dependencies.Like(cert)).foreach(entry => { - log.info(s"$cert replaces the learned certification $entry that cost ${Certification.Cost.Of(entry)} points") - avatar.Certifications -= entry - sendResponse(PlanetsideAttributeMessage(guid, 25, entry.id.toLong)) - }) - sendResponse(ItemTransactionResultMessage(msg.terminal_guid, TransactionType.Learn, true)) - } - else { - log.warn(s"$name already knows the $cert certification, so he can't learn it") - sendResponse(ItemTransactionResultMessage(msg.terminal_guid, TransactionType.Learn, false)) - } - - case Terminal.SellCertification(cert) => - val name = tplayer.Name - if(tplayer.Certifications.contains(cert)) { - val guid = tplayer.GUID - log.info(s"$name is forgetting the $cert certification for ${Certification.Cost.Of(cert)} points") - avatar.Certifications -= cert - sendResponse(PlanetsideAttributeMessage(guid, 25, cert.id.toLong)) - - tplayer.Certifications.intersect(Certification.Dependencies.FromAll(cert)).foreach(entry => { - log.info(s"$name is also forgetting the ${Certification.Cost.Of(entry)}-point $entry certification which depends on $cert") - avatar.Certifications -= entry - sendResponse(PlanetsideAttributeMessage(guid, 25, entry.id.toLong)) - }) - sendResponse(ItemTransactionResultMessage(msg.terminal_guid, TransactionType.Sell, true)) - } - else { - log.warn(s"$name doesn't know what a $cert certification is, so he can't forget it") - sendResponse(ItemTransactionResultMessage(msg.terminal_guid, TransactionType.Learn, false)) - } - - case Terminal.LearnImplant(implant) => - val terminal_guid = msg.terminal_guid - val implant_type = implant.Type - val message = s"Implants: $tplayer wants to learn $implant_type" - val (interface, slotNumber) = tplayer.VehicleSeated match { - case Some(mech_guid) => - ( - continent.Map.TerminalToInterface.get(mech_guid.guid), - if(!avatar.Implants.exists({slot => slot.Implant == implant_type})) { //no duplicates - avatar.InstallImplant(implant) - } - else { - None - } - ) - case _ => - (None, None) - } - - if(interface.contains(terminal_guid.guid) && slotNumber.isDefined) { - val slot = slotNumber.get - log.info(s"$message - put in slot $slot") - sendResponse(AvatarImplantMessage(tplayer.GUID, ImplantAction.Add, slot, implant_type.id)) - sendResponse(ItemTransactionResultMessage(terminal_guid, TransactionType.Learn, true)) - } - else { - if(interface.isEmpty) { - log.warn(s"$message - not interacting with a terminal") - } - else if(!interface.contains(terminal_guid.guid)) { - log.warn(s"$message - interacting with the wrong terminal, ${interface.get}") - } - else if(slotNumber.isEmpty) { - log.warn(s"$message - already knows that implant") - } - else { - log.warn(s"$message - forgot to sit at a terminal") - } - sendResponse(ItemTransactionResultMessage(terminal_guid, TransactionType.Learn, false)) - } - - case Terminal.SellImplant(implant) => - val terminal_guid = msg.terminal_guid - val implant_type = implant.Type - val (interface, slotNumber) = tplayer.VehicleSeated match { - case Some(mech_guid) => - ( - continent.Map.TerminalToInterface.get(mech_guid.guid), - avatar.UninstallImplant(implant_type) - ) - case None => - (None, None) - } - - if(interface.contains(terminal_guid.guid) && slotNumber.isDefined) { - val slot = slotNumber.get - log.info(s"$tplayer is selling $implant_type - take from slot $slot") - sendResponse(AvatarImplantMessage(tplayer.GUID, ImplantAction.Remove, slot, 0)) - sendResponse(ItemTransactionResultMessage(terminal_guid, TransactionType.Sell, true)) - } - else { - val message = s"$tplayer can not sell $implant_type" - if(interface.isEmpty) { - log.warn(s"$message - not interacting with a terminal") - } - else if(!interface.contains(terminal_guid.guid)) { - log.warn(s"$message - interacting with the wrong terminal, ${interface.get}") - } - else if(slotNumber.isEmpty) { - log.warn(s"$message - does not know that implant") - } - else { - log.warn(s"$message - forgot to sit at a terminal") - } - sendResponse(ItemTransactionResultMessage(terminal_guid, TransactionType.Sell, false)) - } - - case Terminal.BuyVehicle(vehicle, weapons, trunk) => - continent.Map.TerminalToSpawnPad.get(msg.terminal_guid.guid) match { - case Some(pad_guid) => - val pad = continent.GUID(pad_guid).get.asInstanceOf[VehicleSpawnPad] - vehicle.Faction = tplayer.Faction - vehicle.Position = pad.Position - vehicle.Orientation = pad.Orientation - //default loadout, weapons - log.info(s"default weapons: ${weapons.size}") - val vWeapons = vehicle.Weapons - weapons.foreach(entry => { - val index = entry.start - vWeapons.get(index) match { - case Some(slot) => - slot.Equipment = None - slot.Equipment = entry.obj - case None => - log.warn(s"applying default loadout to $vehicle, can not find a mounted weapon @ $index") - } - }) - //default loadout, trunk - log.info(s"default trunk: ${trunk.size}") - val vTrunk = vehicle.Trunk - vTrunk.Clear() - trunk.foreach(entry => { vTrunk += entry.start -> entry.obj }) - taskResolver ! RegisterNewVehicle(vehicle, pad) - sendResponse(ItemTransactionResultMessage(msg.terminal_guid, TransactionType.Buy, true)) - - case None => - log.error(s"$tplayer wanted to spawn a vehicle, but there was no spawn pad associated with terminal ${msg.terminal_guid} to accept it") - } - - case Terminal.StartProximityEffect(term) => - val player_guid = player.GUID - val term_guid = term.GUID - StartUsingProximityUnit(term) //redundant but cautious - sendResponse(ProximityTerminalUseMessage(player_guid, term_guid, true)) - localService ! LocalServiceMessage(continent.Id, LocalAction.ProximityTerminalEffect(player_guid, term_guid, true)) - - case Terminal.StopProximityEffect(term) => - val player_guid = player.GUID - val term_guid = term.GUID - StopUsingProximityUnit(term) //redundant but cautious - sendResponse(ProximityTerminalUseMessage(player_guid, term_guid, false)) - localService ! LocalServiceMessage(continent.Id, LocalAction.ProximityTerminalEffect(player_guid, term_guid, false)) - - case Terminal.NoDeal() => - val order : String = if(msg == null) { - s"order $msg" - } - else { - "missing order" - } - log.warn(s"${tplayer.Name} made a request but the terminal rejected the $order") - sendResponse(ItemTransactionResultMessage(msg.terminal_guid, msg.transaction_type, false)) - } - case VehicleSpawnPad.StartPlayerSeatedInVehicle(vehicle, pad) => val vehicle_guid = vehicle.GUID PlayerActionsToCancel() @@ -1411,23 +382,24 @@ class WorldSessionActor extends Actor with MDCContextAware { if(vehicle.Seats(0).isOccupied) { sendResponse(ObjectDetachMessage(pad.GUID, vehicle.GUID, pad.Position + Vector3(0, 0, 0.5f), pad.Orientation.z)) } - ServerVehicleOverride(vehicle, vdef.AutoPilotSpeed1, GlobalDefinitions.isFlightVehicle(vdef):Int) + ServerVehicleOverride(vehicle, vdef.AutoPilotSpeed1, GlobalDefinitions.isFlightVehicle(vdef) : Int) case VehicleSpawnControlGuided.GuidedControl(cmd, vehicle, data) => cmd match { case AutoDriveControls.State.Drive => - val speed : Int = data.getOrElse({ vehicle.Definition.AutoPilotSpeed1 }).asInstanceOf[Int] + val speed : Int = data.getOrElse({ + vehicle.Definition.AutoPilotSpeed1 + }).asInstanceOf[Int] ServerVehicleOverride(vehicle, speed) case AutoDriveControls.State.Climb => - ServerVehicleOverride(vehicle, controlled.getOrElse(0), GlobalDefinitions.isFlightVehicle(vehicle.Definition):Int) + ServerVehicleOverride(vehicle, controlled.getOrElse(0), GlobalDefinitions.isFlightVehicle(vehicle.Definition) : Int) case AutoDriveControls.State.Turn => //TODO how to turn hovering/flying vehicle? val direction = data.getOrElse(15).asInstanceOf[Int] sendResponse(VehicleStateMessage(vehicle.GUID, 0, vehicle.Position, vehicle.Orientation, vehicle.Velocity, None, 0, 0, direction, false, false)) - case AutoDriveControls.State.Stop => ServerVehicleOverride(vehicle, 0) @@ -1449,11 +421,16 @@ class WorldSessionActor extends Actor with MDCContextAware { }) sendResponse(ChatMsg(ChatMessageType.CMT_OPEN, true, "", msg, None)) + case CheckCargoDismount(vehicle_guid, cargo_vehicle_guid, cargo_mountpoint, iteration) => + HandleCheckCargoDismount(vehicle_guid, cargo_vehicle_guid, cargo_mountpoint, iteration) + + case CheckCargoMounting(vehicle_guid, cargo_vehicle_guid, cargo_mountpoint, iteration) => + HandleCheckCargoMounting(vehicle_guid, cargo_vehicle_guid, cargo_mountpoint, iteration) + case ListAccountCharacters => import net.psforever.objects.definition.converter.CharacterSelectConverter val gen : AtomicInteger = new AtomicInteger(1) val converter : CharacterSelectConverter = new CharacterSelectConverter - //load characters SetCharacterSelectScreenGUID(player, gen) val health = player.Health @@ -1463,31 +440,31 @@ class WorldSessionActor extends Actor with MDCContextAware { sendResponse( ObjectCreateDetailedMessage(ObjectClass.avatar, player.GUID, converter.DetailedConstructorData(player).get) ) - if(health > 0) { //player can not be dead; stay spawned as alive + if(health > 0) { + //player can not be dead; stay spawned as alive player.Health = health player.Stamina = stamina player.Armor = armor } - sendResponse(CharacterInfoMessage(15,PlanetSideZoneID(10000), 41605313, player.GUID, false, 6404428)) + sendResponse(CharacterInfoMessage(15, PlanetSideZoneID(10000), 41605313, player.GUID, false, 6404428)) RemoveCharacterSelectScreenGUID(player) - sendResponse(CharacterInfoMessage(0, PlanetSideZoneID(1), 0, PlanetSideGUID(0), true, 0)) sendResponse(CharacterInfoMessage(0, PlanetSideZoneID(1), 0, PlanetSideGUID(0), true, 0)) - case VehicleLoaded(_/*vehicle*/) => ; - //currently being handled by VehicleSpawnPad.LoadVehicle during testing phase + case VehicleLoaded(_ /*vehicle*/) => ; + //currently being handled by VehicleSpawnPad.LoadVehicle during testing phase case Zone.ClientInitialization(zone) => val continentNumber = zone.Number val poplist = zone.Players - val popBO = 0 //TODO black ops test (partition) + val popBO = 0 + //TODO black ops test (partition) val popTR = poplist.count(_.faction == PlanetSideEmpire.TR) val popNC = poplist.count(_.faction == PlanetSideEmpire.NC) val popVS = poplist.count(_.faction == PlanetSideEmpire.VS) - // StopBundlingPackets() is called on ClientInitializationComplete StartBundlingPackets() - zone.Buildings.foreach({ case(id, building) => initBuilding(continentNumber, id, building) }) + zone.Buildings.foreach({ case (id, building) => initBuilding(continentNumber, id, building) }) sendResponse(ZonePopulationUpdateMessage(continentNumber, 414, 138, popTR, 138, popNC, 138, popVS, 138, popBO)) sendResponse(ContinentalLockUpdateMessage(continentNumber, PlanetSideEmpire.NEUTRAL)) //CaptureFlagUpdateMessage() @@ -1521,11 +498,14 @@ class WorldSessionActor extends Actor with MDCContextAware { case vehicle : Vehicle => //TODO replace this bad math with good math or no math //position the player alongside either of the AMS's terminals, facing away from it - val side = if(System.currentTimeMillis() % 2 == 0) 1 else -1 //right | left + val side = if(System.currentTimeMillis() % 2 == 0) 1 + else -1 + //right | left val z = spawn_tube.Orientation.z val zrot = (z + 90) % 360 val x = spawn_tube.Orientation.x - val xsin = 3 * side * math.abs(math.sin(math.toRadians(x))).toFloat + 0.5f //sin because 0-degrees is up + val xsin = 3 * side * math.abs(math.sin(math.toRadians(x))).toFloat + 0.5f + //sin because 0-degrees is up val zrad = math.toRadians(zrot) pos = pos + (Vector3(math.sin(zrad).toFloat, math.cos(zrad).toFloat, 0) * (3 * side)) //x=sin, y=cos because compass-0 is East, not North ori = if(side == 1) { @@ -1534,7 +514,8 @@ class WorldSessionActor extends Actor with MDCContextAware { else { Vector3(0, 0, (z - 90) % 360) } - pos = if(x >= 330) { //leaning to the left + pos = if(x >= 330) { + //leaning to the left pos + Vector3(0, 0, xsin) } else { @@ -1544,54 +525,8 @@ class WorldSessionActor extends Actor with MDCContextAware { case owner => log.warn(s"Zone.Lattice.SpawnPoint: spawn point on $zone_id at ${spawn_tube.Position} has unexpected owner $owner") } - respawnTimer.cancel - reviveTimer.cancel - ClearCurrentAmsSpawnPoint() - val sameZone = zone_id == continent.Id - val backpack = player.isBackpack - val respawnTime : Long = if(sameZone) { 10 } else { 0 } //s - val respawnTimeMillis = respawnTime * 1000 //ms - deadState = DeadState.RespawnTime - sendResponse(AvatarDeadStateMessage(DeadState.RespawnTime, respawnTimeMillis, respawnTimeMillis, Vector3.Zero, player.Faction, true)) - val tplayer = if(backpack) { - RespawnClone(player) //new player - } - else { - val player_guid = player.GUID - sendResponse(ObjectDeleteMessage(player_guid, 4)) - avatarService ! AvatarServiceMessage(continent.Id, AvatarAction.ObjectDelete(player_guid, player_guid, 4)) - player //player is deconstructing self - } - - tplayer.Position = pos - tplayer.Orientation = ori - val (target, msg) : (ActorRef, Any) = if(sameZone) { - if(backpack) { - //respawning from unregistered player - (taskResolver, RegisterAvatar(tplayer)) - } - else { - //move existing player - (self, PlayerLoaded(tplayer)) - } - } - else { - DisownVehicle() - continent.Population ! Zone.Population.Leave(avatar) - val original = player - //TODO check player orientation upon spawn not polluted - if(backpack) { - //unregister avatar locker + GiveWorld - player = tplayer - (taskResolver, TaskBeforeZoneChange(GUIDTask.UnregisterLocker(original.Locker)(continent.GUID), zone_id)) - } - else { - //unregister avatar whole + GiveWorld - (taskResolver, TaskBeforeZoneChange(GUIDTask.UnregisterAvatar(original)(continent.GUID), zone_id)) - } - } - import scala.concurrent.ExecutionContext.Implicits.global - respawnTimer = context.system.scheduler.scheduleOnce(respawnTime seconds, target, msg) + LoadZonePhysicalSpawnPoint(zone_id, pos, ori, if(zone_id == continent.Id) 10 + else 0) case Zone.Lattice.NoValidSpawnPoint(zone_number, None) => log.warn(s"Zone.Lattice.SpawnPoint: zone $zone_number could not be accessed as requested") @@ -1609,43 +544,72 @@ class WorldSessionActor extends Actor with MDCContextAware { RequestSanctuaryZoneSpawn(player, zone_number) } - case Zone.Ground.ItemOnGround(item, pos, orient) => - item.Position = pos - item.Orientation = Vector3(0,0, orient.z) //dropped items rotate towards the user's standing direction - val exclusionId = player.Find(item) match { - case Some(slotNum) => - player.Slot(slotNum).Equipment = None - sendResponse(ObjectDetachMessage(player.GUID, item.GUID, pos, orient.z)) - sendResponse(ActionResultMessage.Pass) - player.GUID //we're dropping it; don't need to see it dropped again - case None => - PlanetSideGUID(0) //object is being introduced into the world upon drop + case Zone.Ground.ItemOnGround(item : BoomerTrigger, pos, orient) => + //dropped the trigger, no longer own the boomer; make certain whole faction is aware of that + val playerGUID = player.GUID + continent.GUID(item.Companion.getOrElse(PlanetSideGUID(0))) match { + case Some(obj : BoomerDeployable) => + val guid = obj.GUID + val factionOnContinentChannel = s"${continent.Id}/${player.Faction}" + obj.Owner = None + obj.OwnerName = None + obj.Faction = PlanetSideEmpire.NEUTRAL + avatar.Deployables.Remove(obj) + UpdateDeployableUIElements(avatar.Deployables.UpdateUIElement(obj.Definition.Item)) + localService ! LocalServiceMessage.Deployables(RemoverActor.AddTask(obj, continent)) + sendResponse(SetEmpireMessage(guid, PlanetSideEmpire.NEUTRAL)) + avatarService ! AvatarServiceMessage(factionOnContinentChannel, AvatarAction.SetEmpire(playerGUID, guid, PlanetSideEmpire.NEUTRAL)) + val info = DeployableInfo(guid, DeployableIcon.Boomer, obj.Position, PlanetSideGUID(0)) + sendResponse(DeployableObjectsInfoMessage(DeploymentAction.Dismiss, info)) + localService ! LocalServiceMessage(factionOnContinentChannel, LocalAction.DeployableMapIcon(playerGUID, DeploymentAction.Dismiss, info)) + PutItemOnGround(item, pos, orient) + case Some(_) | None => + //pointless trigger + val guid = item.GUID + continent.Ground ! Zone.Ground.RemoveItem(guid) //undo; no callback + avatarService ! AvatarServiceMessage(continent.Id, AvatarAction.ObjectDelete(PlanetSideGUID(0), guid)) + taskResolver ! GUIDTask.UnregisterObjectTask(item)(continent.GUID) } - avatarService ! AvatarServiceMessage(continent.Id, AvatarAction.DropItem(exclusionId, item, continent)) + + case Zone.Ground.ItemOnGround(item : ConstructionItem, pos, orient) => + //defensively, reset CItem configuration + item.FireModeIndex = 0 + item.AmmoTypeIndex = 0 + PutItemOnGround(item, pos, orient) + + case Zone.Ground.ItemOnGround(item : PlanetSideGameObject, pos, orient) => + PutItemOnGround(item, pos, orient) case Zone.Ground.CanNotDropItem(zone, item, reason) => log.warn(s"DropItem: $player tried to drop a $item on the ground, but $reason") - case Zone.Ground.ItemInHand(item) => - player.Fit(item) match { - case Some(slotNum) => - val item_guid = item.GUID - val player_guid = player.GUID - player.Slot(slotNum).Equipment = item - val definition = item.Definition - sendResponse( - ObjectCreateDetailedMessage( - definition.ObjectId, - item_guid, - ObjectCreateMessageParent(player_guid, slotNum), - definition.Packet.DetailedConstructorData(item).get - ) - ) - avatarService ! AvatarServiceMessage(continent.Id, AvatarAction.PickupItem(player_guid, continent, player, slotNum, item)) - case None => - continent.Ground ! Zone.Ground.DropItem(item, item.Position, item.Orientation) //restore previous state + case Zone.Ground.ItemInHand(item : BoomerTrigger) => + if(PutItemInHand(item)) { + //pick up the trigger, own the boomer; make certain whole faction is aware of that + continent.GUID(item.Companion.getOrElse(PlanetSideGUID(0))) match { + case Some(obj : BoomerDeployable) => + val guid = obj.GUID + val playerGUID = player.GUID + val faction = player.Faction + val factionOnContinentChannel = s"${continent.Id}/${faction}" + obj.Owner = playerGUID + obj.OwnerName = player.Name + obj.Faction = faction + avatar.Deployables.Add(obj) + UpdateDeployableUIElements(avatar.Deployables.UpdateUIElement(obj.Definition.Item)) + localService ! LocalServiceMessage.Deployables(RemoverActor.ClearSpecific(List(obj), continent)) + sendResponse(SetEmpireMessage(guid, faction)) + avatarService ! AvatarServiceMessage(factionOnContinentChannel, AvatarAction.SetEmpire(playerGUID, guid, faction)) + val info = DeployableInfo(obj.GUID, DeployableIcon.Boomer, obj.Position, obj.Owner.get) + sendResponse(DeployableObjectsInfoMessage(DeploymentAction.Build, info)) + localService ! LocalServiceMessage(factionOnContinentChannel, LocalAction.DeployableMapIcon(playerGUID, DeploymentAction.Build, info)) + case Some(_) | None => ; //pointless trigger; see Zone.Ground.ItemOnGround(BoomerTrigger, ...) + } } + case Zone.Ground.ItemInHand(item : Equipment) => + PutItemInHand(item) + case Zone.Ground.CanNotPickupItem(zone, item_guid, _) => zone.GUID(item_guid) match { case Some(item) => @@ -1654,6 +618,123 @@ class WorldSessionActor extends Actor with MDCContextAware { log.warn(s"DropItem: finding an item ($item_guid) on the ground was suggested, but $player can not see it") } + case Zone.Deployable.DeployableIsBuilt(obj, tool) => + val index = player.Find(tool) match { + case Some(x) => + x + case None => + player.LastDrawnSlot + } + if(avatar.Deployables.Accept(obj) || (avatar.Deployables.Valid(obj) && !avatar.Deployables.Contains(obj))) { + tool.Definition match { + case GlobalDefinitions.ace => + localService ! LocalServiceMessage(continent.Id, LocalAction.TriggerEffectLocation(player.GUID, "spawn_object_effect", obj.Position, obj.Orientation)) + case GlobalDefinitions.advanced_ace => + sendResponse(GenericObjectActionMessage(player.GUID, 212)) //put fdu down; it will be removed from the client's holster + avatarService ! AvatarServiceMessage(continent.Id, AvatarAction.PutDownFDU(player.GUID)) + case _ => + log.warn(s"Zone.Deployable.DeployableIsBuilt: not sure what kind of construction item to animate - ${tool.Definition}") + } + import scala.concurrent.ExecutionContext.Implicits.global + context.system.scheduler.scheduleOnce( + obj.Definition.DeployTime milliseconds, + self, + WorldSessionActor.FinalizeDeployable(obj, tool, index) + ) + } + else { + TryDropConstructionTool(tool, index, obj.Position) + sendResponse(ObjectDeployedMessage.Failure(obj.Definition.Name)) + obj.Position = Vector3.Zero + obj.Owner = None + obj.OwnerName = None + continent.Deployables ! Zone.Deployable.Dismiss(obj) + } + + case WorldSessionActor.FinalizeDeployable(obj : TurretDeployable, tool, index) => + //spitfires and deployable field turrets + StartBundlingPackets() + DeployableBuildActivity(obj) + CommonDestroyConstructionItem(tool, index) + FindReplacementConstructionItem(tool, index) + StopBundlingPackets() + + case WorldSessionActor.FinalizeDeployable(obj : ComplexDeployable, tool, index) => + //deployable_shield_generator + StartBundlingPackets() + DeployableBuildActivity(obj) + CommonDestroyConstructionItem(tool, index) + FindReplacementConstructionItem(tool, index) + StopBundlingPackets() + + case WorldSessionActor.FinalizeDeployable(obj : BoomerDeployable, tool, index) => + //boomers + StartBundlingPackets() + DeployableBuildActivity(obj) + //TODO sufficiently delete the tool + sendResponse(ObjectDeleteMessage(tool.GUID, 0)) + avatarService ! AvatarServiceMessage(continent.Id, AvatarAction.ObjectDelete(player.GUID, tool.GUID)) + taskResolver ! GUIDTask.UnregisterEquipment(tool)(continent.GUID) + val trigger = new BoomerTrigger + trigger.Companion = obj.GUID + obj.Trigger = trigger + val holster = player.Slot(index) + if(holster.Equipment.contains(tool)) { + holster.Equipment = None + taskResolver ! DelayedObjectHeld(player, index, List(PutEquipmentInSlot(player, trigger, index))) + } + else { + //don't know where boomer trigger should go; drop it on the ground + taskResolver ! NewItemDrop(player, continent, avatarService)(trigger) + } + StopBundlingPackets() + + case WorldSessionActor.FinalizeDeployable(obj : ExplosiveDeployable, tool, index) => + //mines + StartBundlingPackets() + DeployableBuildActivity(obj) + CommonDestroyConstructionItem(tool, index) + FindReplacementConstructionItem(tool, index) + StopBundlingPackets() + + case WorldSessionActor.FinalizeDeployable(obj : SensorDeployable, tool, index) => + //motion alarm sensor and sensor disruptor + StartBundlingPackets() + DeployableBuildActivity(obj) + localService ! LocalServiceMessage(continent.Id, LocalAction.TriggerEffectInfo(player.GUID, "on", obj.GUID, true, 1000)) + CommonDestroyConstructionItem(tool, index) + FindReplacementConstructionItem(tool, index) + StopBundlingPackets() + + case WorldSessionActor.FinalizeDeployable(obj : SimpleDeployable, tool, index) => + //tank_trap + StartBundlingPackets() + DeployableBuildActivity(obj) + CommonDestroyConstructionItem(tool, index) + FindReplacementConstructionItem(tool, index) + StopBundlingPackets() + + case WorldSessionActor.FinalizeDeployable(obj : PlanetSideGameObject with Deployable, tool, index) => + val guid = obj.GUID + val definition = obj.Definition + StartBundlingPackets() + sendResponse(GenericObjectActionMessage(guid, 84)) //reset build cooldown + sendResponse(ObjectDeployedMessage.Failure(definition.Name)) + log.warn(s"FinalizeDeployable: deployable ${definition.asInstanceOf[DeployableDefinition].Item}@$guid not handled by specific case") + log.warn(s"FinalizeDeployable: deployable will be cleaned up, but may not get unregistered properly") + TryDropConstructionTool(tool, index, obj.Position) + obj.Position = Vector3.Zero + continent.Deployables ! Zone.Deployable.Dismiss(obj) + StopBundlingPackets() + + //!!only dispatch Zone.Deployable.Dismiss from WSA as cleanup if the target deployable was never fully introduced + case Zone.Deployable.DeployableIsDismissed(obj : TurretDeployable) => + taskResolver ! GUIDTask.UnregisterDeployableTurret(obj)(continent.GUID) + + //!!only dispatch Zone.Deployable.Dismiss from WSA as cleanup if the target deployable was never fully introduced + case Zone.Deployable.DeployableIsDismissed(obj) => + taskResolver ! GUIDTask.UnregisterObjectTask(obj)(continent.GUID) + case InterstellarCluster.ClientInitializationComplete() => StopBundlingPackets() LivePlayerList.Add(sessionId, avatar) @@ -1663,7 +744,8 @@ class WorldSessionActor extends Actor with MDCContextAware { sendResponse(ReplicationStreamMessage(5, Some(6), Vector(SquadListing()))) //clear squad list sendResponse(FriendsResponse(FriendAction.InitializeFriendList, 0, true, true, Nil)) sendResponse(FriendsResponse(FriendAction.InitializeIgnoreList, 0, true, true, Nil)) - avatarService ! Service.Join(avatar.name) //will be same as player.Name + avatarService ! Service.Join(avatar.name) //channel will be player.Name + localService ! Service.Join(avatar.name) //channel will be player.Name cluster ! InterstellarCluster.GetWorld("z6") case InterstellarCluster.GiveWorld(zoneId, zone) => @@ -1680,7 +762,7 @@ class WorldSessionActor extends Actor with MDCContextAware { log.info(s"Player ${tplayer.Name} has been loaded") player = tplayer //LoadMapMessage will cause the client to send back a BeginZoningMessage packet (see below) - sendResponse(LoadMapMessage(continent.Map.Name, continent.Id, 40100,25,true,3770441820L)) + sendResponse(LoadMapMessage(continent.Map.Name, continent.Id, 40100, 25, true, 3770441820L)) AvatarCreate() //important! the LoadMapMessage must be processed by the client before the avatar is created case PlayerLoaded(tplayer) => @@ -1707,199 +789,61 @@ class WorldSessionActor extends Actor with MDCContextAware { } case SetCurrentAvatar(tplayer) => - player = tplayer - val guid = tplayer.GUID - StartBundlingPackets() - sendResponse(SetCurrentAvatarMessage(guid, 0, 0)) - sendResponse(ChatMsg(ChatMessageType.CMT_EXPANSIONS, true, "", "1 on", None)) //CC on //TODO once per respawn? - sendResponse(PlayerStateShiftMessage(ShiftState(1, tplayer.Position, tplayer.Orientation.z))) - //transfer vehicle ownership - player.VehicleOwned match { - case Some(vehicle_guid) => - continent.GUID(vehicle_guid) match { - case Some(vehicle : Vehicle) => - vehicle.Owner = player - vehicleService ! VehicleServiceMessage(continent.Id, VehicleAction.Ownership(guid, vehicle_guid)) - case _ => - player.VehicleOwned = None - } - case None => ; - } - if(spectator) { - sendResponse(ChatMsg(ChatMessageType.CMT_TOGGLESPECTATORMODE, false, "", "on", None)) - } - (0 until DetailedCharacterData.numberOfImplantSlots(tplayer.BEP)).foreach(slot => { - sendResponse(AvatarImplantMessage(guid, ImplantAction.Initialization, slot, 1)) //init implant slot - sendResponse(AvatarImplantMessage(guid, ImplantAction.Activation, slot, 0)) //deactivate implant - //TODO if this implant is Installed but does not have shortcut, add to a free slot or write over slot 61/62/63 - }) - sendResponse(PlanetsideAttributeMessage(PlanetSideGUID(0), 82, 0)) - //TODO if Medkit does not have shortcut, add to a free slot or write over slot 64 - sendResponse(CreateShortcutMessage(guid, 1, 0, true, Shortcut.MEDKIT)) - sendResponse(ChangeShortcutBankMessage(guid, 0)) - //FavoritesMessage - sendResponse(SetChatFilterMessage(ChatChannel.Local, false, ChatChannel.values.toList)) //TODO will not always be "on" like this - deadState = DeadState.Alive - sendResponse(AvatarDeadStateMessage(DeadState.Alive, 0, 0, tplayer.Position, player.Faction, true)) - sendResponse(PlanetsideAttributeMessage(guid, 53, 1)) - sendResponse(AvatarSearchCriteriaMessage(guid, List(0, 0, 0, 0, 0, 0))) - (1 to 73).foreach(i => { - sendResponse(PlanetsideAttributeMessage(PlanetSideGUID(i), 67, 0)) - }) - (0 to 30).foreach(i => { - //TODO 30 for a new character only? - sendResponse(AvatarStatisticsMessage(2, Statistics(0L))) - }) - //AvatarAwardMessage - //DisplayAwardMessage - //SquadDefinitionActionMessage and SquadDetailDefinitionUpdateMessage - //MapObjectStateBlockMessage and ObjectCreateMessage? - //TacticsMessage? - StopBundlingPackets() + HandleSetCurrentAvatar(tplayer) case NtuCharging(tplayer, vehicle) => - log.trace(s"NtuCharging: Vehicle ${vehicle.GUID} is charging NTU capacitor.") - - if(vehicle.Capacitor < vehicle.Definition.MaximumCapacitor) { - // Charging - vehicle.Capacitor += 100 - - sendResponse(PlanetsideAttributeMessage(vehicle.GUID, 45, scala.math.round((vehicle.Capacitor.toFloat / vehicle.Definition.MaximumCapacitor.toFloat) * 10) )) // set ntu on vehicle UI - avatarService ! AvatarServiceMessage(continent.Id, AvatarAction.PlanetsideAttribute(vehicle.GUID, 52, 1L)) // panel glow on - avatarService ! AvatarServiceMessage(continent.Id, AvatarAction.PlanetsideAttribute(vehicle.GUID, 49, 1L)) // orb particle effect on - - antChargingTick = context.system.scheduler.scheduleOnce(1000 milliseconds, self, NtuCharging(player, vehicle)) // Repeat until fully charged - } else { - // Fully charged - sendResponse(PlanetsideAttributeMessage(vehicle.GUID, 45, scala.math.round((vehicle.Capacitor.toFloat / vehicle.Definition.MaximumCapacitor.toFloat) * 10).toInt)) // set ntu on vehicle UI - - // Turning off glow/orb effects on ANT doesn't seem to work when deployed. Try to undeploy ANT from server side - context.system.scheduler.scheduleOnce(vehicle.UndeployTime milliseconds, vehicle.Actor, Deployment.TryUndeploy(DriveState.Undeploying)) - } + HandleNtuCharging(tplayer, vehicle) case NtuDischarging(tplayer, vehicle, silo_guid) => - log.trace(s"NtuDischarging: Vehicle ${vehicle.GUID} is discharging NTU into silo $silo_guid") - avatarService ! AvatarServiceMessage(continent.Id, AvatarAction.PlanetsideAttribute(vehicle.GUID, 49, 0L)) // orb particle effect off - avatarService ! AvatarServiceMessage(continent.Id, AvatarAction.PlanetsideAttribute(vehicle.GUID, 52, 1L)) // panel glow on - - var silo = continent.GUID(silo_guid).get.asInstanceOf[ResourceSilo] - - // Check vehicle is still deployed before continuing. User can undeploy manually or vehicle may not longer be present. - if(vehicle.DeploymentState == DriveState.Deployed) { - if(vehicle.Capacitor > 0 && silo.ChargeLevel < silo.MaximumCharge) { - - // Make sure we don't exceed the silo maximum charge or remove much NTU from ANT if maximum is reached, or try to make ANT go below 0 NTU - var chargeToDeposit = Math.min(Math.min(vehicle.Capacitor, 100), (silo.MaximumCharge - silo.ChargeLevel)) - vehicle.Capacitor -= chargeToDeposit - silo.Actor ! ResourceSilo.UpdateChargeLevel(chargeToDeposit) - - avatarService ! AvatarServiceMessage(continent.Id, AvatarAction.PlanetsideAttribute(silo_guid, 49, 1L)) // panel glow on & orb particles on - sendResponse(PlanetsideAttributeMessage(vehicle.GUID, 45, scala.math.round((vehicle.Capacitor.toFloat / vehicle.Definition.MaximumCapacitor.toFloat) * 10))) // set ntu on vehicle UI - - //todo: grant BEP to user - //todo: grant BEP to squad in range - //todo: notify map service to update ntu % on map for all users - - //todo: handle silo orb / panel glow properly if more than one person is refilling silo and one player stops. effects should stay on until all players stop - - if(vehicle.Capacitor > 0 && silo.ChargeLevel < silo.MaximumCharge) { - log.trace(s"NtuDischarging: ANT not empty and Silo not full. Scheduling another discharge") - // Silo still not full and ant still has charge left - keep rescheduling ticks - antDischargingTick = context.system.scheduler.scheduleOnce(1000 milliseconds, self, NtuDischarging(player, vehicle, silo_guid)) - } else { - log.trace(s"NtuDischarging: ANT NTU empty or Silo NTU full.") - - // Turning off glow/orb effects on ANT doesn't seem to work when deployed. Try to undeploy ANT from server side - context.system.scheduler.scheduleOnce(vehicle.UndeployTime milliseconds, vehicle.Actor, Deployment.TryUndeploy(DriveState.Undeploying)) - - avatarService ! AvatarServiceMessage(continent.Id, AvatarAction.PlanetsideAttribute(silo_guid, 49, 0L)) // panel glow off & orb particles off - antDischargingTick.cancel() - } - } else { - // This shouldn't normally be run, only if the client thinks the ANT has capacitor charge when it doesn't, or thinks the silo isn't full when it is. - log.warn(s"NtuDischarging: Invalid discharge state. ANT Capacitor: ${vehicle.Capacitor} Silo Capacitor: ${silo.ChargeLevel}") - - // Turning off glow/orb effects on ANT doesn't seem to work when deployed. Try to undeploy ANT from server side - context.system.scheduler.scheduleOnce(vehicle.UndeployTime milliseconds, vehicle.Actor, Deployment.TryUndeploy(DriveState.Undeploying)) - - avatarService ! AvatarServiceMessage(continent.Id, AvatarAction.PlanetsideAttribute(silo_guid, 49, 0L)) // panel glow off & orb particles off - antDischargingTick.cancel() - } - } else { - log.trace(s"NtuDischarging: Vehicle is no longer deployed. Removing effects") - // Vehicle has changed from deployed and this should be the last timer tick sent - avatarService ! AvatarServiceMessage(continent.Id, AvatarAction.PlanetsideAttribute(vehicle.GUID, 52, 0L)) // panel glow off - avatarService ! AvatarServiceMessage(continent.Id, AvatarAction.PlanetsideAttribute(silo_guid, 49, 0L)) // panel glow off & orb particles off - antDischargingTick.cancel() - } + HandleNtuDischarging(tplayer, vehicle, silo_guid) case HackingProgress(progressType, tplayer, target, tool_guid, delta, completeAction, tickAction) => - progressBarUpdate.cancel - if(progressBarValue.isDefined) { - val progressBarVal : Float = progressBarValue.get + delta - val vis = if(progressBarVal == 0L) { //hack state for progress bar visibility - HackState.Start - } - else if(progressBarVal > 100L) { - HackState.Finished - } - else { - HackState.Ongoing - } - sendResponse(HackMessage(progressType, target.GUID, player.GUID, progressBarVal.toInt, 0L, vis, 8L)) - if(progressBarVal > 100) { //done - progressBarValue = None -// sendResponse(HackMessage(0, target.GUID, player.GUID, 100, 1114636288L, HackState.Hacked, 8L)) - completeAction() - } - else { //continue next tick - tickAction.getOrElse(() => Unit)() - progressBarValue = Some(progressBarVal) - import scala.concurrent.ExecutionContext.Implicits.global - progressBarUpdate = context.system.scheduler.scheduleOnce(250 milliseconds, self, HackingProgress(progressType, tplayer, target, tool_guid, delta, completeAction)) - } - } + HandleHackingProgress(progressType, tplayer, target, tool_guid, delta, completeAction, tickAction) case DelayedProximityUnitStop(terminal) => StopUsingProximityUnit(terminal) case Vitality.DamageResolution(target : Vehicle) => - val targetGUID = target.GUID - val playerGUID = player.GUID - val continentId = continent.Id - val players = target.Seats.values.filter(seat => { seat.isOccupied && seat.Occupant.get.isAlive }) - if(target.Health > 0) { - //alert occupants to damage source - players.foreach(seat => { - val tplayer = seat.Occupant.get - avatarService ! AvatarServiceMessage(tplayer.Name, AvatarAction.HitHint(playerGUID, tplayer.GUID)) - }) + HandleVehicleDamageResolution(target) + + case Vitality.DamageResolution(target : TrapDeployable) => + //tank_traps + val guid = target.GUID + val health = target.Health + avatarService ! AvatarServiceMessage(continent.Id, AvatarAction.PlanetsideAttribute(guid, 0, health)) + if(health <= 0) { + AnnounceDestroyDeployable(target, None) } - else { - //alert to vehicle death (hence, occupants' deaths) - players.foreach(seat => { - val tplayer = seat.Occupant.get - val tplayerGUID = tplayer.GUID - avatarService ! AvatarServiceMessage(tplayer.Name, AvatarAction.KilledWhileInVehicle(tplayerGUID)) - avatarService ! AvatarServiceMessage(continentId, AvatarAction.ObjectDelete(tplayerGUID, tplayerGUID)) //dead player still sees self - }) - //vehicle wreckage has no weapons - target.Weapons.values - .filter { _.Equipment.nonEmpty } - .foreach(slot => { - val wep = slot.Equipment.get - avatarService ! AvatarServiceMessage(continentId, AvatarAction.ObjectDelete(Service.defaultPlayerGUID, wep.GUID)) - }) - if(target.Definition == GlobalDefinitions.ams) { - target.Actor ! Deployment.TryDeploymentChange(DriveState.Undeploying) - ClearCurrentAmsSpawnPoint() - } - avatarService ! AvatarServiceMessage(continentId, AvatarAction.Destroy(targetGUID, playerGUID, playerGUID, target.Position)) - vehicleService ! VehicleServiceMessage.Decon(RemoverActor.ClearSpecific(List(target), continent)) - vehicleService ! VehicleServiceMessage.Decon(RemoverActor.AddTask(target, continent, Some(1 minute))) + + case Vitality.DamageResolution(target : SensorDeployable) => + //sensors + val guid = target.GUID + val health = target.Health + avatarService ! AvatarServiceMessage(continent.Id, AvatarAction.PlanetsideAttribute(guid, 0, health)) + if(health <= 0) { + AnnounceDestroyDeployable(target, Some(0 seconds)) + } + + case Vitality.DamageResolution(target : SimpleDeployable) => + //boomers, mines + if(target.Health <= 0) { + //update if destroyed + val guid = target.GUID + avatarService ! AvatarServiceMessage(continent.Id, AvatarAction.ObjectDelete(player.GUID, guid)) + AnnounceDestroyDeployable(target, Some(0 seconds)) + } + + case Vitality.DamageResolution(target : TurretDeployable) => + HandleTurretDeployableDamageResolution(target) + + case Vitality.DamageResolution(target : ComplexDeployable) => + //shield_generators + val health = target.Health + val guid = target.GUID + avatarService ! AvatarServiceMessage(continent.Id, AvatarAction.PlanetsideAttribute(guid, 0, health)) + if(health <= 0) { + AnnounceDestroyDeployable(target, None) } - vehicleService ! VehicleServiceMessage(continentId, VehicleAction.PlanetsideAttribute(Service.defaultPlayerGUID, targetGUID, 0, target.Health)) - vehicleService ! VehicleServiceMessage(s"${target.Actor}", VehicleAction.PlanetsideAttribute(Service.defaultPlayerGUID, targetGUID, 68, target.Shields)) case Vitality.DamageResolution(target : PlanetSideGameObject) => log.warn(s"Vital target ${target.Definition.Name} damage resolution not supported using this method") @@ -1912,7 +856,1467 @@ class WorldSessionActor extends Actor with MDCContextAware { sendResponse(pkt) case default => - log.warn(s"Invalid packet class received: $default") + log.warn(s"Invalid packet class received: $default from $sender") + } + + /** + * na + * @param toChannel na + * @param guid na + * @param reply na + */ + def HandleAvatarServiceResponse(toChannel : String, guid : PlanetSideGUID, reply : AvatarResponse.Response) : Unit = { + val tplayer_guid = if(player.HasGUID) player.GUID + else PlanetSideGUID(0) + reply match { + case AvatarResponse.SendResponse(msg) => + sendResponse(msg) + + case AvatarResponse.ArmorChanged(suit, subtype) => + if(tplayer_guid != guid) { + sendResponse(ArmorChangedMessage(guid, suit, subtype)) + } + + case AvatarResponse.ChangeAmmo(weapon_guid, weapon_slot, previous_guid, ammo_id, ammo_guid, ammo_data) => + if(tplayer_guid != guid) { + sendResponse(ObjectDetachMessage(weapon_guid, previous_guid, Vector3.Zero, 0)) + sendResponse( + ObjectCreateMessage( + ammo_id, + ammo_guid, + ObjectCreateMessageParent(weapon_guid, weapon_slot), + ammo_data + ) + ) + sendResponse(ChangeAmmoMessage(weapon_guid, 1)) + } + + case AvatarResponse.ChangeFireMode(item_guid, mode) => + if(tplayer_guid != guid) { + sendResponse(ChangeFireModeMessage(item_guid, mode)) + } + + case AvatarResponse.ChangeFireState_Start(weapon_guid) => + if(tplayer_guid != guid) { + sendResponse(ChangeFireStateMessage_Start(weapon_guid)) + } + + case AvatarResponse.ChangeFireState_Stop(weapon_guid) => + if(tplayer_guid != guid) { + sendResponse(ChangeFireStateMessage_Stop(weapon_guid)) + } + + case AvatarResponse.ConcealPlayer() => + if(tplayer_guid != guid) { + sendResponse(GenericObjectActionMessage(guid, 36)) + } + + case AvatarResponse.DamageResolution(target, resolution_function) => + if(player.isAlive) { + resolution_function(target) + val health = player.Health + val armor = player.Armor + val playerGUID = player.GUID + sendResponse(PlanetsideAttributeMessage(playerGUID, 0, health)) + sendResponse(PlanetsideAttributeMessage(playerGUID, 4, armor)) + avatarService ! AvatarServiceMessage(continent.Id, AvatarAction.PlanetsideAttribute(playerGUID, 0, health)) + avatarService ! AvatarServiceMessage(continent.Id, AvatarAction.PlanetsideAttribute(playerGUID, 4, armor)) + if(health == 0 && player.isAlive) { + KillPlayer(player) + } + } + + case AvatarResponse.Destroy(victim, killer, weapon, pos) => + // guid = victim // killer = killer ;) + sendResponse(DestroyMessage(victim, killer, weapon, pos)) + + case AvatarResponse.DestroyDisplay(killer, victim, method, unk) => + sendResponse(DestroyDisplayMessage(killer, victim, method, unk)) + + case AvatarResponse.DropItem(pkt) => + if(tplayer_guid != guid) { + sendResponse(pkt) + } + + case AvatarResponse.EquipmentInHand(pkt) => + if(tplayer_guid != guid) { + sendResponse(pkt) + } + + case AvatarResponse.HitHint(source_guid) => + sendResponse(HitHint(source_guid, guid)) + + case AvatarResponse.KilledWhileInVehicle() => + if(player.isAlive && player.VehicleSeated.nonEmpty) { + (continent.GUID(player.VehicleSeated.get) match { + case Some(obj : Vehicle) => + if(obj.Health == 0) Some(obj) + else None + case Some(obj : TurretDeployable) => + if(obj.Health == 0) Some(obj) + else None + case Some(obj : FacilityTurret) => + if(obj.Health == 0) Some(obj) + else None + case _ => + None + }) match { + case Some(obj : PlanetSideGameObject with Vitality) => + obj.LastShot match { + case Some(cause) => + player.History(cause) + case None => ; + } + KillPlayer(player) + case _ => ; + } + } + + case AvatarResponse.LoadPlayer(pkt) => + if(tplayer_guid != guid) { + sendResponse(pkt) + } + + case AvatarResponse.ObjectDelete(item_guid, unk) => + if(tplayer_guid != guid) { + sendResponse(ObjectDeleteMessage(item_guid, unk)) + } + + case AvatarResponse.ObjectHeld(slot) => + if(tplayer_guid != guid) { + sendResponse(ObjectHeldMessage(guid, slot, false)) + } + + case AvatarResponse.PlanetsideAttribute(attribute_type, attribute_value) => + if(tplayer_guid != guid) { + sendResponse(PlanetsideAttributeMessage(guid, attribute_type, attribute_value)) + } + + case AvatarResponse.PlayerState(msg, spectating, weaponInHand) => + if(tplayer_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 = Vector3.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)) || + ((msg.vel.isEmpty || Vector3.MagnitudeSquared(msg.vel.get).toInt == 0) && time > 2000) || + (time > 1000)) || + (distanceSq > 160000 && time > 5000)) { + sendResponse( + PlayerStateMessage( + guid, + location, + msg.vel, + msg.facingYaw, + msg.facingPitch, + msg.facingYawUpper, + unk1 = 0, + msg.is_crouching, + msg.is_jumping, + msg.jump_thrust, + msg.is_cloaked + ) + ) + player.lastSeenStreamMessage(guid.guid) = now + } + } + + case AvatarResponse.PutDownFDU(target) => + if(tplayer_guid != guid) { + sendResponse(GenericObjectActionMessage(target, 212)) + } + + case AvatarResponse.Release(tplayer) => + if(tplayer_guid != guid) { + TurnPlayerIntoCorpse(tplayer) + } + + case AvatarResponse.Reload(item_guid) => + if(tplayer_guid != guid) { + sendResponse(ReloadMessage(item_guid, 1, 0)) + } + + case AvatarResponse.SetEmpire(object_guid, faction) => + if(tplayer_guid != guid) { + sendResponse(SetEmpireMessage(object_guid, faction)) + } + + case AvatarResponse.StowEquipment(target, slot, item) => + if(tplayer_guid != guid) { + val definition = item.Definition + sendResponse( + ObjectCreateDetailedMessage( + definition.ObjectId, + item.GUID, + ObjectCreateMessageParent(target, slot), + definition.Packet.DetailedConstructorData(item).get + ) + ) + } + + case AvatarResponse.WeaponDryFire(weapon_guid) => + if(tplayer_guid != guid) { + sendResponse(WeaponDryFireMessage(weapon_guid)) + } + + case _ => ; + } + } + + /** + * na + * @param tplayer na + * @param msg na + * @param order na + */ + def HandleDoorMessage(tplayer : Player, msg : UseItemMessage, order : Door.Exchange) : Unit = { + val door_guid = msg.object_guid + order match { + case Door.OpenEvent() => + continent.GUID(door_guid) match { + case Some(door : Door) => + sendResponse(GenericObjectStateMsg(door_guid, 16)) + localService ! LocalServiceMessage(continent.Id, LocalAction.DoorOpens(tplayer.GUID, continent, door)) + + case _ => + log.warn(s"door $door_guid wanted to be opened but could not be found") + } + + case Door.CloseEvent() => + sendResponse(GenericObjectStateMsg(door_guid, 17)) + localService ! LocalServiceMessage(continent.Id, LocalAction.DoorCloses(tplayer.GUID, door_guid)) + + case Door.NoEvent() => ; + } + } + + /** + * na + * @param toChannel na + * @param guid na + * @param reply na + */ + def HandleLocalServiceResponse(toChannel : String, guid : PlanetSideGUID, reply : LocalResponse.Response) : Unit = { + val tplayer_guid = if(player.HasGUID) player.GUID + else PlanetSideGUID(0) + reply match { + case LocalResponse.AlertDestroyDeployable(obj) => + //the (former) owner (obj.OwnerName) should process this message + avatar.Deployables.Remove(obj) + UpdateDeployableUIElements(avatar.Deployables.UpdateUIElement(obj.Definition.Item)) + + case LocalResponse.DeployableMapIcon(behavior, deployInfo) => + if(tplayer_guid != guid) { + sendResponse(DeployableObjectsInfoMessage(behavior, deployInfo)) + } + + case LocalResponse.DoorOpens(door_guid) => + if(tplayer_guid != guid) { + sendResponse(GenericObjectStateMsg(door_guid, 16)) + } + + case LocalResponse.DoorCloses(door_guid) => //door closes for everyone + sendResponse(GenericObjectStateMsg(door_guid, 17)) + + case LocalResponse.EliminateDeployable(obj : TurretDeployable, guid, pos) => + if(obj.Health == 0) { + DeconstructDeployable(obj, guid, pos) + } + else { + DeconstructDeployable(obj, guid, pos, obj.Orientation, if(obj.MountPoints.isEmpty) 2 + else 1) + } + + case LocalResponse.EliminateDeployable(obj : ComplexDeployable, guid, pos) => + if(obj.Health == 0) { + DeconstructDeployable(obj, guid, pos) + } + else { + DeconstructDeployable(obj, guid, pos, obj.Orientation, 1) + } + + case LocalResponse.EliminateDeployable(obj : ExplosiveDeployable, guid, pos) => + if(obj.Exploded || obj.Health == 0) { + DeconstructDeployable(obj, guid, pos) + } + else { + DeconstructDeployable(obj, guid, pos, obj.Orientation, 2) + } + + case LocalResponse.EliminateDeployable(obj, guid, pos) => + if(obj.Health == 0) { + DeconstructDeployable(obj, guid, pos) + } + else { + DeconstructDeployable(obj, guid, pos, obj.Orientation, 2) + } + + case LocalResponse.HackClear(target_guid, unk1, unk2) => + log.trace(s"Clearing hack for ${target_guid}") + // Reset hack state for all players + sendResponse(HackMessage(0, target_guid, guid, 0, unk1, HackState.HackCleared, unk2)) + // Set the object faction displayed back to it's original owner faction + + continent.GUID(target_guid) match { + case Some(obj) => + sendResponse(SetEmpireMessage(target_guid, obj.asInstanceOf[FactionAffinity].Faction)) + case None => ; + } + + case LocalResponse.HackObject(target_guid, unk1, unk2) => + if(tplayer_guid != guid && continent.GUID(target_guid).get.asInstanceOf[Hackable].HackedBy.get._1.Faction != player.Faction) { + // If the player is not in the faction that hacked this object then send the packet that it's been hacked, so they can either unhack it or use the hacked object + // Don't send this to the faction that hacked the object, otherwise it will interfere with the new SetEmpireMessage QoL change that changes the object colour to their faction (but only visible to that faction) + sendResponse(HackMessage(0, target_guid, guid, 100, unk1, HackState.Hacked, unk2)) + } + if(continent.GUID(target_guid).get.asInstanceOf[Hackable].HackedBy.get._1.Faction == player.Faction) { + // Make the hacked object look like it belongs to the hacking empire, but only for that empire's players (so that infiltrators on stealth missions won't be given away to opposing factions) + sendResponse(SetEmpireMessage(target_guid, player.Faction)) + } + case LocalResponse.HackCaptureTerminal(target_guid, unk1, unk2, isResecured) => + var value = 0L + if(isResecured) { + value = 17039360L + } + else { + import scala.concurrent.ExecutionContext.Implicits.global + val future = ask(localService, HackCaptureActor.GetHackTimeRemainingNanos(target_guid))(1 second) + val time = Await.result(future, 1 second).asInstanceOf[Long] + // todo: blocking call. Not good. + val hack_time_remaining_ms = TimeUnit.MILLISECONDS.convert(time, TimeUnit.NANOSECONDS) + val deciseconds_remaining = (hack_time_remaining_ms / 100) + val hacking_faction = continent.GUID(target_guid).get.asInstanceOf[Hackable].HackedBy.get._1.Faction + // See PlanetSideAttributeMessage #20 documentation for an explanation of how the timer is calculated + val start_num = hacking_faction match { + case PlanetSideEmpire.TR => 65536L + case PlanetSideEmpire.NC => 131072L + case PlanetSideEmpire.VS => 196608L + } + value = start_num + deciseconds_remaining + } + sendResponse(PlanetsideAttributeMessage(target_guid, 20, value)) + + case LocalResponse.ObjectDelete(object_guid, unk) => + if(tplayer_guid != guid) { + sendResponse(ObjectDeleteMessage(object_guid, unk)) + } + + case LocalResponse.ProximityTerminalEffect(object_guid, effectState) => + if(tplayer_guid != guid) { + sendResponse(ProximityTerminalUseMessage(PlanetSideGUID(0), object_guid, effectState)) + } + + case LocalResponse.SetEmpire(object_guid, empire) => + sendResponse(SetEmpireMessage(object_guid, empire)) + + case LocalResponse.TriggerEffect(target_guid, effect, effectInfo, triggerLocation) => + sendResponse(TriggerEffectMessage(target_guid, effect, effectInfo, triggerLocation)) + + case LocalResponse.TriggerSound(sound, pos, unk, volume) => + sendResponse(TriggerSoundMessage(sound, pos, unk, volume)) + + case _ => ; + } + } + + /** + * na + * @param tplayer na + * @param reply na + */ + def HandleMountMessages(tplayer : Player, reply : Mountable.Exchange) : Unit = { + reply match { + case Mountable.CanMount(obj : ImplantTerminalMech, seat_num) => + MountingAction(tplayer, obj, seat_num) + sendResponse(PlanetsideAttributeMessage(obj.GUID, 0, 1000L)) //health of mech + + case Mountable.CanMount(obj : PlanetSideGameObject with WeaponTurret, seat_num) => + obj.WeaponControlledFromSeat(seat_num) match { + case Some(weapon : Tool) => + //update mounted weapon belonging to seat + weapon.AmmoSlots.foreach(slot => { + //update the magazine(s) in the weapon, specifically + val magazine = slot.Box + sendResponse(InventoryStateMessage(magazine.GUID, weapon.GUID, magazine.Capacity.toLong)) + }) + case _ => ; //no weapons to update + } + sendResponse(PlanetsideAttributeMessage(obj.GUID, 0, obj.Health)) + MountingAction(tplayer, obj, seat_num) + + case Mountable.CanMount(obj : Vehicle, seat_num) => + val obj_guid : PlanetSideGUID = obj.GUID + val player_guid : PlanetSideGUID = tplayer.GUID + log.info(s"MountVehicleMsg: $player_guid mounts $obj_guid @ $seat_num") + vehicleService ! VehicleServiceMessage.Decon(RemoverActor.ClearSpecific(List(obj), continent)) //clear timer + PlayerActionsToCancel() + sendResponse(PlanetsideAttributeMessage(obj_guid, 0, obj.Health)) + sendResponse(PlanetsideAttributeMessage(obj_guid, 68, 0)) //shield health + sendResponse(PlanetsideAttributeMessage(obj_guid, 113, 0)) //capacitor + if(seat_num == 0) { + //simplistic vehicle ownership management + obj.Owner match { + case Some(owner_guid) => + continent.GUID(owner_guid) match { + case Some(previous_owner : Player) => + if(previous_owner.VehicleOwned.contains(obj_guid)) { + previous_owner.VehicleOwned = None //simplistic ownership management, player loses vehicle ownership + } + case _ => ; + } + case None => ; + } + tplayer.VehicleOwned = Some(obj_guid) + obj.Owner = Some(tplayer.GUID) + } + obj.WeaponControlledFromSeat(seat_num) match { + case Some(weapon : Tool) => + //update mounted weapon belonging to seat + weapon.AmmoSlots.foreach(slot => { + //update the magazine(s) in the weapon, specifically + val magazine = slot.Box + sendResponse(InventoryStateMessage(magazine.GUID, weapon.GUID, magazine.Capacity)) + }) + case _ => ; //no weapons to update + } + //sendResponse(PlanetsideAttributeMessage(obj.GUID, 0, obj.Health)) //TODO vehicle max health in definition + vehicleService ! VehicleServiceMessage.Decon(RemoverActor.ClearSpecific(List(obj), continent)) //clear timer + AccessContents(obj) + MountingAction(tplayer, obj, seat_num) + + case Mountable.CanMount(obj : Mountable, _) => + log.warn(s"MountVehicleMsg: $obj is some generic mountable object and nothing will happen") + + case Mountable.CanDismount(obj : ImplantTerminalMech, seat_num) => + DismountAction(tplayer, obj, seat_num) + + case Mountable.CanDismount(obj : PlanetSideGameObject with WeaponTurret, seat_num) => + DismountAction(tplayer, obj, seat_num) + + case Mountable.CanDismount(obj : Vehicle, seat_num) => + val player_guid : PlanetSideGUID = tplayer.GUID + if(player_guid == player.GUID) { + //disembarking self + TotalDriverVehicleControl(obj) + UnAccessContents(obj) + DismountAction(tplayer, obj, seat_num) + } + else { + vehicleService ! VehicleServiceMessage(continent.Id, VehicleAction.KickPassenger(player_guid, seat_num, true, obj.GUID)) + } + if(obj.Seats.values.count(_.isOccupied) == 0) { + vehicleService ! VehicleServiceMessage.Decon(RemoverActor.AddTask(obj, continent, obj.Definition.DeconstructionTime)) //start vehicle decay + } + + case Mountable.CanDismount(obj : Mountable, _) => + log.warn(s"DismountVehicleMsg: $obj is some generic mountable object and nothing will happen") + + case Mountable.CanNotMount(obj : Vehicle, seat_num) => + log.warn(s"MountVehicleMsg: $tplayer attempted to mount $obj's seat $seat_num, but was not allowed") + if(obj.SeatPermissionGroup(seat_num).contains(AccessPermissionGroup.Driver)) { + sendResponse(ChatMsg(ChatMessageType.CMT_OPEN, false, "", "You are not the driver of this vehicle.", None)) + } + + case Mountable.CanNotMount(obj : Mountable, seat_num) => + log.warn(s"MountVehicleMsg: $tplayer attempted to mount $obj's seat $seat_num, but was not allowed") + + case Mountable.CanNotDismount(obj, seat_num) => + log.warn(s"DismountVehicleMsg: $tplayer attempted to dismount $obj's seat $seat_num, but was not allowed") + } + } + + /** + * na + * @param tplayer na + * @param msg na + * @param order na + */ + def HandleTerminalMessage(tplayer : Player, msg : ItemTransactionMessage, order : Terminal.Exchange) : Unit = { + order match { + case Terminal.BuyExosuit(exosuit, subtype) => //refresh armor points + tplayer.History(HealFromExoSuitChange(PlayerSource(tplayer), exosuit)) + if(tplayer.ExoSuit == exosuit) { + if(exosuit == ExoSuitType.MAX) { + //special MAX case - clear any special state + player.UsingSpecial = SpecialExoSuitDefinition.Mode.Normal + player.ExoSuit = exosuit + if(Loadout.DetermineSubtype(tplayer) != subtype) { + //special MAX case - suit switching to a different MAX suit; we need to change the main weapon + sendResponse(ArmorChangedMessage(tplayer.GUID, exosuit, subtype)) + avatarService ! AvatarServiceMessage(player.Continent, AvatarAction.ArmorChanged(tplayer.GUID, exosuit, subtype)) + val arms = tplayer.Slot(0).Equipment.get + val putTask = PutEquipmentInSlot(tplayer, Tool(GlobalDefinitions.MAXArms(subtype, tplayer.Faction)), 0) + taskResolver ! DelayedObjectHeld(tplayer, 0, List(TaskResolver.GiveTask(putTask.task, putTask.subs :+ RemoveEquipmentFromSlot(tplayer, arms, 0)))) + } + } + //outside of the MAX condition above, we should seldom reach this point through conventional methods + tplayer.Armor = tplayer.MaxArmor + sendResponse(PlanetsideAttributeMessage(tplayer.GUID, 4, tplayer.Armor)) + avatarService ! AvatarServiceMessage(tplayer.Continent, AvatarAction.PlanetsideAttribute(tplayer.GUID, 4, tplayer.Armor)) + sendResponse(ItemTransactionResultMessage(msg.terminal_guid, TransactionType.Buy, true)) + } + else { + //load a complete new exo-suit and shuffle the inventory around + val originalSuit = tplayer.ExoSuit + //save inventory before it gets cleared (empty holsters) + val dropPred = DropPredicate(tplayer) + val (dropHolsters, beforeHolsters) = clearHolsters(tplayer.Holsters().iterator).partition(dropPred) + val (dropInventory, beforeInventory) = tplayer.Inventory.Clear().partition(dropPred) + //change suit (clear inventory and change holster sizes; note: holsters must be empty before this point) + tplayer.ExoSuit = exosuit + tplayer.Armor = tplayer.MaxArmor + //delete everything not dropped + (beforeHolsters ++ beforeInventory).foreach({ elem => + sendResponse(ObjectDeleteMessage(elem.obj.GUID, 0)) + }) + beforeHolsters.foreach({ elem => + avatarService ! AvatarServiceMessage(tplayer.Continent, AvatarAction.ObjectDelete(tplayer.GUID, elem.obj.GUID)) + }) + //report change + sendResponse(ArmorChangedMessage(tplayer.GUID, exosuit, subtype)) + avatarService ! AvatarServiceMessage(player.Continent, AvatarAction.ArmorChanged(tplayer.GUID, exosuit, subtype)) + sendResponse(PlanetsideAttributeMessage(tplayer.GUID, 4, tplayer.Armor)) + avatarService ! AvatarServiceMessage(player.Continent, AvatarAction.PlanetsideAttribute(tplayer.GUID, 4, tplayer.Armor)) + val finalInventory = if(exosuit == ExoSuitType.MAX) { + //MAX weapon to be placed in first pistol slot; slot to be drawn + taskResolver ! DelayedObjectHeld(tplayer, 0, List(PutEquipmentInSlot(tplayer, Tool(GlobalDefinitions.MAXArms(subtype, tplayer.Faction)), 0))) + //fill melee slot + fillEmptyHolsters(List(tplayer.Slot(4)).iterator, beforeHolsters) ++ beforeInventory + } + else { + //remove potential MAX weapon + val normalWeapons = if(originalSuit == ExoSuitType.MAX) { + val (maxWeapons, normalWeapons) = beforeHolsters.partition(elem => elem.obj.Size == EquipmentSize.Max) + maxWeapons.foreach(entry => { + taskResolver ! GUIDTask.UnregisterEquipment(entry.obj)(continent.GUID) + }) + normalWeapons + } + else { + tplayer.DrawnSlot = Player.HandsDownSlot + sendResponse(ObjectHeldMessage(tplayer.GUID, Player.HandsDownSlot, true)) + avatarService ! AvatarServiceMessage(tplayer.Continent, AvatarAction.ObjectHeld(tplayer.GUID, Player.HandsDownSlot)) + beforeHolsters + } + //fill holsters + val (afterHolsters, toInventory) = normalWeapons.partition(elem => elem.obj.Size == tplayer.Slot(elem.start).Size) + afterHolsters.foreach({ elem => tplayer.Slot(elem.start).Equipment = elem.obj }) + fillEmptyHolsters(tplayer.Holsters().iterator, toInventory ++ beforeInventory) + } + //draw holsters + tplayer.VisibleSlots.foreach({ index => + tplayer.Slot(index).Equipment match { + case Some(obj) => + val definition = obj.Definition + sendResponse( + ObjectCreateDetailedMessage( + definition.ObjectId, + obj.GUID, + ObjectCreateMessageParent(tplayer.GUID, index), + definition.Packet.DetailedConstructorData(obj).get + ) + ) + avatarService ! AvatarServiceMessage(player.Continent, AvatarAction.EquipmentInHand(player.GUID, 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( + 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( + 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 = Vector3(0, 0, tplayer.Orientation.z) + ((dropHolsters ++ dropInventory).map(_.obj) ++ drop).foreach(obj => { + //TODO make a sound when dropping stuff + continent.Ground ! Zone.Ground.DropItem(obj, pos, orient) + }) + sendResponse(ItemTransactionResultMessage(msg.terminal_guid, TransactionType.Buy, true)) + } + + case Terminal.BuyEquipment(item) => ; + tplayer.Fit(item) match { + case Some(index) => + sendResponse(ItemTransactionResultMessage(msg.terminal_guid, TransactionType.Buy, true)) + taskResolver ! PutEquipmentInSlot(tplayer, item, index) + case None => + sendResponse(ItemTransactionResultMessage(msg.terminal_guid, TransactionType.Buy, false)) + } + + case Terminal.SellEquipment() => + tplayer.FreeHand.Equipment match { + case Some(item) => + if(item.GUID == msg.item_guid) { + sendResponse(ItemTransactionResultMessage(msg.terminal_guid, TransactionType.Sell, true)) + taskResolver ! RemoveEquipmentFromSlot(tplayer, item, Player.FreeHandSlot) + } + case None => + sendResponse(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(ItemTransactionResultMessage(msg.terminal_guid, TransactionType.Loadout, true)) + tplayer.History(HealFromExoSuitChange(PlayerSource(tplayer), exosuit)) + //ensure arm is down + tplayer.DrawnSlot = Player.HandsDownSlot + sendResponse(ObjectHeldMessage(tplayer.GUID, Player.HandsDownSlot, true)) + avatarService ! AvatarServiceMessage(tplayer.Continent, AvatarAction.ObjectHeld(tplayer.GUID, Player.HandsDownSlot)) + //load + val dropPred = DropPredicate(tplayer) + val (dropHolsters, beforeHolsters) = clearHolsters(tplayer.Holsters().iterator).partition(dropPred) + val (dropInventory, beforeInventory) = tplayer.Inventory.Clear().partition(dropPred) + val (_, afterHolsters) = holsters.partition(dropPred) + //dropped items are lost + val (_, afterInventory) = inventory.partition(dropPred) + //dropped items are lost + val beforeFreeHand = tplayer.FreeHand.Equipment + //change suit (clear inventory and change holster sizes; note: holsters must be empty before this point) + tplayer.ExoSuit = exosuit + tplayer.Armor = tplayer.MaxArmor + //delete everything (not dropped) + beforeHolsters.foreach({ elem => + avatarService ! AvatarServiceMessage(tplayer.Continent, AvatarAction.ObjectDelete(tplayer.GUID, elem.obj.GUID)) + }) + (beforeHolsters ++ beforeInventory).foreach({ elem => + sendResponse(ObjectDeleteMessage(elem.obj.GUID, 0)) + taskResolver ! GUIDTask.UnregisterEquipment(elem.obj)(continent.GUID) + }) + //report change + sendResponse(ArmorChangedMessage(tplayer.GUID, exosuit, subtype)) + avatarService ! AvatarServiceMessage(tplayer.Continent, AvatarAction.ArmorChanged(tplayer.GUID, exosuit, subtype)) + sendResponse(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 { + case Some(item) => + tplayer.FreeHand.Equipment = beforeFreeHand + val definition = item.Definition + sendResponse( + ObjectCreateDetailedMessage( + definition.ObjectId, + item.GUID, + ObjectCreateMessageParent(tplayer.GUID, Player.FreeHandSlot), + definition.Packet.DetailedConstructorData(item).get + ) + ) + case None => ; + } + //draw holsters + if(exosuit == ExoSuitType.MAX) { + tplayer.DrawnSlot = 0 + val (maxWeapons, otherWeapons) = afterHolsters.partition(entry => { + entry.obj.Size == EquipmentSize.Max + }) + taskResolver ! DelayedObjectHeld(tplayer, 0, List(PutEquipmentInSlot(tplayer, maxWeapons.head.obj, 0))) + otherWeapons + } + else { + afterHolsters + }.foreach(entry => { + taskResolver ! PutEquipmentInSlot(tplayer, entry.obj, entry.start) + }) + //put items into inventory + afterInventory.foreach(entry => { + taskResolver ! PutEquipmentInSlot(tplayer, entry.obj, entry.start) + }) + //drop stuff on ground + val pos = tplayer.Position + val orient = Vector3(0, 0, tplayer.Orientation.z) + ((dropHolsters ++ dropInventory).map(_.obj)).foreach(obj => { + continent.Ground ! Zone.Ground.DropItem(obj, pos, orient) + }) + + case Terminal.VehicleLoadout(definition, weapons, inventory) => + log.info(s"$tplayer wants to change their vehicle equipment loadout to their option #${msg.unk1 + 1}") + FindLocalVehicle match { + case Some(vehicle) => + sendResponse(ItemTransactionResultMessage(msg.terminal_guid, TransactionType.Loadout, true)) + val (_, afterInventory) = inventory.partition(DropPredicate(tplayer)) + //dropped items are lost + //remove old inventory + val deleteEquipment : (Int, Equipment) => Unit = DeleteEquipmentFromVehicle(vehicle) + vehicle.Inventory.Clear().foreach({ case InventoryItem(obj, index) => deleteEquipment(index, obj) }) + val stowEquipment : (Int, Equipment) => TaskResolver.GiveTask = StowNewEquipmentInVehicle(vehicle) + (if(vehicle.Definition == definition) { + //vehicles are the same type; transfer over weapon ammo + //TODO ammo switching? no vehicle weapon does that currently but ... + //TODO want to completely swap weapons, but holster icon vanishes temporarily after swap + //TODO BFR arms must be swapped properly + val channel = s"${vehicle.Actor}" + weapons.foreach({ case InventoryItem(obj, index) => + val savedWeapon = obj.asInstanceOf[Tool] + val existingWeapon = vehicle.Weapons(index).Equipment.get.asInstanceOf[Tool] + (0 until existingWeapon.MaxAmmoSlot).foreach({ index => + val existingBox = existingWeapon.AmmoSlots(index).Box + existingBox.Capacity = savedWeapon.AmmoSlots(index).Box.Capacity + //use VehicleAction.InventoryState2; VehicleAction.InventoryState temporarily glitches ammo count in ui + vehicleService ! VehicleServiceMessage(channel, VehicleAction.InventoryState2(PlanetSideGUID(0), existingBox.GUID, existingWeapon.GUID, existingBox.Capacity)) + }) + }) + afterInventory + } + else { + //do not transfer over weapon ammo + if(vehicle.Definition.TrunkSize == definition.TrunkSize && vehicle.Definition.TrunkOffset == definition.TrunkOffset) { + afterInventory + } + else { + //accommodate as much of inventory as possible + val (stow, _) = GridInventory.recoverInventory(afterInventory, vehicle.Inventory) //dropped items can be forgotten + stow + } + }).foreach({ case InventoryItem(obj, index) => + taskResolver ! stowEquipment(index, obj) + }) + case None => + log.error(s"can not apply the loadout - can not find a vehicle") + sendResponse(ItemTransactionResultMessage(msg.terminal_guid, TransactionType.Loadout, false)) + } + + case Terminal.LearnCertification(cert) => + val name = tplayer.Name + if(!tplayer.Certifications.contains(cert)) { + val guid = tplayer.GUID + log.info(s"$name is learning the $cert certification for ${Certification.Cost.Of(cert)} points") + avatar.Certifications += cert + StartBundlingPackets() + AddToDeployableQuantities(cert, player.Certifications) + sendResponse(PlanetsideAttributeMessage(guid, 24, cert.id.toLong)) + tplayer.Certifications.intersect(Certification.Dependencies.Like(cert)).foreach(entry => { + log.info(s"$cert replaces the learned certification $entry that cost ${Certification.Cost.Of(entry)} points") + avatar.Certifications -= entry + sendResponse(PlanetsideAttributeMessage(guid, 25, entry.id.toLong)) + }) + StopBundlingPackets() + sendResponse(ItemTransactionResultMessage(msg.terminal_guid, TransactionType.Learn, true)) + } + else { + log.warn(s"$name already knows the $cert certification, so he can't learn it") + sendResponse(ItemTransactionResultMessage(msg.terminal_guid, TransactionType.Learn, false)) + } + + case Terminal.SellCertification(cert) => + val name = tplayer.Name + if(tplayer.Certifications.contains(cert)) { + val guid = tplayer.GUID + log.info(s"$name is forgetting the $cert certification for ${Certification.Cost.Of(cert)} points") + avatar.Certifications -= cert + StartBundlingPackets() + RemoveFromDeployablesQuantities(cert, player.Certifications) + sendResponse(PlanetsideAttributeMessage(guid, 25, cert.id.toLong)) + tplayer.Certifications.intersect(Certification.Dependencies.FromAll(cert)).foreach(entry => { + log.info(s"$name is also forgetting the ${Certification.Cost.Of(entry)}-point $entry certification which depends on $cert") + avatar.Certifications -= entry + RemoveFromDeployablesQuantities(entry, player.Certifications) + sendResponse(PlanetsideAttributeMessage(guid, 25, entry.id.toLong)) + }) + StopBundlingPackets() + sendResponse(ItemTransactionResultMessage(msg.terminal_guid, TransactionType.Sell, true)) + } + else { + log.warn(s"$name doesn't know what a $cert certification is, so he can't forget it") + sendResponse(ItemTransactionResultMessage(msg.terminal_guid, TransactionType.Learn, false)) + } + + case Terminal.LearnImplant(implant) => + val terminal_guid = msg.terminal_guid + val implant_type = implant.Type + val message = s"Implants: $tplayer wants to learn $implant_type" + val (interface, slotNumber) = tplayer.VehicleSeated match { + case Some(mech_guid) => + ( + continent.Map.TerminalToInterface.get(mech_guid.guid), + if(!avatar.Implants.exists({ slot => slot.Implant == implant_type })) { + //no duplicates + avatar.InstallImplant(implant) + } + else { + None + } + ) + case _ => + (None, None) + } + if(interface.contains(terminal_guid.guid) && slotNumber.isDefined) { + val slot = slotNumber.get + log.info(s"$message - put in slot $slot") + sendResponse(AvatarImplantMessage(tplayer.GUID, ImplantAction.Add, slot, implant_type.id)) + sendResponse(ItemTransactionResultMessage(terminal_guid, TransactionType.Learn, true)) + } + else { + if(interface.isEmpty) { + log.warn(s"$message - not interacting with a terminal") + } + else if(!interface.contains(terminal_guid.guid)) { + log.warn(s"$message - interacting with the wrong terminal, ${interface.get}") + } + else if(slotNumber.isEmpty) { + log.warn(s"$message - already knows that implant") + } + else { + log.warn(s"$message - forgot to sit at a terminal") + } + sendResponse(ItemTransactionResultMessage(terminal_guid, TransactionType.Learn, false)) + } + + case Terminal.SellImplant(implant) => + val terminal_guid = msg.terminal_guid + val implant_type = implant.Type + val (interface, slotNumber) = tplayer.VehicleSeated match { + case Some(mech_guid) => + ( + continent.Map.TerminalToInterface.get(mech_guid.guid), + avatar.UninstallImplant(implant_type) + ) + case None => + (None, None) + } + if(interface.contains(terminal_guid.guid) && slotNumber.isDefined) { + val slot = slotNumber.get + log.info(s"$tplayer is selling $implant_type - take from slot $slot") + sendResponse(AvatarImplantMessage(tplayer.GUID, ImplantAction.Remove, slot, 0)) + sendResponse(ItemTransactionResultMessage(terminal_guid, TransactionType.Sell, true)) + } + else { + val message = s"$tplayer can not sell $implant_type" + if(interface.isEmpty) { + log.warn(s"$message - not interacting with a terminal") + } + else if(!interface.contains(terminal_guid.guid)) { + log.warn(s"$message - interacting with the wrong terminal, ${interface.get}") + } + else if(slotNumber.isEmpty) { + log.warn(s"$message - does not know that implant") + } + else { + log.warn(s"$message - forgot to sit at a terminal") + } + sendResponse(ItemTransactionResultMessage(terminal_guid, TransactionType.Sell, false)) + } + + case Terminal.BuyVehicle(vehicle, weapons, trunk) => + continent.Map.TerminalToSpawnPad.get(msg.terminal_guid.guid) match { + case Some(pad_guid) => + val pad = continent.GUID(pad_guid).get.asInstanceOf[VehicleSpawnPad] + vehicle.Faction = tplayer.Faction + vehicle.Position = pad.Position + vehicle.Orientation = pad.Orientation + //default loadout, weapons + log.info(s"default weapons: ${weapons.size}") + val vWeapons = vehicle.Weapons + weapons.foreach(entry => { + val index = entry.start + vWeapons.get(index) match { + case Some(slot) => + slot.Equipment = None + slot.Equipment = entry.obj + case None => + log.warn(s"applying default loadout to $vehicle, can not find a mounted weapon @ $index") + } + }) + //default loadout, trunk + log.info(s"default trunk: ${trunk.size}") + val vTrunk = vehicle.Trunk + vTrunk.Clear() + trunk.foreach(entry => { + vTrunk += entry.start -> entry.obj + }) + taskResolver ! RegisterNewVehicle(vehicle, pad) + sendResponse(ItemTransactionResultMessage(msg.terminal_guid, TransactionType.Buy, true)) + + case None => + log.error(s"$tplayer wanted to spawn a vehicle, but there was no spawn pad associated with terminal ${msg.terminal_guid} to accept it") + } + + case Terminal.StartProximityEffect(term) => + val player_guid = player.GUID + val term_guid = term.GUID + StartUsingProximityUnit(term) //redundant but cautious + sendResponse(ProximityTerminalUseMessage(player_guid, term_guid, true)) + localService ! LocalServiceMessage(continent.Id, LocalAction.ProximityTerminalEffect(player_guid, term_guid, true)) + + case Terminal.StopProximityEffect(term) => + val player_guid = player.GUID + val term_guid = term.GUID + StopUsingProximityUnit(term) //redundant but cautious + sendResponse(ProximityTerminalUseMessage(player_guid, term_guid, false)) + localService ! LocalServiceMessage(continent.Id, LocalAction.ProximityTerminalEffect(player_guid, term_guid, false)) + + case Terminal.NoDeal() => + val order : String = if(msg == null) { + s"order $msg" + } + else { + "missing order" + } + log.warn(s"${tplayer.Name} made a request but the terminal rejected the $order") + sendResponse(ItemTransactionResultMessage(msg.terminal_guid, msg.transaction_type, false)) + } + } + + /** + * na + * @param toChannel na + * @param guid na + * @param reply na + */ + def HandleVehicleServiceResponse(toChannel : String, guid : PlanetSideGUID, reply : VehicleResponse.Response) : Unit = { + val tplayer_guid = if(player.HasGUID) player.GUID + else PlanetSideGUID(0) + reply match { + case VehicleResponse.Ownership(vehicle_guid) => + sendResponse(PlanetsideAttributeMessage(guid, 21, vehicle_guid.guid)) + + case VehicleResponse.AttachToRails(vehicle_guid, pad_guid) => + sendResponse(ObjectAttachMessage(pad_guid, vehicle_guid, 3)) + + case VehicleResponse.ChildObjectState(object_guid, pitch, yaw) => + if(tplayer_guid != guid) { + sendResponse(ChildObjectStateMessage(object_guid, pitch, yaw)) + } + + case VehicleResponse.ConcealPlayer(player_guid) => + //TODO this is the correct message; but, I don't know how to undo the effects of it + //sendResponse(GenericObjectActionMessage(player_guid, 36)) + sendResponse(PlanetsideAttributeMessage(player_guid, 29, 1)) + + case VehicleResponse.DismountVehicle(bailType, wasKickedByDriver) => + if(tplayer_guid != guid) { + sendResponse(DismountVehicleMsg(guid, bailType, wasKickedByDriver)) + } + + case VehicleResponse.DeployRequest(object_guid, state, unk1, unk2, pos) => + if(tplayer_guid != guid) { + sendResponse(DeployRequestMessage(guid, object_guid, state, unk1, unk2, pos)) + } + + case VehicleResponse.DetachFromRails(vehicle_guid, pad_guid, pad_position, pad_orientation_z) => + sendResponse(ObjectDetachMessage(pad_guid, vehicle_guid, pad_position + Vector3(0, 0, 0.5f), pad_orientation_z)) + + case VehicleResponse.EquipmentInSlot(pkt) => + if(tplayer_guid != guid) { + sendResponse(pkt) + } + + case VehicleResponse.HitHint(source_guid) => + sendResponse(HitHint(source_guid, player.GUID)) + + case VehicleResponse.InventoryState(obj, parent_guid, start, con_data) => + if(tplayer_guid != guid) { + //TODO prefer ObjectDetachMessage, but how to force ammo pools to update properly? + val obj_guid = obj.GUID + sendResponse(ObjectDeleteMessage(obj_guid, 0)) + sendResponse( + ObjectCreateDetailedMessage( + obj.Definition.ObjectId, + obj_guid, + ObjectCreateMessageParent(parent_guid, start), + con_data + ) + ) + } + + case msg@VehicleResponse.KickPassenger(seat_num, wasKickedByDriver, vehicle_guid) => + // seat_num seems to be correct if passenger is kicked manually by driver, but always seems to return 4 if user is kicked by seat permissions + log.info(s"$msg") + sendResponse(DismountVehicleMsg(guid, BailType.Kicked, wasKickedByDriver)) + if(tplayer_guid == guid) { + continent.GUID(vehicle_guid) match { + case Some(obj : Vehicle) => + UnAccessContents(obj) + case _ => ; + } + } + + case VehicleResponse.InventoryState2(obj_guid, parent_guid, value) => + if(tplayer_guid != guid) { + sendResponse(InventoryStateMessage(obj_guid, 0, parent_guid, value)) + } + + case VehicleResponse.LoadVehicle(vehicle, vtype, vguid, vdata) => + //this is not be suitable for vehicles with people who are seated in it before it spawns (if that is possible) + if(tplayer_guid != guid) { + sendResponse(ObjectCreateMessage(vtype, vguid, vdata)) + ReloadVehicleAccessPermissions(vehicle) + } + + case VehicleResponse.MountVehicle(vehicle_guid, seat) => + if(tplayer_guid != guid) { + sendResponse(ObjectAttachMessage(vehicle_guid, guid, seat)) + } + + case VehicleResponse.PlanetsideAttribute(vehicle_guid, attribute_type, attribute_value) => + if(tplayer_guid != guid) { + sendResponse(PlanetsideAttributeMessage(vehicle_guid, attribute_type, attribute_value)) + } + + case VehicleResponse.ResetSpawnPad(pad_guid) => + sendResponse(GenericObjectActionMessage(pad_guid, 92)) + + case VehicleResponse.RevealPlayer(player_guid) => + //TODO see note in ConcealPlayer + sendResponse(PlanetsideAttributeMessage(player_guid, 29, 0)) + + case VehicleResponse.SeatPermissions(vehicle_guid, seat_group, permission) => + if(tplayer_guid != guid) { + sendResponse(PlanetsideAttributeMessage(vehicle_guid, seat_group, permission)) + } + + case VehicleResponse.StowEquipment(vehicle_guid, slot, item_type, item_guid, item_data) => + if(tplayer_guid != guid) { + //TODO prefer ObjectAttachMessage, but how to force ammo pools to update properly? + sendResponse( + ObjectCreateDetailedMessage(item_type, item_guid, ObjectCreateMessageParent(vehicle_guid, slot), item_data) + ) + } + + case VehicleResponse.UnloadVehicle(vehicle_guid) => + sendResponse(ObjectDeleteMessage(vehicle_guid, 0)) + + case VehicleResponse.UnstowEquipment(item_guid) => + if(tplayer_guid != guid) { + //TODO prefer ObjectDetachMessage, but how to force ammo pools to update properly? + sendResponse(ObjectDeleteMessage(item_guid, 0)) + } + + case VehicleResponse.VehicleState(vehicle_guid, unk1, pos, ang, vel, unk2, unk3, unk4, wheel_direction, unk5, unk6) => + if(tplayer_guid != guid) { + sendResponse(VehicleStateMessage(vehicle_guid, unk1, pos, ang, vel, unk2, unk3, unk4, wheel_direction, unk5, unk6)) + if(player.VehicleSeated.contains(vehicle_guid)) { + player.Position = pos + } + } + case VehicleResponse.SendResponse(msg) => + sendResponse(msg) + + case VehicleResponse.UpdateAmsSpawnPoint(list) => + if(player.isBackpack) { + //dismiss old ams spawn point + ClearCurrentAmsSpawnPoint() + //draw new ams spawn point + list + .filter(tube => tube.Faction == player.Faction) + .sortBy(tube => Vector3.DistanceSquared(tube.Position, player.Position)) + .headOption match { + case Some(tube) => + sendResponse( + BattleplanMessage(41378949, "ams", continent.Number, List(BattleDiagramAction(DiagramActionCode.StartDrawing))) + ) + sendResponse( + BattleplanMessage(41378949, "ams", continent.Number, List(BattleDiagramAction.drawString(tube.Position.x, tube.Position.y, 3, 0, "AMS"))) + ) + amsSpawnPoint = Some(tube) + case None => ; + } + } + + case _ => ; + } + } + + /** + * na + * @param vehicle_guid na + * @param cargo_vehicle_guid na + * @param cargo_mountpoint na + * @param iteration na + */ + def HandleCheckCargoDismount(vehicle_guid : PlanetSideGUID, cargo_vehicle_guid : PlanetSideGUID, cargo_mountpoint : Int, iteration : Int) : Unit = { + val vehicle = continent.GUID(vehicle_guid.guid).get.asInstanceOf[Vehicle] + val cargo_vehicle = continent.GUID(cargo_vehicle_guid).get.asInstanceOf[Vehicle] + val distance = Vector3.Distance(vehicle.Position, cargo_vehicle.Position) + log.info(s"Dismount distance ${distance}") + if(distance > 15 || iteration > 20) { + // Vehicle has moved far enough away - close the cargo door + log.info("Vehicle is far enough away or disembark timed out - closing cargo door and returning full control to driver") + StartBundlingPackets() + // Return control of vehicle to driver + DriverVehicleControl(vehicle) + val cargoStatusMessage = CargoMountPointStatusMessage(cargo_vehicle_guid, PlanetSideGUID(0), PlanetSideGUID(0), vehicle_guid, cargo_mountpoint, CargoStatus.Empty, 0) + log.warn(cargoStatusMessage.toString) + // Do NOT send this packet back to the client directly. If you do and then send it again to all clients in the zone (including the client again) + // The client will get stuck in a state where the player cannot dismount as it thinks it is always trying to remount the cargo hold + avatarService ! AvatarServiceMessage(continent.Id, AvatarAction.SendResponse(player.GUID, cargoStatusMessage)) + StopBundlingPackets() + cargoMountTimer.cancel() + cargoDismountTimer.cancel() + } + else { + // Not far enough away - rescheduling check + import scala.concurrent.ExecutionContext.Implicits.global + cargoDismountTimer = context.system.scheduler.scheduleOnce(250 milliseconds, self, CheckCargoDismount(vehicle_guid, cargo_vehicle.GUID, cargo_mountpoint, iteration = iteration + 1)) + } + } + + /** + * na + * @param vehicle_guid na + * @param cargo_vehicle_guid na + * @param cargo_mountpoint na + * @param iteration na + */ + def HandleCheckCargoMounting(vehicle_guid : PlanetSideGUID, cargo_vehicle_guid : PlanetSideGUID, cargo_mountpoint : Int, iteration : Int) : Unit = { + val vehicle = continent.GUID(vehicle_guid.guid).get.asInstanceOf[Vehicle] + val cargo_vehicle = continent.GUID(cargo_vehicle_guid.guid).get.asInstanceOf[Vehicle] + val distance = Vector3.Distance(vehicle.Position, cargo_vehicle.Position) + log.warn(s"Mount distance ${distance}") + if(distance <= 8) { + // Vehicle is close enough that it should be within the cargo bay. Mount it. + log.info("Mounting vehicle cargo") + cargoMountTimer.cancel() + cargoDismountTimer.cancel() + val vehicle = continent.GUID(vehicle_guid).get.asInstanceOf[Vehicle] + StartBundlingPackets() + vehicleService ! VehicleServiceMessage(s"${vehicle.Actor}", VehicleAction.SendResponse(PlanetSideGUID(0), PlanetsideAttributeMessage(cargo_vehicle_guid, 0, cargo_vehicle.Health))) + vehicleService ! VehicleServiceMessage(s"${vehicle.Actor}", VehicleAction.SendResponse(PlanetSideGUID(0), PlanetsideAttributeMessage(cargo_vehicle_guid, 68, cargo_vehicle.Shields))) + val attachMessage = ObjectAttachMessage(cargo_vehicle_guid, vehicle_guid, cargo_mountpoint) + log.warn(attachMessage.toString) + sendResponse(attachMessage) + // This is required for when DismountVehicleCargoMsg is sent as the cargo_vehicle_guid isn't sent as a parameter + vehicle.MountedIn = cargo_vehicle_guid + cargo_vehicle.CargoHold(cargo_mountpoint).get.Occupant = vehicle + val orientation = if(vehicle.Definition == GlobalDefinitions.router) { + // mount router "sideways" in a lodestar + //todo: BFRs will likely also need this set + 1 + } + else { + 0 + } + val cargoStatusMessage = CargoMountPointStatusMessage(cargo_vehicle_guid, vehicle_guid, vehicle_guid, PlanetSideGUID(0), cargo_mountpoint, CargoStatus.Occupied, orientation) + log.warn(cargoStatusMessage.toString) + sendResponse(cargoStatusMessage) + StopBundlingPackets() + avatarService ! AvatarServiceMessage(continent.Id, AvatarAction.SendResponse(player.GUID, attachMessage)) + avatarService ! AvatarServiceMessage(continent.Id, AvatarAction.SendResponse(player.GUID, cargoStatusMessage)) + } + else if(distance > 25 || iteration >= 15) { + // Vehicle is too far away. Abort mounting. + log.info("Vehicle is too far away or didn't mount within allocated time. Aborting cargo mount.") + val cargoStatusMessage = CargoMountPointStatusMessage(cargo_vehicle_guid, PlanetSideGUID(0), PlanetSideGUID(0), vehicle_guid, cargo_mountpoint, CargoStatus.Empty, 0) + log.warn(cargoStatusMessage.toString) + // Do NOT send this packet back to the client directly. If you do and then send it again to all clients in the zone (including the client again) + // The client will get stuck in a state where the player cannot dismount as it thinks it is always trying to remount the cargo hold + avatarService ! AvatarServiceMessage(continent.Id, AvatarAction.SendResponse(player.GUID, cargoStatusMessage)) + cargoMountTimer.cancel() + cargoDismountTimer.cancel() + } + else { + // Not close enough, far away enough or timeout not exceeded. Reschedule check + import scala.concurrent.ExecutionContext.Implicits.global + cargoMountTimer = context.system.scheduler.scheduleOnce(1 second, self, CheckCargoMounting(vehicle_guid, cargo_vehicle_guid, cargo_mountpoint, iteration = iteration + 1)) + } + } + + /** + * na + * @param target na + */ + def HandleVehicleDamageResolution(target : Vehicle) : Unit = { + val targetGUID = target.GUID + val playerGUID = player.GUID + val continentId = continent.Id + val players = target.Seats.values.filter(seat => { + seat.isOccupied && seat.Occupant.get.isAlive + }) + if(target.Health > 0) { + //alert occupants to damage source + players.foreach(seat => { + val tplayer = seat.Occupant.get + avatarService ! AvatarServiceMessage(tplayer.Name, AvatarAction.HitHint(playerGUID, tplayer.GUID)) + }) + } + else { + //alert to vehicle death (hence, occupants' deaths) + players.foreach(seat => { + val tplayer = seat.Occupant.get + val tplayerGUID = tplayer.GUID + avatarService ! AvatarServiceMessage(tplayer.Name, AvatarAction.KilledWhileInVehicle(tplayerGUID)) + avatarService ! AvatarServiceMessage(continentId, AvatarAction.ObjectDelete(tplayerGUID, tplayerGUID)) //dead player still sees self + }) + //vehicle wreckage has no weapons + target.Weapons.values + .filter { + _.Equipment.nonEmpty + } + .foreach(slot => { + val wep = slot.Equipment.get + avatarService ! AvatarServiceMessage(continentId, AvatarAction.ObjectDelete(Service.defaultPlayerGUID, wep.GUID)) + }) + if(target.Definition == GlobalDefinitions.ams) { + target.Actor ! Deployment.TryDeploymentChange(DriveState.Undeploying) + ClearCurrentAmsSpawnPoint() + } + avatarService ! AvatarServiceMessage(continentId, AvatarAction.Destroy(targetGUID, playerGUID, playerGUID, target.Position)) + vehicleService ! VehicleServiceMessage.Decon(RemoverActor.ClearSpecific(List(target), continent)) + vehicleService ! VehicleServiceMessage.Decon(RemoverActor.AddTask(target, continent, Some(1 minute))) + } + vehicleService ! VehicleServiceMessage(continentId, VehicleAction.PlanetsideAttribute(Service.defaultPlayerGUID, targetGUID, 0, target.Health)) + vehicleService ! VehicleServiceMessage(s"${target.Actor}", VehicleAction.PlanetsideAttribute(Service.defaultPlayerGUID, targetGUID, 68, target.Shields)) + } + + /** + * na + * @param target na + */ + def HandleTurretDeployableDamageResolution(target : TurretDeployable) : Unit = { + //spitfires and field turrets + val health = target.Health + val guid = target.GUID + val continentId = continent.Id + if(health <= 0) { + //if occupants, kill them + target.Seats.values + .filter(seat => { + seat.isOccupied && seat.Occupant.get.isAlive + }) + .foreach(seat => { + val tplayer = seat.Occupant.get + val tplayerGUID = tplayer.GUID + avatarService ! AvatarServiceMessage(tplayer.Name, AvatarAction.KilledWhileInVehicle(tplayerGUID)) + avatarService ! AvatarServiceMessage(continentId, AvatarAction.ObjectDelete(tplayerGUID, tplayerGUID)) //dead player still sees self + }) + //destroy weapons + target.Weapons.values + .map(slot => slot.Equipment) + .collect { case Some(weapon) => + val wguid = weapon.GUID + sendResponse(ObjectDeleteMessage(wguid, 0)) + avatarService ! AvatarServiceMessage(continentId, AvatarAction.ObjectDelete(player.GUID, wguid)) + } + AnnounceDestroyDeployable(target, None) + } + avatarService ! AvatarServiceMessage(continentId, AvatarAction.PlanetsideAttribute(guid, 0, health)) + } + + /** + * na + * @param tplayer na + * @param vehicle na + */ + def HandleNtuCharging(tplayer : Player, vehicle : Vehicle) : Unit = { + log.trace(s"NtuCharging: Vehicle ${vehicle.GUID} is charging NTU capacitor.") + if(vehicle.Capacitor < vehicle.Definition.MaximumCapacitor) { + // Charging + vehicle.Capacitor += 100 + sendResponse(PlanetsideAttributeMessage(vehicle.GUID, 45, scala.math.round((vehicle.Capacitor.toFloat / vehicle.Definition.MaximumCapacitor.toFloat) * 10))) // set ntu on vehicle UI + avatarService ! AvatarServiceMessage(continent.Id, AvatarAction.PlanetsideAttribute(vehicle.GUID, 52, 1L)) // panel glow on + avatarService ! AvatarServiceMessage(continent.Id, AvatarAction.PlanetsideAttribute(vehicle.GUID, 49, 1L)) // orb particle effect on + + antChargingTick = context.system.scheduler.scheduleOnce(1000 milliseconds, self, NtuCharging(player, vehicle)) // Repeat until fully charged + } + else { + // Fully charged + sendResponse(PlanetsideAttributeMessage(vehicle.GUID, 45, scala.math.round((vehicle.Capacitor.toFloat / vehicle.Definition.MaximumCapacitor.toFloat) * 10).toInt)) // set ntu on vehicle UI + + // Turning off glow/orb effects on ANT doesn't seem to work when deployed. Try to undeploy ANT from server side + context.system.scheduler.scheduleOnce(vehicle.UndeployTime milliseconds, vehicle.Actor, Deployment.TryUndeploy(DriveState.Undeploying)) + } + } + + /** + * na + * @param tplayer na + * @param vehicle na + * @param silo_guid na + */ + def HandleNtuDischarging(tplayer : Player, vehicle : Vehicle, silo_guid : PlanetSideGUID) : Unit = { + log.trace(s"NtuDischarging: Vehicle ${vehicle.GUID} is discharging NTU into silo $silo_guid") + avatarService ! AvatarServiceMessage(continent.Id, AvatarAction.PlanetsideAttribute(vehicle.GUID, 49, 0L)) // orb particle effect off + avatarService ! AvatarServiceMessage(continent.Id, AvatarAction.PlanetsideAttribute(vehicle.GUID, 52, 1L)) // panel glow on + + var silo = continent.GUID(silo_guid).get.asInstanceOf[ResourceSilo] + // Check vehicle is still deployed before continuing. User can undeploy manually or vehicle may not longer be present. + if(vehicle.DeploymentState == DriveState.Deployed) { + if(vehicle.Capacitor > 0 && silo.ChargeLevel < silo.MaximumCharge) { + + // Make sure we don't exceed the silo maximum charge or remove much NTU from ANT if maximum is reached, or try to make ANT go below 0 NTU + var chargeToDeposit = Math.min(Math.min(vehicle.Capacitor, 100), (silo.MaximumCharge - silo.ChargeLevel)) + vehicle.Capacitor -= chargeToDeposit + silo.Actor ! ResourceSilo.UpdateChargeLevel(chargeToDeposit) + avatarService ! AvatarServiceMessage(continent.Id, AvatarAction.PlanetsideAttribute(silo_guid, 49, 1L)) // panel glow on & orb particles on + sendResponse(PlanetsideAttributeMessage(vehicle.GUID, 45, scala.math.round((vehicle.Capacitor.toFloat / vehicle.Definition.MaximumCapacitor.toFloat) * 10))) // set ntu on vehicle UI + + //todo: grant BEP to user + //todo: grant BEP to squad in range + //todo: notify map service to update ntu % on map for all users + + //todo: handle silo orb / panel glow properly if more than one person is refilling silo and one player stops. effects should stay on until all players stop + + if(vehicle.Capacitor > 0 && silo.ChargeLevel < silo.MaximumCharge) { + log.trace(s"NtuDischarging: ANT not empty and Silo not full. Scheduling another discharge") + // Silo still not full and ant still has charge left - keep rescheduling ticks + antDischargingTick = context.system.scheduler.scheduleOnce(1000 milliseconds, self, NtuDischarging(player, vehicle, silo_guid)) + } + else { + log.trace(s"NtuDischarging: ANT NTU empty or Silo NTU full.") + // Turning off glow/orb effects on ANT doesn't seem to work when deployed. Try to undeploy ANT from server side + context.system.scheduler.scheduleOnce(vehicle.UndeployTime milliseconds, vehicle.Actor, Deployment.TryUndeploy(DriveState.Undeploying)) + avatarService ! AvatarServiceMessage(continent.Id, AvatarAction.PlanetsideAttribute(silo_guid, 49, 0L)) // panel glow off & orb particles off + antDischargingTick.cancel() + } + } + else { + // This shouldn't normally be run, only if the client thinks the ANT has capacitor charge when it doesn't, or thinks the silo isn't full when it is. + log.warn(s"NtuDischarging: Invalid discharge state. ANT Capacitor: ${vehicle.Capacitor} Silo Capacitor: ${silo.ChargeLevel}") + // Turning off glow/orb effects on ANT doesn't seem to work when deployed. Try to undeploy ANT from server side + context.system.scheduler.scheduleOnce(vehicle.UndeployTime milliseconds, vehicle.Actor, Deployment.TryUndeploy(DriveState.Undeploying)) + avatarService ! AvatarServiceMessage(continent.Id, AvatarAction.PlanetsideAttribute(silo_guid, 49, 0L)) // panel glow off & orb particles off + antDischargingTick.cancel() + } + } + else { + log.trace(s"NtuDischarging: Vehicle is no longer deployed. Removing effects") + // Vehicle has changed from deployed and this should be the last timer tick sent + avatarService ! AvatarServiceMessage(continent.Id, AvatarAction.PlanetsideAttribute(vehicle.GUID, 52, 0L)) // panel glow off + avatarService ! AvatarServiceMessage(continent.Id, AvatarAction.PlanetsideAttribute(silo_guid, 49, 0L)) // panel glow off & orb particles off + antDischargingTick.cancel() + } + } + + /** + * na + * @param progressType na + * @param tplayer na + * @param target na + * @param tool_guid na + * @param delta na + * @param completeAction na + * @param tickAction na + */ + def HandleHackingProgress(progressType : Int, tplayer : Player, target : PlanetSideServerObject, tool_guid : PlanetSideGUID, delta : Float, completeAction : ()=>Unit, tickAction : Option[()=>Unit]) : Unit = { + progressBarUpdate.cancel + if(progressBarValue.isDefined) { + val progressBarVal : Float = progressBarValue.get + delta + val vis = if(progressBarVal == 0L) { + //hack state for progress bar visibility + HackState.Start + } + else if(progressBarVal > 100L) { + HackState.Finished + } + else { + HackState.Ongoing + } + sendResponse(HackMessage(progressType, target.GUID, player.GUID, progressBarVal.toInt, 0L, vis, 8L)) + if(progressBarVal > 100) { + //done + progressBarValue = None + // sendResponse(HackMessage(0, target.GUID, player.GUID, 100, 1114636288L, HackState.Hacked, 8L)) + completeAction() + } + else { + //continue next tick + tickAction.getOrElse(() => Unit)() + progressBarValue = Some(progressBarVal) + import scala.concurrent.ExecutionContext.Implicits.global + progressBarUpdate = context.system.scheduler.scheduleOnce(250 milliseconds, self, HackingProgress(progressType, tplayer, target, tool_guid, delta, completeAction)) + } + } + } + + /** + * na + * @param tplayer na + */ + def HandleSetCurrentAvatar(tplayer : Player) : Unit = { + player = tplayer + val guid = tplayer.GUID + StartBundlingPackets() + InitializeDeployableUIElements(avatar) + sendResponse(PlanetsideAttributeMessage(PlanetSideGUID(0), 75, 0)) + sendResponse(SetCurrentAvatarMessage(guid, 0, 0)) + sendResponse(ChatMsg(ChatMessageType.CMT_EXPANSIONS, true, "", "1 on", None)) //CC on //TODO once per respawn? + sendResponse(PlayerStateShiftMessage(ShiftState(1, tplayer.Position, tplayer.Orientation.z))) + //transfer vehicle ownership + player.VehicleOwned match { + case Some(vehicle_guid) => + continent.GUID(vehicle_guid) match { + case Some(vehicle : Vehicle) => + vehicle.Owner = player + vehicleService ! VehicleServiceMessage(continent.Id, VehicleAction.Ownership(guid, vehicle_guid)) + case _ => + player.VehicleOwned = None + } + case None => ; + } + if(spectator) { + sendResponse(ChatMsg(ChatMessageType.CMT_TOGGLESPECTATORMODE, false, "", "on", None)) + } + (0 until DetailedCharacterData.numberOfImplantSlots(tplayer.BEP)).foreach(slot => { + sendResponse(AvatarImplantMessage(guid, ImplantAction.Initialization, slot, 1)) //init implant slot + sendResponse(AvatarImplantMessage(guid, ImplantAction.Activation, slot, 0)) //deactivate implant + //TODO if this implant is Installed but does not have shortcut, add to a free slot or write over slot 61/62/63 + }) + sendResponse(PlanetsideAttributeMessage(PlanetSideGUID(0), 82, 0)) + //TODO if Medkit does not have shortcut, add to a free slot or write over slot 64 + sendResponse(CreateShortcutMessage(guid, 1, 0, true, Shortcut.MEDKIT)) + sendResponse(ChangeShortcutBankMessage(guid, 0)) + //FavoritesMessage + sendResponse(SetChatFilterMessage(ChatChannel.Local, false, ChatChannel.values.toList)) //TODO will not always be "on" like this + deadState = DeadState.Alive + sendResponse(AvatarDeadStateMessage(DeadState.Alive, 0, 0, tplayer.Position, player.Faction, true)) + sendResponse(PlanetsideAttributeMessage(guid, 53, 1)) + sendResponse(AvatarSearchCriteriaMessage(guid, List(0, 0, 0, 0, 0, 0))) + (1 to 73).foreach(i => { + sendResponse(PlanetsideAttributeMessage(PlanetSideGUID(i), 67, 0)) + }) + (0 to 30).foreach(i => { + //TODO 30 for a new character only? + sendResponse(AvatarStatisticsMessage(2, Statistics(0L))) + }) + //AvatarAwardMessage + //DisplayAwardMessage + //SquadDefinitionActionMessage and SquadDetailDefinitionUpdateMessage + //MapObjectStateBlockMessage and ObjectCreateMessage? + //TacticsMessage? + //change the owner on our deployables (re-draw the icons for our deployables too) + val name = tplayer.Name + val faction = tplayer.Faction + continent.DeployableList + .filter(_.OwnerName.contains(name)) + .foreach(obj => { + obj.Owner = guid + drawDeloyableIcon(obj) + }) + StopBundlingPackets() + drawDeloyableIcon = DontRedrawIcons } def handleControlPkt(pkt : PlanetSideControlPacket) = { @@ -1937,7 +2341,7 @@ class WorldSessionActor extends Actor with MDCContextAware { //TODO begin temp player character auto-loading; remove later import net.psforever.objects.GlobalDefinitions._ import net.psforever.types.CertificationType._ - val avatar = Avatar("TestCharacter" + sessionId.toString, PlanetSideEmpire.VS, CharacterGender.Female, 41, CharacterVoice.Voice1) + val avatar = Avatar(s"TestCharacter$sessionId", PlanetSideEmpire.VS, CharacterGender.Female, 41, CharacterVoice.Voice1) avatar.Certifications += StandardAssault avatar.Certifications += MediumAssault avatar.Certifications += StandardExoSuit @@ -1968,27 +2372,31 @@ class WorldSessionActor extends Actor with MDCContextAware { avatar.Certifications += UniMAX avatar.Certifications += Engineering avatar.Certifications += CombatEngineering - avatar.Certifications += AdvancedEngineering avatar.Certifications += FortificationEngineering avatar.Certifications += AssaultEngineering + avatar.Certifications += Hacking + avatar.Certifications += AdvancedHacking this.avatar = avatar + + InitializeDeployableQuantities(avatar) //set deployables ui elements AwardBattleExperiencePoints(avatar, 1000000L) player = new Player(avatar) //player.Position = Vector3(3561.0f, 2854.0f, 90.859375f) //home3, HART C - player.Position = Vector3(3940.3984f, 4343.625f, 266.45312f) + player.Position = Vector3(3940.3984f, 4343.625f, 266.45312f) //z6, Anguta +// player.Position = Vector3(3571.2266f, 3278.0938f, 119.0f) //ce test player.Orientation = Vector3(0f, 0f, 90f) //player.Position = Vector3(4262.211f ,4067.0625f ,262.35938f) //z6, Akna.tower //player.Orientation = Vector3(0f, 0f, 132.1875f) // player.ExoSuit = ExoSuitType.MAX //TODO strange issue; divide number above by 10 when uncommenting - player.Slot(0).Equipment = SimpleItem(remote_electronics_kit) //Tool(GlobalDefinitions.StandardPistol(player.Faction)) - player.Slot(2).Equipment = Tool(nano_dispenser) //punisher //suppressor + player.Slot(0).Equipment = ConstructionItem(ace) //Tool(GlobalDefinitions.StandardPistol(player.Faction)) + player.Slot(2).Equipment = ConstructionItem(advanced_ace) //punisher //suppressor player.Slot(4).Equipment = Tool(GlobalDefinitions.StandardMelee(player.Faction)) - player.Slot(6).Equipment = AmmoBox(upgrade_canister) //bullet_9mm - player.Slot(9).Equipment = AmmoBox(rocket, 11) //bullet_9mm - player.Slot(12).Equipment = AmmoBox(frag_cartridge) //bullet_9mm - player.Slot(33).Equipment = AmmoBox(bullet_9mm_AP) - player.Slot(36).Equipment = AmmoBox(GlobalDefinitions.StandardPistolAmmo(player.Faction)) - player.Slot(39).Equipment = AmmoBox(plasma_cartridge) //SimpleItem(remote_electronics_kit) + player.Slot(6).Equipment = ConstructionItem(ace) //bullet_9mm + player.Slot(9).Equipment = ConstructionItem(ace) //bullet_9mm + player.Slot(12).Equipment = ConstructionItem(ace) //bullet_9mm + player.Slot(33).Equipment = Tool(suppressor) //AmmoBox(bullet_9mm_AP) + //player.Slot(36).Equipment = AmmoBox(GlobalDefinitions.StandardPistolAmmo(player.Faction)) + //player.Slot(39).Equipment = SimpleItem(remote_electronics_kit) player.Locker.Inventory += 0 -> SimpleItem(remote_electronics_kit) //TODO end temp player character auto-loading self ! ListAccountCharacters @@ -2111,11 +2519,16 @@ class WorldSessionActor extends Actor with MDCContextAware { case msg @ BeginZoningMessage() => log.info("Reticulating splines ...") - traveler.zone = continent.Id + val continentId = continent.Id + traveler.zone = continentId + val faction = player.Faction + val factionOnContinentChannel = s"$continentId/$faction" StartBundlingPackets() - avatarService ! Service.Join(continent.Id) - localService ! Service.Join(continent.Id) - vehicleService ! Service.Join(continent.Id) + avatarService ! Service.Join(continentId) + avatarService ! Service.Join(factionOnContinentChannel) + localService ! Service.Join(continentId) + localService ! Service.Join(factionOnContinentChannel) + vehicleService ! Service.Join(continentId) galaxyService ! Service.Join("galaxy") configZone(continent) sendResponse(TimeOfDayMessage(1191182336)) @@ -2125,6 +2538,65 @@ class WorldSessionActor extends Actor with MDCContextAware { sendResponse(PlanetsideAttributeMessage(PlanetSideGUID(0), 112, 1)) //common //(0 to 255).foreach(i => { sendResponse(SetEmpireMessage(PlanetSideGUID(i), PlanetSideEmpire.VS)) }) + //find and reclaim own deployables, if any + val guid = player.GUID + val foundDeployables = continent.DeployableList.filter(obj => obj.OwnerName.contains(player.Name) && obj.Health > 0) + localService ! LocalServiceMessage.Deployables(RemoverActor.ClearSpecific(foundDeployables, continent)) + foundDeployables.foreach(obj => { + if(avatar.Deployables.Add(obj)) { + obj.Owner = guid + log.info(s"Found a ${obj.Definition.Name} of ours while loading the zone") + } + }) + //render deployable objects + val (turrets, normal) = continent.DeployableList.partition(obj => + DeployableToolbox.UnifiedType(obj.Definition.Item) == DeployedItem.portable_manned_turret + ) + normal.foreach(obj => { + val definition = obj.Definition + sendResponse( + ObjectCreateMessage( + definition.ObjectId, + obj.GUID, + definition.Packet.ConstructorData(obj).get + ) + ) + }) + turrets.foreach(obj => { + val objGUID = obj.GUID + val definition = obj.Definition + sendResponse( + ObjectCreateMessage( + definition.ObjectId, + objGUID, + definition.Packet.ConstructorData(obj).get + ) + ) + //seated players + obj.asInstanceOf[Mountable].Seats.values + .map(_.Occupant) + .collect { + case Some(occupant) => + if(occupant.isAlive) { + val tdefintion = occupant.Definition + sendResponse( + ObjectCreateMessage( + tdefintion.ObjectId, + occupant.GUID, + ObjectCreateMessageParent(objGUID, 0), + tdefintion.Packet.ConstructorData(occupant).get + ) + ) + } + } + }) + //draw our faction's deployables on the map + continent.DeployableList + .filter(obj => obj.Faction == faction && obj.Health > 0) + .foreach(obj => { + val deployInfo = DeployableInfo(obj.GUID, Deployable.Icon(obj.Definition.Item), obj.Position, obj.Owner.getOrElse(PlanetSideGUID(0))) + sendResponse(DeployableObjectsInfoMessage(DeploymentAction.Build, deployInfo)) + }) //render Equipment that was dropped into zone before the player arrived continent.EquipmentOnGround.foreach(item => { val definition = item.Definition @@ -2241,7 +2713,7 @@ class WorldSessionActor extends Actor with MDCContextAware { continent.Map.TurretToWeapon.foreach({ case((turret_guid, weapon_guid)) => val parent_guid = PlanetSideGUID(turret_guid) continent.GUID(turret_guid) match { - case Some(turret : MannedTurret) => + case Some(turret : FacilityTurret) => //attached weapon turret.ControlledWeapon(1) match { case Some(obj : Tool) => @@ -2421,14 +2893,11 @@ class WorldSessionActor extends Actor with MDCContextAware { CSRZone.read(traveler, msg) match { case (true, zone, pos) => if(player.isAlive) { - player.Die //die to suspend client-driven position change updates + player.Die //die to suspend client-driven position change updates (in theory) PlayerActionsToCancel() player.Position = pos - traveler.zone = zone - continent.Population ! Zone.Population.Release(avatar) - continent.Population ! Zone.Population.Leave(avatar) avatarService ! AvatarServiceMessage(continent.Id, AvatarAction.ObjectDelete(player.GUID, player.GUID)) - taskResolver ! TaskBeforeZoneChange(GUIDTask.UnregisterAvatar(player)(continent.GUID), zone) + LoadZonePhysicalSpawnPoint(zone, pos, Vector3.Zero, 0) } case (false, _, _) => ; @@ -2500,151 +2969,41 @@ class WorldSessionActor extends Actor with MDCContextAware { case msg @ ChangeAmmoMessage(item_guid, unk1) => log.info("ChangeAmmo: " + msg) - FindContainedWeapon match { + FindContainedEquipment match { + case(Some(_), Some(obj : ConstructionItem)) => + PerformConstructionItemAmmoChange(obj, obj.AmmoTypeIndex) case (Some(obj), Some(tool : Tool)) => - val originalAmmoType = tool.AmmoType - do { - val requestedAmmoType = tool.NextAmmoType - val fullMagazine = tool.MaxMagazine - if(requestedAmmoType != tool.AmmoSlot.Box.AmmoType) { - FindEquipmentStock(obj, FindAmmoBoxThatUses(requestedAmmoType), fullMagazine, CountAmmunition).reverse match { - case Nil => ; - case x :: xs => - val (deleteFunc, modifyFunc) : ((Int, AmmoBox)=>Unit, (AmmoBox, Int)=>Unit) = obj match { - case (veh : Vehicle) => - (DeleteEquipmentFromVehicle(veh), ModifyAmmunitionInVehicle(veh)) - case _ => - (DeleteEquipment(obj), ModifyAmmunition(obj)) - } - val (stowFuncTask, stowFunc) : ((Int, AmmoBox)=>TaskResolver.GiveTask, (Int, AmmoBox)=>Unit) = obj match { - case (veh : Vehicle) => - (StowNewEquipmentInVehicle(veh), StowEquipmentInVehicles(veh)) - case _ => - (StowNewEquipment(obj), StowEquipment(obj)) - } - xs.foreach(item => { - obj.Inventory -= x.start - deleteFunc(item.start, item.obj.asInstanceOf[AmmoBox]) - }) - - //box will be the replacement ammo; give it the discovered magazine and load it into the weapon @ 0 - val box = x.obj.asInstanceOf[AmmoBox] - val originalBoxCapacity = box.Capacity - val tailReloadValue : Int = if(xs.isEmpty) { 0 } else { xs.map(_.obj.asInstanceOf[AmmoBox].Capacity).reduceLeft(_ + _) } - val sumReloadValue : Int = originalBoxCapacity + tailReloadValue - val previousBox = tool.AmmoSlot.Box //current magazine in tool - sendResponse(ObjectDetachMessage(tool.GUID, previousBox.GUID, Vector3.Zero, 0f)) - sendResponse(ObjectDetachMessage(player.GUID, box.GUID, Vector3.Zero, 0f)) - obj.Inventory -= x.start //remove replacement ammo from inventory - val ammoSlotIndex = tool.FireMode.AmmoSlotIndex - tool.AmmoSlots(ammoSlotIndex).Box = box //put replacement ammo in tool - sendResponse(ObjectAttachMessage(tool.GUID, box.GUID, ammoSlotIndex)) - - //announce swapped ammunition box in weapon - val previous_box_guid = previousBox.GUID - val boxDef = box.Definition - val box_guid = box.GUID - val tool_guid = tool.GUID - sendResponse(ChangeAmmoMessage(tool_guid, box.Capacity)) - avatarService ! AvatarServiceMessage(continent.Id, AvatarAction.ChangeAmmo(player.GUID, tool_guid, ammoSlotIndex,previous_box_guid, boxDef.ObjectId, box.GUID, boxDef.Packet.ConstructorData(box).get)) - - //handle inventory contents - box.Capacity = (if(sumReloadValue <= fullMagazine) { - sumReloadValue - } - else { - val splitReloadAmmo : Int = sumReloadValue - fullMagazine - log.info(s"ChangeAmmo: taking ${originalBoxCapacity - splitReloadAmmo} from a box of ${originalBoxCapacity} $requestedAmmoType") - val boxForInventory = AmmoBox(box.Definition, splitReloadAmmo) - obj.Inventory += x.start -> boxForInventory //block early; assumption warning: swappable ammo types have the same icon size - taskResolver ! stowFuncTask(x.start, boxForInventory) - fullMagazine - }) - sendResponse(InventoryStateMessage(box.GUID, tool.GUID, box.Capacity)) //should work for both players and vehicles - log.info(s"ChangeAmmo: loading ${box.Capacity} $requestedAmmoType into ${tool.GUID} @ $ammoSlotIndex") - if(previousBox.Capacity > 0) { - //divide capacity across other existing and not full boxes of that ammo type - var capacity = previousBox.Capacity - val iter = obj.Inventory.Items - .filter(entry => { - entry.obj match { - case (item : AmmoBox) => - item.AmmoType == originalAmmoType && item.FullCapacity != item.Capacity - case _ => - false - } - }) - .toList - .sortBy(_.start) - .iterator - while(capacity > 0 && iter.hasNext) { - val entry = iter.next - val item : AmmoBox = entry.obj.asInstanceOf[AmmoBox] - val ammoAllocated = math.min(item.FullCapacity - item.Capacity, capacity) - log.info(s"ChangeAmmo: putting $ammoAllocated back into a box of ${item.Capacity} $originalAmmoType") - capacity -= ammoAllocated - modifyFunc(item, -ammoAllocated) - } - previousBox.Capacity = capacity - } - - if(previousBox.Capacity > 0) { - //split previousBox into AmmoBox objects of appropriate max capacity, e.g., 100 9mm -> 2 x 50 9mm - obj.Inventory.Fit(previousBox) match { - case Some(index) => - stowFunc(index, previousBox) - case None => - NormalItemDrop(player, continent, avatarService)(previousBox) - } - val dropFunc : (Equipment)=>Unit = NewItemDrop(player, continent, avatarService) - AmmoBox.Split(previousBox) match { - case Nil | _ :: Nil => ; //done (the former case is technically not possible) - case _ :: xs => - modifyFunc(previousBox, 0) //update to changed capacity value - xs.foreach(box => { - obj.Inventory.Fit(box) match { - case Some(index) => - obj.Inventory += index -> box //block early, for purposes of Fit - taskResolver ! stowFuncTask(index, box) - case None => - dropFunc(box) - } - }) - } - } - else { - taskResolver ! GUIDTask.UnregisterObjectTask(previousBox)(continent.GUID) - } - } - } - } - while(tool.AmmoType != originalAmmoType && tool.AmmoType != tool.AmmoSlot.Box.AmmoType) - - case (_, Some(_)) => - log.error(s"ChangeAmmo: the object that was found for $item_guid was not a Tool") + PerformToolAmmoChange(tool, obj) + case (_, Some(obj)) => + log.error(s"ChangeAmmo: the object ${obj.Definition.Name} is not a valid type") case (_, None) => log.error(s"ChangeAmmo: can not find $item_guid") } case msg @ ChangeFireModeMessage(item_guid, fire_mode) => log.info("ChangeFireMode: " + msg) - FindWeapon match { - case Some(tool : Tool) => - val originalModeIndex = tool.FireModeIndex - tool.NextFireMode - val modeIndex = tool.FireModeIndex - val tool_guid = tool.GUID - if(originalModeIndex != modeIndex) { + FindEquipment match { + case Some(obj : PlanetSideGameObject with FireModeSwitch[_]) => + val originalModeIndex = obj.FireModeIndex + obj match { + case cItem : ConstructionItem => + NextConstructionItemFireMode(cItem, originalModeIndex) + case _ => + obj.NextFireMode + } + val modeIndex = obj.FireModeIndex + val tool_guid = obj.GUID + if(originalModeIndex == modeIndex) { + obj.FireModeIndex = originalModeIndex + sendResponse(ChangeFireModeMessage(tool_guid, originalModeIndex)) //reinforcement + } + else { log.info(s"ChangeFireMode: changing $tool_guid to fire mode $modeIndex") sendResponse(ChangeFireModeMessage(tool_guid, modeIndex)) avatarService ! AvatarServiceMessage(continent.Id, AvatarAction.ChangeFireMode(player.GUID, tool_guid, modeIndex)) } - else { - tool.FireModeIndex = originalModeIndex - sendResponse(ChangeFireModeMessage(tool_guid, originalModeIndex)) - } case Some(_) => - log.error(s"ChangeFireMode: the object that was found for $item_guid was not a Tool") + log.error(s"ChangeFireMode: the object that was found for $item_guid does not possess fire modes") case None => log.error(s"ChangeFireMode: can not find $item_guid") } @@ -2654,7 +3013,7 @@ class WorldSessionActor extends Actor with MDCContextAware { if(shooting.isEmpty) { FindEquipment match { case Some(tool : Tool) => - if(tool.GUID == item_guid && tool.Magazine > 0) { + if(tool.Magazine > 0) { shooting = Some(item_guid) avatarService ! AvatarServiceMessage(continent.Id, AvatarAction.ChangeFireState_Start(player.GUID, item_guid)) } @@ -2692,6 +3051,24 @@ class WorldSessionActor extends Actor with MDCContextAware { if(tool.Magazine == 0) { FireCycleCleanup(tool) } + case Some(trigger : BoomerTrigger) => + val playerGUID = player.GUID + avatarService ! AvatarServiceMessage(continent.Id, AvatarAction.ChangeFireState_Start(playerGUID, item_guid)) + continent.GUID(trigger.Companion.getOrElse(PlanetSideGUID(0))) match { + case Some(boomer : BoomerDeployable) => + val boomerGUID = boomer.GUID + boomer.Exploded = true + sendResponse(TriggerEffectMessage(boomerGUID, "detonate_boomer")) + sendResponse(PlanetsideAttributeMessage(boomerGUID, 29, 1)) + sendResponse(ObjectDeleteMessage(boomerGUID, 0)) + localService ! LocalServiceMessage(continent.Id, LocalAction.TriggerEffect(playerGUID, "detonate_boomer", boomerGUID)) + avatarService ! AvatarServiceMessage(continent.Id, AvatarAction.PlanetsideAttribute(boomerGUID, 29, 1)) + avatarService ! AvatarServiceMessage(continent.Id, AvatarAction.ObjectDelete(playerGUID, boomerGUID)) + localService ! LocalServiceMessage.Deployables(RemoverActor.AddTask(boomer, continent, Some(0 seconds))) + case Some(_) | None => ; + } + FindEquipmentToDelete(item_guid, trigger) + trigger.Companion = None case _ => ; } progressBarUpdate.cancel //TODO independent action? @@ -2852,42 +3229,22 @@ class WorldSessionActor extends Actor with MDCContextAware { log.info(s"RequestDestroy: must own vehicle in order to deconstruct it") } - case Some(obj : Equipment) => - val findFunc : PlanetSideGameObject with Container => Option[(PlanetSideGameObject with Container, Option[Int])] = FindInLocalContainer(object_guid) - - findFunc(player.Locker) - .orElse(findFunc(player)) - .orElse(accessedContainer match { - case Some(parent) => - findFunc(parent) - case None => - None - }) - .orElse(FindLocalVehicle match { - case Some(parent) => - findFunc(parent) - case None => - None - }) - match { - case Some((parent, Some(slot))) => - obj.Position = Vector3.Zero - taskResolver ! RemoveEquipmentFromSlot(parent, obj, slot) - log.info(s"RequestDestroy: equipment $obj") - - case _ => - if(continent.EquipmentOnGround.contains(obj)) { - obj.Position = Vector3.Zero - continent.Ground ! Zone.Ground.RemoveItem(object_guid) - avatarService ! AvatarServiceMessage.Ground(RemoverActor.ClearSpecific(List(obj), continent)) - avatarService ! AvatarServiceMessage(continent.Id, AvatarAction.ObjectDelete(PlanetSideGUID(0), object_guid)) - log.info(s"RequestDestroy: equipment $obj on ground") - } - else { - log.warn(s"RequestDestroy: equipment $obj exists, but can not be reached") - } + case Some(obj : BoomerTrigger) => + if(FindEquipmentToDelete(object_guid, obj)) { + continent.GUID(obj.Companion.getOrElse(PlanetSideGUID(0))) match { + case Some(boomer : BoomerDeployable) => + boomer.Trigger = None + localService ! LocalServiceMessage.Deployables(RemoverActor.AddTask(boomer, continent, Some(0 seconds))) + //continent.Deployables ! Zone.Deployable.Dismiss(boomer) + case Some(thing) => + log.info(s"RequestDestroy: BoomerTrigger object connected to wrong object - $thing") + case None => ; + } } + case Some(obj : Equipment) => + FindEquipmentToDelete(object_guid, obj) + case Some(_ : LocalProjectile) => FindProjectileEntry(object_guid) match { case Some(projectile) => @@ -2901,6 +3258,30 @@ class WorldSessionActor extends Actor with MDCContextAware { log.warn(s"RequestDestroy: projectile ${object_guid.guid} has never been fired") } + case Some(obj : BoomerDeployable) => + localService ! LocalServiceMessage.Deployables(RemoverActor.AddTask(obj, continent, Some(0 seconds))) + obj.Trigger match { + case Some(trigger) => + obj.Trigger = None + val guid = trigger.GUID + Zone.EquipmentIs.Where(trigger, guid, continent) match { + case Some(Zone.EquipmentIs.InContainer(container, index)) => + container.Slot(index).Equipment = None + case Some(Zone.EquipmentIs.OnGround()) => + continent.Ground ! Zone.Ground.RemoveItem(guid) + case Some(Zone.EquipmentIs.Orphaned()) => + log.warn(s"RequestDestroy: boomer_trigger@$guid has been found but it seems to be orphaned") + case _ => ; + } + avatarService ! AvatarServiceMessage(continent.Id, AvatarAction.ObjectDelete(PlanetSideGUID(0), guid)) + GUIDTask.UnregisterObjectTask(trigger)(continent.GUID) + + case None => ; + } + + case Some(obj : PlanetSideGameObject with Deployable) => + localService ! LocalServiceMessage.Deployables(RemoverActor.AddTask(obj, continent, Some(0 seconds))) + case Some(thing) => log.warn(s"RequestDestroy: not allowed to delete object $thing") @@ -2935,7 +3316,12 @@ class WorldSessionActor extends Actor with MDCContextAware { false //abort when too many items at destination or other failure case } } && indexSlot.Equipment.contains(item)) { - PerformMoveItem(item, source, index, destination, dest, destItemEntry) + if(PermitEquipmentStow(item, destination)) { + PerformMoveItem(item, source, index, destination, dest, destItemEntry) + } + else { + log.error(s"MoveItem: $item disallowed storage in $destination") + } } else if(!indexSlot.Equipment.contains(item)) { log.error(s"MoveItem: wanted to move $item_guid, but found unexpected ${indexSlot.Equipment.get} at source location") @@ -2980,7 +3366,12 @@ class WorldSessionActor extends Actor with MDCContextAware { ) }, target.Fit(item)) match { case (Some((source, Some(index))), Some(dest)) => - PerformMoveItem(item, source, index, target, dest, None) + if(PermitEquipmentStow(item, target)) { + PerformMoveItem(item, source, index, target, dest, None) + } + else { + log.error(s"LootItem: $item disallowed storage in $target") + } case (None, _) => log.error(s"LootItem: can not find where $item is put currently") case (_, None) => @@ -3059,7 +3450,6 @@ class WorldSessionActor extends Actor with MDCContextAware { self ! WorldSessionActor.HackingProgress(progressType = 1, player, panel, tool.GUID, hackSpeed, FinishResecuringIFFLock(panel)) log.info("Resecuring an IFF lock") } - } } case _ => ; @@ -3159,7 +3549,7 @@ class WorldSessionActor extends Actor with MDCContextAware { } } - case Some(obj : MannedTurret) => + case Some(obj : FacilityTurret) => player.Slot(player.DrawnSlot).Equipment match { case Some(tool : Tool) => if(tool.Definition == GlobalDefinitions.nano_dispenser && tool.Magazine > 0) { @@ -3312,8 +3702,44 @@ class WorldSessionActor extends Actor with MDCContextAware { } accessedContainer = None - case msg @ DeployObjectMessage(guid, unk1, pos, roll, pitch, yaw, unk2) => - log.info("DeployObject: " + msg) + case msg @ DeployObjectMessage(guid, unk1, pos, orient, unk2) => + log.info(s"DeployObject: $msg") + //the hand with the construction item is no longer drawn + //TODO consider player.Slot(player.LastDrawnSlot) + (player.Holsters.find(slot => slot.Equipment.nonEmpty && slot.Equipment.get.GUID == guid) match { + case Some(slot) => + slot.Equipment + case None => + None + }) match { + case Some(obj : ConstructionItem) => + val ammoType = obj.AmmoType match { + case DeployedItem.portable_manned_turret => + GlobalDefinitions.PortableMannedTurret(player.Faction).Item //faction-specific turret + case turret => + turret + } + log.info(s"Constructing a ${ammoType}") + val dObj : PlanetSideGameObject with Deployable = Deployables.Make(ammoType)() + dObj.Position = pos + dObj.Orientation = orient + dObj.Faction = player.Faction + dObj.Owner = player.GUID + dObj.OwnerName = player.Name + val tasking : TaskResolver.GiveTask = dObj match { + case turret : TurretDeployable => + GUIDTask.RegisterDeployableTurret(turret)(continent.GUID) + case _ => + GUIDTask.RegisterObjectTask(dObj)(continent.GUID) + } + taskResolver ! CallBackForTask(tasking, continent.Deployables, Zone.Deployable.Build(dObj, obj)) + + case Some(obj) => + log.warn(s"$obj is something?") + case None => + log.warn("nothing?") + + } case msg @ GenericObjectStateMsg(object_guid, unk1) => log.info("GenericObjectState: " + msg) @@ -3475,6 +3901,8 @@ class WorldSessionActor extends Actor with MDCContextAware { Some((obj, hitInfo.shot_origin, hitInfo.hit_pos)) case Some(obj : Vehicle) => Some((obj, hitInfo.shot_origin, hitInfo.hit_pos)) + case Some(obj : PlanetSideGameObject with Deployable) => + Some((obj, hitInfo.shot_origin, hitInfo.hit_pos)) case _ => None } @@ -4142,6 +4570,20 @@ class WorldSessionActor extends Actor with MDCContextAware { ) } + def CallBackForTask(task : TaskResolver.GiveTask, sendTo : ActorRef, pass : Any) : TaskResolver.GiveTask = { + TaskResolver.GiveTask( + new Task() { + private val destination = sendTo + private val passMsg = pass + + def Execute(resolver : ActorRef) : Unit = { + destination ! passMsg + resolver ! scala.util.Success(this) + } + }, List(task) + ) + } + /** * 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. @@ -4248,7 +4690,7 @@ class WorldSessionActor extends Actor with MDCContextAware { * @param tool the nano-dispenser that was used to perform this upgrade * @param upgrade the new upgrade state */ - private def FinishUpgradingMannedTurret(target : MannedTurret, tool : Tool, upgrade : TurretUpgrade.Value)() : Unit = { + private def FinishUpgradingMannedTurret(target : FacilityTurret, tool : Tool, upgrade : TurretUpgrade.Value)() : Unit = { log.info(s"Converting manned wall turret weapon to $upgrade") tool.Magazine = 0 sendResponse(InventoryStateMessage(tool.AmmoSlot.Box.GUID, tool.GUID, 0)) @@ -4604,7 +5046,7 @@ class WorldSessionActor extends Actor with MDCContextAware { } /** - * Given an object that contains a box of amunition in its `Inventry` at a certain location, + * Given an object that contains a box of amunition in its `Inventory` at a certain location, * change the amount of ammunition within that box. * @param obj the `Container` * @param box an `AmmoBox` to modify @@ -4849,6 +5291,141 @@ class WorldSessionActor extends Actor with MDCContextAware { } } + /** + * na + * @param equipment na + * @param obj na + * @return `true`, if the object is allowed to contain the type of equipment object + */ + def PermitEquipmentStow(equipment : Equipment, obj : PlanetSideGameObject with Container) : Boolean = { + equipment match { + case item : BoomerTrigger => + obj.isInstanceOf[Player] //a BoomerTrigger can only be stowed in a player's holsters or inventory + case _ => + true + } + } + + def PerformToolAmmoChange(tool : Tool, obj : PlanetSideGameObject with Container) : Unit = { + val originalAmmoType = tool.AmmoType + do { + val requestedAmmoType = tool.NextAmmoType + val fullMagazine = tool.MaxMagazine + if(requestedAmmoType != tool.AmmoSlot.Box.AmmoType) { + FindEquipmentStock(obj, FindAmmoBoxThatUses(requestedAmmoType), fullMagazine, CountAmmunition).reverse match { + case Nil => ; + case x :: xs => + val (deleteFunc, modifyFunc) : ((Int, AmmoBox)=>Unit, (AmmoBox, Int)=>Unit) = obj match { + case (veh : Vehicle) => + (DeleteEquipmentFromVehicle(veh), ModifyAmmunitionInVehicle(veh)) + case _ => + (DeleteEquipment(obj), ModifyAmmunition(obj)) + } + val (stowFuncTask, stowFunc) : ((Int, AmmoBox)=>TaskResolver.GiveTask, (Int, AmmoBox)=>Unit) = obj match { + case (veh : Vehicle) => + (StowNewEquipmentInVehicle(veh), StowEquipmentInVehicles(veh)) + case _ => + (StowNewEquipment(obj), StowEquipment(obj)) + } + xs.foreach(item => { + obj.Inventory -= x.start + deleteFunc(item.start, item.obj.asInstanceOf[AmmoBox]) + }) + + //box will be the replacement ammo; give it the discovered magazine and load it into the weapon @ 0 + val box = x.obj.asInstanceOf[AmmoBox] + val originalBoxCapacity = box.Capacity + val tailReloadValue : Int = if(xs.isEmpty) { 0 } else { xs.map(_.obj.asInstanceOf[AmmoBox].Capacity).reduceLeft(_ + _) } + val sumReloadValue : Int = originalBoxCapacity + tailReloadValue + val previousBox = tool.AmmoSlot.Box //current magazine in tool + sendResponse(ObjectDetachMessage(tool.GUID, previousBox.GUID, Vector3.Zero, 0f)) + sendResponse(ObjectDetachMessage(player.GUID, box.GUID, Vector3.Zero, 0f)) + obj.Inventory -= x.start //remove replacement ammo from inventory + val ammoSlotIndex = tool.FireMode.AmmoSlotIndex + tool.AmmoSlots(ammoSlotIndex).Box = box //put replacement ammo in tool + sendResponse(ObjectAttachMessage(tool.GUID, box.GUID, ammoSlotIndex)) + + //announce swapped ammunition box in weapon + val previous_box_guid = previousBox.GUID + val boxDef = box.Definition + val box_guid = box.GUID + val tool_guid = tool.GUID + sendResponse(ChangeAmmoMessage(tool_guid, box.Capacity)) + avatarService ! AvatarServiceMessage(continent.Id, AvatarAction.ChangeAmmo(player.GUID, tool_guid, ammoSlotIndex,previous_box_guid, boxDef.ObjectId, box.GUID, boxDef.Packet.ConstructorData(box).get)) + + //handle inventory contents + box.Capacity = (if(sumReloadValue <= fullMagazine) { + sumReloadValue + } + else { + val splitReloadAmmo : Int = sumReloadValue - fullMagazine + log.info(s"ChangeAmmo: taking ${originalBoxCapacity - splitReloadAmmo} from a box of ${originalBoxCapacity} $requestedAmmoType") + val boxForInventory = AmmoBox(box.Definition, splitReloadAmmo) + obj.Inventory += x.start -> boxForInventory //block early; assumption warning: swappable ammo types have the same icon size + taskResolver ! stowFuncTask(x.start, boxForInventory) + fullMagazine + }) + sendResponse(InventoryStateMessage(box.GUID, tool.GUID, box.Capacity)) //should work for both players and vehicles + log.info(s"ChangeAmmo: loading ${box.Capacity} $requestedAmmoType into ${tool.GUID} @ $ammoSlotIndex") + if(previousBox.Capacity > 0) { + //divide capacity across other existing and not full boxes of that ammo type + var capacity = previousBox.Capacity + val iter = obj.Inventory.Items + .filter(entry => { + entry.obj match { + case (item : AmmoBox) => + item.AmmoType == originalAmmoType && item.FullCapacity != item.Capacity + case _ => + false + } + }) + .toList + .sortBy(_.start) + .iterator + while(capacity > 0 && iter.hasNext) { + val entry = iter.next + val item : AmmoBox = entry.obj.asInstanceOf[AmmoBox] + val ammoAllocated = math.min(item.FullCapacity - item.Capacity, capacity) + log.info(s"ChangeAmmo: putting $ammoAllocated back into a box of ${item.Capacity} $originalAmmoType") + capacity -= ammoAllocated + modifyFunc(item, -ammoAllocated) + } + previousBox.Capacity = capacity + } + + if(previousBox.Capacity > 0) { + //split previousBox into AmmoBox objects of appropriate max capacity, e.g., 100 9mm -> 2 x 50 9mm + obj.Inventory.Fit(previousBox) match { + case Some(index) => + stowFunc(index, previousBox) + case None => + NormalItemDrop(player, continent, avatarService)(previousBox) + } + val dropFunc : (Equipment)=>TaskResolver.GiveTask = NewItemDrop(player, continent, avatarService) + AmmoBox.Split(previousBox) match { + case Nil | _ :: Nil => ; //done (the former case is technically not possible) + case _ :: xs => + modifyFunc(previousBox, 0) //update to changed capacity value + xs.foreach(box => { + obj.Inventory.Fit(box) match { + case Some(index) => + obj.Inventory += index -> box //block early, for purposes of Fit + taskResolver ! stowFuncTask(index, box) + case None => + taskResolver ! dropFunc(box) + } + }) + } + } + else { + taskResolver ! GUIDTask.UnregisterObjectTask(previousBox)(continent.GUID) + } + } + } + } + while(tool.AmmoType != originalAmmoType && tool.AmmoType != tool.AmmoSlot.Box.AmmoType) + } + /** * Drop an `Equipment` item onto the ground. * Specifically, instruct the item where it will appear, @@ -4864,7 +5441,7 @@ class WorldSessionActor extends Actor with MDCContextAware { * @param item the item */ def NormalItemDrop(obj : PlanetSideGameObject with Container, zone : Zone, service : ActorRef)(item : Equipment) : Unit = { - continent.Ground ! Zone.Ground.DropItem(item, obj.Position, Vector3(0f, 0f, obj.Orientation.z)) + continent.Ground ! Zone.Ground.DropItem(item, obj.Position, Vector3.z(obj.Orientation.z)) } /** @@ -4879,8 +5456,8 @@ class WorldSessionActor extends Actor with MDCContextAware { * curried for callback * @param item the item */ - def NewItemDrop(obj : PlanetSideGameObject with Container, zone : Zone, service : ActorRef)(item : Equipment) : Unit = { - taskResolver ! TaskResolver.GiveTask( + def NewItemDrop(obj : PlanetSideGameObject with Container, zone : Zone, service : ActorRef)(item : Equipment) : TaskResolver.GiveTask = { + TaskResolver.GiveTask( new Task() { private val localItem = item private val localFunc : (Equipment)=>Unit = NormalItemDrop(obj, zone, service) @@ -4934,13 +5511,19 @@ class WorldSessionActor extends Actor with MDCContextAware { /** * A predicate used to determine if an `InventoryItem` object contains `Equipment` that should be dropped. * Used to filter through lists of object data before it is placed into a player's inventory. + * Drop the item if:
+ * - the item is cavern equipment
+ * - the item is a `BoomerTrigger` type object
+ * - the item is another faction's exclusive equipment * @param tplayer the player * @return true if the item is to be dropped; false, otherwise */ - def DropPredicate(tplayer : Player) : (InventoryItem => Boolean) = entry => { //drop if Cavern equipment, or is another faction's exclusive equipment + def DropPredicate(tplayer : Player) : (InventoryItem => Boolean) = entry => { val objDef = entry.obj.Definition val faction = GlobalDefinitions.isFactionEquipment(objDef) - GlobalDefinitions.isCavernEquipment(objDef) || (faction != tplayer.Faction && faction != PlanetSideEmpire.NEUTRAL) + GlobalDefinitions.isCavernEquipment(objDef) || + entry.obj.isInstanceOf[BoomerTrigger] || + (faction != tplayer.Faction && faction != PlanetSideEmpire.NEUTRAL) } /** @@ -5131,8 +5714,8 @@ class WorldSessionActor extends Actor with MDCContextAware { sendResponse(PlanetsideAttributeMessage(amenityId, 47, if(silo.LowNtuWarningOn) 1 else 0)) if(silo.ChargeLevel == 0) { - //todo: temporarily disabled until warpgates can bring ANTs from sanctuary, otherwise we'd be stuck in a situation with an unpowered base and no way to get an ANT to refill it. - //sendResponse(PlanetsideAttributeMessage(PlanetSideGUID(silo.Owner.asInstanceOf[Building].ModelId), 48, 1)) + // temporarily disabled until warpgates can bring ANTs from sanctuary, otherwise we'd be stuck in a situation with an unpowered base and no way to get an ANT to refill it. + // sendResponse(PlanetsideAttributeMessage(PlanetSideGUID(silo.Owner.asInstanceOf[Building].ModelId), 48, 1)) } case _ => ; } @@ -5340,6 +5923,18 @@ class WorldSessionActor extends Actor with MDCContextAware { } case _ => ; } + //disown boomers and drop triggers + val boomers = avatar.Deployables.ClearDeployable(DeployedItem.boomer) + boomers.foreach(boomer => { + continent.GUID(boomer) match { + case Some(obj : BoomerDeployable) => + obj.OwnerName = None + localService ! LocalServiceMessage.Deployables(RemoverActor.AddTask(obj, continent)) + case Some(_) | None => ; + } + }) + val triggers = RemoveBoomerTriggersFromInventory() + triggers.foreach(trigger =>{ NormalItemDrop(obj, continent, avatarService)(trigger) }) } } @@ -5774,6 +6369,9 @@ class WorldSessionActor extends Actor with MDCContextAware { case obj : Vehicle => //damage is synchronized on the vehicle actor (results returned to and distributed from this `WSA`) obj.Actor ! Vitality.Damage(func) + case obj : Deployable => + //damage is synchronized on `LSA` (results returned to and distributed from this `WSA`) + localService ! Vitality.DamageOn(obj, func) case _ => ; } } @@ -5814,6 +6412,722 @@ class WorldSessionActor extends Actor with MDCContextAware { ) } + /** + * Initialize the deployables backend information. + * @param avatar the player's core + */ + def InitializeDeployableQuantities(avatar : Avatar) : Unit = { + log.info("Setting up combat engineering ...") + avatar.Deployables.Initialize(avatar.Certifications.toSet) + } + + /** + * Initialize the UI elements for deployables. + * @param avatar the player's core + */ + def InitializeDeployableUIElements(avatar : Avatar) : Unit = { + log.info("Setting up combat engineering UI ...") + UpdateDeployableUIElements(avatar.Deployables.UpdateUI()) + } + + /** + * The player learned a new certification. + * Update the deployables user interface elements if it was an "Engineering" certification. + * The certification "Advanced Hacking" also relates to an element. + * @param certification the certification that was added + * @param certificationSet all applicable certifications + */ + def AddToDeployableQuantities(certification : CertificationType.Value, certificationSet : Set[CertificationType.Value]) : Unit = { + avatar.Deployables.AddToDeployableQuantities(certification, certificationSet) + UpdateDeployableUIElements(avatar.Deployables.UpdateUI(certification)) + } + + /** + * The player forgot a certification he previously knew. + * Update the deployables user interface elements if it was an "Engineering" certification. + * The certification "Advanced Hacking" also relates to an element. + * @param certification the certification that was added + * @param certificationSet all applicable certifications + */ + def RemoveFromDeployablesQuantities(certification : CertificationType.Value, certificationSet : Set[CertificationType.Value]) : Unit = { + avatar.Deployables.RemoveFromDeployableQuantities(certification, certificationSet) + UpdateDeployableUIElements(avatar.Deployables.UpdateUI(certification)) + } + + /** + * Initialize the deployables user interface elements.
+ *
+ * All element initializations require both the maximum deployable amount and the current deployables active counts. + * Until initialized, all elements will be RED 0/0 as if the cooresponding certification were not `learn`ed. + * The respective element will become a pair of numbers, the second always being non-zero, when properly initialized. + * The numbers will appear GREEN when more deployables of that type can be placed. + * The numbers will appear RED if the player can not place any more of that type of deployable. + * The numbers will appear YELLOW if the current deployable count is greater than the maximum count of that type + * such as may be the case when a player `forget`s a certification. + * @param list a tuple of each UI element with four numbers; + * even numbers are attribute ids; + * odd numbers are quantities; + * first pair is current quantity; + * second pair is maximum quantity + */ + def UpdateDeployableUIElements(list : List[(Int,Int,Int,Int)]) : Unit = { + val guid = PlanetSideGUID(0) + list.foreach({ case((currElem, curr, maxElem, max)) => + //fields must update in ordered pairs: max, curr + sendResponse(PlanetsideAttributeMessage(guid, maxElem, max)) + sendResponse(PlanetsideAttributeMessage(guid, currElem, curr)) + }) + } + + /** + * Draw the icon for this deployable object.
+ *
+ * When a client first joins a zone, all deployables are drawn on the continent map once. + * Should the player place any deployables, those deployables belong to that player. + * Ownership causes icon to be drawn in yellow to the player (as opposed to a white icon) + * and that signifies a certain level of control over the deployable, at least the ability to quietly deconstruct it. + * Under normal death/respawn cycles while the player is in a given zone, + * the map icons for owned deployables ramin manipulable to that given user. + * They do not havwe to be redrawn to stay accurate. + * Upon leaving a zone, where the icons are erased, and returning back to the zone, where they are drawn again, + * the deployables that a player owned should be restored in terms of their map icon visibility. + * TThis control can not be recovered, however, until they are updated with the player's globally unique identifier. + * Since the player does not need to redraw his own deployable icons each time he respawns, + * but will not possess a valid GUID for that zone until he spawns in it at least once, + * this function is swapped with another after the first spawn in any given zone. + * This function is restored upon transferring zones. + * @see `SetCurrentAvatar`
+ * `DontRedrawIcons` + * @param obj a `Deployable` object + */ + def RedrawDeployableIcons(obj : PlanetSideGameObject with Deployable) : Unit = { + val deployInfo = DeployableInfo(obj.GUID, Deployable.Icon(obj.Definition.Item), obj.Position, obj.Owner.get) + sendResponse(DeployableObjectsInfoMessage(DeploymentAction.Build, deployInfo)) + } + + /** + * Do not draw any icon for this deployable object.
+ *
+ * When a client first joins a zone, all deployables are drawn on the continent map once. + * Should the player place any deployables, those deployables belong to that player. + * Ownership causes icon to be drawn in yellow to the player (as opposed to a white icon) + * and that signifies a certain level of control over the deployable, at least the ability to quietly deconstruct it. + * Under normal death/respawn cycles while the player is in a given zone, + * the map icons for owned deployables ramin manipulable to that given user. + * They do not havwe to be redrawn to stay accurate. + * Upon leaving a zone, where the icons are erased, and returning back to the zone, where they are drawn again, + * the deployables that a player owned should be restored in terms of their map icon visibility. + * TThis control can not be recovered, however, until they are updated with the player's globally unique identifier. + * Since the player does not need to redraw his own deployable icons each time he respawns, + * but will not possess a valid GUID for that zone until he spawns in it at least once, + * this function swaps out with another after the first spawn in any given zone. + * It stays swapped in until the player changes zones. + * @see `SetCurrentAvatar`
+ * `RedrawDeployableIcons` + * @param obj a `Deployable` object + */ + def DontRedrawIcons(obj : PlanetSideGameObject with Deployable) : Unit = { } + + /** + * The custom behavior responding to the message `ChangeFireModeMessage` for `ConstructionItem` game objects. + * Each fire mode has sub-modes corresponding to a type of "deployable" as ammunition + * and each of these sub-modes have certification requirements that must be met before they can be used. + * Additional effort is exerted to ensure that the requirements for the given mode and given sub-mode are satisfied. + * If no satisfactory combination is achieved, the original state will be restored. + * @see `PerformConstructionItemAmmoChange`
+ * `FireModeSwitch.NextFireMode` + * @param obj the `ConstructionItem` object + * @param originalModeIndex the starting point fire mode index + * @return the changed fire mode + */ + def NextConstructionItemFireMode(obj : ConstructionItem, originalModeIndex : Int) : ConstructionFireMode = { + val certifications = player.Certifications + do { + obj.NextFireMode + if(!ConstructionItemPermissionComparison(certifications, obj.ModePermissions)) { + PerformConstructionItemAmmoChange(obj, obj.AmmoTypeIndex) + } + sendResponse(ChangeFireModeMessage(obj.GUID, obj.FireModeIndex)) + } + while(!ConstructionItemPermissionComparison(certifications, obj.ModePermissions) && originalModeIndex != obj.FireModeIndex) + obj.FireMode + } + + /** + * The custom behavior responding to the message `ChangeAmmoMessage` for `ConstructionItem` game objects. + * Iterate through sub-modes corresponding to a type of "deployable" as ammunition for this fire mode + * and check each of these sub-modes for their certification requirements to be met before they can be used. + * Additional effort is exerted to ensure that the requirements for the given ammunition are satisfied. + * If no satisfactory combination is achieved, the original state will be restored. + * @param obj the `ConstructionItem` object + * @param originalModeIndex the starting point ammunition type mode index + */ + def PerformConstructionItemAmmoChange(obj : ConstructionItem, originalAmmoIndex : Int) : Unit = { + val certifications = player.Certifications + do { + obj.NextAmmoType + } + while(!ConstructionItemPermissionComparison(certifications, obj.ModePermissions) && originalAmmoIndex != obj.AmmoTypeIndex) + log.info(s"ChangeFireMode: construction object ${obj.Definition.Name} changed to ${obj.AmmoType} (mode ${obj.FireModeIndex})") + sendResponse(ChangeAmmoMessage(obj.GUID, obj.AmmoTypeIndex)) + } + + /** + * Compare sets of certifications to determine if + * the requested `Engineering`-like certification requirements of the one group can be found in a another group. + * @see `CertificationType` + * @param sample the certifications to be compared against + * @param test the desired certifications + * @return `true`, if the desired certification requirements are met; `false`, otherwise + */ + def ConstructionItemPermissionComparison(sample : Set[CertificationType.Value], test : Set[CertificationType.Value]) : Boolean = { + import CertificationType._ + val engineeringCerts : Set[CertificationType.Value] = Set(AssaultEngineering, FortificationEngineering) + val testDiff : Set[CertificationType.Value] = test diff (engineeringCerts ++ Set(AdvancedEngineering)) + //substitute `AssaultEngineering` and `FortificationEngineering` for `AdvancedEngineering` + val sampleIntersect = if(sample contains AdvancedEngineering) { + engineeringCerts + } + else { + sample intersect engineeringCerts + } + val testIntersect = if(test contains AdvancedEngineering) { + engineeringCerts + } + else { + test intersect engineeringCerts + } + (sample intersect testDiff equals testDiff) && (sampleIntersect intersect testIntersect equals testIntersect) + } + + /** + * Common actions related to constructing a new `Deployable` object in the game environment.
+ *
+ * Besides the standard `ObjectCreateMessage` packet that produces the model and game object on the client, + * two messages are dispatched in accordance with enforced deployable limits. + * The first limit of note is the actual number of a specific type of deployable can be placed. + * The second limit of note is the actual number of a specific group (category) of deployables that can be placed. + * For example, the player can place 25 mines but that count adds up all types of mines; + * specific mines have individual limits such as 25 and 5 and only that many of that type can be placed at once. + * Depending on which limit is encountered, an "oldest entry" is struck from the list to make space. + * This generates the first message - "@*OldestDestroyed." + * The other message is generated if the number of that specific type of deployable + * or the number of deployables available in its category + * matches against the maximum count allowed. + * This generates the second message - "@*LimitReached." + * These messages are mutually exclusive, with "@*OldestDestroyed" taking priority over "@*LimitReached."
+ *
+ * The map icon for the deployable just introduced is also created on the clients of all faction-affiliated players. + * This icon is important as, short of destroying it, + * the owner has no other means of controlling the created object that it is associated with. + * @param obj the `Deployable` object to be built + */ + def DeployableBuildActivity(obj : PlanetSideGameObject with Deployable) : Unit = { + val guid = obj.GUID + val definition = obj.Definition + val item = definition.Item + val deployables = avatar.Deployables + val (curr, max) = deployables.CountDeployable(item) + log.info(s"FinalizeDeployable: ${definition.Name}") + //two potential messages related to numerical limitations of deployables + if(!avatar.Deployables.Available(obj)) { + val (removed, msg) = { + if(curr == max) { //too many of a specific type of deployable + (deployables.DisplaceFirst(obj), max > 1) + } + else { //make room by eliminating a different type of deployable + (deployables.DisplaceFirst(obj, { d => d.Definition.Item != item }), true) + } + } + removed match { + case Some(old) => + old.Position = Vector3.Zero + old.Owner = None + localService ! LocalServiceMessage.Deployables(RemoverActor.AddTask(old, continent, Some(0 seconds))) + if(msg) { //max test + sendResponse(ChatMsg(ChatMessageType.UNK_229, false, "", s"@${definition.Descriptor}OldestDestroyed", None)) + } + case None => ; //should be an invalid case + log.warn(s"DeployableBuildActivity: how awkward: we probably shouldn't be allowed to build this deployable right now") + } + } + else { + sendResponse(ObjectDeployedMessage.Success(definition.Name, curr + 1, max)) + val (catCurr, catMax) = deployables.CountCategory(item) + if((max > 1 && curr + 1 == max) || (catMax > 1 && catCurr + 1 == catMax)) { + sendResponse(ChatMsg(ChatMessageType.UNK_229, false, "", s"@${definition.Descriptor}LimitReached", None)) + } + } + avatar.Deployables.Add(obj) + UpdateDeployableUIElements(avatar.Deployables.UpdateUIElement(item)) + sendResponse(GenericObjectActionMessage(guid, 84)) //reset build cooldown + sendResponse(ObjectCreateMessage(definition.ObjectId, guid, definition.Packet.ConstructorData(obj).get)) + avatarService ! AvatarServiceMessage(continent.Id, AvatarAction.DeployItem(player.GUID, obj)) + //map icon + val deployInfo = DeployableInfo(guid, Deployable.Icon(item), obj.Position, obj.Owner.getOrElse(PlanetSideGUID(0))) + sendResponse(DeployableObjectsInfoMessage(DeploymentAction.Build, deployInfo)) + localService ! LocalServiceMessage(s"${continent.Id}/${player.Faction}", LocalAction.DeployableMapIcon(player.GUID, DeploymentAction.Build, deployInfo)) + } + + /** + * If the tool is a form of field deployment unit (FDU, also called an `advanced_ace`), + * completely remove the object from its current position and place it on the ground. + * In the case of a botched deployable construction, dropping the FDU is visually consistent + * as it should already be depicted as on the ground as a part of its animation cycle. + * @param tool the `ConstructionItem` object currently in the slot (checked) + * @param index the slot index + * @param pos where to drop the object in the game world + */ + def TryDropConstructionTool(tool : ConstructionItem, index : Int, pos : Vector3) : Unit = { + if(tool.Definition == GlobalDefinitions.advanced_ace && + SafelyRemoveConstructionItemFromSlot(tool, index, "TryDropConstructionTool")) { + continent.Ground ! Zone.Ground.DropItem(tool, pos, Vector3.Zero) + } + } + + /** + * Destroy a `ConstructionItem` object that can be found in the indexed slot. + * @see `Player.Find` + * @param tool the `ConstructionItem` object currently in the slot (checked) + * @param index the slot index + */ + def CommonDestroyConstructionItem(tool : ConstructionItem, index : Int) : Unit = { + if(SafelyRemoveConstructionItemFromSlot(tool, index, "CommonDestroyConstructionItem")) { + taskResolver ! GUIDTask.UnregisterEquipment(tool)(continent.GUID) + } + } + + /** + * Find the target `ConstructionTool` object, either at the suggested slot or wherever it is on the `player`, + * and remove it from the game world visually.
+ *
+ * Not finding the target object at its intended slot is an entirely recoverable situation + * as long as the target object is discovered to be somewhere else in the player's holsters or inventory space. + * If found after a more thorough search, merely log the discrepancy as a warning. + * If the discrepancy becomes common, the developer messed up the function call + * or he should not be using this function. + * @param tool the `ConstructionItem` object currently in the slot (checked) + * @param index the slot index + * @param logDecorator what kind of designation to give any log entires originating from this function; + * defaults to its own function name + * @return `true`, if the target object was found and removed; + * `false`, otherwise + */ + def SafelyRemoveConstructionItemFromSlot(tool : ConstructionItem, index : Int, logDecorator : String = "SafelyRemoveConstructionItemFromSlot") : Boolean = { + if({ + val holster = player.Slot(index) + if(holster.Equipment.contains(tool)) { + holster.Equipment = None + true + } + else { + player.Find(tool) match { + case Some(newIndex) => + log.warn(s"$logDecorator: looking for item in $index, but item was found at $newIndex instead") + player.Slot(newIndex).Equipment = None + true + case None => + log.error(s"$logDecorator: could not find the target ${tool.Definition.Name}") + false + } + } + }) { + sendResponse(ObjectDeleteMessage(tool.GUID, 0)) + avatarService ! AvatarServiceMessage(continent.Id, AvatarAction.ObjectDelete(player.GUID, tool.GUID)) + true + } + else { + false + } + } + + /** + * Find a `ConstructionItem` object in player's inventory + * that is the same type as a target `ConstructionItem` object and + * transfer it into the designated slot index, usually a holster. + * Draw that holster. + * After being transferred, the replacement should be reconfigured to match the fire mode of the original. + * The primary use of this operation is following the successful manifestation of a deployable in the game world.
+ *
+ * As this function should be used in response to some other action such as actually placing a deployable, + * do not instigate bundling from within the function's scope. + * @see `WorldSessionActor.FinalizeDeployable`
+ * `FindEquipmentStock` + * @param tool the `ConstructionItem` object to match + * @param index where to put the discovered replacement + */ + def FindReplacementConstructionItem(tool : ConstructionItem, index : Int) : Unit = { + val fireMode = tool.FireModeIndex + val ammoType = tool.AmmoTypeIndex + val definition = tool.Definition + + if(player.Slot(index).Equipment.isEmpty) { + FindEquipmentStock(player, { (e) => e.Definition == definition }, 1) match { + case x :: _ => + val guid = player.GUID + val obj = x.obj.asInstanceOf[ConstructionItem] + if((player.Slot(index).Equipment = obj).contains(obj)) { + player.Inventory -= x.start + sendResponse(ObjectAttachMessage(guid, obj.GUID, index)) + + if(obj.FireModeIndex != fireMode) { + obj.FireModeIndex = fireMode + sendResponse(ChangeFireModeMessage(obj.GUID, fireMode)) + } + if(obj.AmmoTypeIndex != ammoType) { + obj.AmmoTypeIndex = ammoType + sendResponse(ChangeAmmoMessage(obj.GUID, ammoType)) + } + if(player.VisibleSlots.contains(index)) { + avatarService ! AvatarServiceMessage(continent.Id, AvatarAction.EquipmentInHand(guid, guid, index, obj)) + if(player.DrawnSlot == Player.HandsDownSlot) { + player.DrawnSlot = index + sendResponse(ObjectHeldMessage(guid, index, false)) + avatarService ! AvatarServiceMessage(continent.Id, AvatarAction.ObjectHeld(guid, index)) + } + } + } + case Nil => ; //no replacements found + } + } + else { + log.warn(s"FindReplacementConstructionItem: slot $index needs to be empty before a replacement ${definition.Name} can be installed") + } + } + + /** + * A simple object searching algorithm that is limited to containers currently known and accessible by the player. + * If all relatively local containers are checked and the object is not found, + * the game environment (items on the ground) will be checked too. + * If the target object is discovered, it is removed from its current location and is completely destroyed. + * @see `RequestDestroyMessage`
+ * `Zone.ItemIs.Where` + * @param object_guid the target object's globally unique identifier; + * it is not expected that the object will be unregistered, but it is also not gauranteed + * @param obj the target object + * @return `true`, if the target object was discovered and removed; + * `false`, otherwise + */ + def FindEquipmentToDelete(object_guid : PlanetSideGUID, obj : Equipment) : Boolean = { + val findFunc : PlanetSideGameObject with Container => Option[(PlanetSideGameObject with Container, Option[Int])] = + FindInLocalContainer(object_guid) + + findFunc(player.Locker) + .orElse(findFunc(player)) + .orElse(accessedContainer match { + case Some(parent) => + findFunc(parent) + case None => + None + }) + .orElse(FindLocalVehicle match { + case Some(parent) => + findFunc(parent) + case None => + None + }) + match { + case Some((parent, Some(slot))) => + obj.Position = Vector3.Zero + taskResolver ! RemoveEquipmentFromSlot(parent, obj, slot) + log.info(s"RequestDestroy: equipment $obj") + true + + case _ => + if(continent.EquipmentOnGround.contains(obj)) { + obj.Position = Vector3.Zero + continent.Ground ! Zone.Ground.RemoveItem(object_guid) + avatarService ! AvatarServiceMessage.Ground(RemoverActor.ClearSpecific(List(obj), continent)) + avatarService ! AvatarServiceMessage(continent.Id, AvatarAction.ObjectDelete(PlanetSideGUID(0), object_guid)) + log.info(s"RequestDestroy: equipment $obj on ground") + true + } + else { + log.warn(s"RequestDestroy: equipment $obj exists, but can not be reached") + false + } + } + } + + /** + * Common behavior for deconstructing expended explosive deployables in the game environment. + * @param obj the deployable + * @param guid the globally unique identifier for the deployable + * @param pos the previous position of the deployable + */ + def DeconstructDeployable(obj : PlanetSideGameObject with Deployable, guid : PlanetSideGUID, pos : Vector3) : Unit = { + StartBundlingPackets() + sendResponse(SetEmpireMessage(guid, PlanetSideEmpire.NEUTRAL)) //for some, removes the green marker circle + sendResponse(ObjectDeleteMessage(guid, 0)) + if(player.Faction == obj.Faction) { + sendResponse( + DeployableObjectsInfoMessage( + DeploymentAction.Dismiss, + DeployableInfo(guid, Deployable.Icon(obj.Definition.Item), pos, obj.Owner.getOrElse(PlanetSideGUID(0))) + ) + ) + } + StopBundlingPackets() + } + + /** + * Common behavior for deconstructing deployables in the game environment. + * @param obj the deployable + * @param guid the globally unique identifier for the deployable + * @param pos the previous position of the deployable + * @param orient the previous orientation of the deployable + * @param deletionType the value passed to `ObjectDeleteMessage` concerning the deconstruction animation + */ + def DeconstructDeployable(obj : PlanetSideGameObject with Deployable, guid : PlanetSideGUID, pos : Vector3, orient : Vector3, deletionType : Int) : Unit = { + StartBundlingPackets() + sendResponse(SetEmpireMessage(guid, PlanetSideEmpire.NEUTRAL)) //for some, removes the green marker circle + sendResponse(TriggerEffectMessage("spawn_object_failed_effect", pos, orient)) + sendResponse(PlanetsideAttributeMessage(guid, 29, 1)) //make deployable vanish + sendResponse(ObjectDeleteMessage(guid, deletionType)) + if(player.Faction == obj.Faction) { + sendResponse( + DeployableObjectsInfoMessage( + DeploymentAction.Dismiss, + DeployableInfo(guid, Deployable.Icon(obj.Definition.Item), pos, obj.Owner.getOrElse(PlanetSideGUID(0))) + ) + ) + } + StopBundlingPackets() + } + + /** + * Distribute information that a deployable has been destroyed. + * The deployable may not have yet been eliminated from the game world (client or server), + * but its health is zero and it has entered the conditions where it is nearly irrelevant.
+ *
+ * The typical use case of this function involves destruction via weapon fire, attributed to a particular player. + * Contrast this to simply destroying a deployable by being the deployable's owner and using the map icon controls. + * This function eventually invokes the same routine + * but mainly goes into effect when the deployable has been destroyed + * and may still leave a physical component in the game world to be cleaned up later. + * That is the task `EliminateDeployable` performs. + * Additionally, since the player who destroyed the deployable isn't necessarily the owner, + * and the real owner will still be aware of the existence of the deployable, + * that player must be informed of the loss of the deployable directly. + * @see `DeployableRemover` + * @see `Vitality.DamageResolution` + * @see `LocalResponse.EliminateDeployable` + * @see `DeconstructDeployable` + * @param target the deployable that is destroyed + * @param time length of time that the deployable is allowed to exist in the game world; + * `None` indicates the normal un-owned existence time (180 seconds) + */ + def AnnounceDestroyDeployable(target : PlanetSideGameObject with Deployable, time : Option[FiniteDuration]) : Unit = { + target.OwnerName match { + case Some(owner) => + target.OwnerName = None + localService ! LocalServiceMessage(owner, LocalAction.AlertDestroyDeployable(PlanetSideGUID(0), target)) + case None => ; + } + localService ! LocalServiceMessage(s"${continent.Id}/${target.Faction}", LocalAction.DeployableMapIcon( + PlanetSideGUID(0), + DeploymentAction.Dismiss, + DeployableInfo(target.GUID, Deployable.Icon(target.Definition.Item), target.Position, PlanetSideGUID(0))) + ) + localService ! LocalServiceMessage.Deployables(RemoverActor.ClearSpecific(List(target), continent)) + localService ! LocalServiceMessage.Deployables(RemoverActor.AddTask(target, continent, time)) + } + + /** + * Search through the player's holsters and their inventory space + * and remove all `BoomerTrigger` objects, both functionally and visually. + * @return all discovered `BoomTrigger` objects + */ + def RemoveBoomerTriggersFromInventory() : List[BoomerTrigger] = { + val player_guid = player.GUID + ((player.Inventory.Items.collect({ case entry @ InventoryItem(obj : BoomerTrigger, index) => (obj, index) })) ++ + (player.Holsters() + .zipWithIndex + .map({ case ((slot, index)) => (slot.Equipment, index) }) + .collect { case ((Some(obj : BoomerTrigger), index)) => (obj, index) } + ) + ) + .map({ case ((obj, index)) => + player.Slot(index).Equipment = None + sendResponse(ObjectDeleteMessage(obj.GUID, 0)) + if(player.VisibleSlots.contains(index)) { + avatarService ! AvatarServiceMessage(continent.Id, AvatarAction.ObjectDelete(player_guid, obj.GUID)) + } + obj + }) + } + + /** + * Collect all deployables previously owned by the player, + * dissociate the avatar's globally unique identifier to remove turnover ownership, + * and, on top of performing the above manipulations, dispose of any boomers discovered. + * (`BoomerTrigger` objects, the companions of the boomers, should be handled by an external implementation + * if they had not already been handled by the time this function is executed.) + * @return all previously-owned deployables after they have been processed; + * boomers are listed before all other deployable types + */ + def DisownDeployables() : List[PlanetSideGameObject with Deployable] = { + val (boomers, deployables) = + avatar.Deployables.Clear() + .map(continent.GUID(_)) + .collect { case Some(obj) => obj.asInstanceOf[PlanetSideGameObject with Deployable] } + .partition(_.isInstanceOf[BoomerDeployable]) + //do not change the OwnerName field at this time + boomers.collect({ case obj : BoomerDeployable => + localService ! LocalServiceMessage.Deployables(RemoverActor.AddTask(obj, continent, Some(0 seconds))) //near-instant + obj.Owner = None + obj.Trigger = None + }) + deployables.foreach(obj => { + localService ! LocalServiceMessage.Deployables(RemoverActor.AddTask(obj, continent)) //normal decay + obj.Owner = None + }) + boomers ++ deployables + } + + /** + * Common behavior for a player who: + * is dead and is respawning; + * is deconstructing at a spawn tube and is respawning; or, + * either of the previous conditions, but the final result involves changing what zone the player occupies. + * This route is not taken when first spawning, unless special conditions need to be satisfied.
+ *
+ * Two choices must be independently made to complete this part of the process. + * The first choice ivolves the state of the player who is spawning + * as the known entry state involve either being alive or being dead. + * A dead player (technically, a "corpse" that can no longer be revived) is embodied + * in a completely new player with a new globally unique identifier and a whole new inventory. + * A player who is transferring continents also satisfies the requirements + * for obtaining a completely new globally unique identifier, + * though the new identifier belongs to the new zone rather than the previous (still current) one. + * The second choice is satisfied by respawning in the same zone while still in a state of still being alive. + * In this singulkar case, the player retains his previous globally unique identifier. + * In all other cases, as indicated, a new globally unique identifier is selected. + * @see `AvatarDeadStateMessage`
+ * `RespawnClone` + * @param zone_id the zone in which the player will be placed + * @param pos the game world coordinates where the player will be positioned + * @param ori the direction in which the player will be oriented + * @param respawnTime the character downtime spent respawning, as clocked on the redeployment screen; + * does not factor in any time required for loading zone or game objects + */ + def LoadZonePhysicalSpawnPoint(zone_id : String, pos : Vector3, ori : Vector3, respawnTime : Long) : Unit = { + respawnTimer.cancel + reviveTimer.cancel + ClearCurrentAmsSpawnPoint() + val backpack = player.isBackpack + val respawnTimeMillis = respawnTime * 1000 //ms + deadState = DeadState.RespawnTime + sendResponse(AvatarDeadStateMessage(DeadState.RespawnTime, respawnTimeMillis, respawnTimeMillis, Vector3.Zero, player.Faction, true)) + val tplayer = if(backpack) { + RespawnClone(player) //new player + } + else { + val player_guid = player.GUID + sendResponse(ObjectDeleteMessage(player_guid, 4)) + avatarService ! AvatarServiceMessage(continent.Id, AvatarAction.ObjectDelete(player_guid, player_guid, 4)) + player //player is deconstructing self + } + + tplayer.Position = pos + tplayer.Orientation = ori + val (target, msg) : (ActorRef, Any) = if(zone_id == continent.Id) { + if(backpack) { + //respawning from unregistered player + (taskResolver, RegisterAvatar(tplayer)) + } + else { + //move existing player; this is the one case where the original GUID is retained by the player + (self, PlayerLoaded(tplayer)) + } + } + else { + val original = player + //release constraints from former zone; vehicle ownership, and deployable ownership, etc.. + DisownVehicle() + RemoveBoomerTriggersFromInventory().foreach(obj => { + taskResolver ! GUIDTask.UnregisterObjectTask(obj)(continent.GUID) + }) + DisownDeployables() + drawDeloyableIcon = RedrawDeployableIcons //important for when SetCurrentAvatar initializes the UI next zone + continent.Population ! Zone.Population.Leave(avatar) + //TODO check player orientation upon spawn not polluted + if(backpack) { + //unregister avatar locker + GiveWorld + player = tplayer + (taskResolver, TaskBeforeZoneChange(GUIDTask.UnregisterLocker(original.Locker)(continent.GUID), zone_id)) + } + else { + //unregister avatar whole + GiveWorld + (taskResolver, TaskBeforeZoneChange(GUIDTask.UnregisterAvatar(original)(continent.GUID), zone_id)) + } + } + import scala.concurrent.ExecutionContext.Implicits.global + respawnTimer = context.system.scheduler.scheduleOnce(respawnTime seconds, target, msg) + } + + /** + * Primary functionality for tranferring a piece of equipment from a player's hands or his inventory to the ground. + * Items are always dropped at player's feet because for simplicity's sake + * because, by virtue of already standing there, the stability of the surface has been proven. + * The only exception to this is dropping items while falling. + * @see `Player.Find`
+ * `ObjectDetachMessage` + * @param item the `Equipment` object in the player's hand + * @param pos the game world coordinates where the object will be dropped + * @param orient a suggested orientation in which the object will settle when on the ground; + * as indicated, the simulation is only concerned with certain angles + */ + def PutItemOnGround(item : Equipment, pos : Vector3, orient : Vector3) : Unit = { + //TODO delay or reverse dropping item when player is falling down + item.Position = pos + item.Orientation = Vector3.z(orient.z) + //dropped items rotate towards the user's standing direction + val exclusionId = player.Find(item) match { + //if the item is in our hands ... + case Some(slotNum) => + player.Slot(slotNum).Equipment = None + sendResponse(ObjectDetachMessage(player.GUID, item.GUID, pos, orient.z)) + sendResponse(ActionResultMessage.Pass) + player.GUID //we're dropping the item; don't need to see it dropped again + case None => + PlanetSideGUID(0) //item is being introduced into the world upon drop + } + avatarService ! AvatarServiceMessage(continent.Id, AvatarAction.DropItem(exclusionId, item, continent)) + } + + /** + * Primary functionality for tranferring a piece of equipment from the ground in a player's hands or his inventory. + * The final destination of the item in terms of slot position is not determined until the attempt is made. + * If it can not be placed in a slot correctly, the item will be returned to the ground in the same place. + * @see `Player.Fit` + * @param item the `Equipment` object on the ground + * @return `true`, if the object was properly picked up; + * `false` if it was returned to the ground + */ + def PutItemInHand(item : Equipment) : Boolean = { + player.Fit(item) match { + case Some(slotNum) => + val item_guid = item.GUID + val player_guid = player.GUID + player.Slot(slotNum).Equipment = item + val definition = item.Definition + sendResponse( + ObjectCreateDetailedMessage( + definition.ObjectId, + item_guid, + ObjectCreateMessageParent(player_guid, slotNum), + definition.Packet.DetailedConstructorData(item).get + ) + ) + avatarService ! AvatarServiceMessage(continent.Id, AvatarAction.PickupItem(player_guid, continent, player, slotNum, item)) + true + case None => + continent.Ground ! Zone.Ground.DropItem(item, item.Position, item.Orientation) //restore previous state + false + } + } + def failWithError(error : String) = { log.error(error) sendResponse(ConnectionClose()) @@ -6000,4 +7314,6 @@ object WorldSessionActor { private final case class NtuCharging(tplayer: Player, vehicle: Vehicle) private final case class NtuDischarging(tplayer: Player, vehicle: Vehicle, silo_guid: PlanetSideGUID) + + private final case class FinalizeDeployable(obj : PlanetSideGameObject with Deployable, tool : ConstructionItem, index : Int) } diff --git a/pslogin/src/main/scala/csr/CSRZoneImpl.scala b/pslogin/src/main/scala/csr/CSRZoneImpl.scala index ff5a2a70..595b7f63 100644 --- a/pslogin/src/main/scala/csr/CSRZoneImpl.scala +++ b/pslogin/src/main/scala/csr/CSRZoneImpl.scala @@ -316,7 +316,7 @@ object CSRZoneImpl { "enkidu" -> Vector3(3217, 3574, 37), "girru" -> Vector3(4475, 5853, 78), "hanish" -> Vector3(3794, 5540, 89), - "irkall" -> Vector3(4742, 5270, 66), + "irkalla" -> Vector3(4742, 5270, 66), "kusag" -> Vector3(6532, 4692, 46), "lahar" -> Vector3(6965, 5306, 38), "marduk" -> Vector3(3059, 2144, 70),