From fde49773cd51aca6bf7c711ce56679eb844a5896 Mon Sep 17 00:00:00 2001 From: FateJH Date: Tue, 3 Apr 2018 23:14:44 -0400 Subject: [PATCH] Splitting single vehicle spawn control process into several subtasks. Vehicle event bus attached to zone objects, especially for interacting with VehicleSpawnControl. Importing SouNourS copy of ServerVehicleOverrideMessage packet and tests. --- .../pad/VehicleSpawnControl.scala | 367 ++++++++++-------- .../serverobject/pad/VehicleSpawnPad.scala | 37 +- .../pad/process/VehicleSpawnControlBase.scala | 71 ++++ .../VehicleSpawnControlConcealPlayer.scala | 49 +++ .../VehicleSpawnControlFinalClearance.scala | 36 ++ .../VehicleSpawnControlLoadVehicle.scala | 48 +++ .../process/VehicleSpawnControlRailJack.scala | 45 +++ .../VehicleSpawnControlSeatDriver.scala | 100 +++++ ...cleSpawnControlServerVehicleOverride.scala | 73 ++++ .../net/psforever/objects/zones/Zone.scala | 59 ++- .../objects/zones/ZoneVehicleActor.scala | 68 +++- .../psforever/packet/GamePacketOpcode.scala | 2 +- .../game/ServerVehicleOverrideMsg.scala | 89 +++++ .../game/ServerVehicleOverrideMsgTest.scala | 73 ++++ .../scala/objects/VehicleSpawnPadTest.scala | 32 +- common/src/test/scala/objects/ZoneTest.scala | 29 +- pslogin/src/main/scala/PsLogin.scala | 19 +- .../src/main/scala/WorldSessionActor.scala | 51 +-- .../services/vehicle/VehicleResponse.scala | 2 + .../services/vehicle/VehicleService.scala | 42 +- .../vehicle/support/DeconstructionActor.scala | 7 +- .../vehicle/support/VehicleContextActor.scala | 33 -- 22 files changed, 993 insertions(+), 339 deletions(-) create mode 100644 common/src/main/scala/net/psforever/objects/serverobject/pad/process/VehicleSpawnControlBase.scala create mode 100644 common/src/main/scala/net/psforever/objects/serverobject/pad/process/VehicleSpawnControlConcealPlayer.scala create mode 100644 common/src/main/scala/net/psforever/objects/serverobject/pad/process/VehicleSpawnControlFinalClearance.scala create mode 100644 common/src/main/scala/net/psforever/objects/serverobject/pad/process/VehicleSpawnControlLoadVehicle.scala create mode 100644 common/src/main/scala/net/psforever/objects/serverobject/pad/process/VehicleSpawnControlRailJack.scala create mode 100644 common/src/main/scala/net/psforever/objects/serverobject/pad/process/VehicleSpawnControlSeatDriver.scala create mode 100644 common/src/main/scala/net/psforever/objects/serverobject/pad/process/VehicleSpawnControlServerVehicleOverride.scala create mode 100644 common/src/main/scala/net/psforever/packet/game/ServerVehicleOverrideMsg.scala create mode 100644 common/src/test/scala/game/ServerVehicleOverrideMsgTest.scala delete mode 100644 pslogin/src/main/scala/services/vehicle/support/VehicleContextActor.scala 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..bd2d18d7 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,173 +1,119 @@ // 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 + self ! VehicleSpawnControl.ProcessControl.GetOrder + } + 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.GetOrder => + 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 } - orders = remainingOrders - completeOrder 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) + + case VehicleSpawnControl.ProcessControl.CancelOrder => + VehicleSpawnControl.recursiveFindOrder(orders.iterator, sender) match { case None => ; + case Some(index) => + val dequeuedOrder = orders(index) + orders = orders.take(index - 1) ++ orders.drop(index + 1) + trace(s"${dequeuedOrder.driver}'s vehicle order has been cancelled") } - 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.ProcessControl.GetNewOrder => + if(sender == concealPlayer) { + trackedOrder = None //guard off + self ! VehicleSpawnControl.ProcessControl.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.ProcessControl.Reminder => + /* + 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. + */ + if(periodicReminder.isCancelled) { + trace(s"the pad has become blocked by ${trackedOrder.get.vehicle.Definition.Name}") + periodicReminder = context.system.scheduler.schedule( + VehicleSpawnControl.initialReminderDelay, + VehicleSpawnControl.periodicReminderDelay, + self, VehicleSpawnControl.ProcessControl.Reminder + ) } - - 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 + else { + VehicleSpawnControl.BlockedReminder(trackedOrder, trackedOrder.get +: orders) } case _ => ; @@ -175,31 +121,144 @@ class VehicleSpawnControl(pad : VehicleSpawnPad) extends Actor with FactionAffin } 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 + * A `TaskResolver` to assist with the deconstruction of vehicles. + * Treated like a `lazy val`, this only gets defined once and then keeps getting reused. + * Since the use case is "if something goes wrong," a limited implementation should be fine. */ - object Process extends Enumeration { + private var emergencyResolver : Option[ActorRef] = None + + /** + * An `Enumeration` of non-data control messages for the vehicle spawn process. + */ + object ProcessControl extends Enumeration { val + Reminder, GetOrder, - ConcealPlayer, - LoadVehicle, - AwaitSeated, - AwaitClearance + GetNewOrder, + CancelOrder = 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 AwaitDriverInSeat(entry : VehicleSpawnControl.Order) extends Order(entry) + final case class DriverInSeat(entry : VehicleSpawnControl.Order) extends Order(entry) + final case class RailJackAction(entry : VehicleSpawnControl.Order) extends Order(entry) + final case class RailJackRelease(entry : VehicleSpawnControl.Order) extends Order(entry) + final case class ServerVehicleOverride(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. + * @param vehicle the vehicle + * @param player the driver + * @param zone the continent on which the vehicle was registered + * @param context an `ActorContext` object for which to create the `TaskResolver` object + */ + def DisposeVehicle(vehicle : Vehicle, player : Player, zone: Zone)(implicit context : ActorContext) : Unit = { + import net.psforever.objects.guid.GUIDTask + emergencyResolver.getOrElse({ + import akka.routing.SmallestMailboxPool + import net.psforever.objects.guid.TaskResolver + val resolver = context.actorOf(SmallestMailboxPool(10).props(Props[TaskResolver]), "vehicle-spawn-control-emergency-decon-resolver") + emergencyResolver = Some(resolver) + resolver + }) ! GUIDTask.UnregisterVehicle(vehicle)(zone.GUID) + zone.VehicleEvents ! VehicleSpawnPad.RevealPlayer(player.GUID, zone.Id) + } + /** + * Properly clean up a vehicle that has been registered and spawned into the game world. + * @param vehicle the vehicle + * @param player the driver + * @param zone the continent on which the vehicle was registered + */ + def DisposeSpawnedVehicle(vehicle : Vehicle, player : Player, zone: Zone) : Unit = { + zone.VehicleEvents ! VehicleSpawnPad.DisposeVehicle(vehicle, zone) + zone.VehicleEvents ! VehicleSpawnPad.RevealPlayer(player.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(s"Your position in the vehicle spawn queue is $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 : Option[VehicleSpawnControl.Order], recipients : Seq[VehicleSpawnControl.Order]) : Unit = { + blockedOrder match { + case Some(entry) => + val msg : String = if(entry.vehicle.Health == 0) { + "The vehicle spawn where you placed your order is blocked by wreckage." + } + else { + "The vehicle spawn where you placed your order is blocked." + } + VehicleSpawnControl.recursiveBlockedReminder(recipients.iterator, msg) + case None => ; + } + } + + @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], msg : String) : Unit = { + if(iter.hasNext) { + val recipient = iter.next + if(recipient.sendTo != ActorRef.noSender) { + recipient.sendTo ! VehicleSpawnPad.PeriodicReminder(msg) + } + recursiveBlockedReminder(iter, msg) + } + } + + @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..43287c8e 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 @@ -3,6 +3,7 @@ package net.psforever.objects.serverobject.pad 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 /** @@ -34,7 +35,12 @@ object VehicleSpawnPad { * An packet `GenericObjectActionMessage(/player/, 36)`, when used on a player character, * will cause that player character's model to fade into transparency. */ - final case class ConcealPlayer() + final case class ConcealPlayer(player_guid : PlanetSideGUID, zone_id : String) + + /** + * Undoes the above message. + */ + final case class RevealPlayer(player_guid : PlanetSideGUID, zone_id : String) /** * A callback step in spawning the vehicle. @@ -44,9 +50,10 @@ object VehicleSpawnPad { * 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. * @param vehicle the vehicle being spawned - * @param pad the pad */ - final case class LoadVehicle(vehicle : Vehicle, pad : VehicleSpawnPad) + final case class LoadVehicle(vehicle : Vehicle, zone : Zone) + + final case class StartPlayerSeatedInVehicle(vehicle : Vehicle) /** * A TEMPORARY callback step in spawning the vehicle. @@ -57,25 +64,13 @@ object VehicleSpawnPad { */ final case class PlayerSeatedInVehicle(vehicle : Vehicle) - /** - * 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. - * @param vehicle the vehicle - * @param warning_count the number of times a warning period has occurred - */ - final case class SpawnPadBlockedWarning(vehicle : Vehicle, warning_count : Int) + final case class ServerVehicleOverrideStart(speed : Int) - /** - * 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 - */ - final case class SpawnPadUnblocked(vehicle_guid : PlanetSideGUID) + final case class ServerVehicleOverrideEnd(speed : Int) + + final case class PeriodicReminder(msg : String) + + final case class DisposeVehicle(vehicle : Vehicle, zone : Zone) /** * Overloaded constructor. 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..f1d98f99 --- /dev/null +++ b/common/src/main/scala/net/psforever/objects/serverobject/pad/process/VehicleSpawnControlBase.scala @@ -0,0 +1,71 @@ +// 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. + * The primary purpose of this superclass is to provide a common convention for the logging system's name. + * Additional functionality that ewcovers the `Zone` of the owned amenity `VehcileSpawnPad` is also included. + * @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 set to `trace`. + * No important messages should processed by this agent; only consume general vehicle spawn status. + * @param msg the message + */ + def trace(msg : String) : Unit = log.info(msg) + + protected def Pad : VehicleSpawnPad = pad + + /** + * 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 called "nowhere." + * Eventually, it will belong to an active `Building` object that belongs to an active `Zone` object with an identifier. + * 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..385221fa --- /dev/null +++ b/common/src/main/scala/net/psforever/objects/serverobject/pad/process/VehicleSpawnControlConcealPlayer.scala @@ -0,0 +1,49 @@ +// 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.isAlive && 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.vehicle, driver, Continent) + context.parent ! VehicleSpawnControl.ProcessControl.GetNewOrder + } + + case VehicleSpawnControl.ProcessControl.Reminder => + context.parent ! VehicleSpawnControl.ProcessControl.Reminder + + case VehicleSpawnControl.ProcessControl.GetNewOrder => + context.parent ! VehicleSpawnControl.ProcessControl.GetNewOrder + + 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..c4014a24 --- /dev/null +++ b/common/src/main/scala/net/psforever/objects/serverobject/pad/process/VehicleSpawnControlFinalClearance.scala @@ -0,0 +1,36 @@ +// 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) => + 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, VehicleSpawnControl.Process.FinalClearance(entry)) + } + + case _ => ; + } +} 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..631fb599 --- /dev/null +++ b/common/src/main/scala/net/psforever/objects/serverobject/pad/process/VehicleSpawnControlLoadVehicle.scala @@ -0,0 +1,48 @@ +// 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.
+ *
+ * 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 seatDriver = context.actorOf(Props(classOf[VehicleSpawnControlSeatDriver], pad), s"${context.parent.path.name}-seat") + + 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}") + Continent.VehicleEvents ! VehicleSpawnPad.LoadVehicle(vehicle, Continent) + context.system.scheduler.scheduleOnce(100 milliseconds, seatDriver, VehicleSpawnControl.Process.SeatDriver(entry)) + } + else { + trace("owner lost; abort order fulfillment") + VehicleSpawnControl.DisposeVehicle(vehicle, entry.driver, Continent) + context.parent ! VehicleSpawnControl.ProcessControl.GetOrder + } + + case VehicleSpawnControl.ProcessControl.Reminder => + context.parent ! VehicleSpawnControl.ProcessControl.Reminder + + case VehicleSpawnControl.ProcessControl.GetNewOrder => + context.parent ! VehicleSpawnControl.ProcessControl.GetNewOrder + + 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..b6fe089a --- /dev/null +++ b/common/src/main/scala/net/psforever/objects/serverobject/pad/process/VehicleSpawnControlRailJack.scala @@ -0,0 +1,45 @@ +// 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. + * It has failure cases should the driver be in an incorrect state.
+ * __It currently does not work__. + * @param pad the `VehicleSpawnPad` object being governed + */ +class VehicleSpawnControlRailJack(pad : VehicleSpawnPad) extends VehicleSpawnControlBase(pad) { + def LogId = "-jacker" + + val vehicleOverride = context.actorOf(Props(classOf[VehicleSpawnControlServerVehicleOverride], pad), s"${context.parent.path.name}-override") + + def receive : Receive = { + case VehicleSpawnControl.Process.RailJackAction(entry) => + if(entry.vehicle.Health == 0) { + //TODO detach vehicle from pad rails if necessary + trace(s"vehicle was already destroyed; clean it up") + VehicleSpawnControl.DisposeSpawnedVehicle(entry.vehicle, entry.driver, Continent) + context.parent ! VehicleSpawnControl.ProcessControl.GetNewOrder + } + else { + trace(s"extending rails with vehicle attached") + context.parent ! VehicleSpawnControl.ProcessControl.Reminder + context.system.scheduler.scheduleOnce(10 milliseconds, vehicleOverride, VehicleSpawnControl.Process.ServerVehicleOverride(entry)) + } + + case VehicleSpawnControl.ProcessControl.GetNewOrder => + context.parent ! VehicleSpawnControl.ProcessControl.GetNewOrder + + 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..128e13c6 --- /dev/null +++ b/common/src/main/scala/net/psforever/objects/serverobject/pad/process/VehicleSpawnControlSeatDriver.scala @@ -0,0 +1,100 @@ +// 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. + * Three 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 railJack = context.actorOf(Props(classOf[VehicleSpawnControlRailJack], pad), s"${context.parent.path.name}-rails") + + def receive : Receive = { + case VehicleSpawnControl.Process.SeatDriver(entry) => + if(entry.vehicle.Actor == ActorRef.noSender) { //wait for the component of the vehicle needed for seating to be loaded + context.system.scheduler.scheduleOnce(50 milliseconds, railJack, VehicleSpawnControl.Process.SeatDriver(entry)) + } + else { + val driver = entry.driver + if(entry.vehicle.Health == 0) { + //TODO detach vehicle from pad rails if necessary + trace("vehicle was already destroyed; clean it up") + VehicleSpawnControl.DisposeSpawnedVehicle(entry.vehicle, driver, Continent) + context.parent ! VehicleSpawnControl.ProcessControl.GetNewOrder + } + else if(entry.sendTo != ActorRef.noSender && driver.isAlive && driver.Continent == Continent.Id && driver.VehicleSeated.isEmpty) { + trace("driver to be made seated in vehicle") + entry.sendTo ! VehicleSpawnPad.StartPlayerSeatedInVehicle(entry.vehicle) + entry.vehicle.Actor.tell(Mountable.TryMount(driver, 0), entry.sendTo) //entry.sendTo should handle replies to TryMount + context.system.scheduler.scheduleOnce(1000 milliseconds, self, VehicleSpawnControl.Process.AwaitDriverInSeat(entry)) + } + else { + trace("driver lost; vehicle stranded on pad") + context.system.scheduler.scheduleOnce(1000 milliseconds, railJack, VehicleSpawnControl.Process.RailJackAction(entry)) + } + } + + case VehicleSpawnControl.Process.AwaitDriverInSeat(entry) => + val driver = entry.driver + if(entry.vehicle.Health == 0) { + //TODO detach vehicle from pad rails if necessary + trace("vehicle was already destroyed; clean it up") + VehicleSpawnControl.DisposeSpawnedVehicle(entry.vehicle, driver, Continent) + context.parent ! VehicleSpawnControl.ProcessControl.GetNewOrder + } + else if(entry.sendTo == ActorRef.noSender) { + trace("driver lost, but operations can continue") + self ! VehicleSpawnControl.Process.RailJackAction(entry) + } + else if(driver.isAlive && driver.Continent == Continent.Id && driver.VehicleSeated.isEmpty) { + context.system.scheduler.scheduleOnce(1000 milliseconds, self, VehicleSpawnControl.Process.AwaitDriverInSeat(entry)) + } + else { + trace(s"driver is sitting down") + context.system.scheduler.scheduleOnce(1000 milliseconds, self, VehicleSpawnControl.Process.DriverInSeat(entry)) + } + + case VehicleSpawnControl.Process.DriverInSeat(entry) => + if(entry.vehicle.Health == 0) { + //TODO detach vehicle from pad rails if necessary + trace(s"vehicle was already destroyed; clean it up") + VehicleSpawnControl.DisposeSpawnedVehicle(entry.vehicle, entry.driver, Continent) + context.parent ! VehicleSpawnControl.ProcessControl.GetNewOrder + } + else if(entry.sendTo != ActorRef.noSender) { + trace(s"driver ${entry.driver.Name} has taken the wheel") + entry.sendTo ! VehicleSpawnPad.PlayerSeatedInVehicle(entry.vehicle) + context.system.scheduler.scheduleOnce(10 milliseconds, railJack, VehicleSpawnControl.Process.RailJackAction(entry)) + } + else { + trace("driver lost, but operations can continue") + context.system.scheduler.scheduleOnce(10 milliseconds, railJack, VehicleSpawnControl.Process.RailJackAction(entry)) + } + + case VehicleSpawnControl.ProcessControl.Reminder => + context.parent ! VehicleSpawnControl.ProcessControl.Reminder + + case VehicleSpawnControl.ProcessControl.GetNewOrder => + context.parent ! VehicleSpawnControl.ProcessControl.GetNewOrder + + case _ => ; + } +} 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..7f9419d6 --- /dev/null +++ b/common/src/main/scala/net/psforever/objects/serverobject/pad/process/VehicleSpawnControlServerVehicleOverride.scala @@ -0,0 +1,73 @@ +// 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 finalClear = context.actorOf(Props(classOf[VehicleSpawnControlFinalClearance], pad), s"${context.parent.path.name}-final") + + def receive : Receive = { + case VehicleSpawnControl.Process.ServerVehicleOverride(entry) => + val vehicle = entry.vehicle + //TODO detach vehicle from pad rails + if(vehicle.Health == 0) { + trace(s"vehicle was already destroyed; but, everything is fine") + finalClear ! VehicleSpawnControl.Process.FinalClearance(entry) + } + else if(entry.sendTo != ActorRef.noSender && 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(22) + context.system.scheduler.scheduleOnce(3000 milliseconds, self, VehicleSpawnControl.Process.DriverVehicleControl(entry)) + } + else { + finalClear ! VehicleSpawnControl.Process.FinalClearance(entry) + } + + case VehicleSpawnControl.Process.DriverVehicleControl(entry) => + val vehicle = entry.vehicle + if(entry.sendTo != ActorRef.noSender) { + if(vehicle.Health == 0) { + trace(s"vehicle was already destroyed; but, everything is fine") + } + else { + val driver = entry.driver + entry.sendTo ! VehicleSpawnPad.ServerVehicleOverrideEnd(8) + if(driver.VehicleSeated.contains(vehicle.GUID)) { + trace(s"returning control of ${vehicle.Definition.Name} to ${driver.Name}") + } + else { + trace(s"${driver.Name} is no longer seated in new ${vehicle.Definition.Name}; can not properly return control to driver") + } + } + } + else { + trace("can not properly return control to driver") + } + finalClear ! VehicleSpawnControl.Process.FinalClearance(entry) + + case msg @ VehicleSpawnControl.Process.FinalClearance(_) => + finalClear ! msg + + case VehicleSpawnControl.ProcessControl.GetNewOrder => + context.parent ! VehicleSpawnControl.ProcessControl.GetNewOrder + + 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/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..9672d985 --- /dev/null +++ b/common/src/main/scala/net/psforever/packet/game/ServerVehicleOverrideMsg.scala @@ -0,0 +1,89 @@ +// 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 the vehicle being overrode. + * No message is displayed if the vehicle is placed under server control. + * The vehicle will operate as if accelerating.
+ *
+ * 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.)
+ *
+ * Speed samples follow (from AMS):
+ * 1 -> 3
+ * 2 -> 7
+ * 3 -> 10
+ * 10 -> 35
+ * 15 -> 52
+ * 20 -> 68 + * @param lock_accelerator driver has no control over whether vehicle accelerates + * @param lock_wheel driver has no control over whether the vehicle turns + * @param reverse drive in reverse + * @param unk4 na + * @param unk5 na + * @param unk6 na + * @param speed "something like speed;" + * for `n`, the pattern to calculate a constant in-game speed is `floor(3.5 x n)`; + * during server control, an acceleration value (?); + * during auto-drive, a velocity value + * @param unk8 na; + * set `lock_wheel` to `true` to expose value + */ +final case class ServerVehicleOverrideMsg(lock_accelerator : Boolean, + lock_wheel : Boolean, + reverse : Boolean, + unk4 : Boolean, + unk5 : Int, + unk6 : Int, + speed : Int, + unk8 : Option[Long] + ) extends PlanetSideGamePacket { + type Packet = ServerVehicleOverrideMsg + def opcode = GamePacketOpcode.ServerVehicleOverrideMsg + def encode = ServerVehicleOverrideMsg.encode(this) +} + +object ServerVehicleOverrideMsg extends Marshallable[ServerVehicleOverrideMsg] { + /** + * Common assert control packet format. + * @param speed "something like speed" + * @return a `ServerVehicleOverrideMsg` packet + */ + def On(speed : Int) : ServerVehicleOverrideMsg = { + ServerVehicleOverrideMsg(true, true, false, false, 0, 0, speed, Some(0)) + } + + /** + * Common relinquish control packet format. + * @param speed "something like speed" + * @return a `ServerVehicleOverrideMsg` packet + */ + def Off(speed : Int) : ServerVehicleOverrideMsg = { + ServerVehicleOverrideMsg(false, false, false, true, 0, 0, speed, None) + } + + implicit val codec: Codec[ServerVehicleOverrideMsg] = ( + ("lock_accelerator" | bool) :: + (("lock_wheel" | bool) >>:~ { test => + ("reverse" | bool) :: + ("unk4" | bool) :: + ("unk5" | uint2L) :: + ("unk6" | uint2L) :: + ("speed" | uintL(9)) :: + conditional(test, "unk8" | uint32L) + }) + ).as[ServerVehicleOverrideMsg] +} diff --git a/common/src/test/scala/game/ServerVehicleOverrideMsgTest.scala b/common/src/test/scala/game/ServerVehicleOverrideMsgTest.scala new file mode 100644 index 00000000..c6e84da2 --- /dev/null +++ b/common/src/test/scala/game/ServerVehicleOverrideMsgTest.scala @@ -0,0 +1,73 @@ +// 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 + } + + "encode (3)" in { + val msg = ServerVehicleOverrideMsg.On(12) + val pkt = PacketCoding.EncodePacket(msg).require.toByteVector + + pkt mustEqual string1 + } + + "encode (4)" in { + val msg = ServerVehicleOverrideMsg.Off(5) + val pkt = PacketCoding.EncodePacket(msg).require.toByteVector + + pkt mustEqual string2 + } +} diff --git a/common/src/test/scala/objects/VehicleSpawnPadTest.scala b/common/src/test/scala/objects/VehicleSpawnPadTest.scala index d4393b5d..697a7dcc 100644 --- a/common/src/test/scala/objects/VehicleSpawnPadTest.scala +++ b/common/src/test/scala/objects/VehicleSpawnPadTest.scala @@ -55,22 +55,22 @@ class VehicleSpawnControl2Test extends ActorTest() { 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) - - 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 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) - - 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) +// assert(reply2.asInstanceOf[VehicleSpawnPad.LoadVehicle].pad == pad) +// +// 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 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) +// +// 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) } } } 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/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..2ba49967 100644 --- a/pslogin/src/main/scala/WorldSessionActor.scala +++ b/pslogin/src/main/scala/WorldSessionActor.scala @@ -409,6 +409,9 @@ class WorldSessionActor extends Actor with MDCContextAware { 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)) @@ -456,6 +459,9 @@ class WorldSessionActor extends Actor with MDCContextAware { sendResponse(ObjectAttachMessage(vehicle_guid, guid, seat)) } + case VehicleResponse.RevealPlayer(player_guid) => + //TODO any action will cause the player to appear after the effects of ConcealPlayer + case VehicleResponse.SeatPermissions(vehicle_guid, seat_group, permission) => if(tplayer_guid != guid) { sendResponse(PlanetsideAttributeMessage(vehicle_guid, seat_group, permission)) @@ -1000,46 +1006,30 @@ 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) => 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 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) - 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(speed) => + sendResponse(ServerVehicleOverrideMsg.On(speed)) - 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(speed) => + sendResponse(ServerVehicleOverrideMsg.Off(speed)) + + case VehicleSpawnPad.PeriodicReminder(msg) => + sendResponse(ChatMsg(ChatMessageType.CMT_OPEN, true, "", msg, None)) case ListAccountCharacters => import net.psforever.objects.definition.converter.CharacterSelectConverter @@ -2753,14 +2743,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 +2758,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))) diff --git a/pslogin/src/main/scala/services/vehicle/VehicleResponse.scala b/pslogin/src/main/scala/services/vehicle/VehicleResponse.scala index a7bed3bc..106cc779 100644 --- a/pslogin/src/main/scala/services/vehicle/VehicleResponse.scala +++ b/pslogin/src/main/scala/services/vehicle/VehicleResponse.scala @@ -11,12 +11,14 @@ object VehicleResponse { 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 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 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..e19a0176 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,35 @@ 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)) + ) + + 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..97c3223a 100644 --- a/pslogin/src/main/scala/services/vehicle/support/DeconstructionActor.scala +++ b/pslogin/src/main/scala/services/vehicle/support/DeconstructionActor.scala @@ -81,10 +81,13 @@ class DeconstructionActor extends Actor { vehiclesToScrap.foreach(entry => { val vehicle = entry.vehicle val zone = entry.zone +<<<<<<< 18a068c10272376450c412425118c86612af7397 vehicle.Position = Vector3.Zero //somewhere it will not disturb anything - entry.zone.Transport ! Zone.DespawnVehicle(vehicle) +======= + vehicle.Position = Vector3.Zero +>>>>>>> Splitting single vehicle spawn control process into several subtasks with their own control Actor objects. Importing SouNourS copy of ServerVehicleOverrideMessage packet and tests. + 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 _ => ; - } -}