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:
Fate-JH 2020-02-14 10:54:52 -05:00 committed by GitHub
parent 06ef3a08c5
commit 53ecee566a
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
29 changed files with 2436 additions and 1239 deletions

View file

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

View file

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

View file

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

View file

@ -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]
/**

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

View file

@ -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 _ => ;

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

@ -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, _)) =>

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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