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.

This commit is contained in:
FateJH 2018-04-03 23:14:44 -04:00
parent dbc2ea8084
commit fde49773cd
22 changed files with 993 additions and 339 deletions

View file

@ -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`.<br>
* 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.<br>
* <br>
* 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:<br>
* 1. the vehicle is attached to a lifting platform that is designed to introduce the vehicle;<br>
* 2. the player is seated in the vehicle's driver seat (seat 0) and is thus declared the owner; <br>
* 3. various properties of the player, the vehicle, and the spawn pad itself are set `PlanetsideAttributesMessage`.<br>
* 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.<br>
* <br>
* 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)
}
}
}

View file

@ -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.

View file

@ -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
}

View file

@ -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.<br>
* <br>
* 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 _ => ;
}
}

View file

@ -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.<br>
* <br>
* 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 _ => ;
}
}

View file

@ -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.<br>
* <br>
* 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 _ => ;
}
}

View file

@ -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.<br>
* <br>
* 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.<br>
* __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 _ => ;
}
}

View file

@ -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.<br>
* <br>
* 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 _ => ;
}
}

View file

@ -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.<br>
* <br>
* 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 _ => ;
}
}

View file

@ -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`.<br>
@ -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.

View file

@ -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`.<br>
* <br>
* 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.<br>
* <br>
* 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.)<br>
* <br>
* 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)
}
}
}
}

View file

@ -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

View file

@ -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.<br>
* <br>
* 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.<br>
* <br>
* 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.)<br>
* <br>
* Speed samples follow (from AMS):<br>
* 1 -> 3<br>
* 2 -> 7<br>
* 3 -> 10<br>
* 10 -> 35<br>
* 15 -> 52<br>
* 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]
}

View file

@ -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
}
}

View file

@ -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)
}
}
}

View file

@ -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)
}
}
}