mirror of
https://github.com/2revoemag/PSF-BotServer.git
synced 2026-01-20 02:24:45 +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
|
|
@ -53,6 +53,8 @@ class Avatar(private val char_id : Long, val name : String, val faction : Planet
|
|||
*/
|
||||
private var lfs : Boolean = false
|
||||
|
||||
private var vehicleOwned : Option[PlanetSideGUID] = None
|
||||
|
||||
def CharId : Long = char_id
|
||||
|
||||
def BEP : Long = bep
|
||||
|
|
@ -197,6 +199,15 @@ class Avatar(private val char_id : Long, val name : String, val faction : Planet
|
|||
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
|
||||
|
||||
/*
|
||||
|
|
|
|||
|
|
@ -1,15 +1,17 @@
|
|||
// Copyright (c) 2017 PSForever
|
||||
package net.psforever.objects
|
||||
|
||||
import akka.actor.ActorRef
|
||||
import scala.concurrent.duration._
|
||||
|
||||
import net.psforever.objects.ce.{Deployable, DeployedItem}
|
||||
import net.psforever.objects.serverobject.PlanetSideServerObject
|
||||
import net.psforever.objects.zones.Zone
|
||||
import net.psforever.packet.game.{DeployableInfo, DeploymentAction}
|
||||
import net.psforever.types.PlanetSideGUID
|
||||
import services.RemoverActor
|
||||
import services.local.{LocalAction, LocalServiceMessage}
|
||||
|
||||
import scala.concurrent.duration.FiniteDuration
|
||||
|
||||
object Deployables {
|
||||
object Make {
|
||||
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.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 vehicleSeated : Option[PlanetSideGUID] = None
|
||||
private var vehicleOwned : Option[PlanetSideGUID] = None
|
||||
|
||||
Continent = "home2" //the zone id
|
||||
|
||||
|
|
@ -605,14 +604,11 @@ class Player(private val core : Avatar) extends PlanetSideServerObject
|
|||
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] = {
|
||||
vehicleOwned = guid
|
||||
VehicleOwned
|
||||
}
|
||||
def VehicleOwned_=(guid : Option[PlanetSideGUID]) : Option[PlanetSideGUID] = core.VehicleOwned_=(guid)
|
||||
|
||||
def DamageModel = exosuit.asInstanceOf[DamageResistanceModel]
|
||||
|
||||
|
|
@ -658,7 +654,6 @@ object Player {
|
|||
def Respawn(player : Player) : Player = {
|
||||
if(player.Release) {
|
||||
val obj = new Player(player.core)
|
||||
obj.VehicleOwned = player.VehicleOwned
|
||||
obj.Continent = player.Continent
|
||||
obj
|
||||
}
|
||||
|
|
|
|||
|
|
@ -102,6 +102,9 @@ class Vehicle(private val vehicleDef : VehicleDefinition) extends AmenityOwner
|
|||
*/
|
||||
private var mountedIn : Option[PlanetSideGUID] = None
|
||||
|
||||
private var vehicleGatingManifest : Option[VehicleManifest] = None
|
||||
private var previousVehicleGatingManifest : Option[VehicleManifest] = None
|
||||
|
||||
//init
|
||||
LoadDefinition()
|
||||
|
||||
|
|
@ -523,6 +526,23 @@ class Vehicle(private val vehicleDef : VehicleDefinition) extends AmenityOwner
|
|||
*/
|
||||
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]
|
||||
|
||||
/**
|
||||
|
|
|
|||
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 {
|
||||
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 {
|
||||
case Player.Die() =>
|
||||
PlayerControl.HandleDestructionAwareness(player, player.GUID, None)
|
||||
|
|
@ -40,8 +43,7 @@ class PlayerControl(player : Player) extends Actor
|
|||
val damageToCapacitor = originalCapacitor - capacitor
|
||||
PlayerControl.HandleDamageResolution(player, cause, damageToHealth, damageToArmor, damageToCapacitor)
|
||||
if(damageToHealth != 0 || damageToArmor != 0 || damageToCapacitor != 0) {
|
||||
org.log4s.getLogger("DamageResolution")
|
||||
.info(s"${player.Name}-infantry: BEFORE=$originalHealth/$originalArmor/$originalCapacitor, AFTER=$health/$armor/$capacitor, CHANGE=$damageToHealth/$damageToArmor/$damageToCapacitor")
|
||||
damageLog.info(s"${player.Name}-infantry: BEFORE=$originalHealth/$originalArmor/$originalCapacitor, AFTER=$health/$armor/$capacitor, CHANGE=$damageToHealth/$damageToArmor/$damageToCapacitor")
|
||||
}
|
||||
}
|
||||
case _ => ;
|
||||
|
|
|
|||
|
|
@ -183,7 +183,7 @@ class GridInventory extends Container {
|
|||
val starth : Int = starty + h - 1
|
||||
if(actualSlot < 0 || actualSlot >= grid.length || startw >= width || starth >= height) {
|
||||
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 {
|
||||
val collisions : mutable.Set[InventoryItem] = mutable.Set[InventoryItem]()
|
||||
|
|
@ -220,22 +220,31 @@ class GridInventory extends Container {
|
|||
val starty : Int = actualSlot / width
|
||||
val startw : Int = startx + w - 1
|
||||
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 {
|
||||
val collisions : mutable.Set[InventoryItem] = mutable.Set[InventoryItem]()
|
||||
var curr = actualSlot
|
||||
val fixedItems = items.toMap
|
||||
val fixedGrid = grid.toList
|
||||
for(_ <- 0 until h) {
|
||||
for(col <- 0 until w) {
|
||||
if(fixedGrid(curr + col) > -1) {
|
||||
collisions += fixedItems(fixedGrid(curr + col))
|
||||
try {
|
||||
for(_ <- 0 until h) {
|
||||
for(col <- 0 until w) {
|
||||
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.
|
||||
* @see `SetCellsNoOffset`
|
||||
* @param start the initial inventory index
|
||||
* @param w the width of the region
|
||||
* @param h the height of the region
|
||||
|
|
@ -287,77 +297,136 @@ class GridInventory extends Container {
|
|||
* defaults to -1 (which is "nothing")
|
||||
*/
|
||||
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.
|
||||
* Perform basic boundary checking for the current inventory dimensions.
|
||||
* @see `SetCellsOnlyNoOffset`
|
||||
* @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
|
||||
* @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) {
|
||||
val startx : Int = start % width
|
||||
val starty : Int = start / width
|
||||
val startw : Int = startx + w - 1
|
||||
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 {
|
||||
var curr = start
|
||||
for(_ <- 0 until h) {
|
||||
for(col <- 0 until w) {
|
||||
grid(curr + col) = value
|
||||
}
|
||||
curr += width
|
||||
SetCellsOnlyNoOffset(start, w, h, 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.
|
||||
* 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 = {
|
||||
val key : Int = entryIndex.getAndIncrement()
|
||||
items.get(key) match {
|
||||
case None => //no redundant insertions or other collisions
|
||||
case None => //no redundant insertions
|
||||
Insertion_CheckCollisions(start, obj, key)
|
||||
case _ =>
|
||||
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 = {
|
||||
CheckCollisions(start, obj) match {
|
||||
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 _ =>
|
||||
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())
|
||||
|
||||
/**
|
||||
* 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 = {
|
||||
val card = InventoryItem(obj, start)
|
||||
items += key -> card
|
||||
val tile = obj.Tile
|
||||
SetCells(start, tile.Width, tile.Height, key)
|
||||
true
|
||||
try {
|
||||
val tile = obj.Tile
|
||||
val updated = SetCellsNoOffset(start - offset, tile.Width, tile.Height, key)
|
||||
val card = InventoryItem(obj, start)
|
||||
items += key -> card
|
||||
grid = updated
|
||||
true
|
||||
}
|
||||
catch {
|
||||
case _ : Exception =>
|
||||
false
|
||||
}
|
||||
}
|
||||
|
||||
def +=(kv : (Int, Equipment)) : Boolean = Insert(kv._1, kv._2)
|
||||
|
||||
def Remove(index : Int) : Boolean = {
|
||||
val key = grid(index - Offset)
|
||||
items.remove(key) match {
|
||||
case Some(item) =>
|
||||
val tile = item.obj.Tile
|
||||
SetCells(item.start, tile.Width, tile.Height)
|
||||
true
|
||||
case None =>
|
||||
false
|
||||
val keyVal = index - offset
|
||||
if(keyVal > -1 && keyVal < grid.length) {
|
||||
val key = grid(index - offset)
|
||||
items.get(key) match {
|
||||
case Some(item) =>
|
||||
val tile = item.obj.Tile
|
||||
val updated = SetCellsNoOffset(item.start - offset, tile.Width, tile.Height)
|
||||
items.remove(key)
|
||||
grid = updated
|
||||
true
|
||||
case None =>
|
||||
false
|
||||
}
|
||||
}
|
||||
else {
|
||||
false
|
||||
}
|
||||
}
|
||||
|
||||
|
|
@ -365,10 +434,12 @@ class GridInventory extends Container {
|
|||
|
||||
def Remove(guid : PlanetSideGUID) : Boolean = {
|
||||
recursiveFindIdentifiedObject(items.keys.iterator, guid) match {
|
||||
case Some(index) =>
|
||||
val item = items.remove(index).get
|
||||
case Some(key) =>
|
||||
val item = items(key)
|
||||
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
|
||||
case None =>
|
||||
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.
|
||||
* @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] = {
|
||||
val list = items.values.toList
|
||||
items.clear
|
||||
SetCellsOffset(0, width, height)
|
||||
//entryIndex.set(0)
|
||||
grid = SetCellsOnlyNoOffset(0, width, height)
|
||||
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
|
||||
inv.CheckCollisionsVar(slot, tile.Width, tile.Height) match {
|
||||
case Success(Nil) => inv.InsertQuickly(slot, equip)
|
||||
case _ => ;
|
||||
case _ => ; //TODO we should handle the exception
|
||||
}
|
||||
|
||||
case None =>
|
||||
|
|
|
|||
|
|
@ -31,7 +31,7 @@ abstract class Amenity extends PlanetSideServerObject with ZoneAware {
|
|||
*/
|
||||
def Owner : AmenityOwner = {
|
||||
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
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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) =>
|
||||
PopulationLeave(avatar, playerMap) match {
|
||||
case None => ;
|
||||
case player @ Some(_) =>
|
||||
case player @ Some(tplayer) =>
|
||||
tplayer.Zone = Zone.Nowhere
|
||||
PlayerLeave(tplayer)
|
||||
sender ! Zone.Population.PlayerHasLeft(zone, player)
|
||||
if(playerMap.isEmpty) {
|
||||
zone.StopPlayerManagementSystems()
|
||||
|
|
@ -45,7 +47,7 @@ class ZonePopulationActor(zone : Zone, playerMap : TrieMap[Avatar, Option[Player
|
|||
sender ! Zone.Population.PlayerAlreadySpawned(zone, player)
|
||||
}
|
||||
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
|
||||
}
|
||||
case None =>
|
||||
|
|
@ -55,8 +57,7 @@ class ZonePopulationActor(zone : Zone, playerMap : TrieMap[Avatar, Option[Player
|
|||
case Zone.Population.Release(avatar) =>
|
||||
PopulationRelease(avatar, playerMap) match {
|
||||
case Some(tplayer) =>
|
||||
tplayer.Actor ! akka.actor.PoisonPill
|
||||
tplayer.Actor = ActorRef.noSender
|
||||
PlayerLeave(tplayer)
|
||||
case 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.
|
||||
* @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))
|
||||
)
|
||||
|
||||
case AvatarAction.TeardownConnection() =>
|
||||
AvatarEvents.publish(
|
||||
AvatarServiceResponse(s"/$forChannel/Avatar", Service.defaultPlayerGUID, AvatarResponse.TeardownConnection())
|
||||
)
|
||||
|
||||
case _ => ;
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -62,6 +62,7 @@ object AvatarAction {
|
|||
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 TeardownConnection() extends Action
|
||||
// final case class PlayerStateShift(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 SendResponseTargeted(target_guid : PlanetSideGUID, msg: PlanetSideGamePacket) extends Response
|
||||
|
||||
final case class TeardownConnection() extends Response
|
||||
// final case class PlayerStateShift(itemID : PlanetSideGUID) extends Response
|
||||
}
|
||||
|
|
|
|||
|
|
@ -53,9 +53,9 @@ class GalaxyService extends Actor {
|
|||
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(
|
||||
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 _ => ;
|
||||
}
|
||||
|
|
|
|||
|
|
@ -2,6 +2,7 @@
|
|||
package services.galaxy
|
||||
|
||||
import net.psforever.objects.Vehicle
|
||||
import net.psforever.objects.vehicles.VehicleManifest
|
||||
import net.psforever.packet.game.BuildingInfoUpdateMessage
|
||||
import net.psforever.types.PlanetSideGUID
|
||||
|
||||
|
|
@ -16,5 +17,5 @@ object GalaxyAction {
|
|||
|
||||
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
|
||||
|
||||
import net.psforever.objects.Vehicle
|
||||
import net.psforever.objects.vehicles.VehicleManifest
|
||||
import net.psforever.objects.zones.HotSpotInfo
|
||||
import net.psforever.packet.game.BuildingInfoUpdateMessage
|
||||
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 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()
|
||||
}
|
||||
|
||||
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 Some((to, _)) =>
|
||||
|
|
|
|||
|
|
@ -133,9 +133,9 @@ class VehicleService(zone : Zone) extends Actor {
|
|||
case VehicleAction.UpdateAmsSpawnPoint(zone : 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(
|
||||
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) =>
|
||||
|
|
|
|||
|
|
@ -44,7 +44,7 @@ object VehicleAction {
|
|||
final case class SendResponse(player_guid: PlanetSideGUID, msg : PlanetSideGamePacket) 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 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 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 KickCargo(cargo : Vehicle, speed : Int, delay : Long) extends Response
|
||||
|
|
|
|||
|
|
@ -1,10 +1,10 @@
|
|||
// Copyright (c) 2017 PSForever
|
||||
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.inventory.{GridInventory, InventoryItem, InventoryTile}
|
||||
import net.psforever.objects.GlobalDefinitions._
|
||||
import net.psforever.objects.inventory.{GridInventory, InventoryDisarrayException, InventoryItem, InventoryTile}
|
||||
import net.psforever.objects.GlobalDefinitions.{bullet_9mm, suppressor}
|
||||
import net.psforever.types.PlanetSideGUID
|
||||
import org.specs2.mutable._
|
||||
|
||||
|
|
@ -19,6 +19,18 @@ class InventoryTest extends Specification {
|
|||
bullet9mmBox2 = AmmoBox(bullet_9mm)
|
||||
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 {
|
||||
"construct" in {
|
||||
val obj : GridInventory = GridInventory()
|
||||
|
|
@ -33,28 +45,28 @@ class InventoryTest extends Specification {
|
|||
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 {
|
||||
val obj : GridInventory = GridInventory(3, 3)
|
||||
//safe
|
||||
obj.CheckCollisionsAsList(0, 3, 3) mustEqual Success(Nil)
|
||||
//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
|
||||
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
|
||||
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 {
|
||||
|
|
@ -64,7 +76,7 @@ class InventoryTest extends Specification {
|
|||
val w = bullet9mmBox2.Tile.Width
|
||||
val h = bullet9mmBox2.Tile.Height
|
||||
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.Failure(_) => ko
|
||||
}
|
||||
|
|
@ -263,35 +275,120 @@ class InventoryTest extends Specification {
|
|||
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 {
|
||||
val obj : GridInventory = GridInventory(9, 6)
|
||||
obj += 0 -> bullet9mmBox1
|
||||
obj.Capacity mustEqual 45
|
||||
obj.hasItem(PlanetSideGUID(1)) mustEqual Some(bullet9mmBox1)
|
||||
obj.hasItem(PlanetSideGUID(1)).contains(bullet9mmBox1) mustEqual true
|
||||
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()
|
||||
ok
|
||||
obj.Size mustEqual 0
|
||||
obj.hasItem(PlanetSideGUID(1)).isEmpty mustEqual true
|
||||
}
|
||||
|
||||
"remove item" in {
|
||||
val obj : GridInventory = GridInventory(9, 6)
|
||||
obj += 0 -> bullet9mmBox1
|
||||
obj.hasItem(PlanetSideGUID(1)) mustEqual Some(bullet9mmBox1)
|
||||
obj.hasItem(PlanetSideGUID(1)).contains(bullet9mmBox1) mustEqual true
|
||||
obj -= PlanetSideGUID(1)
|
||||
obj.hasItem(PlanetSideGUID(1)) mustEqual None
|
||||
obj.hasItem(PlanetSideGUID(1)).isEmpty mustEqual true
|
||||
obj.Clear()
|
||||
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 {
|
||||
val obj : GridInventory = GridInventory(9, 6)
|
||||
obj.CheckCollisions(23, bullet9mmBox1) mustEqual Success(Nil)
|
||||
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 -= PlanetSideGUID(1)
|
||||
obj.hasItem(PlanetSideGUID(1)) mustEqual None
|
||||
obj.hasItem(PlanetSideGUID(1)).isEmpty mustEqual true
|
||||
obj.CheckCollisions(23, bullet9mmBox1) mustEqual Success(Nil)
|
||||
obj.Clear()
|
||||
ok
|
||||
|
|
@ -350,41 +447,82 @@ class InventoryTest extends Specification {
|
|||
ok
|
||||
}
|
||||
|
||||
"insert items quickly (risk overwriting entries)" in {
|
||||
"confirm integrity of inventory as a grid" 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 += 21 -> bullet9mmBox2) mustEqual true
|
||||
//artificially pollute the inventory grid-space
|
||||
obj.SetCells(10, 1, 1, 3)
|
||||
obj.SetCells(19, 2, 2, 4)
|
||||
obj.ElementsOnGridMatchList() mustEqual 5 //number of misses repaired
|
||||
}
|
||||
|
||||
(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
|
||||
"confirm integrity of inventory as a list (no overlap)" in {
|
||||
val obj : GridInventory = GridInventory(9, 9)
|
||||
val gun = Tool(suppressor)
|
||||
obj.InsertQuickly(0, gun)
|
||||
obj.InsertQuickly(33, bullet9mmBox1)
|
||||
//nothing should overlap
|
||||
val lists = obj.ElementsInListCollideInGrid()
|
||||
lists.size mustEqual 0
|
||||
}
|
||||
|
||||
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
|
||||
"confirm integrity of inventory as a list (normal overlap)" in {
|
||||
val obj : GridInventory = GridInventory(9, 9)
|
||||
val gun = Tool(suppressor)
|
||||
val bullet9mmBox3 = AmmoBox(bullet_9mm)
|
||||
obj.InsertQuickly(0, gun)
|
||||
obj.InsertQuickly(18, bullet9mmBox1)
|
||||
obj.InsertQuickly(38, bullet9mmBox2)
|
||||
obj.InsertQuickly(33, bullet9mmBox3)
|
||||
//gun and box1 should overlap
|
||||
//box1 and box2 should overlap
|
||||
//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 avatar = Avatar("Chord", PlanetSideEmpire.TR, CharacterGender.Male, 0, CharacterVoice.Voice5)
|
||||
val player = Player(avatar)
|
||||
player.GUID = PlanetSideGUID(1)
|
||||
zone.Actor = system.actorOf(Props(classOf[ZoneActor], zone), ZoneTest.TestName)
|
||||
zone.Actor ! Zone.Init()
|
||||
expectNoMsg(200 milliseconds)
|
||||
|
|
|
|||
|
|
@ -261,8 +261,8 @@ class TransferPassengerChannelTest extends ActorTest {
|
|||
val service = system.actorOf(Props(classOf[VehicleService], Zone.Nowhere), "v-service")
|
||||
val fury = Vehicle(GlobalDefinitions.fury)
|
||||
service ! Service.Join("test")
|
||||
service ! VehicleServiceMessage("test", VehicleAction.TransferPassengerChannel(PlanetSideGUID(10), "old_channel", "new_channel", fury))
|
||||
expectMsg(VehicleServiceResponse("/test/Vehicle", PlanetSideGUID(10), VehicleResponse.TransferPassengerChannel("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, 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.util.StatusPrinter
|
||||
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.objects.zones._
|
||||
import net.psforever.objects.guid.TaskResolver
|
||||
|
|
@ -19,7 +19,7 @@ import org.slf4j
|
|||
import org.fusesource.jansi.Ansi._
|
||||
import org.fusesource.jansi.Ansi.Color._
|
||||
import services.ServiceManager
|
||||
import services.account.AccountIntermediaryService
|
||||
import services.account.{AccountIntermediaryService, AccountPersistenceService}
|
||||
import services.chat.ChatService
|
||||
import services.galaxy.GalaxyService
|
||||
import services.teamwork.SquadService
|
||||
|
|
@ -275,6 +275,7 @@ object PsLogin {
|
|||
serviceManager ! ServiceManager.Register(Props[GalaxyService], "galaxy")
|
||||
serviceManager ! ServiceManager.Register(Props[SquadService], "squad")
|
||||
serviceManager ! ServiceManager.Register(Props(classOf[InterstellarCluster], continentList), "cluster")
|
||||
serviceManager ! ServiceManager.Register(Props[AccountPersistenceService], "accountPersistence")
|
||||
|
||||
logger.info("Initializing loginRouter & worldRouter")
|
||||
/** Create two actors for handling the login and world server endpoints */
|
||||
|
|
|
|||
|
|
@ -6,8 +6,6 @@ import org.log4s.MDC
|
|||
import scodec.bits._
|
||||
|
||||
import scala.collection.mutable
|
||||
import MDCContextAware.Implicits._
|
||||
import akka.actor.MDCContextAware.MdcMsg
|
||||
import akka.actor.SupervisorStrategy.Stop
|
||||
import net.psforever.packet.PacketCoding
|
||||
import net.psforever.packet.control.ConnectionClose
|
||||
|
|
@ -35,10 +33,10 @@ case class SessionPipeline(nameTemplate : String, props : Props)
|
|||
*
|
||||
* read() route decrypt
|
||||
* UDP Socket -----> [Session Router] -----> [Crypto Actor] -----> [Session Actor]
|
||||
* ^ | ^ | ^ |
|
||||
* /|\ | /|\ | /|\ |
|
||||
* | write() | | encrypt | | response |
|
||||
* +--------------+ +-----------+ +-----------------+
|
||||
**/
|
||||
*/
|
||||
class SessionRouter(role : String, pipeline : List[SessionPipeline]) extends Actor with MDCContextAware {
|
||||
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 preStart = {
|
||||
log.info(s"SessionRouter started...ready for ${role} sessions")
|
||||
log.info(s"SessionRouter (for ${role}s) initializing ...")
|
||||
}
|
||||
|
||||
def receive = initializing
|
||||
|
|
@ -65,10 +63,13 @@ class SessionRouter(role : String, pipeline : List[SessionPipeline]) extends Act
|
|||
def initializing : Receive = {
|
||||
case Hello() =>
|
||||
inputRef = sender()
|
||||
context.become(started)
|
||||
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 =>
|
||||
log.error(s"Unknown message $default. Stopping...")
|
||||
log.error(s"Unknown or unexpected message $default before being properly started. Stopping completely...")
|
||||
context.stop(self)
|
||||
}
|
||||
|
||||
|
|
@ -77,9 +78,7 @@ class SessionRouter(role : String, pipeline : List[SessionPipeline]) extends Act
|
|||
}
|
||||
|
||||
def started : Receive = {
|
||||
case ServiceManager.LookupResult("accountIntermediary", endpoint) =>
|
||||
accountIntermediary = endpoint
|
||||
case recv @ ReceivedPacket(msg, from) =>
|
||||
case _ @ ReceivedPacket(msg, from) =>
|
||||
var session : Session = null
|
||||
|
||||
if(!idBySocket.contains(from)) {
|
||||
|
|
@ -92,7 +91,7 @@ class SessionRouter(role : String, pipeline : List[SessionPipeline]) extends Act
|
|||
|
||||
if(session.state != Closed()) {
|
||||
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))
|
||||
MDC.clear()
|
||||
}
|
||||
|
|
@ -102,7 +101,7 @@ class SessionRouter(role : String, pipeline : List[SessionPipeline]) extends Act
|
|||
if(session.isDefined) {
|
||||
if(session.get.state != Closed()) {
|
||||
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)
|
||||
MDC.clear()
|
||||
}
|
||||
|
|
@ -160,7 +159,7 @@ class SessionRouter(role : String, pipeline : List[SessionPipeline]) extends Act
|
|||
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") {
|
||||
accountIntermediary ! StoreIPAddress(id, new IPAddress(address))
|
||||
|
|
@ -178,14 +177,14 @@ class SessionRouter(role : String, pipeline : List[SessionPipeline]) extends Act
|
|||
val session : Session = sessionOption.get
|
||||
|
||||
if(graceful) {
|
||||
for(i <- 0 to 5) {
|
||||
for(_ <- 0 to 5) {
|
||||
session.send(closePacket)
|
||||
}
|
||||
}
|
||||
|
||||
// kill all session specific actors
|
||||
session.dropSession(graceful)
|
||||
log.info(s"Dropping session ID=${id} (reason: $reason)")
|
||||
log.info(s"Dropping session ID=$id (reason: $reason)")
|
||||
}
|
||||
|
||||
def newSessionId = {
|
||||
|
|
|
|||
File diff suppressed because it is too large
Load diff
Loading…
Reference in a new issue