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 9dcc5193..054ef0f3 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 @@ -49,97 +49,85 @@ class VehicleSpawnControl(pad : VehicleSpawnPad) extends VehicleSpawnControlBase trace(s"order from $player for $vehicle received") orders = orders :+ VehicleSpawnControl.Order(player, vehicle, sender) if(trackedOrder.isEmpty && orders.length == 1) { - self ! VehicleSpawnControl.ProcessControl.GetOrder + SelectOrder() } else { sender ! VehicleSpawnControl.RenderOrderRemainderMsg(orders.length + 1) } - 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 - } - - 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.ProcessControl.GetNewOrder => if(sender == concealPlayer) { trackedOrder = None //guard off - self ! VehicleSpawnControl.ProcessControl.GetOrder + SelectOrder() } + /* + 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 => - /* - 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 - ) - } - else { - VehicleSpawnControl.BlockedReminder(trackedOrder, trackedOrder.get +: orders) + trackedOrder match { + case Some(entry) => + 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 => ; + 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 { private final val initialReminderDelay : FiniteDuration = 10000 milliseconds private final val periodicReminderDelay : FiniteDuration = 10000 milliseconds - /** - * 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. - */ - 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, - GetNewOrder, - CancelOrder + GetNewOrder = Value } /** @@ -170,23 +158,46 @@ object VehicleSpawnControl { 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. - * @see `VehicleSpawnControl.emergencyResolver` + * 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 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(entry.vehicle)(zone.GUID) + 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 @@ -204,7 +215,7 @@ object VehicleSpawnControl { * @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.") + VehicleSpawnPad.PeriodicReminder(VehicleSpawnPad.Reminders.Queue, Some(s"$position")) } /** @@ -212,42 +223,38 @@ object VehicleSpawnControl { * @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) + def BlockedReminder(blockedOrder : VehicleSpawnControl.Order, recipients : Seq[VehicleSpawnControl.Order]) : Unit = { + val wrecked : Option[Any] = if(blockedOrder.vehicle.Health == 0) { + Option("Clear the wreckage.") } else { - recursiveFindOrder(iter, target, index + 1) + None } - } + VehicleSpawnControl.recursiveBlockedReminder(recipients.iterator, wrecked) } - @tailrec private final def recursiveBlockedReminder(iter : Iterator[VehicleSpawnControl.Order], msg : String) : Unit = { +// @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 if(recipient.sendTo != ActorRef.noSender) { - recipient.sendTo ! VehicleSpawnPad.PeriodicReminder(msg) + recipient.sendTo ! VehicleSpawnPad.PeriodicReminder(VehicleSpawnPad.Reminders.Blocked, cause) } - recursiveBlockedReminder(iter, msg) + recursiveBlockedReminder(iter, cause) } } 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 46e8f891..9a4c1abe 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 @@ -18,13 +18,12 @@ import net.psforever.packet.game.PlanetSideGUID */ class VehicleSpawnPad(spDef : VehicleSpawnPadDefinition) extends Amenity { /** - * USE THIS BOOLEAN FOR DEVELOPMENT PURPOSES!
- * Purpose: use the ingame railed platform to lift the spawned vehicle out of the trench. - * When set, the client performs the standard vehicle entry procedure, including rail animations. + * 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. - * The main reason to disable this feature is to avoid an `ObjectAttachMessage` that may be dispatched for an incorrect object. - * Unset if not guaranteed to have the correct ingame globally unique id of the spawn pad. + * 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 @@ -41,7 +40,7 @@ class VehicleSpawnPad(spDef : VehicleSpawnPadDefinition) extends Amenity { 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 @@ -49,53 +48,115 @@ 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(player_guid : PlanetSideGUID, zone_id : String) /** - * Undoes the above message. + * 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) /** - * 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 to properly introduce the vehicle into the zone. * @param vehicle the vehicle being spawned + * @param zone the zone in which the spawn pad is located */ final case class LoadVehicle(vehicle : Vehicle, zone : Zone) + /** + * 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 AttachToRails(vehicle : Vehicle, pad : VehicleSpawnPad, zone_id : String) + /** + * 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) /** - * 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 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 pad the spawn pad + */ final case class ServerVehicleOverrideStart(vehicle : Vehicle, pad : VehicleSpawnPad) + /** + * 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 ServerVehicleOverrideEnd(vehicle : Vehicle, pad : VehicleSpawnPad) - final case class ResetSpawnPad(pad : VehicleSpawnPad, zone_id : String) - - final case class PeriodicReminder(msg : String) - + /** + * 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. * @param spDef the spawn pad's definition entry @@ -109,7 +170,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/VehicleSpawnControlConcealPlayer.scala b/common/src/main/scala/net/psforever/objects/serverobject/pad/process/VehicleSpawnControlConcealPlayer.scala index 5c356c06..a7c3f59d 100644 --- a/common/src/main/scala/net/psforever/objects/serverobject/pad/process/VehicleSpawnControlConcealPlayer.scala +++ b/common/src/main/scala/net/psforever/objects/serverobject/pad/process/VehicleSpawnControlConcealPlayer.scala @@ -27,7 +27,7 @@ class VehicleSpawnControlConcealPlayer(pad : VehicleSpawnPad) extends VehicleSpa 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) { + 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)) diff --git a/common/src/main/scala/net/psforever/objects/serverobject/pad/process/VehicleSpawnControlLoadVehicle.scala b/common/src/main/scala/net/psforever/objects/serverobject/pad/process/VehicleSpawnControlLoadVehicle.scala index 5ee0d14b..e278f51f 100644 --- a/common/src/main/scala/net/psforever/objects/serverobject/pad/process/VehicleSpawnControlLoadVehicle.scala +++ b/common/src/main/scala/net/psforever/objects/serverobject/pad/process/VehicleSpawnControlLoadVehicle.scala @@ -23,7 +23,7 @@ import scala.concurrent.duration._ class VehicleSpawnControlLoadVehicle(pad : VehicleSpawnPad) extends VehicleSpawnControlBase(pad) { def LogId = "-loader" - val seatDriver = context.actorOf(Props(classOf[VehicleSpawnControlSeatDriver], pad), s"${context.parent.path.name}-seat") + val railJack = context.actorOf(Props(classOf[VehicleSpawnControlRailJack], pad), s"${context.parent.path.name}-rails") def receive : Receive = { case VehicleSpawnControl.Process.LoadVehicle(entry) => @@ -32,12 +32,12 @@ class VehicleSpawnControlLoadVehicle(pad : VehicleSpawnPad) extends VehicleSpawn trace(s"loading the ${vehicle.Definition.Name}") 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, seatDriver, VehicleSpawnControl.Process.SeatDriver(entry)) + 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.GetOrder + context.parent ! VehicleSpawnControl.ProcessControl.GetNewOrder } case VehicleSpawnControl.ProcessControl.Reminder => 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 index 6d09c0bd..7fd19eee 100644 --- 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 @@ -22,20 +22,25 @@ import scala.concurrent.duration._ class VehicleSpawnControlRailJack(pad : VehicleSpawnPad) extends VehicleSpawnControlBase(pad) { def LogId = "-lifter" - val vehicleOverride = context.actorOf(Props(classOf[VehicleSpawnControlServerVehicleOverride], pad), s"${context.parent.path.name}-override") + val seatDriver = context.actorOf(Props(classOf[VehicleSpawnControlSeatDriver], pad), s"${context.parent.path.name}-seat") 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") + trace("vehicle was already destroyed; clean it up") VehicleSpawnControl.DisposeSpawnedVehicle(entry, Continent) context.parent ! VehicleSpawnControl.ProcessControl.GetNewOrder } else { - trace(s"extending rails with vehicle attached") + 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 temporarily in pad trench") + } context.parent ! VehicleSpawnControl.ProcessControl.Reminder - context.system.scheduler.scheduleOnce(10 milliseconds, vehicleOverride, VehicleSpawnControl.Process.ServerVehicleOverride(entry)) + context.system.scheduler.scheduleOnce(10 milliseconds, seatDriver, VehicleSpawnControl.Process.SeatDriver(entry)) } case VehicleSpawnControl.ProcessControl.GetNewOrder => diff --git a/common/src/main/scala/net/psforever/objects/serverobject/pad/process/VehicleSpawnControlSeatDriver.scala b/common/src/main/scala/net/psforever/objects/serverobject/pad/process/VehicleSpawnControlSeatDriver.scala index a36d215a..5c6eb8ef 100644 --- a/common/src/main/scala/net/psforever/objects/serverobject/pad/process/VehicleSpawnControlSeatDriver.scala +++ b/common/src/main/scala/net/psforever/objects/serverobject/pad/process/VehicleSpawnControlSeatDriver.scala @@ -25,34 +25,32 @@ import scala.concurrent.duration._ class VehicleSpawnControlSeatDriver(pad : VehicleSpawnPad) extends VehicleSpawnControlBase(pad) { def LogId = "-usher" - val railJack = context.actorOf(Props(classOf[VehicleSpawnControlRailJack], pad), s"${context.parent.path.name}-rails") + val vehicleOverride = context.actorOf(Props(classOf[VehicleSpawnControlServerVehicleOverride], pad), s"${context.parent.path.name}-override") 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)) + context.system.scheduler.scheduleOnce(50 milliseconds, self, VehicleSpawnControl.Process.SeatDriver(entry)) } else { val driver = entry.driver if(entry.vehicle.Health == 0) { trace("vehicle was already destroyed; clean it up") + if(pad.Railed) { + Continent.VehicleEvents ! VehicleSpawnPad.ResetSpawnPad(pad, Continent.Id) + } VehicleSpawnControl.DisposeSpawnedVehicle(entry, 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, 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, VehicleSpawnControl.Process.AwaitDriverInSeat(entry)) + } 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, 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, VehicleSpawnControl.Process.AwaitDriverInSeat(entry)) - } - else { - if(pad.Railed) { - Continent.VehicleEvents ! VehicleSpawnPad.AttachToRails(entry.vehicle, pad, Continent.Id) - } - trace("driver lost; vehicle stranded on pad") - context.system.scheduler.scheduleOnce(1000 milliseconds, railJack, VehicleSpawnControl.Process.RailJackAction(entry)) - } + trace("driver lost; vehicle stranded on pad") + context.system.scheduler.scheduleOnce(1000 milliseconds, vehicleOverride, VehicleSpawnControl.Process.ServerVehicleOverride(entry)) } } @@ -60,40 +58,42 @@ class VehicleSpawnControlSeatDriver(pad : VehicleSpawnPad) extends VehicleSpawnC val driver = entry.driver if(entry.vehicle.Health == 0) { trace("vehicle was already destroyed; clean it up") + if(pad.Railed) { + Continent.VehicleEvents ! VehicleSpawnPad.ResetSpawnPad(pad, Continent.Id) + } VehicleSpawnControl.DisposeSpawnedVehicle(entry, Continent) context.parent ! VehicleSpawnControl.ProcessControl.GetNewOrder } - else if(entry.sendTo == ActorRef.noSender) { + else if(entry.sendTo == ActorRef.noSender || driver.Continent != Continent.Id) { trace("driver lost, but operations can continue") - self ! VehicleSpawnControl.Process.RailJackAction(entry) + vehicleOverride ! VehicleSpawnControl.Process.ServerVehicleOverride(entry) } - else if(driver.isAlive && driver.Continent == Continent.Id && driver.VehicleSeated.isEmpty) { + else if(driver.isAlive && driver.VehicleSeated.isEmpty) { context.system.scheduler.scheduleOnce(100 milliseconds, self, VehicleSpawnControl.Process.AwaitDriverInSeat(entry)) } else { trace(s"driver is sitting down") - context.system.scheduler.scheduleOnce( - VehicleSpawnControlSeatDriver.RaillessSeatAnimationTimes(entry.vehicle.Definition.Name) milliseconds, - self, - VehicleSpawnControl.Process.DriverInSeat(entry) - ) + val time = if(pad.Railed) 1000 else VehicleSpawnControlSeatDriver.RaillessSeatAnimationTimes(entry.vehicle.Definition.Name) + context.system.scheduler.scheduleOnce(time 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") + if(pad.Railed) { + Continent.VehicleEvents ! VehicleSpawnPad.ResetSpawnPad(pad, Continent.Id) + } VehicleSpawnControl.DisposeSpawnedVehicle(entry, Continent) context.parent ! VehicleSpawnControl.ProcessControl.GetNewOrder } - else if(entry.sendTo != ActorRef.noSender) { + else 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) - context.system.scheduler.scheduleOnce(10 milliseconds, railJack, VehicleSpawnControl.Process.RailJackAction(entry)) + context.system.scheduler.scheduleOnce(10 milliseconds, vehicleOverride, VehicleSpawnControl.Process.ServerVehicleOverride(entry)) } else { trace("driver lost, but operations can continue") - context.system.scheduler.scheduleOnce(10 milliseconds, railJack, VehicleSpawnControl.Process.RailJackAction(entry)) + context.system.scheduler.scheduleOnce(10 milliseconds, vehicleOverride, VehicleSpawnControl.Process.ServerVehicleOverride(entry)) } case VehicleSpawnControl.ProcessControl.Reminder => @@ -118,6 +118,7 @@ object VehicleSpawnControlSeatDriver { "fury" -> 600, "quadassault" -> 600, "quadstealth" -> 600, + "two_man_assault_buggy" -> 1000, "skyguard" -> 1300, "threemanheavybuggy" -> 1000, "twomanheavybuggy" -> 1800, 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 index 798e0ffe..7b2c48f4 100644 --- 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 @@ -26,25 +26,31 @@ class VehicleSpawnControlServerVehicleOverride(pad : VehicleSpawnPad) extends Ve def receive : Receive = { case VehicleSpawnControl.Process.ServerVehicleOverride(entry) => val vehicle = entry.vehicle - //TODO detach vehicle from pad rails + 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) + } finalClear ! VehicleSpawnControl.Process.FinalClearance(entry) } - else if(entry.sendTo != ActorRef.noSender && entry.driver.VehicleSeated.contains(vehicle.GUID)) { + 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, self, VehicleSpawnControl.Process.DriverVehicleControl(entry)) } else { if(pad.Railed) { - Continent.VehicleEvents ! VehicleSpawnPad.DetachFromRails(vehicle, pad, Continent.Id) + Continent.VehicleEvents ! VehicleSpawnPad.ResetSpawnPad(pad, Continent.Id) } finalClear ! VehicleSpawnControl.Process.FinalClearance(entry) } 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") } @@ -59,9 +65,6 @@ class VehicleSpawnControlServerVehicleOverride(pad : VehicleSpawnPad) extends Ve } } else { - if(pad.Railed) { - Continent.VehicleEvents ! VehicleSpawnPad.ResetSpawnPad(pad, Continent.Id) - } trace("can not properly return control to driver") } finalClear ! VehicleSpawnControl.Process.FinalClearance(entry) 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/test/scala/objects/VehicleSpawnPadTest.scala b/common/src/test/scala/objects/VehicleSpawnPadTest.scala index 697a7dcc..9ee32ea6 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,465 @@ 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) -// -// 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) + 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 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(200 milliseconds) + 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(10 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 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(0,0,11) + player.VehicleSeated = None //since shared between orders, is necessary + val probe3Msg5 = probe3.receiveOne(4 seconds) + assert(probe3Msg5.isInstanceOf[VehicleSpawnPad.ResetSpawnPad]) + val probe3Msg6 = probe3.receiveOne(4 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 probe3Msg4 = probe3.receiveOne(200 milliseconds) + 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(10 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(0,0,11) + 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 VehicleSpawnControl5bTest 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(10 seconds) + assert(probe1Msg.isInstanceOf[VehicleSpawnPad.PeriodicReminder]) + assert(probe1Msg.asInstanceOf[VehicleSpawnPad.PeriodicReminder].reason == VehicleSpawnPad.Reminders.Blocked) + } + } +} + +class VehicleSpawnControl6aTest extends ActorTest() { + "VehicleSpawnControl" should { + "the vehicle is destroyed while the player is sitting down; 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]) + + val probe3Msg3 = probe3.receiveOne(3 seconds) + assert(probe3Msg3.isInstanceOf[VehicleSpawnPad.AttachToRails]) + + val probe1Msg1 = probe1.receiveOne(200 milliseconds) + assert(probe1Msg1.isInstanceOf[VehicleSpawnPad.StartPlayerSeatedInVehicle]) + vehicle.Health = 0 //problem + + val probe3Msg5 = probe3.receiveOne(4 seconds) + assert(probe3Msg5.isInstanceOf[VehicleSpawnPad.ResetSpawnPad]) + val probe3Msg6 = probe3.receiveOne(100 milliseconds) + assert(probe3Msg6.isInstanceOf[VehicleSpawnPad.DisposeVehicle]) + val probe3Msg7 = probe3.receiveOne(100 milliseconds) + assert(probe3Msg7.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 + } + } +} + +//TODO test was poor; attempting to check the "if(vehicle.Health ..." cases in VehicleSpawnControlSeatDriver +//class VehicleSpawnControl6bTest extends ActorTest() { +// "VehicleSpawnControl" should { +// "player on wrong continent; the vehicle is then destroyed after being partially spawned, but 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]) +// +// 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 +// +// vehicle.Health = 0 //problem 2 +// val probe3Msg3b = probe3.receiveOne(3 seconds) +// assert(probe3Msg3b.isInstanceOf[VehicleSpawnPad.DetachFromRails]) +// +// val probe3Msg4 = probe3.receiveOne(3 seconds) +// assert(probe3Msg4.isInstanceOf[VehicleSpawnPad.ResetSpawnPad]) +// val probe3Msg5 = probe3.receiveOne(1 seconds) +// assert(probe3Msg5.isInstanceOf[VehicleSpawnPad.DisposeVehicle]) +// val probe3Msg6 = probe3.receiveOne(1 seconds) +// assert(probe3Msg6.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 VehicleSpawnControl6cTest 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(10 seconds) + assert(probe1Msg2.isInstanceOf[VehicleSpawnPad.PeriodicReminder]) + assert(probe1Msg2.asInstanceOf[VehicleSpawnPad.PeriodicReminder].reason == VehicleSpawnPad.Reminders.Blocked) + } + } +} + +class VehicleSpawnControl7aTest extends ActorTest() { + "VehicleSpawnControl" should { + "the vehicle is destroyed while attached to the rails; it 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]) + + 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]) + vehicle.Health = 0 //problem + + val probe3Msg4 = probe3.receiveOne(200 milliseconds) + assert(probe3Msg4.isInstanceOf[VehicleSpawnPad.DetachFromRails]) + val probe3Msg5 = probe3.receiveOne(200 milliseconds) + assert(probe3Msg5.isInstanceOf[VehicleSpawnPad.ResetSpawnPad]) + + probe1.receiveOne(200 milliseconds) //Mountable.MountMessage + val probe1Msg4 = probe1.receiveOne(10 seconds) + assert(probe1Msg4.isInstanceOf[VehicleSpawnPad.PeriodicReminder]) + assert(probe1Msg4.asInstanceOf[VehicleSpawnPad.PeriodicReminder].reason == VehicleSpawnPad.Reminders.Blocked) + } + } +} + +class VehicleSpawnControl7bTest 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(10 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/pslogin/src/main/scala/WorldSessionActor.scala b/pslogin/src/main/scala/WorldSessionActor.scala index 03c3a542..8df74569 100644 --- a/pslogin/src/main/scala/WorldSessionActor.scala +++ b/pslogin/src/main/scala/WorldSessionActor.scala @@ -1025,9 +1025,6 @@ class WorldSessionActor extends Actor with MDCContextAware { case VehicleSpawnPad.StartPlayerSeatedInVehicle(vehicle, pad) => val vehicle_guid = vehicle.GUID - if(pad.Railed) { - sendResponse(ObjectAttachMessage(pad.GUID, vehicle_guid, 3)) - } sendResponse(PlanetsideAttributeMessage(vehicle_guid, 22, 1L)) //mount points off? sendResponse(PlanetsideAttributeMessage(vehicle_guid, 21, player.GUID.guid)) //fte and ownership? @@ -1053,7 +1050,13 @@ class WorldSessionActor extends Actor with MDCContextAware { sendResponse(GenericObjectActionMessage(pad.GUID, 92)) //reset spawn pad sendResponse(ServerVehicleOverrideMsg.Auto(vehicle.Definition.AutoPilotSpeed2)) - case VehicleSpawnPad.PeriodicReminder(msg) => + 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 => diff --git a/pslogin/src/main/scala/services/vehicle/support/DeconstructionActor.scala b/pslogin/src/main/scala/services/vehicle/support/DeconstructionActor.scala index 97c3223a..9d7a3efa 100644 --- a/pslogin/src/main/scala/services/vehicle/support/DeconstructionActor.scala +++ b/pslogin/src/main/scala/services/vehicle/support/DeconstructionActor.scala @@ -81,11 +81,7 @@ 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 -======= - 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 taskResolver ! DeconstructionTask(vehicle, zone)