diff --git a/common/src/main/scala/net/psforever/objects/GlobalDefinitions.scala b/common/src/main/scala/net/psforever/objects/GlobalDefinitions.scala index 0e542f53..d763cbcc 100644 --- a/common/src/main/scala/net/psforever/objects/GlobalDefinitions.scala +++ b/common/src/main/scala/net/psforever/objects/GlobalDefinitions.scala @@ -845,6 +845,53 @@ object GlobalDefinitions { } } + /** + * Using the definition for a `Vehicle` determine whether it can fly. + * @param vdef the `VehicleDefinition` of the vehicle + * @return `true`, if it is; `false`, otherwise + */ + def isFlightVehicle(vdef : VehicleDefinition) : Boolean = { + vdef match { + case `mosquito` | `lightgunship` | `wasp` | `liberator` | `vulture` | `phantasm` | `lodestar` | `dropship` | `galaxy_gunship` => + true + case _ => + false + } + } + + /** + * Using the definition for a `Vehicle` determine whether it hovers. + * @param vdef the `VehicleDefinition` of the vehicle + * @return `true`, if it can; `false`, otherwise + */ + def isHoverVehicle(vdef : VehicleDefinition) : Boolean = { + vdef match { + case `twomanhoverbuggy` | `magrider` | `router` | `flail` => + true + case _ => + false + } + } + + /** + * Using the definition for a `Vehicle` determine whether it can rotate its body without forward acceleration. + * @param vdef the `VehicleDefinition` of the vehicle + * @return `true`, if it is; `false`, otherwise + */ + def canStationaryRotate(vdef : VehicleDefinition) : Boolean = { + if(isFlightVehicle(vdef) || isHoverVehicle(vdef)) { + true + } + else { + vdef match { + case `lightning` | `prowler` | `vanguard` => + true + case _ => + false + } + } + } + /** * Initialize `AmmoBoxDefinition` globals. */ @@ -2034,6 +2081,7 @@ object GlobalDefinitions { * Initialize `VehicleDefinition` globals. */ private def init_vehicles() : Unit = { + fury.Name = "fury" fury.Seats += 0 -> new SeatDefinition() fury.Seats(0).Bailable = true fury.Seats(0).ControlledWeapon = 1 @@ -2042,7 +2090,9 @@ object GlobalDefinitions { fury.MountPoints += 2 -> 0 fury.TrunkSize = InventoryTile.Tile1111 fury.TrunkOffset = 30 + fury.AutoPilotSpeeds = (24, 10) + quadassault.Name = "quadassault" quadassault.Seats += 0 -> new SeatDefinition() quadassault.Seats(0).Bailable = true quadassault.Seats(0).ControlledWeapon = 1 @@ -2051,7 +2101,9 @@ object GlobalDefinitions { quadassault.MountPoints += 2 -> 0 quadassault.TrunkSize = InventoryTile.Tile1111 quadassault.TrunkOffset = 30 + quadassault.AutoPilotSpeeds = (24, 10) + quadstealth.Name = "quadstealth" quadstealth.CanCloak = true quadstealth.Seats += 0 -> new SeatDefinition() quadstealth.Seats(0).Bailable = true @@ -2060,7 +2112,9 @@ object GlobalDefinitions { quadstealth.MountPoints += 2 -> 0 quadstealth.TrunkSize = InventoryTile.Tile1111 quadstealth.TrunkOffset = 30 + quadstealth.AutoPilotSpeeds = (24, 10) + two_man_assault_buggy.Name = "two_man_assault_buggy" two_man_assault_buggy.Seats += 0 -> new SeatDefinition() two_man_assault_buggy.Seats(0).Bailable = true two_man_assault_buggy.Seats += 1 -> new SeatDefinition() @@ -2071,7 +2125,9 @@ object GlobalDefinitions { two_man_assault_buggy.MountPoints += 2 -> 1 two_man_assault_buggy.TrunkSize = InventoryTile.Tile1511 two_man_assault_buggy.TrunkOffset = 30 + two_man_assault_buggy.AutoPilotSpeeds = (22, 8) + skyguard.Name = "skyguard" skyguard.Seats += 0 -> new SeatDefinition() skyguard.Seats(0).Bailable = true skyguard.Seats += 1 -> new SeatDefinition() @@ -2083,7 +2139,9 @@ object GlobalDefinitions { skyguard.MountPoints += 3 -> 1 skyguard.TrunkSize = InventoryTile.Tile1511 skyguard.TrunkOffset = 30 + skyguard.AutoPilotSpeeds = (22, 8) + threemanheavybuggy.Name = "threemanheavybuggy" threemanheavybuggy.Seats += 0 -> new SeatDefinition() threemanheavybuggy.Seats(0).Bailable = true threemanheavybuggy.Seats += 1 -> new SeatDefinition() @@ -2099,7 +2157,9 @@ object GlobalDefinitions { threemanheavybuggy.MountPoints += 3 -> 2 threemanheavybuggy.TrunkSize = InventoryTile.Tile1511 threemanheavybuggy.TrunkOffset = 30 + threemanheavybuggy.AutoPilotSpeeds = (22, 8) + twomanheavybuggy.Name = "twomanheavybuggy" twomanheavybuggy.Seats += 0 -> new SeatDefinition() twomanheavybuggy.Seats(0).Bailable = true twomanheavybuggy.Seats += 1 -> new SeatDefinition() @@ -2110,7 +2170,9 @@ object GlobalDefinitions { twomanheavybuggy.MountPoints += 2 -> 1 twomanheavybuggy.TrunkSize = InventoryTile.Tile1511 twomanheavybuggy.TrunkOffset = 30 + twomanheavybuggy.AutoPilotSpeeds = (22, 8) + twomanhoverbuggy.Name = "twomanhoverbuggy" twomanhoverbuggy.Seats += 0 -> new SeatDefinition() twomanhoverbuggy.Seats(0).Bailable = true twomanhoverbuggy.Seats += 1 -> new SeatDefinition() @@ -2121,7 +2183,9 @@ object GlobalDefinitions { twomanhoverbuggy.MountPoints += 2 -> 1 twomanhoverbuggy.TrunkSize = InventoryTile.Tile1511 twomanhoverbuggy.TrunkOffset = 30 + twomanhoverbuggy.AutoPilotSpeeds = (22, 10) + mediumtransport.Name = "mediumtransport" mediumtransport.Seats += 0 -> new SeatDefinition() mediumtransport.Seats(0).ArmorRestriction = SeatArmorRestriction.NoReinforcedOrMax mediumtransport.Seats += 1 -> new SeatDefinition() @@ -2139,7 +2203,9 @@ object GlobalDefinitions { mediumtransport.MountPoints += 5 -> 4 mediumtransport.TrunkSize = InventoryTile.Tile1515 mediumtransport.TrunkOffset = 30 + mediumtransport.AutoPilotSpeeds = (18, 6) + battlewagon.Name = "battlewagon" battlewagon.Seats += 0 -> new SeatDefinition() battlewagon.Seats(0).ArmorRestriction = SeatArmorRestriction.NoReinforcedOrMax battlewagon.Seats += 1 -> new SeatDefinition() @@ -2161,7 +2227,9 @@ object GlobalDefinitions { battlewagon.MountPoints += 5 -> 4 battlewagon.TrunkSize = InventoryTile.Tile1515 battlewagon.TrunkOffset = 30 + battlewagon.AutoPilotSpeeds = (18, 6) + thunderer.Name = "thunderer" thunderer.Seats += 0 -> new SeatDefinition() thunderer.Seats(0).ArmorRestriction = SeatArmorRestriction.NoReinforcedOrMax thunderer.Seats += 1 -> new SeatDefinition() @@ -2179,7 +2247,9 @@ object GlobalDefinitions { thunderer.MountPoints += 5 -> 4 thunderer.TrunkSize = InventoryTile.Tile1515 thunderer.TrunkOffset = 30 + thunderer.AutoPilotSpeeds = (18, 6) + aurora.Name = "aurora" aurora.Seats += 0 -> new SeatDefinition() aurora.Seats(0).ArmorRestriction = SeatArmorRestriction.NoReinforcedOrMax aurora.Seats += 1 -> new SeatDefinition() @@ -2197,7 +2267,9 @@ object GlobalDefinitions { aurora.MountPoints += 5 -> 4 aurora.TrunkSize = InventoryTile.Tile1515 aurora.TrunkOffset = 30 + aurora.AutoPilotSpeeds = (18, 6) + apc_tr.Name = "apc_tr" apc_tr.Seats += 0 -> new SeatDefinition() apc_tr.Seats(0).ArmorRestriction = SeatArmorRestriction.NoReinforcedOrMax apc_tr.Seats += 1 -> new SeatDefinition() @@ -2238,7 +2310,9 @@ object GlobalDefinitions { apc_tr.MountPoints += 12 -> 10 apc_tr.TrunkSize = InventoryTile.Tile2016 apc_tr.TrunkOffset = 30 + apc_tr.AutoPilotSpeeds = (16, 6) + apc_nc.Name = "apc_nc" apc_nc.Seats += 0 -> new SeatDefinition() apc_nc.Seats(0).ArmorRestriction = SeatArmorRestriction.NoReinforcedOrMax apc_nc.Seats += 1 -> new SeatDefinition() @@ -2279,7 +2353,9 @@ object GlobalDefinitions { apc_nc.MountPoints += 12 -> 10 apc_nc.TrunkSize = InventoryTile.Tile2016 apc_nc.TrunkOffset = 30 + apc_nc.AutoPilotSpeeds = (16, 6) + apc_vs.Name = "apc_vs" apc_vs.Seats += 0 -> new SeatDefinition() apc_vs.Seats(0).ArmorRestriction = SeatArmorRestriction.NoReinforcedOrMax apc_vs.Seats += 1 -> new SeatDefinition() @@ -2320,7 +2396,9 @@ object GlobalDefinitions { apc_vs.MountPoints += 12 -> 10 apc_vs.TrunkSize = InventoryTile.Tile2016 apc_vs.TrunkOffset = 30 + apc_vs.AutoPilotSpeeds = (16, 6) + lightning.Name = "lightning" lightning.Seats += 0 -> new SeatDefinition() lightning.Seats(0).ArmorRestriction = SeatArmorRestriction.NoReinforcedOrMax lightning.Seats(0).ControlledWeapon = 1 @@ -2329,7 +2407,9 @@ object GlobalDefinitions { lightning.MountPoints += 2 -> 0 lightning.TrunkSize = InventoryTile.Tile1511 lightning.TrunkOffset = 30 + lightning.AutoPilotSpeeds = (20, 8) + prowler.Name = "prowler" prowler.Seats += 0 -> new SeatDefinition() prowler.Seats(0).ArmorRestriction = SeatArmorRestriction.NoReinforcedOrMax prowler.Seats += 1 -> new SeatDefinition() @@ -2343,7 +2423,9 @@ object GlobalDefinitions { prowler.MountPoints += 3 -> 2 prowler.TrunkSize = InventoryTile.Tile1511 prowler.TrunkOffset = 30 + prowler.AutoPilotSpeeds = (14, 6) + vanguard.Name = "vanguard" vanguard.Seats += 0 -> new SeatDefinition() vanguard.Seats(0).ArmorRestriction = SeatArmorRestriction.NoReinforcedOrMax vanguard.Seats += 1 -> new SeatDefinition() @@ -2353,7 +2435,9 @@ object GlobalDefinitions { vanguard.MountPoints += 2 -> 1 vanguard.TrunkSize = InventoryTile.Tile1511 vanguard.TrunkOffset = 30 + vanguard.AutoPilotSpeeds = (16, 6) + magrider.Name = "magrider" magrider.Seats += 0 -> new SeatDefinition() magrider.Seats(0).ArmorRestriction = SeatArmorRestriction.NoReinforcedOrMax magrider.Seats(0).ControlledWeapon = 2 @@ -2365,14 +2449,18 @@ object GlobalDefinitions { magrider.MountPoints += 2 -> 1 magrider.TrunkSize = InventoryTile.Tile1511 magrider.TrunkOffset = 30 + magrider.AutoPilotSpeeds = (18, 6) val utilityConverter = new UtilityVehicleConverter + ant.Name = "ant" ant.Seats += 0 -> new SeatDefinition() ant.Seats(0).ArmorRestriction = SeatArmorRestriction.NoReinforcedOrMax ant.MountPoints += 1 -> 0 ant.MountPoints += 2 -> 0 + ant.AutoPilotSpeeds = (18, 6) ant.Packet = utilityConverter + ams.Name = "ams" ams.Seats += 0 -> new SeatDefinition() ams.Seats(0).ArmorRestriction = SeatArmorRestriction.NoReinforcedOrMax ams.MountPoints += 1 -> 0 @@ -2384,9 +2472,11 @@ object GlobalDefinitions { ams.Deployment = true ams.DeployTime = 2000 ams.UndeployTime = 2000 + ams.AutoPilotSpeeds = (18, 6) ams.Packet = utilityConverter val variantConverter = new VariantVehicleConverter + router.Name = "router" router.Seats += 0 -> new SeatDefinition() router.MountPoints += 1 -> 0 router.TrunkSize = InventoryTile.Tile1511 @@ -2394,8 +2484,10 @@ object GlobalDefinitions { router.Deployment = true router.DeployTime = 2000 router.UndeployTime = 2000 + router.AutoPilotSpeeds = (16, 6) router.Packet = variantConverter + switchblade.Name = "switchblade" switchblade.Seats += 0 -> new SeatDefinition() switchblade.Seats(0).ControlledWeapon = 1 switchblade.Weapons += 1 -> scythe @@ -2406,8 +2498,10 @@ object GlobalDefinitions { switchblade.Deployment = true switchblade.DeployTime = 2000 switchblade.UndeployTime = 2000 + switchblade.AutoPilotSpeeds = (22, 8) switchblade.Packet = variantConverter + flail.Name = "flail" flail.Seats += 0 -> new SeatDefinition() flail.Seats(0).ControlledWeapon = 1 flail.Weapons += 1 -> flail_weapon @@ -2417,8 +2511,10 @@ object GlobalDefinitions { flail.Deployment = true flail.DeployTime = 2000 flail.UndeployTime = 2000 + flail.AutoPilotSpeeds = (14, 6) flail.Packet = variantConverter + mosquito.Name = "mosquito" mosquito.Seats += 0 -> new SeatDefinition() mosquito.Seats(0).Bailable = true mosquito.Seats(0).ControlledWeapon = 1 @@ -2427,8 +2523,10 @@ object GlobalDefinitions { mosquito.MountPoints += 2 -> 0 mosquito.TrunkSize = InventoryTile.Tile1111 mosquito.TrunkOffset = 30 + mosquito.AutoPilotSpeeds = (0, 6) mosquito.Packet = variantConverter + lightgunship.Name = "lightgunship" lightgunship.Seats += 0 -> new SeatDefinition() lightgunship.Seats(0).Bailable = true lightgunship.Seats(0).ControlledWeapon = 1 @@ -2437,8 +2535,10 @@ object GlobalDefinitions { lightgunship.MountPoints += 2 -> 0 lightgunship.TrunkSize = InventoryTile.Tile1511 lightgunship.TrunkOffset = 30 + lightgunship.AutoPilotSpeeds = (0, 4) lightgunship.Packet = variantConverter + wasp.Name = "wasp" wasp.Seats += 0 -> new SeatDefinition() wasp.Seats(0).Bailable = true wasp.Seats(0).ControlledWeapon = 1 @@ -2447,8 +2547,10 @@ object GlobalDefinitions { wasp.MountPoints += 2 -> 0 wasp.TrunkSize = InventoryTile.Tile1111 wasp.TrunkOffset = 30 + wasp.AutoPilotSpeeds = (0, 6) wasp.Packet = variantConverter + liberator.Name = "liberator" liberator.Seats += 0 -> new SeatDefinition() liberator.Seats(0).ControlledWeapon = 3 liberator.Seats += 1 -> new SeatDefinition() @@ -2464,8 +2566,10 @@ object GlobalDefinitions { liberator.MountPoints += 4 -> 2 liberator.TrunkSize = InventoryTile.Tile1515 liberator.TrunkOffset = 30 + liberator.AutoPilotSpeeds = (0, 4) liberator.Packet = variantConverter + vulture.Name = "vulture" vulture.Seats += 0 -> new SeatDefinition() vulture.Seats(0).ControlledWeapon = 3 vulture.Seats += 1 -> new SeatDefinition() @@ -2481,8 +2585,10 @@ object GlobalDefinitions { vulture.MountPoints += 4 -> 2 vulture.TrunkSize = InventoryTile.Tile1611 vulture.TrunkOffset = 30 + vulture.AutoPilotSpeeds = (0, 4) vulture.Packet = variantConverter + dropship.Name = "dropship" dropship.Seats += 0 -> new SeatDefinition() dropship.Seats += 1 -> new SeatDefinition() dropship.Seats(1).Bailable = true @@ -2528,8 +2634,10 @@ object GlobalDefinitions { dropship.MountPoints += 12 -> 10 dropship.TrunkSize = InventoryTile.Tile1612 dropship.TrunkOffset = 30 + dropship.AutoPilotSpeeds = (0, 4) dropship.Packet = variantConverter + galaxy_gunship.Name = "galaxy_gunship" galaxy_gunship.Seats += 0 -> new SeatDefinition() galaxy_gunship.Seats += 1 -> new SeatDefinition() galaxy_gunship.Seats(1).ControlledWeapon = 6 @@ -2554,14 +2662,18 @@ object GlobalDefinitions { galaxy_gunship.MountPoints += 6 -> 5 galaxy_gunship.TrunkSize = InventoryTile.Tile1816 galaxy_gunship.TrunkOffset = 30 + galaxy_gunship.AutoPilotSpeeds = (0, 4) galaxy_gunship.Packet = variantConverter + lodestar.Name = "lodestar" lodestar.Seats += 0 -> new SeatDefinition() lodestar.MountPoints += 1 -> 0 lodestar.TrunkSize = InventoryTile.Tile1612 lodestar.TrunkOffset = 30 + lodestar.AutoPilotSpeeds = (0, 4) lodestar.Packet = variantConverter + phantasm.Name = "phantasm" phantasm.CanCloak = true phantasm.Seats += 0 -> new SeatDefinition() phantasm.Seats += 1 -> new SeatDefinition() @@ -2579,6 +2691,7 @@ object GlobalDefinitions { phantasm.MountPoints += 5 -> 4 phantasm.TrunkSize = InventoryTile.Tile1107 phantasm.TrunkOffset = 30 + phantasm.AutoPilotSpeeds = (0, 6) phantasm.Packet = variantConverter } } diff --git a/common/src/main/scala/net/psforever/objects/definition/VehicleDefinition.scala b/common/src/main/scala/net/psforever/objects/definition/VehicleDefinition.scala index 4e5900d4..5bb11e89 100644 --- a/common/src/main/scala/net/psforever/objects/definition/VehicleDefinition.scala +++ b/common/src/main/scala/net/psforever/objects/definition/VehicleDefinition.scala @@ -28,6 +28,7 @@ class VehicleDefinition(objectId : Int) extends ObjectDefinition(objectId) { private var trunkOffset : Int = 0 private var canCloak : Boolean = false private var canBeOwned : Boolean = true + private var serverVehicleOverrideSpeeds : (Int, Int) = (0, 0) Name = "vehicle" Packet = VehicleDefinition.converter @@ -102,6 +103,17 @@ class VehicleDefinition(objectId : Int) extends ObjectDefinition(objectId) { trunkOffset = offset TrunkOffset } + + def AutoPilotSpeeds : (Int, Int) = serverVehicleOverrideSpeeds + + def AutoPilotSpeeds_=(speeds : (Int, Int)) : (Int, Int) = { + serverVehicleOverrideSpeeds = speeds + AutoPilotSpeeds + } + + def AutoPilotSpeed1 : Int = serverVehicleOverrideSpeeds._1 + + def AutoPilotSpeed2 : Int = serverVehicleOverrideSpeeds._2 } object VehicleDefinition { 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 8f1e75a4..47f8f119 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 @@ -1,205 +1,267 @@ // Copyright (c) 2017 PSForever package net.psforever.objects.serverobject.pad -import akka.actor.{Actor, ActorRef, Cancellable} +import akka.actor.{ActorContext, ActorRef, Cancellable, Props} import net.psforever.objects.serverobject.affinity.{FactionAffinity, FactionAffinityBehavior} +import net.psforever.objects.serverobject.pad.process.{VehicleSpawnControlBase, VehicleSpawnControlConcealPlayer} +import net.psforever.objects.zones.Zone import net.psforever.objects.{DefaultCancellable, Player, Vehicle} -import net.psforever.types.Vector3 +import scala.annotation.tailrec +import scala.concurrent.ExecutionContext.Implicits.global import scala.concurrent.duration._ /** - * An `Actor` that handles messages being dispatched to a specific `VehicleSpawnPad`.
+ * An `Actor` that handles vehicle spawning orders for a `VehicleSpawnPad`. + * The basic `VehicleSpawnControl` is the root of a simple tree of "spawn control" objects that chain to each other. + * Each object performs on (or more than one related) actions upon the vehicle order that was submitted.
*
+ * The purpose of the base actor is to serve as the entry point for the spawning process. * A spawn pad receives vehicle orders from an attached `Terminal` object. - * At the time when the order is received, the player who submitted the order is completely visible - * and waiting back by the said `Terminal` from where the order was submitted. - * Assuming no other orders are currently being processed, the repeated self message will retrieve this as the next order. - * The player character is first made transparent with a `GenericObjectActionMessage` packet. - * The vehicle model itself is then introduced to the game and three things happen with the following order, more or less:
- * 1. the vehicle is attached to a lifting platform that is designed to introduce the vehicle;
- * 2. the player is seated in the vehicle's driver seat (seat 0) and is thus declared the owner;
- * 3. various properties of the player, the vehicle, and the spawn pad itself are set `PlanetsideAttributesMessage`.
- * When this step is finished, the lifting platform raises the vehicle and the mounted player into the game world. - * The vehicle detaches and is made to roll off the spawn pad a certain distance before being released to user control. - * That is what is supposed to happen within a certain measure of timing.
- *
- * The orders that are submitted to the spawn pad must be composed of at least three elements: - * 1. a player, specifically the one that submitted the order and will be declared the "owner;" - * 2. a vehicle; - * 3. a callback location for sending messages. + * The control object accepts orders, enqueues them, and, + * whenever prompted by a previous complete order or by an absence of active orders, + * will select the first available order to be completed. + * This order will be "tracked" and will be given to the first functional "spawn control" object of the process. + * If the process is completed, or is ever aborted by any of the subsequent tasks, + * control will propagate down back to this control object. * @param pad the `VehicleSpawnPad` object being governed */ -class VehicleSpawnControl(pad : VehicleSpawnPad) extends Actor with FactionAffinityBehavior.Check { - /** an executor for progressing a vehicle order through the normal spawning logic */ - private var process : Cancellable = DefaultCancellable.obj +class VehicleSpawnControl(pad : VehicleSpawnPad) extends VehicleSpawnControlBase(pad) with FactionAffinityBehavior.Check { + /** a reminder sent to future customers */ + var periodicReminder : Cancellable = DefaultCancellable.obj /** a list of vehicle orders that have been submitted for this spawn pad */ - private var orders : List[VehicleSpawnControl.OrderEntry] = List.empty[VehicleSpawnControl.OrderEntry] - /** the current vehicle order being acted upon */ - private var trackedOrder : Option[VehicleSpawnControl.OrderEntry] = None - /** how many times a spawned vehicle (spatially) disrupted the next vehicle from being spawned */ - private var blockingViolations : Int = 0 - private[this] val log = org.log4s.getLogger - private[this] def trace(msg : String) : Unit = log.trace(msg) + private var orders : List[VehicleSpawnControl.Order] = List.empty[VehicleSpawnControl.Order] + /** the current vehicle order being acted upon; + * used as a guard condition to control order processing rate */ + private var trackedOrder : Option[VehicleSpawnControl.Order] = None + + def LogId = "" + + /** + * The first chained action of the vehicle spawning process. + */ + val concealPlayer = context.actorOf(Props(classOf[VehicleSpawnControlConcealPlayer], pad), s"${context.parent.path.name}-conceal") def FactionObject : FactionAffinity = pad def receive : Receive = checkBehavior.orElse { case VehicleSpawnPad.VehicleOrder(player, vehicle) => trace(s"order from $player for $vehicle received") - orders = orders :+ VehicleSpawnControl.OrderEntry(player, vehicle, sender) + orders = orders :+ VehicleSpawnControl.Order(player, vehicle, sender) if(trackedOrder.isEmpty && orders.length == 1) { - self ! VehicleSpawnControl.Process.GetOrder + SelectOrder() + } + else { + sender ! VehicleSpawnControl.RenderOrderRemainderMsg(orders.length + 1) } - case VehicleSpawnControl.Process.GetOrder => - process.cancel - blockingViolations = 0 - val (completeOrder, remainingOrders) : (Option[VehicleSpawnControl.OrderEntry], List[VehicleSpawnControl.OrderEntry]) = orders match { - case x :: Nil => - (Some(x), Nil) - case x :: b => - (Some(x), b) - case Nil => - (None, Nil) + case VehicleSpawnControl.ProcessControl.GetNewOrder => + if(sender == concealPlayer) { + trackedOrder = None //guard off + SelectOrder() } - orders = remainingOrders - completeOrder match { + + /* + When the vehicle is spawned and added to the pad, it will "occupy" the pad and block it from further action. + Normally, the player who wanted to spawn the vehicle will be automatically put into the driver seat. + If this is blocked, the vehicle will idle on the pad and must be moved far enough away from the point of origin. + During this time, a periodic message about the spawn pad being blocked + will be broadcast to all current customers in the order queue. + */ + case VehicleSpawnControl.ProcessControl.Reminder => + trackedOrder match { case Some(entry) => - trace(s"processing order $entry") - trackedOrder = completeOrder - import scala.concurrent.ExecutionContext.Implicits.global - process = context.system.scheduler.scheduleOnce(VehicleSpawnControl.concealPlayerTimeout, self, VehicleSpawnControl.Process.ConcealPlayer) + if(periodicReminder.isCancelled) { + trace (s"the pad has become blocked by ${entry.vehicle.Definition.Name}") + periodicReminder = context.system.scheduler.schedule( + VehicleSpawnControl.initialReminderDelay, + VehicleSpawnControl.periodicReminderDelay, + self, VehicleSpawnControl.ProcessControl.Reminder + ) + } + else { + VehicleSpawnControl.BlockedReminder(entry, entry +: orders) + } case None => ; - } - - case VehicleSpawnControl.Process.ConcealPlayer => - process.cancel - trackedOrder match { - case Some(entry) => - if(entry.player.isAlive && entry.vehicle.Actor != ActorRef.noSender && entry.sendTo != ActorRef.noSender && entry.player.VehicleSeated.isEmpty) { - trace(s"hiding player: ${entry.player}") - entry.sendTo ! VehicleSpawnPad.ConcealPlayer - import scala.concurrent.ExecutionContext.Implicits.global - process = context.system.scheduler.scheduleOnce(VehicleSpawnControl.loadVehicleTimeout, self, VehicleSpawnControl.Process.LoadVehicle) - } - else { - trace("integral component lost; abort order fulfillment") - //TODO Unregister vehicle ... somehow - trackedOrder = None - self ! VehicleSpawnControl.Process.GetOrder - } - case None => - self ! VehicleSpawnControl.Process.GetOrder - } - - case VehicleSpawnControl.Process.LoadVehicle => - process.cancel - trackedOrder match { - case Some(entry) => - if(entry.vehicle.Actor != ActorRef.noSender && entry.sendTo != ActorRef.noSender) { - trace(s"loading vehicle: ${entry.vehicle} defined in order") - entry.sendTo ! VehicleSpawnPad.LoadVehicle(entry.vehicle, pad) - import scala.concurrent.ExecutionContext.Implicits.global - process = context.system.scheduler.scheduleOnce(VehicleSpawnControl.awaitSeatedTimeout, self, VehicleSpawnControl.Process.AwaitSeated) - } - else { - trace("owner or vehicle lost; abort order fulfillment") - //TODO Unregister vehicle ... somehow - trackedOrder = None - self ! VehicleSpawnControl.Process.GetOrder - } - - case None => - self ! VehicleSpawnControl.Process.GetOrder - } - - case VehicleSpawnControl.Process.AwaitSeated => - process.cancel - trackedOrder match { - case Some(entry) => - if(entry.sendTo != ActorRef.noSender) { - trace("owner seated in vehicle") - import scala.concurrent.ExecutionContext.Implicits.global - process = if(entry.player.VehicleOwned.contains(entry.vehicle.GUID)) { - entry.sendTo ! VehicleSpawnPad.PlayerSeatedInVehicle(entry.vehicle) - context.system.scheduler.scheduleOnce(VehicleSpawnControl.awaitClearanceTimeout, self, VehicleSpawnControl.Process.AwaitClearance) - } - else { - context.system.scheduler.scheduleOnce(VehicleSpawnControl.awaitSeatedTimeout, self, VehicleSpawnControl.Process.AwaitSeated) - } - } - else { - trace("owner lost; abort order fulfillment") - trackedOrder = None - self ! VehicleSpawnControl.Process.GetOrder - } - case None => - self ! VehicleSpawnControl.Process.GetOrder - } - - //TODO raise spawn pad rails from ground - - //TODO start auto drive away - - //TODO release auto drive away - - case VehicleSpawnControl.Process.AwaitClearance => - process.cancel - trackedOrder match { - case Some(entry) => - if(entry.sendTo == ActorRef.noSender || entry.vehicle.Actor == ActorRef.noSender) { - trace("integral component lost, but order fulfilled; process next order") - trackedOrder = None - self ! VehicleSpawnControl.Process.GetOrder - } - else if(Vector3.DistanceSquared(entry.vehicle.Position, pad.Position) > 100.0f) { //10m away from pad - trace("pad cleared; process next order") - trackedOrder = None - entry.sendTo ! VehicleSpawnPad.SpawnPadUnblocked(entry.vehicle.GUID) - self ! VehicleSpawnControl.Process.GetOrder - } - else { - trace(s"pad blocked by ${entry.vehicle} ...") - blockingViolations += 1 - entry.sendTo ! VehicleSpawnPad.SpawnPadBlockedWarning(entry.vehicle, blockingViolations) - import scala.concurrent.ExecutionContext.Implicits.global - process = context.system.scheduler.scheduleOnce(VehicleSpawnControl.awaitClearanceTimeout, self, VehicleSpawnControl.Process.AwaitClearance) - } - case None => - self ! VehicleSpawnControl.Process.GetOrder + periodicReminder.cancel } case _ => ; } + + def SelectOrder() : Unit = { + trackedOrder match { + case None => + periodicReminder.cancel + val (completeOrder, remainingOrders) : (Option[VehicleSpawnControl.Order], List[VehicleSpawnControl.Order]) = orders match { + case x :: Nil => + (Some(x), Nil) + case x :: b => + trace(s"order backlog size: ${b.size}") + VehicleSpawnControl.recursiveOrderReminder(b.iterator) + (Some(x), b) + case Nil => + (None, Nil) + } + orders = remainingOrders + completeOrder match { + case Some(entry) => + trace(s"processing next order - a ${entry.vehicle.Definition.Name} for ${entry.driver.Name}") + trackedOrder = completeOrder //guard on + context.system.scheduler.scheduleOnce(2000 milliseconds, concealPlayer, VehicleSpawnControl.Process.ConcealPlayer(entry)) + case None => + trackedOrder = None + } + case Some(_) => ; //do not work on new orders + } + } } object VehicleSpawnControl { - final val concealPlayerTimeout : FiniteDuration = 2000000000L nanoseconds //2s - final val loadVehicleTimeout : FiniteDuration = 1000000000L nanoseconds //1s - final val awaitSeatedTimeout : FiniteDuration = 1000000000L nanoseconds //1s - final val awaitClearanceTimeout : FiniteDuration = 5000000000L nanoseconds //5s + private final val initialReminderDelay : FiniteDuration = 10000 milliseconds + private final val periodicReminderDelay : FiniteDuration = 10000 milliseconds /** - * An `Enumeration` of the stages of a full vehicle spawning process, associated with certain messages passed. - * Some stages are currently TEMPORARY. - * @see VehicleSpawnPad + * An `Enumeration` of non-data control messages for the vehicle spawn process. */ - object Process extends Enumeration { + object ProcessControl extends Enumeration { val - GetOrder, - ConcealPlayer, - LoadVehicle, - AwaitSeated, - AwaitClearance + Reminder, + GetNewOrder = Value } + /** + * An `Enumeration` of the stages of a full vehicle spawning process, passing the current order being processed. + * Messages in this group are used by the `receive` entry points of the multiple child objects + * that perform the vehicle spawning operation. + */ + object Process { + sealed class Order(entry : VehicleSpawnControl.Order) + + final case class ConcealPlayer(entry : VehicleSpawnControl.Order) extends Order(entry) + final case class LoadVehicle(entry : VehicleSpawnControl.Order) extends Order(entry) + final case class SeatDriver(entry : VehicleSpawnControl.Order) extends Order(entry) + final case class RailJackAction(entry : VehicleSpawnControl.Order) extends Order(entry) + final case class ServerVehicleOverride(entry : VehicleSpawnControl.Order) extends Order(entry) + final case class StartGuided(entry : VehicleSpawnControl.Order) extends Order(entry) + final case class DriverVehicleControl(entry : VehicleSpawnControl.Order) extends Order(entry) + final case class FinalClearance(entry : VehicleSpawnControl.Order) extends Order(entry) + } /** * An entry that stores vehicle spawn pad spawning tasks (called "orders"). - * @param player the player + * @param driver the player who wants the vehicle * @param vehicle the vehicle * @param sendTo a callback `Actor` associated with the player (in other words, `WorldSessionActor`) */ - private final case class OrderEntry(player : Player, vehicle : Vehicle, sendTo : ActorRef) + final case class Order(driver : Player, vehicle : Vehicle, sendTo : ActorRef) + + /** + * Properly clean up a vehicle that has been registered, but not yet been spawned into the game world.
+ *
+ * Constructs a temporary `TaskResolver` to deal with the vehicle's registration status. + * This "temporary" router will persist as if it were a `static` variable in some other language + * due to the fact that the `ActorSystem` object will remember it existing. + * After the primary task is complete, the router that was created is stopped so that it can be garbage collected. + * We could re-use it theoretically, but the `context` might be untrustworthy. + * @param entry the order being cancelled + * @param zone the continent on which the vehicle was registered + * @param context an `ActorContext` object for which to create the `TaskResolver` object + */ + def DisposeVehicle(entry : VehicleSpawnControl.Order, zone: Zone)(implicit context : ActorContext) : Unit = { + import akka.actor.{ActorRef, PoisonPill} + import akka.routing.SmallestMailboxPool + import net.psforever.objects.guid.{GUIDTask, Task, TaskResolver} + import net.psforever.types.Vector3 + val vehicle = entry.vehicle + vehicle.Position = Vector3.Zero + zone.VehicleEvents ! VehicleSpawnPad.RevealPlayer(entry.driver.GUID, zone.Id) + + val router = context.actorOf( + SmallestMailboxPool(10).props(Props[TaskResolver]), + s"vehicle-spawn-control-emergency-decon-resolver-${System.nanoTime}" + ) + router ! + TaskResolver.GiveTask( + new Task() { + private val localRouter = router + + override def isComplete = Task.Resolution.Success + + def Execute(resolver : ActorRef) : Unit = { + resolver ! scala.util.Success(this) + } + + override def Cleanup() : Unit = { localRouter ! PoisonPill } //where the router is stopped + }, List(GUIDTask.UnregisterVehicle(vehicle)(zone.GUID)) + ) + } + + /** + * Properly clean up a vehicle that has been registered and spawned into the game world. + * @param entry the order being cancelled + * @param zone the continent on which the vehicle was registered + */ + def DisposeSpawnedVehicle(entry : VehicleSpawnControl.Order, zone: Zone) : Unit = { + //TODO this cleanup will handle the vehicle; but, the former driver may be thrown into the void + zone.VehicleEvents ! VehicleSpawnPad.DisposeVehicle(entry.vehicle, zone) + zone.VehicleEvents ! VehicleSpawnPad.RevealPlayer(entry.driver.GUID, zone.Id) + } + + /** + * Remind a customer how long it will take for their vehicle order to be processed. + * @param position position in the queue + * @return an index-appropriate `VehicleSpawnPad.PeriodicReminder` object + */ + def RenderOrderRemainderMsg(position : Int) : VehicleSpawnPad.PeriodicReminder = { + VehicleSpawnPad.PeriodicReminder(VehicleSpawnPad.Reminders.Queue, Some(s"$position")) + } + + /** + * + * @param blockedOrder the previous order whose vehicle is blocking the spawn pad from operating + * @param recipients all of the customers who will be receiving the message + */ + def BlockedReminder(blockedOrder : VehicleSpawnControl.Order, recipients : Seq[VehicleSpawnControl.Order]) : Unit = { + val wrecked : Option[Any] = if(blockedOrder.vehicle.Health == 0) { + Option("Clear the wreckage.") + } + else { + None + } + VehicleSpawnControl.recursiveBlockedReminder(recipients.iterator, wrecked) + } + +// @tailrec private final def recursiveFindOrder(iter : Iterator[VehicleSpawnControl.Order], target : ActorRef, index : Int = 0) : Option[Int] = { +// if(!iter.hasNext) { +// None +// } +// else { +// val recipient = iter.next +// if(recipient.sendTo == target) { +// Some(index) +// } +// else { +// recursiveFindOrder(iter, target, index + 1) +// } +// } +// } + + @tailrec private final def recursiveBlockedReminder(iter : Iterator[VehicleSpawnControl.Order], cause : Option[Any]) : Unit = { + if(iter.hasNext) { + val recipient = iter.next + recipient.sendTo ! VehicleSpawnPad.PeriodicReminder(VehicleSpawnPad.Reminders.Blocked, cause) + recursiveBlockedReminder(iter, cause) + } + } + + @tailrec private final def recursiveOrderReminder(iter : Iterator[VehicleSpawnControl.Order], position : Int = 2) : Unit = { + if(iter.hasNext) { + val recipient = iter.next + if(recipient.sendTo != ActorRef.noSender) { + recipient.sendTo ! RenderOrderRemainderMsg(position) + } + recursiveOrderReminder(iter, position + 1) + } + } } diff --git a/common/src/main/scala/net/psforever/objects/serverobject/pad/VehicleSpawnPad.scala b/common/src/main/scala/net/psforever/objects/serverobject/pad/VehicleSpawnPad.scala index 19df456d..7412b7cd 100644 --- a/common/src/main/scala/net/psforever/objects/serverobject/pad/VehicleSpawnPad.scala +++ b/common/src/main/scala/net/psforever/objects/serverobject/pad/VehicleSpawnPad.scala @@ -1,8 +1,10 @@ // Copyright (c) 2017 PSForever package net.psforever.objects.serverobject.pad +import net.psforever.objects.serverobject.pad.process.AutoDriveControls import net.psforever.objects.{Player, Vehicle} import net.psforever.objects.serverobject.structures.Amenity +import net.psforever.objects.zones.Zone import net.psforever.packet.game.PlanetSideGUID /** @@ -12,17 +14,43 @@ import net.psforever.packet.game.PlanetSideGUID * maintain the operative queue that introduces the vehicle into the game world and applies initial activity to it and * maintain a position and a direction where the vehicle will be made to appear (as a `PlanetSideServerObject`). * The actual functionality managed by this object is wholly found on its accompanying `Actor`. - * @param spDef the `ObjectDefinition` that constructs this object and maintains some of its immutable fields * @see `VehicleSpawnControl` + * @param spDef the `ObjectDefinition` that constructs this object and maintains some of its immutable fields */ class VehicleSpawnPad(spDef : VehicleSpawnPadDefinition) extends Amenity { + /** + * Use the in-game railed platform to lift the spawned vehicle out of the trench. + * When set, the client performs the standard vehicle entry procedure, including lifting platform animations. + * When unset, the client depicts the player manually boarding the new vehicle within the trench area. + * Eventually, the vehicle is then hoisted out into the open; without this set, that hoisting is abrupt. + * The main reason to disable this feature is to avoid an `ObjectAttachMessage` for an incorrect object designation. + * Unset if not guaranteed to have the correct globally unique id of the spawn pad. + */ + private var onRails : Boolean = true + + private var guidedPath : List[AutoDriveControls.Configuration] = Nil + + def Railed : Boolean = onRails + + def Railed_=(useRails : Boolean) : Boolean = { + onRails = useRails + Railed + } + + def Guide : List[AutoDriveControls.Configuration] = guidedPath + + def Guide_=(path : List[AutoDriveControls.Configuration]) : List[AutoDriveControls.Configuration] = { + guidedPath = path + Guide + } + def Definition : VehicleSpawnPadDefinition = spDef } object VehicleSpawnPad { /** - * Communicate to the spawn pad that it should enqueue the following vehicle. + * Message to the spawn pad to enqueue the following vehicle order. * This is the entry point to vehicle spawn pad functionality. * @param player the player who submitted the order (the "owner") * @param vehicle the vehicle produced from the order @@ -30,52 +58,114 @@ object VehicleSpawnPad { final case class VehicleOrder(player : Player, vehicle : Vehicle) /** - * The first callback step in spawning the vehicle. - * An packet `GenericObjectActionMessage(/player/, 36)`, when used on a player character, - * will cause that player character's model to fade into transparency. + * Message to indicate that a certain player should be made transparent. + * @see `GenericObjectActionMessage` + * @param player_guid the player + * @param zone_id the zone in which the spawn pad is located */ - final case class ConcealPlayer() + final case class ConcealPlayer(player_guid : PlanetSideGUID, zone_id : String) /** - * A callback step in spawning the vehicle. - * The vehicle is properly introduced into the game world. - * If information about the vehicle itself that is important to its spawning has not yet been set, - * this callback is the last ideal situation to set that properties without having to adjust the vehicle visually. - * The primary operation that should occur is a content-appropriate `ObjectCreateMessage` packet and - * having the player sit down in the driver's seat (seat 0) of the vehicle. + * Message is intended to undo the effects of the above message, `ConcealPlayer`. + * @see `ConcealPlayer` + * @param player_guid the player + * @param zone_id the zone in which the spawn pad is located + */ + final case class RevealPlayer(player_guid : PlanetSideGUID, zone_id : String) + + /** + * Message to properly introduce the vehicle into the zone. * @param vehicle the vehicle being spawned - * @param pad the pad + * @param zone the zone in which the spawn pad is located */ - final case class LoadVehicle(vehicle : Vehicle, pad : VehicleSpawnPad) + final case class LoadVehicle(vehicle : Vehicle, zone : Zone) /** - * A TEMPORARY callback step in spawning the vehicle. - * From a state of transparency, while the vehicle is attached to the lifting platform of the spawn pad, - * the player designated the "owner" by callback is made to sit in the driver's seat (always seat 0). - * This message is the next step after that. + * Message to attach the vehicle to the spawn pad's lifting platform ("put on rails"). + * The attachment process (to the third slot) itself begins autonomous operation of the lifting platform. + * @see `ObjectAttachMessage` * @param vehicle the vehicle being spawned + * @param pad the spawn pad + * @param zone_id the zone in which the spawn pad is located */ - final case class PlayerSeatedInVehicle(vehicle : Vehicle) + final case class AttachToRails(vehicle : Vehicle, pad : VehicleSpawnPad, zone_id : String) /** - * A TEMPORARY callback step in (successfully) spawning the vehicle. - * While the vehicle is still occupying the pad just after being spawned and its driver seat mounted, - * that vehicle is considered blocking the pad from being used for further spawning operations. - * This message allows the user to be made known about this blockage. + * Message to detach the vehicle from the spawn pad's lifting platform ("put on rails"). + * @see `ObjectDetachMessage` + * @param vehicle the vehicle being spawned + * @param pad the spawn pad + * @param zone_id the zone in which the spawn pad is located + */ + final case class DetachFromRails(vehicle : Vehicle, pad : VehicleSpawnPad, zone_id : String) + + /** + * Message that resets the spawn pad for its next order fulfillment operation by lowering the lifting platform. + * @see `GenericObjectActionMessage` + * @param pad the spawn pad + * @param zone_id the zone in which the spawn pad is located + */ + final case class ResetSpawnPad(pad : VehicleSpawnPad, zone_id : String) + + /** + * Message that acts as callback to the driver that the process of sitting in the driver seat will be initiated soon. + * This information should only be communicated to the driver's client only. + * @param vehicle the vehicle being spawned + * @param pad the spawn pad + */ + final case class StartPlayerSeatedInVehicle(vehicle : Vehicle, pad : VehicleSpawnPad) + + /** + * Message that acts as callback to the driver that the process of sitting in the driver seat should be finished. + * This information should only be communicated to the driver's client only. + * @param vehicle the vehicle being spawned + * @param pad the spawn pad + */ + final case class PlayerSeatedInVehicle(vehicle : Vehicle, pad : VehicleSpawnPad) //TODO while using fake rails + + /** + * Message that starts the newly-spawned vehicle to begin driving away from the spawn pad. + * Information about the driving process is available on the vehicle itself. + * This information should only be communicated to the driver's client only. + * @see `VehicleDefinition` * @param vehicle the vehicle - * @param warning_count the number of times a warning period has occurred + * @param pad the spawn pad */ - final case class SpawnPadBlockedWarning(vehicle : Vehicle, warning_count : Int) + final case class ServerVehicleOverrideStart(vehicle : Vehicle, pad : VehicleSpawnPad) /** - * A TEMPORARY callback step in (successfully) spawning the vehicle. - * While the vehicle is still occupying the pad just after being spawned and its driver seat mounted, - * that vehicle is considered blocking the pad from being used for further spawning operations. - * A timeout will begin counting until the vehicle is despawned automatically for its driver's negligence. - * This message is used to clear the deconstruction countdown, primarily. - * @param vehicle_guid the vehicle + * Message that transitions the newly-spawned vehicle into a cancellable auto-drive state. + * Information about the driving process is available on the vehicle itself. + * This information should only be communicated to the driver's client only. + * @see `VehicleDefinition` + * @param vehicle the vehicle + * @param pad the spawn pad */ - final case class SpawnPadUnblocked(vehicle_guid : PlanetSideGUID) + final case class ServerVehicleOverrideEnd(vehicle : Vehicle, pad : VehicleSpawnPad) + + /** + * Message to initiate the process of properly disposing of the vehicle that may have been or was spawned into the game world. + * @param vehicle the vehicle + * @param zone the zone in which the spawn pad is located + */ + final case class DisposeVehicle(vehicle : Vehicle, zone : Zone) + + /** + * Message to send targeted messages to the clients of specific users. + * @param reason the nature of the message + * @param data optional information for rendering the message to the client + */ + final case class PeriodicReminder(reason : Reminders.Value, data : Option[Any] = None) + + /** + * An `Enumeration` of reasons for sending a periodic reminder to the user. + */ + object Reminders extends Enumeration { + val + Queue, //optional data is the numeric position in the queue + Blocked //optional data is a message regarding the blockage + = Value + } /** * Overloaded constructor. @@ -90,7 +180,7 @@ object VehicleSpawnPad { import net.psforever.types.Vector3 /** - * Instantiate an configure a `VehicleSpawnPad` object + * Instantiate and configure a `VehicleSpawnPad` object * @param pos the position (used to determine spawn point) * @param orient the orientation (used to indicate spawn direction) * @param id the unique id that will be assigned to this entity diff --git a/common/src/main/scala/net/psforever/objects/serverobject/pad/process/AutoDriveControls.scala b/common/src/main/scala/net/psforever/objects/serverobject/pad/process/AutoDriveControls.scala new file mode 100644 index 00000000..5059c21f --- /dev/null +++ b/common/src/main/scala/net/psforever/objects/serverobject/pad/process/AutoDriveControls.scala @@ -0,0 +1,304 @@ +// Copyright (c) 2017 PSForever +package net.psforever.objects.serverobject.pad.process + +import net.psforever.objects.{GlobalDefinitions, Vehicle} +import net.psforever.types.Vector3 + +/** + * Instructions to be processed by `VehicleSpawnControlGuided`. + * These instructions coordinate basic vehicle manipulations such as driving, turning, and stopping. + * If defined, they will operate on a newly formed vehicle after it is released from its spawn pad lifting platform + * and after it has been issued at least one `ServerVehicleOverrideMsg` packet. + */ +object AutoDriveControls { + + /** + * A container that translates to a new `Setting` instruction. + * Instructions are maintained in this form until they will be used due to the nature of the `Setting` object. + * The least that this object needs to do is accept parameters that matches the specific `Setting` that it outputs. + */ + sealed trait Configuration { + def Create : Setting + } + + /** + * An instruction to be consumed by the cyclic operation of `VehicleSpawnControlGuided` + * and are created by a `Configuration` object. + * They are considered semi-immutable `case class` objects. + * Externally, they are immutable by proper Scala standards. + * Internally, they will be permitted `private` fields that can be modified the first time the object is used. + */ + sealed trait Setting { + /** + * The nature of the action being performed. + * @return an enumerated value that explains the purpose of the action + */ + def Type : State.Value + /** + * The delay in between checks to determine if this setting has accomplished its goal or has entered an invalid state. + * @return the length of the delay + */ + def Delay : Long = 200L + /** + * Data that is important for fulfilling the instruction on a user's client. + * Highly specific to the implementation. + * @return any data deemed important; `None`, if unnecessary + */ + def Data : Option[Any] = None + /** + * Perform a test to determine if the vehicle is capable of performing the action this instruction requires. + * The test is typically simplistic in nature and often boils down to whether o not the vehicle is mobile. + * @param vehicle the vehicle being controlled + * @return `true`, if the action can (probably) be accomplished under the current conditions; `false`, otherwise + */ + def Validate(vehicle : Vehicle) : Boolean = Vector3.MagnitudeSquared(vehicle.Velocity.getOrElse(Vector3.Zero).xy) > 0 + /** + * Perform a test to determine if the vehicle has reached a set of conditions + * where the action performed by the instruction has been fulfilled. + * This should count as the "end of this step" and the "beginning of the next step." + * @param vehicle the vehicle being controlled + * @return `true`, if the action has run to completion; `false`, otherwise + */ + def CompletionTest(vehicle : Vehicle) : Boolean + } + + /** + * The nature of the action being performed. + * Different actions can be divided into types. + */ + object State extends Enumeration { + val + Cancel, + Climb, + Drive, + Stop, + Turn, + Wait + = Value + } + + protected final case class AutoDrive(speed : Int) extends Setting { + def Type = State.Drive + + override def Data = Some(speed) + + override def Validate(vehicle : Vehicle) : Boolean = true + + def CompletionTest(vehicle : Vehicle) = Vector3.MagnitudeSquared(vehicle.Velocity.getOrElse(Vector3.Zero).xy) > 0 + } + + protected final case class AutoDriveDistance(start : Vector3, sqDistance : Float) extends Setting { + def Type = State.Wait + + def CompletionTest(vehicle : Vehicle) : Boolean = { + Vector3.DistanceSquared(vehicle.Position.xy, start) > sqDistance + } + } + + protected final case class AutoDriveDistanceFromHere(sqDistance : Float) extends Setting { + private var start : Option[Vector3] = None + + def Type = State.Wait + + def CompletionTest(vehicle : Vehicle) : Boolean = { + val startLoc = start.getOrElse({ + start = Some(vehicle.Position.xy) + start.get + }) + Vector3.DistanceSquared(vehicle.Position.xy, startLoc) > sqDistance + } + } + + protected final case class AutoDriveForTime(length : Long) extends Setting { + private var start : Option[Long] = None + + def Type = State.Wait + + def CompletionTest(vehicle : Vehicle) : Boolean = { + val time : Long = System.currentTimeMillis + val startTime = start.getOrElse({ + start = Some(time) + time + }) + time - startTime >= length + } + + override def Validate(vehicle : Vehicle) : Boolean = true + } + + protected final case class AutoDriveTurnBy(angle : Float, direction : Int) extends Setting { + private var end : Option[Float] = None + private var currAng : Float = 0f + + def Type = State.Turn + + override def Delay : Long = 100L //increased frequency + + override def Data = Some(direction) + + def CompletionTest(vehicle : Vehicle) : Boolean = { + val endAng = end.getOrElse { + currAng = vehicle.Orientation.z + var ang = (currAng + angle) % 360f + if(ang < 0f) { + ang += 360f + } + end = Some(ang) + ang + } + val lastAng = currAng + currAng = vehicle.Orientation.z + //check that the expected angle is sandwiched between the previous angle and the current angle + currAng == endAng || (lastAng < endAng && endAng <= currAng) || (lastAng > endAng && endAng >= currAng) + } + + override def Validate(vehicle : Vehicle) : Boolean = direction != 15 && super.Validate(vehicle) + } + + protected final case class AutoDriveFirstGear() extends Setting { + private var speed : Int = 0 + + def Type = State.Drive + + override def Data = Some(speed) + + def CompletionTest(vehicle : Vehicle) = Vector3.MagnitudeSquared(vehicle.Velocity.getOrElse(Vector3.Zero)) > 0 + + override def Validate(vehicle : Vehicle) : Boolean = { + speed = vehicle.Definition.AutoPilotSpeed1 + true + } + } + + protected final case class AutoDriveSecondGear() extends Setting { + private var speed : Int = 0 + + def Type = State.Drive + + override def Data = Some(speed) + + def CompletionTest(vehicle : Vehicle) = Vector3.MagnitudeSquared(vehicle.Velocity.getOrElse(Vector3.Zero)) > 0 + + override def Validate(vehicle : Vehicle) : Boolean = { + speed = vehicle.Definition.AutoPilotSpeed2 + true + } + } + + protected final case class AutoDriveClimb(altitude : Float) extends Setting { + def Type = State.Climb + + override def Data = Some(altitude) + + def CompletionTest(vehicle : Vehicle) = { + vehicle.Position.z >= altitude + } + + override def Validate(vehicle : Vehicle) : Boolean = GlobalDefinitions.isFlightVehicle(vehicle.Definition) + } + + protected final case class AutoDriveCancelEarly(test : (Vehicle) => Boolean) extends Setting { + def Type = State.Cancel + + def CompletionTest(vehicle : Vehicle) = true + + override def Validate(vehicle : Vehicle) : Boolean = test(vehicle) + } + + protected final case class AutoDriveStop() extends Setting { + def Type = State.Stop + + override def Validate(vehicle : Vehicle) : Boolean = true + + def CompletionTest(vehicle : Vehicle) = Validate(vehicle) + } + + /** + * Use a validation test to determine if the remainder of the instructions should be processed. + * @param test the custom valid conditions of the vehicle for continuing + */ + final case class CancelEarly(test : (Vehicle)=>Boolean) extends Configuration { + def Create : Setting = AutoDriveControls.AutoDriveCancelEarly(test) + } + + /** + * Gain altitude with a flying vehicle. + * The climb speed is fixed. + * @param altitude the vertical distance to ascend + */ + final case class Climb(altitude : Float) extends Configuration { + def Create : Setting = AutoDriveControls.AutoDriveClimb(altitude) + } + + /** + * Drive a certain distance from somewhere. + * @param start the fixed coordinates of the origin point + * @param distance how far from the origin point the vehicle should travel + */ + final case class Distance(start : Vector3, distance : Float) extends Configuration { + def Create : Setting = AutoDriveControls.AutoDriveDistance(start, distance * distance) + } + + /** + * Drive a certain distance from where the vehicle is at the time that the instruction is called. + * The starting position is the current position of the vehicle. + * @param distance how far from the origin point the vehicle should travel + */ + final case class DistanceFromHere(distance : Float) extends Configuration { + def Create : Setting = AutoDriveControls.AutoDriveDistanceFromHere(distance * distance) + } + + /** + * Basic drive forward instruction. + * @see `ServerVehicleOverrideMsg.forward_speed` + * @param speed the speed that the vehicle accelerates to; + * scaled in a curious way + */ + final case class Drive(speed : Int) extends Configuration { + def Create : Setting = AutoDriveControls.AutoDrive(speed) + } + + /** + * Special drive forward instruction. + * @see `ServerVehicleOverrideMsg.forward_speed` + * @see `VehicleDefinition.AutoPilotSpeed1` + */ + final case class FirstGear() extends Configuration { + def Create : Setting = AutoDriveControls.AutoDriveFirstGear() + } + + /** + * Drive or idle for a certain duration. + * The starting position is the current position of the vehicle. + * @param time how long to contiue driving under the current conditions + */ + final case class ForTime(time : Long) extends Configuration { + def Create : Setting = AutoDriveControls.AutoDriveForTime(time) + } + + /** + * Special drive forward instruction. + * @see `ServerVehicleOverrideMsg.forward_speed` + * @see `VehicleDefinition.AutoPilotSpeed2` + */ + final case class SecondGear() extends Configuration { + def Create : Setting = AutoDriveControls.AutoDriveSecondGear() + } + + /** + * Stop driving (but do not cancel the server override state). + */ + final case class Stop() extends Configuration { + def Create : Setting = AutoDriveControls.AutoDriveStop() + } + + /** + * Cause the vehicle to turn a certain amount. + * @see `VehicleMessage.wheel_direction` + * @param angle the angle by which to turn the vehicle + * @param direction the wheel direction of the vehicle + */ + final case class TurnBy(angle : Float, direction : Int) extends Configuration { + def Create : Setting = AutoDriveControls.AutoDriveTurnBy(angle, direction) + } +} diff --git a/common/src/main/scala/net/psforever/objects/serverobject/pad/process/VehicleSpawnControlBase.scala b/common/src/main/scala/net/psforever/objects/serverobject/pad/process/VehicleSpawnControlBase.scala new file mode 100644 index 00000000..284d0999 --- /dev/null +++ b/common/src/main/scala/net/psforever/objects/serverobject/pad/process/VehicleSpawnControlBase.scala @@ -0,0 +1,69 @@ +// Copyright (c) 2017 PSForever +package net.psforever.objects.serverobject.pad.process + +import akka.actor.Actor +import net.psforever.objects.serverobject.pad.VehicleSpawnPad +import net.psforever.objects.serverobject.structures.Building +import net.psforever.objects.zones.Zone +import org.log4s.Logger + +/** + * Base for all `VehicleSpawnControl`-related `Actor` classes. + * Provide a common convention for the logging system's name. + * Additional functionality that recovers the `Zone` of the owned amenity `VehicleSpawnPad`. + * @param pad a `VehicleSpawnPad` object + */ +abstract class VehicleSpawnControlBase(pad : VehicleSpawnPad) extends Actor { + /** the log reference */ + private var baseLogger : Option[Logger] = None + + /** + * Initialize, if appropriate, and provide a log-keeping agent for the requested task. + * If a consistent logger does not yet exist, initialize one that will be returned this time and for every subsequent request. + * If the underlying spawn pad has not been registered yet, however, produce a throw-away logger. + * @param logid a special identifier that distinguishes a logger whose name is built of common features + * @return a `Logger` object + */ + private def GetLogger(logid : String) : Logger = baseLogger match { + case None => + if(!pad.HasGUID || Continent == Zone.Nowhere) { + org.log4s.getLogger(s"uninitialized_${pad.Definition.Name}$logid") + } + else { + baseLogger = Some(org.log4s.getLogger(s"${Continent.Id}-${pad.Definition.Name}-${pad.GUID.guid}$logid")) + baseLogger.get + } + case Some(logger) => + logger + } + + /** + * Implement this to add a suffix to the identifying name of the logger. + * @return a special identifier that distinguishes a logger whose name is built of common features + */ + def LogId : String + + /** + * Act as if a variable for the logging agent. + * @return a `Logger` object + */ + def log : Logger = GetLogger(LogId) + + /** + * A common manner of utilizing the logging agent such that all messages have the same logging level. + * The default should be `trace`-level comments. + * No important messages should processed by this agent; only consume general vehicle spawn status. + * @param msg the message + */ + def trace(msg : String) : Unit = log.trace(msg) + + /** + * The continent the pad recognizes as a place of installation will change as its `Owner` changes. + * Originally, it belongs to a default non-`Building` object that is owned by a default non-`Zone` object. + * Eventually, it will belong to an active `Building` object that will belong to an active `Zone` object. + * With respect to `GetLogger(String)`, the active `Zone` object will be valid shortly after the object is registered, + * but will still be separated from being owned by a valid `Building` object by a few validation checks. + * @return the (current) `Zone` object + */ + def Continent : Zone = pad.Owner.asInstanceOf[Building].Zone +} 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 new file mode 100644 index 00000000..21baed97 --- /dev/null +++ b/common/src/main/scala/net/psforever/objects/serverobject/pad/process/VehicleSpawnControlConcealPlayer.scala @@ -0,0 +1,46 @@ +// Copyright (c) 2017 PSForever +package net.psforever.objects.serverobject.pad.process + +import akka.actor.{ActorRef, Props} +import net.psforever.objects.serverobject.pad.{VehicleSpawnControl, VehicleSpawnPad} + +import scala.concurrent.ExecutionContext.Implicits.global +import scala.concurrent.duration._ + +/** + * An `Actor` that handles vehicle spawning orders for a `VehicleSpawnPad`. + * The basic `VehicleSpawnControl` is the root of a simple tree of "spawn control" objects that chain to each other. + * Each object performs on (or more than one related) actions upon the vehicle order that was submitted.
+ *
+ * This object is the first link in the process chain that spawns the ordered vehicle. + * It is devoted to causing the prospective driver to become hidden during the first part of the process + * with the goal of appearing to be "teleported" into the driver seat. + * It has failure cases should the driver be in an incorrect state. + * @param pad the `VehicleSpawnPad` object being governed + */ +class VehicleSpawnControlConcealPlayer(pad : VehicleSpawnPad) extends VehicleSpawnControlBase(pad) { + def LogId = "-concealer" + + val loadVehicle = context.actorOf(Props(classOf[VehicleSpawnControlLoadVehicle], pad), s"${context.parent.path.name}-load") + + def receive : Receive = { + case VehicleSpawnControl.Process.ConcealPlayer(entry) => + val driver = entry.driver + //TODO how far can the driver get stray from the Terminal before his order is cancelled? + if(entry.sendTo != ActorRef.noSender && driver.Continent == Continent.Id && driver.VehicleSeated.isEmpty) { + trace(s"hiding ${driver.Name}") + Continent.VehicleEvents ! VehicleSpawnPad.ConcealPlayer(driver.GUID, Continent.Id) + context.system.scheduler.scheduleOnce(2000 milliseconds, loadVehicle, VehicleSpawnControl.Process.LoadVehicle(entry)) + } + else { + trace(s"integral component lost; abort order fulfillment") + VehicleSpawnControl.DisposeVehicle(entry, Continent) + context.parent ! VehicleSpawnControl.ProcessControl.GetNewOrder + } + + case msg @ (VehicleSpawnControl.ProcessControl.Reminder | VehicleSpawnControl.ProcessControl.GetNewOrder) => + context.parent ! msg + + case _ => ; + } +} 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 new file mode 100644 index 00000000..98db2d77 --- /dev/null +++ b/common/src/main/scala/net/psforever/objects/serverobject/pad/process/VehicleSpawnControlDriverControl.scala @@ -0,0 +1,54 @@ +// Copyright (c) 2017 PSForever +package net.psforever.objects.serverobject.pad.process + +import akka.actor.{ActorRef, Props} +import net.psforever.objects.serverobject.pad.{VehicleSpawnControl, VehicleSpawnPad} + +/** + * An `Actor` that handles vehicle spawning orders for a `VehicleSpawnPad`. + * The basic `VehicleSpawnControl` is the root of a simple tree of "spawn control" objects that chain to each other. + * Each object performs on (or more than one related) actions upon the vehicle order that was submitted.
+ *
+ * A certain amount of time after the server has asserted control over a newly-spawned vehicle, + * control of that vehicle is given over to the driver. + * It has failure cases should the driver be in an incorrect state. + * @param pad the `VehicleSpawnPad` object being governed + */ +class VehicleSpawnControlDriverControl(pad : VehicleSpawnPad) extends VehicleSpawnControlBase(pad) { + def LogId = "-overrider" + + val finalClear = context.actorOf(Props(classOf[VehicleSpawnControlFinalClearance], pad), s"${context.parent.path.name}-final") + + def receive : Receive = { + case VehicleSpawnControl.Process.DriverVehicleControl(entry) => + val vehicle = entry.vehicle + if(pad.Railed) { + Continent.VehicleEvents ! VehicleSpawnPad.ResetSpawnPad(pad, Continent.Id) + } + if(vehicle.Health == 0) { + trace(s"vehicle was already destroyed; but, everything is fine") + } + if(entry.sendTo != ActorRef.noSender) { + val driver = entry.driver + entry.sendTo ! VehicleSpawnPad.ServerVehicleOverrideEnd(vehicle, pad) + if(driver.VehicleSeated.contains(vehicle.GUID)) { + trace(s"returning control of ${vehicle.Definition.Name} to ${driver.Name}") + } + else { + trace(s"${driver.Name} is not seated in ${vehicle.Definition.Name}; vehicle controls have been locked") + } + } + else { + trace("can not properly return control to driver") + } + finalClear ! VehicleSpawnControl.Process.FinalClearance(entry) + + case msg @ (VehicleSpawnControl.ProcessControl.Reminder | VehicleSpawnControl.ProcessControl.GetNewOrder) => + context.parent ! msg + + case msg @ VehicleSpawnControl.Process.FinalClearance(_) => + finalClear ! msg + + case _ => ; + } +} diff --git a/common/src/main/scala/net/psforever/objects/serverobject/pad/process/VehicleSpawnControlFinalClearance.scala b/common/src/main/scala/net/psforever/objects/serverobject/pad/process/VehicleSpawnControlFinalClearance.scala new file mode 100644 index 00000000..c5f32288 --- /dev/null +++ b/common/src/main/scala/net/psforever/objects/serverobject/pad/process/VehicleSpawnControlFinalClearance.scala @@ -0,0 +1,44 @@ +// Copyright (c) 2017 PSForever +package net.psforever.objects.serverobject.pad.process + +import net.psforever.objects.serverobject.pad.{VehicleSpawnControl, VehicleSpawnPad} +import net.psforever.types.Vector3 + +import scala.concurrent.ExecutionContext.Implicits.global +import scala.concurrent.duration._ + +/** + * An `Actor` that handles vehicle spawning orders for a `VehicleSpawnPad`. + * The basic `VehicleSpawnControl` is the root of a simple tree of "spawn control" objects that chain to each other. + * Each object performs on (or more than one related) actions upon the vehicle order that was submitted.
+ *
+ * There is nothing left to do + * except make certain that the vehicle has moved far enough away from the spawn pad + * to not block the next order that may be queued. + * A long call is made to the root of this `Actor` object chain to start work on any subsequent vehicle order. + * @param pad the `VehicleSpawnPad` object being governed + */ +class VehicleSpawnControlFinalClearance(pad : VehicleSpawnPad) extends VehicleSpawnControlBase(pad) { + def LogId = "-clearer" + + def receive : Receive = { + case VehicleSpawnControl.Process.FinalClearance(entry) => + context.parent ! VehicleSpawnControl.ProcessControl.Reminder + self ! VehicleSpawnControlFinalClearance.Test(entry) + + case VehicleSpawnControlFinalClearance.Test(entry) => + if(Vector3.DistanceSquared(entry.vehicle.Position, pad.Position) > 100.0f) { //10m away from pad + trace("pad cleared") + context.parent ! VehicleSpawnControl.ProcessControl.GetNewOrder + } + else { + context.system.scheduler.scheduleOnce(2000 milliseconds, self, VehicleSpawnControlFinalClearance.Test(entry)) + } + + case _ => ; + } +} + +object VehicleSpawnControlFinalClearance { + private final case class Test(entry : VehicleSpawnControl.Order) +} diff --git a/common/src/main/scala/net/psforever/objects/serverobject/pad/process/VehicleSpawnControlGuided.scala b/common/src/main/scala/net/psforever/objects/serverobject/pad/process/VehicleSpawnControlGuided.scala new file mode 100644 index 00000000..9011082d --- /dev/null +++ b/common/src/main/scala/net/psforever/objects/serverobject/pad/process/VehicleSpawnControlGuided.scala @@ -0,0 +1,126 @@ +// Copyright (c) 2017 PSForever +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 +import scala.concurrent.duration._ + +/** + * An `Actor` that handles vehicle spawning orders for a `VehicleSpawnPad`. + * The basic `VehicleSpawnControl` is the root of a simple tree of "spawn control" objects that chain to each other. + * Each object performs on (or more than one related) actions upon the vehicle order that was submitted.
+ *
+ * After the vehicle has been released from the spawn pad lifting platform, + * it enters into an auto-drive mode that has at least two stages. + * An undefined number of stages cane be included, however. + * This can lead the newly-spawned vehicle through a rough pre-defined path.
+ *
+ * Throughout this process, the conditions of `ServerVehicleOverrideMsg` are still in effect. + * @param pad the `VehicleSpawnPad` object being governed + */ +class VehicleSpawnControlGuided(pad : VehicleSpawnPad) extends VehicleSpawnControlBase(pad) { + def LogId = "-guide" + + val driverControl = context.actorOf(Props(classOf[VehicleSpawnControlDriverControl], pad), s"${context.parent.path.name}-driver") + + def receive : Receive = { + case VehicleSpawnControl.Process.StartGuided(entry) => + pad.Guide match { + case Nil => + trace("no guided path for this pad") + driverControl ! VehicleSpawnControl.Process.DriverVehicleControl(entry) + case path => + self ! VehicleSpawnControlGuided.InitialGuided(entry, path.map { _.Create }) + } + + case VehicleSpawnControlGuided.SelectNextGuided(entry, actions) => + actions match { + case Nil | _ :: Nil => + trace("custom vehicle path completed") + driverControl ! VehicleSpawnControl.Process.DriverVehicleControl(entry) + case _ :: xs => + self ! VehicleSpawnControlGuided.InitialGuided(entry, xs) + } + + case VehicleSpawnControlGuided.InitialGuided(entry, actions) => + val vehicle = entry.vehicle + if(entry.sendTo != ActorRef.noSender && vehicle.Health != 0 && entry.driver.VehicleSeated.contains(vehicle.GUID) && actions.head.Validate(vehicle)) { + trace(s"custom vehicle path plotted - ${actions.head.Type}") + entry.sendTo ! VehicleSpawnControlGuided.GuidedControl(actions.head.Type, vehicle, actions.head.Data) + self ! VehicleSpawnControlGuided.ContinueGuided(entry, actions) + } + else { + trace(s"projected ${vehicle.Definition.Name} path interruption; exit guided mode") + driverControl ! VehicleSpawnControl.Process.DriverVehicleControl(entry) + } + + case VehicleSpawnControlGuided.ValidateGuided(entry, actions) => + val vehicle = entry.vehicle + if(entry.sendTo != ActorRef.noSender && vehicle.Health != 0 && entry.driver.VehicleSeated.contains(vehicle.GUID) && actions.head.Validate(vehicle)) { + self ! VehicleSpawnControlGuided.ContinueGuided(entry, actions) + } + else { + trace(s"plotted ${vehicle.Definition.Name} path interruption; exit guided mode") + driverControl ! VehicleSpawnControl.Process.DriverVehicleControl(entry) + } + + case VehicleSpawnControlGuided.ContinueGuided(entry, actions) => + if(actions.head.CompletionTest(entry.vehicle)) { + trace("step completed") + self ! VehicleSpawnControlGuided.SelectNextGuided(entry, actions) + } + else { + context.system.scheduler.scheduleOnce(actions.head.Delay milliseconds, self, VehicleSpawnControlGuided.ValidateGuided(entry, actions)) + } + + case msg @ (VehicleSpawnControl.ProcessControl.Reminder | VehicleSpawnControl.ProcessControl.GetNewOrder) => + context.parent ! msg + + case msg @ VehicleSpawnControl.Process.FinalClearance(_) => + driverControl ! msg + + case _ => ; + } +} + +object VehicleSpawnControlGuided { + /** + * Select the first instruction from the list. + * @param entry the vehicle order + * @param actions the list of instructions related to this spawn pad + */ + private final case class InitialGuided(entry : VehicleSpawnControl.Order, actions : List[AutoDriveControls.Setting]) + /** + * Swap to the next instruction, if it exists. + * @param entry the vehicle order + * @param actions the list of instructions related to this spawn pad + */ + private final case class SelectNextGuided(entry : VehicleSpawnControl.Order, actions : List[AutoDriveControls.Setting]) + /** + * The validation test determines whether the vehicle, the driver, and any other important elements + * are still in a state where the current instruction can be accomplished. + * If the validation test passes, the current instruction can continue to run to completion. + * If the validation test fails, the remainder of the instructions are aborted. + * @param entry the vehicle order + * @param actions the list of instructions related to this spawn pad + */ + private final case class ValidateGuided(entry : VehicleSpawnControl.Order, actions : List[AutoDriveControls.Setting]) + /** + * If the previous validation test passes, the current instruction can continue to run to completion. + * Once completed, the next instruction can be selected. + * @param entry the vehicle order + * @param actions the list of instructions related to this spawn pad + */ + private final case class ContinueGuided(entry : VehicleSpawnControl.Order, actions : List[AutoDriveControls.Setting]) + + /** + * A message that explains the current instruction in the guided mode to another agency. + * @param command the nature of the action being performed + * @param vehicle the vehicle being controlled + * @param data optional data used to process the instruction + */ + final case class GuidedControl(command : AutoDriveControls.State.Value, vehicle : Vehicle, data : Option[Any]) +} 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 new file mode 100644 index 00000000..2510387d --- /dev/null +++ b/common/src/main/scala/net/psforever/objects/serverobject/pad/process/VehicleSpawnControlLoadVehicle.scala @@ -0,0 +1,51 @@ +// Copyright (c) 2017 PSForever +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.types.Vector3 + +import scala.concurrent.ExecutionContext.Implicits.global +import scala.concurrent.duration._ + +/** + * An `Actor` that handles vehicle spawning orders for a `VehicleSpawnPad`. + * The basic `VehicleSpawnControl` is the root of a simple tree of "spawn control" objects that chain to each other. + * Each object performs on (or more than one related) actions upon the vehicle order that was submitted.
+ *
+ * This object introduces the vehicle into the game environment. + * The vehicle must be added to the `Continent`, loaded onto other players' clients, and given an initial timed deconstruction event. + * For actual details on this process, please refer to the external source represented by `Continent.VehicleEvents`. + * It has failure cases should the driver be in an incorrect state. + * @param pad the `VehicleSpawnPad` object being governed + */ +class VehicleSpawnControlLoadVehicle(pad : VehicleSpawnPad) extends VehicleSpawnControlBase(pad) { + def LogId = "-loader" + + val railJack = context.actorOf(Props(classOf[VehicleSpawnControlRailJack], pad), s"${context.parent.path.name}-rails") + + def receive : Receive = { + case VehicleSpawnControl.Process.LoadVehicle(entry) => + val vehicle = entry.vehicle + if(entry.driver.Continent == Continent.Id) { + trace(s"loading the ${vehicle.Definition.Name}") + if(pad.Railed) { + //load the vehicle in the spawn pad trench, underground, initially + vehicle.Position = vehicle.Position - Vector3(0, 0, if(GlobalDefinitions.isFlightVehicle(vehicle.Definition)) 9 else 5) + } + Continent.VehicleEvents ! VehicleSpawnPad.LoadVehicle(vehicle, Continent) + context.system.scheduler.scheduleOnce(100 milliseconds, railJack, VehicleSpawnControl.Process.RailJackAction(entry)) + } + else { + trace("owner lost; abort order fulfillment") + VehicleSpawnControl.DisposeVehicle(entry, Continent) + context.parent ! VehicleSpawnControl.ProcessControl.GetNewOrder + } + + case msg @ (VehicleSpawnControl.ProcessControl.Reminder | VehicleSpawnControl.ProcessControl.GetNewOrder) => + context.parent ! msg + + case _ => ; + } +} diff --git a/common/src/main/scala/net/psforever/objects/serverobject/pad/process/VehicleSpawnControlRailJack.scala b/common/src/main/scala/net/psforever/objects/serverobject/pad/process/VehicleSpawnControlRailJack.scala new file mode 100644 index 00000000..bb3bf5bd --- /dev/null +++ b/common/src/main/scala/net/psforever/objects/serverobject/pad/process/VehicleSpawnControlRailJack.scala @@ -0,0 +1,43 @@ +// Copyright (c) 2017 PSForever +package net.psforever.objects.serverobject.pad.process + +import akka.actor.Props +import net.psforever.objects.serverobject.pad.{VehicleSpawnControl, VehicleSpawnPad} + +import scala.concurrent.ExecutionContext.Implicits.global +import scala.concurrent.duration._ + +/** + * An `Actor` that handles vehicle spawning orders for a `VehicleSpawnPad`. + * The basic `VehicleSpawnControl` is the root of a simple tree of "spawn control" objects that chain to each other. + * Each object performs on (or more than one related) actions upon the vehicle order that was submitted.
+ *
+ * When the vehicle is added into the environment, it is attached to the spawn pad platform. + * On cue, the trapdoor of the platform will open, and the vehicle will be raised up into plain sight on a group of rails. + * These actions are actually integrated into previous stages and into later stages of the process. + * The primary objective to be completed is a specific place to start a frequent message to the other customers. + * It has failure cases should the driver be in an incorrect state. + * @param pad the `VehicleSpawnPad` object being governed + */ +class VehicleSpawnControlRailJack(pad : VehicleSpawnPad) extends VehicleSpawnControlBase(pad) { + def LogId = "-lifter" + + val seatDriver = context.actorOf(Props(classOf[VehicleSpawnControlSeatDriver], pad), s"${context.parent.path.name}-seat") + + def receive : Receive = { + case VehicleSpawnControl.Process.RailJackAction(entry) => + if(pad.Railed) { + trace(s"attaching vehicle to railed platform") + Continent.VehicleEvents ! VehicleSpawnPad.AttachToRails(entry.vehicle, pad, Continent.Id) + } + else { + trace(s"railed platform skipped; vehicle positioned in pad trench temporarily") + } + context.system.scheduler.scheduleOnce(10 milliseconds, seatDriver, VehicleSpawnControl.Process.SeatDriver(entry)) + + case msg @ (VehicleSpawnControl.ProcessControl.Reminder | VehicleSpawnControl.ProcessControl.GetNewOrder) => + context.parent ! msg + + case _ => ; + } +} 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 new file mode 100644 index 00000000..2b67fa47 --- /dev/null +++ b/common/src/main/scala/net/psforever/objects/serverobject/pad/process/VehicleSpawnControlSeatDriver.scala @@ -0,0 +1,136 @@ +// Copyright (c) 2017 PSForever +package net.psforever.objects.serverobject.pad.process + +import akka.actor.{ActorRef, Props} +import net.psforever.objects.serverobject.mount.Mountable +import net.psforever.objects.serverobject.pad.{VehicleSpawnControl, VehicleSpawnPad} + +import scala.concurrent.ExecutionContext.Implicits.global +import scala.concurrent.duration._ + +/** + * An `Actor` that handles vehicle spawning orders for a `VehicleSpawnPad`. + * The basic `VehicleSpawnControl` is the root of a simple tree of "spawn control" objects that chain to each other. + * Each object performs on (or more than one related) actions upon the vehicle order that was submitted.
+ *
+ * This object forces the prospective driver to take the driver seat. + * Multiple separate but sequentially significant steps occur within the scope of this object. + * First, this step waits for the vehicle to be completely ready to accept the driver. + * Second, this step triggers the player to actually be moved into the driver seat. + * Finally, this step waits until the driver is properly in the driver seat. + * It has failure cases should the driver or the vehicle be in an incorrect state. + * @see `ZonePopulationActor` + * @param pad the `VehicleSpawnPad` object being governed + */ +class VehicleSpawnControlSeatDriver(pad : VehicleSpawnPad) extends VehicleSpawnControlBase(pad) { + def LogId = "-usher" + + val vehicleOverride = context.actorOf(Props(classOf[VehicleSpawnControlServerVehicleOverride], pad), s"${context.parent.path.name}-override") + + def receive : Receive = { + case VehicleSpawnControl.Process.SeatDriver(entry) => + self ! VehicleSpawnControlSeatDriver.AwaitVehicleReadiness(entry) + + case VehicleSpawnControlSeatDriver.AwaitVehicleReadiness(entry) => + if(entry.vehicle.Actor == ActorRef.noSender) { //wait for a necessary vehicle component to be loaded + context.system.scheduler.scheduleOnce(50 milliseconds, self, VehicleSpawnControlSeatDriver.AwaitVehicleReadiness(entry)) + } + else { + trace("vehicle ready") + self ! VehicleSpawnControlSeatDriver.BeginDriverInSeat(entry) + } + + case VehicleSpawnControlSeatDriver.BeginDriverInSeat(entry) => + val driver = entry.driver + if(entry.sendTo != ActorRef.noSender && entry.vehicle.Health > 0 && driver.isAlive && driver.Continent == Continent.Id && driver.VehicleSeated.isEmpty) { + trace("driver to be made seated in vehicle") + entry.sendTo ! VehicleSpawnPad.StartPlayerSeatedInVehicle(entry.vehicle, pad) + entry.vehicle.Actor.tell(Mountable.TryMount(driver, 0), entry.sendTo) //entry.sendTo should handle replies to TryMount + context.system.scheduler.scheduleOnce(1000 milliseconds, self, VehicleSpawnControlSeatDriver.AwaitDriverInSeat(entry)) + } + else { + trace("driver lost; vehicle stranded on pad") + context.system.scheduler.scheduleOnce(1000 milliseconds, vehicleOverride, VehicleSpawnControl.Process.ServerVehicleOverride(entry)) + } + + case VehicleSpawnControlSeatDriver.AwaitDriverInSeat(entry) => + val driver = entry.driver + if(entry.sendTo == ActorRef.noSender || driver.Continent != Continent.Id) { + trace("driver lost, but operations can continue") + vehicleOverride ! VehicleSpawnControl.Process.ServerVehicleOverride(entry) + } + else if(driver.isAlive && driver.VehicleSeated.isEmpty) { + context.system.scheduler.scheduleOnce(100 milliseconds, self, VehicleSpawnControlSeatDriver.AwaitDriverInSeat(entry)) + } + else { + trace(s"driver is sitting down") + val time = if(pad.Railed) 1000 else VehicleSpawnControlSeatDriver.RaillessSeatAnimationTimes(entry.vehicle.Definition.Name) + context.system.scheduler.scheduleOnce(time milliseconds, self, VehicleSpawnControlSeatDriver.DriverInSeat(entry)) + } + + case VehicleSpawnControlSeatDriver.DriverInSeat(entry) => + if(entry.sendTo != ActorRef.noSender || entry.driver.Continent != Continent.Id) { + trace(s"driver ${entry.driver.Name} has taken the wheel") + entry.sendTo ! VehicleSpawnPad.PlayerSeatedInVehicle(entry.vehicle, pad) + } + else { + trace("driver lost, but operations can continue") + } + context.system.scheduler.scheduleOnce(250 milliseconds, vehicleOverride, VehicleSpawnControl.Process.ServerVehicleOverride(entry)) + + case msg @ (VehicleSpawnControl.ProcessControl.Reminder | VehicleSpawnControl.ProcessControl.GetNewOrder) => + context.parent ! msg + + case _ => ; + } +} + +object VehicleSpawnControlSeatDriver { + final case class AwaitVehicleReadiness(entry : VehicleSpawnControl.Order) + + final case class BeginDriverInSeat(entry : VehicleSpawnControl.Order) + + final case class AwaitDriverInSeat(entry : VehicleSpawnControl.Order) + + final case class DriverInSeat(entry : VehicleSpawnControl.Order) + + /** + * If the spawn pad associated with this `Actor` chain is not `Railed` - + * not guaranteed to have the correct ingame globally unique id of the spawn pad - + * then the animation of the driver boarding their vehicle will be displayed. + * Although the network is finicky, these times should compensate a beneficial visual delay. + * The BFRs, the Switchblade, and the Flail are all untested. + */ + private val RaillessSeatAnimationTimes : Map[String, Int] = Map( + "fury" -> 600, + "quadassault" -> 600, + "quadstealth" -> 600, + "two_man_assault_buggy" -> 1000, + "skyguard" -> 1300, + "threemanheavybuggy" -> 1000, + "twomanheavybuggy" -> 1800, + "twomanhoverbuggy" -> 1800, + "mediumtransport" -> 1300, + "battlewagon" -> 1300, + "thunderer" -> 1300, + "aurora" -> 1300, + "apc_tr" -> 2300, + "apc_nc" -> 2300, + "apc_vs" -> 2300, + "prowler" -> 1000, + "vanguard" -> 2000, + "magrider" -> 1800, + "ant" -> 2500, + "ams" -> 1000, + "router" -> 2500, + "mosquito" -> 2000, + "lightgunship" -> 2000, + "wasp" -> 2000, + "liberator" -> 1800, + "vulture" -> 1800, + "dropship" -> 2000, + "galaxy_gunship" -> 2000, + "lodestar" -> 2000, + "phantasm" -> 1800 + ).withDefaultValue(1000) +} diff --git a/common/src/main/scala/net/psforever/objects/serverobject/pad/process/VehicleSpawnControlServerVehicleOverride.scala b/common/src/main/scala/net/psforever/objects/serverobject/pad/process/VehicleSpawnControlServerVehicleOverride.scala new file mode 100644 index 00000000..41194d46 --- /dev/null +++ b/common/src/main/scala/net/psforever/objects/serverobject/pad/process/VehicleSpawnControlServerVehicleOverride.scala @@ -0,0 +1,60 @@ +// Copyright (c) 2017 PSForever +package net.psforever.objects.serverobject.pad.process + +import akka.actor.{ActorRef, Props} +import net.psforever.objects.serverobject.pad.{VehicleSpawnControl, VehicleSpawnPad} + +import scala.concurrent.ExecutionContext.Implicits.global +import scala.concurrent.duration._ + +/** + * An `Actor` that handles vehicle spawning orders for a `VehicleSpawnPad`. + * The basic `VehicleSpawnControl` is the root of a simple tree of "spawn control" objects that chain to each other. + * Each object performs on (or more than one related) actions upon the vehicle order that was submitted.
+ *
+ * This object asserts automated control over the vehicle's motion after it has been released from its lifting platform. + * Normally, the vehicle drives forward for a bit under its own power. + * After a certain amount of time, control of the vehicle is given over to the driver. + * It has failure cases should the driver be in an incorrect state. + * @param pad the `VehicleSpawnPad` object being governed + */ +class VehicleSpawnControlServerVehicleOverride(pad : VehicleSpawnPad) extends VehicleSpawnControlBase(pad) { + def LogId = "-overrider" + + val vehicleGuide = context.actorOf(Props(classOf[VehicleSpawnControlGuided], pad), s"${context.parent.path.name}-guide") + + def receive : Receive = { + case VehicleSpawnControl.Process.ServerVehicleOverride(entry) => + val vehicle = entry.vehicle + val pad_railed = pad.Railed + if(pad_railed) { + Continent.VehicleEvents ! VehicleSpawnPad.DetachFromRails(vehicle, pad, Continent.Id) + } + if(vehicle.Health == 0) { + trace(s"vehicle was already destroyed; but, everything is fine") + if(pad_railed) { + Continent.VehicleEvents ! VehicleSpawnPad.ResetSpawnPad(pad, Continent.Id) + } + vehicleGuide ! VehicleSpawnControl.Process.FinalClearance(entry) + } + else if(entry.sendTo != ActorRef.noSender && entry.driver.isAlive && entry.driver.Continent == Continent.Id && entry.driver.VehicleSeated.contains(vehicle.GUID)) { + trace(s"telling ${entry.driver.Name} that the server is assuming control of the ${vehicle.Definition.Name}") + entry.sendTo ! VehicleSpawnPad.ServerVehicleOverrideStart(vehicle, pad) + context.system.scheduler.scheduleOnce(3000 milliseconds, vehicleGuide, VehicleSpawnControl.Process.StartGuided(entry)) + } + else { + if(pad_railed) { + Continent.VehicleEvents ! VehicleSpawnPad.ResetSpawnPad(pad, Continent.Id) + } + vehicleGuide ! VehicleSpawnControl.Process.FinalClearance(entry) + } + + case msg @ (VehicleSpawnControl.ProcessControl.Reminder | VehicleSpawnControl.ProcessControl.GetNewOrder) => + context.parent ! msg + + case msg @ VehicleSpawnControl.Process.FinalClearance(_) => + vehicleGuide ! msg + + case _ => ; + } +} diff --git a/common/src/main/scala/net/psforever/objects/zones/Zone.scala b/common/src/main/scala/net/psforever/objects/zones/Zone.scala index 5104c7dc..87f6cbd1 100644 --- a/common/src/main/scala/net/psforever/objects/zones/Zone.scala +++ b/common/src/main/scala/net/psforever/objects/zones/Zone.scala @@ -14,7 +14,6 @@ import net.psforever.objects.serverobject.tube.SpawnTube import net.psforever.packet.game.PlanetSideGUID import net.psforever.types.Vector3 -import scala.annotation.tailrec import scala.collection.concurrent.TrieMap import scala.collection.mutable.ListBuffer import scala.collection.immutable.{Map => PairMap} @@ -50,11 +49,11 @@ class Zone(private val zoneId : String, zoneMap : ZoneMap, zoneNumber : Int) { guid.AddPool("dynamic", (3001 to 10000).toList).Selector = new RandomSelector //TODO unlump pools later; do not make too big /** A synchronized `List` of items (`Equipment`) dropped by players on the ground and can be collected again. */ private val equipmentOnGround : ListBuffer[Equipment] = ListBuffer[Equipment]() + /** */ + private val vehicles : ListBuffer[Vehicle] = ListBuffer[Vehicle]() /** Used by the `Zone` to coordinate `Equipment` dropping and collection requests. */ private var ground : ActorRef = ActorRef.noSender /** */ - private var vehicles : List[Vehicle] = List[Vehicle]() - /** */ private var transport : ActorRef = ActorRef.noSender /** */ private val players : TrieMap[Avatar, Option[Player]] = TrieMap[Avatar, Option[Player]]() @@ -66,6 +65,8 @@ class Zone(private val zoneId : String, zoneMap : ZoneMap, zoneNumber : Int) { private var buildings : PairMap[Int, Building] = PairMap.empty[Int, Building] /** key - spawn zone id, value - buildings belonging to spawn zone */ private var spawnGroups : Map[Building, List[SpawnTube]] = PairMap[Building, List[SpawnTube]]() + /** */ + private var vehicleEvents : ActorRef = ActorRef.noSender /** * Establish the basic accessible conditions necessary for a functional `Zone`.
@@ -89,7 +90,7 @@ class Zone(private val zoneId : String, zoneMap : ZoneMap, zoneNumber : Int) { implicit val guid : NumberPoolHub = this.guid //passed into builderObject.Build implicitly accessor = context.actorOf(RandomPool(25).props(Props(classOf[UniqueNumberSystem], guid, UniqueNumberSystem.AllocateNumberPoolActors(guid))), s"$Id-uns") ground = context.actorOf(Props(classOf[ZoneGroundActor], equipmentOnGround), s"$Id-ground") - transport = context.actorOf(Props(classOf[ZoneVehicleActor], this), s"$Id-vehicles") + transport = context.actorOf(Props(classOf[ZoneVehicleActor], this, vehicles), s"$Id-vehicles") population = context.actorOf(Props(classOf[ZonePopulationActor], this, players, corpses), s"$Id-players") Map.LocalObjects.foreach({ builderObject => builderObject.Build }) @@ -189,7 +190,7 @@ class Zone(private val zoneId : String, zoneMap : ZoneMap, zoneNumber : Int) { */ def EquipmentOnGround : List[Equipment] = equipmentOnGround.toList - def Vehicles : List[Vehicle] = vehicles + def Vehicles : List[Vehicle] = vehicles.toList def Players : List[Avatar] = players.keys.toList @@ -197,35 +198,6 @@ class Zone(private val zoneId : String, zoneMap : ZoneMap, zoneNumber : Int) { def Corpses : List[Player] = corpses.toList - def AddVehicle(vehicle : Vehicle) : List[Vehicle] = { - vehicles = vehicles :+ vehicle - Vehicles - } - - def RemoveVehicle(vehicle : Vehicle) : List[Vehicle] = { - vehicles = recursiveFindVehicle(vehicles.iterator, vehicle) match { - case Some(index) => - vehicles.take(index) ++ vehicles.drop(index + 1) - case None => ; - vehicles - } - Vehicles - } - - @tailrec private def recursiveFindVehicle(iter : Iterator[Vehicle], target : Vehicle, index : Int = 0) : Option[Int] = { - if(!iter.hasNext) { - None - } - else { - if(iter.next.equals(target)) { - Some(index) - } - else { - recursiveFindVehicle(iter, target, index + 1) - } - } - } - /** * Coordinate `Equipment` that has been dropped on the ground or to-be-dropped on the ground. * @return synchronized reference to the ground @@ -303,6 +275,15 @@ class Zone(private val zoneId : String, zoneMap : ZoneMap, zoneNumber : Int) { * @return the `Zone` object */ def ClientInitialization() : Zone = this + + def VehicleEvents : ActorRef = vehicleEvents + + def VehicleEvents_=(bus : ActorRef) : ActorRef = { + if(vehicleEvents == ActorRef.noSender) { + vehicleEvents = bus + } + VehicleEvents + } } object Zone { @@ -428,9 +409,15 @@ object Zone { */ final case class ItemFromGround(player : Player, item : Equipment) - final case class SpawnVehicle(vehicle : Vehicle) + object Vehicle { + final case class Spawn(vehicle : Vehicle) - final case class DespawnVehicle(vehicle : Vehicle) + final case class Despawn(vehicle : Vehicle) + + final case class CanNotSpawn(zone : Zone, vehicle : Vehicle, reason : String) + + final case class CanNotDespawn(zone : Zone, vehicle : Vehicle, reason : String) + } /** * Message to report the packet messages that initialize the client. diff --git a/common/src/main/scala/net/psforever/objects/zones/ZoneActor.scala b/common/src/main/scala/net/psforever/objects/zones/ZoneActor.scala index 0f93d918..f14f31bb 100644 --- a/common/src/main/scala/net/psforever/objects/zones/ZoneActor.scala +++ b/common/src/main/scala/net/psforever/objects/zones/ZoneActor.scala @@ -55,10 +55,10 @@ class ZoneActor(zone : Zone) extends Actor { zone.Ground forward msg //frwd to Vehicle Actor - case msg @ Zone.SpawnVehicle => + case msg @ Zone.Vehicle.Spawn => zone.Transport forward msg - case msg @ Zone.DespawnVehicle => + case msg @ Zone.Vehicle.Despawn => zone.Transport forward msg //own diff --git a/common/src/main/scala/net/psforever/objects/zones/ZoneVehicleActor.scala b/common/src/main/scala/net/psforever/objects/zones/ZoneVehicleActor.scala index b7d3c6a5..bdb2c2c3 100644 --- a/common/src/main/scala/net/psforever/objects/zones/ZoneVehicleActor.scala +++ b/common/src/main/scala/net/psforever/objects/zones/ZoneVehicleActor.scala @@ -1,22 +1,76 @@ // Copyright (c) 2017 PSForever package net.psforever.objects.zones -import akka.actor.Actor +import akka.actor.{Actor, ActorRef, Props} +import net.psforever.objects.Vehicle +import net.psforever.objects.vehicles.VehicleControl + +import scala.annotation.tailrec +import scala.collection.mutable.ListBuffer /** * Synchronize management of the list of `Vehicles` maintained by some `Zone`. - * @param zone the `Zone` object */ -class ZoneVehicleActor(zone : Zone) extends Actor { +//COMMENTS IMPORTED FROM FORMER VehicleContextActor: +/** + * Provide a context for a `Vehicle` `Actor` - the `VehicleControl`.
+ *
+ * A vehicle can be passed between different zones and, therefore, does not belong to the zone. + * A vehicle cna be given to different players and can persist and change though players have gone. + * Therefore, also does not belong to `WorldSessionActor`. + * A vehicle must anchored to something that exists outside of the `InterstellarCluster` and its agents.
+ *
+ * The only purpose of this `Actor` is to allow vehicles to borrow a context for the purpose of `Actor` creation. + * It is also be allowed to be responsible for cleaning up that context. + * (In reality, it can be cleaned up anywhere a `PoisonPill` can be sent.)
+ *
+ * This `Actor` is intended to sit on top of the event system that handles broadcast messaging. + */ +class ZoneVehicleActor(zone : Zone, vehicleList : ListBuffer[Vehicle]) extends Actor { //private[this] val log = org.log4s.getLogger def receive : Receive = { - case Zone.SpawnVehicle(vehicle) => - zone.AddVehicle(vehicle) + case Zone.Vehicle.Spawn(vehicle) => + if(!vehicle.HasGUID) { + sender ! Zone.Vehicle.CanNotSpawn(zone, vehicle, "not registered yet") + } + else if(vehicleList.contains(vehicle)) { + sender ! Zone.Vehicle.CanNotSpawn(zone, vehicle, "already in zone") + } + else if(vehicle.Actor != ActorRef.noSender) { + sender ! Zone.Vehicle.CanNotSpawn(zone, vehicle, "already in another zone") + } + else { + vehicleList += vehicle + vehicle.Actor = context.actorOf(Props(classOf[VehicleControl], vehicle), s"${vehicle.Definition.Name}_${vehicle.GUID.guid}") + } - case Zone.DespawnVehicle(vehicle) => - zone.RemoveVehicle(vehicle) + case Zone.Vehicle.Despawn(vehicle) => + ZoneVehicleActor.recursiveFindVehicle(vehicleList.iterator, vehicle) match { + case Some(index) => + vehicleList.remove(index) + vehicle.Actor ! akka.actor.PoisonPill + vehicle.Actor = ActorRef.noSender + case None => ; + sender ! Zone.Vehicle.CanNotDespawn(zone, vehicle, "can not find") + } case _ => ; } } + +object ZoneVehicleActor { + @tailrec final def recursiveFindVehicle(iter : Iterator[Vehicle], target : Vehicle, index : Int = 0) : Option[Int] = { + if(!iter.hasNext) { + None + } + else { + if(iter.next.equals(target)) { + Some(index) + } + else { + recursiveFindVehicle(iter, target, index + 1) + } + } + } +} diff --git a/common/src/main/scala/net/psforever/packet/GamePacketOpcode.scala b/common/src/main/scala/net/psforever/packet/GamePacketOpcode.scala index 1c118547..e7ebd353 100644 --- a/common/src/main/scala/net/psforever/packet/GamePacketOpcode.scala +++ b/common/src/main/scala/net/psforever/packet/GamePacketOpcode.scala @@ -410,7 +410,7 @@ object GamePacketOpcode extends Enumeration { case 0x4b => game.DeployRequestMessage.decode case 0x4c => noDecoder(UnknownMessage76) case 0x4d => game.RepairMessage.decode - case 0x4e => noDecoder(ServerVehicleOverrideMsg) + case 0x4e => game.ServerVehicleOverrideMsg.decode case 0x4f => game.LashMessage.decode // OPCODES 0x50-5f diff --git a/common/src/main/scala/net/psforever/packet/game/ServerVehicleOverrideMsg.scala b/common/src/main/scala/net/psforever/packet/game/ServerVehicleOverrideMsg.scala new file mode 100644 index 00000000..266396a8 --- /dev/null +++ b/common/src/main/scala/net/psforever/packet/game/ServerVehicleOverrideMsg.scala @@ -0,0 +1,73 @@ +// Copyright (c) 2017 PSForever +package net.psforever.packet.game + +import net.psforever.packet.{GamePacketOpcode, Marshallable, PlanetSideGamePacket} +import scodec.Codec +import scodec.codecs._ + +/** + * Dispatched by server to assert control of a player's vehicle, usually temporarily, and to relinquish that control.
+ *
+ * The "vehicle" counts as any mobile platform where the user's character is currently sitting. + * If the player is not sitting in what the game considers a "vehicle," the packet is wasted. + * Either of the first two parameters - `lock_accelerator` or `lock_wheel` - constitutes any vehicle being overrode. + * Either of the latter two parameters - `lock_thrust` or `lock_strafe` - constitutes a flight vehicle being overrode. + * No message is displayed if the vehicle is placed under any form of server control. + * During server control, this is an acceleration value (?); + * during cancellable auto-drive, a constant velocity value. + * Vertical thrust control for aircraft is either on or off; + * the amount of that thrust can not be controlled.
+ *
+ * After being controlled, when the vehicle is no longer under control, + * it will transition into a state of constant speed auto-drive. + * The message regarding the vehicle being back in the driver's control will display, + * unless one of the aforementioned `lock_*` parameters is still set to `true`. + * When dismounting a bailable vehicle while it is under the server's control, + * the player will behave like they are bailing from it. + * (The vehicle actually has to be "bailable" first, of course.)
+ *
+ * "Something like speed:"
+ * For ground vehicles, for `n`, the calculated in-game speed for the value in this packet will be at least `3.45 x n`. + * For flight vehicles, for `n`, the forward air speed for the value in this packet will be at least `1.18 * n`. + * This approximation is not always going to be accurate but serves as a good rule of thumb. + * @param lock_accelerator driver has no control over vehicle acceleration + * @param lock_wheel driver has no control over vehicle turning + * @param reverse move in reverse + * @param unk4 na; + * something to do with vehicle bailable speed + * @param lock_vthrust pilot has no control over vertical thrust; + * asserts a constant positive vertical thrust; + * the only valid setting appears to be 1 + * @param lock_strafe pilot has no control over strafing thrust; + * the only valid setting appears to be 1 + * @param forward_speed "something like speed" + * @param unk8 na; + * set `lock_wheel` to `true` to expose this value + */ +final case class ServerVehicleOverrideMsg(lock_accelerator : Boolean, + lock_wheel : Boolean, + reverse : Boolean, + unk4 : Boolean, + lock_vthrust : Int, + lock_strafe : Int, + forward_speed : Int, + unk8 : Option[Long] + ) extends PlanetSideGamePacket { + type Packet = ServerVehicleOverrideMsg + def opcode = GamePacketOpcode.ServerVehicleOverrideMsg + def encode = ServerVehicleOverrideMsg.encode(this) +} + +object ServerVehicleOverrideMsg extends Marshallable[ServerVehicleOverrideMsg] { + implicit val codec: Codec[ServerVehicleOverrideMsg] = ( + ("lock_accelerator" | bool) :: + (("lock_wheel" | bool) >>:~ { test => + ("reverse" | bool) :: + ("unk4" | bool) :: + ("lock_vthrust" | uint2L) :: + ("lock_strafe" | uint2L) :: + ("forward_speed" | uintL(9)) :: + conditional(test, "unk8" | uint32L) + }) + ).as[ServerVehicleOverrideMsg] +} diff --git a/common/src/main/scala/net/psforever/packet/game/VehicleStateMessage.scala b/common/src/main/scala/net/psforever/packet/game/VehicleStateMessage.scala index aa71caa2..3dbc037b 100644 --- a/common/src/main/scala/net/psforever/packet/game/VehicleStateMessage.scala +++ b/common/src/main/scala/net/psforever/packet/game/VehicleStateMessage.scala @@ -19,8 +19,8 @@ import scodec.codecs._ * @param unk4 na * @param wheel_direction for ground vehicles, whether the wheels are being turned; * 15 for straight; - * 0 for hard left; - * 30 for hard right + * 0 for hard right; + * 30 for hard left * @param unk5 na * @param unk6 na * @see `PlacementData` diff --git a/common/src/test/scala/game/ServerVehicleOverrideMsgTest.scala b/common/src/test/scala/game/ServerVehicleOverrideMsgTest.scala new file mode 100644 index 00000000..91a10784 --- /dev/null +++ b/common/src/test/scala/game/ServerVehicleOverrideMsgTest.scala @@ -0,0 +1,59 @@ +// Copyright (c) 2017 PSForever +package game + +import org.specs2.mutable._ +import net.psforever.packet._ +import net.psforever.packet.game._ +import scodec.bits._ + +class ServerVehicleOverrideMsgTest extends Specification { + val string1 = hex"4E C0 0C0 00000000 0" + val string2 = hex"4E 10 050 0" + + "decode (1)" in { + PacketCoding.DecodePacket(string1).require match { + case ServerVehicleOverrideMsg(u1, u2, u3, u4, u5, u6, u7, u8) => + u1 mustEqual true + u2 mustEqual true + u3 mustEqual false + u4 mustEqual false + u5 mustEqual 0 + u6 mustEqual 0 + u7 mustEqual 12 + u8.isDefined mustEqual true + u8.get mustEqual 0L + case _ => + ko + } + } + + "decode (2)" in { + PacketCoding.DecodePacket(string2).require match { + case ServerVehicleOverrideMsg(u1, u2, u3, u4, u5, u6, u7, u8) => + u1 mustEqual false + u2 mustEqual false + u3 mustEqual false + u4 mustEqual true + u5 mustEqual 0 + u6 mustEqual 0 + u7 mustEqual 5 + u8.isDefined mustEqual false + case _ => + ko + } + } + + "encode (1)" in { + val msg = ServerVehicleOverrideMsg(true, true, false, false, 0, 0, 12, Some(0L)) + val pkt = PacketCoding.EncodePacket(msg).require.toByteVector + + pkt mustEqual string1 + } + + "encode (2)" in { + val msg = ServerVehicleOverrideMsg(false, false, false, true, 0, 0, 5, None) + val pkt = PacketCoding.EncodePacket(msg).require.toByteVector + + pkt mustEqual string2 + } +} diff --git a/common/src/test/scala/objects/AutoDriveControlsTest.scala b/common/src/test/scala/objects/AutoDriveControlsTest.scala new file mode 100644 index 00000000..e26c7d9b --- /dev/null +++ b/common/src/test/scala/objects/AutoDriveControlsTest.scala @@ -0,0 +1,498 @@ +// Copyright (c) 2017 PSForever +package objects + +import akka.actor.Props +import akka.testkit.TestProbe +import net.psforever.objects.serverobject.pad.{VehicleSpawnControl, VehicleSpawnPad} +import net.psforever.objects.{Avatar, GlobalDefinitions, Player, Vehicle} +import net.psforever.objects.serverobject.pad.process.{AutoDriveControls, VehicleSpawnControlGuided} +import net.psforever.packet.game.PlanetSideGUID +import net.psforever.types.{CharacterGender, PlanetSideEmpire, Vector3} +import org.specs2.mutable.Specification + +import scala.concurrent.duration._ + +class AutoDriveControlsTest extends Specification { + "CancelEntry" should { + val vehicle = Vehicle(GlobalDefinitions.fury) + def exampleTest(vehicle : Vehicle) : Boolean = { vehicle.Position == Vector3(1,1,1) } + + "create" in { + val config : AutoDriveControls.Configuration = AutoDriveControls.CancelEarly(exampleTest) + val setting : AutoDriveControls.Setting = config.Create + setting.Type mustEqual AutoDriveControls.State.Cancel + setting.Data mustEqual None + setting.Delay mustEqual 200L + } + + "validate" in { + val config : AutoDriveControls.Configuration = AutoDriveControls.CancelEarly(exampleTest) + val setting : AutoDriveControls.Setting = config.Create + + vehicle.Position mustEqual Vector3.Zero + setting.Validate(vehicle) mustEqual false + vehicle.Position = Vector3(1,1,1) + setting.Validate(vehicle) mustEqual true + } + + "completion" in { + val config : AutoDriveControls.Configuration = AutoDriveControls.CancelEarly(exampleTest) + val setting : AutoDriveControls.Setting = config.Create + setting.CompletionTest(vehicle) mustEqual true //always true + } + } + + "Climb" should { + val vehicle = Vehicle(GlobalDefinitions.mosquito) + + "create" in { + val config : AutoDriveControls.Configuration = AutoDriveControls.Climb(10.5f) + val setting : AutoDriveControls.Setting = config.Create + setting.Type mustEqual AutoDriveControls.State.Climb + setting.Data mustEqual Some(10.5f) + setting.Delay mustEqual 200L + } + + "validate" in { + val vehicle_fury = Vehicle(GlobalDefinitions.fury) + val config : AutoDriveControls.Configuration = AutoDriveControls.Climb(10.5f) + val setting : AutoDriveControls.Setting = config.Create + + setting.Validate(vehicle) mustEqual true //mosquito is a flying vehicle + setting.Validate(vehicle_fury) mustEqual false + } + + "completion" in { + val config : AutoDriveControls.Configuration = AutoDriveControls.Climb(10.5f) + val setting : AutoDriveControls.Setting = config.Create + + vehicle.Position mustEqual Vector3.Zero + setting.CompletionTest(vehicle) mustEqual false + vehicle.Position = Vector3(0,0,10.5f) + setting.CompletionTest(vehicle) mustEqual true + } + } + + "Distance" should { + val vehicle = Vehicle(GlobalDefinitions.fury) + + "create" in { + val config : AutoDriveControls.Configuration = AutoDriveControls.Distance(Vector3.Zero, 10.5f) + val setting : AutoDriveControls.Setting = config.Create + setting.Type mustEqual AutoDriveControls.State.Wait + setting.Data mustEqual None + setting.Delay mustEqual 200L + } + "validate" in { + val config : AutoDriveControls.Configuration = AutoDriveControls.Distance(Vector3.Zero, 10.5f) + val setting : AutoDriveControls.Setting = config.Create + + vehicle.Velocity mustEqual None + setting.Validate(vehicle) mustEqual false + vehicle.Velocity = Vector3.Zero + setting.Validate(vehicle) mustEqual false + vehicle.Velocity = Vector3(1,0,0) + setting.Validate(vehicle) mustEqual true + } + + "completion" in { + val config : AutoDriveControls.Configuration = AutoDriveControls.Distance(Vector3.Zero, 10.5f) + val setting : AutoDriveControls.Setting = config.Create + + vehicle.Position = Vector3(0,0,0) + setting.CompletionTest(vehicle) mustEqual false + vehicle.Position = Vector3(10.5f,0,0) + setting.CompletionTest(vehicle) mustEqual false + vehicle.Position = Vector3(11,0,0) + setting.CompletionTest(vehicle) mustEqual true + vehicle.Position = Vector3(0,11,0) + setting.CompletionTest(vehicle) mustEqual true + vehicle.Position = Vector3(0,0,11) + setting.CompletionTest(vehicle) mustEqual false + vehicle.Position = Vector3(7.5f,7.5f,0) + setting.CompletionTest(vehicle) mustEqual true + } + } + + "DistanceFromHere" should { + val vehicle = Vehicle(GlobalDefinitions.fury) + + "create" in { + val config : AutoDriveControls.Configuration = AutoDriveControls.DistanceFromHere(10.5f) + val setting : AutoDriveControls.Setting = config.Create + setting.Type mustEqual AutoDriveControls.State.Wait + setting.Data mustEqual None + setting.Delay mustEqual 200L + } + + "validate" in { + val config : AutoDriveControls.Configuration = AutoDriveControls.DistanceFromHere(10.5f) + val setting : AutoDriveControls.Setting = config.Create + + vehicle.Velocity mustEqual None + setting.Validate(vehicle) mustEqual false + vehicle.Velocity = Vector3.Zero + setting.Validate(vehicle) mustEqual false + vehicle.Velocity = Vector3(1,0,0) + setting.Validate(vehicle) mustEqual true + } + + "completion" in { + val config : AutoDriveControls.Configuration = AutoDriveControls.DistanceFromHere(10.5f) + val setting : AutoDriveControls.Setting = config.Create + + vehicle.Position = Vector3(0,0,0) + setting.CompletionTest(vehicle) mustEqual false + vehicle.Position = Vector3(10.5f,0,0) + setting.CompletionTest(vehicle) mustEqual false + vehicle.Position = Vector3(11,0,0) + setting.CompletionTest(vehicle) mustEqual true + vehicle.Position = Vector3(0,11,0) + setting.CompletionTest(vehicle) mustEqual true + vehicle.Position = Vector3(0,0,11) + setting.CompletionTest(vehicle) mustEqual false + vehicle.Position = Vector3(7.5f,7.5f,0) + setting.CompletionTest(vehicle) mustEqual true + } + } + + "Drive" should { + val vehicle = Vehicle(GlobalDefinitions.fury) + + "create" in { + val config : AutoDriveControls.Configuration = AutoDriveControls.Drive(3) + val setting : AutoDriveControls.Setting = config.Create + setting.Type mustEqual AutoDriveControls.State.Drive + setting.Data mustEqual Some(3) + setting.Delay mustEqual 200L + } + + "validate" in { + val config : AutoDriveControls.Configuration = AutoDriveControls.Drive(3) + val setting : AutoDriveControls.Setting = config.Create + setting.Validate(vehicle) mustEqual true + } + + "completion" in { + val config : AutoDriveControls.Configuration = AutoDriveControls.Drive(3) + val setting : AutoDriveControls.Setting = config.Create + vehicle.Velocity mustEqual None + setting.CompletionTest(vehicle) mustEqual false + + vehicle.Velocity = Vector3.Zero + vehicle.Velocity mustEqual Some(Vector3.Zero) + setting.CompletionTest(vehicle) mustEqual false + + vehicle.Velocity = Vector3(1,0,0) + vehicle.Velocity mustEqual Some(Vector3(1,0,0)) + setting.CompletionTest(vehicle) mustEqual true + + } + } + + "FirstGear" should { + val veh_def = GlobalDefinitions.mediumtransport + val vehicle = Vehicle(veh_def) + + "create" in { + val config : AutoDriveControls.Configuration = AutoDriveControls.FirstGear() + val setting : AutoDriveControls.Setting = config.Create + setting.Type mustEqual AutoDriveControls.State.Drive + setting.Data mustEqual Some(0) + setting.Delay mustEqual 200L + } + + "validate" in { + val config : AutoDriveControls.Configuration = AutoDriveControls.FirstGear() + val setting : AutoDriveControls.Setting = config.Create + setting.Validate(vehicle) mustEqual true //always true + } + + "completion" in { + val config : AutoDriveControls.Configuration = AutoDriveControls.FirstGear() + val setting : AutoDriveControls.Setting = config.Create + + vehicle.Velocity mustEqual None + setting.CompletionTest(vehicle) mustEqual false + vehicle.Velocity = Vector3.Zero + setting.CompletionTest(vehicle) mustEqual false + vehicle.Velocity = Vector3(1,0,0) + setting.CompletionTest(vehicle) mustEqual true + } + + "data" in { + val config : AutoDriveControls.Configuration = AutoDriveControls.FirstGear() + val setting : AutoDriveControls.Setting = config.Create + + setting.Data mustEqual Some(0) + setting.Validate(vehicle) + setting.Data mustEqual Some(veh_def.AutoPilotSpeed1) + } + } + + "ForTime" should { + val veh_def = GlobalDefinitions.mediumtransport + val vehicle = Vehicle(veh_def) + + "create" in { + val config : AutoDriveControls.Configuration = AutoDriveControls.ForTime(1200L) + val setting : AutoDriveControls.Setting = config.Create + setting.Type mustEqual AutoDriveControls.State.Wait + setting.Data mustEqual None + setting.Delay mustEqual 200L + } + + "validate" in { + val config : AutoDriveControls.Configuration = AutoDriveControls.ForTime(1200L) + val setting : AutoDriveControls.Setting = config.Create + setting.Validate(vehicle) mustEqual true //always true + } + + "completion" in { + val config : AutoDriveControls.Configuration = AutoDriveControls.ForTime(1200L) + val setting : AutoDriveControls.Setting = config.Create + setting.CompletionTest(vehicle) mustEqual false + + Thread.sleep(1100) + setting.CompletionTest(vehicle) mustEqual false + + Thread.sleep(200) + setting.CompletionTest(vehicle) mustEqual true + } + } + + "SecondGear" should { + val veh_def = GlobalDefinitions.mediumtransport + val vehicle = Vehicle(veh_def) + + "create" in { + val config : AutoDriveControls.Configuration = AutoDriveControls.SecondGear() + val setting : AutoDriveControls.Setting = config.Create + setting.Type mustEqual AutoDriveControls.State.Drive + setting.Data mustEqual Some(0) + setting.Delay mustEqual 200L + } + + "validate" in { + val config : AutoDriveControls.Configuration = AutoDriveControls.SecondGear() + val setting : AutoDriveControls.Setting = config.Create + setting.Validate(vehicle) mustEqual true //always true + } + + "completion" in { + val config : AutoDriveControls.Configuration = AutoDriveControls.SecondGear() + val setting : AutoDriveControls.Setting = config.Create + + vehicle.Velocity mustEqual None + setting.CompletionTest(vehicle) mustEqual false + vehicle.Velocity = Vector3.Zero + setting.CompletionTest(vehicle) mustEqual false + vehicle.Velocity = Vector3(1,0,0) + setting.CompletionTest(vehicle) mustEqual true + } + + "data" in { + val config : AutoDriveControls.Configuration = AutoDriveControls.SecondGear() + val setting : AutoDriveControls.Setting = config.Create + + setting.Data mustEqual Some(0) + setting.Validate(vehicle) + setting.Data mustEqual Some(veh_def.AutoPilotSpeed2) + } + } + + "Stop" should { + val vehicle = Vehicle(GlobalDefinitions.mediumtransport) + + "create" in { + val config : AutoDriveControls.Configuration = AutoDriveControls.Stop() + val setting : AutoDriveControls.Setting = config.Create + setting.Type mustEqual AutoDriveControls.State.Stop + setting.Data mustEqual None + setting.Delay mustEqual 200L + } + + "validate" in { + val config : AutoDriveControls.Configuration = AutoDriveControls.Stop() + val setting : AutoDriveControls.Setting = config.Create + setting.Validate(vehicle) mustEqual true //always true + } + + "completion" in { + val config : AutoDriveControls.Configuration = AutoDriveControls.Stop() + val setting : AutoDriveControls.Setting = config.Create + setting.CompletionTest(vehicle) mustEqual true //always true + } + } + + "TurnBy" should { + "create" in { + val config : AutoDriveControls.Configuration = AutoDriveControls.TurnBy(35.5f, 23) + val setting : AutoDriveControls.Setting = config.Create + setting.Type mustEqual AutoDriveControls.State.Turn + setting.Data mustEqual Some(23) + setting.Delay mustEqual 100L + } + + "validate (velocity)" in { + val vehicle = Vehicle(GlobalDefinitions.mediumtransport) + val config : AutoDriveControls.Configuration = AutoDriveControls.TurnBy(35.5f, 23) + val setting : AutoDriveControls.Setting = config.Create + + vehicle.Velocity mustEqual None + setting.Validate(vehicle) mustEqual false + vehicle.Velocity = Vector3(1,1,1) + setting.Validate(vehicle) mustEqual true + } + + "validate (wheel direction = 15)" in { + val vehicle = Vehicle(GlobalDefinitions.mediumtransport) + val config : AutoDriveControls.Configuration = AutoDriveControls.TurnBy(35.5f, 15) + val setting : AutoDriveControls.Setting = config.Create + + vehicle.Velocity = Vector3(1,1,1) + setting.Validate(vehicle) mustEqual false + } + + "completion (passing 35.5-up)" in { + val vehicle = Vehicle(GlobalDefinitions.mediumtransport) + val config : AutoDriveControls.Configuration = AutoDriveControls.TurnBy(35.5f, 25) + val setting : AutoDriveControls.Setting = config.Create + + vehicle.Orientation mustEqual Vector3.Zero + setting.CompletionTest(vehicle) mustEqual false + vehicle.Orientation = Vector3(0,0,34.5f) + setting.CompletionTest(vehicle) mustEqual false + vehicle.Orientation = Vector3(0,0,35f) + setting.CompletionTest(vehicle) mustEqual false + vehicle.Orientation = Vector3(0,0,36.0f) + setting.CompletionTest(vehicle) mustEqual true + } + + "completion (passing 35.5 down)" in { + val vehicle = Vehicle(GlobalDefinitions.mediumtransport) + val config : AutoDriveControls.Configuration = AutoDriveControls.TurnBy(-35.5f, 25) + val setting : AutoDriveControls.Setting = config.Create + + vehicle.Orientation = Vector3(0,0,40f) + setting.CompletionTest(vehicle) mustEqual false + vehicle.Orientation = Vector3(0,0,5f) + setting.CompletionTest(vehicle) mustEqual false + vehicle.Orientation = Vector3(0,0,4f) + setting.CompletionTest(vehicle) mustEqual true + } + } +} + +class GuidedControlTest1 extends ActorTest { + "VehicleSpawnControlGuided" should { + "unguided" in { + val vehicle = Vehicle(GlobalDefinitions.mediumtransport) + vehicle.GUID = PlanetSideGUID(1) + val driver = Player(Avatar("", PlanetSideEmpire.TR, CharacterGender.Male, 0,0)) + driver.VehicleSeated = vehicle.GUID + val sendTo = TestProbe() + val order = VehicleSpawnControl.Order(driver, vehicle, sendTo.ref) + val pad = VehicleSpawnPad(GlobalDefinitions.spawn_pad) + pad.GUID = PlanetSideGUID(1) + pad.Railed = false //suppress certain events + val guided = system.actorOf(Props(classOf[VehicleSpawnControlGuided], pad), "pad") + + guided ! VehicleSpawnControl.Process.StartGuided(order) + val msg = sendTo.receiveOne(100 milliseconds) + assert(msg.isInstanceOf[VehicleSpawnPad.ServerVehicleOverrideEnd]) + } + } +} + +class GuidedControlTest2 extends ActorTest { + "VehicleSpawnControlGuided" should { + "guided (one)" in { + val vehicle = Vehicle(GlobalDefinitions.mediumtransport) + vehicle.GUID = PlanetSideGUID(1) + vehicle.Velocity = Vector3(1,1,1) + val driver = Player(Avatar("", PlanetSideEmpire.TR, CharacterGender.Male, 0,0)) + driver.VehicleSeated = vehicle.GUID + val sendTo = TestProbe() + val order = VehicleSpawnControl.Order(driver, vehicle, sendTo.ref) + val pad = VehicleSpawnPad(GlobalDefinitions.spawn_pad) + pad.Railed = false //suppress certain events + val guided = system.actorOf(Props(classOf[VehicleSpawnControlGuided], pad), "pad") + + pad.Guide = List(AutoDriveControls.FirstGear()) + guided ! VehicleSpawnControl.Process.StartGuided(order) + val msg1 = sendTo.receiveOne(100 milliseconds) + assert(msg1.isInstanceOf[VehicleSpawnControlGuided.GuidedControl]) + assert(msg1.asInstanceOf[VehicleSpawnControlGuided.GuidedControl].command == AutoDriveControls.State.Drive) + val msg2 = sendTo.receiveOne(200 milliseconds) + assert(msg2.isInstanceOf[VehicleSpawnPad.ServerVehicleOverrideEnd]) + } + } +} + +class GuidedControlTest3 extends ActorTest { + "VehicleSpawnControlGuided" should { + "guided (three)" in { + val vehicle = Vehicle(GlobalDefinitions.mediumtransport) + vehicle.GUID = PlanetSideGUID(1) + vehicle.Velocity = Vector3(1,1,1) + val driver = Player(Avatar("", PlanetSideEmpire.TR, CharacterGender.Male, 0,0)) + driver.VehicleSeated = vehicle.GUID + val sendTo = TestProbe() + val order = VehicleSpawnControl.Order(driver, vehicle, sendTo.ref) + val pad = VehicleSpawnPad(GlobalDefinitions.spawn_pad) + pad.Railed = false //suppress certain events + val guided = system.actorOf(Props(classOf[VehicleSpawnControlGuided], pad), "pad") + + pad.Guide = List( + AutoDriveControls.FirstGear(), + AutoDriveControls.ForTime(1000L), + AutoDriveControls.SecondGear() + ) + guided ! VehicleSpawnControl.Process.StartGuided(order) + val msg1 = sendTo.receiveOne(100 milliseconds) + assert(msg1.isInstanceOf[VehicleSpawnControlGuided.GuidedControl]) + assert(msg1.asInstanceOf[VehicleSpawnControlGuided.GuidedControl].command == AutoDriveControls.State.Drive) + val msg2 = sendTo.receiveOne(100 milliseconds) + assert(msg2.isInstanceOf[VehicleSpawnControlGuided.GuidedControl]) + assert(msg2.asInstanceOf[VehicleSpawnControlGuided.GuidedControl].command == AutoDriveControls.State.Wait) + sendTo.expectNoMsg(1000 milliseconds) + val msg3 = sendTo.receiveOne(100 milliseconds) + assert(msg3.isInstanceOf[VehicleSpawnControlGuided.GuidedControl]) + assert(msg3.asInstanceOf[VehicleSpawnControlGuided.GuidedControl].command == AutoDriveControls.State.Drive) + val msg4 = sendTo.receiveOne(200 milliseconds) + assert(msg4.isInstanceOf[VehicleSpawnPad.ServerVehicleOverrideEnd]) + } + } +} + +class GuidedControlTest4 extends ActorTest { + "VehicleSpawnControlGuided" should { + "fail validation test" in { + def validationFailure(vehicle : Vehicle) : Boolean = false + + val vehicle = Vehicle(GlobalDefinitions.mediumtransport) + vehicle.GUID = PlanetSideGUID(1) + vehicle.Velocity = Vector3(1,1,1) + val driver = Player(Avatar("", PlanetSideEmpire.TR, CharacterGender.Male, 0,0)) + driver.VehicleSeated = vehicle.GUID + val sendTo = TestProbe() + val order = VehicleSpawnControl.Order(driver, vehicle, sendTo.ref) + val pad = VehicleSpawnPad(GlobalDefinitions.spawn_pad) + pad.Railed = false //suppress certain events + val guided = system.actorOf(Props(classOf[VehicleSpawnControlGuided], pad), "pad") + + pad.Guide = List( + AutoDriveControls.FirstGear(), + AutoDriveControls.CancelEarly(validationFailure), + AutoDriveControls.SecondGear() + ) + guided ! VehicleSpawnControl.Process.StartGuided(order) + val msg1 = sendTo.receiveOne(100 milliseconds) + assert(msg1.isInstanceOf[VehicleSpawnControlGuided.GuidedControl]) + assert(msg1.asInstanceOf[VehicleSpawnControlGuided.GuidedControl].command == AutoDriveControls.State.Drive) + val msg2 = sendTo.receiveOne(200 milliseconds) + assert(msg2.isInstanceOf[VehicleSpawnPad.ServerVehicleOverrideEnd]) + } + } +} diff --git a/common/src/test/scala/objects/VehicleSpawnPadTest.scala b/common/src/test/scala/objects/VehicleSpawnPadTest.scala index d4393b5d..2c9e1ba7 100644 --- a/common/src/test/scala/objects/VehicleSpawnPadTest.scala +++ b/common/src/test/scala/objects/VehicleSpawnPadTest.scala @@ -2,16 +2,17 @@ package objects import akka.actor.{ActorRef, ActorSystem, Props} +import akka.testkit.TestProbe +import net.psforever.objects.serverobject.mount.Mountable import net.psforever.objects.serverobject.pad.{VehicleSpawnControl, VehicleSpawnPad} -import net.psforever.objects.serverobject.structures.{Building, StructureType} -import net.psforever.objects.vehicles.VehicleControl -import net.psforever.objects.zones.Zone +import net.psforever.objects.serverobject.structures.StructureType import net.psforever.objects.{Avatar, GlobalDefinitions, Player, Vehicle} +import net.psforever.objects.zones.Zone import net.psforever.packet.game.PlanetSideGUID -import net.psforever.types.{CharacterGender, PlanetSideEmpire, Vector3} +import net.psforever.types.{PlanetSideEmpire, Vector3} import org.specs2.mutable.Specification -import scala.concurrent.duration.Duration +import scala.concurrent.duration._ class VehicleSpawnPadTest extends Specification { "VehicleSpawnPadDefinition" should { @@ -25,6 +26,14 @@ class VehicleSpawnPadTest extends Specification { val obj = VehicleSpawnPad(GlobalDefinitions.spawn_pad) obj.Actor mustEqual ActorRef.noSender obj.Definition mustEqual GlobalDefinitions.spawn_pad + obj.Railed mustEqual true + } + + "un-railed" in { + val obj = VehicleSpawnPad(GlobalDefinitions.spawn_pad) + obj.Railed mustEqual true + obj.Railed = false + obj.Railed mustEqual false } } } @@ -39,78 +48,342 @@ class VehicleSpawnControl1Test extends ActorTest() { } } -class VehicleSpawnControl2Test extends ActorTest() { +class VehicleSpawnControl2aTest extends ActorTest() { + // This long runs for a long time. "VehicleSpawnControl" should { - "spawn a vehicle" in { - val (player, pad) = VehicleSpawnPadControl.SetUpAgents(PlanetSideEmpire.TR) - player.Spawn - val vehicle = Vehicle(GlobalDefinitions.two_man_assault_buggy) - vehicle.GUID = PlanetSideGUID(1) - vehicle.Actor = system.actorOf(Props(classOf[VehicleControl], vehicle), "vehicle") + "complete on a vehicle order (block a second one until the first is done and the spawn pad is cleared)" in { + val (vehicle, player, pad, zone) = VehicleSpawnPadControlTest.SetUpAgents(PlanetSideEmpire.TR) + //we can recycle the vehicle and the player for each order + val probe1 = new TestProbe(system, "first-order") + val probe2 = new TestProbe(system, "second-order") + val probe3 = new TestProbe(system, "zone-events") + zone.VehicleEvents = probe3.ref - pad.Actor ! VehicleSpawnPad.VehicleOrder(player, vehicle) - val reply = receiveOne(Duration.create(10000, "ms")) - assert(reply == VehicleSpawnPad.ConcealPlayer) //explicit: isInstanceOf does not work + pad.Actor.tell(VehicleSpawnPad.VehicleOrder(player, vehicle), probe1.ref) //first order + pad.Actor.tell(VehicleSpawnPad.VehicleOrder(player, vehicle), probe2.ref) //second order - val reply2 = receiveOne(Duration.create(10000, "ms")) - assert(reply2.isInstanceOf[VehicleSpawnPad.LoadVehicle]) - assert(reply2.asInstanceOf[VehicleSpawnPad.LoadVehicle].vehicle == vehicle) - assert(reply2.asInstanceOf[VehicleSpawnPad.LoadVehicle].pad == pad) + val probe2Msg1 = probe2.receiveOne(100 milliseconds) + assert(probe2Msg1.isInstanceOf[VehicleSpawnPad.PeriodicReminder]) + assert(probe2Msg1.asInstanceOf[VehicleSpawnPad.PeriodicReminder].reason == VehicleSpawnPad.Reminders.Queue) + assert(probe2Msg1.asInstanceOf[VehicleSpawnPad.PeriodicReminder].data.contains("2")) - player.VehicleOwned = Some(vehicle.GUID) - val reply3 = receiveOne(Duration.create(10000, "ms")) - assert(reply3.isInstanceOf[VehicleSpawnPad.PlayerSeatedInVehicle]) - assert(reply3.asInstanceOf[VehicleSpawnPad.PlayerSeatedInVehicle].vehicle == vehicle) + val probe3Msg1 = probe3.receiveOne(3 seconds) + assert(probe3Msg1.isInstanceOf[VehicleSpawnPad.ConcealPlayer]) - val reply4 = receiveOne(Duration.create(10000, "ms")) - assert(reply4.isInstanceOf[VehicleSpawnPad.SpawnPadBlockedWarning]) - assert(reply4.asInstanceOf[VehicleSpawnPad.SpawnPadBlockedWarning].vehicle == vehicle) - assert(reply4.asInstanceOf[VehicleSpawnPad.SpawnPadBlockedWarning].warning_count > 0) + val probe3Msg2 = probe3.receiveOne(3 seconds) + assert(probe3Msg2.isInstanceOf[VehicleSpawnPad.LoadVehicle]) - vehicle.Position = Vector3(11f, 0f, 0f) //greater than 10m - val reply5 = receiveOne(Duration.create(10000, "ms")) - assert(reply5.isInstanceOf[VehicleSpawnPad.SpawnPadUnblocked]) - assert(reply5.asInstanceOf[VehicleSpawnPad.SpawnPadUnblocked].vehicle_guid == vehicle.GUID) + val probe3Msg3 = probe3.receiveOne(200 milliseconds) + assert(probe3Msg3.isInstanceOf[VehicleSpawnPad.AttachToRails]) + + val probe1Msg1 = probe1.receiveOne(200 milliseconds) + assert(probe1Msg1.isInstanceOf[VehicleSpawnPad.StartPlayerSeatedInVehicle]) + val probe1Msg2 = probe1.receiveOne(200 milliseconds) + assert(probe1Msg2.isInstanceOf[Mountable.MountMessages]) + val probe1Msg2Contents = probe1Msg2.asInstanceOf[Mountable.MountMessages] + assert(probe1Msg2Contents.response.isInstanceOf[Mountable.CanMount]) + val probe1Msg3 = probe1.receiveOne(3 seconds) + assert(probe1Msg3.isInstanceOf[VehicleSpawnPad.PlayerSeatedInVehicle]) + + val probe3Msg4 = probe3.receiveOne(1 seconds) + assert(probe3Msg4.isInstanceOf[VehicleSpawnPad.DetachFromRails]) + + val probe1Msg4 = probe1.receiveOne(1 seconds) + assert(probe1Msg4.isInstanceOf[VehicleSpawnPad.ServerVehicleOverrideStart]) + val probe1Msg5 = probe1.receiveOne(4 seconds) + assert(probe1Msg5.isInstanceOf[VehicleSpawnPad.ServerVehicleOverrideEnd]) + + val probe1Msg6 = probe1.receiveOne(11 seconds) + assert(probe1Msg6.isInstanceOf[VehicleSpawnPad.PeriodicReminder]) + assert(probe1Msg6.asInstanceOf[VehicleSpawnPad.PeriodicReminder].reason == VehicleSpawnPad.Reminders.Blocked) + val probe2Msg2 = probe2.receiveOne(100 milliseconds) + assert(probe2Msg2.isInstanceOf[VehicleSpawnPad.PeriodicReminder]) + assert(probe2Msg2.asInstanceOf[VehicleSpawnPad.PeriodicReminder].reason == VehicleSpawnPad.Reminders.Blocked) + + //if we move the vehicle more than 25m away from the pad, we should receive a ResetSpawnPad, and a second ConcealPlayer message + //that means that the first order has cleared and the spawn pad is now working on the second order successfully + vehicle.Position = Vector3(11,0,0) + player.VehicleSeated = None //since shared between orders, is necessary + val probe3Msg5 = probe3.receiveOne(4 seconds) + assert(probe3Msg5.isInstanceOf[VehicleSpawnPad.ResetSpawnPad]) + val probe3Msg6 = probe3.receiveOne(5 seconds) + assert(probe3Msg6.isInstanceOf[VehicleSpawnPad.ConcealPlayer]) + } + } +} + +class VehicleSpawnControl2bTest extends ActorTest() { + // This long runs for a long time. + "VehicleSpawnControl" should { + "complete on a vehicle order (railless)" in { + val (vehicle, player, pad, zone) = VehicleSpawnPadControlTest.SetUpAgents(PlanetSideEmpire.TR) + //we can recycle the vehicle and the player for each order + val probe1 = new TestProbe(system, "first-order") + val probe2 = new TestProbe(system, "second-order") + val probe3 = new TestProbe(system, "zone-events") + zone.VehicleEvents = probe3.ref + pad.Railed = false + + pad.Actor.tell(VehicleSpawnPad.VehicleOrder(player, vehicle), probe1.ref) //first order + pad.Actor.tell(VehicleSpawnPad.VehicleOrder(player, vehicle), probe2.ref) //second order + + val probe2Msg1 = probe2.receiveOne(100 milliseconds) + assert(probe2Msg1.isInstanceOf[VehicleSpawnPad.PeriodicReminder]) + assert(probe2Msg1.asInstanceOf[VehicleSpawnPad.PeriodicReminder].reason == VehicleSpawnPad.Reminders.Queue) + assert(probe2Msg1.asInstanceOf[VehicleSpawnPad.PeriodicReminder].data.contains("2")) + + val probe3Msg1 = probe3.receiveOne(3 seconds) + assert(probe3Msg1.isInstanceOf[VehicleSpawnPad.ConcealPlayer]) + + val probe3Msg2 = probe3.receiveOne(3 seconds) + assert(probe3Msg2.isInstanceOf[VehicleSpawnPad.LoadVehicle]) + + val probe1Msg1 = probe1.receiveOne(200 milliseconds) + assert(probe1Msg1.isInstanceOf[VehicleSpawnPad.StartPlayerSeatedInVehicle]) + val probe1Msg2 = probe1.receiveOne(200 milliseconds) + assert(probe1Msg2.isInstanceOf[Mountable.MountMessages]) + val probe1Msg2Contents = probe1Msg2.asInstanceOf[Mountable.MountMessages] + assert(probe1Msg2Contents.response.isInstanceOf[Mountable.CanMount]) + val probe1Msg3 = probe1.receiveOne(3 seconds) + assert(probe1Msg3.isInstanceOf[VehicleSpawnPad.PlayerSeatedInVehicle]) + + val probe1Msg4 = probe1.receiveOne(1 seconds) + assert(probe1Msg4.isInstanceOf[VehicleSpawnPad.ServerVehicleOverrideStart]) + val probe1Msg5 = probe1.receiveOne(4 seconds) + assert(probe1Msg5.isInstanceOf[VehicleSpawnPad.ServerVehicleOverrideEnd]) + + val probe1Msg6 = probe1.receiveOne(11 seconds) + assert(probe1Msg6.isInstanceOf[VehicleSpawnPad.PeriodicReminder]) + assert(probe1Msg6.asInstanceOf[VehicleSpawnPad.PeriodicReminder].reason == VehicleSpawnPad.Reminders.Blocked) + val probe2Msg2 = probe2.receiveOne(100 milliseconds) + assert(probe2Msg2.isInstanceOf[VehicleSpawnPad.PeriodicReminder]) + assert(probe2Msg2.asInstanceOf[VehicleSpawnPad.PeriodicReminder].reason == VehicleSpawnPad.Reminders.Blocked) + + //if we move the vehicle more than 10m away from the pad, we should receive a second ConcealPlayer message + //that means that the first order has cleared and the spawn pad is now working on the second order successfully + vehicle.Position = Vector3(11,0,0) + player.VehicleSeated = None //since shared between orders, is necessary + val probe3Msg6 = probe3.receiveOne(4 seconds) + assert(probe3Msg6.isInstanceOf[VehicleSpawnPad.ConcealPlayer]) } } } class VehicleSpawnControl3Test extends ActorTest() { "VehicleSpawnControl" should { - "not spawn a vehicle if player is dead" in { - val (player, pad) = VehicleSpawnPadControl.SetUpAgents(PlanetSideEmpire.TR) - val vehicle = Vehicle(GlobalDefinitions.two_man_assault_buggy) - vehicle.GUID = PlanetSideGUID(1) - vehicle.Actor = system.actorOf(Props(classOf[VehicleControl], vehicle), "vehicle") + "player is on wrong continent before vehicle can partially load; vehicle is cleaned up" in { + val (vehicle, player, pad, zone) = VehicleSpawnPadControlTest.SetUpAgents(PlanetSideEmpire.TR) + val probe1 = new TestProbe(system, "first-order") + val probe3 = new TestProbe(system, "zone-events") + zone.VehicleEvents = probe3.ref + player.Continent = "problem" //problem - pad.Actor ! VehicleSpawnPad.VehicleOrder(player, vehicle) - val reply = receiveOne(Duration.create(5000, "ms")) - assert(reply == null) + assert(vehicle.HasGUID) + pad.Actor.tell(VehicleSpawnPad.VehicleOrder(player, vehicle), probe1.ref) + + val probe3Msg1 = probe3.receiveOne(3 seconds) + assert(probe3Msg1.isInstanceOf[VehicleSpawnPad.RevealPlayer]) + probe3.expectNoMsg(5 seconds) + assert(!vehicle.HasGUID) //vehicle has been unregistered } } } class VehicleSpawnControl4Test extends ActorTest() { "VehicleSpawnControl" should { - "not spawn a vehicle if vehicle Actor is missing" in { - val (player, pad) = VehicleSpawnPadControl.SetUpAgents(PlanetSideEmpire.TR) - player.Spawn - val vehicle = Vehicle(GlobalDefinitions.two_man_assault_buggy) - vehicle.GUID = PlanetSideGUID(1) + "the player is on wrong continent when the vehicle tries to load; vehicle is cleaned up" in { + val (vehicle, player, pad, zone) = VehicleSpawnPadControlTest.SetUpAgents(PlanetSideEmpire.TR) + val probe1 = new TestProbe(system, "first-order") + val probe3 = new TestProbe(system, "zone-events") + zone.VehicleEvents = probe3.ref - pad.Actor ! VehicleSpawnPad.VehicleOrder(player, vehicle) - val reply = receiveOne(Duration.create(5000, "ms")) - assert(reply == null) + pad.Actor.tell(VehicleSpawnPad.VehicleOrder(player, vehicle), probe1.ref) + + val probe3Msg1 = probe3.receiveOne(3 seconds) + assert(probe3Msg1.isInstanceOf[VehicleSpawnPad.ConcealPlayer]) + player.Continent = "problem" //problem + assert(vehicle.HasGUID) + + val probe3Msg2 = probe3.receiveOne(3 seconds) + assert(probe3Msg2.isInstanceOf[VehicleSpawnPad.RevealPlayer]) + probe3.expectNoMsg(5 seconds) + assert(!vehicle.HasGUID) //vehicle has been unregistered } } } -object VehicleSpawnPadControl { - def SetUpAgents(faction : PlanetSideEmpire.Value)(implicit system : ActorSystem) : (Player, VehicleSpawnPad) = { - val pad = VehicleSpawnPad(GlobalDefinitions.spawn_pad) - pad.Actor = system.actorOf(Props(classOf[VehicleSpawnControl], pad), "test-pad") - pad.Owner = new Building(0, Zone.Nowhere, StructureType.Building) - pad.Owner.Faction = faction - (Player(Avatar("test", faction, CharacterGender.Male, 0, 0)), pad) +//class VehicleSpawnControl5aTest extends ActorTest() { +// "VehicleSpawnControl" should { +// "the vehicle is destroyed before being fully loaded; the vehicle is cleaned up" in { +// val (vehicle, player, pad, zone) = VehicleSpawnPadControlTest.SetUpAgents(PlanetSideEmpire.TR) +// //we can recycle the vehicle and the player for each order +// val probe1 = new TestProbe(system, "first-order") +// val probe3 = new TestProbe(system, "zone-events") +// zone.VehicleEvents = probe3.ref +// +// pad.Actor.tell(VehicleSpawnPad.VehicleOrder(player, vehicle), probe1.ref) +// +// val probe3Msg1 = probe3.receiveOne(3 seconds) +// assert(probe3Msg1.isInstanceOf[VehicleSpawnPad.ConcealPlayer]) +// +// val probe3Msg2 = probe3.receiveOne(3 seconds) +// assert(probe3Msg2.isInstanceOf[VehicleSpawnPad.LoadVehicle]) +// vehicle.Health = 0 //problem +// +// val probe3Msg3 = probe3.receiveOne(3 seconds) +// assert(probe3Msg3.isInstanceOf[VehicleSpawnPad.DisposeVehicle]) +// val probe3Msg4 = probe3.receiveOne(100 milliseconds) +// assert(probe3Msg4.isInstanceOf[VehicleSpawnPad.RevealPlayer]) +// //note: the vehicle will not be unregistered by this logic alone +// //since LoadVehicle should introduce it into the game world properly, it has to be handled properly +// } +// } +//} + +class VehicleSpawnControl5Test extends ActorTest() { + "VehicleSpawnControl" should { + "player dies right after vehicle partially loads; the vehicle spawns and blocks the pad" in { + val (vehicle, player, pad, zone) = VehicleSpawnPadControlTest.SetUpAgents(PlanetSideEmpire.TR) + //we can recycle the vehicle and the player for each order + val probe1 = new TestProbe(system, "first-order") + val probe3 = new TestProbe(system, "zone-events") + zone.VehicleEvents = probe3.ref + + pad.Actor.tell(VehicleSpawnPad.VehicleOrder(player, vehicle), probe1.ref) + + val probe3Msg1 = probe3.receiveOne(3 seconds) + assert(probe3Msg1.isInstanceOf[VehicleSpawnPad.ConcealPlayer]) + + val probe3Msg2 = probe3.receiveOne(3 seconds) + assert(probe3Msg2.isInstanceOf[VehicleSpawnPad.LoadVehicle]) + player.Die //problem + + val probe3Msg3 = probe3.receiveOne(3 seconds) + assert(probe3Msg3.isInstanceOf[VehicleSpawnPad.AttachToRails]) + val probe3Msg4 = probe3.receiveOne(3 seconds) + assert(probe3Msg4.isInstanceOf[VehicleSpawnPad.DetachFromRails]) + + val probe1Msg = probe1.receiveOne(12 seconds) + assert(probe1Msg.isInstanceOf[VehicleSpawnPad.PeriodicReminder]) + assert(probe1Msg.asInstanceOf[VehicleSpawnPad.PeriodicReminder].reason == VehicleSpawnPad.Reminders.Blocked) + } + } +} + +class VehicleSpawnControl6Test extends ActorTest() { + "VehicleSpawnControl" should { + "the player can not sit in vehicle; vehicle spawns and blocks the pad" in { + val (vehicle, player, pad, zone) = VehicleSpawnPadControlTest.SetUpAgents(PlanetSideEmpire.TR) + //we can recycle the vehicle and the player for each order + val probe1 = new TestProbe(system, "first-order") + val probe3 = new TestProbe(system, "zone-events") + zone.VehicleEvents = probe3.ref + + pad.Actor.tell(VehicleSpawnPad.VehicleOrder(player, vehicle), probe1.ref) + + val probe3Msg1 = probe3.receiveOne(3 seconds) + assert(probe3Msg1.isInstanceOf[VehicleSpawnPad.ConcealPlayer]) + + val probe3Msg2 = probe3.receiveOne(3 seconds) + assert(probe3Msg2.isInstanceOf[VehicleSpawnPad.LoadVehicle]) + + val probe3Msg3 = probe3.receiveOne(3 seconds) + assert(probe3Msg3.isInstanceOf[VehicleSpawnPad.AttachToRails]) + + val probe1Msg1 = probe1.receiveOne(200 milliseconds) + assert(probe1Msg1.isInstanceOf[VehicleSpawnPad.StartPlayerSeatedInVehicle]) + player.Continent = "problem" //problem 1 + probe1.receiveOne(200 milliseconds) //Mountable.MountMessage + + val probe3Msg4 = probe3.receiveOne(3 seconds) + assert(probe3Msg4.isInstanceOf[VehicleSpawnPad.DetachFromRails]) + val probe3Msg5 = probe3.receiveOne(3 seconds) + assert(probe3Msg5.isInstanceOf[VehicleSpawnPad.ResetSpawnPad]) + + val probe1Msg2 = probe1.receiveOne(12 seconds) + assert(probe1Msg2.isInstanceOf[VehicleSpawnPad.PeriodicReminder]) + assert(probe1Msg2.asInstanceOf[VehicleSpawnPad.PeriodicReminder].reason == VehicleSpawnPad.Reminders.Blocked) + } + } +} + +class VehicleSpawnControl7Test extends ActorTest() { + "VehicleSpawnControl" should { + "player dies after getting in driver seat; the vehicle blocks the pad" in { + val (vehicle, player, pad, zone) = VehicleSpawnPadControlTest.SetUpAgents(PlanetSideEmpire.TR) + //we can recycle the vehicle and the player for each order + val probe1 = new TestProbe(system, "first-order") + val probe3 = new TestProbe(system, "zone-events") + zone.VehicleEvents = probe3.ref + + pad.Actor.tell(VehicleSpawnPad.VehicleOrder(player, vehicle), probe1.ref) + + val probe3Msg1 = probe3.receiveOne(3 seconds) + assert(probe3Msg1.isInstanceOf[VehicleSpawnPad.ConcealPlayer]) + + val probe3Msg2 = probe3.receiveOne(3 seconds) + assert(probe3Msg2.isInstanceOf[VehicleSpawnPad.LoadVehicle]) + + val probe3Msg3 = probe3.receiveOne(3 seconds) + assert(probe3Msg3.isInstanceOf[VehicleSpawnPad.AttachToRails]) + + val probe1Msg1 = probe1.receiveOne(200 milliseconds) + assert(probe1Msg1.isInstanceOf[VehicleSpawnPad.StartPlayerSeatedInVehicle]) + val probe1Msg2 = probe1.receiveOne(200 milliseconds) + assert(probe1Msg2.isInstanceOf[Mountable.MountMessages]) + val probe1Msg2Contents = probe1Msg2.asInstanceOf[Mountable.MountMessages] + assert(probe1Msg2Contents.response.isInstanceOf[Mountable.CanMount]) + val probe1Msg3 = probe1.receiveOne(3 seconds) + assert(probe1Msg3.isInstanceOf[VehicleSpawnPad.PlayerSeatedInVehicle]) + player.Die //problem + + val probe3Msg4 = probe3.receiveOne(3 seconds) + assert(probe3Msg4.isInstanceOf[VehicleSpawnPad.DetachFromRails]) + val probe3Msg5 = probe3.receiveOne(100 milliseconds) + assert(probe3Msg5.isInstanceOf[VehicleSpawnPad.ResetSpawnPad]) + + val probe1Msg4 = probe1.receiveOne(12 seconds) + assert(probe1Msg4.isInstanceOf[VehicleSpawnPad.PeriodicReminder]) + assert(probe1Msg4.asInstanceOf[VehicleSpawnPad.PeriodicReminder].reason == VehicleSpawnPad.Reminders.Blocked) + } + } +} + +object VehicleSpawnPadControlTest { + import net.psforever.objects.zones.ZoneMap + private val map = new ZoneMap("test-map") + + def SetUpAgents(faction : PlanetSideEmpire.Value)(implicit system : ActorSystem) : (Vehicle, Player, VehicleSpawnPad, Zone) = { + import net.psforever.objects.guid.NumberPoolHub + import net.psforever.objects.guid.source.LimitedNumberSource + import net.psforever.objects.serverobject.structures.Building + import net.psforever.objects.vehicles.VehicleControl + import net.psforever.objects.zones.ZoneActor + import net.psforever.objects.Tool + import net.psforever.types.CharacterGender + + val zone = new Zone("test-zone", map, 0) + val vehicle = Vehicle(GlobalDefinitions.two_man_assault_buggy) + val weapon = vehicle.WeaponControlledFromSeat(1).get.asInstanceOf[Tool] + val guid : NumberPoolHub = new NumberPoolHub(LimitedNumberSource(3)) + guid.AddPool("test-pool", (0 to 2).toList) + guid.register(vehicle, "test-pool") + guid.register(weapon, "test-pool") + guid.register(weapon.AmmoSlot.Box, "test-pool") + zone.GUID(guid) + zone.Actor = system.actorOf(Props(classOf[ZoneActor], zone), s"test-zone-${System.nanoTime()}") + zone.Actor ! Zone.Init() + vehicle.Actor = system.actorOf(Props(classOf[VehicleControl], vehicle), s"vehicle-control-${System.nanoTime()}") + + val pad = VehicleSpawnPad(GlobalDefinitions.spawn_pad) + pad.Actor = system.actorOf(Props(classOf[VehicleSpawnControl], pad), s"test-pad-${System.nanoTime()}") + pad.Owner = new Building(0, zone, StructureType.Building) + pad.Owner.Faction = faction + val player = Player(Avatar("test", faction, CharacterGender.Male, 0, 0)) + player.GUID = PlanetSideGUID(10) + player.Continent = zone.Id + player.Spawn + //note: pad and vehicle are both at Vector3(0,0,0) so they count as blocking + (vehicle, player, pad, zone) } } diff --git a/common/src/test/scala/objects/ZoneTest.scala b/common/src/test/scala/objects/ZoneTest.scala index 2c5cfcb6..251694f3 100644 --- a/common/src/test/scala/objects/ZoneTest.scala +++ b/common/src/test/scala/objects/ZoneTest.scala @@ -8,13 +8,14 @@ import net.psforever.objects.entity.IdentifiableEntity import net.psforever.objects.equipment.Equipment import net.psforever.objects.guid.NumberPoolHub import net.psforever.objects.guid.source.LimitedNumberSource -import net.psforever.objects.serverobject.structures.{Building, FoundationBuilder, StructureType} import net.psforever.objects.serverobject.terminals.Terminal import net.psforever.objects.serverobject.tube.SpawnTube -import net.psforever.objects.zones.{Zone, ZoneActor, ZoneMap} import net.psforever.objects._ import net.psforever.packet.game.PlanetSideGUID import net.psforever.types.{CharacterGender, PlanetSideEmpire, Vector3} +import net.psforever.objects.serverobject.structures.{Building, FoundationBuilder, StructureType} +import net.psforever.objects.zones.{Zone, ZoneActor, ZoneMap} +import net.psforever.objects.Vehicle import org.specs2.mutable.Specification import scala.concurrent.duration.Duration @@ -29,8 +30,6 @@ class ZoneTest extends Specification { } "references bases by a positive building id (defaults to 0)" in { - def test(a: Int, b : Zone, c : ActorContext) : Building = { Building.NoBuilding } - val map = new ZoneMap("map13") map.LocalBuildings mustEqual Map.empty map.LocalBuilding(10, FoundationBuilder(test)) @@ -110,28 +109,6 @@ class ZoneTest extends Specification { guid2.AddPool("pool4", (51 to 75).toList) zone.GUID(guid2) mustEqual false } - - "can keep track of Vehicles" in { - val zone = new Zone("home3", map13, 13) - val fury = Vehicle(GlobalDefinitions.fury) - zone.Vehicles mustEqual List() - zone.AddVehicle(fury) - zone.Vehicles mustEqual List(fury) - } - - "can forget specific vehicles" in { - val zone = new Zone("home3", map13, 13) - val fury = Vehicle(GlobalDefinitions.fury) - val wraith = Vehicle(GlobalDefinitions.quadstealth) - val basilisk = Vehicle(GlobalDefinitions.quadassault) - zone.AddVehicle(wraith) - zone.AddVehicle(fury) - zone.AddVehicle(basilisk) - zone.Vehicles mustEqual List(wraith, fury, basilisk) - - zone.RemoveVehicle(fury) - zone.Vehicles mustEqual List(wraith, basilisk) - } } } diff --git a/pslogin/src/main/scala/Maps.scala b/pslogin/src/main/scala/Maps.scala index 8b9351b1..4051208c 100644 --- a/pslogin/src/main/scala/Maps.scala +++ b/pslogin/src/main/scala/Maps.scala @@ -126,14 +126,15 @@ object Maps { LocalObject(2323, Door.Constructor) //spawn tube door LocalObject(2324, Door.Constructor) //spawn tube door LocalObject(2419, Terminal.Constructor(ground_vehicle_terminal)) - LocalObject(500, + LocalObject(1479, VehicleSpawnPad.Constructor(Vector3(3962.0f, 4334.0f, 267.75f), Vector3(0f, 0f, 180.0f)) - ) //TODO guid not correct + ) LocalObject(224, Terminal.Constructor(dropship_vehicle_terminal)) - LocalObject(501, - VehicleSpawnPad.Constructor(Vector3(4012.3594f, 4364.8047f, 271.90625f), Vector3(0f, 0f, 180.0f)) - ) //TODO guid not correct + LocalObject(223, + VehicleSpawnPad.Constructor(Vector3(4012.3594f, 4364.8047f, 271.90625f), Vector3(0f, 0f, 0f)) + ) ObjectToBuilding(222, 2) + ObjectToBuilding(223, 2) ObjectToBuilding(224, 2) ObjectToBuilding(370, 2) ObjectToBuilding(371, 2) @@ -204,6 +205,7 @@ object Maps { ObjectToBuilding(1188, 2) ObjectToBuilding(1492, 2) ObjectToBuilding(1494, 2) + ObjectToBuilding(1479, 2) ObjectToBuilding(1564, 2) ObjectToBuilding(1568, 2) ObjectToBuilding(1569, 2) @@ -228,8 +230,6 @@ object Maps { ObjectToBuilding(2323, 2) ObjectToBuilding(2324, 2) ObjectToBuilding(2419, 2) - ObjectToBuilding(500, 2) - ObjectToBuilding(501, 2) DoorToLock(375, 863) DoorToLock(376, 860) DoorToLock(384, 866) @@ -244,8 +244,8 @@ object Maps { DoorToLock(638, 882) DoorToLock(642, 884) DoorToLock(715, 751) - TerminalToSpawnPad(224, 501) - TerminalToSpawnPad(2419, 500) + TerminalToSpawnPad(224, 223) + TerminalToSpawnPad(2419, 1479) } def Building38() : Unit = { @@ -404,20 +404,23 @@ object Maps { Building29() Building42() Building51() + Building52() Building77() + Building79() + Building81() def Building1() : Unit = { //warpgate? LocalBuilding(1, FoundationBuilder(WarpGate.Structure)) } + // LocalBuilding(2, FoundationBuilder(WarpGate.Structure)) //TODO might be wrong? + def Building3() : Unit = { //warpgate? LocalBuilding(3, FoundationBuilder(WarpGate.Structure)) } -// LocalBuilding(2, FoundationBuilder(WarpGate.Structure)) //TODO might be wrong? - // LocalObject(520, ImplantTerminalMech.Constructor) //Hart B // LocalObject(1081, Terminal.Constructor(implant_terminal_interface)) //tube 520 // TerminalToInterface(520, 1081) @@ -616,24 +619,60 @@ object Maps { LocalBuilding(51, FoundationBuilder(Building.Structure(StructureType.Platform))) LocalObject(304, Terminal.Constructor(dropship_vehicle_terminal)) LocalObject(292, - VehicleSpawnPad.Constructor(Vector3(3508.9844f, 2895.961f, 92.296875f), Vector3(0f, 0f, 270.0f)) + VehicleSpawnPad.Constructor(Vector3(3508.9844f, 2895.961f, 92.296875f), Vector3(0f, 0f, 90.0f)) ) ObjectToBuilding(304, 51) ObjectToBuilding(292, 51) TerminalToSpawnPad(304, 292) } + def Building52() : Unit = { + //air terminal southwest of HART C + LocalBuilding(52, FoundationBuilder(Building.Structure(StructureType.Platform))) + LocalObject(305, Terminal.Constructor(dropship_vehicle_terminal)) + LocalObject(293, + VehicleSpawnPad.Constructor(Vector3(3575.0781f, 2654.9766f, 92.296875f), Vector3(0f, 0f, 45.0f)) + ) + ObjectToBuilding(305, 52) + ObjectToBuilding(293, 52) + TerminalToSpawnPad(305, 293) + } + def Building77() : Unit = { //ground terminal west of HART C LocalBuilding(77, FoundationBuilder(Building.Structure(StructureType.Platform))) LocalObject(1063, Terminal.Constructor(ground_vehicle_terminal)) LocalObject(706, - VehicleSpawnPad.Constructor(Vector3(3506.0f, 2820.0f, 92.0f), Vector3(0f, 0f, 270.0f)) + VehicleSpawnPad.Constructor(Vector3(3506.0f, 2820.0f, 92.0625f), Vector3(0f, 0f, 270.0f)) ) ObjectToBuilding(1063, 77) ObjectToBuilding(706, 77) TerminalToSpawnPad(1063, 706) } + + def Building79() : Unit = { + //ground terminal south of HART C + LocalBuilding(79, FoundationBuilder(Building.Structure(StructureType.Platform))) + LocalObject(1065, Terminal.Constructor(ground_vehicle_terminal)) + LocalObject(710, + VehicleSpawnPad.Constructor(Vector3(3659.836f, 2589.875f, 92.0625f), Vector3(0f, 0f, 180.0f)) + ) + ObjectToBuilding(1065, 79) + ObjectToBuilding(710, 79) + TerminalToSpawnPad(1065, 710) + } + + def Building81() : Unit = { + //ground terminal south of HART C + LocalBuilding(81, FoundationBuilder(Building.Structure(StructureType.Platform))) + LocalObject(1067, Terminal.Constructor(ground_vehicle_terminal)) + LocalObject(712, + VehicleSpawnPad.Constructor(Vector3(3724.0156f, 2589.875f, 92.0625f), Vector3(0f, 0f, 180.0f)) + ) + ObjectToBuilding(1067, 81) + ObjectToBuilding(712, 81) + TerminalToSpawnPad(1067, 712) + } } val map14 = new ZoneMap("map14") diff --git a/pslogin/src/main/scala/PsLogin.scala b/pslogin/src/main/scala/PsLogin.scala index 05de1604..e689aa9e 100644 --- a/pslogin/src/main/scala/PsLogin.scala +++ b/pslogin/src/main/scala/PsLogin.scala @@ -203,12 +203,29 @@ object PsLogin { ) */ + val continentList = createContinents() val serviceManager = ServiceManager.boot serviceManager ! ServiceManager.Register(RandomPool(50).props(Props[TaskResolver]), "taskResolver") serviceManager ! ServiceManager.Register(Props[AvatarService], "avatar") serviceManager ! ServiceManager.Register(Props[LocalService], "local") serviceManager ! ServiceManager.Register(Props[VehicleService], "vehicle") - serviceManager ! ServiceManager.Register(Props(classOf[InterstellarCluster], createContinents()), "galaxy") + serviceManager ! ServiceManager.Register(Props(classOf[InterstellarCluster], continentList), "galaxy") + + //attach event bus entry point to each zone + import akka.pattern.ask + import akka.util.Timeout + import scala.concurrent.ExecutionContext.Implicits.global + import scala.concurrent.Future + import scala.util.{Failure, Success} + implicit val timeout = Timeout(200 milliseconds) + val requestVehicleEventBus : Future[ServiceManager.LookupResult] = + (ServiceManager.serviceManager ask ServiceManager.Lookup("vehicle")).mapTo[ServiceManager.LookupResult] + requestVehicleEventBus.onComplete { + case Success(ServiceManager.LookupResult(_, bus)) => + continentList.foreach { _.VehicleEvents = bus } + case Failure(_) => ; + //TODO how to fail + } /** Create two actors for handling the login and world server endpoints */ loginRouter = Props(new SessionRouter("Login", loginTemplate)) diff --git a/pslogin/src/main/scala/WorldSessionActor.scala b/pslogin/src/main/scala/WorldSessionActor.scala index 46fe9aef..b7e803aa 100644 --- a/pslogin/src/main/scala/WorldSessionActor.scala +++ b/pslogin/src/main/scala/WorldSessionActor.scala @@ -26,13 +26,13 @@ import net.psforever.objects.serverobject.implantmech.ImplantTerminalMech import net.psforever.objects.serverobject.locks.IFFLock import net.psforever.objects.serverobject.mblocker.Locker import net.psforever.objects.serverobject.pad.VehicleSpawnPad -import net.psforever.objects.serverobject.terminals.{MatrixTerminalDefinition, ProximityTerminal, Terminal} -import net.psforever.objects.serverobject.terminals.Terminal.TerminalMessage -import net.psforever.objects.vehicles.{AccessPermissionGroup, Utility, VehicleLockState} +import net.psforever.objects.serverobject.pad.process.{AutoDriveControls, VehicleSpawnControlGuided} import net.psforever.objects.serverobject.structures.{Building, StructureType, WarpGate} +import net.psforever.objects.serverobject.terminals.{MatrixTerminalDefinition, ProximityTerminal, Terminal} import net.psforever.objects.serverobject.terminals.Terminal +import net.psforever.objects.serverobject.terminals.Terminal.TerminalMessage import net.psforever.objects.serverobject.tube.SpawnTube -import net.psforever.objects.vehicles.{AccessPermissionGroup, VehicleLockState} +import net.psforever.objects.vehicles.{AccessPermissionGroup, Utility, VehicleLockState} import net.psforever.objects.zones.{InterstellarCluster, Zone} import net.psforever.packet.game.objectcreate._ import net.psforever.types._ @@ -69,11 +69,20 @@ class WorldSessionActor extends Actor with MDCContextAware { var usingMedicalTerminal : Option[PlanetSideGUID] = None var usingProximityTerminal : Set[PlanetSideGUID] = Set.empty var delayedProximityTerminalResets : Map[PlanetSideGUID, Cancellable] = Map.empty + var controlled : Option[Int] = None //keep track of avatar's ServerVehicleOverride state var clientKeepAlive : Cancellable = DefaultCancellable.obj var progressBarUpdate : Cancellable = DefaultCancellable.obj var reviveTimer : Cancellable = DefaultCancellable.obj + /** + * Convert a boolean value into an integer value. + * Use: `true:Int` or `false:Int` + * @param b `true` or `false` (or `null`) + * @return 1 for `true`; 0 for `false` + */ + implicit def boolToInt(b : Boolean) : Int = if(b) 1 else 0 + override def postStop() = { clientKeepAlive.cancel reviveTimer.cancel @@ -404,11 +413,17 @@ class WorldSessionActor extends Actor with MDCContextAware { //resets exclamation point fte marker (once) sendResponse(PlanetsideAttributeMessage(guid, 21, vehicle_guid.guid.toLong)) + case VehicleResponse.AttachToRails(vehicle_guid, pad_guid) => + sendResponse(ObjectAttachMessage(pad_guid, vehicle_guid, 3)) + case VehicleResponse.ChildObjectState(object_guid, pitch, yaw) => if(tplayer_guid != guid) { sendResponse(ChildObjectStateMessage(object_guid, pitch, yaw)) } + case VehicleResponse.ConcealPlayer(player_guid) => + sendResponse(GenericObjectActionMessage(player_guid, 36)) + case VehicleResponse.DismountVehicle(unk1, unk2) => if(tplayer_guid != guid) { sendResponse(DismountVehicleMsg(guid, unk1, unk2)) @@ -419,6 +434,9 @@ class WorldSessionActor extends Actor with MDCContextAware { sendResponse(DeployRequestMessage(guid, object_guid, state, unk1, unk2, pos)) } + case VehicleResponse.DetachFromRails(vehicle_guid, pad_guid, pad_position, pad_orientation_z) => + sendResponse(ObjectDetachMessage(pad_guid, vehicle_guid, pad_position + Vector3(0,0,0.5f), 0, 0, pad_orientation_z)) + case VehicleResponse.InventoryState(obj, parent_guid, start, con_data) => if(tplayer_guid != guid) { //TODO prefer ObjectDetachMessage, but how to force ammo pools to update properly? @@ -456,6 +474,16 @@ class WorldSessionActor extends Actor with MDCContextAware { sendResponse(ObjectAttachMessage(vehicle_guid, guid, seat)) } + case VehicleResponse.ResetSpawnPad(pad_guid) => + sendResponse(GenericObjectActionMessage(pad_guid, 92)) + + case VehicleResponse.RevealPlayer(player_guid) => + //TODO any action will cause the player to appear after the effects of ConcealPlayer + if(player.GUID == player_guid) { + sendResponse(ChatMsg(ChatMessageType.CMT_OPEN, true, "", "You are in a strange situation.", None)) + KillPlayer(player) + } + case VehicleResponse.SeatPermissions(vehicle_guid, seat_group, permission) => if(tplayer_guid != guid) { sendResponse(PlanetsideAttributeMessage(vehicle_guid, seat_group, permission)) @@ -614,6 +642,7 @@ class WorldSessionActor extends Actor with MDCContextAware { if(player_guid == player.GUID) { //disembarking self log.info(s"DismountVehicleMsg: $player_guid dismounts $obj @ $seat_num") + TotalDriverVehicleControl(obj) sendResponse(DismountVehicleMsg(player_guid, seat_num, false)) vehicleService ! VehicleServiceMessage(continent.Id, VehicleAction.DismountVehicle(player_guid, seat_num, false)) UnAccessContents(obj) @@ -1000,46 +1029,63 @@ class WorldSessionActor extends Actor with MDCContextAware { sendResponse(ItemTransactionResultMessage(msg.terminal_guid, msg.transaction_type, false)) } - case VehicleSpawnPad.ConcealPlayer => - sendResponse(GenericObjectActionMessage(player.GUID, 36)) - avatarService ! AvatarServiceMessage(continent.Id, AvatarAction.ConcealPlayer(player.GUID)) - - case VehicleSpawnPad.LoadVehicle(vehicle, _/*pad*/) => - val player_guid = player.GUID - val definition = vehicle.Definition - val objedtId = definition.ObjectId + case VehicleSpawnPad.StartPlayerSeatedInVehicle(vehicle, pad) => val vehicle_guid = vehicle.GUID - val vdata = definition.Packet.ConstructorData(vehicle).get - sendResponse(ObjectCreateMessage(objedtId, vehicle_guid, vdata)) - continent.Transport ! Zone.SpawnVehicle(vehicle) - vehicleService ! VehicleServiceMessage(continent.Id, VehicleAction.LoadVehicle(player_guid, vehicle, objedtId, vehicle_guid, vdata)) sendResponse(PlanetsideAttributeMessage(vehicle_guid, 22, 1L)) //mount points off? - sendResponse(PlanetsideAttributeMessage(vehicle_guid, 21, player_guid.guid)) //fte and ownership? - //sendResponse(ObjectAttachMessage(vehicle_guid, player_guid, 0)) - vehicleService ! VehicleServiceMessage.UnscheduleDeconstruction(vehicle_guid) //cancel queue timeout delay - vehicleService ! VehicleServiceMessage.DelayedVehicleDeconstruction(vehicle, continent, 21L) //temporary drive away from pad delay - vehicle.Actor ! Mountable.TryMount(player, 0) + sendResponse(PlanetsideAttributeMessage(vehicle_guid, 21, player.GUID.guid)) //fte and ownership? - case VehicleSpawnPad.PlayerSeatedInVehicle(vehicle) => - vehicleService ! VehicleServiceMessage.DelayedVehicleDeconstruction(vehicle, continent, 21L) //sitting in the vehicle clears the drive away delay + case VehicleSpawnPad.PlayerSeatedInVehicle(vehicle, pad) => val vehicle_guid = vehicle.GUID + if(player.VehicleSeated.nonEmpty) { + vehicleService ! VehicleServiceMessage.UnscheduleDeconstruction(vehicle_guid) + } sendResponse(PlanetsideAttributeMessage(vehicle_guid, 22, 0L)) //mount points on? //sendResponse(PlanetsideAttributeMessage(vehicle_guid, 0, vehicle.Definition.MaxHealth))) sendResponse(PlanetsideAttributeMessage(vehicle_guid, 68, 0L)) //??? sendResponse(PlanetsideAttributeMessage(vehicle_guid, 113, 0L)) //??? ReloadVehicleAccessPermissions(vehicle) + ServerVehicleLock(vehicle) - case VehicleSpawnPad.SpawnPadBlockedWarning(vehicle, warning_count) => - if(warning_count > 2) { - sendResponse(TriggerSoundMessage(TriggeredSound.Unknown14, vehicle.Position, 20, 1f)) - sendResponse( - ChatMsg(ChatMessageType.CMT_TELL, true, "", "\\#FYour vehicle is blocking the spawn pad, and will be deconstructed if not moved.", None) - ) + case VehicleSpawnPad.ServerVehicleOverrideStart(vehicle, pad) => + val vdef = vehicle.Definition + if(vehicle.Seats(0).isOccupied) { + sendResponse(ObjectDetachMessage(pad.GUID, vehicle.GUID, pad.Position + Vector3(0, 0, 0.5f), 0, 0, pad.Orientation.z)) + } + ServerVehicleOverride(vehicle, vdef.AutoPilotSpeed1, GlobalDefinitions.isFlightVehicle(vdef):Int) + + case VehicleSpawnControlGuided.GuidedControl(cmd, vehicle, data) => + cmd match { + case AutoDriveControls.State.Drive => + val speed : Int = data.getOrElse({ vehicle.Definition.AutoPilotSpeed1 }).asInstanceOf[Int] + ServerVehicleOverride(vehicle, speed) + + case AutoDriveControls.State.Climb => + ServerVehicleOverride(vehicle, controlled.getOrElse(0), GlobalDefinitions.isFlightVehicle(vehicle.Definition):Int) + + case AutoDriveControls.State.Turn => + //TODO how to turn hovering/flying vehicle? + val direction = data.getOrElse(15).asInstanceOf[Int] + sendResponse(VehicleStateMessage(vehicle.GUID, 0, vehicle.Position, vehicle.Orientation, vehicle.Velocity, None, 0, 0, direction, false, false)) + + + case AutoDriveControls.State.Stop => + ServerVehicleOverride(vehicle, 0) + + case _ => ; } - case VehicleSpawnPad.SpawnPadUnblocked(vehicle_guid) => - //vehicle has moved away from spawn pad after initial spawn - vehicleService ! VehicleServiceMessage.UnscheduleDeconstruction(vehicle_guid) //cancel temporary drive away from pad delay + case VehicleSpawnPad.ServerVehicleOverrideEnd(vehicle, pad) => + sendResponse(GenericObjectActionMessage(pad.GUID, 92)) //reset spawn pad + DriverVehicleControl(vehicle, vehicle.Definition.AutoPilotSpeed2) + + case VehicleSpawnPad.PeriodicReminder(cause, data) => + val msg : String = (cause match { + case VehicleSpawnPad.Reminders.Blocked => + s"The vehicle spawn where you placed your order is blocked. ${data.getOrElse("")}" + case VehicleSpawnPad.Reminders.Queue => + s"Your position in the vehicle spawn queue is ${data.get}." + }) + sendResponse(ChatMsg(ChatMessageType.CMT_OPEN, true, "", msg, None)) case ListAccountCharacters => import net.psforever.objects.definition.converter.CharacterSelectConverter @@ -1539,7 +1585,7 @@ class WorldSessionActor extends Actor with MDCContextAware { case _ => log.warn(s"VehicleState: no vehicle $vehicle_guid found in zone") } - //log.info("VehicleState: " + msg) + //log.info(s"VehicleState: $msg") case msg @ VehicleSubStateMessage(vehicle_guid, player_guid, vehicle_pos, vehicle_ang, vel, unk1, unk2) => //log.info(s"VehicleSubState: $vehicle_guid, $player_guid, $vehicle_pos, $vehicle_ang, $vel, $unk1, $unk2") @@ -2753,14 +2799,13 @@ class WorldSessionActor extends Actor with MDCContextAware { new Task() { private val localVehicle = obj private val localPad = pad.Actor - private val localAnnounce = vehicleService private val localSession : String = sessionId.toString private val localPlayer = player private val localVehicleService = vehicleService private val localZone = continent override def isComplete : Task.Resolution.Value = { - if(localVehicle.Actor != ActorRef.noSender) { + if(localVehicle.HasGUID) { Task.Resolution.Success } else { @@ -2769,9 +2814,7 @@ class WorldSessionActor extends Actor with MDCContextAware { } def Execute(resolver : ActorRef) : Unit = { - localAnnounce ! VehicleServiceMessage.GiveActorControl(obj, localSession) localPad ! VehicleSpawnPad.VehicleOrder(localPlayer, localVehicle) - localVehicleService ! VehicleServiceMessage.DelayedVehicleDeconstruction(localVehicle, localZone, 60L) resolver ! scala.util.Success(this) } }, List(RegisterVehicle(obj))) @@ -3496,6 +3539,7 @@ class WorldSessionActor extends Actor with MDCContextAware { sendResponse(AvatarDeadStateMessage(DeadState.Dead, respawnTimer, respawnTimer, pos, player.Faction, true)) if(tplayer.VehicleSeated.nonEmpty) { //make player invisible (if not, the cadaver sticks out the side in a seated position) + TotalDriverVehicleControl(continent.GUID(tplayer.VehicleSeated.get).get.asInstanceOf[Vehicle]) sendResponse(PlanetsideAttributeMessage(player_guid, 29, 1)) avatarService ! AvatarServiceMessage(continent.Id, AvatarAction.PlanetsideAttribute(player_guid, 29, 1)) } @@ -3852,6 +3896,54 @@ class WorldSessionActor extends Actor with MDCContextAware { tplayer.Armor == tplayer.MaxArmor } + /** + * Lock all applicable controls of the current vehicle. + * This includes forward motion, turning, and, if applicable, strafing. + * @param vehicle the vehicle being controlled + */ + def ServerVehicleLock(vehicle : Vehicle) : Unit = { + controlled = Some(0) + sendResponse(ServerVehicleOverrideMsg(true, true, false, false, 0, 1, 0, Some(0))) + } + + /** + * Place the current vehicle under the control of the server's commands. + * @param vehicle the vehicle + * @param speed how fast the vehicle is moving forward + * @param flight whether the vehicle is ascending or not, if the vehicle is an applicable type + */ + def ServerVehicleOverride(vehicle : Vehicle, speed : Int = 0, flight : Int = 0) : Unit = { + controlled = Some(speed) + sendResponse(ServerVehicleOverrideMsg(true, true, false, false, flight, 0, speed, Some(0))) + } + + /** + * Place the current vehicle under the control of the driver's commands, + * but leave it in a cancellable auto-drive. + * @param vehicle the vehicle + * @param speed how fast the vehicle is moving forward + * @param flight whether the vehicle is ascending or not, if the vehicle is an applicable type + */ + def DriverVehicleControl(vehicle : Vehicle, speed : Int = 0, flight : Int = 0) : Unit = { + if(controlled.nonEmpty) { + controlled = None + sendResponse(ServerVehicleOverrideMsg(false, false, false, true, flight, 0, speed, None)) + } + } + + /** + * Place the current vehicle under the control of the driver's commands, + * but leave it in a cancellable auto-drive. + * Stop all movement entirely. + * @param vehicle the vehicle + */ + def TotalDriverVehicleControl(vehicle : Vehicle) : Unit = { + if(controlled.nonEmpty) { + controlled = None + sendResponse(ServerVehicleOverrideMsg(false, false, false, false, 0, 0, 0, None)) + } + } + def failWithError(error : String) = { log.error(error) sendResponse(ConnectionClose()) diff --git a/pslogin/src/main/scala/Zones.scala b/pslogin/src/main/scala/Zones.scala index 1944179f..55d18bb9 100644 --- a/pslogin/src/main/scala/Zones.scala +++ b/pslogin/src/main/scala/Zones.scala @@ -1,5 +1,7 @@ // Copyright (c) 2017 PSForever import akka.actor.ActorContext +import net.psforever.objects.serverobject.pad.VehicleSpawnPad +import net.psforever.objects.serverobject.pad.process._ import net.psforever.objects.zones.Zone import net.psforever.types.PlanetSideEmpire @@ -49,6 +51,10 @@ object Zones { import net.psforever.types.PlanetSideEmpire Buildings.values.foreach { _.Faction = PlanetSideEmpire.VS } Building(29).get.Faction = PlanetSideEmpire.NC //South Villa Gun Tower + GUID(293).get.asInstanceOf[VehicleSpawnPad].Railed = false //building 52 + GUID(706).get.asInstanceOf[VehicleSpawnPad].Guide = List(AutoDriveControls.DistanceFromHere(50f)) //building 77 + GUID(710).get.asInstanceOf[VehicleSpawnPad].Railed = false //building 79 + GUID(712).get.asInstanceOf[VehicleSpawnPad].Railed = false //building 81 } } diff --git a/pslogin/src/main/scala/services/vehicle/VehicleResponse.scala b/pslogin/src/main/scala/services/vehicle/VehicleResponse.scala index a7bed3bc..5c2fb5b2 100644 --- a/pslogin/src/main/scala/services/vehicle/VehicleResponse.scala +++ b/pslogin/src/main/scala/services/vehicle/VehicleResponse.scala @@ -9,14 +9,19 @@ import net.psforever.types.{DriveState, Vector3} object VehicleResponse { trait Response + final case class AttachToRails(vehicle_guid : PlanetSideGUID, rails_guid : PlanetSideGUID) extends Response final case class Awareness(vehicle_guid : PlanetSideGUID) extends Response final case class ChildObjectState(object_guid : PlanetSideGUID, pitch : Float, yaw : Float) extends Response + final case class ConcealPlayer(player_guid : PlanetSideGUID) extends Response final case class DeployRequest(object_guid : PlanetSideGUID, state : DriveState.Value, unk1 : Int, unk2 : Boolean, pos : Vector3) extends Response + final case class DetachFromRails(vehicle_guid : PlanetSideGUID, rails_guid : PlanetSideGUID, rails_pos : Vector3, rails_rot : Float) extends Response final case class DismountVehicle(unk1 : Int, unk2 : Boolean) extends Response final case class InventoryState(obj : PlanetSideGameObject, parent_guid : PlanetSideGUID, start : Int, con_data : ConstructorData) extends Response final case class KickPassenger(unk1 : Int, unk2 : Boolean, vehicle_guid : PlanetSideGUID) extends Response final case class LoadVehicle(vehicle : Vehicle, vtype : Int, vguid : PlanetSideGUID, vdata : ConstructorData) extends Response final case class MountVehicle(object_guid : PlanetSideGUID, seat : Int) extends Response + final case class ResetSpawnPad(pad_guid : PlanetSideGUID) extends Response + final case class RevealPlayer(player_guid : PlanetSideGUID) extends Response final case class SeatPermissions(vehicle_guid : PlanetSideGUID, seat_group : Int, permission : Long) extends Response final case class StowEquipment(vehicle_guid : PlanetSideGUID, slot : Int, itype : Int, iguid : PlanetSideGUID, idata : ConstructorData) extends Response final case class UnloadVehicle(vehicle_guid : PlanetSideGUID) extends Response diff --git a/pslogin/src/main/scala/services/vehicle/VehicleService.scala b/pslogin/src/main/scala/services/vehicle/VehicleService.scala index 26a2412e..e820db05 100644 --- a/pslogin/src/main/scala/services/vehicle/VehicleService.scala +++ b/pslogin/src/main/scala/services/vehicle/VehicleService.scala @@ -2,11 +2,12 @@ package services.vehicle import akka.actor.{Actor, ActorRef, Props} -import services.vehicle.support.{DeconstructionActor, DelayedDeconstructionActor, VehicleContextActor} +import net.psforever.objects.serverobject.pad.VehicleSpawnPad +import net.psforever.objects.zones.Zone +import services.vehicle.support.{DeconstructionActor, DelayedDeconstructionActor} import services.{GenericEventBus, Service} class VehicleService extends Actor { - private val vehicleContext : ActorRef = context.actorOf(Props[VehicleContextActor], "vehicle-context-root") private val vehicleDecon : ActorRef = context.actorOf(Props[DeconstructionActor], "vehicle-decon-agent") private val vehicleDelayedDecon : ActorRef = context.actorOf(Props[DelayedDeconstructionActor], "vehicle-delayed-decon-agent") vehicleDecon ! DeconstructionActor.RequestTaskResolver @@ -91,14 +92,6 @@ class VehicleService extends Actor { case _ => ; } - //message to VehicleContext - case VehicleServiceMessage.GiveActorControl(vehicle, actorName) => - vehicleContext ! VehicleServiceMessage.GiveActorControl(vehicle, actorName) - - //message to VehicleContext - case VehicleServiceMessage.RevokeActorControl(vehicle) => - vehicleContext ! VehicleServiceMessage.RevokeActorControl(vehicle) - //message to DeconstructionActor case VehicleServiceMessage.RequestDeleteVehicle(vehicle, continent) => vehicleDecon ! DeconstructionActor.RequestDeleteVehicle(vehicle, continent) @@ -117,6 +110,53 @@ class VehicleService extends Actor { VehicleServiceResponse(s"/$zone_id/Vehicle", Service.defaultPlayerGUID, VehicleResponse.UnloadVehicle(vehicle_guid)) ) + //from VehicleSpawnControl + case VehicleSpawnPad.ConcealPlayer(player_guid, zone_id) => + VehicleEvents.publish( + VehicleServiceResponse(s"/$zone_id/Vehicle", Service.defaultPlayerGUID, VehicleResponse.ConcealPlayer(player_guid)) + ) + + //from VehicleSpawnControl + case VehicleSpawnPad.AttachToRails(vehicle, pad, zone_id) => + VehicleEvents.publish( + VehicleServiceResponse(s"/$zone_id/Vehicle", Service.defaultPlayerGUID, VehicleResponse.AttachToRails(vehicle.GUID, pad.GUID)) + ) + + //from VehicleSpawnControl + case VehicleSpawnPad.DetachFromRails(vehicle, pad, zone_id) => + VehicleEvents.publish( + VehicleServiceResponse(s"/$zone_id/Vehicle", Service.defaultPlayerGUID, VehicleResponse.DetachFromRails(vehicle.GUID, pad.GUID, pad.Position, pad.Orientation.z)) + ) + + //from VehicleSpawnControl + case VehicleSpawnPad.ResetSpawnPad(pad, zone_id) => + VehicleEvents.publish( + VehicleServiceResponse(s"/$zone_id/Vehicle", Service.defaultPlayerGUID, VehicleResponse.ResetSpawnPad(pad.GUID)) + ) + + case VehicleSpawnPad.RevealPlayer(player_guid, zone_id) => + VehicleEvents.publish( + VehicleServiceResponse(s"/$zone_id/Vehicle", Service.defaultPlayerGUID, VehicleResponse.RevealPlayer(player_guid)) + ) + + //from VehicleSpawnControl + case VehicleSpawnPad.LoadVehicle(vehicle, zone) => + val definition = vehicle.Definition + val vtype = definition.ObjectId + val vguid = vehicle.GUID + val vdata = definition.Packet.ConstructorData(vehicle).get + zone.Transport ! Zone.Vehicle.Spawn(vehicle) + VehicleEvents.publish( + VehicleServiceResponse(s"/${zone.Id}/Vehicle", Service.defaultPlayerGUID, VehicleResponse.LoadVehicle(vehicle, vtype, vguid, vdata)) + ) + vehicleDelayedDecon ! DelayedDeconstructionActor.UnscheduleDeconstruction(vguid) + vehicleDelayedDecon ! DelayedDeconstructionActor.ScheduleDeconstruction(vehicle, zone, 600L) //10min + + //from VehicleSpawnControl + case VehicleSpawnPad.DisposeVehicle(vehicle, zone) => + vehicleDelayedDecon ! DelayedDeconstructionActor.UnscheduleDeconstruction(vehicle.GUID) + vehicleDecon ! DeconstructionActor.RequestDeleteVehicle(vehicle, zone) + case msg => log.info(s"Unhandled message $msg from $sender") } diff --git a/pslogin/src/main/scala/services/vehicle/support/DeconstructionActor.scala b/pslogin/src/main/scala/services/vehicle/support/DeconstructionActor.scala index 2aaefff4..9d7a3efa 100644 --- a/pslogin/src/main/scala/services/vehicle/support/DeconstructionActor.scala +++ b/pslogin/src/main/scala/services/vehicle/support/DeconstructionActor.scala @@ -82,9 +82,8 @@ class DeconstructionActor extends Actor { val vehicle = entry.vehicle val zone = entry.zone vehicle.Position = Vector3.Zero //somewhere it will not disturb anything - entry.zone.Transport ! Zone.DespawnVehicle(vehicle) + entry.zone.Transport ! Zone.Vehicle.Despawn(vehicle) context.parent ! DeconstructionActor.DeleteVehicle(vehicle.GUID, zone.Id) //call up to the main event system - context.parent ! VehicleServiceMessage.RevokeActorControl(vehicle) //call up to a sibling manager taskResolver ! DeconstructionTask(vehicle, zone) }) diff --git a/pslogin/src/main/scala/services/vehicle/support/VehicleContextActor.scala b/pslogin/src/main/scala/services/vehicle/support/VehicleContextActor.scala deleted file mode 100644 index ebc954aa..00000000 --- a/pslogin/src/main/scala/services/vehicle/support/VehicleContextActor.scala +++ /dev/null @@ -1,33 +0,0 @@ -// Copyright (c) 2017 PSForever -package services.vehicle.support - -import akka.actor.{Actor, ActorRef, Props} -import net.psforever.objects.vehicles.VehicleControl -import services.vehicle.VehicleServiceMessage - -/** - * Provide a context for a `Vehicle` `Actor` - the `VehicleControl`.
- *
- * A vehicle can be passed between different zones and, therefore, does not belong to the zone. - * A vehicle cna be given to different players and can persist and change though players have gone. - * Therefore, also does not belong to `WorldSessionActor`. - * A vehicle must anchored to something that exists outside of the `InterstellarCluster` and its agents.
- *
- * The only purpose of this `Actor` is to allow vehicles to borrow a context for the purpose of `Actor` creation. - * It is also be allowed to be responsible for cleaning up that context. - * (In reality, it can be cleaned up anywhere a `PoisonPill` can be sent.)
- *
- * This `Actor` is intended to sit on top of the event system that handles broadcast messaging. - */ -class VehicleContextActor() extends Actor { - def receive : Receive = { - case VehicleServiceMessage.GiveActorControl(vehicle, actorName) => - vehicle.Actor = context.actorOf(Props(classOf[VehicleControl], vehicle), s"${vehicle.Definition.Name}_$actorName.${System.nanoTime()}") - - case VehicleServiceMessage.RevokeActorControl(vehicle) => - vehicle.Actor ! akka.actor.PoisonPill - vehicle.Actor = ActorRef.noSender - - case _ => ; - } -}