mirror of
https://github.com/2revoemag/PSF-BotServer.git
synced 2026-07-12 15:04:39 +00:00
Stripped down LivePlayerList functionality, moving player lists onto the appropriate corresponding zones. Corpses are now created, stored, and deleted.
This commit is contained in:
parent
8a21df429b
commit
20b7726653
10 changed files with 526 additions and 264 deletions
|
|
@ -1,9 +1,6 @@
|
||||||
// Copyright (c) 2017 PSForever
|
// Copyright (c) 2017 PSForever
|
||||||
package net.psforever.objects
|
package net.psforever.objects
|
||||||
|
|
||||||
import net.psforever.packet.game.PlanetSideGUID
|
|
||||||
|
|
||||||
import scala.annotation.tailrec
|
|
||||||
import scala.collection.concurrent.{Map, TrieMap}
|
import scala.collection.concurrent.{Map, TrieMap}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
|
@ -12,157 +9,42 @@ import scala.collection.concurrent.{Map, TrieMap}
|
||||||
*/
|
*/
|
||||||
private class LivePlayerList {
|
private class LivePlayerList {
|
||||||
/** key - the session id; value - a `Player` object */
|
/** key - the session id; value - a `Player` object */
|
||||||
private val sessionMap : Map[Long, Player] = new TrieMap[Long, Player]
|
private val sessionMap : Map[Long, Avatar] = new TrieMap[Long, Avatar]
|
||||||
/** the index of the List corresponds to zone number 1-32 with 0 being "Nowhere" */
|
|
||||||
/** each mapping: key - the global unique identifier; value - the session id */
|
|
||||||
private val zoneMap : List[Map[Int, Long]] = List.fill(33)(new TrieMap[Int,Long])
|
|
||||||
|
|
||||||
def WorldPopulation(predicate : ((_, Player)) => Boolean) : List[Player] = {
|
def WorldPopulation(predicate : ((_, Avatar)) => Boolean) : List[Avatar] = {
|
||||||
sessionMap.filter(predicate).values.toList
|
sessionMap.filter(predicate).values.toList
|
||||||
}
|
}
|
||||||
|
|
||||||
def ZonePopulation(zone : Int, predicate : ((_, Player)) => Boolean) : List[Player] = {
|
def Add(sessionId : Long, avatar : Avatar) : Boolean = {
|
||||||
zoneMap.lift(zone) match {
|
sessionMap.values.find(char => char.equals(avatar)) match {
|
||||||
case Some(map) =>
|
|
||||||
val list = map.values.toList
|
|
||||||
sessionMap.filter({ case ((sess, _)) => list.contains(sess) }).filter(predicate).values.toList
|
|
||||||
case None =>
|
case None =>
|
||||||
Nil
|
sessionMap.putIfAbsent(sessionId, avatar).isEmpty
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
def Add(sessionId : Long, player : Player) : Boolean = {
|
|
||||||
sessionMap.values.find(char => char.equals(player)) match {
|
|
||||||
case None =>
|
|
||||||
sessionMap.putIfAbsent(sessionId, player).isEmpty
|
|
||||||
case Some(_) =>
|
case Some(_) =>
|
||||||
false
|
false
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
def Remove(sessionId : Long) : Option[Player] = {
|
def Remove(sessionId : Long) : Option[Avatar] = {
|
||||||
sessionMap.remove(sessionId) match {
|
sessionMap.remove(sessionId)
|
||||||
case Some(char) =>
|
|
||||||
zoneMap.foreach(zone => {
|
|
||||||
recursiveRemoveSession(zone.iterator, sessionId) match {
|
|
||||||
case Some(guid) =>
|
|
||||||
zone.remove(guid)
|
|
||||||
case None => ;
|
|
||||||
}
|
|
||||||
})
|
|
||||||
Some(char)
|
|
||||||
case None =>
|
|
||||||
None
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
@tailrec private def recursiveRemoveSession(iter : Iterator[(Int, Long)], sessionId : Long) : Option[Int] = {
|
def Shutdown : List[Avatar] = {
|
||||||
if(!iter.hasNext) {
|
|
||||||
None
|
|
||||||
}
|
|
||||||
else {
|
|
||||||
val (guid : Int, sess : Long) = iter.next
|
|
||||||
if(sess == sessionId) {
|
|
||||||
Some(guid)
|
|
||||||
}
|
|
||||||
else {
|
|
||||||
recursiveRemoveSession(iter, sessionId)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
def Get(zone : Int, guid : PlanetSideGUID) : Option[Player] = {
|
|
||||||
Get(zone, guid.guid)
|
|
||||||
}
|
|
||||||
|
|
||||||
def Get(zone : Int, guid : Int) : Option[Player] = {
|
|
||||||
zoneMap.lift(zone) match {
|
|
||||||
case Some(map) =>
|
|
||||||
map.get(guid) match {
|
|
||||||
case Some(sessionId) =>
|
|
||||||
sessionMap.get(sessionId)
|
|
||||||
case _ =>
|
|
||||||
None
|
|
||||||
}
|
|
||||||
case None =>
|
|
||||||
None
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
def Assign(zone: Int, sessionId : Long, guid : PlanetSideGUID) : Boolean = Assign(zone, sessionId, guid.guid)
|
|
||||||
|
|
||||||
def Assign(zone : Int, sessionId : Long, guid : Int) : Boolean = {
|
|
||||||
sessionMap.get(sessionId) match {
|
|
||||||
case Some(_) =>
|
|
||||||
zoneMap.lift(zone) match {
|
|
||||||
case Some(zn) =>
|
|
||||||
AssignToZone(zn, sessionId, guid)
|
|
||||||
case None =>
|
|
||||||
false
|
|
||||||
}
|
|
||||||
|
|
||||||
case None =>
|
|
||||||
false
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
private def AssignToZone(zone : Map[Int, Long], sessionId : Long, guid : Int) : Boolean = {
|
|
||||||
zone.get(guid) match {
|
|
||||||
case Some(_) =>
|
|
||||||
false
|
|
||||||
case None =>
|
|
||||||
zone(guid) = sessionId
|
|
||||||
true
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
def Drop(zone : Int, guid : PlanetSideGUID) : Option[Player] = Drop(zone, guid.guid)
|
|
||||||
|
|
||||||
def Drop(zone : Int, guid : Int) : Option[Player] = {
|
|
||||||
zoneMap.lift(zone) match {
|
|
||||||
case Some(map) =>
|
|
||||||
map.remove(guid) match {
|
|
||||||
case Some(sessionId) =>
|
|
||||||
sessionMap.get(sessionId)
|
|
||||||
case None =>
|
|
||||||
None
|
|
||||||
}
|
|
||||||
case None =>
|
|
||||||
None
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
def Shutdown : List[Player] = {
|
|
||||||
val list = sessionMap.values.toList
|
val list = sessionMap.values.toList
|
||||||
sessionMap.clear
|
sessionMap.clear
|
||||||
zoneMap.foreach(map => map.clear())
|
|
||||||
list
|
list
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* A class for storing `Player` mappings for users that are currently online.
|
* A class for storing `Player` mappings for users that are currently online.
|
||||||
* The mapping system is tightly coupled between the `Player` class and to an instance of `WorldSessionActor`.
|
* The mapping system is tightly coupled between the `Avatar` class and to an instance of `WorldSessionActor`.
|
||||||
* Looser couplings exist between the instance of `WorldSessionActor` and a given `Player`'s globally unique id.
|
|
||||||
* These looser couplings are zone-specific.
|
|
||||||
* Though the user may have local knowledge of the zone they inhabit on their `Player` object,
|
|
||||||
* it should not be trusted.<br>
|
|
||||||
* <br>
|
* <br>
|
||||||
* Use:<br>
|
* Use:<br>
|
||||||
* 1) When a users logs in during `WorldSessionActor`, associate that user's session id and the character.<br>
|
* 1) When a users logs in during `WorldSessionActor`, associate that user's session id and their character (avatar).<br>
|
||||||
* `LivePlayerList.Add(session, player)`<br>
|
* `LivePlayerList.Add(session, avatar)`<br>
|
||||||
* 2) When that user's chosen character is declared his avatar using `SetCurrentAvatarMessage`,
|
* 2) In between the previous two steps, a range of characters may be queried based on provided statistics.<br>
|
||||||
* also associate the user's session with their current GUID.<br>
|
|
||||||
* `LivePlayerList.Assign(zone, session, guid)`<br>
|
|
||||||
* 3) Repeat the previous step for as many times the user's GUID changes, especially during the aforementioned condition.<br>
|
|
||||||
* 4a) In between the previous two steps, a user's character may be referenced by their current GUID.<br>
|
|
||||||
* `LivePlayerList.Get(zone, guid)`<br>
|
|
||||||
* 4b) Also in between those same previous steps, a range of characters may be queried based on provided statistics.<br>
|
|
||||||
* `LivePlayerList.WorldPopulation(...)`<br>
|
* `LivePlayerList.WorldPopulation(...)`<br>
|
||||||
* `LivePlayerList.ZonePopulation(zone, ...)`<br>
|
* 3) When the user leaves the game entirely, his character's entry is removed from the mapping.<br>
|
||||||
* 5) When the user navigates away from a region completely, their entry is forgotten.<br>
|
|
||||||
* `LivePlayerList.Drop(zone, guid)`<br>
|
|
||||||
* 6) When the user leaves the game entirely, his character's entries are removed from the mappings.<br>
|
|
||||||
* `LivePlayerList.Remove(session)`
|
* `LivePlayerList.Remove(session)`
|
||||||
*/
|
*/
|
||||||
object LivePlayerList {
|
object LivePlayerList {
|
||||||
|
|
@ -179,100 +61,28 @@ object LivePlayerList {
|
||||||
* @param predicate the conditions for filtering the live `Player`s
|
* @param predicate the conditions for filtering the live `Player`s
|
||||||
* @return a list of users's `Player`s that fit the criteria
|
* @return a list of users's `Player`s that fit the criteria
|
||||||
*/
|
*/
|
||||||
def WorldPopulation(predicate : ((_, Player)) => Boolean) : List[Player] = Instance.WorldPopulation(predicate)
|
def WorldPopulation(predicate : ((_, Avatar)) => Boolean) : List[Avatar] = Instance.WorldPopulation(predicate)
|
||||||
|
|
||||||
/**
|
|
||||||
* Given some criteria, examine the mapping of user characters for a zone and find the ones that fulfill the requirements.<br>
|
|
||||||
* <br>
|
|
||||||
* Note the signature carefully.
|
|
||||||
* A two-element tuple is checked, but only the second element of that tuple - a `Player` - is eligible for being queried.
|
|
||||||
* The first element is ignored.
|
|
||||||
* Even a predicate as simple as `{ case ((x : Long, _)) => x > 0 }` will not work for that reason.
|
|
||||||
* @param zone the number of the zone
|
|
||||||
* @param predicate the conditions for filtering the live `Player`s
|
|
||||||
* @return a list of users's `Player`s that fit the criteria
|
|
||||||
*/
|
|
||||||
def ZonePopulation(zone : Int, predicate : ((_, Player)) => Boolean) : List[Player] = Instance.ZonePopulation(zone, predicate)
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Create a mapped entry between the user's session and a user's character.
|
* Create a mapped entry between the user's session and a user's character.
|
||||||
* Neither the player nor the session may exist in the current mappings if this is to work.
|
* Neither the player nor the session may exist in the current mappings if this is to work.
|
||||||
* @param sessionId the session
|
* @param sessionId the session
|
||||||
* @param player the character
|
* @param avatar the character
|
||||||
* @return `true`, if the session was association was made; `false`, otherwise
|
* @return `true`, if the session was association was made; `false`, otherwise
|
||||||
*/
|
*/
|
||||||
def Add(sessionId : Long, player : Player) : Boolean = Instance.Add(sessionId, player)
|
def Add(sessionId : Long, avatar : Avatar) : Boolean = Instance.Add(sessionId, avatar)
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Remove all entries related to the given session identifier from the mappings.
|
* Remove all entries related to the given session identifier from the mappings.
|
||||||
* The player no longer counts as "online."
|
* The character no longer counts as "online."
|
||||||
* This function cleans up __all__ associations - those created by `Add`, and those created by `Assign`.
|
|
||||||
* @param sessionId the session
|
* @param sessionId the session
|
||||||
* @return any character that was afffected by the mapping removal
|
* @return any character that was afffected by the mapping removal
|
||||||
*/
|
*/
|
||||||
def Remove(sessionId : Long) : Option[Player] = Instance.Remove(sessionId)
|
def Remove(sessionId : Long) : Option[Avatar] = Instance.Remove(sessionId)
|
||||||
|
|
||||||
/**
|
|
||||||
* Get a user's character from the mappings.
|
|
||||||
* @param zone the number of the zone
|
|
||||||
* @param guid the current GUID of the character
|
|
||||||
* @return the character, if it can be found using the GUID
|
|
||||||
*/
|
|
||||||
def Get(zone : Int, guid : PlanetSideGUID) : Option[Player] = Instance.Get(zone, guid)
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Get a user's character from the mappings.
|
|
||||||
* @param zone the number of the zone
|
|
||||||
* @param guid the current GUID of the character
|
|
||||||
* @return the character, if it can be found using the GUID
|
|
||||||
*/
|
|
||||||
def Get(zone : Int, guid : Int) : Option[Player] = Instance.Get(zone, guid)
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Given a session that maps to a user's character, create a mapping between the character's current GUID and the session.
|
|
||||||
* If the user already has a GUID in the mappings, remove it and assert the new one.
|
|
||||||
* @param zone the number of the zone
|
|
||||||
* @param sessionId the session
|
|
||||||
* @param guid the GUID to associate with the character;
|
|
||||||
* technically, it has already been assigned and should be findable using `{character}.GUID.guid`
|
|
||||||
* @return `true`, if the mapping was created;
|
|
||||||
* `false`, if the session can not be found or if the character's GUID doesn't match the one provided
|
|
||||||
*/
|
|
||||||
def Assign(zone : Int, sessionId : Long, guid : PlanetSideGUID) : Boolean = Instance.Assign(zone, sessionId, guid)
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Given a session that maps to a user's character, create a mapping between the character's current GUID and the session.
|
|
||||||
* If the user already has a GUID in the mappings, remove it and assert the new one.
|
|
||||||
* @param zone the number of the zone
|
|
||||||
* @param sessionId the session
|
|
||||||
* @param guid the GUID to associate with the character;
|
|
||||||
* technically, it has already been assigned and should be findable using `{character}.GUID.guid`
|
|
||||||
* @return `true`, if the mapping was created;
|
|
||||||
* `false`, if the session can not be found or if the character's GUID doesn't match the one provided
|
|
||||||
*/
|
|
||||||
def Assign(zone : Int, sessionId : Long, guid : Int) : Boolean = Instance.Assign(zone, sessionId, guid)
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Given a GUID, remove any record of it.
|
|
||||||
* @param zone the number of the zone
|
|
||||||
* @param guid a GUID associated with the character;
|
|
||||||
* it does not have to be findable using `{character}.GUID.guid`
|
|
||||||
* @return any `Player` that may have been associated with this GUID
|
|
||||||
*/
|
|
||||||
def Drop(zone : Int, guid : PlanetSideGUID) : Option[Player] = Instance.Drop(zone, guid)
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Given a GUID, remove any record of it.
|
|
||||||
* @param zone the number of the zone
|
|
||||||
* @param guid a GUID associated with the character;
|
|
||||||
* it does not have to be findable using `{character}.GUID.guid`
|
|
||||||
* @return any `Player` that may have been associated with this GUID
|
|
||||||
*/
|
|
||||||
def Drop(zone : Int, guid : Int) : Option[Player] = Instance.Drop(zone, guid)
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Hastily remove all mappings and ids.
|
* Hastily remove all mappings and ids.
|
||||||
* @return an unsorted list of the characters that were still online
|
* @return an unsorted list of the characters that were still online
|
||||||
*/
|
*/
|
||||||
def Shutdown : List[Player] = Instance.Shutdown
|
def Shutdown : List[Avatar] = Instance.Shutdown
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -0,0 +1,124 @@
|
||||||
|
// Copyright (c) 2017 PSForever
|
||||||
|
package net.psforever.objects.definition.converter
|
||||||
|
|
||||||
|
import net.psforever.objects.{EquipmentSlot, Player}
|
||||||
|
import net.psforever.objects.equipment.Equipment
|
||||||
|
import net.psforever.packet.game.objectcreate.{BasicCharacterData, CharacterAppearanceData, CharacterData, DetailedCharacterData, DrawnSlot, InternalSlot, InventoryData, PlacementData, RibbonBars}
|
||||||
|
import net.psforever.types.{CharacterGender, GrenadeState}
|
||||||
|
|
||||||
|
import scala.annotation.tailrec
|
||||||
|
import scala.util.{Failure, Success, Try}
|
||||||
|
|
||||||
|
class CorpseConverter extends AvatarConverter {
|
||||||
|
override def ConstructorData(obj : Player) : Try[CharacterData] =
|
||||||
|
Failure(new Exception("CorpseConverter should not be used to generate CharacterData"))
|
||||||
|
|
||||||
|
override def DetailedConstructorData(obj : Player) : Try[DetailedCharacterData] = {
|
||||||
|
Success(
|
||||||
|
DetailedCharacterData(
|
||||||
|
MakeAppearanceData(obj),
|
||||||
|
0, 0, 0, 0, 0, 0, 0,
|
||||||
|
Nil, Nil, Nil, Nil,
|
||||||
|
None,
|
||||||
|
InventoryData((MakeHolsters(obj) ++ MakeInventory(obj)).sortBy(_.parentSlot)),
|
||||||
|
DrawnSlot.None
|
||||||
|
)
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Compose some data from a `Player` into a representation common to both `CharacterData` and `DetailedCharacterData`.
|
||||||
|
* @param obj the `Player` game object
|
||||||
|
* @return the resulting `CharacterAppearanceData`
|
||||||
|
*/
|
||||||
|
private def MakeAppearanceData(obj : Player) : CharacterAppearanceData = {
|
||||||
|
CharacterAppearanceData(
|
||||||
|
PlacementData(obj.Position, obj.Orientation),
|
||||||
|
BasicCharacterData(obj.Name, obj.Faction, CharacterGender.Male, 0, 0),
|
||||||
|
0,
|
||||||
|
false,
|
||||||
|
false,
|
||||||
|
obj.ExoSuit,
|
||||||
|
"",
|
||||||
|
0,
|
||||||
|
true,
|
||||||
|
obj.Orientation.y, //TODO is this important?
|
||||||
|
0,
|
||||||
|
true,
|
||||||
|
GrenadeState.None,
|
||||||
|
false,
|
||||||
|
false,
|
||||||
|
false,
|
||||||
|
RibbonBars()
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Given a player with an inventory, convert the contents of that inventory into converted-decoded packet data.
|
||||||
|
* The inventory is not represented in a `0x17` `Player`, so the conversion is only valid for `0x18` avatars.
|
||||||
|
* It will always be "`Detailed`".
|
||||||
|
* @param obj the `Player` game object
|
||||||
|
* @return a list of all items that were in the inventory in decoded packet form
|
||||||
|
*/
|
||||||
|
private def MakeInventory(obj : Player) : List[InternalSlot] = {
|
||||||
|
obj.Inventory.Items
|
||||||
|
.map({
|
||||||
|
case(_, item) =>
|
||||||
|
val equip : Equipment = item.obj
|
||||||
|
BuildEquipment(item.start, equip)
|
||||||
|
}).toList
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Given a player with equipment holsters, convert the contents of those holsters into converted-decoded packet data.
|
||||||
|
* The decoded packet form is determined by the function in the parameters as both `0x17` and `0x18` conversions are available,
|
||||||
|
* with exception to the contents of the fifth slot.
|
||||||
|
* The fifth slot is only represented if the `Player` is an `0x18` type.
|
||||||
|
* @param obj the `Player` game object
|
||||||
|
* @return a list of all items that were in the holsters in decoded packet form
|
||||||
|
*/
|
||||||
|
private def MakeHolsters(obj : Player) : List[InternalSlot] = {
|
||||||
|
recursiveMakeHolsters(obj.Holsters().iterator)
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Given some equipment holsters, convert the contents of those holsters into converted-decoded packet data.
|
||||||
|
* @param iter an `Iterator` of `EquipmentSlot` objects that are a part of the player's holsters
|
||||||
|
* @param list the current `List` of transformed data
|
||||||
|
* @param index which holster is currently being explored
|
||||||
|
* @return the `List` of inventory data created from the holsters
|
||||||
|
*/
|
||||||
|
@tailrec private def recursiveMakeHolsters(iter : Iterator[EquipmentSlot], list : List[InternalSlot] = Nil, index : Int = 0) : List[InternalSlot] = {
|
||||||
|
if(!iter.hasNext) {
|
||||||
|
list
|
||||||
|
}
|
||||||
|
else {
|
||||||
|
val slot : EquipmentSlot = iter.next
|
||||||
|
if(slot.Equipment.isDefined) {
|
||||||
|
val equip : Equipment = slot.Equipment.get
|
||||||
|
recursiveMakeHolsters(
|
||||||
|
iter,
|
||||||
|
list :+ BuildEquipment(index, equip),
|
||||||
|
index + 1
|
||||||
|
)
|
||||||
|
}
|
||||||
|
else {
|
||||||
|
recursiveMakeHolsters(iter, list, index + 1)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* A builder method for turning an object into `0x17` decoded packet form.
|
||||||
|
* @param index the position of the object
|
||||||
|
* @param equip the game object
|
||||||
|
* @return the game object in decoded packet form
|
||||||
|
*/
|
||||||
|
private def BuildEquipment(index : Int, equip : Equipment) : InternalSlot = {
|
||||||
|
InternalSlot(equip.Definition.ObjectId, equip.GUID, index, equip.Definition.Packet.DetailedConstructorData(equip).get)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
object CorpseConverter {
|
||||||
|
val converter = new CorpseConverter
|
||||||
|
}
|
||||||
|
|
@ -22,7 +22,7 @@ class VehicleConverter extends ObjectCreateConverter[Vehicle]() {
|
||||||
PlanetSideGUID(0) //if(obj.Owner.isDefined) { obj.Owner.get } else { PlanetSideGUID(0) } //TODO is this really Owner?
|
PlanetSideGUID(0) //if(obj.Owner.isDefined) { obj.Owner.get } else { PlanetSideGUID(0) } //TODO is this really Owner?
|
||||||
),
|
),
|
||||||
0,
|
0,
|
||||||
obj.Health / obj.MaxHealth * 255, //TODO not precise
|
255 * obj.Health / obj.MaxHealth, //TODO not precise
|
||||||
false, false,
|
false, false,
|
||||||
obj.DeploymentState,
|
obj.DeploymentState,
|
||||||
false,
|
false,
|
||||||
|
|
|
||||||
|
|
@ -3,7 +3,7 @@ package net.psforever.objects.zones
|
||||||
|
|
||||||
import akka.actor.{ActorContext, ActorRef, Props}
|
import akka.actor.{ActorContext, ActorRef, Props}
|
||||||
import akka.routing.RandomPool
|
import akka.routing.RandomPool
|
||||||
import net.psforever.objects.{PlanetSideGameObject, Player, Vehicle}
|
import net.psforever.objects.{Avatar, PlanetSideGameObject, Player, Vehicle}
|
||||||
import net.psforever.objects.equipment.Equipment
|
import net.psforever.objects.equipment.Equipment
|
||||||
import net.psforever.objects.guid.NumberPoolHub
|
import net.psforever.objects.guid.NumberPoolHub
|
||||||
import net.psforever.objects.guid.actor.UniqueNumberSystem
|
import net.psforever.objects.guid.actor.UniqueNumberSystem
|
||||||
|
|
@ -14,6 +14,7 @@ import net.psforever.packet.game.PlanetSideGUID
|
||||||
import net.psforever.types.Vector3
|
import net.psforever.types.Vector3
|
||||||
|
|
||||||
import scala.annotation.tailrec
|
import scala.annotation.tailrec
|
||||||
|
import scala.collection.concurrent.TrieMap
|
||||||
import scala.collection.mutable.ListBuffer
|
import scala.collection.mutable.ListBuffer
|
||||||
import scala.collection.immutable.{Map => PairMap}
|
import scala.collection.immutable.{Map => PairMap}
|
||||||
|
|
||||||
|
|
@ -54,6 +55,12 @@ class Zone(private val zoneId : String, zoneMap : ZoneMap, zoneNumber : Int) {
|
||||||
private var vehicles : List[Vehicle] = List[Vehicle]()
|
private var vehicles : List[Vehicle] = List[Vehicle]()
|
||||||
/** */
|
/** */
|
||||||
private var transport : ActorRef = ActorRef.noSender
|
private var transport : ActorRef = ActorRef.noSender
|
||||||
|
/** */
|
||||||
|
private val players : TrieMap[Avatar, Option[Player]] = TrieMap[Avatar, Option[Player]]()
|
||||||
|
/** */
|
||||||
|
private var corpses : List[Player] = List[Player]()
|
||||||
|
/** */
|
||||||
|
private var population : ActorRef = ActorRef.noSender
|
||||||
|
|
||||||
private var buildings : PairMap[Int, Building] = PairMap.empty[Int, Building]
|
private var buildings : PairMap[Int, Building] = PairMap.empty[Int, Building]
|
||||||
|
|
||||||
|
|
@ -80,6 +87,7 @@ class Zone(private val zoneId : String, zoneMap : ZoneMap, zoneNumber : Int) {
|
||||||
accessor = context.actorOf(RandomPool(25).props(Props(classOf[UniqueNumberSystem], guid, UniqueNumberSystem.AllocateNumberPoolActors(guid))), s"$Id-uns")
|
accessor = context.actorOf(RandomPool(25).props(Props(classOf[UniqueNumberSystem], guid, UniqueNumberSystem.AllocateNumberPoolActors(guid))), s"$Id-uns")
|
||||||
ground = context.actorOf(Props(classOf[ZoneGroundActor], equipmentOnGround), s"$Id-ground")
|
ground = context.actorOf(Props(classOf[ZoneGroundActor], equipmentOnGround), s"$Id-ground")
|
||||||
transport = context.actorOf(Props(classOf[ZoneVehicleActor], this), s"$Id-vehicles")
|
transport = context.actorOf(Props(classOf[ZoneVehicleActor], this), s"$Id-vehicles")
|
||||||
|
population = context.actorOf(Props(classOf[ZonePopulationActor], this), s"$Id-players")
|
||||||
|
|
||||||
Map.LocalObjects.foreach({ builderObject => builderObject.Build })
|
Map.LocalObjects.foreach({ builderObject => builderObject.Build })
|
||||||
MakeBuildings(context)
|
MakeBuildings(context)
|
||||||
|
|
@ -179,6 +187,12 @@ class Zone(private val zoneId : String, zoneMap : ZoneMap, zoneNumber : Int) {
|
||||||
|
|
||||||
def Vehicles : List[Vehicle] = vehicles
|
def Vehicles : List[Vehicle] = vehicles
|
||||||
|
|
||||||
|
def Players : List[Avatar] = players.keys.toList
|
||||||
|
|
||||||
|
def LivePlayers : List[Player] = players.values.collect( { case Some(tplayer) => tplayer }).toList
|
||||||
|
|
||||||
|
def Corpses : List[Player] = corpses
|
||||||
|
|
||||||
def AddVehicle(vehicle : Vehicle) : List[Vehicle] = {
|
def AddVehicle(vehicle : Vehicle) : List[Vehicle] = {
|
||||||
vehicles = vehicles :+ vehicle
|
vehicles = vehicles :+ vehicle
|
||||||
Vehicles
|
Vehicles
|
||||||
|
|
@ -208,6 +222,78 @@ class Zone(private val zoneId : String, zoneMap : ZoneMap, zoneNumber : Int) {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
def PopulationJoin(avatar : Avatar) : Boolean = {
|
||||||
|
players.get(avatar) match {
|
||||||
|
case Some(_) =>
|
||||||
|
false
|
||||||
|
case None =>
|
||||||
|
players += avatar -> None
|
||||||
|
true
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
def PopulationLeave(avatar : Avatar) : Option[Player] = {
|
||||||
|
players.remove(avatar) match {
|
||||||
|
case None =>
|
||||||
|
None
|
||||||
|
case Some(tplayer) =>
|
||||||
|
tplayer
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
def PopulationSpawn(avatar : Avatar, player : Player) : Option[Player] = {
|
||||||
|
players.get(avatar) match {
|
||||||
|
case None =>
|
||||||
|
None
|
||||||
|
case Some(tplayer) =>
|
||||||
|
tplayer match {
|
||||||
|
case Some(aplayer) =>
|
||||||
|
Some(aplayer)
|
||||||
|
case None =>
|
||||||
|
players(avatar) = Some(player)
|
||||||
|
Some(player)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
def PopulationRelease(avatar : Avatar) : Option[Player] = {
|
||||||
|
players.get(avatar) match {
|
||||||
|
case None =>
|
||||||
|
None
|
||||||
|
case Some(tplayer) =>
|
||||||
|
players(avatar) = None
|
||||||
|
tplayer
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
def CorpseAdd(player : Player) : Unit = {
|
||||||
|
if(player.isBackpack && recursiveFindCorpse(players.values.filter(_.nonEmpty).map(_.get).iterator, player.GUID).isEmpty) {
|
||||||
|
corpses = corpses :+ player
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
def CorpseRemove(player : Player) : Unit = {
|
||||||
|
recursiveFindCorpse(corpses.iterator, player.GUID) match {
|
||||||
|
case Some(index) =>
|
||||||
|
corpses = corpses.take(index-1) ++ corpses.drop(index)
|
||||||
|
case None => ;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@tailrec final def recursiveFindCorpse(iter : Iterator[Player], guid : PlanetSideGUID, index : Int = 0) : Option[Int] = {
|
||||||
|
if(!iter.hasNext) {
|
||||||
|
None
|
||||||
|
}
|
||||||
|
else {
|
||||||
|
if(iter.next.GUID == guid) {
|
||||||
|
Some(index)
|
||||||
|
}
|
||||||
|
else {
|
||||||
|
recursiveFindCorpse(iter, guid, index + 1)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Coordinate `Equipment` that has been dropped on the ground or to-be-dropped on the ground.
|
* Coordinate `Equipment` that has been dropped on the ground or to-be-dropped on the ground.
|
||||||
* @return synchronized reference to the ground
|
* @return synchronized reference to the ground
|
||||||
|
|
@ -220,6 +306,8 @@ class Zone(private val zoneId : String, zoneMap : ZoneMap, zoneNumber : Int) {
|
||||||
|
|
||||||
def Transport : ActorRef = transport
|
def Transport : ActorRef = transport
|
||||||
|
|
||||||
|
def Population : ActorRef = population
|
||||||
|
|
||||||
def Buildings : Map[Int, Building] = buildings
|
def Buildings : Map[Int, Building] = buildings
|
||||||
|
|
||||||
def Building(id : Int) : Option[Building] = {
|
def Building(id : Int) : Option[Building] = {
|
||||||
|
|
@ -266,6 +354,21 @@ object Zone {
|
||||||
*/
|
*/
|
||||||
final case class Init()
|
final case class Init()
|
||||||
|
|
||||||
|
object Population {
|
||||||
|
final case class Join(avatar : Avatar)
|
||||||
|
final case class Leave(avatar : Avatar)
|
||||||
|
final case class Spawn(avatar : Avatar, player : Player)
|
||||||
|
final case class Release(avatar : Avatar)
|
||||||
|
final case class PlayerHasLeft(zone : Zone, player : Option[Player]) //Leave(avatar), but still has a player
|
||||||
|
final case class PlayerAlreadySpawned(player : Player) //Spawn(avatar, player), but avatar already has a player
|
||||||
|
final case class PlayerCanNotSpawn(zone : Zone, player : Player) //Spawn(avatar, player), but avatar did not initially Join(avatar)
|
||||||
|
}
|
||||||
|
|
||||||
|
object Corpse {
|
||||||
|
final case class Add(player : Player)
|
||||||
|
final case class Remove(player : Player)
|
||||||
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Message to relinguish an item and place in on the ground.
|
* Message to relinguish an item and place in on the ground.
|
||||||
* @param item the piece of `Equipment`
|
* @param item the piece of `Equipment`
|
||||||
|
|
|
||||||
|
|
@ -0,0 +1,43 @@
|
||||||
|
// Copyright (c) 2017 PSForever
|
||||||
|
package net.psforever.objects.zones
|
||||||
|
|
||||||
|
import akka.actor.Actor
|
||||||
|
|
||||||
|
class ZonePopulationActor(zone : Zone) extends Actor {
|
||||||
|
def receive : Receive = {
|
||||||
|
case Zone.Population.Join(avatar) =>
|
||||||
|
zone.PopulationJoin(avatar)
|
||||||
|
|
||||||
|
case Zone.Population.Leave(avatar) =>
|
||||||
|
zone.PopulationLeave(avatar) match {
|
||||||
|
case None => ;
|
||||||
|
case player @ Some(_) =>
|
||||||
|
sender ! Zone.Population.PlayerHasLeft(zone, player)
|
||||||
|
}
|
||||||
|
|
||||||
|
case Zone.Population.Spawn(avatar, player) =>
|
||||||
|
zone.PopulationSpawn(avatar, player) match {
|
||||||
|
case Some(tplayer) =>
|
||||||
|
if(tplayer != player) {
|
||||||
|
sender ! Zone.Population.PlayerCanNotSpawn(zone, player)
|
||||||
|
}
|
||||||
|
case None =>
|
||||||
|
sender ! Zone.Population.PlayerCanNotSpawn(zone, player)
|
||||||
|
}
|
||||||
|
|
||||||
|
case Zone.Population.Release(avatar) =>
|
||||||
|
zone.PopulationRelease(avatar) match {
|
||||||
|
case Some(_) => ;
|
||||||
|
case None =>
|
||||||
|
sender ! Zone.Population.PlayerHasLeft(zone, None)
|
||||||
|
}
|
||||||
|
|
||||||
|
case Zone.Corpse.Add(player) =>
|
||||||
|
zone.CorpseAdd(player)
|
||||||
|
|
||||||
|
case Zone.Corpse.Remove(player) =>
|
||||||
|
zone.CorpseRemove(player)
|
||||||
|
|
||||||
|
case _ => ;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
@ -12,6 +12,7 @@ import MDCContextAware.Implicits._
|
||||||
import net.psforever.objects.GlobalDefinitions._
|
import net.psforever.objects.GlobalDefinitions._
|
||||||
import services.ServiceManager.Lookup
|
import services.ServiceManager.Lookup
|
||||||
import net.psforever.objects._
|
import net.psforever.objects._
|
||||||
|
import net.psforever.objects.definition.converter.CorpseConverter
|
||||||
import net.psforever.objects.equipment._
|
import net.psforever.objects.equipment._
|
||||||
import net.psforever.objects.guid.{GUIDTask, Task, TaskResolver}
|
import net.psforever.objects.guid.{GUIDTask, Task, TaskResolver}
|
||||||
import net.psforever.objects.inventory.{Container, GridInventory, InventoryItem}
|
import net.psforever.objects.inventory.{Container, GridInventory, InventoryItem}
|
||||||
|
|
@ -67,40 +68,37 @@ class WorldSessionActor extends Actor with MDCContextAware {
|
||||||
var progressBarUpdate : Cancellable = DefaultCancellable.obj
|
var progressBarUpdate : Cancellable = DefaultCancellable.obj
|
||||||
|
|
||||||
override def postStop() = {
|
override def postStop() = {
|
||||||
if(clientKeepAlive != null)
|
clientKeepAlive.cancel()
|
||||||
clientKeepAlive.cancel()
|
localService ! Service.Leave()
|
||||||
localService ! Service.Leave()
|
vehicleService ! Service.Leave()
|
||||||
vehicleService ! Service.Leave()
|
avatarService ! Service.Leave()
|
||||||
avatarService ! Service.Leave()
|
|
||||||
LivePlayerList.Remove(sessionId) match {
|
|
||||||
case Some(tplayer) =>
|
|
||||||
tplayer.VehicleSeated match {
|
|
||||||
case Some(vehicle_guid) =>
|
|
||||||
//TODO do this at some other time
|
|
||||||
vehicleService ! VehicleServiceMessage(continent.Id, VehicleAction.KickPassenger(tplayer.GUID, 0, true, vehicle_guid))
|
|
||||||
case None => ;
|
|
||||||
}
|
|
||||||
tplayer.VehicleOwned match {
|
|
||||||
case Some(vehicle_guid) =>
|
|
||||||
continent.GUID(vehicle_guid) match {
|
|
||||||
case Some(vehicle : Vehicle) =>
|
|
||||||
vehicle.Owner = None
|
|
||||||
//TODO temporary solution; to un-own, permit driver seat to Empire access level
|
|
||||||
vehicle.PermissionGroup(10, VehicleLockState.Empire.id)
|
|
||||||
vehicleService ! VehicleServiceMessage(continent.Id, VehicleAction.SeatPermissions(tplayer.GUID, vehicle_guid, 10, VehicleLockState.Empire.id))
|
|
||||||
case _ => ;
|
|
||||||
}
|
|
||||||
case None => ;
|
|
||||||
}
|
|
||||||
|
|
||||||
if(tplayer.HasGUID) {
|
|
||||||
val guid = tplayer.GUID
|
|
||||||
avatarService ! AvatarServiceMessage(tplayer.Continent, AvatarAction.ObjectDelete(guid, guid))
|
|
||||||
taskResolver ! GUIDTask.UnregisterAvatar(tplayer)(continent.GUID)
|
|
||||||
//TODO normally, the actual player avatar persists a minute or so after the user disconnects
|
|
||||||
}
|
|
||||||
|
|
||||||
|
LivePlayerList.Remove(sessionId)
|
||||||
|
if(player != null && player.HasGUID) {
|
||||||
|
val player_guid = player.GUID
|
||||||
|
player.VehicleSeated match {
|
||||||
|
case Some(vehicle_guid) =>
|
||||||
|
//TODO do this at some other time
|
||||||
|
vehicleService ! VehicleServiceMessage(continent.Id, VehicleAction.KickPassenger(player_guid, 0, true, vehicle_guid))
|
||||||
case None => ;
|
case None => ;
|
||||||
|
}
|
||||||
|
player.VehicleOwned match {
|
||||||
|
case Some(vehicle_guid) =>
|
||||||
|
continent.GUID(vehicle_guid) match {
|
||||||
|
case Some(vehicle : Vehicle) =>
|
||||||
|
vehicle.Owner = None
|
||||||
|
//TODO temporary solution; to un-own, permit driver seat to Empire access level
|
||||||
|
vehicle.PermissionGroup(10, VehicleLockState.Empire.id)
|
||||||
|
vehicleService ! VehicleServiceMessage(continent.Id, VehicleAction.SeatPermissions(player_guid, vehicle_guid, 10, VehicleLockState.Empire.id))
|
||||||
|
case _ => ;
|
||||||
|
}
|
||||||
|
case None => ;
|
||||||
|
}
|
||||||
|
continent.Population ! Zone.Population.Release(avatar)
|
||||||
|
continent.Population ! Zone.Population.Leave(avatar)
|
||||||
|
avatarService ! AvatarServiceMessage(player.Continent, AvatarAction.ObjectDelete(player_guid, player_guid))
|
||||||
|
taskResolver ! GUIDTask.UnregisterAvatar(player)(continent.GUID)
|
||||||
|
//TODO normally, the actual player avatar persists a minute or so after the user disconnects
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -151,6 +149,7 @@ class WorldSessionActor extends Actor with MDCContextAware {
|
||||||
case game @ GamePacket(_, _, _) =>
|
case game @ GamePacket(_, _, _) =>
|
||||||
handlePktContainer(game)
|
handlePktContainer(game)
|
||||||
// temporary hack to keep the client from disconnecting
|
// temporary hack to keep the client from disconnecting
|
||||||
|
//it's been a "temporary hack" since 2016 :P
|
||||||
case PokeClient() =>
|
case PokeClient() =>
|
||||||
sendResponse(KeepAliveMessage())
|
sendResponse(KeepAliveMessage())
|
||||||
|
|
||||||
|
|
@ -281,6 +280,11 @@ class WorldSessionActor extends Actor with MDCContextAware {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
case AvatarResponse.Release(tplayer) =>
|
||||||
|
if(tplayer_guid != guid) {
|
||||||
|
turnPlayerIntoCorpse(tplayer)
|
||||||
|
}
|
||||||
|
|
||||||
case AvatarResponse.Reload(item_guid) =>
|
case AvatarResponse.Reload(item_guid) =>
|
||||||
if(tplayer_guid != guid) {
|
if(tplayer_guid != guid) {
|
||||||
sendResponse(ReloadMessage(item_guid, 1, 0))
|
sendResponse(ReloadMessage(item_guid, 1, 0))
|
||||||
|
|
@ -973,11 +977,11 @@ class WorldSessionActor extends Actor with MDCContextAware {
|
||||||
|
|
||||||
case Zone.ClientInitialization(zone) =>
|
case Zone.ClientInitialization(zone) =>
|
||||||
val continentNumber = zone.Number
|
val continentNumber = zone.Number
|
||||||
val poplist = LivePlayerList.ZonePopulation(continentNumber, _ => true)
|
val poplist = zone.Players
|
||||||
val popBO = 0 //TODO black ops test (partition)
|
val popBO = 0 //TODO black ops test (partition)
|
||||||
val popTR = poplist.count(_.Faction == PlanetSideEmpire.TR)
|
val popTR = poplist.count(_.faction == PlanetSideEmpire.TR)
|
||||||
val popNC = poplist.count(_.Faction == PlanetSideEmpire.NC)
|
val popNC = poplist.count(_.faction == PlanetSideEmpire.NC)
|
||||||
val popVS = poplist.count(_.Faction == PlanetSideEmpire.VS)
|
val popVS = poplist.count(_.faction == PlanetSideEmpire.VS)
|
||||||
|
|
||||||
zone.Buildings.foreach({ case(id, building) => initBuilding(continentNumber, id, building) })
|
zone.Buildings.foreach({ case(id, building) => initBuilding(continentNumber, id, building) })
|
||||||
sendResponse(ZonePopulationUpdateMessage(continentNumber, 414, 138, popTR, 138, popNC, 138, popVS, 138, popBO))
|
sendResponse(ZonePopulationUpdateMessage(continentNumber, 414, 138, popTR, 138, popNC, 138, popVS, 138, popBO))
|
||||||
|
|
@ -990,7 +994,16 @@ class WorldSessionActor extends Actor with MDCContextAware {
|
||||||
sendResponse(ZoneForcedCavernConnectionsMessage(continentNumber, 0))
|
sendResponse(ZoneForcedCavernConnectionsMessage(continentNumber, 0))
|
||||||
sendResponse(HotSpotUpdateMessage(continentNumber, 1, Nil)) //normally set in bulk; should be fine doing per continent
|
sendResponse(HotSpotUpdateMessage(continentNumber, 1, Nil)) //normally set in bulk; should be fine doing per continent
|
||||||
|
|
||||||
|
case Zone.Population.PlayerHasLeft(zone, None) =>
|
||||||
|
log.info(s"$avatar does not have a body on ${zone.Id}")
|
||||||
|
|
||||||
|
case Zone.Population.PlayerHasLeft(zone, Some(tplayer)) =>
|
||||||
|
if(tplayer.isAlive) {
|
||||||
|
log.info(s"$tplayer has left zone ${zone.Id}")
|
||||||
|
}
|
||||||
|
|
||||||
case InterstellarCluster.ClientInitializationComplete() =>
|
case InterstellarCluster.ClientInitializationComplete() =>
|
||||||
|
LivePlayerList.Add(sessionId, avatar)
|
||||||
//PropertyOverrideMessage
|
//PropertyOverrideMessage
|
||||||
sendResponse(PlanetsideAttributeMessage(PlanetSideGUID(0), 112, 1))
|
sendResponse(PlanetsideAttributeMessage(PlanetSideGUID(0), 112, 1))
|
||||||
sendResponse(ReplicationStreamMessage(5, Some(6), Vector(SquadListing()))) //clear squad list
|
sendResponse(ReplicationStreamMessage(5, Some(6), Vector(SquadListing()))) //clear squad list
|
||||||
|
|
@ -1002,6 +1015,7 @@ class WorldSessionActor extends Actor with MDCContextAware {
|
||||||
log.info(s"Zone $zoneId has been loaded")
|
log.info(s"Zone $zoneId has been loaded")
|
||||||
player.Continent = zoneId
|
player.Continent = zoneId
|
||||||
continent = zone
|
continent = zone
|
||||||
|
continent.Population ! Zone.Population.Join(avatar)
|
||||||
taskResolver ! RegisterNewAvatar(player)
|
taskResolver ! RegisterNewAvatar(player)
|
||||||
|
|
||||||
case NewPlayerLoaded(tplayer) =>
|
case NewPlayerLoaded(tplayer) =>
|
||||||
|
|
@ -1009,7 +1023,7 @@ class WorldSessionActor extends Actor with MDCContextAware {
|
||||||
player = tplayer
|
player = tplayer
|
||||||
//LoadMapMessage will cause the client to send back a BeginZoningMessage packet (see below)
|
//LoadMapMessage will cause the client to send back a BeginZoningMessage packet (see below)
|
||||||
sendResponse(LoadMapMessage(continent.Map.Name, continent.Id, 40100,25,true,3770441820L))
|
sendResponse(LoadMapMessage(continent.Map.Name, continent.Id, 40100,25,true,3770441820L))
|
||||||
AvatarCreate()
|
AvatarCreate() //important! the LoadMapMessage must be processed by the client before the avatar is created
|
||||||
|
|
||||||
case PlayerLoaded(tplayer) =>
|
case PlayerLoaded(tplayer) =>
|
||||||
log.info(s"Player $tplayer has been loaded")
|
log.info(s"Player $tplayer has been loaded")
|
||||||
|
|
@ -1026,7 +1040,6 @@ class WorldSessionActor extends Actor with MDCContextAware {
|
||||||
case SetCurrentAvatar(tplayer) =>
|
case SetCurrentAvatar(tplayer) =>
|
||||||
player = tplayer
|
player = tplayer
|
||||||
val guid = tplayer.GUID
|
val guid = tplayer.GUID
|
||||||
LivePlayerList.Assign(continent.Number, sessionId, guid)
|
|
||||||
sendResponse(SetCurrentAvatarMessage(guid,0,0))
|
sendResponse(SetCurrentAvatarMessage(guid,0,0))
|
||||||
|
|
||||||
(0 until DetailedCharacterData.numberOfImplantSlots(tplayer.BEP)).foreach(slot => {
|
(0 until DetailedCharacterData.numberOfImplantSlots(tplayer.BEP)).foreach(slot => {
|
||||||
|
|
@ -1256,7 +1269,6 @@ class WorldSessionActor extends Actor with MDCContextAware {
|
||||||
case CharacterRequestAction.Delete =>
|
case CharacterRequestAction.Delete =>
|
||||||
sendResponse(ActionResultMessage(false, Some(1)))
|
sendResponse(ActionResultMessage(false, Some(1)))
|
||||||
case CharacterRequestAction.Select =>
|
case CharacterRequestAction.Select =>
|
||||||
LivePlayerList.Add(sessionId, player)
|
|
||||||
//TODO check if can spawn on last continent/location from player?
|
//TODO check if can spawn on last continent/location from player?
|
||||||
//TODO if yes, get continent guid accessors
|
//TODO if yes, get continent guid accessors
|
||||||
//TODO if no, get sanctuary guid accessors and reset the player's expectations
|
//TODO if no, get sanctuary guid accessors and reset the player's expectations
|
||||||
|
|
@ -1296,20 +1308,18 @@ class WorldSessionActor extends Actor with MDCContextAware {
|
||||||
)
|
)
|
||||||
})
|
})
|
||||||
//load active players in zone
|
//load active players in zone
|
||||||
LivePlayerList.ZonePopulation(continent.Number, _ => true).foreach(char => {
|
continent.LivePlayers.filterNot(_.GUID == player.GUID).foreach(char => {
|
||||||
sendResponse(
|
sendResponse(
|
||||||
ObjectCreateMessage(ObjectClass.avatar, char.GUID, char.Definition.Packet.ConstructorData(char).get)
|
ObjectCreateMessage(ObjectClass.avatar, char.GUID, char.Definition.Packet.ConstructorData(char).get)
|
||||||
)
|
)
|
||||||
})
|
})
|
||||||
|
//load corpses in zone
|
||||||
|
continent.Corpses.foreach( turnPlayerIntoCorpse(_) )
|
||||||
//load active vehicles in zone
|
//load active vehicles in zone
|
||||||
continent.Vehicles.foreach(vehicle => {
|
continent.Vehicles.foreach(vehicle => {
|
||||||
val definition = vehicle.Definition
|
val definition = vehicle.Definition
|
||||||
sendResponse(
|
sendResponse(
|
||||||
ObjectCreateMessage(
|
ObjectCreateMessage(definition.ObjectId, vehicle.GUID, definition.Packet.ConstructorData(vehicle).get)
|
||||||
definition.ObjectId,
|
|
||||||
vehicle.GUID,
|
|
||||||
definition.Packet.ConstructorData(vehicle).get
|
|
||||||
)
|
|
||||||
)
|
)
|
||||||
//seat vehicle occupants
|
//seat vehicle occupants
|
||||||
vehicle.Definition.MountPoints.values.foreach(seat_num => {
|
vehicle.Definition.MountPoints.values.foreach(seat_num => {
|
||||||
|
|
@ -1362,7 +1372,7 @@ class WorldSessionActor extends Actor with MDCContextAware {
|
||||||
self ! SetCurrentAvatar(player)
|
self ! SetCurrentAvatar(player)
|
||||||
|
|
||||||
case msg @ PlayerStateMessageUpstream(avatar_guid, pos, vel, yaw, pitch, yaw_upper, seq_time, unk3, is_crouching, is_jumping, unk4, is_cloaking, unk5, unk6) =>
|
case msg @ PlayerStateMessageUpstream(avatar_guid, pos, vel, yaw, pitch, yaw_upper, seq_time, unk3, is_crouching, is_jumping, unk4, is_cloaking, unk5, unk6) =>
|
||||||
if(!player.isAlive) {
|
if(player.isAlive) {
|
||||||
player.Position = pos
|
player.Position = pos
|
||||||
player.Velocity = vel
|
player.Velocity = vel
|
||||||
player.Orientation = Vector3(player.Orientation.x, pitch, yaw)
|
player.Orientation = Vector3(player.Orientation.x, pitch, yaw)
|
||||||
|
|
@ -1434,10 +1444,14 @@ class WorldSessionActor extends Actor with MDCContextAware {
|
||||||
|
|
||||||
case msg @ ReleaseAvatarRequestMessage() =>
|
case msg @ ReleaseAvatarRequestMessage() =>
|
||||||
log.info(s"ReleaseAvatarRequest: ${player.GUID} on ${continent.Id} has released")
|
log.info(s"ReleaseAvatarRequest: ${player.GUID} on ${continent.Id} has released")
|
||||||
|
//TODO is it easier to delete the player, then re-create them as a corpse?
|
||||||
player.Release
|
player.Release
|
||||||
|
continent.Population ! Zone.Population.Release(avatar)
|
||||||
|
continent.Population ! Zone.Corpse.Add(player)
|
||||||
val knife = player.Slot(4).Equipment.get
|
val knife = player.Slot(4).Equipment.get
|
||||||
taskResolver ! RemoveEquipmentFromSlot(player, knife, 4)
|
taskResolver ! RemoveEquipmentFromSlot(player, knife, 4)
|
||||||
sendResponse(PlanetsideAttributeMessage(player.GUID, 6, 1))
|
turnPlayerIntoCorpse(player)
|
||||||
|
avatarService ! AvatarServiceMessage(continent.Id, AvatarAction.Release(player, continent))
|
||||||
sendResponse(AvatarDeadStateMessage(DeadState.Release, 0, 0, player.Position, 2, true))
|
sendResponse(AvatarDeadStateMessage(DeadState.Release, 0, 0, player.Position, 2, true))
|
||||||
|
|
||||||
case msg @ SpawnRequestMessage(u1, u2, u3, u4, u5) =>
|
case msg @ SpawnRequestMessage(u1, u2, u3, u4, u5) =>
|
||||||
|
|
@ -1449,7 +1463,7 @@ class WorldSessionActor extends Actor with MDCContextAware {
|
||||||
val tplayer = SpawnRequest(player) //new player
|
val tplayer = SpawnRequest(player) //new player
|
||||||
tplayer.Position = tube.Position
|
tplayer.Position = tube.Position
|
||||||
tplayer.Orientation = tube.Orientation
|
tplayer.Orientation = tube.Orientation
|
||||||
log.info(s"SpawnRequestMessage: new player will spawn in ${building.Id} @ tube ${tube.GUID.guid}")
|
log.info(s"SpawnRequestMessage: new player will spawn in ${building.Id} @ ${tube.GUID.guid}")
|
||||||
sendResponse(AvatarDeadStateMessage(DeadState.RespawnTime, 10000, 10000, Vector3.Zero, 2, true))
|
sendResponse(AvatarDeadStateMessage(DeadState.RespawnTime, 10000, 10000, Vector3.Zero, 2, true))
|
||||||
import scala.concurrent.duration._
|
import scala.concurrent.duration._
|
||||||
import scala.concurrent.ExecutionContext.Implicits.global
|
import scala.concurrent.ExecutionContext.Implicits.global
|
||||||
|
|
@ -1507,7 +1521,9 @@ class WorldSessionActor extends Actor with MDCContextAware {
|
||||||
player.Die
|
player.Die
|
||||||
sendResponse(PlanetsideAttributeMessage(player_guid, 0, 0))
|
sendResponse(PlanetsideAttributeMessage(player_guid, 0, 0))
|
||||||
sendResponse(PlanetsideAttributeMessage(player_guid, 2, 0))
|
sendResponse(PlanetsideAttributeMessage(player_guid, 2, 0))
|
||||||
sendResponse(DestroyMessage(player_guid, player_guid, PlanetSideGUID(0), pos))
|
avatarService ! AvatarServiceMessage(continent.Id, AvatarAction.PlanetsideAttribute(player_guid, 0, 0))
|
||||||
|
avatarService ! AvatarServiceMessage(continent.Id, AvatarAction.PlanetsideAttribute(player_guid, 2, 0))
|
||||||
|
sendResponse(DestroyMessage(player_guid, player_guid, PlanetSideGUID(0), pos)) //how many players get this message?
|
||||||
sendResponse(AvatarDeadStateMessage(DeadState.Dead, 300000, 300000, pos, 2, true))
|
sendResponse(AvatarDeadStateMessage(DeadState.Dead, 300000, 300000, pos, 2, true))
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -1521,8 +1537,8 @@ class WorldSessionActor extends Actor with MDCContextAware {
|
||||||
sendResponse(DropSession(sessionId, "user quit"))
|
sendResponse(DropSession(sessionId, "user quit"))
|
||||||
}
|
}
|
||||||
|
|
||||||
if(contents.trim.equals("!loc")) { //dev hack; consider bang-commands to complement slash-commands
|
if(contents.trim.equals("!loc")) { //dev hack; consider bang-commands to complement slash-commands in future
|
||||||
echoContents = s"pos=${player.Position.x}, ${player.Position.y}, ${player.Position.z}; ori=${player.Orientation.x}, ${player.Orientation.y}, ${player.Orientation.z}"
|
echoContents = s"zone=${continent.Id} pos=${player.Position.x},${player.Position.y},${player.Position.z}; ori=${player.Orientation.x},${player.Orientation.y},${player.Orientation.z}"
|
||||||
log.info(echoContents)
|
log.info(echoContents)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -3315,6 +3331,7 @@ class WorldSessionActor extends Actor with MDCContextAware {
|
||||||
val dcdata = packet.DetailedConstructorData(player).get
|
val dcdata = packet.DetailedConstructorData(player).get
|
||||||
sendResponse(ObjectCreateDetailedMessage(ObjectClass.avatar, player.GUID, dcdata))
|
sendResponse(ObjectCreateDetailedMessage(ObjectClass.avatar, player.GUID, dcdata))
|
||||||
avatarService ! AvatarServiceMessage(player.Continent, AvatarAction.LoadPlayer(player.GUID, packet.ConstructorData(player).get))
|
avatarService ! AvatarServiceMessage(player.Continent, AvatarAction.LoadPlayer(player.GUID, packet.ConstructorData(player).get))
|
||||||
|
continent.Population ! Zone.Population.Spawn(avatar, player)
|
||||||
log.debug(s"ObjectCreateDetailedMessage: $dcdata")
|
log.debug(s"ObjectCreateDetailedMessage: $dcdata")
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -3335,6 +3352,13 @@ class WorldSessionActor extends Actor with MDCContextAware {
|
||||||
obj
|
obj
|
||||||
}
|
}
|
||||||
|
|
||||||
|
def turnPlayerIntoCorpse(tplayer : Player) : Unit = {
|
||||||
|
//sendResponse(PlanetsideAttributeMessage(tplayer.GUID, 6, 1))
|
||||||
|
sendResponse(
|
||||||
|
ObjectCreateDetailedMessage(ObjectClass.avatar, tplayer.GUID, CorpseConverter.converter.DetailedConstructorData(tplayer).get)
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
def failWithError(error : String) = {
|
def failWithError(error : String) = {
|
||||||
log.error(error)
|
log.error(error)
|
||||||
sendResponse(ConnectionClose())
|
sendResponse(ConnectionClose())
|
||||||
|
|
|
||||||
|
|
@ -1,7 +1,9 @@
|
||||||
// Copyright (c) 2017 PSForever
|
// Copyright (c) 2017 PSForever
|
||||||
package services.avatar
|
package services.avatar
|
||||||
|
|
||||||
|
import net.psforever.objects.Player
|
||||||
import net.psforever.objects.equipment.Equipment
|
import net.psforever.objects.equipment.Equipment
|
||||||
|
import net.psforever.objects.zones.Zone
|
||||||
import net.psforever.packet.game.{PlanetSideGUID, PlayerStateMessageUpstream}
|
import net.psforever.packet.game.{PlanetSideGUID, PlayerStateMessageUpstream}
|
||||||
import net.psforever.packet.game.objectcreate.ConstructorData
|
import net.psforever.packet.game.objectcreate.ConstructorData
|
||||||
import net.psforever.types.{ExoSuitType, Vector3}
|
import net.psforever.types.{ExoSuitType, Vector3}
|
||||||
|
|
@ -24,6 +26,7 @@ object AvatarAction {
|
||||||
final case class ObjectHeld(player_guid : PlanetSideGUID, slot : Int) extends Action
|
final case class ObjectHeld(player_guid : PlanetSideGUID, slot : Int) extends Action
|
||||||
final case class PlanetsideAttribute(player_guid : PlanetSideGUID, attribute_type : Int, attribute_value : Long) extends Action
|
final case class PlanetsideAttribute(player_guid : PlanetSideGUID, attribute_type : Int, attribute_value : Long) extends Action
|
||||||
final case class PlayerState(player_guid : PlanetSideGUID, msg : PlayerStateMessageUpstream, spectator : Boolean, weaponInHand : Boolean) extends Action
|
final case class PlayerState(player_guid : PlanetSideGUID, msg : PlayerStateMessageUpstream, spectator : Boolean, weaponInHand : Boolean) extends Action
|
||||||
|
final case class Release(player : Player, zone : Zone) extends Action
|
||||||
final case class Reload(player_guid : PlanetSideGUID, weapon_guid : PlanetSideGUID) extends Action
|
final case class Reload(player_guid : PlanetSideGUID, weapon_guid : PlanetSideGUID) extends Action
|
||||||
final case class WeaponDryFire(player_guid : PlanetSideGUID, weapon_guid : PlanetSideGUID) extends Action
|
final case class WeaponDryFire(player_guid : PlanetSideGUID, weapon_guid : PlanetSideGUID) extends Action
|
||||||
// final case class PlayerStateShift(killer : PlanetSideGUID, victim : PlanetSideGUID) extends Action
|
// final case class PlayerStateShift(killer : PlanetSideGUID, victim : PlanetSideGUID) extends Action
|
||||||
|
|
|
||||||
|
|
@ -1,6 +1,7 @@
|
||||||
// Copyright (c) 2017 PSForever
|
// Copyright (c) 2017 PSForever
|
||||||
package services.avatar
|
package services.avatar
|
||||||
|
|
||||||
|
import net.psforever.objects.Player
|
||||||
import net.psforever.objects.equipment.Equipment
|
import net.psforever.objects.equipment.Equipment
|
||||||
import net.psforever.packet.game.{PlanetSideGUID, PlayerStateMessageUpstream}
|
import net.psforever.packet.game.{PlanetSideGUID, PlayerStateMessageUpstream}
|
||||||
import net.psforever.packet.game.objectcreate.ConstructorData
|
import net.psforever.packet.game.objectcreate.ConstructorData
|
||||||
|
|
@ -24,6 +25,7 @@ object AvatarResponse {
|
||||||
final case class ObjectHeld(slot : Int) extends Response
|
final case class ObjectHeld(slot : Int) extends Response
|
||||||
final case class PlanetsideAttribute(attribute_type : Int, attribute_value : Long) extends Response
|
final case class PlanetsideAttribute(attribute_type : Int, attribute_value : Long) extends Response
|
||||||
final case class PlayerState(msg : PlayerStateMessageUpstream, spectator : Boolean, weaponInHand : Boolean) extends Response
|
final case class PlayerState(msg : PlayerStateMessageUpstream, spectator : Boolean, weaponInHand : Boolean) extends Response
|
||||||
|
final case class Release(player : Player) extends Response
|
||||||
final case class Reload(weapon_guid : PlanetSideGUID) extends Response
|
final case class Reload(weapon_guid : PlanetSideGUID) extends Response
|
||||||
final case class WeaponDryFire(weapon_guid : PlanetSideGUID) extends Response
|
final case class WeaponDryFire(weapon_guid : PlanetSideGUID) extends Response
|
||||||
// final case class PlayerStateShift(itemID : PlanetSideGUID) extends Response
|
// final case class PlayerStateShift(itemID : PlanetSideGUID) extends Response
|
||||||
|
|
|
||||||
|
|
@ -1,11 +1,14 @@
|
||||||
// Copyright (c) 2017 PSForever
|
// Copyright (c) 2017 PSForever
|
||||||
package services.avatar
|
package services.avatar
|
||||||
|
|
||||||
import akka.actor.Actor
|
import akka.actor.{Actor, ActorRef, Props}
|
||||||
|
import services.avatar.support.UndertakerActor
|
||||||
import services.{GenericEventBus, Service}
|
import services.{GenericEventBus, Service}
|
||||||
|
|
||||||
class AvatarService extends Actor {
|
class AvatarService extends Actor {
|
||||||
//import AvatarServiceResponse._
|
private val undertaker : ActorRef = context.actorOf(Props[UndertakerActor], "corpse-removal-agent")
|
||||||
|
undertaker ! "startup"
|
||||||
|
|
||||||
private [this] val log = org.log4s.getLogger
|
private [this] val log = org.log4s.getLogger
|
||||||
|
|
||||||
override def preStart = {
|
override def preStart = {
|
||||||
|
|
@ -87,6 +90,11 @@ class AvatarService extends Actor {
|
||||||
AvatarEvents.publish(
|
AvatarEvents.publish(
|
||||||
AvatarServiceResponse(s"/$forChannel/Avatar", guid, AvatarResponse.PlayerState(msg, spectator, weapon))
|
AvatarServiceResponse(s"/$forChannel/Avatar", guid, AvatarResponse.PlayerState(msg, spectator, weapon))
|
||||||
)
|
)
|
||||||
|
case AvatarAction.Release(player, zone) =>
|
||||||
|
undertaker ! UndertakerActor.AddCorpse(player, zone)
|
||||||
|
AvatarEvents.publish(
|
||||||
|
AvatarServiceResponse(s"/$forChannel/Avatar", player.GUID, AvatarResponse.Release(player))
|
||||||
|
)
|
||||||
case AvatarAction.Reload(player_guid, weapon_guid) =>
|
case AvatarAction.Reload(player_guid, weapon_guid) =>
|
||||||
AvatarEvents.publish(
|
AvatarEvents.publish(
|
||||||
AvatarServiceResponse(s"/$forChannel/Avatar", player_guid, AvatarResponse.Reload(weapon_guid))
|
AvatarServiceResponse(s"/$forChannel/Avatar", player_guid, AvatarResponse.Reload(weapon_guid))
|
||||||
|
|
|
||||||
|
|
@ -0,0 +1,145 @@
|
||||||
|
// Copyright (c) 2017 PSForever
|
||||||
|
package services.avatar.support
|
||||||
|
|
||||||
|
import akka.actor.{Actor, ActorRef, Cancellable}
|
||||||
|
import net.psforever.objects.guid.TaskResolver
|
||||||
|
import net.psforever.objects.{DefaultCancellable, Player}
|
||||||
|
import net.psforever.objects.zones.Zone
|
||||||
|
import services.{Service, ServiceManager}
|
||||||
|
import services.ServiceManager.Lookup
|
||||||
|
import services.avatar.{AvatarAction, AvatarServiceMessage}
|
||||||
|
|
||||||
|
import scala.annotation.tailrec
|
||||||
|
import scala.concurrent.duration._
|
||||||
|
|
||||||
|
class UndertakerActor extends Actor {
|
||||||
|
private var burial : Cancellable = DefaultCancellable.obj
|
||||||
|
|
||||||
|
private var corpses : List[UndertakerActor.Entry] = List()
|
||||||
|
|
||||||
|
private var taskResolver : ActorRef = Actor.noSender
|
||||||
|
|
||||||
|
private[this] val log = org.log4s.getLogger("Cart Master")
|
||||||
|
|
||||||
|
override def postStop() = {
|
||||||
|
corpses.foreach { BurialTask }
|
||||||
|
}
|
||||||
|
|
||||||
|
def receive : Receive = {
|
||||||
|
case "startup" =>
|
||||||
|
ServiceManager.serviceManager ! Lookup("taskResolver") //ask for a resolver to deal with the GUID system
|
||||||
|
|
||||||
|
case ServiceManager.LookupResult("taskResolver", endpoint) =>
|
||||||
|
taskResolver = endpoint
|
||||||
|
context.become(Processing)
|
||||||
|
|
||||||
|
case _ => ;
|
||||||
|
}
|
||||||
|
|
||||||
|
def Processing : Receive = {
|
||||||
|
case UndertakerActor.AddCorpse(corpse, zone, time) =>
|
||||||
|
if(corpse.isBackpack) {
|
||||||
|
corpses = corpses :+ UndertakerActor.Entry(corpse, zone, time)
|
||||||
|
if(corpses.size == 1) { //we were the only entry so the event must be started from scratch
|
||||||
|
import scala.concurrent.ExecutionContext.Implicits.global
|
||||||
|
burial = context.system.scheduler.scheduleOnce(UndertakerActor.timeout, self, UndertakerActor.Dispose())
|
||||||
|
}
|
||||||
|
}
|
||||||
|
else {
|
||||||
|
log.warn(s"he's not dead yet - $corpse")
|
||||||
|
}
|
||||||
|
|
||||||
|
case UndertakerActor.Dispose() =>
|
||||||
|
burial.cancel
|
||||||
|
val now : Long = System.nanoTime
|
||||||
|
val (buried, rotting) = PartitionEntries(corpses, now)
|
||||||
|
corpses = rotting
|
||||||
|
buried.foreach { BurialTask }
|
||||||
|
if(rotting.nonEmpty) {
|
||||||
|
val short_timeout : FiniteDuration = math.max(1, UndertakerActor.timeout_time - (now - rotting.head.time)) nanoseconds
|
||||||
|
import scala.concurrent.ExecutionContext.Implicits.global
|
||||||
|
burial = context.system.scheduler.scheduleOnce(short_timeout, self, UndertakerActor.Dispose())
|
||||||
|
}
|
||||||
|
|
||||||
|
case UndertakerActor.FailureToWork(target, zone, ex) =>
|
||||||
|
log.error(s"$target failed to be properly cleaned up from $zone - $ex")
|
||||||
|
|
||||||
|
case _ => ;
|
||||||
|
}
|
||||||
|
|
||||||
|
def BurialTask(entry : UndertakerActor.Entry) : Unit = {
|
||||||
|
val target = entry.corpse
|
||||||
|
val zone = entry.zone
|
||||||
|
entry.zone.Population ! Zone.Corpse.Remove(target)
|
||||||
|
context.parent ! AvatarServiceMessage(zone.Id, AvatarAction.ObjectDelete(Service.defaultPlayerGUID, target.GUID)) //call up to the main event system
|
||||||
|
taskResolver ! BurialTask(target, zone)
|
||||||
|
}
|
||||||
|
|
||||||
|
def BurialTask(corpse : Player, zone : Zone) : TaskResolver.GiveTask = {
|
||||||
|
import net.psforever.objects.guid.{GUIDTask, Task}
|
||||||
|
TaskResolver.GiveTask (
|
||||||
|
new Task() {
|
||||||
|
private val localCorpse = corpse
|
||||||
|
private val localZone = zone
|
||||||
|
private val localAnnounce = self
|
||||||
|
|
||||||
|
override def isComplete : Task.Resolution.Value = Task.Resolution.Success
|
||||||
|
|
||||||
|
def Execute(resolver : ActorRef) : Unit = {
|
||||||
|
resolver ! scala.util.Success(this)
|
||||||
|
}
|
||||||
|
|
||||||
|
override def onFailure(ex : Throwable): Unit = {
|
||||||
|
localAnnounce ! UndertakerActor.FailureToWork(localCorpse, localZone, ex)
|
||||||
|
}
|
||||||
|
}, List(GUIDTask.UnregisterAvatar(corpse)(zone.GUID))
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
private def PartitionEntries(list : List[UndertakerActor.Entry], now : Long) : (List[UndertakerActor.Entry], List[UndertakerActor.Entry]) = {
|
||||||
|
val n : Int = recursivePartitionEntries(list.iterator, now, UndertakerActor.timeout_time)
|
||||||
|
(list.take(n), list.drop(n)) //take and drop so to always return new lists
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Mark the index where the `List` of elements can be divided into two:
|
||||||
|
* a `List` of elements that have exceeded the time limit,
|
||||||
|
* and a `List` of elements that still satisfy the time limit.
|
||||||
|
* @param iter the `Iterator` of entries to divide
|
||||||
|
* @param now the time right now (in nanoseconds)
|
||||||
|
* @param index a persistent record of the index where list division should occur;
|
||||||
|
* defaults to 0
|
||||||
|
* @return the index where division will occur
|
||||||
|
*/
|
||||||
|
@tailrec private def recursivePartitionEntries(iter : Iterator[UndertakerActor.Entry], now : Long, duration : Long, index : Int = 0) : Int = {
|
||||||
|
if(!iter.hasNext) {
|
||||||
|
index
|
||||||
|
}
|
||||||
|
else {
|
||||||
|
val entry = iter.next()
|
||||||
|
if(now - entry.time >= duration) {
|
||||||
|
recursivePartitionEntries(iter, now, duration, index + 1)
|
||||||
|
}
|
||||||
|
else {
|
||||||
|
index
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
object UndertakerActor {
|
||||||
|
/** A `Long` for calculation simplicity */
|
||||||
|
private final val timeout_time : Long = 180000000000L //3 min (180s)
|
||||||
|
/** A `FiniteDuration` for `Executor` simplicity */
|
||||||
|
private final val timeout : FiniteDuration = timeout_time nanoseconds
|
||||||
|
|
||||||
|
final case class AddCorpse(corpse : Player, zone : Zone, time : Long = System.nanoTime())
|
||||||
|
|
||||||
|
final case class Entry(corpse : Player, zone : Zone, time : Long = System.nanoTime())
|
||||||
|
|
||||||
|
final case class FailureToWork(corpse : Player, zone : Zone, ex : Throwable)
|
||||||
|
|
||||||
|
final case class Dispose()
|
||||||
|
|
||||||
|
//TODO design mass disposal cases
|
||||||
|
}
|
||||||
Loading…
Add table
Add a link
Reference in a new issue