From c7ebe6a34f111c6048e08eb6c58740d5b5a6f1a6 Mon Sep 17 00:00:00 2001 From: Fate-JH Date: Mon, 21 Jun 2021 23:40:44 -0400 Subject: [PATCH] Separate Vehicle Controls (#871) * broke vehicle control agency down into specific agencies for different types of vehicles * moved shared cargo vehicle pain onto the carrier control agency; apc-type vehicles have charging capacitors and emit emp's * comments and documentation; cargo learning about damage to carrier corrected; fixed tests and added tests * adjustment to explosive deployable distance filtering; apc now uses this filter when determining valid emp targets by distance --- .../objects/AutoRepairIntegrationTest.scala | 7 +- .../actor/objects/VehicleSpawnPadTest.scala | 2 +- .../actors/session/SessionActor.scala | 25 +- .../objects/ExplosiveDeployable.scala | 2 +- .../psforever/objects/GlobalDefinitions.scala | 23 +- .../net/psforever/objects/SpecialEmp.scala | 5 + .../definition/VehicleDefinition.scala | 107 +++- .../damage/DamageableVehicle.scala | 58 +- .../serverobject/deploy/Deployment.scala | 4 + .../objects/vehicles/control/AmsControl.scala | 58 ++ .../objects/vehicles/control/AntControl.scala | 60 +++ .../objects/vehicles/control/ApcControl.scala | 116 ++++ .../control/CargoCarrierControl.scala | 88 +++ .../control/DeployingVehicleControl.scala | 97 ++++ .../vehicles/control/RouterControl.scala | 50 ++ .../{ => control}/VehicleControl.scala | 501 ++++++------------ .../objects/zones/ZoneVehicleActor.scala | 10 +- .../game/PlanetsideAttributeMessage.scala | 3 +- src/test/scala/objects/DamageableTest.scala | 112 ++-- src/test/scala/objects/RepairableTest.scala | 2 +- .../scala/objects/TelepadRouterTest.scala | 15 +- .../scala/objects/VehicleControlTest.scala | 242 ++++++--- src/test/scala/service/LocalServiceTest.scala | 2 +- .../scala/service/VehicleServiceTest.scala | 2 +- 24 files changed, 1061 insertions(+), 530 deletions(-) create mode 100644 src/main/scala/net/psforever/objects/vehicles/control/AmsControl.scala create mode 100644 src/main/scala/net/psforever/objects/vehicles/control/AntControl.scala create mode 100644 src/main/scala/net/psforever/objects/vehicles/control/ApcControl.scala create mode 100644 src/main/scala/net/psforever/objects/vehicles/control/CargoCarrierControl.scala create mode 100644 src/main/scala/net/psforever/objects/vehicles/control/DeployingVehicleControl.scala create mode 100644 src/main/scala/net/psforever/objects/vehicles/control/RouterControl.scala rename src/main/scala/net/psforever/objects/vehicles/{ => control}/VehicleControl.scala (61%) diff --git a/server/src/test/scala/actor/objects/AutoRepairIntegrationTest.scala b/server/src/test/scala/actor/objects/AutoRepairIntegrationTest.scala index cc3ea960f..1f646d955 100644 --- a/server/src/test/scala/actor/objects/AutoRepairIntegrationTest.scala +++ b/server/src/test/scala/actor/objects/AutoRepairIntegrationTest.scala @@ -14,7 +14,6 @@ import net.psforever.objects.serverobject.deploy.Deployment import net.psforever.objects.serverobject.resourcesilo.{ResourceSilo, ResourceSiloControl} import net.psforever.objects.serverobject.structures.{AutoRepairStats, Building, StructureType} import net.psforever.objects.serverobject.terminals.{OrderTerminalDefinition, Terminal, TerminalControl} -import net.psforever.objects.vehicles.VehicleControl import net.psforever.objects.vital.Vitality import net.psforever.objects.vital.base.DamageResolution import net.psforever.objects.vital.damage.DamageProfile @@ -201,7 +200,7 @@ class AutoRepairFacilityIntegrationAntGiveNtuTest extends FreedContextActorTest val maxNtuCap = ant.Definition.MaxNtuCapacitor player.Spawn() ant.NtuCapacitor = maxNtuCap - ant.Actor = context.actorOf(Props(classOf[VehicleControl], ant), name = "test-ant") + ant.Definition.Initialize(ant, context) ant.Zone = zone ant.Seats(0).mount(player) ant.DeploymentState = DriveState.Deployed @@ -295,7 +294,7 @@ class AutoRepairFacilityIntegrationTerminalDestroyedTerminalAntTest extends Free val maxNtuCap = ant.Definition.MaxNtuCapacitor player.Spawn() ant.NtuCapacitor = maxNtuCap - ant.Actor = context.actorOf(Props(classOf[VehicleControl], ant), name = "test-ant") + ant.Definition.Initialize(ant, context) ant.Zone = zone ant.Seats(0).mount(player) ant.DeploymentState = DriveState.Deployed @@ -397,7 +396,7 @@ class AutoRepairFacilityIntegrationTerminalIncompleteRepairTest extends FreedCon val maxNtuCap = ant.Definition.MaxNtuCapacitor player.Spawn() ant.NtuCapacitor = maxNtuCap - ant.Actor = context.actorOf(Props(classOf[VehicleControl], ant), name = "test-ant") + ant.Definition.Initialize(ant, context) ant.Zone = zone ant.Seats(0).mount(player) ant.DeploymentState = DriveState.Deployed diff --git a/server/src/test/scala/actor/objects/VehicleSpawnPadTest.scala b/server/src/test/scala/actor/objects/VehicleSpawnPadTest.scala index f2f00f6dd..886b01039 100644 --- a/server/src/test/scala/actor/objects/VehicleSpawnPadTest.scala +++ b/server/src/test/scala/actor/objects/VehicleSpawnPadTest.scala @@ -215,7 +215,7 @@ object VehicleSpawnPadControlTest { import net.psforever.objects.guid.NumberPoolHub import net.psforever.objects.guid.source.MaxNumberSource import net.psforever.objects.serverobject.structures.Building - import net.psforever.objects.vehicles.VehicleControl + import net.psforever.objects.vehicles.control.VehicleControl import net.psforever.objects.Tool import net.psforever.types.CharacterSex diff --git a/src/main/scala/net/psforever/actors/session/SessionActor.scala b/src/main/scala/net/psforever/actors/session/SessionActor.scala index 679a766b4..44b3ef51e 100644 --- a/src/main/scala/net/psforever/actors/session/SessionActor.scala +++ b/src/main/scala/net/psforever/actors/session/SessionActor.scala @@ -958,8 +958,8 @@ class SessionActor(middlewareActor: typed.ActorRef[MiddlewareActor.Command], con } case Deployment.CanNotChangeDeployment(obj, state, reason) => - if (Deployment.CheckForDeployState(state) && !VehicleControl.DeploymentAngleCheck(obj)) { - CanNotChangeDeployment(obj, state, "ground too steep") + if (Deployment.CheckForDeployState(state) && !Deployment.AngleCheck(obj)) { + CanNotChangeDeployment(obj, state, reason = "ground too steep") } else { CanNotChangeDeployment(obj, state, reason) } @@ -2407,13 +2407,12 @@ class SessionActor(middlewareActor: typed.ActorRef[MiddlewareActor.Command], con val obj_guid: PlanetSideGUID = obj.GUID CancelAllProximityUnits() sendResponse(PlanetsideAttributeMessage(obj_guid, 0, obj.Health)) - sendResponse(PlanetsideAttributeMessage(obj_guid, 68, obj.Shields)) //shield health + sendResponse(PlanetsideAttributeMessage(obj_guid, 68, obj.Shields)) if (obj.Definition == GlobalDefinitions.ant) { sendResponse(PlanetsideAttributeMessage(obj_guid, 45, obj.NtuCapacitorScaled)) } if (obj.Definition.MaxCapacitor > 0) { - val capacitor = scala.math.ceil((obj.Capacitor.toFloat / obj.Definition.MaxCapacitor.toFloat) * 10).toInt - sendResponse(PlanetsideAttributeMessage(obj_guid, 113, capacitor)) + sendResponse(PlanetsideAttributeMessage(obj_guid, 113, obj.Capacitor)) } if (seat_number == 0) { if (obj.Definition == GlobalDefinitions.quadstealth) { @@ -3150,14 +3149,16 @@ class SessionActor(middlewareActor: typed.ActorRef[MiddlewareActor.Command], con vehicle.Actor ! JammableUnit.ClearJammeredSound() } //positive shield strength - if (vehicle.Shields > 0) { + if (vehicle.Definition.MaxShields > 0) { sendResponse(PlanetsideAttributeMessage(vehicle.GUID, 68, vehicle.Shields)) } // ANT capacitor if (vehicle.Definition == GlobalDefinitions.ant) { - sendResponse( - PlanetsideAttributeMessage(vehicle.GUID, 45, vehicle.NtuCapacitorScaled) - ) // set ntu on vehicle UI + sendResponse(PlanetsideAttributeMessage(vehicle.GUID, 45, vehicle.NtuCapacitorScaled)) // set ntu on vehicle UI + } + // vehicle capacitor + if (vehicle.Definition.MaxCapacitor > 0) { + sendResponse(PlanetsideAttributeMessage(vehicle.GUID, 113, vehicle.Capacitor)) } LoadZoneTransferPassengerMessages( guid, @@ -4966,7 +4967,13 @@ class SessionActor(middlewareActor: typed.ActorRef[MiddlewareActor.Command], con } case msg @ GenericObjectActionMessage(object_guid, code) => + log.debug(s"$msg") ValidObject(object_guid) match { + case Some(vehicle: Vehicle) => + if (code == 55) { + //apc emp + vehicle.Actor ! SpecialEmp.Burst() + } case Some(tool: Tool) => if (tool.Definition == GlobalDefinitions.maelstrom && code == 35) { //maelstrom primary fire mode effect (no target) diff --git a/src/main/scala/net/psforever/objects/ExplosiveDeployable.scala b/src/main/scala/net/psforever/objects/ExplosiveDeployable.scala index 30bc205c2..dbad8716f 100644 --- a/src/main/scala/net/psforever/objects/ExplosiveDeployable.scala +++ b/src/main/scala/net/psforever/objects/ExplosiveDeployable.scala @@ -244,7 +244,7 @@ object ExplosiveDeployableControl { //val scalar = Vector3.ScalarProjection(dir, up) val point1 = g1.pointOnOutside(dir).asVector3 val point2 = g2.pointOnOutside(Vector3.neg(dir)).asVector3 - val scalar = Vector3.ScalarProjection(point2 - point1, up) + val scalar = Vector3.ScalarProjection(point2 - obj1.Position, up) (scalar >= 0 || Vector3.MagnitudeSquared(up * scalar) < 0.35f) && math.min( Vector3.DistanceSquared(g1.center.asVector3, g2.center.asVector3), diff --git a/src/main/scala/net/psforever/objects/GlobalDefinitions.scala b/src/main/scala/net/psforever/objects/GlobalDefinitions.scala index 7feb758fa..6d6bfa971 100644 --- a/src/main/scala/net/psforever/objects/GlobalDefinitions.scala +++ b/src/main/scala/net/psforever/objects/GlobalDefinitions.scala @@ -897,11 +897,11 @@ object GlobalDefinitions { val aurora = VehicleDefinition(ObjectClass.aurora) - val apc_tr = VehicleDefinition(ObjectClass.apc_tr) + val apc_tr = VehicleDefinition.Apc(ObjectClass.apc_tr) - val apc_nc = VehicleDefinition(ObjectClass.apc_nc) + val apc_nc = VehicleDefinition.Apc(ObjectClass.apc_nc) - val apc_vs = VehicleDefinition(ObjectClass.apc_vs) + val apc_vs = VehicleDefinition.Apc(ObjectClass.apc_vs) val lightning = VehicleDefinition(ObjectClass.lightning) @@ -911,15 +911,15 @@ object GlobalDefinitions { val magrider = VehicleDefinition(ObjectClass.magrider) - val ant = VehicleDefinition(ObjectClass.ant) + val ant = VehicleDefinition.Ant(ObjectClass.ant) - val ams = VehicleDefinition(ObjectClass.ams) + val ams = VehicleDefinition.Ams(ObjectClass.ams) - val router = VehicleDefinition(ObjectClass.router) + val router = VehicleDefinition.Router(ObjectClass.router) - val switchblade = VehicleDefinition(ObjectClass.switchblade) + val switchblade = VehicleDefinition.Deploying(ObjectClass.switchblade) - val flail = VehicleDefinition(ObjectClass.flail) + val flail = VehicleDefinition.Deploying(ObjectClass.flail) val mosquito = VehicleDefinition(ObjectClass.mosquito) @@ -931,11 +931,11 @@ object GlobalDefinitions { val vulture = VehicleDefinition(ObjectClass.vulture) - val dropship = VehicleDefinition(ObjectClass.dropship) + val dropship = VehicleDefinition.Carrier(ObjectClass.dropship) val galaxy_gunship = VehicleDefinition(ObjectClass.galaxy_gunship) - val lodestar = VehicleDefinition(ObjectClass.lodestar) + val lodestar = VehicleDefinition.Carrier(ObjectClass.lodestar) val phantasm = VehicleDefinition(ObjectClass.phantasm) @@ -6146,6 +6146,7 @@ object GlobalDefinitions { apc_tr.MaxDepth = 3 apc_tr.UnderwaterLifespan(suffocation = 15000L, recovery = 7500L) apc_tr.Geometry = apcForm + apc_tr.MaxCapacitor = 300 apc_nc.Name = "apc_nc" // Vindicator apc_nc.MaxHealth = 6000 @@ -6208,6 +6209,7 @@ object GlobalDefinitions { apc_nc.MaxDepth = 3 apc_nc.UnderwaterLifespan(suffocation = 15000L, recovery = 7500L) apc_nc.Geometry = apcForm + apc_nc.MaxCapacitor = 300 apc_vs.Name = "apc_vs" // Leviathan apc_vs.MaxHealth = 6000 @@ -6270,6 +6272,7 @@ object GlobalDefinitions { apc_vs.MaxDepth = 3 apc_vs.UnderwaterLifespan(suffocation = 15000L, recovery = 7500L) apc_vs.Geometry = apcForm + apc_vs.MaxCapacitor = 300 lightning.Name = "lightning" lightning.MaxHealth = 2000 diff --git a/src/main/scala/net/psforever/objects/SpecialEmp.scala b/src/main/scala/net/psforever/objects/SpecialEmp.scala index 1f2d1b79c..7fd2da1db 100644 --- a/src/main/scala/net/psforever/objects/SpecialEmp.scala +++ b/src/main/scala/net/psforever/objects/SpecialEmp.scala @@ -41,6 +41,11 @@ object SpecialEmp { innateDamage = emp } + /** + * Trigger an electromagnetic pulse. + */ + final case class Burst() + /** * The damage interaction for an electromagnetic pulse effect. * @param empEffect information about the effect diff --git a/src/main/scala/net/psforever/objects/definition/VehicleDefinition.scala b/src/main/scala/net/psforever/objects/definition/VehicleDefinition.scala index 83a41a353..790b23072 100644 --- a/src/main/scala/net/psforever/objects/definition/VehicleDefinition.scala +++ b/src/main/scala/net/psforever/objects/definition/VehicleDefinition.scala @@ -1,9 +1,11 @@ // Copyright (c) 2017 PSForever package net.psforever.objects.definition -import net.psforever.objects.NtuContainerDefinition +import akka.actor.{ActorContext, Props} +import net.psforever.objects.{Default, NtuContainerDefinition, Vehicle} import net.psforever.objects.definition.converter.VehicleConverter import net.psforever.objects.inventory.InventoryTile +import net.psforever.objects.serverobject.PlanetSideServerObject import net.psforever.objects.vehicles.{DestroyedVehicle, MountableWeaponsDefinition, UtilityType} import net.psforever.objects.vital._ import net.psforever.objects.vital.damage.DamageCalculations @@ -181,6 +183,19 @@ class VehicleDefinition(objectId: Int) destroyedModel = model DestroyedModel } + + def Initialize(obj: Vehicle, context: ActorContext): Unit = { + import net.psforever.objects.vehicles.control.VehicleControl + obj.Actor = context.actorOf( + Props(classOf[VehicleControl], obj), + PlanetSideServerObject.UniqueActorName(obj) + ) + } + + def Uninitialize(obj: Vehicle, context: ActorContext): Unit = { + obj.Actor ! akka.actor.PoisonPill + obj.Actor = Default.Actor + } } object VehicleDefinition { @@ -189,4 +204,94 @@ object VehicleDefinition { def apply(objectId: Int): VehicleDefinition = { new VehicleDefinition(objectId) } + + protected class AmsDefinition(objectId: Int) extends VehicleDefinition(objectId) { + import net.psforever.objects.vehicles.control.AmsControl + override def Initialize(obj: Vehicle, context: ActorContext): Unit = { + obj.Actor = context.actorOf( + Props(classOf[AmsControl], obj), + PlanetSideServerObject.UniqueActorName(obj) + ) + } + } + /** + * Vehicle definition for the advanced mobile spawn (AMS) vehicle. + * @param objectId the object id that is associated with this sort of `Vehicle` + */ + def Ams(objectId: Int): VehicleDefinition = new AmsDefinition(objectId) + + protected class AntDefinition(objectId: Int) extends VehicleDefinition(objectId) { + import net.psforever.objects.vehicles.control.AntControl + override def Initialize(obj: Vehicle, context: ActorContext): Unit = { + obj.Actor = context.actorOf( + Props(classOf[AntControl], obj), + PlanetSideServerObject.UniqueActorName(obj) + ) + } + } + /** + * Vehicle definition for the advanced nanite transport (ANT) vehicle. + * @param objectId the object id that is associated with this sort of `Vehicle` + */ + def Ant(objectId: Int): VehicleDefinition = new AntDefinition(objectId) + + protected class ApcDefinition(objectId: Int) extends VehicleDefinition(objectId) { + import net.psforever.objects.vehicles.control.ApcControl + override def Initialize(obj: Vehicle, context: ActorContext): Unit = { + obj.Actor = context.actorOf( + Props(classOf[ApcControl], obj), + PlanetSideServerObject.UniqueActorName(obj) + ) + } + } + /** + * Vehicle definition(s) for the armored personnel carrier (`apc*`) vehicles. + * @param objectId the object id that is associated with this sort of `Vehicle` + */ + def Apc(objectId: Int): VehicleDefinition = new ApcDefinition(objectId) + + protected class CarrierDefinition(objectId: Int) extends VehicleDefinition(objectId) { + import net.psforever.objects.vehicles.control.CargoCarrierControl + override def Initialize(obj: Vehicle, context: ActorContext): Unit = { + obj.Actor = context.actorOf( + Props(classOf[CargoCarrierControl], obj), + PlanetSideServerObject.UniqueActorName(obj) + ) + } + } + /** + * Vehicle definition(s) for the vehicles (carriers) that are used to transport other vehicles (cargo). + * @param objectId the object id that is associated with this sort of `Vehicle` + */ + def Carrier(objectId: Int): VehicleDefinition = new CarrierDefinition(objectId) + + protected class DeployingDefinition(objectId: Int) extends VehicleDefinition(objectId) { + import net.psforever.objects.vehicles.control.DeployingVehicleControl + override def Initialize(obj: Vehicle, context: ActorContext): Unit = { + obj.Actor = context.actorOf( + Props(classOf[DeployingVehicleControl], obj), + PlanetSideServerObject.UniqueActorName(obj) + ) + } + } + /** + * Vehicle definition(s) for the vehicles that perform significant mode state transitions. + * @param objectId the object id that is associated with this sort of `Vehicle` + */ + def Deploying(objectId: Int): VehicleDefinition = new DeployingDefinition(objectId) + + protected class RouterDefinition(objectId: Int) extends VehicleDefinition(objectId) { + import net.psforever.objects.vehicles.control.RouterControl + override def Initialize(obj: Vehicle, context: ActorContext): Unit = { + obj.Actor = context.actorOf( + Props(classOf[RouterControl], obj), + PlanetSideServerObject.UniqueActorName(obj) + ) + } + } + /** + * Vehicle definition for the Router. + * @param objectId the object id that is associated with this sort of `Vehicle` + */ + def Router(objectId: Int): VehicleDefinition = new RouterDefinition(objectId) } diff --git a/src/main/scala/net/psforever/objects/serverobject/damage/DamageableVehicle.scala b/src/main/scala/net/psforever/objects/serverobject/damage/DamageableVehicle.scala index ebbeb7918..9efe3dc82 100644 --- a/src/main/scala/net/psforever/objects/serverobject/damage/DamageableVehicle.scala +++ b/src/main/scala/net/psforever/objects/serverobject/damage/DamageableVehicle.scala @@ -28,27 +28,26 @@ trait DamageableVehicle } /** whether or not the vehicle has been damaged directly, report that damage has occurred */ - private var reportDamageToVehicle: Boolean = false + protected var reportDamageToVehicle: Boolean = false def DamageableObject: Vehicle def AggravatedObject : Vehicle = DamageableObject - override val takesDamage: Receive = - originalTakesDamage - .orElse(aggravatedBehavior) - .orElse { - case DamageableVehicle.Damage(cause, damage) => - //cargo vehicles inherit feedback from carrier - reportDamageToVehicle = damage > 0 - DamageAwareness(DamageableObject, cause, amount = 0) + override val takesDamage: Receive = originalTakesDamage + .orElse(aggravatedBehavior) + .orElse { + case DamageableVehicle.Damage(cause, damage) => + //cargo vehicles inherit feedback from carrier + reportDamageToVehicle = damage > 0 + DamageAwareness(DamageableObject, cause, amount = 0) - case DamageableVehicle.Destruction(cause) => - //cargo vehicles are destroyed when carrier is destroyed - val obj = DamageableObject - obj.Health = 0 - obj.History(cause) - DestructionAwareness(obj, cause) - } + case DamageableVehicle.Destruction(cause) => + //cargo vehicles are destroyed when carrier is destroyed + val obj = DamageableObject + obj.Health = 0 + obj.History(cause) + DestructionAwareness(obj, cause) + } /** * Vehicles may have charged shields that absorb damage before the vehicle's own health is affected. @@ -83,8 +82,6 @@ trait DamageableVehicle /** * Most all vehicles and the weapons mounted to them can jam * if the projectile that strikes (near) them has jammering properties. - * A damaged carrier alerts its cargo vehicles of the source of the damage, - * but it will not be affected by the same jammering effect. * If this vehicle has shields that were affected by previous damage, that is also reported to the clients. * @see `Service.defaultPlayerGUID` * @see `Vehicle.CargoHolds` @@ -156,22 +153,10 @@ trait DamageableVehicle //alert to damage source DamageableMountable.DamageAwareness(obj, cause, totalDamage) } - //alert cargo occupants to damage source - obj.CargoHolds.values.foreach(hold => { - hold.occupant match { - case Some(cargo) => - cargo.Actor ! DamageableVehicle.Damage(cause, totalDamage) - case None => ; - } - }) } } /** - * A destroyed carrier informs its cargo vehicles that they should also be destroyed - * for reasons of the same cause being inherited as the source of damage. - * Regardless of the amount of damage they carrier takes or some other target would take, - * its cargo vehicles die immediately. * The vehicle's shields are zero'd out if they were previously energized * so that the vehicle's corpse does not act like it is still protected by vehicle shields. * Finally, the vehicle is tasked for deconstruction. @@ -196,14 +181,6 @@ trait DamageableVehicle EndAllAggravation() //passengers die with us DamageableMountable.DestructionAwareness(obj, cause) - //cargo vehicles die with us - obj.CargoHolds.values.foreach(hold => { - hold.occupant match { - case Some(cargo) => - cargo.Actor ! DamageableVehicle.Destruction(cause) - case None => ; - } - }) //things positioned around us can get hurt from us Zone.serverSideDamage(obj.Zone, target, Zone.explosionDamage(Some(cause))) //special considerations for certain vehicles @@ -224,17 +201,16 @@ trait DamageableVehicle } object DamageableVehicle { - /** * Message for instructing the target's cargo vehicles about a damage source affecting their carrier. * @param cause historical information about damage */ - private case class Damage(cause: DamageResult, amount: Int) + final case class Damage(cause: DamageResult, amount: Int) /** * Message for instructing the target's cargo vehicles that their carrier is destroyed, * and they should be destroyed too. * @param cause historical information about damage */ - private case class Destruction(cause: DamageResult) + final case class Destruction(cause: DamageResult) } diff --git a/src/main/scala/net/psforever/objects/serverobject/deploy/Deployment.scala b/src/main/scala/net/psforever/objects/serverobject/deploy/Deployment.scala index 719c421e3..34aa78b2a 100644 --- a/src/main/scala/net/psforever/objects/serverobject/deploy/Deployment.scala +++ b/src/main/scala/net/psforever/objects/serverobject/deploy/Deployment.scala @@ -110,4 +110,8 @@ object Deployment { */ def CheckForUndeployState(state: DriveState.Value): Boolean = state == DriveState.Undeploying || state == DriveState.Mobile || state == DriveState.State7 + + def AngleCheck(obj: Deployment.DeploymentObject): Boolean = { + obj.Orientation.x <= 30 || obj.Orientation.x >= 330 + } } diff --git a/src/main/scala/net/psforever/objects/vehicles/control/AmsControl.scala b/src/main/scala/net/psforever/objects/vehicles/control/AmsControl.scala new file mode 100644 index 000000000..38109464b --- /dev/null +++ b/src/main/scala/net/psforever/objects/vehicles/control/AmsControl.scala @@ -0,0 +1,58 @@ +// Copyright (c) 2021 PSForever +package net.psforever.objects.vehicles.control + +import net.psforever.objects._ +import net.psforever.services.Service +import net.psforever.services.vehicle.{VehicleAction, VehicleServiceMessage} +import net.psforever.types.DriveState + +/** + * A vehicle control agency exclusive to the advanced mobile spawn (AMS). + * When deployed, infantry troops may manifest nearby the vehicle + * as they switch from being deconstructed (or dead) to being alive. + * @param vehicle the AMS + */ +class AmsControl(vehicle: Vehicle) + extends DeployingVehicleControl(vehicle) { + + /** + * React to a deployment state change. + * Announce that this AMS is ready to accept troop deployment. + * @param state the deployment state + */ + override def specificResponseToDeployment(state: DriveState.Value): Unit = { + state match { + case DriveState.Deployed => + val zone = vehicle.Zone + val driverChannel = vehicle.Seats(0).occupant match { + case Some(tplayer) => tplayer.Name + case None => "" + } + val events = zone.VehicleEvents + events ! VehicleServiceMessage.AMSDeploymentChange(zone) + events ! VehicleServiceMessage(driverChannel, VehicleAction.PlanetsideAttribute(Service.defaultPlayerGUID, vehicle.GUID, 81, 1)) + case _ => ; + } + } + + /** + * React to an undeployment state change. + * This AMS is now off the grid. + * @param state the deployment state + */ + override def specificResponseToUndeployment(state: DriveState.Value): Unit = { + state match { + case DriveState.Undeploying => + val zone = vehicle.Zone + val driverChannel = vehicle.Seats(0).occupant match { + case Some(tplayer) => tplayer.Name + case None => "" + } + val events = zone.VehicleEvents + events ! VehicleServiceMessage.AMSDeploymentChange(zone) + events ! VehicleServiceMessage(driverChannel, VehicleAction.PlanetsideAttribute(Service.defaultPlayerGUID, vehicle.GUID, 81, 0)) + case _ => ; + } + } +} + diff --git a/src/main/scala/net/psforever/objects/vehicles/control/AntControl.scala b/src/main/scala/net/psforever/objects/vehicles/control/AntControl.scala new file mode 100644 index 000000000..d682fafe6 --- /dev/null +++ b/src/main/scala/net/psforever/objects/vehicles/control/AntControl.scala @@ -0,0 +1,60 @@ +// Copyright (c) 2021 PSForever +package net.psforever.objects.vehicles.control + +import net.psforever.objects._ +import net.psforever.objects.serverobject.transfer.TransferBehavior +import net.psforever.objects.vehicles._ +import net.psforever.types.DriveState + +import scala.concurrent.ExecutionContext.Implicits.global +import scala.concurrent.duration._ + +/** + * A vehicle control agency exclusive to the advanced nanite transport (ANT). + * When deployed, nanites in the package of nanite transfer units (NTU) are moved around + * and may be may be acquired from a Warp Gate structure + * or supplied to a nanite resource silo belonging to a mjaor facility. + * @param vehicle the ANT + */ +class AntControl(vehicle: Vehicle) + extends DeployingVehicleControl(vehicle) + with AntTransferBehavior { + def ChargeTransferObject = vehicle + + findChargeTargetFunc = Vehicles.FindANTChargingSource + findDischargeTargetFunc = Vehicles.FindANTDischargingTarget + + override def commonEnabledBehavior: Receive = super.commonEnabledBehavior.orElse(antBehavior) + + /** + * React to a deployment state change. + * Make ourselves available to nanite charging or discharging. + * @param state the deployment state + */ + override def specificResponseToDeployment(state: DriveState.Value): Unit = { + state match { + case DriveState.Deployed => + // Start ntu regeneration + // If vehicle sends UseItemMessage with silo as target NTU regeneration will be disabled and orb particles will be disabled + context.system.scheduler.scheduleOnce( + delay = 1000 milliseconds, + vehicle.Actor, + TransferBehavior.Charging(Ntu.Nanites) + ) + case _ => ; + } + } + + /** + * React to an undeployment state change. + * Stop charging or discharging and tell the partner entity that it is no longer interacting with this vehicle. + * @param state the undeployment state + */ + override def specificResponseToUndeployment(state: DriveState.Value): Unit = { + state match { + case DriveState.Undeploying => + TryStopChargingEvent(vehicle) + case _ => ; + } + } +} diff --git a/src/main/scala/net/psforever/objects/vehicles/control/ApcControl.scala b/src/main/scala/net/psforever/objects/vehicles/control/ApcControl.scala new file mode 100644 index 000000000..5655ec931 --- /dev/null +++ b/src/main/scala/net/psforever/objects/vehicles/control/ApcControl.scala @@ -0,0 +1,116 @@ +// Copyright (c) 2021 PSForever +package net.psforever.objects.vehicles.control + +import net.psforever.objects._ +import net.psforever.objects.serverobject.damage.Damageable.Target +import net.psforever.objects.vital.interaction.DamageResult +import net.psforever.objects.zones.Zone +import net.psforever.packet.game.{TriggerEffectMessage, TriggeredEffectLocation} +import net.psforever.services.Service +import net.psforever.services.vehicle.{VehicleAction, VehicleServiceMessage} + +import scala.concurrent.ExecutionContext.Implicits.global +import scala.concurrent.duration._ + +/** + * A vehicle control agency exclusive to the armored personnel carrier (APC) ground transport vehicles. + * These vehicles include the Juggernaut (`apc_tr`), the Vindicator (`apc_nc`), the and Leviathan (`apc_vs`). + * A completely faction-neutral transport in the same sytle (`apc`) does exist but is unused. + * A common characteristic of this type of vehicle is the ability to discharge a defensive wide-area electromagnetic pulse. + * @param vehicle the APC + */ +class ApcControl(vehicle: Vehicle) + extends VehicleControl(vehicle) { + protected var capacitor = Default.Cancellable + + startCapacitorTimer() + + override def postStop() : Unit = { + super.postStop() + capacitor.cancel() + } + + override def commonEnabledBehavior : Receive = + super.commonEnabledBehavior + .orElse { + case ApcControl.CapacitorCharge(amount) => + if (vehicle.Capacitor < vehicle.Definition.MaxCapacitor) { + val capacitance = vehicle.Capacitor += amount + vehicle.Zone.VehicleEvents ! VehicleServiceMessage( + self.toString(), + VehicleAction.PlanetsideAttribute(Service.defaultPlayerGUID, vehicle.GUID, 113, capacitance) + ) + startCapacitorTimer() + } else { + capacitor = Default.Cancellable + } + + case SpecialEmp.Burst() => + if (vehicle.Capacitor == vehicle.Definition.MaxCapacitor) { //only if the capacitor is full + val zone = vehicle.Zone + val events = zone.VehicleEvents + val pos = vehicle.Position + val GUID0 = Service.defaultPlayerGUID + val emp = vehicle.Definition.innateDamage.getOrElse { SpecialEmp.emp } + val faction = vehicle.Faction + //drain the capacitor + vehicle.Capacitor = 0 + events ! VehicleServiceMessage( + self.toString(), + VehicleAction.PlanetsideAttribute(GUID0, vehicle.GUID, 113, 0) + ) + //cause the emp + events ! VehicleServiceMessage( + zone.id, + VehicleAction.SendResponse( + GUID0, + TriggerEffectMessage( + GUID0, + s"apc_explosion_emp_${faction.toString.toLowerCase}", + None, + Some(TriggeredEffectLocation(pos, vehicle.Orientation)) + ) + ) + ) + //resolve what targets are affected by the emp + Zone.serverSideDamage( + zone, + vehicle, + emp, + SpecialEmp.createEmpInteraction(emp, pos), + ExplosiveDeployableControl.detectionForExplosiveSource(vehicle), + Zone.findAllTargets + ) + //start charging again + startCapacitorTimer() + } + } + + override def PrepareForDisabled(kickPassengers: Boolean) : Unit = { + super.PrepareForDisabled(kickPassengers) + capacitor.cancel() + } + + override protected def DestructionAwareness(target: Target, cause: DamageResult): Unit = { + super.DestructionAwareness(target, cause) + capacitor.cancel() + vehicle.Capacitor = 0 + } + + //TODO switch from magic numbers to definition numbers? + private def startCapacitorTimer(): Unit = { + capacitor = context.system.scheduler.scheduleOnce( + delay = 1000 millisecond, + self, + ApcControl.CapacitorCharge(10) + ) + } +} + +object ApcControl { + /** + * Charge the vehicle's internal capacitor by the given amount during the schedulefd charge event. + * @param amount how much energy in the charge + */ + private case class CapacitorCharge(amount: Int) +} diff --git a/src/main/scala/net/psforever/objects/vehicles/control/CargoCarrierControl.scala b/src/main/scala/net/psforever/objects/vehicles/control/CargoCarrierControl.scala new file mode 100644 index 000000000..ccfc6c3dd --- /dev/null +++ b/src/main/scala/net/psforever/objects/vehicles/control/CargoCarrierControl.scala @@ -0,0 +1,88 @@ +// Copyright (c) 2021 PSForever +package net.psforever.objects.vehicles.control + +import net.psforever.objects._ +import net.psforever.objects.serverobject.damage.{Damageable, DamageableVehicle} +import net.psforever.objects.vehicles.CargoBehavior +import net.psforever.objects.vital.interaction.DamageResult + +/** + * A vehicle control agency exclusive to vehicles that can physically transport other vehicles. + * This includes the Galaxy (`dropship`) and the Lodestar. + * @param vehicle the vehicle + */ +class CargoCarrierControl(vehicle: Vehicle) + extends VehicleControl(vehicle) + with CargoBehavior { + def CargoObject = vehicle + + override def commonEnabledBehavior: Receive = super.commonEnabledBehavior.orElse(cargoBehavior) + + /** + * If the vehicle becomes disabled, the safety and autonomy of the cargo should be prioritized. + * @param kickPassengers passengers need to be ejected "by force" + */ + override def PrepareForDisabled(kickPassengers: Boolean) : Unit = { + //abandon all cargo + vehicle.CargoHolds.values + .collect { + case hold if hold.isOccupied => + val cargo = hold.occupant.get + CargoBehavior.HandleVehicleCargoDismount( + cargo.GUID, + cargo, + vehicle.GUID, + vehicle, + bailed = false, + requestedByPassenger = false, + kicked = false + ) + } + super.PrepareForDisabled(kickPassengers) + } + + /** + * A damaged carrier alerts its cargo vehicles of the source of the damage, + * but that cargo will not be affected by either damage directly or by other effects applied to the carrier. + * @param target the entity being destroyed + * @param cause historical information about the damage + * @param amount how much damage was performed + */ + override protected def DamageAwareness(target: Damageable.Target, cause: DamageResult, amount: Any): Unit = { + val report = amount match { + case (a: Int, b: Int) => a + b + case a: Int => a + case _ => 0 + } + val announceConfrontation: Boolean = reportDamageToVehicle || report > 0 + super.DamageAwareness(target, cause, amount) + if (announceConfrontation) { + //alert cargo occupants to damage source + vehicle.CargoHolds.values.foreach(hold => { + hold.occupant match { + case Some(cargo) => cargo.Actor ! DamageableVehicle.Damage(cause, report) + case None => ; + } + }) + } + } + + /** + * A destroyed carrier informs its cargo vehicles that they should also be destroyed + * for reasons of the same cause being inherited as the source of damage. + * Regardless of the amount of damage they carrier takes or some other target would take, + * its cargo vehicles die immediately. + * @param target the entity being destroyed + * @param cause historical information about the damage + */ + override protected def DestructionAwareness(target: Damageable.Target, cause: DamageResult): Unit = { + super.DestructionAwareness(target, cause) + //cargo vehicles die with us + vehicle.CargoHolds.values.foreach { hold => + hold.occupant match { + case Some(cargo) => cargo.Actor ! DamageableVehicle.Destruction(cause) + case None => ; + } + } + } +} diff --git a/src/main/scala/net/psforever/objects/vehicles/control/DeployingVehicleControl.scala b/src/main/scala/net/psforever/objects/vehicles/control/DeployingVehicleControl.scala new file mode 100644 index 000000000..ef5014eea --- /dev/null +++ b/src/main/scala/net/psforever/objects/vehicles/control/DeployingVehicleControl.scala @@ -0,0 +1,97 @@ +// Copyright (c) 2021 PSForever +package net.psforever.objects.vehicles.control + +import net.psforever.objects._ +import net.psforever.objects.serverobject.deploy.Deployment.DeploymentObject +import net.psforever.objects.serverobject.deploy.{Deployment, DeploymentBehavior} +import net.psforever.objects.serverobject.mount.Mountable +import net.psforever.types._ + +/** + * A vehicle control agency exclusive to vehicles that can switch out a navigation mode + * and convert to a sessile mode that affords additional functionality. + * This includes only the Switchblade and the Flail. + * Other vehicles that deploy are handled by specific instances of this control agency. + * @see `AmsControl` + * @see `AntControl` + * @see `RouterControl` + * @param vehicle the vehicle + */ +class DeployingVehicleControl(vehicle: Vehicle) + extends VehicleControl(vehicle) + with DeploymentBehavior { + def DeploymentObject = vehicle + + override def commonEnabledBehavior : Receive = super.commonEnabledBehavior.orElse(deployBehavior) + + /** + * Even when disabled, the vehicle can be made to undeploy. + * Even when disabled, passengers can formally dismount from the vehicle. + */ + override def commonDisabledBehavior : Receive = + super.commonDisabledBehavior + .orElse { + case msg : Deployment.TryUndeploy => + deployBehavior.apply(msg) + + case msg @ Mountable.TryDismount(_, seat_num) => + dismountBehavior.apply(msg) + dismountCleanup(seat_num) + } + + /** + * Even when on the verge of deletion, the vehicle can be made to undeploy. + */ + override def commonDeleteBehavior : Receive = + super.commonDeleteBehavior + .orElse { + case msg : Deployment.TryUndeploy => + deployBehavior.apply(msg) + } + + /** + * Even when disabled, the vehicle can be made to undeploy. + */ + override def PrepareForDisabled(kickPassengers: Boolean) : Unit = { + vehicle.Actor ! Deployment.TryUndeploy(DriveState.Undeploying) + super.PrepareForDisabled(kickPassengers) + } + + /** + * Even when on the verge of deletion, the vehicle can be made to undeploy. + */ + override def PrepareForDeletion() : Unit = { + vehicle.Actor ! Deployment.TryUndeploy(DriveState.Undeploying) + super.PrepareForDeletion() + } + + override def TryDeploymentChange(obj: Deployment.DeploymentObject, state: DriveState.Value): Boolean = { + Deployment.AngleCheck(obj) && super.TryDeploymentChange(obj, state) + } + + override def DeploymentAction( + obj: DeploymentObject, + state: DriveState.Value, + prevState: DriveState.Value + ): DriveState.Value = { + val out = super.DeploymentAction(obj, state, prevState) + Vehicles.ReloadAccessPermissions(vehicle, vehicle.Faction.toString) + specificResponseToDeployment(state) + out + } + + def specificResponseToDeployment(state: DriveState.Value): Unit = { } + + override def UndeploymentAction( + obj: DeploymentObject, + state: DriveState.Value, + prevState: DriveState.Value + ): DriveState.Value = { + val out = if (decaying) state else super.UndeploymentAction(obj, state, prevState) + Vehicles.ReloadAccessPermissions(vehicle, vehicle.Faction.toString) + specificResponseToUndeployment(state) + out + } + + def specificResponseToUndeployment(state: DriveState.Value): Unit = { } +} diff --git a/src/main/scala/net/psforever/objects/vehicles/control/RouterControl.scala b/src/main/scala/net/psforever/objects/vehicles/control/RouterControl.scala new file mode 100644 index 000000000..010a44ee0 --- /dev/null +++ b/src/main/scala/net/psforever/objects/vehicles/control/RouterControl.scala @@ -0,0 +1,50 @@ +// Copyright (c) 2021 PSForever +package net.psforever.objects.vehicles.control + +import net.psforever.objects._ +import net.psforever.objects.ce.TelepadLike +import net.psforever.objects.vehicles.{Utility, UtilityType} +import net.psforever.types.DriveState + +/** + * A vehicle control agency exclusive to the router. + * When deployed, any router telepad that was acquired from this particular router + * and then constructed into a router telepad somewhere in the world + * may synchronize with the vehicle to establish a short to medium range infantry teleportation system. + * @param vehicle the router + */ +class RouterControl(vehicle: Vehicle) + extends DeployingVehicleControl(vehicle) { + + /** + * React to a deployment state change. + * Activate the internal telepad mechanism. + * @param state the deployment state + */ + override def specificResponseToDeployment(state: DriveState.Value): Unit = { + state match { + case DriveState.Deployed => + vehicle.Utility(UtilityType.internal_router_telepad_deployable) match { + case Some(util: Utility.InternalTelepad) => util.Actor ! TelepadLike.Activate(util) + case _ => ; + } + case _ => ; + } + } + + /** + * React to an undeployment state change. + * Deactivate the internal telepad mechanism. + * @param state the deployment state + */ + override def specificResponseToUndeployment(state: DriveState.Value): Unit = { + state match { + case DriveState.Undeploying => + vehicle.Utility(UtilityType.internal_router_telepad_deployable) match { + case Some(util: Utility.InternalTelepad) => util.Actor ! TelepadLike.Deactivate(util) + case _ => ; + } + case _ => ; + } + } +} diff --git a/src/main/scala/net/psforever/objects/vehicles/VehicleControl.scala b/src/main/scala/net/psforever/objects/vehicles/control/VehicleControl.scala similarity index 61% rename from src/main/scala/net/psforever/objects/vehicles/VehicleControl.scala rename to src/main/scala/net/psforever/objects/vehicles/control/VehicleControl.scala index fff3c7fe6..f001da233 100644 --- a/src/main/scala/net/psforever/objects/vehicles/VehicleControl.scala +++ b/src/main/scala/net/psforever/objects/vehicles/control/VehicleControl.scala @@ -1,11 +1,10 @@ -// Copyright (c) 2017-2020 PSForever -package net.psforever.objects.vehicles +// Copyright (c) 2017-2021 PSForever +package net.psforever.objects.vehicles.control import akka.actor.{Actor, Cancellable} import net.psforever.actors.zone.ZoneActor import net.psforever.objects._ import net.psforever.objects.ballistics.VehicleSource -import net.psforever.objects.ce.TelepadLike import net.psforever.objects.entity.WorldEntity import net.psforever.objects.equipment.{Equipment, EquipmentSlot, JammableMountedWeapons} import net.psforever.objects.guid.GUIDTask @@ -14,14 +13,12 @@ import net.psforever.objects.serverobject.{CommonMessages, PlanetSideServerObjec import net.psforever.objects.serverobject.affinity.{FactionAffinity, FactionAffinityBehavior} import net.psforever.objects.serverobject.containable.{Containable, ContainableBehavior} import net.psforever.objects.serverobject.damage.{AggravatedBehavior, DamageableVehicle} -import net.psforever.objects.serverobject.deploy.Deployment.DeploymentObject -import net.psforever.objects.serverobject.deploy.{Deployment, DeploymentBehavior} import net.psforever.objects.serverobject.environment._ import net.psforever.objects.serverobject.hackable.GenericHackables import net.psforever.objects.serverobject.mount.{Mountable, MountableBehavior} import net.psforever.objects.serverobject.repair.RepairableVehicle import net.psforever.objects.serverobject.terminals.Terminal -import net.psforever.objects.serverobject.transfer.TransferBehavior +import net.psforever.objects.vehicles.{AccessPermissionGroup, CargoBehavior, Utility, VehicleLockState} import net.psforever.objects.vital.interaction.{DamageInteraction, DamageResult} import net.psforever.objects.vital.VehicleShieldCharge import net.psforever.objects.vital.environment.EnvironmentReason @@ -40,45 +37,36 @@ import scala.concurrent.duration._ /** * An `Actor` that handles messages being dispatched to a specific `Vehicle`.
*
- * Vehicle-controlling actors have two behavioral states - responsive and "`Disabled`." + * Vehicle-controlling actors have two important behavioral states - responsive and "`Disabled`." * The latter is applicable only when the specific vehicle is being deconstructed. - * + * Furthermore, being "ready to delete" is also a behavoral state for the end of life operations of the vehicle. * @param vehicle the `Vehicle` object being governed */ class VehicleControl(vehicle: Vehicle) - extends Actor + extends Actor with FactionAffinityBehavior.Check - with DeploymentBehavior with MountableBehavior - with CargoBehavior with DamageableVehicle with RepairableVehicle with JammableMountedWeapons with ContainableBehavior - with AntTransferBehavior with AggravatedBehavior with RespondsToZoneEnvironment { //make control actors belonging to utilities when making control actor belonging to vehicle - vehicle.Utilities.foreach({ case (_, util) => util.Setup }) + vehicle.Utilities.foreach { case (_, util) => util.Setup } def MountableObject = vehicle - def CargoObject = vehicle - def JammableObject = vehicle def FactionObject = vehicle - def DeploymentObject = vehicle - def DamageableObject = vehicle def RepairableObject = vehicle def ContainerObject = vehicle - def ChargeTransferObject = vehicle - def InteractiveObject = vehicle SetInteraction(EnvironmentAttribute.Water, doInteractingWithWater) SetInteraction(EnvironmentAttribute.Lava, doInteractingWithLava) @@ -87,10 +75,6 @@ class VehicleControl(vehicle: Vehicle) SetInteractionStop(EnvironmentAttribute.Water, stopInteractingWithWater) } - if (vehicle.Definition == GlobalDefinitions.ant) { - findChargeTargetFunc = Vehicles.FindANTChargingSource - findDischargeTargetFunc = Vehicles.FindANTDischargingTarget - } /** cheap flag for whether the vehicle is decaying */ var decaying : Boolean = false /** primary vehicle decay timer */ @@ -112,199 +96,197 @@ class VehicleControl(vehicle: Vehicle) recoverFromEnvironmentInteracting() } - def Enabled : Receive = - checkBehavior - .orElse(deployBehavior) - .orElse(cargoBehavior) - .orElse(jammableBehavior) - .orElse(takesDamage) - .orElse(canBeRepairedByNanoDispenser) - .orElse(containerBehavior) - .orElse(antBehavior) - .orElse(environmentBehavior) - .orElse { - case Vehicle.Ownership(None) => - LoseOwnership() + def commonEnabledBehavior: Receive = checkBehavior + .orElse(jammableBehavior) + .orElse(takesDamage) + .orElse(canBeRepairedByNanoDispenser) + .orElse(containerBehavior) + .orElse(environmentBehavior) + .orElse { + case Vehicle.Ownership(None) => + LoseOwnership() - case Vehicle.Ownership(Some(player)) => - GainOwnership(player) + case Vehicle.Ownership(Some(player)) => + GainOwnership(player) - case msg @ Mountable.TryMount(player, mount_point) => - mountBehavior.apply(msg) - mountCleanup(mount_point, player) + case msg @ Mountable.TryMount(player, mount_point) => + mountBehavior.apply(msg) + mountCleanup(mount_point, player) - case msg @ Mountable.TryDismount(_, seat_num) => - dismountBehavior.apply(msg) - dismountCleanup(seat_num) + case msg @ Mountable.TryDismount(_, seat_num) => + dismountBehavior.apply(msg) + dismountCleanup(seat_num) - case Vehicle.ChargeShields(amount) => - val now : Long = System.currentTimeMillis() - //make certain vehicle doesn't charge shields too quickly - if ( - vehicle.Health > 0 && vehicle.Shields < vehicle.MaxShields && - !vehicle.History.exists(VehicleControl.LastShieldChargeOrDamage(now)) - ) { - vehicle.History(VehicleShieldCharge(VehicleSource(vehicle), amount)) - vehicle.Shields = vehicle.Shields + amount - vehicle.Zone.VehicleEvents ! VehicleServiceMessage( - s"${vehicle.Actor}", - VehicleAction.PlanetsideAttribute(PlanetSideGUID(0), vehicle.GUID, 68, vehicle.Shields) - ) - } + case Vehicle.ChargeShields(amount) => + val now : Long = System.currentTimeMillis() + //make certain vehicles don't charge shields too quickly + if ( + vehicle.Health > 0 && vehicle.Shields < vehicle.MaxShields && + !vehicle.History.exists(VehicleControl.LastShieldChargeOrDamage(now)) + ) { + vehicle.History(VehicleShieldCharge(VehicleSource(vehicle), amount)) + vehicle.Shields = vehicle.Shields + amount + vehicle.Zone.VehicleEvents ! VehicleServiceMessage( + s"${vehicle.Actor}", + VehicleAction.PlanetsideAttribute(PlanetSideGUID(0), vehicle.GUID, 68, vehicle.Shields) + ) + } - case Vehicle.UpdateZoneInteractionProgressUI(player) => - updateZoneInteractionProgressUI(player) + case Vehicle.UpdateZoneInteractionProgressUI(player) => + updateZoneInteractionProgressUI(player) - case FactionAffinity.ConvertFactionAffinity(faction) => - val originalAffinity = vehicle.Faction - if (originalAffinity != (vehicle.Faction = faction)) { - vehicle.Utilities.foreach({ - case (_ : Int, util : Utility) => util().Actor forward FactionAffinity.ConfirmFactionAffinity() - }) - } - sender() ! FactionAffinity.AssertFactionAffinity(vehicle, faction) + case FactionAffinity.ConvertFactionAffinity(faction) => + val originalAffinity = vehicle.Faction + if (originalAffinity != (vehicle.Faction = faction)) { + vehicle.Utilities.foreach({ + case (_ : Int, util : Utility) => util().Actor forward FactionAffinity.ConfirmFactionAffinity() + }) + } + sender() ! FactionAffinity.AssertFactionAffinity(vehicle, faction) - case CommonMessages.Use(player, Some(item : SimpleItem)) - if item.Definition == GlobalDefinitions.remote_electronics_kit => - //TODO setup certifications check - if (vehicle.Faction != player.Faction) { - sender() ! CommonMessages.Progress( - GenericHackables.GetHackSpeed(player, vehicle), - Vehicles.FinishHackingVehicle(vehicle, player, 3212836864L), - GenericHackables.HackingTickAction(progressType = 1, player, vehicle, item.GUID) - ) - } + case CommonMessages.Use(player, Some(item : SimpleItem)) + if item.Definition == GlobalDefinitions.remote_electronics_kit => + //TODO setup certifications check + if (vehicle.Faction != player.Faction) { + sender() ! CommonMessages.Progress( + GenericHackables.GetHackSpeed(player, vehicle), + Vehicles.FinishHackingVehicle(vehicle, player, 3212836864L), + GenericHackables.HackingTickAction(progressType = 1, player, vehicle, item.GUID) + ) + } - case Terminal.TerminalMessage(player, msg, reply) => - reply match { - case Terminal.VehicleLoadout(definition, weapons, inventory) => - org.log4s - .getLogger(vehicle.Definition.Name) - .info(s"changing vehicle equipment loadout to ${player.Name}'s option #${msg.unk1 + 1}") - //remove old inventory - val oldInventory = vehicle.Inventory.Clear().map { case InventoryItem(obj, _) => (obj, obj.GUID) } - //"dropped" items are lost; if it doesn't go in the trunk, it vanishes into the nanite cloud - val (_, afterInventory) = inventory.partition(ContainableBehavior.DropPredicate(player)) - val (oldWeapons, newWeapons, finalInventory) = if (vehicle.Definition == definition) { - //vehicles are the same type - //TODO want to completely swap weapons, but holster icon vanishes temporarily after swap - //TODO BFR arms must be swapped properly - // //remove old weapons - // val oldWeapons = vehicle.Weapons.values.collect { case slot if slot.Equipment.nonEmpty => - // val obj = slot.Equipment.get - // slot.Equipment = None - // (obj, obj.GUID) - // }.toList - // (oldWeapons, weapons, afterInventory) - //TODO for now, just refill ammo; assume weapons stay the same - vehicle.Weapons - .collect { case (_, slot : EquipmentSlot) if slot.Equipment.nonEmpty => slot.Equipment.get } - .collect { - case weapon : Tool => - weapon.AmmoSlots.foreach { ammo => ammo.Box.Capacity = ammo.Box.Definition.Capacity } - } - (Nil, Nil, afterInventory) + case Terminal.TerminalMessage(player, msg, reply) => + reply match { + case Terminal.VehicleLoadout(definition, weapons, inventory) => + org.log4s + .getLogger(vehicle.Definition.Name) + .info(s"changing vehicle equipment loadout to ${player.Name}'s option #${msg.unk1 + 1}") + //remove old inventory + val oldInventory = vehicle.Inventory.Clear().map { case InventoryItem(obj, _) => (obj, obj.GUID) } + //"dropped" items are lost; if it doesn't go in the trunk, it vanishes into the nanite cloud + val (_, afterInventory) = inventory.partition(ContainableBehavior.DropPredicate(player)) + val (oldWeapons, newWeapons, finalInventory) = if (vehicle.Definition == definition) { + //vehicles are the same type + //TODO want to completely swap weapons, but holster icon vanishes temporarily after swap + //TODO BFR arms must be swapped properly + // //remove old weapons + // val oldWeapons = vehicle.Weapons.values.collect { case slot if slot.Equipment.nonEmpty => + // val obj = slot.Equipment.get + // slot.Equipment = None + // (obj, obj.GUID) + // }.toList + // (oldWeapons, weapons, afterInventory) + //TODO for now, just refill ammo; assume weapons stay the same + vehicle.Weapons + .collect { case (_, slot : EquipmentSlot) if slot.Equipment.nonEmpty => slot.Equipment.get } + .collect { + case weapon : Tool => + weapon.AmmoSlots.foreach { ammo => ammo.Box.Capacity = ammo.Box.Definition.Capacity } + } + (Nil, Nil, afterInventory) + } + else { + //vehicle loadout is not for this vehicle + //do not transfer over weapon ammo + if ( + vehicle.Definition.TrunkSize == definition.TrunkSize && vehicle.Definition.TrunkOffset == definition.TrunkOffset + ) { + (Nil, Nil, afterInventory) //trunk is the same dimensions, however } else { - //vehicle loadout is not for this vehicle - //do not transfer over weapon ammo - if ( - vehicle.Definition.TrunkSize == definition.TrunkSize && vehicle.Definition.TrunkOffset == definition.TrunkOffset - ) { - (Nil, Nil, afterInventory) //trunk is the same dimensions, however - } - else { - //accommodate as much of inventory as possible - val (stow, _) = GridInventory.recoverInventory(afterInventory, vehicle.Inventory) - (Nil, Nil, stow) - } + //accommodate as much of inventory as possible + val (stow, _) = GridInventory.recoverInventory(afterInventory, vehicle.Inventory) + (Nil, Nil, stow) } - finalInventory.foreach { - _.obj.Faction = vehicle.Faction - } - player.Zone.VehicleEvents ! VehicleServiceMessage( - player.Zone.id, - VehicleAction.ChangeLoadout(vehicle.GUID, oldWeapons, newWeapons, oldInventory, finalInventory) - ) - player.Zone.AvatarEvents ! AvatarServiceMessage( - player.Name, - AvatarAction.TerminalOrderResult(msg.terminal_guid, msg.transaction_type, true) - ) + } + finalInventory.foreach { + _.obj.Faction = vehicle.Faction + } + player.Zone.VehicleEvents ! VehicleServiceMessage( + player.Zone.id, + VehicleAction.ChangeLoadout(vehicle.GUID, oldWeapons, newWeapons, oldInventory, finalInventory) + ) + player.Zone.AvatarEvents ! AvatarServiceMessage( + player.Name, + AvatarAction.TerminalOrderResult(msg.terminal_guid, msg.transaction_type, true) + ) - case _ => ; - } + case _ => ; + } - case VehicleControl.Disable() => - PrepareForDisabled(kickPassengers = false) - context.become(Disabled) + case VehicleControl.Disable() => + PrepareForDisabled(kickPassengers = false) + context.become(Disabled) - case Vehicle.Deconstruct(time) => - time match { - case Some(delay) if vehicle.Definition.undergoesDecay => - decaying = true - decayTimer.cancel() - decayTimer = context.system.scheduler.scheduleOnce(delay, self, VehicleControl.PrepareForDeletion()) - case _ => - PrepareForDisabled(kickPassengers = true) - PrepareForDeletion() - context.become(ReadyToDelete) - } + case Vehicle.Deconstruct(time) => + time match { + case Some(delay) if vehicle.Definition.undergoesDecay => + decaying = true + decayTimer.cancel() + decayTimer = context.system.scheduler.scheduleOnce(delay, self, VehicleControl.PrepareForDeletion()) + case _ => + PrepareForDisabled(kickPassengers = true) + PrepareForDeletion() + context.become(ReadyToDelete) + } - case VehicleControl.PrepareForDeletion() => - PrepareForDisabled(kickPassengers = true) - PrepareForDeletion() - context.become(ReadyToDelete) + case VehicleControl.PrepareForDeletion() => + PrepareForDisabled(kickPassengers = true) + PrepareForDeletion() + context.become(ReadyToDelete) - case VehicleControl.AssignOwnership(player) => - vehicle.AssignOwnership(player) + case VehicleControl.AssignOwnership(player) => + vehicle.AssignOwnership(player) + } + final def Enabled: Receive = + commonEnabledBehavior + .orElse { case _ => ; } - def Disabled : Receive = - checkBehavior - .orElse { - case msg : Deployment.TryUndeploy => - deployBehavior.apply(msg) + def commonDisabledBehavior: Receive = checkBehavior + .orElse { + case msg @ Mountable.TryDismount(_, seat_num) => + dismountBehavior.apply(msg) + dismountCleanup(seat_num) - case msg @ Mountable.TryDismount(_, seat_num) => - dismountBehavior.apply(msg) - dismountCleanup(seat_num) + case Vehicle.Deconstruct(time) => + time match { + case Some(delay) if vehicle.Definition.undergoesDecay => + decaying = true + decayTimer.cancel() + decayTimer = context.system.scheduler.scheduleOnce(delay, self, VehicleControl.PrepareForDeletion()) + case _ => + PrepareForDeletion() + context.become(ReadyToDelete) + } - case Vehicle.Deconstruct(time) => - time match { - case Some(delay) if vehicle.Definition.undergoesDecay => - decaying = true - decayTimer.cancel() - decayTimer = context.system.scheduler.scheduleOnce(delay, self, VehicleControl.PrepareForDeletion()) - case _ => - PrepareForDeletion() - context.become(ReadyToDelete) - } + case VehicleControl.PrepareForDeletion() => + PrepareForDeletion() + context.become(ReadyToDelete) + } - case VehicleControl.PrepareForDeletion() => - PrepareForDeletion() - context.become(ReadyToDelete) + final def Disabled: Receive = commonDisabledBehavior + .orElse { + case _ => ; + } - case _ => - } + def commonDeleteBehavior: Receive = checkBehavior + .orElse { + case VehicleControl.Deletion() => + val zone = vehicle.Zone + zone.VehicleEvents ! VehicleServiceMessage( + zone.id, + VehicleAction.UnloadVehicle(Service.defaultPlayerGUID, vehicle, vehicle.GUID) + ) + zone.Transport.tell(Zone.Vehicle.Despawn(vehicle), zone.Transport) + } - def ReadyToDelete : Receive = - checkBehavior - .orElse { - case msg : Deployment.TryUndeploy => - deployBehavior.apply(msg) - - case VehicleControl.Deletion() => - val zone = vehicle.Zone - zone.VehicleEvents ! VehicleServiceMessage( - zone.id, - VehicleAction.UnloadVehicle(Service.defaultPlayerGUID, vehicle, vehicle.GUID) - ) - zone.Transport.tell(Zone.Vehicle.Despawn(vehicle), zone.Transport) - - case _ => - } + final def ReadyToDelete: Receive = commonDeleteBehavior + .orElse { + case _ => ; + } override protected def mountTest( obj: PlanetSideServerObject with Mountable, @@ -387,7 +369,6 @@ class VehicleControl(vehicle: Vehicle) val events = zone.VehicleEvents //miscellaneous changes recoverFromEnvironmentInteracting() - Vehicles.BeforeUnloadVehicle(vehicle, zone) //escape being someone else's cargo vehicle.MountedIn match { case Some(_) => @@ -417,28 +398,11 @@ class VehicleControl(vehicle: Vehicle) } } } - //abandon all cargo - vehicle.CargoHolds.values - .collect { - case hold if hold.isOccupied => - val cargo = hold.occupant.get - CargoBehavior.HandleVehicleCargoDismount( - cargo.GUID, - cargo, - guid, - vehicle, - bailed = false, - requestedByPassenger = false, - kicked = false - ) - } - } + } def PrepareForDeletion() : Unit = { decaying = false val zone = vehicle.Zone - //miscellaneous changes - Vehicles.BeforeUnloadVehicle(vehicle, zone) //cancel jammed behavior CancelJammeredSound(vehicle) CancelJammeredStatus(vehicle) @@ -557,119 +521,6 @@ class VehicleControl(vehicle: Vehicle) ) } - override def TryDeploymentChange(obj: Deployment.DeploymentObject, state: DriveState.Value): Boolean = { - VehicleControl.DeploymentAngleCheck(obj) && super.TryDeploymentChange(obj, state) - } - - override def DeploymentAction( - obj: DeploymentObject, - state: DriveState.Value, - prevState: DriveState.Value - ): DriveState.Value = { - val out = super.DeploymentAction(obj, state, prevState) - obj match { - case vehicle: Vehicle => - val guid = vehicle.GUID - val zone = vehicle.Zone - val zoneChannel = zone.id - val GUID0 = Service.defaultPlayerGUID - val driverChannel = vehicle.Seats(0).occupant match { - case Some(tplayer) => tplayer.Name - case None => "" - } - Vehicles.ReloadAccessPermissions(vehicle, vehicle.Faction.toString) - //ams - if (vehicle.Definition == GlobalDefinitions.ams) { - val events = zone.VehicleEvents - state match { - case DriveState.Deployed => - events ! VehicleServiceMessage.AMSDeploymentChange(zone) - events ! VehicleServiceMessage(driverChannel, VehicleAction.PlanetsideAttribute(GUID0, guid, 81, 1)) - case _ => ; - } - } - //ant - else if (vehicle.Definition == GlobalDefinitions.ant) { - state match { - case DriveState.Deployed => - // Start ntu regeneration - // If vehicle sends UseItemMessage with silo as target NTU regeneration will be disabled and orb particles will be disabled - context.system.scheduler.scheduleOnce( - delay = 1000 milliseconds, - vehicle.Actor, - TransferBehavior.Charging(Ntu.Nanites) - ) - case _ => ; - } - } - //router - else if (vehicle.Definition == GlobalDefinitions.router) { - val events = zone.LocalEvents - state match { - case DriveState.Deploying => - vehicle.Utility(UtilityType.internal_router_telepad_deployable) match { - case Some(util: Utility.InternalTelepad) => util.Actor ! TelepadLike.Activate(util) - case _ => ; - } - case _ => ; - } - } - case _ => ; - } - out - } - - override def UndeploymentAction( - obj: DeploymentObject, - state: DriveState.Value, - prevState: DriveState.Value - ): DriveState.Value = { - val out = if (decaying) state else super.UndeploymentAction(obj, state, prevState) - obj match { - case vehicle: Vehicle => - val guid = vehicle.GUID - val zone = vehicle.Zone - val GUID0 = Service.defaultPlayerGUID - val driverChannel = vehicle.Seats(0).occupant match { - case Some(tplayer) => tplayer.Name - case None => "" - } - Vehicles.ReloadAccessPermissions(vehicle, vehicle.Faction.toString) - //ams - if (vehicle.Definition == GlobalDefinitions.ams) { - val events = zone.VehicleEvents - state match { - case DriveState.Undeploying => - events ! VehicleServiceMessage.AMSDeploymentChange(zone) - events ! VehicleServiceMessage(driverChannel, VehicleAction.PlanetsideAttribute(GUID0, guid, 81, 0)) - case _ => ; - } - } - //ant - else if (vehicle.Definition == GlobalDefinitions.ant) { - state match { - case DriveState.Undeploying => - TryStopChargingEvent(vehicle) - case _ => ; - } - } - //router - else if (vehicle.Definition == GlobalDefinitions.router) { - state match { - case DriveState.Undeploying => - //deactivate internal router before trying to reset the system - vehicle.Utility(UtilityType.internal_router_telepad_deployable) match { - case Some(util: Utility.InternalTelepad) => util.Actor ! TelepadLike.Deactivate(util) - case _ => ; - } - case _ => ; - } - } - case _ => ; - } - out - } - /** * Water causes vehicles to become disabled if they dive off too far, too deep. * Flying vehicles do not display progress towards being waterlogged. They just disable outright. @@ -895,8 +746,4 @@ object VehicleControl { case _ => false } } - - def DeploymentAngleCheck(obj: Deployment.DeploymentObject): Boolean = { - obj.Orientation.x <= 30 || obj.Orientation.x >= 330 - } } diff --git a/src/main/scala/net/psforever/objects/zones/ZoneVehicleActor.scala b/src/main/scala/net/psforever/objects/zones/ZoneVehicleActor.scala index c07e827c7..ea474375b 100644 --- a/src/main/scala/net/psforever/objects/zones/ZoneVehicleActor.scala +++ b/src/main/scala/net/psforever/objects/zones/ZoneVehicleActor.scala @@ -1,11 +1,9 @@ // Copyright (c) 2017 PSForever package net.psforever.objects.zones -import akka.actor.{Actor, Props} +import akka.actor.Actor import net.psforever.actors.zone.ZoneActor import net.psforever.objects.{Default, Vehicle} -import net.psforever.objects.serverobject.PlanetSideServerObject -import net.psforever.objects.vehicles.VehicleControl import scala.annotation.tailrec import scala.collection.mutable.ListBuffer @@ -42,8 +40,7 @@ class ZoneVehicleActor(zone: Zone, vehicleList: ListBuffer[Vehicle]) extends Act } else { vehicleList += vehicle vehicle.Zone = zone - vehicle.Actor = - context.actorOf(Props(classOf[VehicleControl], vehicle), PlanetSideServerObject.UniqueActorName(vehicle)) + vehicle.Definition.Initialize(vehicle, context) } if (vehicle.MountedIn.isEmpty) { zone.actor ! ZoneActor.AddToBlockMap(vehicle, vehicle.Position) @@ -54,8 +51,7 @@ class ZoneVehicleActor(zone: Zone, vehicleList: ListBuffer[Vehicle]) extends Act ZoneVehicleActor.recursiveFindVehicle(vehicleList.iterator, vehicle) match { case Some(index) => vehicleList.remove(index) - context.stop(vehicle.Actor) - vehicle.Actor = Default.Actor + vehicle.Definition.Uninitialize(vehicle, context) zone.actor ! ZoneActor.RemoveFromBlockMap(vehicle) sender() ! Zone.Vehicle.HasDespawned(zone, vehicle) case None => ; diff --git a/src/main/scala/net/psforever/packet/game/PlanetsideAttributeMessage.scala b/src/main/scala/net/psforever/packet/game/PlanetsideAttributeMessage.scala index f4951b633..a22fd9255 100644 --- a/src/main/scala/net/psforever/packet/game/PlanetsideAttributeMessage.scala +++ b/src/main/scala/net/psforever/packet/game/PlanetsideAttributeMessage.scala @@ -145,7 +145,7 @@ import scodec.codecs._ * `45 - NTU charge bar 0-10, 5 = 50% full. Seems to apply to both ANT and NTU Silo (possibly siphons?)`
* `46 - Sends "Generator damage is at a critical level!" message` * `47 - Sets base NTU level to CRITICAL.`
- * `48 - Set to 1 to send base power loss message & turns on red warning lights throughout base.
+ * `48 - Set to 1 to send base power loss message & turns on red warning lights throughout base.`
* `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`
@@ -191,6 +191,7 @@ import scodec.codecs._ * `21 - Declare a player the vehicle's owner, by globally unique identifier`
* `22 - Toggles gunner and passenger mount points (1 = hides, 0 = reveals; this also locks their permissions)`
* `54 - Plays jammed buzzing sound in vicinity of target`
+ * `55 - Trigger APC EMP`
* `68 - Vehicle shield health`
* `79 - ???`
* `80 - Damage vehicle (unknown value)`
diff --git a/src/test/scala/objects/DamageableTest.scala b/src/test/scala/objects/DamageableTest.scala index 4809d1531..cd0ee5896 100644 --- a/src/test/scala/objects/DamageableTest.scala +++ b/src/test/scala/objects/DamageableTest.scala @@ -3,7 +3,8 @@ package objects import akka.actor.Props import akka.testkit.TestProbe -import base.ActorTest +import base.{ActorTest, FreedContextActorTest} +import net.psforever.actors.zone.ZoneActor import net.psforever.objects._ import net.psforever.objects.ballistics._ import net.psforever.objects.equipment.JammableUnit @@ -15,7 +16,7 @@ import net.psforever.objects.serverobject.structures.{Building, StructureType} import net.psforever.objects.serverobject.terminals.{Terminal, TerminalControl, TerminalDefinition} import net.psforever.objects.serverobject.tube.SpawnTube import net.psforever.objects.serverobject.turret.{FacilityTurret, FacilityTurretControl, TurretUpgrade} -import net.psforever.objects.vehicles.VehicleControl +import net.psforever.objects.vehicles.control.VehicleControl import net.psforever.objects.vital.Vitality import net.psforever.objects.zones.{Zone, ZoneMap} import net.psforever.packet.game.DamageWithPositionMessage @@ -1222,7 +1223,7 @@ class DamageableVehicleDamageTest extends ActorTest { } } -class DamageableVehicleDamageMountedTest extends ActorTest { +class DamageableVehicleDamageMountedTest extends FreedContextActorTest { val guid = new NumberPoolHub(new MaxNumberSource(15)) val zone = new Zone("test", new ZoneMap("test"), 0) { override def SetupNumberPools() = {} @@ -1234,6 +1235,8 @@ class DamageableVehicleDamageMountedTest extends ActorTest { zone.Activity = activityProbe.ref zone.AvatarEvents = avatarProbe.ref zone.VehicleEvents = vehicleProbe.ref + import akka.actor.typed.scaladsl.adapter._ + zone.actor = new TestProbe(system).ref.toTyped[ZoneActor.Command] val lodestar = Vehicle(GlobalDefinitions.lodestar) //guid=1 & 4,5,6,7,8,9 lodestar.Position = Vector3(1, 0, 0) @@ -1271,7 +1274,7 @@ class DamageableVehicleDamageMountedTest extends ActorTest { guid.register(atv, 11) //the lodestar control actor needs to load after the utilities have guid's assigned - lodestar.Actor = system.actorOf(Props(classOf[VehicleControl], lodestar), "lodestar-control") + lodestar.Definition.Initialize(lodestar, context) lodestar.Zone = zone lodestar.Seats(0).mount(player2) player2.VehicleSeated = lodestar.GUID @@ -1314,50 +1317,38 @@ class DamageableVehicleDamageMountedTest extends ActorTest { assert(atv.Shields == 1) lodestar.Actor ! Vitality.Damage(applyDamageTo) - val msg12 = vehicleProbe.receiveN(2, 200 milliseconds) - val msg3 = activityProbe.receiveOne(200 milliseconds) - val msg45 = avatarProbe.receiveN(2,200 milliseconds) - assert( - msg12.head match { - case VehicleServiceMessage(_, VehicleAction.PlanetsideAttribute(PlanetSideGUID(0), PlanetSideGUID(1), 68, _)) => true - case _ => false - } - ) - assert( - msg12(1) match { - case VehicleServiceMessage("test", VehicleAction.PlanetsideAttribute(PlanetSideGUID(0), PlanetSideGUID(1), 0, _)) => true - case _ => false - } - ) - assert( - msg3 match { - case activity: Zone.HotSpot.Activity => - activity.attacker == pSource && - activity.defender == SourceEntry(lodestar) && - activity.location == Vector3(1, 0, 0) - case _ => false - } - ) - assert( - msg45.head match { - case AvatarServiceMessage( - "TestCharacter2", - AvatarAction.SendResponse(Service.defaultPlayerGUID, DamageWithPositionMessage(400, Vector3(2, 0, 0))) - ) => - true - case _ => false - } - ) - assert( - msg45(1) match { - case AvatarServiceMessage( - "TestCharacter3", - AvatarAction.SendResponse(Service.defaultPlayerGUID, DamageWithPositionMessage(0, Vector3(2, 0, 0))) - ) => - true - case _ => false - } - ) + val msg12 = vehicleProbe.receiveN(2, 500 milliseconds) + val msg3 = activityProbe.receiveOne(500 milliseconds) + val msg45 = avatarProbe.receiveN(2,500 milliseconds) + msg12.head match { + case VehicleServiceMessage(_, VehicleAction.PlanetsideAttribute(PlanetSideGUID(0), PlanetSideGUID(1), 68, _)) => ; + case _ => assert(false) + } + msg12(1) match { + case VehicleServiceMessage("test", VehicleAction.PlanetsideAttribute(PlanetSideGUID(0), PlanetSideGUID(1), 0, _)) => ; + case _ => assert(false) + } + msg3 match { + case activity: Zone.HotSpot.Activity => + assert(activity.attacker == pSource && + activity.defender == SourceEntry(lodestar) && + activity.location == Vector3(1, 0, 0)) + case _ => assert(false) + } + msg45.head match { + case AvatarServiceMessage( + "TestCharacter2", + AvatarAction.SendResponse(Service.defaultPlayerGUID, DamageWithPositionMessage(400, Vector3(2, 0, 0))) + ) => ; + case _ => assert(false) + } + msg45(1) match { + case AvatarServiceMessage( + "TestCharacter3", + AvatarAction.SendResponse(Service.defaultPlayerGUID, DamageWithPositionMessage(0, Vector3(2, 0, 0))) + ) => ; + case _ => assert(false) + } assert(lodestar.Health < lodestar.Definition.DefaultHealth) assert(lodestar.Shields == 0) assert(atv.Health == atv.Definition.DefaultHealth) @@ -1365,7 +1356,7 @@ class DamageableVehicleDamageMountedTest extends ActorTest { } } -class DamageableVehicleJammeringMountedTest extends ActorTest { +class DamageableVehicleJammeringMountedTest extends FreedContextActorTest { val guid = new NumberPoolHub(new MaxNumberSource(15)) val zone = new Zone("test", new ZoneMap("test"), 0) { override def SetupNumberPools() = {} @@ -1417,7 +1408,7 @@ class DamageableVehicleJammeringMountedTest extends ActorTest { guid.register(player2, 12) guid.register(player3, 13) - lodestar.Actor = system.actorOf(Props(classOf[VehicleControl], lodestar), "lodestar-control") + lodestar.Definition.Initialize(lodestar, context) atv.Zone = zone lodestar.Zone = zone atv.Seats(0).mount(player2) @@ -1585,7 +1576,7 @@ class DamageableVehicleDestroyTest extends ActorTest { } } -class DamageableVehicleDestroyMountedTest extends ActorTest { +class DamageableVehicleDestroyMountedTest extends FreedContextActorTest { val atv = Vehicle(GlobalDefinitions.quadassault) //guid=1 atv.Actor = system.actorOf(Props(classOf[VehicleControl], atv), "atv-control") atv.Position = Vector3(1, 0, 0) @@ -1611,19 +1602,22 @@ class DamageableVehicleDestroyMountedTest extends ActorTest { val player3Probe = TestProbe() player3.Actor = player3Probe.ref - + val activityProbe = TestProbe() + val avatarProbe = TestProbe() + val vehicleProbe = TestProbe() + val catchall = TestProbe() val guid = new NumberPoolHub(new MaxNumberSource(15)) val zone = new Zone("test", new ZoneMap("test"), 0) { override def SetupNumberPools() = {} GUID(guid) override def LivePlayers = List(player1, player2, player3) + override def Activity = activityProbe.ref + override def AvatarEvents = avatarProbe.ref + override def VehicleEvents = vehicleProbe.ref + override def tasks = catchall.ref + import akka.actor.typed.scaladsl.adapter._ + this.actor = catchall.ref.toTyped[ZoneActor.Command] } - val activityProbe = TestProbe() - val avatarProbe = TestProbe() - val vehicleProbe = TestProbe() - zone.Activity = activityProbe.ref - zone.AvatarEvents = avatarProbe.ref - zone.VehicleEvents = vehicleProbe.ref guid.register(atv, 1) guid.register(atvWeapon, 2) @@ -1639,7 +1633,7 @@ class DamageableVehicleDestroyMountedTest extends ActorTest { guid.register(player2, 12) guid.register(player3, 13) - lodestar.Actor = system.actorOf(Props(classOf[VehicleControl], lodestar), "lodestar-control") + lodestar.Definition.Initialize(lodestar, context) atv.Zone = zone lodestar.Zone = zone atv.Seats(0).mount(player2) diff --git a/src/test/scala/objects/RepairableTest.scala b/src/test/scala/objects/RepairableTest.scala index 83cde24a1..3d2c6fc38 100644 --- a/src/test/scala/objects/RepairableTest.scala +++ b/src/test/scala/objects/RepairableTest.scala @@ -13,7 +13,7 @@ import net.psforever.objects.serverobject.generator.{Generator, GeneratorControl import net.psforever.objects.serverobject.structures.{Building, StructureType} import net.psforever.objects.serverobject.terminals.{Terminal, TerminalControl} import net.psforever.objects.serverobject.turret.{FacilityTurret, FacilityTurretControl} -import net.psforever.objects.vehicles.VehicleControl +import net.psforever.objects.vehicles.control.VehicleControl import net.psforever.objects.zones.{Zone, ZoneMap} import net.psforever.packet.game.{InventoryStateMessage, RepairMessage} import net.psforever.types._ diff --git a/src/test/scala/objects/TelepadRouterTest.scala b/src/test/scala/objects/TelepadRouterTest.scala index b95c04af2..84f1b5a55 100644 --- a/src/test/scala/objects/TelepadRouterTest.scala +++ b/src/test/scala/objects/TelepadRouterTest.scala @@ -4,14 +4,14 @@ package objects import akka.actor.{ActorRef, Props} import akka.actor.typed.scaladsl.adapter._ import akka.testkit.TestProbe -import base.ActorTest +import base.{ActorTest, FreedContextActorTest} import net.psforever.actors.zone.ZoneActor import net.psforever.objects._ import net.psforever.objects.ce.{DeployableCategory, DeployedItem, TelepadLike} import net.psforever.objects.guid.NumberPoolHub import net.psforever.objects.guid.source.MaxNumberSource import net.psforever.objects.serverobject.deploy.Deployment -import net.psforever.objects.vehicles.{Utility, UtilityType, VehicleControl} +import net.psforever.objects.vehicles.{Utility, UtilityType} import net.psforever.objects.zones.{Zone, ZoneDeployableActor, ZoneMap} import net.psforever.packet.game.objectcreate.ObjectCreateMessageParent import net.psforever.packet.game._ @@ -214,7 +214,7 @@ class TelepadDeployableAttemptTest extends ActorTest { } } -class TelepadDeployableResponseFromRouterTest extends ActorTest { +class TelepadDeployableResponseFromRouterTest extends FreedContextActorTest { val eventsProbe = new TestProbe(system) val telepad = new TelepadDeployable(TelepadRouterTest.router_telepad_deployable) //guid=1 val router = Vehicle(GlobalDefinitions.router) //guid=2 @@ -242,17 +242,20 @@ class TelepadDeployableResponseFromRouterTest extends ActorTest { guid.register(internal, number = 3) guid.register(router.Utility(UtilityType.teleportpad_terminal).get, number = 4) //necessary router.Zone = zone - router.Actor = system.actorOf(Props(classOf[VehicleControl], router), "test-router") + router.Definition.Initialize(router, context) telepad.Router = PlanetSideGUID(2) //artificial "TelepadDeployable" should { "link with a connected router" in { assert(!telepad.Active, "link to router test - telepad active earlier than intended (1)") assert(!internal.Active, "link to router test - router internals active earlier than intended") - router.Actor.tell(Deployment.TryDeploy(DriveState.Deploying), new TestProbe(system).ref) + val deploymentProbe = new TestProbe(system) + router.Actor.tell(Deployment.TryDeploy(DriveState.Deploying), deploymentProbe.ref) eventsProbe.receiveN(10, 10.seconds) //flush all messages related to deployment + deploymentProbe.receiveOne(2.seconds) //CanDeploy + deploymentProbe.expectNoMessage(2.seconds) //intentional delay + assert(internal.Active, "link to router test - router internals not active when expected") assert(!telepad.Active, "link to router test - telepad active earlier than intended (2)") - assert(internal.Active, "link to router test - router internals active not active when expected") assert(deployableList.isEmpty, "link to router test - deployable list is not empty") zone.Deployables ! Zone.Deployable.Build(telepad) diff --git a/src/test/scala/objects/VehicleControlTest.scala b/src/test/scala/objects/VehicleControlTest.scala index 8ce6a880f..8379868ff 100644 --- a/src/test/scala/objects/VehicleControlTest.scala +++ b/src/test/scala/objects/VehicleControlTest.scala @@ -7,15 +7,17 @@ import akka.testkit.TestProbe import base.{ActorTest, FreedContextActorTest} import net.psforever.actors.zone.ZoneActor import net.psforever.objects.avatar.{Avatar, PlayerControl} -import net.psforever.objects.{GlobalDefinitions, Player, Vehicle} +import net.psforever.objects.ce.DeployedItem +import net.psforever.objects._ import net.psforever.objects.guid.NumberPoolHub import net.psforever.objects.guid.source.MaxNumberSource import net.psforever.objects.serverobject.environment._ import net.psforever.objects.serverobject.mount.Mountable -import net.psforever.objects.vehicles.{VehicleControl, VehicleLockState} -import net.psforever.objects.vital.VehicleShieldCharge +import net.psforever.objects.vehicles.VehicleLockState +import net.psforever.objects.vehicles.control.VehicleControl +import net.psforever.objects.vital.{VehicleShieldCharge, Vitality} import net.psforever.objects.zones.{Zone, ZoneMap} -import net.psforever.packet.game.{CargoMountPointStatusMessage, ObjectDetachMessage, PlanetsideAttributeMessage} +import net.psforever.packet.game._ import net.psforever.services.ServiceManager import net.psforever.services.avatar.{AvatarAction, AvatarServiceMessage} import net.psforever.services.vehicle.{VehicleAction, VehicleServiceMessage} @@ -201,12 +203,16 @@ class VehicleControlPrepareForDeletionMountedInTest extends FreedContextActorTes } class VehicleControlPrepareForDeletionMountedCargoTest extends FreedContextActorTest { + val vehicleProbe = new TestProbe(system) + val catchall = new TestProbe(system) val guid = new NumberPoolHub(new MaxNumberSource(10)) ServiceManager.boot val zone = new Zone("test", new ZoneMap("test"), 0) { GUID(guid) override def SetupNumberPools(): Unit = {} + override def VehicleEvents = vehicleProbe.ref + override def tasks = catchall.ref } zone.actor = system.spawn(ZoneActor(zone), "test-zone-actor") // crappy workaround but without it the zone doesn't get initialized in time @@ -219,6 +225,7 @@ class VehicleControlPrepareForDeletionMountedCargoTest extends FreedContextActor vehicle.Actor = cargoProbe.ref val lodestar = Vehicle(GlobalDefinitions.lodestar) lodestar.Faction = PlanetSideEmpire.TR + lodestar.Zone = zone val player1 = Player(VehicleTest.avatar1) //name="test1" val player2 = Player(VehicleTest.avatar2) //name="test2" @@ -237,85 +244,64 @@ class VehicleControlPrepareForDeletionMountedCargoTest extends FreedContextActor player2.VehicleSeated = lodestar.GUID lodestar.CargoHolds(1).mount(vehicle) vehicle.MountedIn = lodestar.GUID - - val vehicleProbe = new TestProbe(system) - zone.VehicleEvents = vehicleProbe.ref - zone.Transport ! Zone.Vehicle.Spawn(lodestar) //can not fake this + lodestar.Definition.Initialize(lodestar, context) "VehicleControl" should { "if with mounted cargo, eject it when marked for deconstruction" in { lodestar.Actor ! Vehicle.Deconstruct() val vehicle_msg = vehicleProbe.receiveN(6, 500 milliseconds) - assert( - vehicle_msg.head match { - case VehicleServiceMessage( + vehicle_msg(5) match { + case VehicleServiceMessage( "test", VehicleAction.KickPassenger(PlanetSideGUID(4), 4, false, PlanetSideGUID(2)) - ) => - true - case _ => false - } - ) + ) => ; + case _ => assert(false) + } assert(player2.VehicleSeated.isEmpty) assert(lodestar.Seats(0).occupant.isEmpty) //cargo dismounting messages - assert( - vehicle_msg(1) match { - case VehicleServiceMessage( + vehicle_msg.head match { + case VehicleServiceMessage( _, VehicleAction.SendResponse(_, PlanetsideAttributeMessage(PlanetSideGUID(1), 0, _)) - ) => - true - case _ => false - } - ) - assert( - vehicle_msg(2) match { - case VehicleServiceMessage( + ) => ; + case _ => assert(false) + } + vehicle_msg(1) match { + case VehicleServiceMessage( _, VehicleAction.SendResponse(_, PlanetsideAttributeMessage(PlanetSideGUID(1), 68, _)) - ) => - true - case _ => false - } - ) - assert( - vehicle_msg(3) match { - case VehicleServiceMessage( + ) => ; + case _ => assert(false) + } + vehicle_msg(2) match { + case VehicleServiceMessage( "test", VehicleAction.SendResponse( - _, - CargoMountPointStatusMessage(PlanetSideGUID(2), _, PlanetSideGUID(1), _, 1, CargoStatus.InProgress, 0) + _, + CargoMountPointStatusMessage(PlanetSideGUID(2), _, PlanetSideGUID(1), _, 1, CargoStatus.InProgress, 0) ) - ) => - true - case _ => false - } - ) - assert( - vehicle_msg(4) match { - case VehicleServiceMessage( + ) => ; + case _ => assert(false) + } + vehicle_msg(3) match { + case VehicleServiceMessage( "test", VehicleAction.SendResponse(_, ObjectDetachMessage(PlanetSideGUID(2), PlanetSideGUID(1), _, _, _, _)) - ) => - true - case _ => false - } - ) - assert( - vehicle_msg(5) match { - case VehicleServiceMessage( + ) => ; + case _ => assert(false) + } + vehicle_msg(4) match { + case VehicleServiceMessage( "test", VehicleAction.SendResponse( - _, - CargoMountPointStatusMessage(PlanetSideGUID(2), _, _, PlanetSideGUID(1), 1, CargoStatus.Empty, 0) + _, + CargoMountPointStatusMessage(PlanetSideGUID(2), _, _, PlanetSideGUID(1), 1, CargoStatus.Empty, 0) ) - ) => - true - case _ => false - } - ) + ) => ; + case _ => assert(false) + } } } } @@ -945,6 +931,142 @@ class VehicleControlInteractWithDeathTest extends ActorTest { } } +class ApcControlCanChargeCapacitor extends FreedContextActorTest { + val apc = Vehicle(GlobalDefinitions.apc_tr) //guid=1, weapons not registered + + val guid = new NumberPoolHub(new MaxNumberSource(max = 5)) + val localProbe = TestProbe() + val vehicleProbe = TestProbe() + val catchall = TestProbe() + val zone = new Zone(id = "test-zone", new ZoneMap(name = "test-map"), zoneNumber = 0) { + override def SetupNumberPools(): Unit = {} + GUID(guid) + override def Vehicles = List(apc) + override def VehicleEvents = vehicleProbe.ref + override def LocalEvents = localProbe.ref + override def AvatarEvents = catchall.ref + override def Activity = catchall.ref + } + + guid.register(apc, number = 1) + apc.Faction = PlanetSideEmpire.VS + apc.Zone = zone + //apc.Definition.Initialize(apc, context) //do later ... + zone.blockMap.addTo(apc) + + val maxCapacitor = apc.Definition.MaxCapacitor + + "ApcControl" should { + "charge its capacitors when initialized" in { + assert(apc.Capacitor == 0) + apc.Definition.Initialize(apc, context) + do { + val msg = vehicleProbe.receiveOne(3.seconds) + msg match { + case VehicleServiceMessage(_, VehicleAction.PlanetsideAttribute(_, PlanetSideGUID(1), 113, capacitance)) => + assert(capacitance > 0) + case _ => + assert(false) + } + } + while(apc.Capacitor < maxCapacitor) + vehicleProbe.expectNoMessage(5.seconds) + assert(apc.Capacitor == maxCapacitor) + } + } +} + +class ApcControlCanEmp extends FreedContextActorTest { + val apc = Vehicle(GlobalDefinitions.apc_vs) //guid=1, weapons not registered + val fury = Vehicle(GlobalDefinitions.fury) //guid=2, weapons not registered + val boomer = Deployables.Make(DeployedItem.boomer)() //guid=3, no trigger + val boomer2 = Deployables.Make(DeployedItem.boomer)() //guid=4, no trigger + + val guid = new NumberPoolHub(new MaxNumberSource(max = 5)) + val localProbe = TestProbe() + val vehicleProbe = TestProbe() + val catchall = TestProbe() + val zone = new Zone(id = "test-zone", new ZoneMap(name = "test-map"), zoneNumber = 0) { + override def SetupNumberPools(): Unit = {} + GUID(guid) + override def Vehicles = List(apc, fury) + override def DeployableList = List(boomer, boomer2) + override def VehicleEvents = vehicleProbe.ref + override def LocalEvents = localProbe.ref + override def AvatarEvents = catchall.ref + override def Activity = catchall.ref + } + + guid.register(apc, number = 1) + apc.Faction = PlanetSideEmpire.VS + apc.Zone = zone + apc.Capacitor = apc.Definition.MaxCapacitor + apc.Definition.Initialize(apc, context) + zone.blockMap.addTo(apc) + + val furyProbe = TestProbe() + guid.register(fury, number = 2) + fury.Position = Vector3(4, 0, 0) //within 15m of apc + fury.Faction = PlanetSideEmpire.TR + fury.Zone = zone + fury.Actor = furyProbe.ref + zone.blockMap.addTo(fury) + + val boomerProbe = TestProbe() + guid.register(boomer, number = 3) + boomer.Position = Vector3(0, 14, 0) //within 15m of apc + boomer.Faction = PlanetSideEmpire.TR + boomer.Zone = zone + boomer.Actor = boomerProbe.ref + zone.blockMap.addTo(boomer) + + val boomer2Probe = TestProbe() + guid.register(boomer2, number = 4) + boomer2.Position = Vector3(0, 30, 0) //beyond 15m of apc + boomer2.Faction = PlanetSideEmpire.TR + boomer2.Zone = zone + boomer2.Actor = boomer2Probe.ref + zone.blockMap.addTo(boomer2) + + "ApcControl" should { + "charge its capacitors when initialized" in { + assert(apc.Capacitor == apc.Definition.MaxCapacitor) + apc.Definition.Initialize(apc, context) + vehicleProbe.expectNoMessage(5.seconds) //the capacitor is max, so no charging is needed + + apc.Actor ! SpecialEmp.Burst() + val vehicleMsgs = vehicleProbe.receiveN(2, 500.milliseconds) + vehicleMsgs.head match { + case VehicleServiceMessage(_, VehicleAction.PlanetsideAttribute(_, PlanetSideGUID(1), 113, 0)) => ; + case _ => assert(false) + } + vehicleMsgs(1) match { + case VehicleServiceMessage( + "test-zone", + VehicleAction.SendResponse( + _, + TriggerEffectMessage(_, "apc_explosion_emp_vs", None, Some(TriggeredEffectLocation(Vector3.Zero, Vector3.Zero))) + ) + ) => ; + case _ => assert(false) + } + assert(apc.Capacitor == 0) + + val furyMsg = furyProbe.receiveOne(200.milliseconds) + furyMsg match { + case Vitality.Damage(_) => ; + case _ => assert(false) + } + val boomerMsg = boomerProbe.receiveOne(200.milliseconds) + boomerMsg match { + case Vitality.Damage(_) => ; + case _ => assert(false) + } + boomer2Probe.expectNoMessage(400.milliseconds) //out of range + } + } +} + object VehicleControlTest { import net.psforever.objects.avatar.Avatar import net.psforever.types.{CharacterSex, PlanetSideEmpire} diff --git a/src/test/scala/service/LocalServiceTest.scala b/src/test/scala/service/LocalServiceTest.scala index 774cc2d49..7c96d6ed1 100644 --- a/src/test/scala/service/LocalServiceTest.scala +++ b/src/test/scala/service/LocalServiceTest.scala @@ -7,7 +7,7 @@ import base.{ActorTest, FreedContextActorTest} import net.psforever.objects.{GlobalDefinitions, SensorDeployable, Vehicle} import net.psforever.objects.serverobject.PlanetSideServerObject import net.psforever.objects.serverobject.terminals.{ProximityTerminal, Terminal} -import net.psforever.objects.vehicles.VehicleControl +import net.psforever.objects.vehicles.control.VehicleControl import net.psforever.objects.zones.{Zone, ZoneMap} import net.psforever.packet.game._ import net.psforever.types.{PlanetSideEmpire, PlanetSideGUID, Vector3} diff --git a/src/test/scala/service/VehicleServiceTest.scala b/src/test/scala/service/VehicleServiceTest.scala index 63deec93a..691780b58 100644 --- a/src/test/scala/service/VehicleServiceTest.scala +++ b/src/test/scala/service/VehicleServiceTest.scala @@ -4,7 +4,7 @@ package service import akka.actor.Props import base.ActorTest import net.psforever.objects._ -import net.psforever.objects.vehicles.VehicleControl +import net.psforever.objects.vehicles.control.VehicleControl import net.psforever.objects.zones.Zone import net.psforever.types.{PlanetSideGUID, _} import net.psforever.services.{Service, ServiceManager}