From 9f12cfa6253406fc6fb341cb2ea847de669b9c59 Mon Sep 17 00:00:00 2001 From: Fate-JH Date: Mon, 27 Apr 2020 21:22:59 -0400 Subject: [PATCH] Vehicles Must Be Destroyed (#390) * internalized the actions of the VehicleRemover into the vehicle control agency; this required modifications to vehicle deconstruction messaging, but also had implications for the vehicle spawn pad * fixed tests; created a with-context ActorTest environment; hacked away the unnecessary aspects of VehicleRemover * changes to tests; simplifications to terminated cargoing abilities * removing unnecessary indirection from cargo handling at the end of a vehicle's life --- .../scala/net/psforever/objects/Vehicle.scala | 8 +- .../net/psforever/objects/Vehicles.scala | 9 +- .../damage/DamageableVehicle.scala | 5 +- .../pad/VehicleSpawnControl.scala | 25 +- .../VehicleSpawnControlConcealPlayer.scala | 2 +- .../VehicleSpawnControlDriverControl.scala | 7 +- .../VehicleSpawnControlLoadVehicle.scala | 3 +- .../VehicleSpawnControlSeatDriver.scala | 10 +- .../objects/vehicles/CargoBehavior.scala | 49 +- .../objects/vehicles/VehicleControl.scala | 101 ++++- .../main/scala/services/RemoverActor.scala | 5 +- .../account/AccountPersistenceService.scala | 7 +- .../services/vehicle/VehicleService.scala | 11 - .../vehicle/VehicleServiceMessage.scala | 1 - .../vehicle/VehicleServiceResponse.scala | 1 - .../vehicle/support/VehicleRemover.scala | 93 +--- .../scala/base/FreedContextActorTest.scala | 54 +++ .../test/scala/objects/DamageableTest.scala | 41 +- .../src/test/scala/objects/VehicleTest.scala | 424 +++++++++++++----- .../src/main/scala/WorldSessionActor.scala | 34 +- .../actor/objects/VehicleSpawnPadTest.scala | 9 +- 21 files changed, 553 insertions(+), 346 deletions(-) create mode 100644 common/src/test/scala/base/FreedContextActorTest.scala diff --git a/common/src/main/scala/net/psforever/objects/Vehicle.scala b/common/src/main/scala/net/psforever/objects/Vehicle.scala index 6ad779d7..c5c1ed9a 100644 --- a/common/src/main/scala/net/psforever/objects/Vehicle.scala +++ b/common/src/main/scala/net/psforever/objects/Vehicle.scala @@ -16,6 +16,7 @@ import net.psforever.objects.vital.{DamageResistanceModel, StandardResistancePro import net.psforever.types.{PlanetSideEmpire, PlanetSideGUID} import scala.annotation.tailrec +import scala.concurrent.duration.FiniteDuration /** * The server-side support object that represents a vehicle.
@@ -577,11 +578,12 @@ object Vehicle { final case class VehicleMessages(player : Player, response : Exchange) /** - * The `Vehicle` will become unresponsive to player activity. - * Usually, it does this to await deconstruction and clean-up. + * Initiate vehicle deconstruction. * @see `VehicleControl` + * @param time the delay before deconstruction should initiate; + * should initiate instantly when `None` */ - final case class PrepareForDeletion() + final case class Deconstruct(time : Option[FiniteDuration] = None) /** * The `Vehicle` will resume previous unresponsiveness to player activity. diff --git a/common/src/main/scala/net/psforever/objects/Vehicles.scala b/common/src/main/scala/net/psforever/objects/Vehicles.scala index 8a9ca35e..4725ed27 100644 --- a/common/src/main/scala/net/psforever/objects/Vehicles.scala +++ b/common/src/main/scala/net/psforever/objects/Vehicles.scala @@ -6,7 +6,7 @@ import net.psforever.objects.vehicles.{CargoBehavior, VehicleLockState} import net.psforever.objects.zones.Zone import net.psforever.packet.game.TriggeredSound import net.psforever.types.{DriveState, PlanetSideGUID} -import services.{RemoverActor, Service} +import services.Service import services.avatar.{AvatarAction, AvatarServiceMessage} import services.local.{LocalAction, LocalServiceMessage} import services.vehicle.{VehicleAction, VehicleServiceMessage} @@ -191,10 +191,10 @@ object Vehicles { case Some(cargo : Vehicle) => { cargo.Seats(0).Occupant match { case Some(cargoDriver: Player) => - CargoBehavior.HandleVehicleCargoDismount(target.Zone, cargoDriver.GUID, cargo.GUID, bailed = target.Flying, requestedByPassenger = false, kicked = true ) + CargoBehavior.HandleVehicleCargoDismount(target.Zone, cargo.GUID, bailed = target.Flying, requestedByPassenger = false, kicked = true ) case None => log.error("FinishHackingVehicle: vehicle in cargo hold missing driver") - CargoBehavior.HandleVehicleCargoDismount(hacker.GUID, cargo.GUID, cargo, target.GUID, target, false, false, true) + CargoBehavior.HandleVehicleCargoDismount(cargo.GUID, cargo, target.GUID, target, false, false, true) } } case None => ; @@ -215,8 +215,7 @@ object Vehicles { // If the vehicle can fly and is flying deconstruct it, and well played to whomever managed to hack a plane in mid air. I'm impressed. if(target.Definition.CanFly && target.Flying) { // todo: Should this force the vehicle to land in the same way as when a pilot bails with passengers on board? - zone.VehicleEvents ! VehicleServiceMessage.Decon(RemoverActor.ClearSpecific(List(target), zone)) - zone.VehicleEvents ! VehicleServiceMessage.Decon(RemoverActor.AddTask(target, zone, Some(0 seconds))) + target.Actor ! Vehicle.Deconstruct() } else { // Otherwise handle ownership transfer as normal // Remove ownership of our current vehicle, if we have one hacker.VehicleOwned match { diff --git a/common/src/main/scala/net/psforever/objects/serverobject/damage/DamageableVehicle.scala b/common/src/main/scala/net/psforever/objects/serverobject/damage/DamageableVehicle.scala index dce43d36..c0b6f2ff 100644 --- a/common/src/main/scala/net/psforever/objects/serverobject/damage/DamageableVehicle.scala +++ b/common/src/main/scala/net/psforever/objects/serverobject/damage/DamageableVehicle.scala @@ -8,7 +8,7 @@ import net.psforever.objects.serverobject.damage.Damageable.Target import net.psforever.objects.serverobject.deploy.Deployment import net.psforever.objects.vital.resolution.ResolutionCalculations import net.psforever.types.{DriveState, PlanetSideGUID} -import services.{RemoverActor, Service} +import services.Service import services.local.{LocalAction, LocalServiceMessage} import services.vehicle.{VehicleAction, VehicleService, VehicleServiceMessage} @@ -180,8 +180,7 @@ object DamageableVehicle { target.Shields = 0 zone.VehicleEvents ! VehicleServiceMessage(zone.Id, VehicleAction.PlanetsideAttribute(Service.defaultPlayerGUID, target.GUID, 68, 0)) } - zone.VehicleEvents ! VehicleServiceMessage.Decon(RemoverActor.ClearSpecific(List(target), zone)) - zone.VehicleEvents ! VehicleServiceMessage.Decon(RemoverActor.AddTask(target, zone, Some(1 minute))) + target.Actor ! Vehicle.Deconstruct(Some(1 minute)) target.ClearHistory() } } diff --git a/common/src/main/scala/net/psforever/objects/serverobject/pad/VehicleSpawnControl.scala b/common/src/main/scala/net/psforever/objects/serverobject/pad/VehicleSpawnControl.scala index 574c9d31..13733f01 100644 --- a/common/src/main/scala/net/psforever/objects/serverobject/pad/VehicleSpawnControl.scala +++ b/common/src/main/scala/net/psforever/objects/serverobject/pad/VehicleSpawnControl.scala @@ -6,6 +6,8 @@ import net.psforever.objects.serverobject.affinity.{FactionAffinity, FactionAffi import net.psforever.objects.serverobject.pad.process.{VehicleSpawnControlBase, VehicleSpawnControlConcealPlayer} import net.psforever.objects.zones.Zone import net.psforever.objects.{DefaultCancellable, Player, Vehicle} +import services.RemoverActor +import services.vehicle.VehicleServiceMessage import scala.annotation.tailrec import scala.concurrent.ExecutionContext.Implicits.global @@ -64,9 +66,7 @@ class VehicleSpawnControl(pad : VehicleSpawnPad) extends VehicleSpawnControlBase handleOrderFunc(VehicleSpawnControl.Order(player, vehicle)) } catch { - case _ : AssertionError if vehicle.HasGUID => //same as order being dropped - VehicleSpawnControl.DisposeSpawnedVehicle(vehicle, pad.Zone) - case _ : AssertionError => ; //shrug + case _ : AssertionError => ; //ehhh case e : Exception => //something unexpected e.printStackTrace() } @@ -143,7 +143,7 @@ class VehicleSpawnControl(pad : VehicleSpawnPad) extends VehicleSpawnControlBase pad.Zone.VehicleEvents ! VehicleSpawnPad.PeriodicReminder(name, VehicleSpawnPad.Reminders.Queue, Some(orders.length + 1)) } else { - VehicleSpawnControl.DisposeSpawnedVehicle(order, pad.Zone) + VehicleSpawnControl.DisposeVehicle(order.vehicle, pad.Zone) } } @@ -282,19 +282,24 @@ object VehicleSpawnControl { * Properly clean up a vehicle that has been registered and spawned into the game world. * Call this downstream of "`ConcealPlayer`". * @param entry the order being cancelled - * @param zone the continent on which the vehicle was registered + * @param zone the zone in which the vehicle is registered (should be located) */ def DisposeSpawnedVehicle(entry : VehicleSpawnControl.Order, zone: Zone) : Unit = { - DisposeSpawnedVehicle(entry.vehicle, zone) + DisposeVehicle(entry.vehicle, zone) zone.VehicleEvents ! VehicleSpawnPad.RevealPlayer(entry.DriverGUID) } /** * Properly clean up a vehicle that has been registered and spawned into the game world. - * @param vehicle the vehicle being cancelled - * @param zone the continent on which the vehicle was registered + * @param vehicle the vehicle being disposed + * @param zone the zone in which the vehicle is registered (should be located) */ - def DisposeSpawnedVehicle(vehicle : Vehicle, zone: Zone) : Unit = { - zone.VehicleEvents ! VehicleSpawnPad.DisposeVehicle(vehicle) + def DisposeVehicle(vehicle : Vehicle, zone : Zone) : Unit = { + if(zone.Vehicles.exists(_.GUID == vehicle.GUID)) { //already added to zone + vehicle.Actor ! Vehicle.Deconstruct() + } + else { //just registered to zone + zone.VehicleEvents ! VehicleServiceMessage.Decon(RemoverActor.AddTask(vehicle, zone, Some(0 seconds))) + } } } diff --git a/common/src/main/scala/net/psforever/objects/serverobject/pad/process/VehicleSpawnControlConcealPlayer.scala b/common/src/main/scala/net/psforever/objects/serverobject/pad/process/VehicleSpawnControlConcealPlayer.scala index 3663d8e6..fe5350c9 100644 --- a/common/src/main/scala/net/psforever/objects/serverobject/pad/process/VehicleSpawnControlConcealPlayer.scala +++ b/common/src/main/scala/net/psforever/objects/serverobject/pad/process/VehicleSpawnControlConcealPlayer.scala @@ -33,7 +33,7 @@ class VehicleSpawnControlConcealPlayer(pad : VehicleSpawnPad) extends VehicleSpa } else { trace(s"integral component lost; abort order fulfillment") - VehicleSpawnControl.DisposeSpawnedVehicle(order.vehicle, pad.Zone) + VehicleSpawnControl.DisposeVehicle(order.vehicle, pad.Zone) context.parent ! VehicleSpawnControl.ProcessControl.GetNewOrder } diff --git a/common/src/main/scala/net/psforever/objects/serverobject/pad/process/VehicleSpawnControlDriverControl.scala b/common/src/main/scala/net/psforever/objects/serverobject/pad/process/VehicleSpawnControlDriverControl.scala index edc0ba13..1fa79b0d 100644 --- a/common/src/main/scala/net/psforever/objects/serverobject/pad/process/VehicleSpawnControlDriverControl.scala +++ b/common/src/main/scala/net/psforever/objects/serverobject/pad/process/VehicleSpawnControlDriverControl.scala @@ -21,15 +21,12 @@ class VehicleSpawnControlDriverControl(pad : VehicleSpawnPad) extends VehicleSpa def receive : Receive = { case order @ VehicleSpawnControl.Order(driver, vehicle) => - if(vehicle.Health == 0) { - trace(s"vehicle was already destroyed; but, everything is fine") - } - if(vehicle.PassengerInSeat(driver).contains(0)) { + if(vehicle.Health > 0 && vehicle.PassengerInSeat(driver).contains(0)) { trace(s"returning control of ${vehicle.Definition.Name} to ${driver.Name}") pad.Zone.VehicleEvents ! VehicleSpawnPad.ServerVehicleOverrideEnd(driver.Name, vehicle, pad) } else { - trace(s"${driver.Name} is not seated in ${vehicle.Definition.Name}; vehicle controls have been locked") + trace(s"${driver.Name} is not seated in ${vehicle.Definition.Name}; vehicle controls might have been locked") } finalClear ! order diff --git a/common/src/main/scala/net/psforever/objects/serverobject/pad/process/VehicleSpawnControlLoadVehicle.scala b/common/src/main/scala/net/psforever/objects/serverobject/pad/process/VehicleSpawnControlLoadVehicle.scala index df8a5505..a2ec9212 100644 --- a/common/src/main/scala/net/psforever/objects/serverobject/pad/process/VehicleSpawnControlLoadVehicle.scala +++ b/common/src/main/scala/net/psforever/objects/serverobject/pad/process/VehicleSpawnControlLoadVehicle.scala @@ -4,6 +4,7 @@ package net.psforever.objects.serverobject.pad.process import akka.actor.Props import net.psforever.objects.GlobalDefinitions import net.psforever.objects.serverobject.pad.{VehicleSpawnControl, VehicleSpawnPad} +import net.psforever.objects.zones.Zone import net.psforever.types.Vector3 import scala.concurrent.ExecutionContext.Implicits.global @@ -36,7 +37,7 @@ class VehicleSpawnControlLoadVehicle(pad : VehicleSpawnPad) extends VehicleSpawn } else { trace("owner lost or vehicle in poor condition; abort order fulfillment") - VehicleSpawnControl.DisposeSpawnedVehicle(order, pad.Zone) + VehicleSpawnControl.DisposeVehicle(order.vehicle, pad.Zone) context.parent ! VehicleSpawnControl.ProcessControl.GetNewOrder } diff --git a/common/src/main/scala/net/psforever/objects/serverobject/pad/process/VehicleSpawnControlSeatDriver.scala b/common/src/main/scala/net/psforever/objects/serverobject/pad/process/VehicleSpawnControlSeatDriver.scala index 7a0e58d0..b58243ea 100644 --- a/common/src/main/scala/net/psforever/objects/serverobject/pad/process/VehicleSpawnControlSeatDriver.scala +++ b/common/src/main/scala/net/psforever/objects/serverobject/pad/process/VehicleSpawnControlSeatDriver.scala @@ -2,6 +2,7 @@ package net.psforever.objects.serverobject.pad.process import akka.actor.{ActorRef, Props} +import net.psforever.objects.Vehicle import net.psforever.objects.serverobject.pad.{VehicleSpawnControl, VehicleSpawnPad} import scala.concurrent.ExecutionContext.Implicits.global @@ -38,9 +39,12 @@ class VehicleSpawnControlSeatDriver(pad : VehicleSpawnPad) extends VehicleSpawnC case VehicleSpawnControlSeatDriver.BeginDriverInSeat(entry) => val driver = entry.driver - if(entry.vehicle.Health > 0 && driver.isAlive && driver.Continent == pad.Continent && driver.VehicleSeated.isEmpty) { + val vehicle = entry.vehicle + //avoid unattended vehicle blocking the pad; user should mount (and does so normally) to reset decon timer + vehicle.Actor ! Vehicle.Deconstruct(Some(30 seconds)) + if(vehicle.Health > 0 && driver.isAlive && driver.Continent == pad.Continent && driver.VehicleSeated.isEmpty) { trace("driver to be made seated in vehicle") - pad.Zone.VehicleEvents ! VehicleSpawnPad.StartPlayerSeatedInVehicle(entry.driver.Name, entry.vehicle, pad) + pad.Zone.VehicleEvents ! VehicleSpawnPad.StartPlayerSeatedInVehicle(driver.Name, vehicle, pad) } else{ trace("driver lost; vehicle stranded on pad") @@ -48,7 +52,7 @@ class VehicleSpawnControlSeatDriver(pad : VehicleSpawnPad) extends VehicleSpawnC context.system.scheduler.scheduleOnce(2500 milliseconds, self, VehicleSpawnControlSeatDriver.DriverInSeat(entry)) case VehicleSpawnControlSeatDriver.DriverInSeat(entry) => - if(entry.driver.isAlive && entry.vehicle.PassengerInSeat(entry.driver).contains(0)) { + if(entry.vehicle.Health > 0 && entry.driver.isAlive && entry.vehicle.PassengerInSeat(entry.driver).contains(0)) { trace(s"driver ${entry.driver.Name} has taken the wheel") pad.Zone.VehicleEvents ! VehicleSpawnPad.PlayerSeatedInVehicle(entry.driver.Name, entry.vehicle, pad) } diff --git a/common/src/main/scala/net/psforever/objects/vehicles/CargoBehavior.scala b/common/src/main/scala/net/psforever/objects/vehicles/CargoBehavior.scala index 689ffff6..5f7c9795 100644 --- a/common/src/main/scala/net/psforever/objects/vehicles/CargoBehavior.scala +++ b/common/src/main/scala/net/psforever/objects/vehicles/CargoBehavior.scala @@ -8,7 +8,7 @@ import net.psforever.objects.vehicles.CargoBehavior.{CheckCargoDismount, CheckCa import net.psforever.packet.game.{CargoMountPointStatusMessage, ObjectAttachMessage, ObjectDetachMessage, PlanetsideAttributeMessage} import net.psforever.types.{CargoStatus, PlanetSideGUID, Vector3} import services.avatar.{AvatarAction, AvatarServiceMessage} -import services.{RemoverActor, Service} +import services.Service import services.vehicle.{VehicleAction, VehicleServiceMessage} import scala.concurrent.duration._ @@ -231,18 +231,18 @@ object CargoBehavior { /** * na - * @param player_guid na + * @param zone na * @param cargo_guid na * @param bailed na * @param requestedByPassenger na * @param kicked na */ - def HandleVehicleCargoDismount(zone : Zone, player_guid : PlanetSideGUID, cargo_guid : PlanetSideGUID, bailed : Boolean, requestedByPassenger : Boolean, kicked : Boolean) : Unit = { + def HandleVehicleCargoDismount(zone : Zone, cargo_guid : PlanetSideGUID, bailed : Boolean, requestedByPassenger : Boolean, kicked : Boolean) : Unit = { zone.GUID(cargo_guid) match { case Some(cargo : Vehicle) => zone.GUID(cargo.MountedIn) match { case Some(ferry : Vehicle) => - HandleVehicleCargoDismount(player_guid, cargo_guid, cargo, ferry.GUID, ferry, bailed, requestedByPassenger, kicked) + HandleVehicleCargoDismount(cargo_guid, cargo, ferry.GUID, ferry, bailed, requestedByPassenger, kicked) case _ => log.warn(s"DismountVehicleCargo: target ${cargo.Definition.Name}@$cargo_guid does not know what treats it as cargo") } @@ -253,7 +253,6 @@ object CargoBehavior { /** * na - * @param player_guid the target player * @param cargoGUID the globally unique number for the vehicle being ferried * @param cargo the vehicle being ferried * @param carrierGUID the globally unique number for the vehicle doing the ferrying @@ -262,7 +261,7 @@ object CargoBehavior { * @param requestedByPassenger the ferried vehicle is being politely disembarked from the cargo hold * @param kicked the ferried vehicle is being kicked out of the cargo hold */ - def HandleVehicleCargoDismount(player_guid : PlanetSideGUID, cargoGUID : PlanetSideGUID, cargo : Vehicle, carrierGUID : PlanetSideGUID, carrier : Vehicle, bailed : Boolean, requestedByPassenger : Boolean, kicked : Boolean) : Unit = { + def HandleVehicleCargoDismount(cargoGUID : PlanetSideGUID, cargo : Vehicle, carrierGUID : PlanetSideGUID, carrier : Vehicle, bailed : Boolean, requestedByPassenger : Boolean, kicked : Boolean) : Unit = { val zone = carrier.Zone carrier.CargoHolds.find({case(_, hold) => hold.Occupant.contains(cargo)}) match { case Some((mountPoint, hold)) => @@ -284,41 +283,43 @@ object CargoBehavior { //the lodestar's cargo hold is almost the center of the vehicle carrier.Position } - zone.VehicleEvents ! VehicleServiceMessage(s"${cargo.Actor}", VehicleAction.SendResponse(PlanetSideGUID(0), PlanetsideAttributeMessage(cargoGUID, 0, cargo.Health))) - zone.VehicleEvents ! VehicleServiceMessage(s"${cargo.Actor}", VehicleAction.SendResponse(PlanetSideGUID(0), PlanetsideAttributeMessage(cargoGUID, 68, cargo.Shields))) + val GUID0 = Service.defaultPlayerGUID + val zoneId = zone.Id + val events = zone.VehicleEvents + val cargoActor = cargo.Actor + events ! VehicleServiceMessage(s"$cargoActor", VehicleAction.SendResponse(GUID0, PlanetsideAttributeMessage(cargoGUID, 0, cargo.Health))) + events ! VehicleServiceMessage(s"$cargoActor", VehicleAction.SendResponse(GUID0, PlanetsideAttributeMessage(cargoGUID, 68, cargo.Shields))) if(carrier.Flying) { //the carrier vehicle is flying; eject the cargo vehicle - val ejectCargoMsg = CargoMountPointStatusMessage(carrierGUID, PlanetSideGUID(0), PlanetSideGUID(0), cargoGUID, mountPoint, CargoStatus.InProgress, 0) + val ejectCargoMsg = CargoMountPointStatusMessage(carrierGUID, GUID0, GUID0, cargoGUID, mountPoint, CargoStatus.InProgress, 0) val detachCargoMsg = ObjectDetachMessage(carrierGUID, cargoGUID, cargoHoldPosition - Vector3.z(1), rotation) - val resetCargoMsg = CargoMountPointStatusMessage(carrierGUID, PlanetSideGUID(0), PlanetSideGUID(0), cargoGUID, mountPoint, CargoStatus.Empty, 0) - zone.VehicleEvents ! VehicleServiceMessage(zone.Id, VehicleAction.SendResponse(Service.defaultPlayerGUID, ejectCargoMsg)) - zone.VehicleEvents ! VehicleServiceMessage(zone.Id, VehicleAction.SendResponse(Service.defaultPlayerGUID, detachCargoMsg)) - zone.VehicleEvents ! VehicleServiceMessage(zone.Id, VehicleAction.SendResponse(Service.defaultPlayerGUID, resetCargoMsg)) + val resetCargoMsg = CargoMountPointStatusMessage(carrierGUID, GUID0, GUID0, cargoGUID, mountPoint, CargoStatus.Empty, 0) + events ! VehicleServiceMessage(zoneId, VehicleAction.SendResponse(GUID0, ejectCargoMsg)) + events ! VehicleServiceMessage(zoneId, VehicleAction.SendResponse(GUID0, detachCargoMsg)) + events ! VehicleServiceMessage(zoneId, VehicleAction.SendResponse(GUID0, resetCargoMsg)) log.debug(ejectCargoMsg.toString) log.debug(detachCargoMsg.toString) if(driverOpt.isEmpty) { //TODO cargo should drop like a rock like normal; until then, deconstruct it - zone.VehicleEvents ! VehicleServiceMessage.Decon(RemoverActor.ClearSpecific(List(cargo), zone)) - zone.VehicleEvents ! VehicleServiceMessage.Decon(RemoverActor.AddTask(cargo, zone, Some(0 seconds))) + cargo.Actor ! Vehicle.Deconstruct() } } else { //the carrier vehicle is not flying; just open the door and let the cargo vehicle back out; force it out if necessary - val cargoStatusMessage = CargoMountPointStatusMessage(carrierGUID, PlanetSideGUID(0), cargoGUID, PlanetSideGUID(0), mountPoint, CargoStatus.InProgress, 0) + val cargoStatusMessage = CargoMountPointStatusMessage(carrierGUID, GUID0, cargoGUID, GUID0, mountPoint, CargoStatus.InProgress, 0) val cargoDetachMessage = ObjectDetachMessage(carrierGUID, cargoGUID, cargoHoldPosition + Vector3.z(1f), rotation) - zone.VehicleEvents ! VehicleServiceMessage(zone.Id, VehicleAction.SendResponse(Service.defaultPlayerGUID, cargoStatusMessage)) - zone.VehicleEvents ! VehicleServiceMessage(zone.Id, VehicleAction.SendResponse(Service.defaultPlayerGUID, cargoDetachMessage)) + events ! VehicleServiceMessage(zoneId, VehicleAction.SendResponse(GUID0, cargoStatusMessage)) + events ! VehicleServiceMessage(zoneId, VehicleAction.SendResponse(GUID0, cargoDetachMessage)) driverOpt match { case Some(driver) => - zone.VehicleEvents ! VehicleServiceMessage(s"${driver.Name}", VehicleAction.KickCargo(player_guid, cargo, cargo.Definition.AutoPilotSpeed2, 2500)) + events ! VehicleServiceMessage(s"${driver.Name}", VehicleAction.KickCargo(GUID0, cargo, cargo.Definition.AutoPilotSpeed2, 2500)) //check every quarter second if the vehicle has moved far enough away to be considered dismounted - cargo.Actor ! CheckCargoDismount(carrierGUID, mountPoint, 0) + cargoActor ! CheckCargoDismount(carrierGUID, mountPoint, 0) case None => - val resetCargoMsg = CargoMountPointStatusMessage(carrierGUID, PlanetSideGUID(0), PlanetSideGUID(0), cargoGUID, mountPoint, CargoStatus.Empty, 0) - zone.VehicleEvents ! VehicleServiceMessage(zone.Id, VehicleAction.SendResponse(PlanetSideGUID(0), resetCargoMsg)) //lazy + val resetCargoMsg = CargoMountPointStatusMessage(carrierGUID, GUID0, GUID0, cargoGUID, mountPoint, CargoStatus.Empty, 0) + events ! VehicleServiceMessage(zoneId, VehicleAction.SendResponse(GUID0, resetCargoMsg)) //lazy //TODO cargo should back out like normal; until then, deconstruct it - zone.VehicleEvents ! VehicleServiceMessage.Decon(RemoverActor.ClearSpecific(List(cargo), zone)) - zone.VehicleEvents ! VehicleServiceMessage.Decon(RemoverActor.AddTask(cargo, zone, Some(0 seconds))) + cargoActor ! Vehicle.Deconstruct() } } diff --git a/common/src/main/scala/net/psforever/objects/vehicles/VehicleControl.scala b/common/src/main/scala/net/psforever/objects/vehicles/VehicleControl.scala index 8d3f315d..9f4e0ed9 100644 --- a/common/src/main/scala/net/psforever/objects/vehicles/VehicleControl.scala +++ b/common/src/main/scala/net/psforever/objects/vehicles/VehicleControl.scala @@ -1,8 +1,8 @@ // Copyright (c) 2017 PSForever package net.psforever.objects.vehicles -import akka.actor.{Actor, ActorRef} -import net.psforever.objects.{GlobalDefinitions, SimpleItem, Vehicle, Vehicles} +import akka.actor.{Actor, ActorRef, Cancellable} +import net.psforever.objects.{DefaultCancellable, GlobalDefinitions, SimpleItem, Vehicle, Vehicles} import net.psforever.objects.ballistics.{ResolvedProjectile, VehicleSource} import net.psforever.objects.equipment.JammableMountedWeapons import net.psforever.objects.serverobject.CommonMessages @@ -13,9 +13,14 @@ import net.psforever.objects.serverobject.deploy.DeploymentBehavior import net.psforever.objects.serverobject.hackable.GenericHackables import net.psforever.objects.serverobject.repair.RepairableVehicle import net.psforever.objects.vital.VehicleShieldCharge -import net.psforever.types.{ExoSuitType, PlanetSideGUID} +import net.psforever.objects.zones.Zone +import net.psforever.types.{DriveState, ExoSuitType, PlanetSideGUID, Vector3} +import services.{RemoverActor, Service} import services.vehicle.{VehicleAction, VehicleServiceMessage} +import scala.concurrent.ExecutionContext.Implicits.global +import scala.concurrent.duration._ + /** * An `Actor` that handles messages being dispatched to a specific `Vehicle`.
*
@@ -44,10 +49,17 @@ class VehicleControl(vehicle : Vehicle) extends Actor def DamageableObject = vehicle def RepairableObject = vehicle + /** cheap flag for whether the vehicle is decaying */ + var decaying : Boolean = false + /** primary vehicle decay timer */ + var decayTimer : Cancellable = DefaultCancellable.obj + def receive : Receive = Enabled override def postStop() : Unit = { super.postStop() + decaying = false + decayTimer.cancel vehicle.Utilities.values.foreach { util => context.stop(util().Actor) util().Actor = ActorRef.noSender @@ -63,9 +75,18 @@ class VehicleControl(vehicle : Vehicle) extends Actor .orElse { case msg : Mountable.TryMount => tryMountBehavior.apply(msg) + if(MountableObject.Seats.values.exists(_.isOccupied)) { + decaying = false + decayTimer.cancel + } case msg : Mountable.TryDismount => dismountBehavior.apply(msg) + val obj = MountableObject + if(!decaying && obj.Owner.isEmpty && obj.Seats.values.forall(!_.isOccupied)) { + decaying = true + decayTimer = context.system.scheduler.scheduleOnce(MountableObject.Definition.DeconstructionTime.getOrElse(5 minutes), self, VehicleControl.PrepareForDeletion()) + } case Vehicle.ChargeShields(amount) => val now : Long = System.nanoTime @@ -94,10 +115,18 @@ class VehicleControl(vehicle : Vehicle) extends Actor ) } - case Vehicle.PrepareForDeletion() => - CancelJammeredSound(vehicle) - CancelJammeredStatus(vehicle) - context.become(Disabled) + case Vehicle.Deconstruct(time) => + time match { + case Some(delay) => + decaying = true + decayTimer.cancel + decayTimer = context.system.scheduler.scheduleOnce(delay, self, VehicleControl.PrepareForDeletion()) + case _ => + PrepareForDeletion() + } + + case VehicleControl.PrepareForDeletion() => + PrepareForDeletion() case _ => ; } @@ -128,14 +157,56 @@ class VehicleControl(vehicle : Vehicle) extends Actor } } + def PrepareForDeletion() : Unit = { + decaying = false + val guid = vehicle.GUID + val zone = vehicle.Zone + val zoneId = zone.Id + val events = zone.VehicleEvents + //become disabled + context.become(Disabled) + //cancel jammed behavior + CancelJammeredSound(vehicle) + CancelJammeredStatus(vehicle) + //escape being someone else's cargo + vehicle.MountedIn match { + case Some(_) => + CargoBehavior.HandleVehicleCargoDismount(zone, guid, bailed = false, requestedByPassenger = false, kicked = false) + case _ => ; + } + //kick all passengers + vehicle.Seats.values.foreach(seat => { + seat.Occupant match { + case Some(player) => + seat.Occupant = None + player.VehicleSeated = None + if(player.HasGUID) { + events ! VehicleServiceMessage(zoneId, VehicleAction.KickPassenger(player.GUID, 4, false, guid)) + } + case None => ; + } + //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) + } + }) + //unregister + events ! VehicleServiceMessage.Decon(RemoverActor.AddTask(vehicle, zone, Some(0 seconds))) + //banished to the shadow realm + vehicle.Position = Vector3.Zero + vehicle.DeploymentState = DriveState.Mobile + //queue final deletion + decayTimer = context.system.scheduler.scheduleOnce(5 seconds, self, VehicleControl.Deletion()) + } + def Disabled : Receive = checkBehavior .orElse { - case msg : Mountable.TryDismount => - dismountBehavior.apply(msg) - - case Vehicle.Reactivate() => - context.become(Enabled) - + case VehicleControl.Deletion() => + val zone = vehicle.Zone + zone.VehicleEvents ! VehicleServiceMessage(zone.Id, VehicleAction.UnloadVehicle(Service.defaultPlayerGUID, zone, vehicle, vehicle.GUID)) + zone.Transport ! Zone.Vehicle.Despawn(vehicle) case _ => } @@ -150,6 +221,10 @@ object VehicleControl { import net.psforever.objects.vital.{DamageFromProjectile, VehicleShieldCharge, VitalsActivity} import scala.concurrent.duration._ + private case class PrepareForDeletion() + + private case class Deletion() + /** * Determine if a given activity entry would invalidate the act of charging vehicle shields this tick. * @param now the current time (in nanoseconds) diff --git a/common/src/main/scala/services/RemoverActor.scala b/common/src/main/scala/services/RemoverActor.scala index 1f9f7371..58415aa5 100644 --- a/common/src/main/scala/services/RemoverActor.scala +++ b/common/src/main/scala/services/RemoverActor.scala @@ -61,7 +61,7 @@ abstract class RemoverActor extends SupportActor[RemoverActor.Entry] { */ override def preStart() : Unit = { super.preStart() - self ! Service.Startup() + ServiceManager.serviceManager ! ServiceManager.Lookup("taskResolver") //ask for a resolver to deal with the GUID system } /** @@ -84,9 +84,6 @@ abstract class RemoverActor extends SupportActor[RemoverActor.Entry] { } def receive : Receive = { - case Service.Startup() => - ServiceManager.serviceManager ! ServiceManager.Lookup("taskResolver") //ask for a resolver to deal with the GUID system - case ServiceManager.LookupResult("taskResolver", endpoint) => taskResolver = endpoint context.become(Processing) diff --git a/common/src/main/scala/services/account/AccountPersistenceService.scala b/common/src/main/scala/services/account/AccountPersistenceService.scala index 32c2c3d2..bbc261f2 100644 --- a/common/src/main/scala/services/account/AccountPersistenceService.scala +++ b/common/src/main/scala/services/account/AccountPersistenceService.scala @@ -348,16 +348,15 @@ class PersistenceMonitor(name : String, squadService : ActorRef, taskResolver : def DisownVehicle(player : Player) : Unit = { Vehicles.Disown(player, inZone) match { case Some(vehicle) if vehicle.Health == 0 || (vehicle.Seats.values.forall(seat => !seat.isOccupied) && vehicle.Owner.isEmpty) => - inZone.VehicleEvents ! VehicleServiceMessage.Decon(RemoverActor.ClearSpecific(List(vehicle), inZone)) - inZone.VehicleEvents ! VehicleServiceMessage.Decon(RemoverActor.AddTask(vehicle, inZone, + vehicle.Actor ! Vehicle.Deconstruct( if(vehicle.Flying) { //TODO gravity - Some(0 seconds) //immediate deconstruction + None //immediate deconstruction } else { vehicle.Definition.DeconstructionTime //normal deconstruction } - )) + ) case _ => ; } } diff --git a/common/src/main/scala/services/vehicle/VehicleService.scala b/common/src/main/scala/services/vehicle/VehicleService.scala index bad2bb0b..d535056e 100644 --- a/common/src/main/scala/services/vehicle/VehicleService.scala +++ b/common/src/main/scala/services/vehicle/VehicleService.scala @@ -137,11 +137,6 @@ class VehicleService(zone : Zone) extends Actor { VehicleEvents.publish( VehicleServiceResponse(s"/$forChannel/Vehicle", player_guid, VehicleResponse.TransferPassengerChannel(old_channel, temp_channel, vehicle, vehicle_to_delete)) ) - - case VehicleAction.ForceDismountVehicleCargo(player_guid, vehicle_guid, bailed, requestedByPassenger, kicked) => - VehicleEvents.publish( - VehicleServiceResponse(s"/$forChannel/Vehicle", player_guid, VehicleResponse.ForceDismountVehicleCargo(vehicle_guid, bailed, requestedByPassenger, kicked)) - ) case VehicleAction.KickCargo(player_guid, cargo, speed, delay) => VehicleEvents.publish( VehicleServiceResponse(s"/$forChannel/Vehicle", player_guid, VehicleResponse.KickCargo(cargo, speed, delay)) @@ -216,12 +211,6 @@ class VehicleService(zone : Zone) extends Actor { VehicleEvents.publish( VehicleServiceResponse(s"/${zone.Id}/Vehicle", Service.defaultPlayerGUID, VehicleResponse.LoadVehicle(vehicle, vtype, vguid, vdata)) ) - //avoid unattended vehicle spawning blocking the pad; user should mount (and does so normally) to reset decon timer - vehicleDecon forward RemoverActor.AddTask(vehicle, zone, Some(30 seconds)) - - case VehicleSpawnPad.DisposeVehicle(vehicle) => - vehicleDecon forward RemoverActor.AddTask(vehicle, zone, Some(0 seconds)) - vehicleDecon forward RemoverActor.HurrySpecific(List(vehicle), zone) //correspondence from WorldSessionActor case VehicleServiceMessage.AMSDeploymentChange(_) => diff --git a/common/src/main/scala/services/vehicle/VehicleServiceMessage.scala b/common/src/main/scala/services/vehicle/VehicleServiceMessage.scala index 931ce8d8..8d101911 100644 --- a/common/src/main/scala/services/vehicle/VehicleServiceMessage.scala +++ b/common/src/main/scala/services/vehicle/VehicleServiceMessage.scala @@ -46,6 +46,5 @@ object VehicleAction { final case class TransferPassengerChannel(player_guid : PlanetSideGUID, temp_channel : String, new_channel : String, vehicle : Vehicle, vehicle_to_delete : PlanetSideGUID) extends Action - final case class ForceDismountVehicleCargo(player_guid : PlanetSideGUID, vehicle_guid : PlanetSideGUID, bailed : Boolean, requestedByPassenger : Boolean, kicked : Boolean) extends Action final case class KickCargo(player_guid : PlanetSideGUID, cargo : Vehicle, speed : Int, delay : Long) extends Action } diff --git a/common/src/main/scala/services/vehicle/VehicleServiceResponse.scala b/common/src/main/scala/services/vehicle/VehicleServiceResponse.scala index 8075e8c8..68834cc4 100644 --- a/common/src/main/scala/services/vehicle/VehicleServiceResponse.scala +++ b/common/src/main/scala/services/vehicle/VehicleServiceResponse.scala @@ -52,6 +52,5 @@ object VehicleResponse { final case class TransferPassengerChannel(old_channel : String, temp_channel : String, vehicle : Vehicle, vehicle_to_delete : PlanetSideGUID) extends Response - final case class ForceDismountVehicleCargo(vehicle_guid : PlanetSideGUID, bailed : Boolean, requestedByPassenger : Boolean, kicked : Boolean) extends Response final case class KickCargo(cargo : Vehicle, speed : Int, delay : Long) extends Response } diff --git a/common/src/main/scala/services/vehicle/support/VehicleRemover.scala b/common/src/main/scala/services/vehicle/support/VehicleRemover.scala index 3637393a..1f57e85f 100644 --- a/common/src/main/scala/services/vehicle/support/VehicleRemover.scala +++ b/common/src/main/scala/services/vehicle/support/VehicleRemover.scala @@ -1,88 +1,31 @@ -// Copyright (c) 2017 PSForever +// Copyright (c) 2017-2020 PSForever package services.vehicle.support -import akka.actor.ActorRef +import akka.actor.{Actor, ActorRef} import net.psforever.objects.Vehicle -import net.psforever.objects.guid.{GUIDTask, TaskResolver} -import net.psforever.objects.zones.Zone -import net.psforever.types.{DriveState, PlanetSideGUID} -import services.{RemoverActor, Service} -import services.vehicle.{VehicleAction, VehicleServiceMessage} +import net.psforever.objects.guid.GUIDTask.UnregisterVehicle +import services.{RemoverActor, ServiceManager} -import scala.concurrent.duration._ +class VehicleRemover extends Actor { + var taskResolver : ActorRef = Actor.noSender -class VehicleRemover extends RemoverActor { - final val FirstStandardDuration : FiniteDuration = 5 minutes - - final val SecondStandardDuration : FiniteDuration = 5 seconds - - def InclusionTest(entry : RemoverActor.Entry) : Boolean = { - entry.obj.isInstanceOf[Vehicle] + override def preStart() : Unit = { + super.preStart() + ServiceManager.serviceManager ! ServiceManager.Lookup("taskResolver") //ask for a resolver to deal with the GUID system } - def InitialJob(entry : RemoverActor.Entry) : Unit = { } + def receive : Receive = { + case ServiceManager.LookupResult("taskResolver", endpoint) => + taskResolver = endpoint + context.become(Processing) - def FirstJob(entry : RemoverActor.Entry) : Unit = { - val vehicleGUID = entry.obj.GUID - entry.zone.GUID(vehicleGUID) match { - case Some(vehicle : Vehicle) if vehicle.HasGUID => - val zoneId = entry.zone.Id - if(vehicle.Actor != ActorRef.noSender) { - vehicle.Actor ! Vehicle.PrepareForDeletion() - } - //escape being someone else's cargo - (vehicle.MountedIn match { - case Some(carrierGUID) => - entry.zone.Vehicles.find(v => v.GUID == carrierGUID) - case None => - None - }) match { - case Some(carrier : Vehicle) => - val driverName = carrier.Seats(0).Occupant match { - case Some(driver) => driver.Name - case _ => zoneId - } - context.parent ! VehicleServiceMessage(s"$driverName", VehicleAction.ForceDismountVehicleCargo(PlanetSideGUID(0), vehicleGUID, true, false, false)) - case _ => ; - } - //kick out all passengers - vehicle.Seats.values.foreach(seat => { - seat.Occupant match { - case Some(tplayer) => - seat.Occupant = None - tplayer.VehicleSeated = None - if(tplayer.HasGUID) { - context.parent ! VehicleServiceMessage(zoneId, VehicleAction.KickPassenger(tplayer.GUID, 4, false, vehicleGUID)) - } - case None => ; - } - //abandon all cargo - vehicle.CargoHolds.values - .collect { case hold if hold.isOccupied => - val cargo = hold.Occupant.get - context.parent ! VehicleServiceMessage(zoneId, VehicleAction.ForceDismountVehicleCargo(PlanetSideGUID(0), cargo.GUID, true, false, false)) - } - }) - case _ => ; - } + case _ => ; } - override def SecondJob(entry : RemoverActor.Entry) : Unit = { - val vehicleGUID = entry.obj.GUID - entry.zone.GUID(vehicleGUID) match { - case Some(vehicle : Vehicle) if vehicle.HasGUID => - val zone = entry.zone - vehicle.DeploymentState = DriveState.Mobile - zone.Transport ! Zone.Vehicle.Despawn(vehicle) - context.parent ! VehicleServiceMessage(zone.Id, VehicleAction.UnloadVehicle(Service.defaultPlayerGUID, zone, vehicle, vehicleGUID)) - super.SecondJob(entry) - case _ => ; - } - } + def Processing : Receive = { + case RemoverActor.AddTask(obj : Vehicle, zone, _) => + taskResolver ! UnregisterVehicle(obj)(zone.GUID) - def ClearanceTest(entry : RemoverActor.Entry) : Boolean = entry.obj.asInstanceOf[Vehicle].Seats.values.count(_.isOccupied) == 0 - - def DeletionTask(entry : RemoverActor.Entry) : TaskResolver.GiveTask = { - GUIDTask.UnregisterVehicle(entry.obj.asInstanceOf[Vehicle])(entry.zone.GUID) + case _ => ; } } diff --git a/common/src/test/scala/base/FreedContextActorTest.scala b/common/src/test/scala/base/FreedContextActorTest.scala new file mode 100644 index 00000000..759b85fe --- /dev/null +++ b/common/src/test/scala/base/FreedContextActorTest.scala @@ -0,0 +1,54 @@ +// Copyright (c) 2020 PSForever +package base + +import akka.actor.{Actor, ActorContext, ActorRef, Props} +import akka.pattern.ask +import akka.util.Timeout + +import scala.concurrent.duration._ +import scala.concurrent.Await + +/** + * Create an `ActorTest` environment that has an `ActorContext` object. + */ +abstract class FreedContextActorTest extends ActorTest { + /* + Never do this in actual production code! + ActorSystem and ActorContext offer similar mechanisms for instantiating actors. + This is a consequence of their shared inheritance of the ActorRefFactory trait. + They are not equivalent enough to be able to pass one as the other as a parameter. + Because the ActorSystem has no context of its own, + various bizarre mechanisms have to be developed to use any methods that would pass in a context object. + We create a middleman Actor whose main purpose is to surrender its context object to the test environment directly + and then direct all messages sent to that object to the test environment. + */ + private val _testContextHandler = system.actorOf(Props(classOf[ContextSensitive]), "actor-test-cs") + private implicit val timeout = Timeout(5 seconds) + private val _testContextHandlerResult = ask(_testContextHandler, message = "", self) + implicit val context = Await.result(_testContextHandlerResult, timeout.duration).asInstanceOf[ActorContext] +} + +/** + * Surrender your `context` object for a greater good! + */ +private class ContextSensitive extends Actor { + var output : ActorRef = ActorRef.noSender + + def receive : Receive = { + case _ => + context.become(PassThroughBehavior) + output = sender + sender ! context + } + + /** + * Once the `context` object has been leased, + * this `Actor` becomes transparent. + * Calling `context.parent` from whatever `Actor` was spurned by the previously provided `context`, + * will now refer to whatever was the contact to gain access to it - the test environment. + * @return something to `become` + */ + def PassThroughBehavior : Receive = { + case msg => output forward msg + } +} diff --git a/common/src/test/scala/objects/DamageableTest.scala b/common/src/test/scala/objects/DamageableTest.scala index b857f001..6facd258 100644 --- a/common/src/test/scala/objects/DamageableTest.scala +++ b/common/src/test/scala/objects/DamageableTest.scala @@ -21,7 +21,7 @@ import net.psforever.objects.vital.Vitality import net.psforever.objects.zones.{Zone, ZoneMap} import net.psforever.packet.game.DamageWithPositionMessage import net.psforever.types._ -import services.{RemoverActor, Service} +import services.Service import services.avatar.{AvatarAction, AvatarServiceMessage} import services.support.SupportActor import services.vehicle.support.TurretUpgrader @@ -1307,7 +1307,6 @@ class DamageableVehicleDestroyTest extends ActorTest { atv.Actor ! Vitality.Damage(applyDamageTo) val msg124 = avatarProbe.receiveN(3, 500 milliseconds) val msg3 = player2Probe.receiveOne(200 milliseconds) - val msg567 = vehicleProbe.receiveN(2, 200 milliseconds) assert( msg124.head match { case AvatarServiceMessage("test", AvatarAction.PlanetsideAttributeToAll(PlanetSideGUID(1), 0, _)) => true @@ -1332,18 +1331,6 @@ class DamageableVehicleDestroyTest extends ActorTest { case _ => false } ) - assert( - msg567.head match { - case VehicleServiceMessage.Decon(SupportActor.ClearSpecific(List(target), _zone)) if (target eq atv) && (_zone eq zone) => true - case _ => false - } - ) - assert( - msg567(1) match { - case VehicleServiceMessage.Decon(RemoverActor.AddTask(target, _zone, _)) if (target eq atv) && (_zone eq zone) => true - case _ => false - } - ) assert(atv.Health <= atv.Definition.DamageDestroysAt) assert(atv.Destroyed) // @@ -1467,7 +1454,7 @@ class DamageableVehicleDestroyMountedTest extends ActorTest { player2Probe.expectNoMsg(10 milliseconds) val msg_player3 = player3Probe.receiveOne(200 milliseconds) player3Probe.expectNoMsg(10 milliseconds) - val msg_vehicle = vehicleProbe.receiveN(6, 200 milliseconds) + val msg_vehicle = vehicleProbe.receiveN(2, 200 milliseconds) vehicleProbe.expectNoMsg(10 milliseconds) assert( msg_avatar.exists( { @@ -1511,30 +1498,6 @@ class DamageableVehicleDestroyMountedTest extends ActorTest { case _ => false } ) - assert( - msg_vehicle.exists( { - case VehicleServiceMessage.Decon(SupportActor.ClearSpecific(List(target), _zone)) if (target eq lodestar) && (_zone eq zone) => true - case _ => false - }) - ) - assert( - msg_vehicle.exists( { - case VehicleServiceMessage.Decon(RemoverActor.AddTask(target, _zone, _)) if (target eq lodestar) && (_zone eq zone) => true - case _ => false - }) - ) - assert( - msg_vehicle.exists( { - case VehicleServiceMessage.Decon(SupportActor.ClearSpecific(List(target), _zone)) if (target eq atv) && (_zone eq zone) => true - case _ => false - }) - ) - assert( - msg_vehicle.exists( { - case VehicleServiceMessage.Decon(RemoverActor.AddTask(target, _zone, _)) if (target eq atv) && (_zone eq zone) => true - case _ => false - }) - ) assert( msg_vehicle.exists( { case VehicleServiceMessage("test", VehicleAction.PlanetsideAttribute(Service.defaultPlayerGUID, PlanetSideGUID(4), 27, 0))=> true diff --git a/common/src/test/scala/objects/VehicleTest.scala b/common/src/test/scala/objects/VehicleTest.scala index 7fc1fbae..d06a0762 100644 --- a/common/src/test/scala/objects/VehicleTest.scala +++ b/common/src/test/scala/objects/VehicleTest.scala @@ -3,16 +3,19 @@ package objects import akka.actor.Props import akka.testkit.TestProbe -import base.ActorTest +import base.{ActorTest, FreedContextActorTest} import net.psforever.objects._ -import net.psforever.objects.ballistics.{PlayerSource, Projectile, ProjectileResolution, ResolvedProjectile} import net.psforever.objects.definition.{SeatDefinition, VehicleDefinition} +import net.psforever.objects.guid.NumberPoolHub +import net.psforever.objects.guid.source.LimitedNumberSource import net.psforever.objects.serverobject.mount.Mountable import net.psforever.objects.vehicles._ -import net.psforever.objects.vital.{VehicleShieldCharge, Vitality} -import net.psforever.objects.zones.{Zone, ZoneMap} +import net.psforever.objects.vital.VehicleShieldCharge +import net.psforever.objects.zones.{Zone, ZoneActor, ZoneMap} +import net.psforever.packet.game.{CargoMountPointStatusMessage, ObjectDetachMessage, PlanetsideAttributeMessage} import net.psforever.types.{PlanetSideGUID, _} import org.specs2.mutable._ +import services.{RemoverActor, ServiceManager} import services.vehicle.{VehicleAction, VehicleServiceMessage} import scala.concurrent.duration._ @@ -30,13 +33,13 @@ class VehicleTest extends Specification { val t_seat = new SeatDefinition t_seat.ArmorRestriction mustEqual SeatArmorRestriction.NoMax t_seat.Bailable mustEqual false - t_seat.ControlledWeapon mustEqual None + t_seat.ControlledWeapon.isEmpty mustEqual true } "define (custom)" in { seat.ArmorRestriction mustEqual SeatArmorRestriction.MaxOnly seat.Bailable mustEqual true - seat.ControlledWeapon mustEqual Some(5) + seat.ControlledWeapon.contains(5) } } @@ -47,13 +50,13 @@ class VehicleTest extends Specification { fury.CanCloak mustEqual false fury.Seats.size mustEqual 1 fury.Seats(0).Bailable mustEqual true - fury.Seats(0).ControlledWeapon mustEqual Some(1) + fury.Seats(0).ControlledWeapon.contains(1) fury.MountPoints.size mustEqual 2 - fury.MountPoints.get(1) mustEqual Some(0) - fury.MountPoints.get(2) mustEqual Some(0) + fury.MountPoints.get(1).contains(0) + fury.MountPoints.get(2).contains(0) fury.Weapons.size mustEqual 1 - fury.Weapons.get(0) mustEqual None - fury.Weapons.get(1) mustEqual Some(GlobalDefinitions.fury_weapon_systema) + fury.Weapons.get(0).isEmpty mustEqual true + fury.Weapons.get(1).contains(GlobalDefinitions.fury_weapon_systema) fury.TrunkSize.Width mustEqual 11 fury.TrunkSize.Height mustEqual 11 fury.TrunkOffset mustEqual 30 @@ -70,9 +73,9 @@ class VehicleTest extends Specification { val seat = new Seat(seat_def) seat.ArmorRestriction mustEqual SeatArmorRestriction.MaxOnly seat.Bailable mustEqual true - seat.ControlledWeapon mustEqual Some(5) + seat.ControlledWeapon.contains(5) seat.isOccupied mustEqual false - seat.Occupant mustEqual None + seat.Occupant.isEmpty mustEqual true } "player can sit" in { @@ -136,19 +139,19 @@ class VehicleTest extends Specification { "construct (detailed)" in { val fury_vehicle = Vehicle(GlobalDefinitions.fury) - fury_vehicle.Owner mustEqual None + fury_vehicle.Owner.isEmpty mustEqual true fury_vehicle.Seats.size mustEqual 1 fury_vehicle.Seats(0).ArmorRestriction mustEqual SeatArmorRestriction.NoMax fury_vehicle.Seats(0).isOccupied mustEqual false - fury_vehicle.Seats(0).Occupant mustEqual None + fury_vehicle.Seats(0).Occupant.isEmpty mustEqual true fury_vehicle.Seats(0).Bailable mustEqual true - fury_vehicle.Seats(0).ControlledWeapon mustEqual Some(1) - fury_vehicle.PermissionGroup(0) mustEqual Some(VehicleLockState.Locked) //driver - fury_vehicle.PermissionGroup(1) mustEqual Some(VehicleLockState.Empire) //gunner - fury_vehicle.PermissionGroup(2) mustEqual Some(VehicleLockState.Empire) //passenger - fury_vehicle.PermissionGroup(3) mustEqual Some(VehicleLockState.Locked) //trunk + fury_vehicle.Seats(0).ControlledWeapon.contains(1) + fury_vehicle.PermissionGroup(0).contains(VehicleLockState.Locked) //driver + fury_vehicle.PermissionGroup(1).contains(VehicleLockState.Empire) //gunner + fury_vehicle.PermissionGroup(2).contains(VehicleLockState.Empire) //passenger + fury_vehicle.PermissionGroup(3).contains(VehicleLockState.Locked) //trunk fury_vehicle.Weapons.size mustEqual 1 - fury_vehicle.Weapons.get(0) mustEqual None + fury_vehicle.Weapons.get(0).isEmpty mustEqual true fury_vehicle.Weapons.get(1).isDefined mustEqual true fury_vehicle.Weapons(1).Equipment.isDefined mustEqual true fury_vehicle.Weapons(1).Equipment.get.Definition mustEqual GlobalDefinitions.fury.Weapons(1) @@ -156,8 +159,8 @@ class VehicleTest extends Specification { fury_vehicle.Trunk.Width mustEqual 11 fury_vehicle.Trunk.Height mustEqual 11 fury_vehicle.Trunk.Offset mustEqual 30 - fury_vehicle.GetSeatFromMountPoint(1) mustEqual Some(0) - fury_vehicle.GetSeatFromMountPoint(2) mustEqual Some(0) + fury_vehicle.GetSeatFromMountPoint(1).contains(0) + fury_vehicle.GetSeatFromMountPoint(2).contains(0) fury_vehicle.Decal mustEqual 0 fury_vehicle.Health mustEqual fury_vehicle.Definition.MaxHealth } @@ -192,30 +195,30 @@ class VehicleTest extends Specification { "can use mount point to get seat number" in { val fury_vehicle = Vehicle(GlobalDefinitions.fury) - fury_vehicle.GetSeatFromMountPoint(0) mustEqual None - fury_vehicle.GetSeatFromMountPoint(1) mustEqual Some(0) - fury_vehicle.GetSeatFromMountPoint(2) mustEqual Some(0) - fury_vehicle.GetSeatFromMountPoint(3) mustEqual None + fury_vehicle.GetSeatFromMountPoint(0).isEmpty mustEqual true + fury_vehicle.GetSeatFromMountPoint(1).contains(0) + fury_vehicle.GetSeatFromMountPoint(2).contains(0) + fury_vehicle.GetSeatFromMountPoint(3).isEmpty mustEqual true } "has four permission groups" in { val fury_vehicle = Vehicle(GlobalDefinitions.fury) - fury_vehicle.PermissionGroup(AccessPermissionGroup.Driver.id) mustEqual Some(VehicleLockState.Locked) - fury_vehicle.PermissionGroup(AccessPermissionGroup.Gunner.id) mustEqual Some(VehicleLockState.Empire) - fury_vehicle.PermissionGroup(AccessPermissionGroup.Passenger.id) mustEqual Some(VehicleLockState.Empire) - fury_vehicle.PermissionGroup(AccessPermissionGroup.Trunk.id) mustEqual Some(VehicleLockState.Locked) + fury_vehicle.PermissionGroup(AccessPermissionGroup.Driver.id).contains(VehicleLockState.Locked) + fury_vehicle.PermissionGroup(AccessPermissionGroup.Gunner.id).contains(VehicleLockState.Empire) + fury_vehicle.PermissionGroup(AccessPermissionGroup.Passenger.id).contains(VehicleLockState.Empire) + fury_vehicle.PermissionGroup(AccessPermissionGroup.Trunk.id).contains(VehicleLockState.Locked) } "set new permission level" in { val fury_vehicle = Vehicle(GlobalDefinitions.fury) - fury_vehicle.PermissionGroup(AccessPermissionGroup.Driver.id) mustEqual Some(VehicleLockState.Locked) - fury_vehicle.PermissionGroup(AccessPermissionGroup.Driver.id, VehicleLockState.Group.id) mustEqual Some(VehicleLockState.Group) + fury_vehicle.PermissionGroup(AccessPermissionGroup.Driver.id).contains(VehicleLockState.Locked) + fury_vehicle.PermissionGroup(AccessPermissionGroup.Driver.id, VehicleLockState.Group.id).contains(VehicleLockState.Group) } "set the same permission level" in { val fury_vehicle = Vehicle(GlobalDefinitions.fury) - fury_vehicle.PermissionGroup(AccessPermissionGroup.Driver.id) mustEqual Some(VehicleLockState.Locked) - fury_vehicle.PermissionGroup(AccessPermissionGroup.Driver.id, VehicleLockState.Locked.id) mustEqual None + fury_vehicle.PermissionGroup(AccessPermissionGroup.Driver.id).contains(VehicleLockState.Locked) + fury_vehicle.PermissionGroup(AccessPermissionGroup.Driver.id, VehicleLockState.Locked.id).isEmpty mustEqual true } "alternate permission level indices" in { @@ -226,15 +229,15 @@ class VehicleTest extends Specification { fury_vehicle.PermissionGroup(AccessPermissionGroup.Trunk.id) mustEqual fury_vehicle.PermissionGroup(13) (AccessPermissionGroup.Driver.id + 10) mustEqual 10 - fury_vehicle.PermissionGroup(AccessPermissionGroup.Driver.id, VehicleLockState.Group.id) mustEqual Some(VehicleLockState.Group) + fury_vehicle.PermissionGroup(AccessPermissionGroup.Driver.id, VehicleLockState.Group.id).contains(VehicleLockState.Group) fury_vehicle.PermissionGroup(AccessPermissionGroup.Driver.id) mustEqual fury_vehicle.PermissionGroup(10) } "can determine permission group from seat" in { val harasser_vehicle = Vehicle(GlobalDefinitions.two_man_assault_buggy) - harasser_vehicle.SeatPermissionGroup(0) mustEqual Some(AccessPermissionGroup.Driver) - harasser_vehicle.SeatPermissionGroup(1) mustEqual Some(AccessPermissionGroup.Gunner) - harasser_vehicle.SeatPermissionGroup(2) mustEqual None + harasser_vehicle.SeatPermissionGroup(0).contains(AccessPermissionGroup.Driver) + harasser_vehicle.SeatPermissionGroup(1).contains(AccessPermissionGroup.Gunner) + harasser_vehicle.SeatPermissionGroup(2).isEmpty mustEqual true //TODO test for AccessPermissionGroup.Passenger later } @@ -247,11 +250,11 @@ class VehicleTest extends Specification { harasser_vehicle.Seat(0).get.Occupant = player1 //don't worry about ownership for now harasser_vehicle.Seat(1).get.Occupant = player2 - harasser_vehicle.PassengerInSeat(player1) mustEqual Some(0) - harasser_vehicle.PassengerInSeat(player2) mustEqual Some(1) + harasser_vehicle.PassengerInSeat(player1).contains(0) + harasser_vehicle.PassengerInSeat(player2).contains(1) harasser_vehicle.Seat(0).get.Occupant = None - harasser_vehicle.PassengerInSeat(player1) mustEqual None - harasser_vehicle.PassengerInSeat(player2) mustEqual Some(1) + harasser_vehicle.PassengerInSeat(player1).isEmpty mustEqual true + harasser_vehicle.PassengerInSeat(player2).contains(1) } "can find a weapon controlled from seat" in { @@ -259,7 +262,7 @@ class VehicleTest extends Specification { val chaingun_p = harasser_vehicle.Weapons(2).Equipment chaingun_p.isDefined mustEqual true - harasser_vehicle.WeaponControlledFromSeat(0) mustEqual None + harasser_vehicle.WeaponControlledFromSeat(0).isEmpty mustEqual true harasser_vehicle.WeaponControlledFromSeat(1) mustEqual chaingun_p } @@ -273,12 +276,12 @@ class VehicleTest extends Specification { obj.Utilities.size mustEqual 3 obj.Utilities(-1).UtilType mustEqual UtilityType.order_terminala obj.Utilities(0).UtilType mustEqual UtilityType.order_terminalb - obj.Utilities.get(1) mustEqual None + obj.Utilities.get(1).isEmpty mustEqual true obj.Utilities(2).UtilType mustEqual UtilityType.order_terminalb val filteredMap = Vehicle.EquipmentUtilities(obj.Utilities) filteredMap.size mustEqual 2 - filteredMap.get(-1) mustEqual None + filteredMap.get(-1).isEmpty mustEqual true filteredMap(0).UtilType mustEqual UtilityType.order_terminalb filteredMap(2).UtilType mustEqual UtilityType.order_terminalb } @@ -303,7 +306,7 @@ class VehicleTest extends Specification { val harasser_vehicle = Vehicle(GlobalDefinitions.two_man_assault_buggy) harasser_vehicle.Weapons(2).Equipment.get.GUID = PlanetSideGUID(10) - harasser_vehicle.Find(PlanetSideGUID(10)) mustEqual Some(2) + harasser_vehicle.Find(PlanetSideGUID(10)).contains(2) } "find items in its trunk by GUID" in { @@ -312,101 +315,284 @@ class VehicleTest extends Specification { ammobox.GUID = PlanetSideGUID(10) harasser_vehicle.Inventory += 30 -> ammobox - harasser_vehicle.Find(PlanetSideGUID(10)) mustEqual Some(30) + harasser_vehicle.Find(PlanetSideGUID(10)).contains(30) } } } -class VehicleControlStopMountingTest extends ActorTest { - "Vehicle Control" should { - "deactivate and stop handling mount messages" in { - val player1 = Player(VehicleTest.avatar1) - player1.GUID = PlanetSideGUID(1) - val player2 = Player(VehicleTest.avatar2) - val vehicle = Vehicle(GlobalDefinitions.two_man_assault_buggy) - vehicle.Faction = PlanetSideEmpire.TR - vehicle.GUID = PlanetSideGUID(3) - vehicle.Actor = system.actorOf(Props(classOf[VehicleControl], vehicle), "vehicle-test") - vehicle.Zone = new Zone("test", new ZoneMap("test"), 0) { - VehicleEvents = new TestProbe(system).ref //necessary - } - vehicle.Weapons(2).Equipment.get.GUID = PlanetSideGUID(4) - val probe = new TestProbe(system) - - vehicle.Actor.tell(Mountable.TryMount(player1, 0), probe.ref) - val reply = probe.receiveOne(Duration.create(200, "ms")) - assert(reply.isInstanceOf[Mountable.MountMessages]) - - vehicle.Actor.tell(Vehicle.PrepareForDeletion(), probe.ref) - vehicle.Actor.tell(Mountable.TryMount(player2, 1), probe.ref) - probe.expectNoMsg(Duration.create(200, "ms")) //assertion failed: received unexpected message MountMessages(CanMount - } - } -} - -class VehicleControlRestartMountingTest extends ActorTest { - val player1 = Player(VehicleTest.avatar1) - player1.GUID = PlanetSideGUID(1) - val player2 = Player(VehicleTest.avatar2) - player2.GUID = PlanetSideGUID(2) +class VehicleControlPrepareForDeletionTest extends ActorTest { val vehicle = Vehicle(GlobalDefinitions.two_man_assault_buggy) vehicle.Faction = PlanetSideEmpire.TR - vehicle.GUID = PlanetSideGUID(3) vehicle.Actor = system.actorOf(Props(classOf[VehicleControl], vehicle), "vehicle-test") + val vehicleProbe = new TestProbe(system) vehicle.Zone = new Zone("test", new ZoneMap("test"), 0) { - VehicleEvents = new TestProbe(system).ref + VehicleEvents = vehicleProbe.ref } - vehicle.Weapons(2).Equipment.get.GUID = PlanetSideGUID(4) - val probe = new TestProbe(system) - "Vehicle Control" should { - "reactivate and resume handling mount messages" in { - vehicle.Actor.tell(Mountable.TryMount(player1, 0), probe.ref) - probe.receiveOne(Duration.create(200, "ms")) //discard - vehicle.Actor.tell(Vehicle.PrepareForDeletion(), probe.ref) - vehicle.Actor.tell(Mountable.TryMount(player2, 1), probe.ref) - probe.expectNoMsg(Duration.create(200, "ms")) + vehicle.GUID = PlanetSideGUID(1) + expectNoMsg(200 milliseconds) - vehicle.Actor.tell(Vehicle.Reactivate(), probe.ref) - vehicle.Actor.tell(Mountable.TryMount(player2, 1), probe.ref) - val reply = probe.receiveOne(Duration.create(200, "ms")) - assert(reply.isInstanceOf[Mountable.MountMessages]) + "VehicleControl" should { + "submit for unregistering when marked for deconstruction" in { + vehicle.Actor ! Vehicle.Deconstruct() + + val vehicle_msg = vehicleProbe.receiveN(1, 500 milliseconds) + assert( + vehicle_msg.head match { + case VehicleServiceMessage.Decon(RemoverActor.AddTask(v, z, _)) => (v eq vehicle) && (z == vehicle.Zone) + case _ => false + } + ) + + val vehicle_msg_final = vehicleProbe.receiveN(1, 6 seconds) + assert( + vehicle_msg_final.head match { + case VehicleServiceMessage("test", VehicleAction.UnloadVehicle(_, z, v, PlanetSideGUID(1))) => (v eq vehicle) && (z == vehicle.Zone) + case _ => false + } + ) } } } -class VehicleControlAlwaysDismountTest extends ActorTest { - val probe = new TestProbe(system) - val player1 = Player(VehicleTest.avatar1) - player1.GUID = PlanetSideGUID(1) - val player2 = Player(VehicleTest.avatar2) - player2.GUID = PlanetSideGUID(2) +class VehicleControlPrepareForDeletionPassengerTest extends ActorTest { val vehicle = Vehicle(GlobalDefinitions.two_man_assault_buggy) vehicle.Faction = PlanetSideEmpire.TR - vehicle.GUID = PlanetSideGUID(3) vehicle.Actor = system.actorOf(Props(classOf[VehicleControl], vehicle), "vehicle-test") + val vehicleProbe = new TestProbe(system) vehicle.Zone = new Zone("test", new ZoneMap("test"), 0) { - VehicleEvents = new TestProbe(system).ref + VehicleEvents = vehicleProbe.ref } - vehicle.Weapons(2).Equipment.get.GUID = PlanetSideGUID(4) + val player1 = Player(VehicleTest.avatar1) - "Vehicle Control" should { - "always allow dismount messages" in { - vehicle.Actor.tell(Mountable.TryMount(player1, 0), probe.ref) - probe.receiveOne(Duration.create(100, "ms")) //discard - vehicle.Actor.tell(Mountable.TryMount(player2, 1), probe.ref) - probe.receiveOne(Duration.create(100, "ms")) //discard + vehicle.GUID = PlanetSideGUID(1) + player1.GUID = PlanetSideGUID(2) + vehicle.Seats(1).Occupant = player1 //passenger seat + player1.VehicleSeated = vehicle.GUID + expectNoMsg(200 milliseconds) - vehicle.Actor.tell(Mountable.TryDismount(player2, 1), probe.ref) //player2 requests dismount - val reply1 = probe.receiveOne(Duration.create(100, "ms")) - assert(reply1.isInstanceOf[Mountable.MountMessages]) - assert(reply1.asInstanceOf[Mountable.MountMessages].response.isInstanceOf[Mountable.CanDismount]) //player2 dismounts + "VehicleControl" should { + "kick all players when marked for deconstruction" in { + vehicle.Actor ! Vehicle.Deconstruct() - vehicle.Actor.tell(Vehicle.PrepareForDeletion(), probe.ref) - vehicle.Actor.tell(Mountable.TryDismount(player1, 0), probe.ref) //player1 requests dismount - val reply2 = probe.receiveOne(Duration.create(100, "ms")) - assert(reply2.isInstanceOf[Mountable.MountMessages]) - assert(reply2.asInstanceOf[Mountable.MountMessages].response.isInstanceOf[Mountable.CanDismount]) //player1 dismounts + val vehicle_msg = vehicleProbe.receiveN(2, 500 milliseconds) + assert( + vehicle_msg.head match { + case VehicleServiceMessage("test", VehicleAction.KickPassenger(PlanetSideGUID(2), 4, false, PlanetSideGUID(1))) => true + case _ => false + } + ) + assert(player1.VehicleSeated.isEmpty) + assert(vehicle.Seats(1).Occupant.isEmpty) + assert( + vehicle_msg(1) match { + case VehicleServiceMessage.Decon(RemoverActor.AddTask(v, z, _)) => (v eq vehicle) && (z == vehicle.Zone) + case _ => false + } + ) + } + } +} + +class VehicleControlPrepareForDeletionMountedInTest extends FreedContextActorTest { + ServiceManager.boot + val guid = new NumberPoolHub(new LimitedNumberSource(10)) + val zone = new Zone("test", new ZoneMap("test"), 0) { + GUID(guid) + override def SetupNumberPools() : Unit = { } + } + zone.Actor = system.actorOf(Props(classOf[ZoneActor], zone), "test-zone-actor") + zone.Init(context) + + val vehicle = Vehicle(GlobalDefinitions.two_man_assault_buggy) + vehicle.Faction = PlanetSideEmpire.TR + vehicle.Actor = system.actorOf(Props(classOf[VehicleControl], vehicle), "vehicle-test-cargo") + vehicle.Zone = zone + val lodestar = Vehicle(GlobalDefinitions.lodestar) + lodestar.Faction = PlanetSideEmpire.TR + val player1 = Player(VehicleTest.avatar1) //name="test1" + val player2 = Player(VehicleTest.avatar2) //name="test2" + + guid.register(vehicle, 1) + guid.register(lodestar, 2) + player1.GUID = PlanetSideGUID(3) + var utilityId = 10 + lodestar.Utilities.values.foreach { util => + util().GUID = PlanetSideGUID(utilityId) + utilityId += 1 + } + vehicle.Seats(1).Occupant = player1 //passenger seat + player1.VehicleSeated = vehicle.GUID + lodestar.Seats(0).Occupant = player2 + player2.VehicleSeated = lodestar.GUID + lodestar.CargoHolds(1).Occupant = vehicle + vehicle.MountedIn = lodestar.GUID + + val vehicleProbe = new TestProbe(system) + zone.VehicleEvents = vehicleProbe.ref + zone.Transport ! Zone.Vehicle.Spawn(lodestar) //can not fake this + expectNoMsg(200 milliseconds) + + "VehicleControl" should { + "if mounted as cargo, self-eject when marked for deconstruction" in { + vehicle.Actor ! Vehicle.Deconstruct() + + val vehicle_msg = vehicleProbe.receiveN(7, 500 milliseconds) + //dismounting as cargo messages + assert( + vehicle_msg.head match { + case VehicleServiceMessage(_, VehicleAction.SendResponse(_, PlanetsideAttributeMessage(PlanetSideGUID(1), 0, _))) => true + case _ => false + } + ) + assert( + vehicle_msg(1) match { + case VehicleServiceMessage(_, VehicleAction.SendResponse(_, PlanetsideAttributeMessage(PlanetSideGUID(1), 68, _))) => true + case _ => false + } + ) + assert( + vehicle_msg(2) match { + case VehicleServiceMessage("test", VehicleAction.SendResponse(_, + CargoMountPointStatusMessage(PlanetSideGUID(2), _, PlanetSideGUID(1), _, 1, CargoStatus.InProgress, 0) + )) => true + case _ => false + } + ) + assert( + vehicle_msg(3) match { + case VehicleServiceMessage("test", VehicleAction.SendResponse(_, + ObjectDetachMessage(PlanetSideGUID(2), PlanetSideGUID(1), _, _, _, _) + )) => true + case _ => false + } + ) + assert( + vehicle_msg(4) match { + case VehicleServiceMessage("test", VehicleAction.SendResponse(_, + CargoMountPointStatusMessage(PlanetSideGUID(2), _, _, PlanetSideGUID(1), 1, CargoStatus.Empty, 0) + )) => true + case _ => false + } + ) + //dismounting as cargo messages + //TODO: does not actually kick out the cargo, but instigates the process + assert( + vehicle_msg(5) match { + case VehicleServiceMessage("test", VehicleAction.KickPassenger(PlanetSideGUID(3), 4, false, PlanetSideGUID(1))) => true + case _ => false + } + ) + assert(player1.VehicleSeated.isEmpty) + assert(vehicle.Seats(1).Occupant.isEmpty) + assert( + vehicle_msg(6) match { + case VehicleServiceMessage.Decon(RemoverActor.AddTask(v, z, _)) => (v eq vehicle) && (z == vehicle.Zone) + case _ => false + } + ) + } + } +} + +class VehicleControlPrepareForDeletionMountedCargoTest extends FreedContextActorTest { + val guid = new NumberPoolHub(new LimitedNumberSource(10)) + ServiceManager.boot + val zone = new Zone("test", new ZoneMap("test"), 0) { + GUID(guid) + override def SetupNumberPools() : Unit = { } + } + zone.Actor = system.actorOf(Props(classOf[ZoneActor], zone), "test-zone-actor") + zone.Init(context) + + val vehicle = Vehicle(GlobalDefinitions.two_man_assault_buggy) + vehicle.Faction = PlanetSideEmpire.TR + vehicle.Zone = zone + val cargoProbe = new TestProbe(system) + vehicle.Actor = cargoProbe.ref + val lodestar = Vehicle(GlobalDefinitions.lodestar) + lodestar.Faction = PlanetSideEmpire.TR + val player1 = Player(VehicleTest.avatar1) //name="test1" + val player2 = Player(VehicleTest.avatar2) //name="test2" + + guid.register(vehicle, 1) + guid.register(lodestar, 2) + player1.GUID = PlanetSideGUID(3) + player2.GUID = PlanetSideGUID(4) + var utilityId = 10 + lodestar.Utilities.values.foreach { util => + util().GUID = PlanetSideGUID(utilityId) + utilityId += 1 + } + vehicle.Seats(1).Occupant = player1 //passenger seat + player1.VehicleSeated = vehicle.GUID + lodestar.Seats(0).Occupant = player2 + player2.VehicleSeated = lodestar.GUID + lodestar.CargoHolds(1).Occupant = vehicle + vehicle.MountedIn = lodestar.GUID + + val vehicleProbe = new TestProbe(system) + zone.VehicleEvents = vehicleProbe.ref + zone.Transport ! Zone.Vehicle.Spawn(lodestar) //can not fake this + expectNoMsg(200 milliseconds) + + "VehicleControl" should { + "if with mounted cargo, eject it when marked for deconstruction" in { + lodestar.Actor ! Vehicle.Deconstruct() + + val vehicle_msg = vehicleProbe.receiveN(7, 500 milliseconds) + assert( + vehicle_msg.head match { + case VehicleServiceMessage("test", VehicleAction.KickPassenger(PlanetSideGUID(4), 4, false, PlanetSideGUID(2))) => true + case _ => false + } + ) + assert(player2.VehicleSeated.isEmpty) + assert(lodestar.Seats(0).Occupant.isEmpty) + //cargo dismounting messages + assert( + vehicle_msg(1) match { + case VehicleServiceMessage(_, VehicleAction.SendResponse(_, PlanetsideAttributeMessage(PlanetSideGUID(1), 0, _))) => true + case _ => false + } + ) + assert( + vehicle_msg(2) match { + case VehicleServiceMessage(_, VehicleAction.SendResponse(_, PlanetsideAttributeMessage(PlanetSideGUID(1), 68, _))) => true + case _ => false + } + ) + assert( + vehicle_msg(3) match { + case VehicleServiceMessage("test", VehicleAction.SendResponse(_, + CargoMountPointStatusMessage(PlanetSideGUID(2), _, PlanetSideGUID(1), _, 1, CargoStatus.InProgress, 0) + )) => true + case _ => false + } + ) + assert( + vehicle_msg(4) match { + case VehicleServiceMessage("test", VehicleAction.SendResponse(_, + ObjectDetachMessage(PlanetSideGUID(2), PlanetSideGUID(1), _, _, _, _) + )) => true + case _ => false + } + ) + assert( + vehicle_msg(5) match { + case VehicleServiceMessage("test", VehicleAction.SendResponse(_, + CargoMountPointStatusMessage(PlanetSideGUID(2), _, _, PlanetSideGUID(1), 1, CargoStatus.Empty, 0) + )) => true + case _ => false + } + ) + //cargo dismounting messages + assert( + vehicle_msg(6) match { + case VehicleServiceMessage.Decon(RemoverActor.AddTask(v, z, _)) => (v eq lodestar) && (z == vehicle.Zone) + case _ => false + } + ) } } } diff --git a/pslogin/src/main/scala/WorldSessionActor.scala b/pslogin/src/main/scala/WorldSessionActor.scala index c132f904..27eabeb2 100644 --- a/pslogin/src/main/scala/WorldSessionActor.scala +++ b/pslogin/src/main/scala/WorldSessionActor.scala @@ -1400,14 +1400,14 @@ class WorldSessionActor extends Actor else { inZone.GUID(p.VehicleSeated) match { case Some(v : Vehicle) if v.Destroyed => - inZone.VehicleEvents ! VehicleServiceMessage.Decon(RemoverActor.ClearSpecific(List(v), inZone)) - inZone.VehicleEvents ! VehicleServiceMessage.Decon(RemoverActor.AddTask(v, inZone, if(v.Flying) { + v.Actor ! Vehicle.Deconstruct( + if(v.Flying) { //TODO gravity - Some(0 seconds) //immediate deconstruction + None //immediate deconstruction } else { v.Definition.DeconstructionTime //normal deconstruction - })) + }) case _ => ; } } @@ -2283,7 +2283,6 @@ class WorldSessionActor extends Actor sendResponse(PlanetsideAttributeMessage(obj_guid, 113, capacitor)) } if(seat_num == 0) { - continent.VehicleEvents ! VehicleServiceMessage.Decon(RemoverActor.ClearSpecific(List(obj), continent)) //clear timer //simplistic vehicle ownership management obj.Owner match { case Some(owner_guid) => @@ -2336,8 +2335,6 @@ class WorldSessionActor extends Actor case Mountable.CanDismount(obj : Vehicle, seat_num) if obj.Definition == GlobalDefinitions.droppod => UnAccessContents(obj) DismountAction(tplayer, obj, seat_num) - continent.VehicleEvents ! VehicleServiceMessage.Decon(RemoverActor.ClearSpecific(List(obj), continent)) - continent.VehicleEvents ! VehicleServiceMessage.Decon(RemoverActor.AddTask(obj, continent, obj.Definition.DeconstructionTime)) case Mountable.CanDismount(obj : Vehicle, seat_num) => val player_guid : PlanetSideGUID = tplayer.GUID @@ -3007,9 +3004,8 @@ class WorldSessionActor extends Actor ) } - case msg@VehicleResponse.KickPassenger(seat_num, wasKickedByDriver, vehicle_guid) => + case 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 { @@ -3100,9 +3096,6 @@ class WorldSessionActor extends Actor galaxyService ! Service.Join(temp_channel) //temporary vehicle-specific channel } - case VehicleResponse.ForceDismountVehicleCargo(cargo_guid, bailed, requestedByPassenger, kicked) => - CargoBehavior.HandleVehicleCargoDismount(continent, tplayer_guid, cargo_guid, bailed, requestedByPassenger, kicked) - case VehicleResponse.KickCargo(vehicle, speed, delay) => if(player.VehicleSeated.nonEmpty && deadState == DeadState.Alive) { if(speed > 0) { @@ -3114,7 +3107,7 @@ class WorldSessionActor extends Actor controlled = Some(reverseSpeed) sendResponse(ServerVehicleOverrideMsg(true, true, true, false, 0, strafe, reverseSpeed, Some(0))) import scala.concurrent.ExecutionContext.Implicits.global - context.system.scheduler.scheduleOnce(delay milliseconds, self, VehicleServiceResponse(toChannel, tplayer_guid, VehicleResponse.KickCargo(vehicle, 0, delay))) + context.system.scheduler.scheduleOnce(delay milliseconds, self, VehicleServiceResponse(toChannel, PlanetSideGUID(0), VehicleResponse.KickCargo(vehicle, 0, delay))) } else { controlled = None @@ -3650,7 +3643,7 @@ class WorldSessionActor extends Actor case Some(cargo : Vehicle) if !requestedByPassenger => continent.GUID(cargo.MountedIn) match { case Some(carrier : Vehicle) => - CargoBehavior.HandleVehicleCargoDismount(continent, player_guid, cargo_guid, bailed, requestedByPassenger, kicked) + CargoBehavior.HandleVehicleCargoDismount(continent, cargo_guid, bailed, requestedByPassenger, kicked) case _ => ; } case _ => ; @@ -4218,8 +4211,7 @@ class WorldSessionActor extends Actor case v : Vehicle if seatNum == 0 && v.Flying => TotalDriverVehicleControl(v) UnAccessContents(v) - continent.VehicleEvents ! VehicleServiceMessage.Decon(RemoverActor.ClearSpecific(List(obj), continent)) - continent.VehicleEvents ! VehicleServiceMessage.Decon(RemoverActor.AddTask(obj, continent, Some(0 seconds))) + v.Actor ! Vehicle.Deconstruct() case _ => ; } case _ => ; //found no vehicle where one was expected; since we're dead, let's not dwell on it @@ -5035,8 +5027,7 @@ class WorldSessionActor extends Actor if((player.VehicleOwned.contains(object_guid) && vehicle.Owner.contains(player.GUID)) || (player.Faction == vehicle.Faction && ((vehicle.Owner.isEmpty || continent.GUID(vehicle.Owner.get).isEmpty) || vehicle.Destroyed))) { - continent.VehicleEvents ! VehicleServiceMessage.Decon(RemoverActor.ClearSpecific(List(vehicle), continent)) - continent.VehicleEvents ! VehicleServiceMessage.Decon(RemoverActor.AddTask(vehicle, continent, Some(0 seconds))) + vehicle.Actor ! Vehicle.Deconstruct() log.info(s"RequestDestroy: vehicle $vehicle") } else { @@ -6024,8 +6015,7 @@ class WorldSessionActor extends Actor //todo: kick cargo passengers out. To be added after PR #216 is merged obj match { case v : Vehicle if bailType == BailType.Bailed && seat_num == 0 && v.Flying => - continent.VehicleEvents ! VehicleServiceMessage.Decon(RemoverActor.ClearSpecific(List(obj), continent)) - continent.VehicleEvents ! VehicleServiceMessage.Decon(RemoverActor.AddTask(obj, continent, Some(0 seconds))) // Immediately deconstruct vehicle + v.Actor ! Vehicle.Deconstruct() // Immediately deconstruct vehicle case _ => ; } @@ -9724,7 +9714,7 @@ class WorldSessionActor extends Actor case ("MISSING_DRIVER", index) => val cargo = vehicle.CargoHolds(index).Occupant.get log.error(s"LoadZoneInVehicleAsDriver: eject cargo in hold $index; vehicle missing driver") - CargoBehavior.HandleVehicleCargoDismount(pguid, cargo.GUID, cargo, vehicle.GUID, vehicle, false, false, true) + CargoBehavior.HandleVehicleCargoDismount(cargo.GUID, cargo, vehicle.GUID, vehicle, false, false, true) case (name, index) => val cargo = vehicle.CargoHolds(index).Occupant.get continent.VehicleEvents ! VehicleServiceMessage(name, VehicleAction.TransferPassengerChannel(pguid, s"${cargo.Actor}", toChannel, cargo, topLevel)) @@ -10016,8 +10006,6 @@ class WorldSessionActor extends Actor sendResponse(PlayerStateShiftMessage(ShiftState(0, dest.Position, player.Orientation.z))) UseRouterTelepadEffect(pguid, sguid, dguid) StopBundlingPackets() - // continent.VehicleEvents ! VehicleServiceMessage.Decon(RemoverActor.ClearSpecific(List(router), continent)) - // continent.VehicleEvents p! VehicleServiceMessage.Decon(RemoverActor.AddTask(router, continent, router.Definition.DeconstructionTime)) continent.LocalEvents ! LocalServiceMessage(continent.Id, LocalAction.RouterTelepadTransport(pguid, pguid, sguid, dguid)) } else { diff --git a/pslogin/src/test/scala/actor/objects/VehicleSpawnPadTest.scala b/pslogin/src/test/scala/actor/objects/VehicleSpawnPadTest.scala index e39a7227..03d48ebd 100644 --- a/pslogin/src/test/scala/actor/objects/VehicleSpawnPadTest.scala +++ b/pslogin/src/test/scala/actor/objects/VehicleSpawnPadTest.scala @@ -9,6 +9,7 @@ import net.psforever.objects.serverobject.structures.StructureType import net.psforever.objects.{Avatar, GlobalDefinitions, Player, Vehicle} import net.psforever.objects.zones.Zone import net.psforever.types.{PlanetSideGUID, _} +import services.RemoverActor import services.vehicle.{VehicleAction, VehicleServiceMessage} import scala.concurrent.duration._ @@ -107,7 +108,13 @@ class VehicleSpawnControl4Test extends ActorTest { pad.Actor ! VehicleSpawnPad.VehicleOrder(player, vehicle) //order - probe.expectMsgClass(1 minute, classOf[VehicleSpawnPad.DisposeVehicle]) + val msg = probe.receiveOne(1 minute) + assert( + msg match { + case VehicleServiceMessage.Decon(RemoverActor.AddTask(v, z , _)) => (v == vehicle) && (z == zone) + case _ => false + } + ) probe.expectNoMsg(5 seconds) } }