mirror of
https://github.com/2revoemag/PSF-BotServer.git
synced 2026-07-14 07:55:07 +00:00
Persistence (#337)
* constructed service actor to handle persistence of player on server beyond the reaches of WSA from one login to the next; created in PSLogin, interfaced with and updated in WSA * for what it's worth, players can be completely logged out of the world after 60s of inactivity, alive Infantry only right now; some code was removed from WSA to make it accessible to other classes but it's incomparable to the new code; broke, fixed, compromised on the code that handles loadouts, server login time, etc. * moved another common vehicle function into global space; persistence object for players in vehicles during log-out or relogging in a vehicle * support for relogging when dead/released/unfound; silenced implant slot setup during character select screen * tested and commented code for managing player avatar persistence * clarificaion of WSA postStop * test fixed * postStop change to account for WSA being cut short during initialization * clarification of SquadService logout * player died during setup; probably a relog * kill the doppelganger WSA; die when you are actually dead * created manifest to assist with vehicle gating; vehicle gating now accomodates the persistence model much better * fixed the test * fixed initial vehicle seat access permissions; moved a subscription to AvatarService to support persistence * bug fixes: safer GridInventory collision checks, plus specific exceptions; SessionRouter waits for the account intermediary before allowing itself to be started; WSA - match error colution, and MAX without arm now creates the arm it expects * adjusted insertion and removal code to make inventory management less prone to partoial insertions of removals; inventory integrity checking code writen, but no plans on implementing it yet
This commit is contained in:
parent
06ef3a08c5
commit
53ecee566a
29 changed files with 2436 additions and 1239 deletions
|
|
@ -53,6 +53,8 @@ class Avatar(private val char_id : Long, val name : String, val faction : Planet
|
||||||
*/
|
*/
|
||||||
private var lfs : Boolean = false
|
private var lfs : Boolean = false
|
||||||
|
|
||||||
|
private var vehicleOwned : Option[PlanetSideGUID] = None
|
||||||
|
|
||||||
def CharId : Long = char_id
|
def CharId : Long = char_id
|
||||||
|
|
||||||
def BEP : Long = bep
|
def BEP : Long = bep
|
||||||
|
|
@ -197,6 +199,15 @@ class Avatar(private val char_id : Long, val name : String, val faction : Planet
|
||||||
LFS
|
LFS
|
||||||
}
|
}
|
||||||
|
|
||||||
|
def VehicleOwned : Option[PlanetSideGUID] = vehicleOwned
|
||||||
|
|
||||||
|
def VehicleOwned_=(guid : PlanetSideGUID) : Option[PlanetSideGUID] = VehicleOwned_=(Some(guid))
|
||||||
|
|
||||||
|
def VehicleOwned_=(guid : Option[PlanetSideGUID]) : Option[PlanetSideGUID] = {
|
||||||
|
vehicleOwned = guid
|
||||||
|
VehicleOwned
|
||||||
|
}
|
||||||
|
|
||||||
def Definition : AvatarDefinition = GlobalDefinitions.avatar
|
def Definition : AvatarDefinition = GlobalDefinitions.avatar
|
||||||
|
|
||||||
/*
|
/*
|
||||||
|
|
|
||||||
|
|
@ -1,15 +1,17 @@
|
||||||
// Copyright (c) 2017 PSForever
|
// Copyright (c) 2017 PSForever
|
||||||
package net.psforever.objects
|
package net.psforever.objects
|
||||||
|
|
||||||
|
import akka.actor.ActorRef
|
||||||
|
import scala.concurrent.duration._
|
||||||
|
|
||||||
import net.psforever.objects.ce.{Deployable, DeployedItem}
|
import net.psforever.objects.ce.{Deployable, DeployedItem}
|
||||||
import net.psforever.objects.serverobject.PlanetSideServerObject
|
import net.psforever.objects.serverobject.PlanetSideServerObject
|
||||||
|
import net.psforever.objects.zones.Zone
|
||||||
import net.psforever.packet.game.{DeployableInfo, DeploymentAction}
|
import net.psforever.packet.game.{DeployableInfo, DeploymentAction}
|
||||||
import net.psforever.types.PlanetSideGUID
|
import net.psforever.types.PlanetSideGUID
|
||||||
import services.RemoverActor
|
import services.RemoverActor
|
||||||
import services.local.{LocalAction, LocalServiceMessage}
|
import services.local.{LocalAction, LocalServiceMessage}
|
||||||
|
|
||||||
import scala.concurrent.duration.FiniteDuration
|
|
||||||
|
|
||||||
object Deployables {
|
object Deployables {
|
||||||
object Make {
|
object Make {
|
||||||
def apply(item : DeployedItem.Value) : ()=>PlanetSideGameObject with Deployable = cemap(item)
|
def apply(item : DeployedItem.Value) : ()=>PlanetSideGameObject with Deployable = cemap(item)
|
||||||
|
|
@ -72,4 +74,32 @@ object Deployables {
|
||||||
zone.LocalEvents ! LocalServiceMessage.Deployables(RemoverActor.ClearSpecific(List(target), zone))
|
zone.LocalEvents ! LocalServiceMessage.Deployables(RemoverActor.ClearSpecific(List(target), zone))
|
||||||
zone.LocalEvents ! LocalServiceMessage.Deployables(RemoverActor.AddTask(target, zone, time))
|
zone.LocalEvents ! LocalServiceMessage.Deployables(RemoverActor.AddTask(target, zone, time))
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Collect all deployables previously owned by the player,
|
||||||
|
* dissociate the avatar's globally unique identifier to remove turnover ownership,
|
||||||
|
* and, on top of performing the above manipulations, dispose of any boomers discovered.
|
||||||
|
* (`BoomerTrigger` objects, the companions of the boomers, should be handled by an external implementation
|
||||||
|
* if they had not already been handled by the time this function is executed.)
|
||||||
|
* @return all previously-owned deployables after they have been processed;
|
||||||
|
* boomers are listed before all other deployable types
|
||||||
|
*/
|
||||||
|
def Disown(zone : Zone, avatar : Avatar, replyTo : ActorRef) : List[PlanetSideGameObject with Deployable] = {
|
||||||
|
val (boomers, deployables) =
|
||||||
|
avatar.Deployables.Clear()
|
||||||
|
.map(zone.GUID)
|
||||||
|
.collect { case Some(obj) => obj.asInstanceOf[PlanetSideGameObject with Deployable] }
|
||||||
|
.partition(_.isInstanceOf[BoomerDeployable])
|
||||||
|
//do not change the OwnerName field at this time
|
||||||
|
boomers.collect({ case obj : BoomerDeployable =>
|
||||||
|
zone.LocalEvents.tell(LocalServiceMessage.Deployables(RemoverActor.AddTask(obj, zone, Some(0 seconds))), replyTo) //near-instant
|
||||||
|
obj.Owner = None
|
||||||
|
obj.Trigger = None
|
||||||
|
})
|
||||||
|
deployables.foreach(obj => {
|
||||||
|
zone.LocalEvents.tell(LocalServiceMessage.Deployables(RemoverActor.AddTask(obj, zone)), replyTo) //normal decay
|
||||||
|
obj.Owner = None
|
||||||
|
})
|
||||||
|
boomers ++ deployables
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -51,7 +51,6 @@ class Player(private val core : Avatar) extends PlanetSideServerObject
|
||||||
private var cloaked : Boolean = false
|
private var cloaked : Boolean = false
|
||||||
|
|
||||||
private var vehicleSeated : Option[PlanetSideGUID] = None
|
private var vehicleSeated : Option[PlanetSideGUID] = None
|
||||||
private var vehicleOwned : Option[PlanetSideGUID] = None
|
|
||||||
|
|
||||||
Continent = "home2" //the zone id
|
Continent = "home2" //the zone id
|
||||||
|
|
||||||
|
|
@ -605,14 +604,11 @@ class Player(private val core : Avatar) extends PlanetSideServerObject
|
||||||
VehicleSeated
|
VehicleSeated
|
||||||
}
|
}
|
||||||
|
|
||||||
def VehicleOwned : Option[PlanetSideGUID] = vehicleOwned
|
def VehicleOwned : Option[PlanetSideGUID] = core.VehicleOwned
|
||||||
|
|
||||||
def VehicleOwned_=(guid : PlanetSideGUID) : Option[PlanetSideGUID] = VehicleOwned_=(Some(guid))
|
def VehicleOwned_=(guid : PlanetSideGUID) : Option[PlanetSideGUID] = core.VehicleOwned_=(Some(guid))
|
||||||
|
|
||||||
def VehicleOwned_=(guid : Option[PlanetSideGUID]) : Option[PlanetSideGUID] = {
|
def VehicleOwned_=(guid : Option[PlanetSideGUID]) : Option[PlanetSideGUID] = core.VehicleOwned_=(guid)
|
||||||
vehicleOwned = guid
|
|
||||||
VehicleOwned
|
|
||||||
}
|
|
||||||
|
|
||||||
def DamageModel = exosuit.asInstanceOf[DamageResistanceModel]
|
def DamageModel = exosuit.asInstanceOf[DamageResistanceModel]
|
||||||
|
|
||||||
|
|
@ -658,7 +654,6 @@ object Player {
|
||||||
def Respawn(player : Player) : Player = {
|
def Respawn(player : Player) : Player = {
|
||||||
if(player.Release) {
|
if(player.Release) {
|
||||||
val obj = new Player(player.core)
|
val obj = new Player(player.core)
|
||||||
obj.VehicleOwned = player.VehicleOwned
|
|
||||||
obj.Continent = player.Continent
|
obj.Continent = player.Continent
|
||||||
obj
|
obj
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -102,6 +102,9 @@ class Vehicle(private val vehicleDef : VehicleDefinition) extends AmenityOwner
|
||||||
*/
|
*/
|
||||||
private var mountedIn : Option[PlanetSideGUID] = None
|
private var mountedIn : Option[PlanetSideGUID] = None
|
||||||
|
|
||||||
|
private var vehicleGatingManifest : Option[VehicleManifest] = None
|
||||||
|
private var previousVehicleGatingManifest : Option[VehicleManifest] = None
|
||||||
|
|
||||||
//init
|
//init
|
||||||
LoadDefinition()
|
LoadDefinition()
|
||||||
|
|
||||||
|
|
@ -523,6 +526,23 @@ class Vehicle(private val vehicleDef : VehicleDefinition) extends AmenityOwner
|
||||||
*/
|
*/
|
||||||
def TrunkLockState : VehicleLockState.Value = groupPermissions(3)
|
def TrunkLockState : VehicleLockState.Value = groupPermissions(3)
|
||||||
|
|
||||||
|
def PrepareGatingManifest() : VehicleManifest = {
|
||||||
|
val manifest = VehicleManifest(this)
|
||||||
|
seats.collect { case (index, seat) if index > 0 => seat.Occupant = None }
|
||||||
|
vehicleGatingManifest = Some(manifest)
|
||||||
|
previousVehicleGatingManifest = None
|
||||||
|
manifest
|
||||||
|
}
|
||||||
|
|
||||||
|
def PublishGatingManifest() : Option[VehicleManifest] = {
|
||||||
|
val out = vehicleGatingManifest
|
||||||
|
previousVehicleGatingManifest = vehicleGatingManifest
|
||||||
|
vehicleGatingManifest = None
|
||||||
|
out
|
||||||
|
}
|
||||||
|
|
||||||
|
def PreviousGatingManifest() : Option[VehicleManifest] = previousVehicleGatingManifest
|
||||||
|
|
||||||
def DamageModel = Definition.asInstanceOf[DamageResistanceModel]
|
def DamageModel = Definition.asInstanceOf[DamageResistanceModel]
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
|
|
||||||
154
common/src/main/scala/net/psforever/objects/Vehicles.scala
Normal file
154
common/src/main/scala/net/psforever/objects/Vehicles.scala
Normal file
|
|
@ -0,0 +1,154 @@
|
||||||
|
// Copyright (c) 2020 PSForever
|
||||||
|
package net.psforever.objects
|
||||||
|
|
||||||
|
import net.psforever.objects.vehicles.VehicleLockState
|
||||||
|
import net.psforever.objects.zones.Zone
|
||||||
|
import net.psforever.types.PlanetSideGUID
|
||||||
|
import services.avatar.{AvatarAction, AvatarServiceMessage}
|
||||||
|
import services.vehicle.{VehicleAction, VehicleServiceMessage}
|
||||||
|
|
||||||
|
object Vehicles {
|
||||||
|
/**
|
||||||
|
* na
|
||||||
|
* @param vehicle na
|
||||||
|
* @param tplayer na
|
||||||
|
* @return na
|
||||||
|
*/
|
||||||
|
def Own(vehicle : Vehicle, tplayer : Player) : Option[Vehicle] = Own(vehicle, Some(tplayer))
|
||||||
|
|
||||||
|
/**
|
||||||
|
* na
|
||||||
|
* @param vehicle na
|
||||||
|
* @param playerOpt na
|
||||||
|
* @return na
|
||||||
|
*/
|
||||||
|
def Own(vehicle : Vehicle, playerOpt : Option[Player]) : Option[Vehicle] = {
|
||||||
|
playerOpt match {
|
||||||
|
case Some(tplayer) =>
|
||||||
|
tplayer.VehicleOwned = vehicle.GUID
|
||||||
|
vehicle.AssignOwnership(playerOpt)
|
||||||
|
vehicle.Zone.VehicleEvents ! VehicleServiceMessage(vehicle.Zone.Id, VehicleAction.Ownership(tplayer.GUID, vehicle.GUID))
|
||||||
|
Vehicles.ReloadAccessPermissions(vehicle, tplayer.Name)
|
||||||
|
Some(vehicle)
|
||||||
|
case None =>
|
||||||
|
None
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Disassociate a player from a vehicle that he owns.
|
||||||
|
* The vehicle must exist in the game world on the specified continent.
|
||||||
|
* This is similar but unrelated to the natural exchange of ownership when someone else sits in the vehicle's driver seat.
|
||||||
|
* This is the player side of vehicle ownership removal.
|
||||||
|
* @param player the player
|
||||||
|
*/
|
||||||
|
def Disown(player : Player, zone : Zone) : Option[Vehicle] = Disown(player, Some(zone))
|
||||||
|
/**
|
||||||
|
* Disassociate a player from a vehicle that he owns.
|
||||||
|
* The vehicle must exist in the game world on the specified continent.
|
||||||
|
* This is similar but unrelated to the natural exchange of ownership when someone else sits in the vehicle's driver seat.
|
||||||
|
* This is the player side of vehicle ownership removal.
|
||||||
|
* @param player the player
|
||||||
|
*/
|
||||||
|
def Disown(player : Player, zoneOpt : Option[Zone]) : Option[Vehicle] = {
|
||||||
|
player.VehicleOwned match {
|
||||||
|
case Some(vehicle_guid) =>
|
||||||
|
player.VehicleOwned = None
|
||||||
|
zoneOpt.getOrElse(player.Zone).GUID(vehicle_guid) match {
|
||||||
|
case Some(vehicle : Vehicle) =>
|
||||||
|
Disown(player, vehicle)
|
||||||
|
case _ =>
|
||||||
|
None
|
||||||
|
}
|
||||||
|
case None =>
|
||||||
|
None
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Disassociate a player from a vehicle that he owns without associating a different player as the owner.
|
||||||
|
* Set the vehicle's driver seat permissions and passenger and gunner seat permissions to "allow empire,"
|
||||||
|
* then reload them for all clients.
|
||||||
|
* This is the vehicle side of vehicle ownership removal.
|
||||||
|
* @param player the player
|
||||||
|
*/
|
||||||
|
def Disown(player : Player, vehicle : Vehicle) : Option[Vehicle] = {
|
||||||
|
val pguid = player.GUID
|
||||||
|
if(vehicle.Owner.contains(pguid)) {
|
||||||
|
vehicle.AssignOwnership(None)
|
||||||
|
val factionChannel = s"${vehicle.Faction}"
|
||||||
|
vehicle.Zone.VehicleEvents ! VehicleServiceMessage(factionChannel, VehicleAction.Ownership(pguid, PlanetSideGUID(0)))
|
||||||
|
val vguid = vehicle.GUID
|
||||||
|
val empire = VehicleLockState.Empire.id
|
||||||
|
(0 to 2).foreach(group => {
|
||||||
|
vehicle.PermissionGroup(group, empire)
|
||||||
|
vehicle.Zone.VehicleEvents ! VehicleServiceMessage(factionChannel, VehicleAction.SeatPermissions(pguid, vguid, group, empire))
|
||||||
|
})
|
||||||
|
ReloadAccessPermissions(vehicle, player.Name)
|
||||||
|
Some(vehicle)
|
||||||
|
}
|
||||||
|
else {
|
||||||
|
None
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Iterate over vehicle permissions and turn them into `PlanetsideAttributeMessage` packets.<br>
|
||||||
|
* <br>
|
||||||
|
* For the purposes of ensuring that other players are always aware of the proper permission state of the trunk and seats,
|
||||||
|
* packets are intentionally dispatched to the current client to update the states.
|
||||||
|
* Perform this action just after any instance where the client would initially gain awareness of the vehicle.
|
||||||
|
* The most important examples include either the player or the vehicle itself spawning in for the first time.
|
||||||
|
* @param vehicle the `Vehicle`
|
||||||
|
*/
|
||||||
|
def ReloadAccessPermissions(vehicle : Vehicle, toChannel : String) : Unit = {
|
||||||
|
val vehicle_guid = vehicle.GUID
|
||||||
|
(0 to 3).foreach(group => {
|
||||||
|
vehicle.Zone.AvatarEvents ! AvatarServiceMessage(
|
||||||
|
toChannel,
|
||||||
|
AvatarAction.PlanetsideAttributeToAll(vehicle_guid, group + 10, vehicle.PermissionGroup(group).get.id)
|
||||||
|
)
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* A recursive test that explores all the seats of a target vehicle
|
||||||
|
* and all the seats of any discovered cargo vehicles
|
||||||
|
* and then the same criteria in those cargo vehicles
|
||||||
|
* to determine if any of their combined passenger roster remains in a given zone.<br>
|
||||||
|
* <br>
|
||||||
|
* The original zone is expected to be defined in the internal vehicle gating manifest file
|
||||||
|
* and, if this file does not exist, we fail the testing process.
|
||||||
|
* The target zone is the one wherever the vehicle currently is located (`vehicle.Zone`).
|
||||||
|
* All participant passengers, also defined in the manifest, are expected to be in the target zone at the same time.
|
||||||
|
* This test excludes (rejects) same-zone transitions
|
||||||
|
* though it would automatically pass the test under those conditions.<br>
|
||||||
|
* <br>
|
||||||
|
* While it should be possible to recursively explore up a parent-child relationship -
|
||||||
|
* testing the ferrying vehicle to which the current tested vehicle is considered a cargo vehicle -
|
||||||
|
* the relationship expressed is one of globally unique refertences and not one of object references -
|
||||||
|
* that suggested super-ferrying vehicle may not exist in the zone unless special considerations are imposed.
|
||||||
|
* For the purpose of these special considerations,
|
||||||
|
* implemented by enforcing a strictly downwards order of vehicular zone transportation,
|
||||||
|
* where drivers move vehicles and call passengers and immediate cargo vehicle drivers,
|
||||||
|
* it becomes unnecessary to test any vehicle that might be ferrying the target vehicle.
|
||||||
|
* @see `ZoneAware`
|
||||||
|
* @param vehicle the target vehicle being moved around between zones
|
||||||
|
* @return `true`, if all passengers of the vehicle, and its cargo vehicles, etc., have reported being in the same zone;
|
||||||
|
* `false`, if no manifest entry exists, or if the vehicle is moving to the same zone
|
||||||
|
*/
|
||||||
|
def AllGatedOccupantsInSameZone(vehicle : Vehicle) : Boolean = {
|
||||||
|
val vzone = vehicle.Zone
|
||||||
|
vehicle.PreviousGatingManifest() match {
|
||||||
|
case Some(manifest) if vzone != manifest.origin =>
|
||||||
|
val manifestPassengers = manifest.passengers.collect { case (name, _) => name } :+ manifest.driverName
|
||||||
|
val manifestPassengerResults = manifestPassengers.map { name => vzone.Players.exists(_.name.equals(name)) }
|
||||||
|
manifestPassengerResults.forall(_ == true) &&
|
||||||
|
vehicle.CargoHolds.values
|
||||||
|
.collect { case hold if hold.isOccupied => AllGatedOccupantsInSameZone(hold.Occupant.get) }
|
||||||
|
.forall(_ == true)
|
||||||
|
case _ =>
|
||||||
|
false
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
@ -22,6 +22,9 @@ class PlayerControl(player : Player) extends Actor
|
||||||
with JammableBehavior {
|
with JammableBehavior {
|
||||||
def JammableObject = player
|
def JammableObject = player
|
||||||
|
|
||||||
|
private [this] val log = org.log4s.getLogger(player.Name)
|
||||||
|
private [this] val damageLog = org.log4s.getLogger("DamageResolution")
|
||||||
|
|
||||||
def receive : Receive = jammableBehavior.orElse {
|
def receive : Receive = jammableBehavior.orElse {
|
||||||
case Player.Die() =>
|
case Player.Die() =>
|
||||||
PlayerControl.HandleDestructionAwareness(player, player.GUID, None)
|
PlayerControl.HandleDestructionAwareness(player, player.GUID, None)
|
||||||
|
|
@ -40,8 +43,7 @@ class PlayerControl(player : Player) extends Actor
|
||||||
val damageToCapacitor = originalCapacitor - capacitor
|
val damageToCapacitor = originalCapacitor - capacitor
|
||||||
PlayerControl.HandleDamageResolution(player, cause, damageToHealth, damageToArmor, damageToCapacitor)
|
PlayerControl.HandleDamageResolution(player, cause, damageToHealth, damageToArmor, damageToCapacitor)
|
||||||
if(damageToHealth != 0 || damageToArmor != 0 || damageToCapacitor != 0) {
|
if(damageToHealth != 0 || damageToArmor != 0 || damageToCapacitor != 0) {
|
||||||
org.log4s.getLogger("DamageResolution")
|
damageLog.info(s"${player.Name}-infantry: BEFORE=$originalHealth/$originalArmor/$originalCapacitor, AFTER=$health/$armor/$capacitor, CHANGE=$damageToHealth/$damageToArmor/$damageToCapacitor")
|
||||||
.info(s"${player.Name}-infantry: BEFORE=$originalHealth/$originalArmor/$originalCapacitor, AFTER=$health/$armor/$capacitor, CHANGE=$damageToHealth/$damageToArmor/$damageToCapacitor")
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
case _ => ;
|
case _ => ;
|
||||||
|
|
|
||||||
|
|
@ -183,7 +183,7 @@ class GridInventory extends Container {
|
||||||
val starth : Int = starty + h - 1
|
val starth : Int = starty + h - 1
|
||||||
if(actualSlot < 0 || actualSlot >= grid.length || startw >= width || starth >= height) {
|
if(actualSlot < 0 || actualSlot >= grid.length || startw >= width || starth >= height) {
|
||||||
val bounds : String = if(startx < 0) { "left" } else if(startw >= width) { "right" } else { "bottom" }
|
val bounds : String = if(startx < 0) { "left" } else if(startw >= width) { "right" } else { "bottom" }
|
||||||
Failure(new IndexOutOfBoundsException(s"requested region escapes the $bounds edge of the grid inventory - $startx, $starty; $w x $h"))
|
Failure(new IndexOutOfBoundsException(s"requested region escapes the $bounds edge of the grid inventory - $startx + $w, $starty + $h"))
|
||||||
}
|
}
|
||||||
else {
|
else {
|
||||||
val collisions : mutable.Set[InventoryItem] = mutable.Set[InventoryItem]()
|
val collisions : mutable.Set[InventoryItem] = mutable.Set[InventoryItem]()
|
||||||
|
|
@ -220,22 +220,31 @@ class GridInventory extends Container {
|
||||||
val starty : Int = actualSlot / width
|
val starty : Int = actualSlot / width
|
||||||
val startw : Int = startx + w - 1
|
val startw : Int = startx + w - 1
|
||||||
val bounds : String = if(startx < 0) { "left" } else if(startw >= width) { "right" } else { "bottom" }
|
val bounds : String = if(startx < 0) { "left" } else if(startw >= width) { "right" } else { "bottom" }
|
||||||
Failure(new IndexOutOfBoundsException(s"requested region escapes the $bounds edge of the grid inventory - $startx, $starty; $w x $h"))
|
Failure(new IndexOutOfBoundsException(s"requested region escapes the $bounds edge of the grid inventory - $startx + $w, $starty + $h"))
|
||||||
}
|
}
|
||||||
else {
|
else {
|
||||||
val collisions : mutable.Set[InventoryItem] = mutable.Set[InventoryItem]()
|
val collisions : mutable.Set[InventoryItem] = mutable.Set[InventoryItem]()
|
||||||
var curr = actualSlot
|
var curr = actualSlot
|
||||||
val fixedItems = items.toMap
|
val fixedItems = items.toMap
|
||||||
val fixedGrid = grid.toList
|
val fixedGrid = grid.toList
|
||||||
for(_ <- 0 until h) {
|
try {
|
||||||
for(col <- 0 until w) {
|
for(_ <- 0 until h) {
|
||||||
if(fixedGrid(curr + col) > -1) {
|
for(col <- 0 until w) {
|
||||||
collisions += fixedItems(fixedGrid(curr + col))
|
val itemIndex = fixedGrid(curr + col)
|
||||||
|
if(itemIndex > -1) {
|
||||||
|
collisions += fixedItems(itemIndex)
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
curr += width
|
||||||
}
|
}
|
||||||
curr += width
|
Success(collisions.toList)
|
||||||
|
}
|
||||||
|
catch {
|
||||||
|
case e : NoSuchElementException =>
|
||||||
|
Failure(InventoryDisarrayException(s"inventory contained old item data", e))
|
||||||
|
case e : Exception =>
|
||||||
|
Failure(e)
|
||||||
}
|
}
|
||||||
Success(collisions.toList)
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -280,6 +289,7 @@ class GridInventory extends Container {
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Define a region of inventory grid cells and set them to a given value.
|
* Define a region of inventory grid cells and set them to a given value.
|
||||||
|
* @see `SetCellsNoOffset`
|
||||||
* @param start the initial inventory index
|
* @param start the initial inventory index
|
||||||
* @param w the width of the region
|
* @param w the width of the region
|
||||||
* @param h the height of the region
|
* @param h the height of the region
|
||||||
|
|
@ -287,77 +297,136 @@ class GridInventory extends Container {
|
||||||
* defaults to -1 (which is "nothing")
|
* defaults to -1 (which is "nothing")
|
||||||
*/
|
*/
|
||||||
def SetCells(start : Int, w : Int, h : Int, value : Int = -1) : Unit = {
|
def SetCells(start : Int, w : Int, h : Int, value : Int = -1) : Unit = {
|
||||||
SetCellsOffset(math.max(start - offset, 0), w, h, value)
|
grid = SetCellsNoOffset(start - offset, w, h, value)
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Define a region of inventory grid cells and set them to a given value.
|
* Define a region of inventory grid cells and set them to a given value.
|
||||||
|
* Perform basic boundary checking for the current inventory dimensions.
|
||||||
|
* @see `SetCellsOnlyNoOffset`
|
||||||
* @param start the initial inventory index, without the inventory offset (required)
|
* @param start the initial inventory index, without the inventory offset (required)
|
||||||
* @param w the width of the region
|
* @param w the width of the region
|
||||||
* @param h the height of the region
|
* @param h the height of the region
|
||||||
* @param value the value to set all the cells in the defined region;
|
* @param value the value to set all the cells in the defined region;
|
||||||
* defaults to -1 (which is "nothing")
|
* defaults to -1 (which is "nothing")
|
||||||
|
* @return a copy of the inventory as a grid, with the anticipated modifications
|
||||||
* @throws IndexOutOfBoundsException if the region extends outside of the grid boundaries
|
* @throws IndexOutOfBoundsException if the region extends outside of the grid boundaries
|
||||||
*/
|
*/
|
||||||
def SetCellsOffset(start : Int, w : Int, h : Int, value : Int = -1) : Unit = {
|
def SetCellsNoOffset(start : Int, w : Int, h : Int, value : Int = -1) : Array[Int] = {
|
||||||
if(start < 0 || start > grid.length || (start % width) + w - 1 > width || (start / width) + h- 1 > height) {
|
if(start < 0 || start > grid.length || (start % width) + w - 1 > width || (start / width) + h- 1 > height) {
|
||||||
val startx : Int = start % width
|
val startx : Int = start % width
|
||||||
val starty : Int = start / width
|
val starty : Int = start / width
|
||||||
val startw : Int = startx + w - 1
|
val startw : Int = startx + w - 1
|
||||||
val bounds : String = if(startx < 0) { "left" } else if(startw >= width) { "right" } else { "bottom" }
|
val bounds : String = if(startx < 0) { "left" } else if(startw >= width) { "right" } else { "bottom" }
|
||||||
throw new IndexOutOfBoundsException(s"requested region escapes the $bounds of the grid inventory - $startx, $starty; $w x $h")
|
throw new IndexOutOfBoundsException(s"requested region escapes the $bounds of the grid inventory - $startx + $w, $starty + $h")
|
||||||
}
|
}
|
||||||
else {
|
SetCellsOnlyNoOffset(start, w, h, value)
|
||||||
var curr = start
|
}
|
||||||
for(_ <- 0 until h) {
|
|
||||||
for(col <- 0 until w) {
|
/**
|
||||||
grid(curr + col) = value
|
* Define a region of inventory grid cells and set them to a given value.
|
||||||
}
|
* Ignore inventory boundary checking and just set the appropriate cell values.
|
||||||
curr += width
|
* Do not use this unless boundary checking was already performed.
|
||||||
|
* @param start the initial inventory index, without the inventory offset (required)
|
||||||
|
* @param w the width of the region
|
||||||
|
* @param h the height of the region
|
||||||
|
* @param value the value to set all the cells in the defined region;
|
||||||
|
* defaults to -1 (which is "nothing")
|
||||||
|
* @return a copy of the inventory as a grid, with the anticipated modifications
|
||||||
|
*/
|
||||||
|
private def SetCellsOnlyNoOffset(start : Int, w : Int, h : Int, value : Int = -1) : Array[Int] = {
|
||||||
|
val out : Array[Int] = grid.clone()
|
||||||
|
var curr = start
|
||||||
|
for(_ <- 0 until h) {
|
||||||
|
for(col <- 0 until w) {
|
||||||
|
out(curr + col) = value
|
||||||
}
|
}
|
||||||
|
curr += width
|
||||||
}
|
}
|
||||||
|
out
|
||||||
}
|
}
|
||||||
|
|
||||||
def Insert(start : Int, obj : Equipment) : Boolean = {
|
def Insert(start : Int, obj : Equipment) : Boolean = {
|
||||||
val key : Int = entryIndex.getAndIncrement()
|
val key : Int = entryIndex.getAndIncrement()
|
||||||
items.get(key) match {
|
items.get(key) match {
|
||||||
case None => //no redundant insertions or other collisions
|
case None => //no redundant insertions
|
||||||
Insertion_CheckCollisions(start, obj, key)
|
Insertion_CheckCollisions(start, obj, key)
|
||||||
case _ =>
|
case _ =>
|
||||||
false
|
false
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Perform a collisions check and, if it passes, perform the insertion.
|
||||||
|
* @param start the starting slot
|
||||||
|
* @param obj the `Equipment` item to be inserted
|
||||||
|
* @param key the internal numeric identifier for this item
|
||||||
|
* @return the success or the failure of the insertion process
|
||||||
|
*/
|
||||||
def Insertion_CheckCollisions(start : Int, obj : Equipment, key : Int) : Boolean = {
|
def Insertion_CheckCollisions(start : Int, obj : Equipment, key : Int) : Boolean = {
|
||||||
CheckCollisions(start, obj) match {
|
CheckCollisions(start, obj) match {
|
||||||
case Success(Nil) =>
|
case Success(Nil) =>
|
||||||
InsertQuickly(start, obj, key)
|
val tile = obj.Tile
|
||||||
|
grid = SetCellsOnlyNoOffset(start - offset, tile.Width, tile.Height, key)
|
||||||
|
val card = InventoryItem(obj, start)
|
||||||
|
items += key -> card
|
||||||
|
true
|
||||||
case _ =>
|
case _ =>
|
||||||
false
|
false
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Just insert an item into the inventory without checking for item collisions.
|
||||||
|
* @param start the starting slot
|
||||||
|
* @param obj the `Equipment` item to be inserted
|
||||||
|
* @return whether the insertion succeeded
|
||||||
|
*/
|
||||||
def InsertQuickly(start : Int, obj : Equipment) : Boolean = InsertQuickly(start, obj, entryIndex.getAndIncrement())
|
def InsertQuickly(start : Int, obj : Equipment) : Boolean = InsertQuickly(start, obj, entryIndex.getAndIncrement())
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Just insert an item into the inventory without checking for item collisions.
|
||||||
|
* Inventory boundary checking still occurs but the `Exception` is caught and discarded.
|
||||||
|
* Discarding the `Exception` is normally bad practice; the result is the only thing we care about here.
|
||||||
|
* @param start the starting slot
|
||||||
|
* @param obj the `Equipment` item to be inserted
|
||||||
|
* @param key the internal numeric identifier for this item
|
||||||
|
* @return whether the insertion succeeded
|
||||||
|
*/
|
||||||
private def InsertQuickly(start : Int, obj : Equipment, key : Int) : Boolean = {
|
private def InsertQuickly(start : Int, obj : Equipment, key : Int) : Boolean = {
|
||||||
val card = InventoryItem(obj, start)
|
try {
|
||||||
items += key -> card
|
val tile = obj.Tile
|
||||||
val tile = obj.Tile
|
val updated = SetCellsNoOffset(start - offset, tile.Width, tile.Height, key)
|
||||||
SetCells(start, tile.Width, tile.Height, key)
|
val card = InventoryItem(obj, start)
|
||||||
true
|
items += key -> card
|
||||||
|
grid = updated
|
||||||
|
true
|
||||||
|
}
|
||||||
|
catch {
|
||||||
|
case _ : Exception =>
|
||||||
|
false
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
def +=(kv : (Int, Equipment)) : Boolean = Insert(kv._1, kv._2)
|
def +=(kv : (Int, Equipment)) : Boolean = Insert(kv._1, kv._2)
|
||||||
|
|
||||||
def Remove(index : Int) : Boolean = {
|
def Remove(index : Int) : Boolean = {
|
||||||
val key = grid(index - Offset)
|
val keyVal = index - offset
|
||||||
items.remove(key) match {
|
if(keyVal > -1 && keyVal < grid.length) {
|
||||||
case Some(item) =>
|
val key = grid(index - offset)
|
||||||
val tile = item.obj.Tile
|
items.get(key) match {
|
||||||
SetCells(item.start, tile.Width, tile.Height)
|
case Some(item) =>
|
||||||
true
|
val tile = item.obj.Tile
|
||||||
case None =>
|
val updated = SetCellsNoOffset(item.start - offset, tile.Width, tile.Height)
|
||||||
false
|
items.remove(key)
|
||||||
|
grid = updated
|
||||||
|
true
|
||||||
|
case None =>
|
||||||
|
false
|
||||||
|
}
|
||||||
|
}
|
||||||
|
else {
|
||||||
|
false
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -365,10 +434,12 @@ class GridInventory extends Container {
|
||||||
|
|
||||||
def Remove(guid : PlanetSideGUID) : Boolean = {
|
def Remove(guid : PlanetSideGUID) : Boolean = {
|
||||||
recursiveFindIdentifiedObject(items.keys.iterator, guid) match {
|
recursiveFindIdentifiedObject(items.keys.iterator, guid) match {
|
||||||
case Some(index) =>
|
case Some(key) =>
|
||||||
val item = items.remove(index).get
|
val item = items(key)
|
||||||
val tile = item.obj.Tile
|
val tile = item.obj.Tile
|
||||||
SetCells(item.start, tile.Width, tile.Height)
|
val updated = SetCellsNoOffset(item.start - offset, tile.Width, tile.Height)
|
||||||
|
items.remove(key)
|
||||||
|
grid = updated
|
||||||
true
|
true
|
||||||
case None =>
|
case None =>
|
||||||
false
|
false
|
||||||
|
|
@ -406,6 +477,99 @@ class GridInventory extends Container {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Align the "inventory as a grid" with the "inventory as a list."
|
||||||
|
* The grid is a faux-two-dimensional map of object identifiers that should point to items in the list.
|
||||||
|
* (Not the same as the global unique identifier number.)
|
||||||
|
* The objects in the list are considered actually being in the inventory.
|
||||||
|
* Only the references to those objects in grid-space can be considered out of alignment
|
||||||
|
* by not pointing to objects in the list.
|
||||||
|
* The inventory as a grid can be repaired but only a higher authority can perform inventory synchronization.
|
||||||
|
* @see `InventoryDisarrayException`
|
||||||
|
* @return the number of stale object references found and corrected
|
||||||
|
*/
|
||||||
|
def ElementsOnGridMatchList() : Int = {
|
||||||
|
var misses : Int = 0
|
||||||
|
grid = grid.map {
|
||||||
|
case n if items.get(n).nonEmpty => n
|
||||||
|
case -1 => -1
|
||||||
|
case _ =>
|
||||||
|
misses += 1
|
||||||
|
-1
|
||||||
|
}
|
||||||
|
misses
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Check whether any items in the "inventory as a list" datastructure would overlap in the "inventory as a grid".
|
||||||
|
* Most likely, if an overlap is discovered,
|
||||||
|
* the grid-space is already compromised by having lost a section of some item's `Tile`.
|
||||||
|
* The inventory system actually lacks mechanics to properly resolve any discovered issues.
|
||||||
|
* For that reason, it will return a list of overlap issues that need to be resolved by a higher authority.
|
||||||
|
* @see `InventoryDisarrayException`
|
||||||
|
* @see `recursiveRelatedListCollisions`
|
||||||
|
* @return a list of item overlap collision combinations
|
||||||
|
*/
|
||||||
|
def ElementsInListCollideInGrid() : List[List[InventoryItem]] = {
|
||||||
|
val testGrid : mutable.Map[Int, List[Int]] = mutable.Map[Int, List[Int]]()
|
||||||
|
//on average this will run the same number of times as capacity
|
||||||
|
items.foreach {
|
||||||
|
case (itemId, item) =>
|
||||||
|
var start = item.start
|
||||||
|
val wide = item.obj.Tile.Width
|
||||||
|
val high = item.obj.Tile.Height
|
||||||
|
//allocate all of the slots that comprise this item's tile
|
||||||
|
(0 until high).foreach { _ =>
|
||||||
|
(0 until wide).foreach { w =>
|
||||||
|
val slot = start + w
|
||||||
|
testGrid(slot) = testGrid.getOrElse(slot, Nil) :+ itemId
|
||||||
|
}
|
||||||
|
start += width
|
||||||
|
}
|
||||||
|
}
|
||||||
|
//lists with multiple entries represent a group of items that collide
|
||||||
|
//perform a specific distinct on the list of lists of numeric id overlaps
|
||||||
|
//THEN map each list of list's item id to an item
|
||||||
|
val out = testGrid.collect {
|
||||||
|
case (_, list) if list.size > 1 => list.sortWith(_ < _)
|
||||||
|
}
|
||||||
|
recursiveRelatedListCollisions(out.iterator, out.toList).map { list => list.map { items } }
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Filter out element overlap combinations until only unique overlap combinations remain.
|
||||||
|
* The filtration operation not only removes exact duplicates, leaving but one of an entry's kind,
|
||||||
|
* but also removes that very entry if another element contains the entry as a subset of itself.
|
||||||
|
* For example:
|
||||||
|
* if A overlaps B and B overlaps C, but A doesn't overlap C, the output will be A-B and B-C;
|
||||||
|
* if A and C do overlap, however, the output will be A-B-C, eliminating both A-B and B-C in the process.
|
||||||
|
* @see `ElementsInListCollideInGrid`
|
||||||
|
* @see `SeqLike.containsSlice`
|
||||||
|
* @see `tailrec`
|
||||||
|
* @param original an iterator of the original list of overlapping elements
|
||||||
|
* @param updated an list of overlapping elements not filtered out of the original list
|
||||||
|
* @return the final list of unique overlapping element combinations
|
||||||
|
*/
|
||||||
|
@tailrec private def recursiveRelatedListCollisions(original : Iterator[List[Int]], updated : List[List[Int]]) : List[List[Int]] = {
|
||||||
|
if(original.hasNext) {
|
||||||
|
val target = original.next
|
||||||
|
val filtered = updated.filterNot(item => item.equals(target))
|
||||||
|
val newupdated = if(filtered.size == updated.size) {
|
||||||
|
updated //the lists are the same size, nothing was filtered
|
||||||
|
}
|
||||||
|
else if(updated.exists(test => test.containsSlice(target) && !test.equals(target))) {
|
||||||
|
filtered //some element that is not the target element contains the target element as a subset
|
||||||
|
}
|
||||||
|
else {
|
||||||
|
filtered :+ target //restore one entry for the target element
|
||||||
|
}
|
||||||
|
recursiveRelatedListCollisions(original, newupdated)
|
||||||
|
}
|
||||||
|
else {
|
||||||
|
updated
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Clear the inventory by removing all of its items.
|
* Clear the inventory by removing all of its items.
|
||||||
* @return a `List` of the previous items in the inventory as their `InventoryItemData` tiles
|
* @return a `List` of the previous items in the inventory as their `InventoryItemData` tiles
|
||||||
|
|
@ -413,7 +577,8 @@ class GridInventory extends Container {
|
||||||
def Clear() : List[InventoryItem] = {
|
def Clear() : List[InventoryItem] = {
|
||||||
val list = items.values.toList
|
val list = items.values.toList
|
||||||
items.clear
|
items.clear
|
||||||
SetCellsOffset(0, width, height)
|
//entryIndex.set(0)
|
||||||
|
grid = SetCellsOnlyNoOffset(0, width, height)
|
||||||
list
|
list
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -0,0 +1,25 @@
|
||||||
|
// Copyright (c) 2020 PSForever
|
||||||
|
package net.psforever.objects.inventory
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Some data in the grid portion of a `GridInventory`
|
||||||
|
* does not match against data that is expected to be found in the "list" portion of `GridInventory`.
|
||||||
|
* While merely eliminating the old data is possible,
|
||||||
|
* the discovery of this errant data could be hiding significantly greater issues,
|
||||||
|
* and these greater issues must be explored at a higher level of governance.
|
||||||
|
* @param message the explanation of why the exception was thrown
|
||||||
|
* @param cause any prior `Exception` that was thrown then wrapped in this one
|
||||||
|
*/
|
||||||
|
final case class InventoryDisarrayException(private val message: String = "", private val cause: Throwable)
|
||||||
|
extends Exception(message, cause)
|
||||||
|
|
||||||
|
object InventoryDisarrayException {
|
||||||
|
/**
|
||||||
|
* Overloaded constructor that constructs the `Exception` without nesting any prior `Exceptions`.
|
||||||
|
* Just the custom error message is included.
|
||||||
|
* @param message the explanation of why the exception was thrown
|
||||||
|
* @return an `InventoryDisarrayException` object
|
||||||
|
*/
|
||||||
|
def apply(message : String) : InventoryDisarrayException =
|
||||||
|
InventoryDisarrayException(message, None.orNull)
|
||||||
|
}
|
||||||
|
|
@ -35,7 +35,7 @@ class InventoryEquipmentSlot(private val slot : Int, private val inv : GridInven
|
||||||
val tile = equip.Definition.Tile
|
val tile = equip.Definition.Tile
|
||||||
inv.CheckCollisionsVar(slot, tile.Width, tile.Height) match {
|
inv.CheckCollisionsVar(slot, tile.Width, tile.Height) match {
|
||||||
case Success(Nil) => inv.InsertQuickly(slot, equip)
|
case Success(Nil) => inv.InsertQuickly(slot, equip)
|
||||||
case _ => ;
|
case _ => ; //TODO we should handle the exception
|
||||||
}
|
}
|
||||||
|
|
||||||
case None =>
|
case None =>
|
||||||
|
|
|
||||||
|
|
@ -31,7 +31,7 @@ abstract class Amenity extends PlanetSideServerObject with ZoneAware {
|
||||||
*/
|
*/
|
||||||
def Owner : AmenityOwner = {
|
def Owner : AmenityOwner = {
|
||||||
if(owner == Building.NoBuilding) {
|
if(owner == Building.NoBuilding) {
|
||||||
log.warn(s"Amenity $GUID in zone $Zone tried to access owner, but doesn't have one.")
|
log.warn(s"Amenity $GUID in zone ${Zone.Id} tried to access owner, but doesn't have one.")
|
||||||
}
|
}
|
||||||
owner
|
owner
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -0,0 +1,58 @@
|
||||||
|
// Copyright (c) 2020 PSForever
|
||||||
|
package net.psforever.objects.vehicles
|
||||||
|
|
||||||
|
import net.psforever.objects.Vehicle
|
||||||
|
import net.psforever.objects.zones.Zone
|
||||||
|
|
||||||
|
/**
|
||||||
|
* na
|
||||||
|
* @param file the id of this manifest entry;
|
||||||
|
* used as the channel name for summoning passengers to the vehicle
|
||||||
|
* after it has been loaded to a new location or to a new zone;
|
||||||
|
* this channel name should be unique to the vehicle for at least the duration of the transition;
|
||||||
|
* the vehicle-specific channel with which all passengers are coordinated back to the original vehicle
|
||||||
|
* @param vehicle na
|
||||||
|
* @param origin na
|
||||||
|
* @param driverName na
|
||||||
|
* @param passengers na
|
||||||
|
* @param cargo na
|
||||||
|
*/
|
||||||
|
/**
|
||||||
|
* The channel name for summoning passengers to the vehicle
|
||||||
|
* after it has been loaded to a new location or to a new zone.
|
||||||
|
* This channel name should be unique to the vehicle for at least the duration of the transition.
|
||||||
|
* The vehicle-specific channel with which all passengers are coordinated back to the original vehicle.
|
||||||
|
* @param vehicle the vehicle being moved (or having been moved)
|
||||||
|
* @return the channel name
|
||||||
|
*/
|
||||||
|
final case class VehicleManifest(file : String,
|
||||||
|
vehicle : Vehicle,
|
||||||
|
origin : Zone,
|
||||||
|
driverName : String,
|
||||||
|
passengers : List[(String, Int)],
|
||||||
|
cargo : List[(String, Int)])
|
||||||
|
|
||||||
|
object VehicleManifest {
|
||||||
|
def apply(vehicle : Vehicle) : VehicleManifest = {
|
||||||
|
val driverName = vehicle.Seats(0).Occupant match {
|
||||||
|
case Some(driver) => driver.Name
|
||||||
|
case None => "MISSING_DRIVER"
|
||||||
|
}
|
||||||
|
val passengers = vehicle.Seats.collect { case (index, seat) if index > 0 && seat.isOccupied =>
|
||||||
|
(seat.Occupant.get.Name, index)
|
||||||
|
}
|
||||||
|
val cargo = vehicle.CargoHolds.collect { case (index, hold) if hold.Occupant.nonEmpty =>
|
||||||
|
hold.Occupant.get.Seats(0).Occupant match {
|
||||||
|
case Some(driver) =>
|
||||||
|
(driver.Name, index)
|
||||||
|
case None =>
|
||||||
|
("MISSING_DRIVER", index)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
VehicleManifest(ManifestChannelName(vehicle), vehicle, vehicle.Zone, driverName, passengers.toList, cargo.toList)
|
||||||
|
}
|
||||||
|
|
||||||
|
def ManifestChannelName(vehicle : Vehicle) : String = {
|
||||||
|
s"transport-vehicle-channel-${vehicle.GUID.guid}"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
@ -30,7 +30,9 @@ class ZonePopulationActor(zone : Zone, playerMap : TrieMap[Avatar, Option[Player
|
||||||
case Zone.Population.Leave(avatar) =>
|
case Zone.Population.Leave(avatar) =>
|
||||||
PopulationLeave(avatar, playerMap) match {
|
PopulationLeave(avatar, playerMap) match {
|
||||||
case None => ;
|
case None => ;
|
||||||
case player @ Some(_) =>
|
case player @ Some(tplayer) =>
|
||||||
|
tplayer.Zone = Zone.Nowhere
|
||||||
|
PlayerLeave(tplayer)
|
||||||
sender ! Zone.Population.PlayerHasLeft(zone, player)
|
sender ! Zone.Population.PlayerHasLeft(zone, player)
|
||||||
if(playerMap.isEmpty) {
|
if(playerMap.isEmpty) {
|
||||||
zone.StopPlayerManagementSystems()
|
zone.StopPlayerManagementSystems()
|
||||||
|
|
@ -45,7 +47,7 @@ class ZonePopulationActor(zone : Zone, playerMap : TrieMap[Avatar, Option[Player
|
||||||
sender ! Zone.Population.PlayerAlreadySpawned(zone, player)
|
sender ! Zone.Population.PlayerAlreadySpawned(zone, player)
|
||||||
}
|
}
|
||||||
else if(newToZone) {
|
else if(newToZone) {
|
||||||
player.Actor = context.actorOf(Props(classOf[PlayerControl], player), s"${player.Name}_${player.GUID.guid}_${System.currentTimeMillis}")
|
player.Actor = context.actorOf(Props(classOf[PlayerControl], player), s"${player.CharId}_${player.GUID.guid}_${System.currentTimeMillis}")
|
||||||
player.Zone = zone
|
player.Zone = zone
|
||||||
}
|
}
|
||||||
case None =>
|
case None =>
|
||||||
|
|
@ -55,8 +57,7 @@ class ZonePopulationActor(zone : Zone, playerMap : TrieMap[Avatar, Option[Player
|
||||||
case Zone.Population.Release(avatar) =>
|
case Zone.Population.Release(avatar) =>
|
||||||
PopulationRelease(avatar, playerMap) match {
|
PopulationRelease(avatar, playerMap) match {
|
||||||
case Some(tplayer) =>
|
case Some(tplayer) =>
|
||||||
tplayer.Actor ! akka.actor.PoisonPill
|
PlayerLeave(tplayer)
|
||||||
tplayer.Actor = ActorRef.noSender
|
|
||||||
case None =>
|
case None =>
|
||||||
sender ! Zone.Population.PlayerHasLeft(zone, None)
|
sender ! Zone.Population.PlayerHasLeft(zone, None)
|
||||||
}
|
}
|
||||||
|
|
@ -175,6 +176,11 @@ object ZonePopulationActor {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
def PlayerLeave(player : Player) : Unit = {
|
||||||
|
player.Actor ! akka.actor.PoisonPill
|
||||||
|
player.Actor = ActorRef.noSender
|
||||||
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* A recursive function that finds and removes a specific player from a list of players.
|
* A recursive function that finds and removes a specific player from a list of players.
|
||||||
* @param iter an `Iterator` of `Player` objects
|
* @param iter an `Iterator` of `Player` objects
|
||||||
|
|
|
||||||
|
|
@ -0,0 +1,383 @@
|
||||||
|
// Copyright (c) 2020 PSForever
|
||||||
|
package services.account
|
||||||
|
|
||||||
|
import akka.actor.{Actor, ActorRef, Cancellable, Props}
|
||||||
|
|
||||||
|
import scala.collection.mutable
|
||||||
|
import scala.concurrent.duration._
|
||||||
|
import scala.concurrent.ExecutionContext.Implicits.global
|
||||||
|
import net.psforever.objects.guid.GUIDTask
|
||||||
|
import net.psforever.objects._
|
||||||
|
import net.psforever.objects.serverobject.mount.Mountable
|
||||||
|
import net.psforever.objects.zones.Zone
|
||||||
|
import net.psforever.types.Vector3
|
||||||
|
import services.{RemoverActor, Service, ServiceManager}
|
||||||
|
import services.avatar.{AvatarAction, AvatarServiceMessage}
|
||||||
|
import services.vehicle.VehicleServiceMessage
|
||||||
|
|
||||||
|
/**
|
||||||
|
* A global service that manages user behavior as divided into the following three categories:
|
||||||
|
* persistence (ongoing participation in the game world),
|
||||||
|
* relogging (short-term client connectivity issue resolution), and
|
||||||
|
* logout (end-of-life conditions involving the separation of a user from the game world).<br>
|
||||||
|
* <br>
|
||||||
|
* A user polls this service and the services either creates a new `PersistenceMonitor` entity
|
||||||
|
* or returns whatever `PersistenceMonitor` entity currently exists.
|
||||||
|
* Performing informative pdates to the monitor about the user's eventual player avatar instance
|
||||||
|
* (which can be performed by messaging the service indirectly,
|
||||||
|
* though sending directly to the monitor is recommended)
|
||||||
|
* facilitate the management of persistence.
|
||||||
|
* If connectivity isssues with the client are encountered by the user,
|
||||||
|
* within a reasonable amount of time to connection restoration,
|
||||||
|
* the user may regain control of their existing persistence monitor and, thus, the same player avatar.
|
||||||
|
* End of life is mainly managed by the monitors internally
|
||||||
|
* and the monitors only communicate up to this service when executing their "end-of-life" operations.
|
||||||
|
*/
|
||||||
|
class AccountPersistenceService extends Actor {
|
||||||
|
/** an association of user text descriptors - player names - and their current monitor indices<br>
|
||||||
|
* key - player name, value - monitor index
|
||||||
|
*/
|
||||||
|
var userIndices : mutable.Map[String, Int] = mutable.Map[String, Int]()
|
||||||
|
/**
|
||||||
|
* an association of user test descriptors - player names - and their current monitor<br>
|
||||||
|
* key - player name, value - player monitor
|
||||||
|
*/
|
||||||
|
val accounts : mutable.Map[String, ActorRef] = mutable.Map[String, ActorRef]()
|
||||||
|
/** squad service event hook */
|
||||||
|
var squad : ActorRef = ActorRef.noSender
|
||||||
|
/** task resolver service event hook */
|
||||||
|
var resolver : ActorRef = ActorRef.noSender
|
||||||
|
/** log, for trace and warnings only */
|
||||||
|
val log = org.log4s.getLogger
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Retrieve the required system event service hooks.
|
||||||
|
* @see `ServiceManager.LookupResult`
|
||||||
|
*/
|
||||||
|
override def preStart : Unit = {
|
||||||
|
ServiceManager.serviceManager ! ServiceManager.Lookup("squad")
|
||||||
|
ServiceManager.serviceManager ! ServiceManager.Lookup("taskResolver")
|
||||||
|
log.trace("Awaiting system service hooks ...")
|
||||||
|
}
|
||||||
|
|
||||||
|
override def postStop : Unit = {
|
||||||
|
accounts.foreach { case (_, monitor) => context.stop(monitor) }
|
||||||
|
accounts.clear
|
||||||
|
}
|
||||||
|
|
||||||
|
def receive : Receive = Setup
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Entry point for persistence monitoring setup.
|
||||||
|
* Primarily intended to deal with the initial condition of verifying/assuring of an enqueued persistence monitor.
|
||||||
|
* Updates to persistence can be received and will be distributed, if possible;
|
||||||
|
* but, updating should be reserved for individual persistence monitor callback (by the user who is being monitored).
|
||||||
|
*/
|
||||||
|
val Started : Receive = {
|
||||||
|
case msg @ AccountPersistenceService.Login(name) =>
|
||||||
|
(accounts.get(name) match {
|
||||||
|
case Some(ref) => ref
|
||||||
|
case None => CreateNewPlayerToken(name)
|
||||||
|
}).tell(msg, sender)
|
||||||
|
|
||||||
|
case msg @ AccountPersistenceService.Update(name, _, _) =>
|
||||||
|
accounts.get(name) match {
|
||||||
|
case Some(ref) =>
|
||||||
|
ref ! msg
|
||||||
|
case None =>
|
||||||
|
log.warn(s"tried to update a player entry ($name) that did not yet exist; rebuilding entry ...")
|
||||||
|
CreateNewPlayerToken(name).tell(msg, sender)
|
||||||
|
}
|
||||||
|
|
||||||
|
case Logout(target) => //TODO use context.watch and Terminated?
|
||||||
|
accounts.remove(target)
|
||||||
|
|
||||||
|
case _ => ;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Process the system event service hooks when they arrive, before starting proper persistence monitoring.
|
||||||
|
* @see `ServiceManager.LookupResult`
|
||||||
|
*/
|
||||||
|
val Setup : Receive = {
|
||||||
|
case ServiceManager.LookupResult(id, endpoint) =>
|
||||||
|
id match {
|
||||||
|
case "squad" =>
|
||||||
|
squad = endpoint
|
||||||
|
case "taskResolver" =>
|
||||||
|
resolver = endpoint
|
||||||
|
}
|
||||||
|
if(squad != ActorRef.noSender &&
|
||||||
|
resolver != ActorRef.noSender) {
|
||||||
|
log.trace("Service hooks obtained. Continuing with standard operation.")
|
||||||
|
context.become(Started)
|
||||||
|
}
|
||||||
|
|
||||||
|
case msg =>
|
||||||
|
log.warn(s"Not yet started; received a $msg that will go unhandled")
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Enqueue a new persistency monitor object for this player.
|
||||||
|
* @param name the unique name of the player
|
||||||
|
* @return the persistence monitor object
|
||||||
|
*/
|
||||||
|
def CreateNewPlayerToken(name : String) : ActorRef = {
|
||||||
|
val ref = context.actorOf(Props(classOf[PersistenceMonitor], name, squad, resolver), s"$name-${NextPlayerIndex(name)}")
|
||||||
|
accounts += name -> ref
|
||||||
|
ref
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Get the next account unique login index.
|
||||||
|
* The index suggests the number of times the player has logged into the game.
|
||||||
|
* The main purpose is to give each player a meaninfgul ordinal number of logging agencies
|
||||||
|
* whose names did not interfere with each other (`Actor` name uniqueness).
|
||||||
|
* @param name the text personal descriptor used by the player
|
||||||
|
* @return the next index for this player, starting at 0
|
||||||
|
*/
|
||||||
|
def NextPlayerIndex(name : String) : Int = {
|
||||||
|
userIndices.get(name) match {
|
||||||
|
case Some(n) =>
|
||||||
|
val p = n + 1
|
||||||
|
userIndices += name -> p
|
||||||
|
p
|
||||||
|
case None =>
|
||||||
|
userIndices += name -> 0
|
||||||
|
0
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
object AccountPersistenceService {
|
||||||
|
/**
|
||||||
|
* Message to begin persistence monitoring of user with this text descriptor (player name).
|
||||||
|
* If the persistence monitor already exists, use that instead and synchronize the data.
|
||||||
|
* @param name the unique name of the player
|
||||||
|
*/
|
||||||
|
final case class Login(name : String)
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Update the persistence monitor that was setup for a user with the given text descriptor (player name).
|
||||||
|
* The player's name should be able to satisfy the condition:<br>
|
||||||
|
* `zone.LivePlayers.exists(p => p.Name.equals(name))`<br>
|
||||||
|
* @param name the unique name of the player
|
||||||
|
* @param zone the current zone the player is in
|
||||||
|
* @param position the location of the player in game world coordinates
|
||||||
|
*/
|
||||||
|
final case class Update(name : String, zone : Zone, position : Vector3)
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Observe and manage the persistence of a single named player avatar entity in the game world,
|
||||||
|
* with special care to the conditions of short interruption in connectivity (relogging)
|
||||||
|
* and end-of-life operations.
|
||||||
|
* Upon login, the monitor will echo all of the current information about the user's (recent) login back to the `sender`.
|
||||||
|
* With a zone and a coordinate position in that zone, a user's player avatar can be properly reconnected
|
||||||
|
* or can be reconstructed.
|
||||||
|
* Without actual recent activity,
|
||||||
|
* the default information about the zone is an indication that the user must start this player avatar from scratch.
|
||||||
|
* The monitor expects a reliable update messaging (heartbeat) to keep track of the important information
|
||||||
|
* and to determine the conditions for end-of-life activity.
|
||||||
|
* @param name the unique name of the player
|
||||||
|
* @param squadService a hook into the `SquadService` event system
|
||||||
|
* @param taskResolver a hook into the `TaskResolver` event system;
|
||||||
|
* used for object unregistering
|
||||||
|
*/
|
||||||
|
class PersistenceMonitor(name : String, squadService : ActorRef, taskResolver : ActorRef) extends Actor {
|
||||||
|
/** the last-reported zone of this player */
|
||||||
|
var inZone : Zone = Zone.Nowhere
|
||||||
|
/** the last-reported game coordinate position of this player */
|
||||||
|
var lastPosition : Vector3 = Vector3.Zero
|
||||||
|
/** the ongoing amount of permissible inactivity */
|
||||||
|
var timer : Cancellable = DefaultCancellable.obj
|
||||||
|
/** the sparingly-used log */
|
||||||
|
val log = org.log4s.getLogger
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Perform logout operations before the persistence monitor finally stops.
|
||||||
|
*/
|
||||||
|
override def postStop() : Unit = {
|
||||||
|
timer.cancel
|
||||||
|
PerformLogout()
|
||||||
|
}
|
||||||
|
|
||||||
|
def receive : Receive = {
|
||||||
|
case AccountPersistenceService.Login(_) =>
|
||||||
|
sender ! PlayerToken.LoginInfo(name, inZone, lastPosition)
|
||||||
|
UpdateTimer()
|
||||||
|
|
||||||
|
case AccountPersistenceService.Update(_, z, p) =>
|
||||||
|
inZone = z
|
||||||
|
lastPosition = p
|
||||||
|
UpdateTimer()
|
||||||
|
|
||||||
|
case Logout(_) =>
|
||||||
|
context.parent ! Logout(name)
|
||||||
|
context.stop(self)
|
||||||
|
|
||||||
|
case _ => ;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Restart the minimum activity timer.
|
||||||
|
*/
|
||||||
|
def UpdateTimer() : Unit = {
|
||||||
|
timer.cancel
|
||||||
|
timer = context.system.scheduler.scheduleOnce(60 seconds, self, Logout(name))
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* When the sustenance updates of the persistence monitor come to an end,
|
||||||
|
* and the persistence monitor itself is about to clean itself up,
|
||||||
|
* the player and avatar combination that has been associated with it will also undergo independent end of life activity.
|
||||||
|
* This is the true purpose of the persistence object - to perform a proper logout.<br>
|
||||||
|
* <br>
|
||||||
|
* The updates have been providing the zone
|
||||||
|
* and the basic information about the user (player name) has been provided since the beginning
|
||||||
|
* and it's a trivial matter to find where the avatar and player and asess their circumstances.
|
||||||
|
* The four important vectors are:
|
||||||
|
* the player avatar is in a vehicle,
|
||||||
|
* the player avatar is standing,
|
||||||
|
* only the avatar exists and the player released,
|
||||||
|
* and neither the avatar nor the player exist.
|
||||||
|
* It does not matter whether the player, if encountered, is alive or dead,
|
||||||
|
* only if they have been rendered a corpse and did not respawn.
|
||||||
|
* The fourth condition is not technically a failure condition,
|
||||||
|
* and can arise during normal transitional gameplay,
|
||||||
|
* but should be uncommon.
|
||||||
|
*/
|
||||||
|
def PerformLogout() : Unit = {
|
||||||
|
log.info(s"logout of $name")
|
||||||
|
(inZone.Players.find(p => p.name == name), inZone.LivePlayers.find(p => p.Name == name)) match {
|
||||||
|
case (Some(avatar), Some(player)) if player.VehicleSeated.nonEmpty =>
|
||||||
|
//alive or dead in a vehicle
|
||||||
|
//if the avatar is dead while in a vehicle, they haven't released yet
|
||||||
|
//TODO perform any last minute saving now ...
|
||||||
|
(inZone.GUID(player.VehicleSeated) match {
|
||||||
|
case Some(obj : Mountable) =>
|
||||||
|
(Some(obj), obj.Seat(obj.PassengerInSeat(player).getOrElse(-1)))
|
||||||
|
case _ => (None, None) //bad data?
|
||||||
|
}) match {
|
||||||
|
case (Some(_), Some(seat)) =>
|
||||||
|
seat.Occupant = None //unseat
|
||||||
|
case _ => ;
|
||||||
|
}
|
||||||
|
PlayerAvatarLogout(avatar, player)
|
||||||
|
|
||||||
|
case (Some(avatar), Some(player)) =>
|
||||||
|
//alive or dead, as standard Infantry
|
||||||
|
//TODO perform any last minute saving now ...
|
||||||
|
PlayerAvatarLogout(avatar, player)
|
||||||
|
|
||||||
|
case (Some(avatar), None) =>
|
||||||
|
//player has released
|
||||||
|
//our last body was turned into a corpse; just the avatar remains
|
||||||
|
//TODO perform any last minute saving now ...
|
||||||
|
AvatarLogout(avatar)
|
||||||
|
inZone.GUID(avatar.VehicleOwned) match {
|
||||||
|
case Some(obj : Vehicle) if obj.OwnerName.contains(avatar.name) =>
|
||||||
|
obj.AssignOwnership(None)
|
||||||
|
case _ => ;
|
||||||
|
}
|
||||||
|
taskResolver.tell(GUIDTask.UnregisterLocker(avatar.Locker)(inZone.GUID), context.parent)
|
||||||
|
|
||||||
|
case _ =>
|
||||||
|
//user stalled during initial session, or was caught in between zone transfer
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* A common set of actions to perform in the course of logging out a player avatar.
|
||||||
|
* Of the four scenarios described - in transport, on foot, released, missing - two of them utilize these operations.
|
||||||
|
* One of the other two uses a modified version of some of these activities to facilitate its log out.
|
||||||
|
* As this persistence monitor is about to become invalid,
|
||||||
|
* any messages sent in response to what we are sending are received by the monitor's parent.
|
||||||
|
* @see `Avatar`
|
||||||
|
* @see `AvatarAction.ObjectDelete`
|
||||||
|
* @see `AvatarServiceMessage`
|
||||||
|
* @see `DisownVehicle`
|
||||||
|
* @see `GUIDTask.UnregisterAvatar`
|
||||||
|
* @see `Player`
|
||||||
|
* @see `Zone.AvatarEvents`
|
||||||
|
* @see `Zone.Population.Release`
|
||||||
|
* @param avatar the avatar
|
||||||
|
* @param player the player
|
||||||
|
*/
|
||||||
|
def PlayerAvatarLogout(avatar : Avatar, player : Player) : Unit = {
|
||||||
|
val pguid = player.GUID
|
||||||
|
val parent = context.parent
|
||||||
|
player.Position = Vector3.Zero
|
||||||
|
player.Health = 0
|
||||||
|
DisownVehicle(player)
|
||||||
|
inZone.Population.tell(Zone.Population.Release(avatar), parent)
|
||||||
|
inZone.AvatarEvents.tell(AvatarServiceMessage(inZone.Id, AvatarAction.ObjectDelete(pguid, pguid)), parent)
|
||||||
|
AvatarLogout(avatar)
|
||||||
|
taskResolver.tell(GUIDTask.UnregisterAvatar(player)(inZone.GUID), parent)
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* A common set of actions to perform in the course of logging out an avatar.
|
||||||
|
* Of the four scenarios described - in transport, on foot, released, missing - three of them utilize these operations.
|
||||||
|
* The avatar will virtually always be in an existential position, one that needs to be handled at logout
|
||||||
|
* @see `Avatar`
|
||||||
|
* @see `Deployables.Disown`
|
||||||
|
* @see `Service.Leave`
|
||||||
|
* @see `Zone.Population.Leave`
|
||||||
|
* @param avatar the avatar
|
||||||
|
*/
|
||||||
|
def AvatarLogout(avatar : Avatar) : Unit = {
|
||||||
|
val parent = context.parent
|
||||||
|
val charId = avatar.CharId
|
||||||
|
LivePlayerList.Remove(charId)
|
||||||
|
squadService.tell(Service.Leave(Some(charId.toString)), parent)
|
||||||
|
Deployables.Disown(inZone, avatar, parent)
|
||||||
|
inZone.Population.tell(Zone.Population.Leave(avatar), parent)
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Vehicle cleanup that is specific to log out behavior.
|
||||||
|
* @see `Vehicles.Disown`
|
||||||
|
* @see `RemoverActor.AddTask`
|
||||||
|
* @see `RemoverActor.ClearSpecific`
|
||||||
|
* @see `Vehicle.Flying`
|
||||||
|
* @see `VehicleDefinition.DeconstructionTime`
|
||||||
|
* @see `VehicleServiceMessage.Decon`
|
||||||
|
* @see `Zone.VehicleEvents`
|
||||||
|
*/
|
||||||
|
def DisownVehicle(player : Player) : Unit = {
|
||||||
|
Vehicles.Disown(player, inZone) match {
|
||||||
|
case Some(vehicle) if vehicle.Health == 0 || (vehicle.Seats.values.forall(seat => !seat.isOccupied) && vehicle.Owner.isEmpty) =>
|
||||||
|
inZone.VehicleEvents ! VehicleServiceMessage.Decon(RemoverActor.ClearSpecific(List(vehicle), inZone))
|
||||||
|
inZone.VehicleEvents ! VehicleServiceMessage.Decon(RemoverActor.AddTask(vehicle, inZone,
|
||||||
|
if(vehicle.Flying) {
|
||||||
|
//TODO gravity
|
||||||
|
Some(0 seconds) //immediate deconstruction
|
||||||
|
}
|
||||||
|
else {
|
||||||
|
vehicle.Definition.DeconstructionTime //normal deconstruction
|
||||||
|
}
|
||||||
|
))
|
||||||
|
case _ => ;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Internal message that flags that the player has surpassed the maximum amount of inactivity allowable
|
||||||
|
* and should stop existing.
|
||||||
|
* @param name the unique name of the player
|
||||||
|
*/
|
||||||
|
private[this] case class Logout(name : String)
|
||||||
|
|
||||||
|
object PlayerToken {
|
||||||
|
/**
|
||||||
|
* Message dispatched to confirm that a player with given locational attributes exists.
|
||||||
|
* Agencies outside of the `AccountPersistanceService`/`PlayerToken` system make use of this message.
|
||||||
|
* ("Exists" does not imply an ongoing process and can also mean "just joined the game" here.)
|
||||||
|
* @param name the name of the player
|
||||||
|
* @param zone the zone in which the player is location
|
||||||
|
* @param position where in the zone the player is located
|
||||||
|
*/
|
||||||
|
final case class LoginInfo(name : String, zone : Zone, position : Vector3)
|
||||||
|
}
|
||||||
|
|
@ -229,6 +229,11 @@ class AvatarService(zone : Zone) extends Actor {
|
||||||
AvatarServiceResponse(s"/$forChannel/Avatar", target_guid, AvatarResponse.Revive(target_guid))
|
AvatarServiceResponse(s"/$forChannel/Avatar", target_guid, AvatarResponse.Revive(target_guid))
|
||||||
)
|
)
|
||||||
|
|
||||||
|
case AvatarAction.TeardownConnection() =>
|
||||||
|
AvatarEvents.publish(
|
||||||
|
AvatarServiceResponse(s"/$forChannel/Avatar", Service.defaultPlayerGUID, AvatarResponse.TeardownConnection())
|
||||||
|
)
|
||||||
|
|
||||||
case _ => ;
|
case _ => ;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -62,6 +62,7 @@ object AvatarAction {
|
||||||
final case class SendResponse(player_guid: PlanetSideGUID, msg: PlanetSideGamePacket) extends Action
|
final case class SendResponse(player_guid: PlanetSideGUID, msg: PlanetSideGamePacket) extends Action
|
||||||
final case class SendResponseTargeted(target_guid: PlanetSideGUID, msg: PlanetSideGamePacket) extends Action
|
final case class SendResponseTargeted(target_guid: PlanetSideGUID, msg: PlanetSideGamePacket) extends Action
|
||||||
|
|
||||||
|
final case class TeardownConnection() extends Action
|
||||||
// final case class PlayerStateShift(killer : PlanetSideGUID, victim : PlanetSideGUID) extends Action
|
// final case class PlayerStateShift(killer : PlanetSideGUID, victim : PlanetSideGUID) extends Action
|
||||||
// final case class DestroyDisplay(killer : PlanetSideGUID, victim : PlanetSideGUID) extends Action
|
// final case class DestroyDisplay(killer : PlanetSideGUID, victim : PlanetSideGUID) extends Action
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -54,5 +54,7 @@ object AvatarResponse {
|
||||||
|
|
||||||
final case class SendResponse(msg: PlanetSideGamePacket) extends Response
|
final case class SendResponse(msg: PlanetSideGamePacket) extends Response
|
||||||
final case class SendResponseTargeted(target_guid : PlanetSideGUID, msg: PlanetSideGamePacket) extends Response
|
final case class SendResponseTargeted(target_guid : PlanetSideGUID, msg: PlanetSideGamePacket) extends Response
|
||||||
|
|
||||||
|
final case class TeardownConnection() extends Response
|
||||||
// final case class PlayerStateShift(itemID : PlanetSideGUID) extends Response
|
// final case class PlayerStateShift(itemID : PlanetSideGUID) extends Response
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -53,9 +53,9 @@ class GalaxyService extends Actor {
|
||||||
GalaxyServiceResponse(s"/Galaxy", GalaxyResponse.MapUpdate(msg))
|
GalaxyServiceResponse(s"/Galaxy", GalaxyResponse.MapUpdate(msg))
|
||||||
)
|
)
|
||||||
|
|
||||||
case GalaxyAction.TransferPassenger(player_guid, temp_channel, vehicle, vehicle_to_delete) =>
|
case GalaxyAction.TransferPassenger(player_guid, temp_channel, vehicle, vehicle_to_delete, manifest) =>
|
||||||
GalaxyEvents.publish(
|
GalaxyEvents.publish(
|
||||||
GalaxyServiceResponse(s"/$forChannel/Galaxy", GalaxyResponse.TransferPassenger(temp_channel, vehicle, vehicle_to_delete))
|
GalaxyServiceResponse(s"/$forChannel/Galaxy", GalaxyResponse.TransferPassenger(temp_channel, vehicle, vehicle_to_delete, manifest))
|
||||||
)
|
)
|
||||||
case _ => ;
|
case _ => ;
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -2,6 +2,7 @@
|
||||||
package services.galaxy
|
package services.galaxy
|
||||||
|
|
||||||
import net.psforever.objects.Vehicle
|
import net.psforever.objects.Vehicle
|
||||||
|
import net.psforever.objects.vehicles.VehicleManifest
|
||||||
import net.psforever.packet.game.BuildingInfoUpdateMessage
|
import net.psforever.packet.game.BuildingInfoUpdateMessage
|
||||||
import net.psforever.types.PlanetSideGUID
|
import net.psforever.types.PlanetSideGUID
|
||||||
|
|
||||||
|
|
@ -16,5 +17,5 @@ object GalaxyAction {
|
||||||
|
|
||||||
final case class MapUpdate(msg: BuildingInfoUpdateMessage) extends Action
|
final case class MapUpdate(msg: BuildingInfoUpdateMessage) extends Action
|
||||||
|
|
||||||
final case class TransferPassenger(player_guid : PlanetSideGUID, temp_channel : String, vehicle : Vehicle, vehicle_to_delete : PlanetSideGUID) extends Action
|
final case class TransferPassenger(player_guid : PlanetSideGUID, temp_channel : String, vehicle : Vehicle, vehicle_to_delete : PlanetSideGUID, manifest : VehicleManifest) extends Action
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -2,6 +2,7 @@
|
||||||
package services.galaxy
|
package services.galaxy
|
||||||
|
|
||||||
import net.psforever.objects.Vehicle
|
import net.psforever.objects.Vehicle
|
||||||
|
import net.psforever.objects.vehicles.VehicleManifest
|
||||||
import net.psforever.objects.zones.HotSpotInfo
|
import net.psforever.objects.zones.HotSpotInfo
|
||||||
import net.psforever.packet.game.BuildingInfoUpdateMessage
|
import net.psforever.packet.game.BuildingInfoUpdateMessage
|
||||||
import net.psforever.types.PlanetSideGUID
|
import net.psforever.types.PlanetSideGUID
|
||||||
|
|
@ -17,5 +18,5 @@ object GalaxyResponse {
|
||||||
final case class HotSpotUpdate(zone_id : Int, priority : Int, host_spot_info : List[HotSpotInfo]) extends Response
|
final case class HotSpotUpdate(zone_id : Int, priority : Int, host_spot_info : List[HotSpotInfo]) extends Response
|
||||||
final case class MapUpdate(msg: BuildingInfoUpdateMessage) extends Response
|
final case class MapUpdate(msg: BuildingInfoUpdateMessage) extends Response
|
||||||
|
|
||||||
final case class TransferPassenger(temp_channel : String, vehicle : Vehicle, vehicle_to_delete : PlanetSideGUID) extends Response
|
final case class TransferPassenger(temp_channel : String, vehicle : Vehicle, vehicle_to_delete : PlanetSideGUID, manifest : VehicleManifest) extends Response
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -376,7 +376,23 @@ class SquadService extends Actor {
|
||||||
e.printStackTrace()
|
e.printStackTrace()
|
||||||
}
|
}
|
||||||
|
|
||||||
case Service.Leave(Some(char_id)) => LeaveService(char_id, sender)
|
case Service.Leave(Some(faction)) if "TRNCVS".indexOf(faction) > -1 =>
|
||||||
|
val path = s"/$faction/Squad"
|
||||||
|
val who = sender()
|
||||||
|
debug(s"$who has left $path")
|
||||||
|
SquadEvents.unsubscribe(who, path)
|
||||||
|
|
||||||
|
case Service.Leave(Some(char_id)) =>
|
||||||
|
try {
|
||||||
|
LeaveService(char_id.toLong, sender)
|
||||||
|
}
|
||||||
|
catch {
|
||||||
|
case _ : ClassCastException =>
|
||||||
|
log.warn(s"Service.Leave: tried $char_id as a unique character identifier, but it could not be casted")
|
||||||
|
case e : Exception =>
|
||||||
|
log.error(s"Service.Leave: unexpected exception using $char_id as data - ${e.getLocalizedMessage}")
|
||||||
|
e.printStackTrace()
|
||||||
|
}
|
||||||
|
|
||||||
case Service.Leave(None) | Service.LeaveAll() => UserEvents find { case(_, subscription) => subscription.path.equals(sender.path)} match {
|
case Service.Leave(None) | Service.LeaveAll() => UserEvents find { case(_, subscription) => subscription.path.equals(sender.path)} match {
|
||||||
case Some((to, _)) =>
|
case Some((to, _)) =>
|
||||||
|
|
|
||||||
|
|
@ -133,9 +133,9 @@ class VehicleService(zone : Zone) extends Actor {
|
||||||
case VehicleAction.UpdateAmsSpawnPoint(zone : Zone) =>
|
case VehicleAction.UpdateAmsSpawnPoint(zone : Zone) =>
|
||||||
sender ! VehicleServiceResponse(s"/$forChannel/Vehicle", Service.defaultPlayerGUID, VehicleResponse.UpdateAmsSpawnPoint(AmsSpawnPoints(zone)))
|
sender ! VehicleServiceResponse(s"/$forChannel/Vehicle", Service.defaultPlayerGUID, VehicleResponse.UpdateAmsSpawnPoint(AmsSpawnPoints(zone)))
|
||||||
|
|
||||||
case VehicleAction.TransferPassengerChannel(player_guid, old_channel, temp_channel, vehicle) =>
|
case VehicleAction.TransferPassengerChannel(player_guid, old_channel, temp_channel, vehicle, vehicle_to_delete) =>
|
||||||
VehicleEvents.publish(
|
VehicleEvents.publish(
|
||||||
VehicleServiceResponse(s"/$forChannel/Vehicle", player_guid, VehicleResponse.TransferPassengerChannel(old_channel, temp_channel, vehicle))
|
VehicleServiceResponse(s"/$forChannel/Vehicle", player_guid, VehicleResponse.TransferPassengerChannel(old_channel, temp_channel, vehicle, vehicle_to_delete))
|
||||||
)
|
)
|
||||||
|
|
||||||
case VehicleAction.ForceDismountVehicleCargo(player_guid, vehicle_guid, bailed, requestedByPassenger, kicked) =>
|
case VehicleAction.ForceDismountVehicleCargo(player_guid, vehicle_guid, bailed, requestedByPassenger, kicked) =>
|
||||||
|
|
|
||||||
|
|
@ -44,7 +44,7 @@ object VehicleAction {
|
||||||
final case class SendResponse(player_guid: PlanetSideGUID, msg : PlanetSideGamePacket) extends Action
|
final case class SendResponse(player_guid: PlanetSideGUID, msg : PlanetSideGamePacket) extends Action
|
||||||
final case class UpdateAmsSpawnPoint(zone : Zone) extends Action
|
final case class UpdateAmsSpawnPoint(zone : Zone) extends Action
|
||||||
|
|
||||||
final case class TransferPassengerChannel(player_guid : PlanetSideGUID, temp_channel : String, new_channel : String, vehicle : Vehicle) extends Action
|
final case class TransferPassengerChannel(player_guid : PlanetSideGUID, temp_channel : String, new_channel : String, vehicle : Vehicle, vehicle_to_delete : PlanetSideGUID) extends Action
|
||||||
|
|
||||||
final case class ForceDismountVehicleCargo(player_guid : PlanetSideGUID, vehicle_guid : PlanetSideGUID, bailed : Boolean, requestedByPassenger : Boolean, kicked : Boolean) extends Action
|
final case class ForceDismountVehicleCargo(player_guid : PlanetSideGUID, vehicle_guid : PlanetSideGUID, bailed : Boolean, requestedByPassenger : Boolean, kicked : Boolean) extends Action
|
||||||
final case class KickCargo(player_guid : PlanetSideGUID, cargo : Vehicle, speed : Int, delay : Long) extends Action
|
final case class KickCargo(player_guid : PlanetSideGUID, cargo : Vehicle, speed : Int, delay : Long) extends Action
|
||||||
|
|
|
||||||
|
|
@ -50,7 +50,7 @@ object VehicleResponse {
|
||||||
final case class ResetSpawnPad(pad_guid : PlanetSideGUID) extends Response
|
final case class ResetSpawnPad(pad_guid : PlanetSideGUID) extends Response
|
||||||
final case class PeriodicReminder(reason : Reminders.Value, data : Option[Any] = None) extends Response
|
final case class PeriodicReminder(reason : Reminders.Value, data : Option[Any] = None) extends Response
|
||||||
|
|
||||||
final case class TransferPassengerChannel(old_channel : String, temp_channel : String, vehicle : Vehicle) extends Response
|
final case class TransferPassengerChannel(old_channel : String, temp_channel : String, vehicle : Vehicle, vehicle_to_delete : PlanetSideGUID) extends Response
|
||||||
|
|
||||||
final case class ForceDismountVehicleCargo(vehicle_guid : PlanetSideGUID, bailed : Boolean, requestedByPassenger : Boolean, kicked : Boolean) extends Response
|
final case class ForceDismountVehicleCargo(vehicle_guid : PlanetSideGUID, bailed : Boolean, requestedByPassenger : Boolean, kicked : Boolean) extends Response
|
||||||
final case class KickCargo(cargo : Vehicle, speed : Int, delay : Long) extends Response
|
final case class KickCargo(cargo : Vehicle, speed : Int, delay : Long) extends Response
|
||||||
|
|
|
||||||
|
|
@ -1,10 +1,10 @@
|
||||||
// Copyright (c) 2017 PSForever
|
// Copyright (c) 2017 PSForever
|
||||||
package objects
|
package objects
|
||||||
|
|
||||||
import net.psforever.objects.{AmmoBox, SimpleItem}
|
import net.psforever.objects.{AmmoBox, SimpleItem, Tool}
|
||||||
import net.psforever.objects.definition.SimpleItemDefinition
|
import net.psforever.objects.definition.SimpleItemDefinition
|
||||||
import net.psforever.objects.inventory.{GridInventory, InventoryItem, InventoryTile}
|
import net.psforever.objects.inventory.{GridInventory, InventoryDisarrayException, InventoryItem, InventoryTile}
|
||||||
import net.psforever.objects.GlobalDefinitions._
|
import net.psforever.objects.GlobalDefinitions.{bullet_9mm, suppressor}
|
||||||
import net.psforever.types.PlanetSideGUID
|
import net.psforever.types.PlanetSideGUID
|
||||||
import org.specs2.mutable._
|
import org.specs2.mutable._
|
||||||
|
|
||||||
|
|
@ -19,6 +19,18 @@ class InventoryTest extends Specification {
|
||||||
bullet9mmBox2 = AmmoBox(bullet_9mm)
|
bullet9mmBox2 = AmmoBox(bullet_9mm)
|
||||||
bullet9mmBox2.GUID = PlanetSideGUID(2)
|
bullet9mmBox2.GUID = PlanetSideGUID(2)
|
||||||
|
|
||||||
|
"InventoryDisarrayException" should {
|
||||||
|
"construct" in {
|
||||||
|
InventoryDisarrayException("slot out of bounds")
|
||||||
|
ok
|
||||||
|
}
|
||||||
|
|
||||||
|
"construct (with Throwable)" in {
|
||||||
|
InventoryDisarrayException("slot out of bounds", new Throwable())
|
||||||
|
ok
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
"GridInventory" should {
|
"GridInventory" should {
|
||||||
"construct" in {
|
"construct" in {
|
||||||
val obj : GridInventory = GridInventory()
|
val obj : GridInventory = GridInventory()
|
||||||
|
|
@ -33,28 +45,28 @@ class InventoryTest extends Specification {
|
||||||
obj.Size mustEqual 0
|
obj.Size mustEqual 0
|
||||||
}
|
}
|
||||||
|
|
||||||
"insert item" in {
|
|
||||||
val obj : GridInventory = GridInventory(9, 6)
|
|
||||||
obj.CheckCollisions(23, bullet9mmBox1) mustEqual Success(Nil)
|
|
||||||
obj += 2 -> bullet9mmBox1
|
|
||||||
obj.TotalCapacity mustEqual 54
|
|
||||||
obj.Capacity mustEqual 45
|
|
||||||
obj.Size mustEqual 1
|
|
||||||
obj.hasItem(PlanetSideGUID(1)) mustEqual Some(bullet9mmBox1)
|
|
||||||
obj.Clear()
|
|
||||||
obj.Size mustEqual 0
|
|
||||||
}
|
|
||||||
|
|
||||||
"check for collision with inventory border" in {
|
"check for collision with inventory border" in {
|
||||||
val obj : GridInventory = GridInventory(3, 3)
|
val obj : GridInventory = GridInventory(3, 3)
|
||||||
//safe
|
//safe
|
||||||
obj.CheckCollisionsAsList(0, 3, 3) mustEqual Success(Nil)
|
obj.CheckCollisionsAsList(0, 3, 3) mustEqual Success(Nil)
|
||||||
//right
|
//right
|
||||||
obj.CheckCollisionsAsList(-1, 3, 3).isFailure mustEqual true
|
obj.CheckCollisionsAsList(-1, 3, 3) match {
|
||||||
|
case scala.util.Failure(fail) =>
|
||||||
|
fail.isInstanceOf[IndexOutOfBoundsException] mustEqual true
|
||||||
|
case _ => ko
|
||||||
|
}
|
||||||
//left
|
//left
|
||||||
obj.CheckCollisionsAsList(1, 3, 3).isFailure mustEqual true
|
obj.CheckCollisionsAsList(1, 3, 3) match {
|
||||||
|
case scala.util.Failure(fail) =>
|
||||||
|
fail.isInstanceOf[IndexOutOfBoundsException] mustEqual true
|
||||||
|
case _ => ko
|
||||||
|
}
|
||||||
//bottom
|
//bottom
|
||||||
obj.CheckCollisionsAsList(3, 3, 3).isFailure mustEqual true
|
obj.CheckCollisionsAsList(3, 3, 3) match {
|
||||||
|
case scala.util.Failure(fail) =>
|
||||||
|
fail.isInstanceOf[IndexOutOfBoundsException] mustEqual true
|
||||||
|
case _ => ko
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
"check for item collision (right insert)" in {
|
"check for item collision (right insert)" in {
|
||||||
|
|
@ -64,7 +76,7 @@ class InventoryTest extends Specification {
|
||||||
val w = bullet9mmBox2.Tile.Width
|
val w = bullet9mmBox2.Tile.Width
|
||||||
val h = bullet9mmBox2.Tile.Height
|
val h = bullet9mmBox2.Tile.Height
|
||||||
val list0 = obj.CheckCollisionsAsList(0, w, h)
|
val list0 = obj.CheckCollisionsAsList(0, w, h)
|
||||||
list0 match {
|
obj.CheckCollisionsAsList(0, w, h) match {
|
||||||
case scala.util.Success(list) => list.length mustEqual 1
|
case scala.util.Success(list) => list.length mustEqual 1
|
||||||
case scala.util.Failure(_) => ko
|
case scala.util.Failure(_) => ko
|
||||||
}
|
}
|
||||||
|
|
@ -263,35 +275,120 @@ class InventoryTest extends Specification {
|
||||||
ok
|
ok
|
||||||
}
|
}
|
||||||
|
|
||||||
|
"insert item" in {
|
||||||
|
val obj : GridInventory = GridInventory(9, 6)
|
||||||
|
obj.CheckCollisions(23, bullet9mmBox1) mustEqual Success(Nil)
|
||||||
|
obj += 2 -> bullet9mmBox1
|
||||||
|
obj.TotalCapacity mustEqual 54
|
||||||
|
obj.Capacity mustEqual 45
|
||||||
|
obj.Size mustEqual 1
|
||||||
|
obj.hasItem(PlanetSideGUID(1)).contains(bullet9mmBox1) mustEqual true
|
||||||
|
obj.Clear()
|
||||||
|
obj.Size mustEqual 0
|
||||||
|
}
|
||||||
|
|
||||||
|
"not insert into an invalid slot (n < 0)" in {
|
||||||
|
val obj : GridInventory = GridInventory(9, 6)
|
||||||
|
obj.Capacity mustEqual 54
|
||||||
|
obj.Size mustEqual 0
|
||||||
|
obj.Insert(-1, bullet9mmBox1) must throwA[IndexOutOfBoundsException]
|
||||||
|
obj.Capacity mustEqual 54
|
||||||
|
obj.Size mustEqual 0
|
||||||
|
}
|
||||||
|
|
||||||
|
"not insert into an invalid slot (n > capacity)" in {
|
||||||
|
val obj : GridInventory = GridInventory(9, 6)
|
||||||
|
obj.Capacity mustEqual 54
|
||||||
|
obj.Size mustEqual 0
|
||||||
|
obj.Insert(55, bullet9mmBox1) must throwA[IndexOutOfBoundsException]
|
||||||
|
obj.Capacity mustEqual 54
|
||||||
|
obj.Size mustEqual 0
|
||||||
|
}
|
||||||
|
|
||||||
"block insertion if item collision" in {
|
"block insertion if item collision" in {
|
||||||
val obj : GridInventory = GridInventory(9, 6)
|
val obj : GridInventory = GridInventory(9, 6)
|
||||||
obj += 0 -> bullet9mmBox1
|
obj += 0 -> bullet9mmBox1
|
||||||
obj.Capacity mustEqual 45
|
obj.Capacity mustEqual 45
|
||||||
obj.hasItem(PlanetSideGUID(1)) mustEqual Some(bullet9mmBox1)
|
obj.hasItem(PlanetSideGUID(1)).contains(bullet9mmBox1) mustEqual true
|
||||||
obj += 2 -> bullet9mmBox2
|
obj += 2 -> bullet9mmBox2
|
||||||
obj.hasItem(PlanetSideGUID(2)) mustEqual None
|
obj.hasItem(PlanetSideGUID(2)).isEmpty mustEqual true
|
||||||
|
}
|
||||||
|
|
||||||
|
"insert items quickly (risk overwriting entries)" in {
|
||||||
|
val obj : GridInventory = GridInventory(6, 6)
|
||||||
|
(obj += 0 -> bullet9mmBox1) mustEqual true
|
||||||
|
val collision1 = obj.CheckCollisions(0,1,1)
|
||||||
|
obj.CheckCollisions(1,1,1) mustEqual collision1
|
||||||
|
obj.CheckCollisions(2,1,1) mustEqual collision1
|
||||||
|
obj.CheckCollisions(6,1,1) mustEqual collision1
|
||||||
|
obj.CheckCollisions(7,1,1) mustEqual collision1
|
||||||
|
obj.CheckCollisions(8,1,1) mustEqual collision1
|
||||||
|
obj.CheckCollisions(12,1,1) mustEqual collision1
|
||||||
|
obj.CheckCollisions(13,1,1) mustEqual collision1
|
||||||
|
obj.CheckCollisions(14,1,1) mustEqual collision1
|
||||||
|
|
||||||
|
(obj += 7 -> bullet9mmBox2) mustEqual false //can not insert overlapping object
|
||||||
|
obj.CheckCollisions(0,1,1) mustEqual collision1
|
||||||
|
obj.CheckCollisions(1,1,1) mustEqual collision1
|
||||||
|
obj.CheckCollisions(2,1,1) mustEqual collision1
|
||||||
|
obj.CheckCollisions(6,1,1) mustEqual collision1
|
||||||
|
obj.CheckCollisions(7,1,1) mustEqual collision1
|
||||||
|
obj.CheckCollisions(8,1,1) mustEqual collision1
|
||||||
|
obj.CheckCollisions(12,1,1) mustEqual collision1
|
||||||
|
obj.CheckCollisions(13,1,1) mustEqual collision1
|
||||||
|
obj.CheckCollisions(14,1,1) mustEqual collision1
|
||||||
|
|
||||||
|
obj.InsertQuickly(7, bullet9mmBox2) mustEqual true //overwrite
|
||||||
|
val collision2 = obj.CheckCollisions(7,1,1)
|
||||||
|
obj.CheckCollisions(0,1,1) mustEqual collision1
|
||||||
|
obj.CheckCollisions(1,1,1) mustEqual collision1
|
||||||
|
obj.CheckCollisions(2,1,1) mustEqual collision1
|
||||||
|
obj.CheckCollisions(6,1,1) mustEqual collision1
|
||||||
|
obj.CheckCollisions(7,1,1) mustEqual collision2
|
||||||
|
obj.CheckCollisions(8,1,1) mustEqual collision2
|
||||||
|
obj.CheckCollisions(12,1,1) mustEqual collision1
|
||||||
|
obj.CheckCollisions(13,1,1) mustEqual collision2
|
||||||
|
obj.CheckCollisions(14,1,1) mustEqual collision2
|
||||||
|
}
|
||||||
|
|
||||||
|
"clear all items" in {
|
||||||
|
val obj : GridInventory = GridInventory(9, 6)
|
||||||
|
obj += 2 -> bullet9mmBox1
|
||||||
|
obj.Size mustEqual 1
|
||||||
|
obj.hasItem(PlanetSideGUID(1)).contains(bullet9mmBox1) mustEqual true
|
||||||
obj.Clear()
|
obj.Clear()
|
||||||
ok
|
obj.Size mustEqual 0
|
||||||
|
obj.hasItem(PlanetSideGUID(1)).isEmpty mustEqual true
|
||||||
}
|
}
|
||||||
|
|
||||||
"remove item" in {
|
"remove item" in {
|
||||||
val obj : GridInventory = GridInventory(9, 6)
|
val obj : GridInventory = GridInventory(9, 6)
|
||||||
obj += 0 -> bullet9mmBox1
|
obj += 0 -> bullet9mmBox1
|
||||||
obj.hasItem(PlanetSideGUID(1)) mustEqual Some(bullet9mmBox1)
|
obj.hasItem(PlanetSideGUID(1)).contains(bullet9mmBox1) mustEqual true
|
||||||
obj -= PlanetSideGUID(1)
|
obj -= PlanetSideGUID(1)
|
||||||
obj.hasItem(PlanetSideGUID(1)) mustEqual None
|
obj.hasItem(PlanetSideGUID(1)).isEmpty mustEqual true
|
||||||
obj.Clear()
|
obj.Clear()
|
||||||
ok
|
ok
|
||||||
}
|
}
|
||||||
|
|
||||||
|
"fail to remove from an invalid slot (n < 0)" in {
|
||||||
|
val obj : GridInventory = GridInventory(9, 6)
|
||||||
|
(obj -= -1) mustEqual false
|
||||||
|
}
|
||||||
|
|
||||||
|
"fail to remove from an invalid slot (n > capacity)" in {
|
||||||
|
val obj : GridInventory = GridInventory(9, 6)
|
||||||
|
(obj -= 55) mustEqual false
|
||||||
|
}
|
||||||
|
|
||||||
"unblock insertion on item removal" in {
|
"unblock insertion on item removal" in {
|
||||||
val obj : GridInventory = GridInventory(9, 6)
|
val obj : GridInventory = GridInventory(9, 6)
|
||||||
obj.CheckCollisions(23, bullet9mmBox1) mustEqual Success(Nil)
|
obj.CheckCollisions(23, bullet9mmBox1) mustEqual Success(Nil)
|
||||||
obj += 23 -> bullet9mmBox1
|
obj += 23 -> bullet9mmBox1
|
||||||
obj.hasItem(PlanetSideGUID(1)) mustEqual Some(bullet9mmBox1)
|
obj.hasItem(PlanetSideGUID(1)).contains(bullet9mmBox1) mustEqual true
|
||||||
obj.CheckCollisions(23, bullet9mmBox1) mustEqual Success(1 :: Nil)
|
obj.CheckCollisions(23, bullet9mmBox1) mustEqual Success(1 :: Nil)
|
||||||
obj -= PlanetSideGUID(1)
|
obj -= PlanetSideGUID(1)
|
||||||
obj.hasItem(PlanetSideGUID(1)) mustEqual None
|
obj.hasItem(PlanetSideGUID(1)).isEmpty mustEqual true
|
||||||
obj.CheckCollisions(23, bullet9mmBox1) mustEqual Success(Nil)
|
obj.CheckCollisions(23, bullet9mmBox1) mustEqual Success(Nil)
|
||||||
obj.Clear()
|
obj.Clear()
|
||||||
ok
|
ok
|
||||||
|
|
@ -350,41 +447,82 @@ class InventoryTest extends Specification {
|
||||||
ok
|
ok
|
||||||
}
|
}
|
||||||
|
|
||||||
"insert items quickly (risk overwriting entries)" in {
|
"confirm integrity of inventory as a grid" in {
|
||||||
val obj : GridInventory = GridInventory(6, 6)
|
val obj : GridInventory = GridInventory(6, 6)
|
||||||
(obj += 0 -> bullet9mmBox1) mustEqual true
|
(obj += 0 -> bullet9mmBox1) mustEqual true
|
||||||
val collision1 = obj.CheckCollisions(0,1,1)
|
(obj += 21 -> bullet9mmBox2) mustEqual true
|
||||||
obj.CheckCollisions(1,1,1) mustEqual collision1
|
//artificially pollute the inventory grid-space
|
||||||
obj.CheckCollisions(2,1,1) mustEqual collision1
|
obj.SetCells(10, 1, 1, 3)
|
||||||
obj.CheckCollisions(6,1,1) mustEqual collision1
|
obj.SetCells(19, 2, 2, 4)
|
||||||
obj.CheckCollisions(7,1,1) mustEqual collision1
|
obj.ElementsOnGridMatchList() mustEqual 5 //number of misses repaired
|
||||||
obj.CheckCollisions(8,1,1) mustEqual collision1
|
}
|
||||||
obj.CheckCollisions(12,1,1) mustEqual collision1
|
|
||||||
obj.CheckCollisions(13,1,1) mustEqual collision1
|
|
||||||
obj.CheckCollisions(14,1,1) mustEqual collision1
|
|
||||||
|
|
||||||
(obj += 7 -> bullet9mmBox2) mustEqual false //can not insert overlapping object
|
"confirm integrity of inventory as a list (no overlap)" in {
|
||||||
obj.CheckCollisions(0,1,1) mustEqual collision1
|
val obj : GridInventory = GridInventory(9, 9)
|
||||||
obj.CheckCollisions(1,1,1) mustEqual collision1
|
val gun = Tool(suppressor)
|
||||||
obj.CheckCollisions(2,1,1) mustEqual collision1
|
obj.InsertQuickly(0, gun)
|
||||||
obj.CheckCollisions(6,1,1) mustEqual collision1
|
obj.InsertQuickly(33, bullet9mmBox1)
|
||||||
obj.CheckCollisions(7,1,1) mustEqual collision1
|
//nothing should overlap
|
||||||
obj.CheckCollisions(8,1,1) mustEqual collision1
|
val lists = obj.ElementsInListCollideInGrid()
|
||||||
obj.CheckCollisions(12,1,1) mustEqual collision1
|
lists.size mustEqual 0
|
||||||
obj.CheckCollisions(13,1,1) mustEqual collision1
|
}
|
||||||
obj.CheckCollisions(14,1,1) mustEqual collision1
|
|
||||||
|
|
||||||
obj.InsertQuickly(7, bullet9mmBox2) mustEqual true //overwrite
|
"confirm integrity of inventory as a list (normal overlap)" in {
|
||||||
val collision2 = obj.CheckCollisions(7,1,1)
|
val obj : GridInventory = GridInventory(9, 9)
|
||||||
obj.CheckCollisions(0,1,1) mustEqual collision1
|
val gun = Tool(suppressor)
|
||||||
obj.CheckCollisions(1,1,1) mustEqual collision1
|
val bullet9mmBox3 = AmmoBox(bullet_9mm)
|
||||||
obj.CheckCollisions(2,1,1) mustEqual collision1
|
obj.InsertQuickly(0, gun)
|
||||||
obj.CheckCollisions(6,1,1) mustEqual collision1
|
obj.InsertQuickly(18, bullet9mmBox1)
|
||||||
obj.CheckCollisions(7,1,1) mustEqual collision2
|
obj.InsertQuickly(38, bullet9mmBox2)
|
||||||
obj.CheckCollisions(8,1,1) mustEqual collision2
|
obj.InsertQuickly(33, bullet9mmBox3)
|
||||||
obj.CheckCollisions(12,1,1) mustEqual collision1
|
//gun and box1 should overlap
|
||||||
obj.CheckCollisions(13,1,1) mustEqual collision2
|
//box1 and box2 should overlap
|
||||||
obj.CheckCollisions(14,1,1) mustEqual collision2
|
//box3 should not overlap with anything
|
||||||
|
val lists = obj.ElementsInListCollideInGrid()
|
||||||
|
lists.size mustEqual 2
|
||||||
|
lists.foreach { list =>
|
||||||
|
val out = list.map { _.obj }
|
||||||
|
if(out.size == 2 && out.contains(gun) && out.contains(bullet9mmBox1)) {
|
||||||
|
ok
|
||||||
|
}
|
||||||
|
else if(out.size == 2 && out.contains(bullet9mmBox1) && out.contains(bullet9mmBox2)) {
|
||||||
|
ok
|
||||||
|
}
|
||||||
|
else {
|
||||||
|
ko
|
||||||
|
}
|
||||||
|
}
|
||||||
|
ok
|
||||||
|
}
|
||||||
|
|
||||||
|
"confirm integrity of inventory as a list (triple overlap)" in {
|
||||||
|
val obj : GridInventory = GridInventory(9, 9)
|
||||||
|
val gun = Tool(suppressor)
|
||||||
|
val bullet9mmBox3 = AmmoBox(bullet_9mm)
|
||||||
|
val bullet9mmBox4 = AmmoBox(bullet_9mm)
|
||||||
|
obj.InsertQuickly(0, gun)
|
||||||
|
obj.InsertQuickly(18, bullet9mmBox1)
|
||||||
|
obj.InsertQuickly(36, bullet9mmBox2)
|
||||||
|
obj.InsertQuickly(38, bullet9mmBox3)
|
||||||
|
obj.InsertQuickly(33, bullet9mmBox4)
|
||||||
|
//gun and box1 should overlap
|
||||||
|
//box1, box2, and box3 should overlap
|
||||||
|
//box4 should not overlap with anything
|
||||||
|
val lists = obj.ElementsInListCollideInGrid()
|
||||||
|
lists.size mustEqual 2
|
||||||
|
lists.foreach { list =>
|
||||||
|
val out = list.map { _.obj }
|
||||||
|
if(out.size == 2 && out.contains(gun) && out.contains(bullet9mmBox1)) {
|
||||||
|
ok
|
||||||
|
}
|
||||||
|
else if(out.size == 3 && out.contains(bullet9mmBox1) && out.contains(bullet9mmBox2) && out.contains(bullet9mmBox3)) {
|
||||||
|
ok
|
||||||
|
}
|
||||||
|
else {
|
||||||
|
ko
|
||||||
|
}
|
||||||
|
}
|
||||||
|
ok
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -357,6 +357,7 @@ class ZonePopulationTest extends ActorTest {
|
||||||
val zone = new Zone("test", new ZoneMap(""), 0) { override def SetupNumberPools() = { } }
|
val zone = new Zone("test", new ZoneMap(""), 0) { override def SetupNumberPools() = { } }
|
||||||
val avatar = Avatar("Chord", PlanetSideEmpire.TR, CharacterGender.Male, 0, CharacterVoice.Voice5)
|
val avatar = Avatar("Chord", PlanetSideEmpire.TR, CharacterGender.Male, 0, CharacterVoice.Voice5)
|
||||||
val player = Player(avatar)
|
val player = Player(avatar)
|
||||||
|
player.GUID = PlanetSideGUID(1)
|
||||||
zone.Actor = system.actorOf(Props(classOf[ZoneActor], zone), ZoneTest.TestName)
|
zone.Actor = system.actorOf(Props(classOf[ZoneActor], zone), ZoneTest.TestName)
|
||||||
zone.Actor ! Zone.Init()
|
zone.Actor ! Zone.Init()
|
||||||
expectNoMsg(200 milliseconds)
|
expectNoMsg(200 milliseconds)
|
||||||
|
|
|
||||||
|
|
@ -261,8 +261,8 @@ class TransferPassengerChannelTest extends ActorTest {
|
||||||
val service = system.actorOf(Props(classOf[VehicleService], Zone.Nowhere), "v-service")
|
val service = system.actorOf(Props(classOf[VehicleService], Zone.Nowhere), "v-service")
|
||||||
val fury = Vehicle(GlobalDefinitions.fury)
|
val fury = Vehicle(GlobalDefinitions.fury)
|
||||||
service ! Service.Join("test")
|
service ! Service.Join("test")
|
||||||
service ! VehicleServiceMessage("test", VehicleAction.TransferPassengerChannel(PlanetSideGUID(10), "old_channel", "new_channel", fury))
|
service ! VehicleServiceMessage("test", VehicleAction.TransferPassengerChannel(PlanetSideGUID(10), "old_channel", "new_channel", fury, PlanetSideGUID(11)))
|
||||||
expectMsg(VehicleServiceResponse("/test/Vehicle", PlanetSideGUID(10), VehicleResponse.TransferPassengerChannel("old_channel", "new_channel", fury)))
|
expectMsg(VehicleServiceResponse("/test/Vehicle", PlanetSideGUID(10), VehicleResponse.TransferPassengerChannel("old_channel", "new_channel", fury, PlanetSideGUID(11))))
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -11,7 +11,7 @@ import ch.qos.logback.core.joran.spi.JoranException
|
||||||
import ch.qos.logback.core.status._
|
import ch.qos.logback.core.status._
|
||||||
import ch.qos.logback.core.util.StatusPrinter
|
import ch.qos.logback.core.util.StatusPrinter
|
||||||
import com.typesafe.config.ConfigFactory
|
import com.typesafe.config.ConfigFactory
|
||||||
import net.psforever.config.{Valid, Invalid}
|
import net.psforever.config.{Invalid, Valid}
|
||||||
import net.psforever.crypto.CryptoInterface
|
import net.psforever.crypto.CryptoInterface
|
||||||
import net.psforever.objects.zones._
|
import net.psforever.objects.zones._
|
||||||
import net.psforever.objects.guid.TaskResolver
|
import net.psforever.objects.guid.TaskResolver
|
||||||
|
|
@ -19,7 +19,7 @@ import org.slf4j
|
||||||
import org.fusesource.jansi.Ansi._
|
import org.fusesource.jansi.Ansi._
|
||||||
import org.fusesource.jansi.Ansi.Color._
|
import org.fusesource.jansi.Ansi.Color._
|
||||||
import services.ServiceManager
|
import services.ServiceManager
|
||||||
import services.account.AccountIntermediaryService
|
import services.account.{AccountIntermediaryService, AccountPersistenceService}
|
||||||
import services.chat.ChatService
|
import services.chat.ChatService
|
||||||
import services.galaxy.GalaxyService
|
import services.galaxy.GalaxyService
|
||||||
import services.teamwork.SquadService
|
import services.teamwork.SquadService
|
||||||
|
|
@ -275,6 +275,7 @@ object PsLogin {
|
||||||
serviceManager ! ServiceManager.Register(Props[GalaxyService], "galaxy")
|
serviceManager ! ServiceManager.Register(Props[GalaxyService], "galaxy")
|
||||||
serviceManager ! ServiceManager.Register(Props[SquadService], "squad")
|
serviceManager ! ServiceManager.Register(Props[SquadService], "squad")
|
||||||
serviceManager ! ServiceManager.Register(Props(classOf[InterstellarCluster], continentList), "cluster")
|
serviceManager ! ServiceManager.Register(Props(classOf[InterstellarCluster], continentList), "cluster")
|
||||||
|
serviceManager ! ServiceManager.Register(Props[AccountPersistenceService], "accountPersistence")
|
||||||
|
|
||||||
logger.info("Initializing loginRouter & worldRouter")
|
logger.info("Initializing loginRouter & worldRouter")
|
||||||
/** Create two actors for handling the login and world server endpoints */
|
/** Create two actors for handling the login and world server endpoints */
|
||||||
|
|
|
||||||
|
|
@ -6,8 +6,6 @@ import org.log4s.MDC
|
||||||
import scodec.bits._
|
import scodec.bits._
|
||||||
|
|
||||||
import scala.collection.mutable
|
import scala.collection.mutable
|
||||||
import MDCContextAware.Implicits._
|
|
||||||
import akka.actor.MDCContextAware.MdcMsg
|
|
||||||
import akka.actor.SupervisorStrategy.Stop
|
import akka.actor.SupervisorStrategy.Stop
|
||||||
import net.psforever.packet.PacketCoding
|
import net.psforever.packet.PacketCoding
|
||||||
import net.psforever.packet.control.ConnectionClose
|
import net.psforever.packet.control.ConnectionClose
|
||||||
|
|
@ -35,10 +33,10 @@ case class SessionPipeline(nameTemplate : String, props : Props)
|
||||||
*
|
*
|
||||||
* read() route decrypt
|
* read() route decrypt
|
||||||
* UDP Socket -----> [Session Router] -----> [Crypto Actor] -----> [Session Actor]
|
* UDP Socket -----> [Session Router] -----> [Crypto Actor] -----> [Session Actor]
|
||||||
* ^ | ^ | ^ |
|
* /|\ | /|\ | /|\ |
|
||||||
* | write() | | encrypt | | response |
|
* | write() | | encrypt | | response |
|
||||||
* +--------------+ +-----------+ +-----------------+
|
* +--------------+ +-----------+ +-----------------+
|
||||||
**/
|
*/
|
||||||
class SessionRouter(role : String, pipeline : List[SessionPipeline]) extends Actor with MDCContextAware {
|
class SessionRouter(role : String, pipeline : List[SessionPipeline]) extends Actor with MDCContextAware {
|
||||||
private[this] val log = org.log4s.getLogger(self.path.name)
|
private[this] val log = org.log4s.getLogger(self.path.name)
|
||||||
|
|
||||||
|
|
@ -57,7 +55,7 @@ class SessionRouter(role : String, pipeline : List[SessionPipeline]) extends Act
|
||||||
override def supervisorStrategy = OneForOneStrategy() { case _ => Stop }
|
override def supervisorStrategy = OneForOneStrategy() { case _ => Stop }
|
||||||
|
|
||||||
override def preStart = {
|
override def preStart = {
|
||||||
log.info(s"SessionRouter started...ready for ${role} sessions")
|
log.info(s"SessionRouter (for ${role}s) initializing ...")
|
||||||
}
|
}
|
||||||
|
|
||||||
def receive = initializing
|
def receive = initializing
|
||||||
|
|
@ -65,10 +63,13 @@ class SessionRouter(role : String, pipeline : List[SessionPipeline]) extends Act
|
||||||
def initializing : Receive = {
|
def initializing : Receive = {
|
||||||
case Hello() =>
|
case Hello() =>
|
||||||
inputRef = sender()
|
inputRef = sender()
|
||||||
context.become(started)
|
|
||||||
ServiceManager.serviceManager ! Lookup("accountIntermediary")
|
ServiceManager.serviceManager ! Lookup("accountIntermediary")
|
||||||
|
case ServiceManager.LookupResult("accountIntermediary", endpoint) =>
|
||||||
|
accountIntermediary = endpoint
|
||||||
|
log.info(s"SessionRouter starting; ready for $role sessions")
|
||||||
|
context.become(started)
|
||||||
case default =>
|
case default =>
|
||||||
log.error(s"Unknown message $default. Stopping...")
|
log.error(s"Unknown or unexpected message $default before being properly started. Stopping completely...")
|
||||||
context.stop(self)
|
context.stop(self)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -77,9 +78,7 @@ class SessionRouter(role : String, pipeline : List[SessionPipeline]) extends Act
|
||||||
}
|
}
|
||||||
|
|
||||||
def started : Receive = {
|
def started : Receive = {
|
||||||
case ServiceManager.LookupResult("accountIntermediary", endpoint) =>
|
case _ @ ReceivedPacket(msg, from) =>
|
||||||
accountIntermediary = endpoint
|
|
||||||
case recv @ ReceivedPacket(msg, from) =>
|
|
||||||
var session : Session = null
|
var session : Session = null
|
||||||
|
|
||||||
if(!idBySocket.contains(from)) {
|
if(!idBySocket.contains(from)) {
|
||||||
|
|
@ -92,7 +91,7 @@ class SessionRouter(role : String, pipeline : List[SessionPipeline]) extends Act
|
||||||
|
|
||||||
if(session.state != Closed()) {
|
if(session.state != Closed()) {
|
||||||
MDC("sessionId") = session.sessionId.toString
|
MDC("sessionId") = session.sessionId.toString
|
||||||
log.trace(s"RECV: ${msg} -> ${session.getPipeline.head.path.name}")
|
log.trace(s"RECV: $msg -> ${session.getPipeline.head.path.name}")
|
||||||
session.receive(RawPacket(msg))
|
session.receive(RawPacket(msg))
|
||||||
MDC.clear()
|
MDC.clear()
|
||||||
}
|
}
|
||||||
|
|
@ -102,7 +101,7 @@ class SessionRouter(role : String, pipeline : List[SessionPipeline]) extends Act
|
||||||
if(session.isDefined) {
|
if(session.isDefined) {
|
||||||
if(session.get.state != Closed()) {
|
if(session.get.state != Closed()) {
|
||||||
MDC("sessionId") = session.get.sessionId.toString
|
MDC("sessionId") = session.get.sessionId.toString
|
||||||
log.trace(s"SEND: ${msg} -> ${inputRef.path.name}")
|
log.trace(s"SEND: $msg -> ${inputRef.path.name}")
|
||||||
session.get.send(msg)
|
session.get.send(msg)
|
||||||
MDC.clear()
|
MDC.clear()
|
||||||
}
|
}
|
||||||
|
|
@ -160,7 +159,7 @@ class SessionRouter(role : String, pipeline : List[SessionPipeline]) extends Act
|
||||||
sessionByActor{actor} = session
|
sessionByActor{actor} = session
|
||||||
}
|
}
|
||||||
|
|
||||||
log.info(s"New session ID=${id} from " + address.toString)
|
log.info(s"New session ID=$id from " + address.toString)
|
||||||
|
|
||||||
if(role == "Login") {
|
if(role == "Login") {
|
||||||
accountIntermediary ! StoreIPAddress(id, new IPAddress(address))
|
accountIntermediary ! StoreIPAddress(id, new IPAddress(address))
|
||||||
|
|
@ -178,14 +177,14 @@ class SessionRouter(role : String, pipeline : List[SessionPipeline]) extends Act
|
||||||
val session : Session = sessionOption.get
|
val session : Session = sessionOption.get
|
||||||
|
|
||||||
if(graceful) {
|
if(graceful) {
|
||||||
for(i <- 0 to 5) {
|
for(_ <- 0 to 5) {
|
||||||
session.send(closePacket)
|
session.send(closePacket)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// kill all session specific actors
|
// kill all session specific actors
|
||||||
session.dropSession(graceful)
|
session.dropSession(graceful)
|
||||||
log.info(s"Dropping session ID=${id} (reason: $reason)")
|
log.info(s"Dropping session ID=$id (reason: $reason)")
|
||||||
}
|
}
|
||||||
|
|
||||||
def newSessionId = {
|
def newSessionId = {
|
||||||
|
|
|
||||||
File diff suppressed because it is too large
Load diff
Loading…
Add table
Add a link
Reference in a new issue