From 20b7726653d9c09dbe73bcfb45eef90e7f7657a3 Mon Sep 17 00:00:00 2001 From: FateJH Date: Wed, 21 Mar 2018 08:50:42 -0400 Subject: [PATCH] Stripped down LivePlayerList functionality, moving player lists onto the appropriate corresponding zones. Corpses are now created, stored, and deleted. --- .../psforever/objects/LivePlayerList.scala | 228 ++---------------- .../converter/CorpseConverter.scala | 124 ++++++++++ .../converter/VehicleConverter.scala | 2 +- .../net/psforever/objects/zones/Zone.scala | 105 +++++++- .../objects/zones/ZonePopulationActor.scala | 43 ++++ .../src/main/scala/WorldSessionActor.scala | 126 ++++++---- .../scala/services/avatar/AvatarAction.scala | 3 + .../services/avatar/AvatarResponse.scala | 2 + .../scala/services/avatar/AvatarService.scala | 12 +- .../avatar/support/UndertakerActor.scala | 145 +++++++++++ 10 files changed, 526 insertions(+), 264 deletions(-) create mode 100644 common/src/main/scala/net/psforever/objects/definition/converter/CorpseConverter.scala create mode 100644 common/src/main/scala/net/psforever/objects/zones/ZonePopulationActor.scala create mode 100644 pslogin/src/main/scala/services/avatar/support/UndertakerActor.scala diff --git a/common/src/main/scala/net/psforever/objects/LivePlayerList.scala b/common/src/main/scala/net/psforever/objects/LivePlayerList.scala index c0e398568..af1ab40ed 100644 --- a/common/src/main/scala/net/psforever/objects/LivePlayerList.scala +++ b/common/src/main/scala/net/psforever/objects/LivePlayerList.scala @@ -1,9 +1,6 @@ // Copyright (c) 2017 PSForever package net.psforever.objects -import net.psforever.packet.game.PlanetSideGUID - -import scala.annotation.tailrec import scala.collection.concurrent.{Map, TrieMap} /** @@ -12,157 +9,42 @@ import scala.collection.concurrent.{Map, TrieMap} */ private class LivePlayerList { /** key - the session id; value - a `Player` object */ - private val sessionMap : Map[Long, Player] = new TrieMap[Long, Player] - /** 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]) + private val sessionMap : Map[Long, Avatar] = new TrieMap[Long, Avatar] - def WorldPopulation(predicate : ((_, Player)) => Boolean) : List[Player] = { + def WorldPopulation(predicate : ((_, Avatar)) => Boolean) : List[Avatar] = { sessionMap.filter(predicate).values.toList } - def ZonePopulation(zone : Int, predicate : ((_, Player)) => Boolean) : List[Player] = { - zoneMap.lift(zone) match { - case Some(map) => - val list = map.values.toList - sessionMap.filter({ case ((sess, _)) => list.contains(sess) }).filter(predicate).values.toList + def Add(sessionId : Long, avatar : Avatar) : Boolean = { + sessionMap.values.find(char => char.equals(avatar)) match { case None => - Nil - } - } - - def Add(sessionId : Long, player : Player) : Boolean = { - sessionMap.values.find(char => char.equals(player)) match { - case None => - sessionMap.putIfAbsent(sessionId, player).isEmpty + sessionMap.putIfAbsent(sessionId, avatar).isEmpty case Some(_) => false } } - def Remove(sessionId : Long) : Option[Player] = { - sessionMap.remove(sessionId) match { - case Some(char) => - zoneMap.foreach(zone => { - recursiveRemoveSession(zone.iterator, sessionId) match { - case Some(guid) => - zone.remove(guid) - case None => ; - } - }) - Some(char) - case None => - None - } + def Remove(sessionId : Long) : Option[Avatar] = { + sessionMap.remove(sessionId) } - @tailrec private def recursiveRemoveSession(iter : Iterator[(Int, Long)], sessionId : Long) : Option[Int] = { - 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] = { + def Shutdown : List[Avatar] = { val list = sessionMap.values.toList sessionMap.clear - zoneMap.foreach(map => map.clear()) list } } /** * 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`. - * 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.
+ * The mapping system is tightly coupled between the `Avatar` class and to an instance of `WorldSessionActor`. *
* Use:
- * 1) When a users logs in during `WorldSessionActor`, associate that user's session id and the character.
- *        `LivePlayerList.Add(session, player)`
- * 2) When that user's chosen character is declared his avatar using `SetCurrentAvatarMessage`, - * also associate the user's session with their current GUID.
- *        `LivePlayerList.Assign(zone, session, guid)`
- * 3) Repeat the previous step for as many times the user's GUID changes, especially during the aforementioned condition.
- * 4a) In between the previous two steps, a user's character may be referenced by their current GUID.
- *        `LivePlayerList.Get(zone, guid)`
- * 4b) Also in between those same previous steps, a range of characters may be queried based on provided statistics.
+ * 1) When a users logs in during `WorldSessionActor`, associate that user's session id and their character (avatar).
+ *        `LivePlayerList.Add(session, avatar)`
+ * 2) In between the previous two steps, a range of characters may be queried based on provided statistics.
*        `LivePlayerList.WorldPopulation(...)`
- *        `LivePlayerList.ZonePopulation(zone, ...)`
- * 5) When the user navigates away from a region completely, their entry is forgotten.
- *        `LivePlayerList.Drop(zone, guid)`
- * 6) When the user leaves the game entirely, his character's entries are removed from the mappings.
+ * 3) When the user leaves the game entirely, his character's entry is removed from the mapping.
*        `LivePlayerList.Remove(session)` */ object LivePlayerList { @@ -179,100 +61,28 @@ object LivePlayerList { * @param predicate the conditions for filtering the live `Player`s * @return a list of users's `Player`s that fit the criteria */ - def WorldPopulation(predicate : ((_, Player)) => Boolean) : List[Player] = Instance.WorldPopulation(predicate) - - /** - * Given some criteria, examine the mapping of user characters for a zone and find the ones that fulfill the requirements.
- *
- * 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) + def WorldPopulation(predicate : ((_, Avatar)) => Boolean) : List[Avatar] = Instance.WorldPopulation(predicate) /** * 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. * @param sessionId the session - * @param player the character + * @param avatar the character * @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. - * The player no longer counts as "online." - * This function cleans up __all__ associations - those created by `Add`, and those created by `Assign`. + * The character no longer counts as "online." * @param sessionId the session * @return any character that was afffected by the mapping removal */ - def Remove(sessionId : Long) : Option[Player] = 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) + def Remove(sessionId : Long) : Option[Avatar] = Instance.Remove(sessionId) /** * Hastily remove all mappings and ids. * @return an unsorted list of the characters that were still online */ - def Shutdown : List[Player] = Instance.Shutdown + def Shutdown : List[Avatar] = Instance.Shutdown } diff --git a/common/src/main/scala/net/psforever/objects/definition/converter/CorpseConverter.scala b/common/src/main/scala/net/psforever/objects/definition/converter/CorpseConverter.scala new file mode 100644 index 000000000..73699a10b --- /dev/null +++ b/common/src/main/scala/net/psforever/objects/definition/converter/CorpseConverter.scala @@ -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 +} diff --git a/common/src/main/scala/net/psforever/objects/definition/converter/VehicleConverter.scala b/common/src/main/scala/net/psforever/objects/definition/converter/VehicleConverter.scala index 5a0ba6300..81f3f2b65 100644 --- a/common/src/main/scala/net/psforever/objects/definition/converter/VehicleConverter.scala +++ b/common/src/main/scala/net/psforever/objects/definition/converter/VehicleConverter.scala @@ -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? ), 0, - obj.Health / obj.MaxHealth * 255, //TODO not precise + 255 * obj.Health / obj.MaxHealth, //TODO not precise false, false, obj.DeploymentState, false, diff --git a/common/src/main/scala/net/psforever/objects/zones/Zone.scala b/common/src/main/scala/net/psforever/objects/zones/Zone.scala index 859aafa2d..7281891d7 100644 --- a/common/src/main/scala/net/psforever/objects/zones/Zone.scala +++ b/common/src/main/scala/net/psforever/objects/zones/Zone.scala @@ -3,7 +3,7 @@ package net.psforever.objects.zones import akka.actor.{ActorContext, ActorRef, Props} 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.guid.NumberPoolHub import net.psforever.objects.guid.actor.UniqueNumberSystem @@ -14,6 +14,7 @@ import net.psforever.packet.game.PlanetSideGUID import net.psforever.types.Vector3 import scala.annotation.tailrec +import scala.collection.concurrent.TrieMap import scala.collection.mutable.ListBuffer import scala.collection.immutable.{Map => PairMap} @@ -54,6 +55,12 @@ class Zone(private val zoneId : String, zoneMap : ZoneMap, zoneNumber : Int) { private var vehicles : List[Vehicle] = List[Vehicle]() /** */ 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] @@ -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") ground = context.actorOf(Props(classOf[ZoneGroundActor], equipmentOnGround), s"$Id-ground") 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 }) MakeBuildings(context) @@ -179,6 +187,12 @@ class Zone(private val zoneId : String, zoneMap : ZoneMap, zoneNumber : Int) { 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] = { vehicles = vehicles :+ vehicle 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. * @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 Population : ActorRef = population + def Buildings : Map[Int, Building] = buildings def Building(id : Int) : Option[Building] = { @@ -266,6 +354,21 @@ object Zone { */ 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. * @param item the piece of `Equipment` diff --git a/common/src/main/scala/net/psforever/objects/zones/ZonePopulationActor.scala b/common/src/main/scala/net/psforever/objects/zones/ZonePopulationActor.scala new file mode 100644 index 000000000..6787166c4 --- /dev/null +++ b/common/src/main/scala/net/psforever/objects/zones/ZonePopulationActor.scala @@ -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 _ => ; + } +} diff --git a/pslogin/src/main/scala/WorldSessionActor.scala b/pslogin/src/main/scala/WorldSessionActor.scala index fc24d57e2..7ce17fe86 100644 --- a/pslogin/src/main/scala/WorldSessionActor.scala +++ b/pslogin/src/main/scala/WorldSessionActor.scala @@ -12,6 +12,7 @@ import MDCContextAware.Implicits._ import net.psforever.objects.GlobalDefinitions._ import services.ServiceManager.Lookup import net.psforever.objects._ +import net.psforever.objects.definition.converter.CorpseConverter import net.psforever.objects.equipment._ import net.psforever.objects.guid.{GUIDTask, Task, TaskResolver} import net.psforever.objects.inventory.{Container, GridInventory, InventoryItem} @@ -67,40 +68,37 @@ class WorldSessionActor extends Actor with MDCContextAware { var progressBarUpdate : Cancellable = DefaultCancellable.obj override def postStop() = { - if(clientKeepAlive != null) - clientKeepAlive.cancel() - localService ! Service.Leave() - vehicleService ! 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 - } + clientKeepAlive.cancel() + localService ! Service.Leave() + vehicleService ! Service.Leave() + avatarService ! Service.Leave() + 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 => ; + } + 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(_, _, _) => handlePktContainer(game) // temporary hack to keep the client from disconnecting + //it's been a "temporary hack" since 2016 :P case PokeClient() => 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) => if(tplayer_guid != guid) { sendResponse(ReloadMessage(item_guid, 1, 0)) @@ -973,11 +977,11 @@ class WorldSessionActor extends Actor with MDCContextAware { case Zone.ClientInitialization(zone) => val continentNumber = zone.Number - val poplist = LivePlayerList.ZonePopulation(continentNumber, _ => true) + val poplist = zone.Players val popBO = 0 //TODO black ops test (partition) - val popTR = poplist.count(_.Faction == PlanetSideEmpire.TR) - val popNC = poplist.count(_.Faction == PlanetSideEmpire.NC) - val popVS = poplist.count(_.Faction == PlanetSideEmpire.VS) + val popTR = poplist.count(_.faction == PlanetSideEmpire.TR) + val popNC = poplist.count(_.faction == PlanetSideEmpire.NC) + val popVS = poplist.count(_.faction == PlanetSideEmpire.VS) zone.Buildings.foreach({ case(id, building) => initBuilding(continentNumber, id, building) }) 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(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() => + LivePlayerList.Add(sessionId, avatar) //PropertyOverrideMessage sendResponse(PlanetsideAttributeMessage(PlanetSideGUID(0), 112, 1)) 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") player.Continent = zoneId continent = zone + continent.Population ! Zone.Population.Join(avatar) taskResolver ! RegisterNewAvatar(player) case NewPlayerLoaded(tplayer) => @@ -1009,7 +1023,7 @@ class WorldSessionActor extends Actor with MDCContextAware { player = tplayer //LoadMapMessage will cause the client to send back a BeginZoningMessage packet (see below) 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) => log.info(s"Player $tplayer has been loaded") @@ -1026,7 +1040,6 @@ class WorldSessionActor extends Actor with MDCContextAware { case SetCurrentAvatar(tplayer) => player = tplayer val guid = tplayer.GUID - LivePlayerList.Assign(continent.Number, sessionId, guid) sendResponse(SetCurrentAvatarMessage(guid,0,0)) (0 until DetailedCharacterData.numberOfImplantSlots(tplayer.BEP)).foreach(slot => { @@ -1256,7 +1269,6 @@ class WorldSessionActor extends Actor with MDCContextAware { case CharacterRequestAction.Delete => sendResponse(ActionResultMessage(false, Some(1))) case CharacterRequestAction.Select => - LivePlayerList.Add(sessionId, player) //TODO check if can spawn on last continent/location from player? //TODO if yes, get continent guid accessors //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 - LivePlayerList.ZonePopulation(continent.Number, _ => true).foreach(char => { + continent.LivePlayers.filterNot(_.GUID == player.GUID).foreach(char => { sendResponse( ObjectCreateMessage(ObjectClass.avatar, char.GUID, char.Definition.Packet.ConstructorData(char).get) ) }) + //load corpses in zone + continent.Corpses.foreach( turnPlayerIntoCorpse(_) ) //load active vehicles in zone continent.Vehicles.foreach(vehicle => { val definition = vehicle.Definition sendResponse( - ObjectCreateMessage( - definition.ObjectId, - vehicle.GUID, - definition.Packet.ConstructorData(vehicle).get - ) + ObjectCreateMessage(definition.ObjectId, vehicle.GUID, definition.Packet.ConstructorData(vehicle).get) ) //seat vehicle occupants vehicle.Definition.MountPoints.values.foreach(seat_num => { @@ -1362,7 +1372,7 @@ class WorldSessionActor extends Actor with MDCContextAware { 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) => - if(!player.isAlive) { + if(player.isAlive) { player.Position = pos player.Velocity = vel player.Orientation = Vector3(player.Orientation.x, pitch, yaw) @@ -1434,10 +1444,14 @@ class WorldSessionActor extends Actor with MDCContextAware { case msg @ ReleaseAvatarRequestMessage() => 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 + continent.Population ! Zone.Population.Release(avatar) + continent.Population ! Zone.Corpse.Add(player) val knife = player.Slot(4).Equipment.get 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)) case msg @ SpawnRequestMessage(u1, u2, u3, u4, u5) => @@ -1449,7 +1463,7 @@ class WorldSessionActor extends Actor with MDCContextAware { val tplayer = SpawnRequest(player) //new player tplayer.Position = tube.Position 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)) import scala.concurrent.duration._ import scala.concurrent.ExecutionContext.Implicits.global @@ -1507,7 +1521,9 @@ class WorldSessionActor extends Actor with MDCContextAware { player.Die sendResponse(PlanetsideAttributeMessage(player_guid, 0, 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)) } @@ -1521,8 +1537,8 @@ class WorldSessionActor extends Actor with MDCContextAware { sendResponse(DropSession(sessionId, "user quit")) } - if(contents.trim.equals("!loc")) { //dev hack; consider bang-commands to complement slash-commands - echoContents = s"pos=${player.Position.x}, ${player.Position.y}, ${player.Position.z}; ori=${player.Orientation.x}, ${player.Orientation.y}, ${player.Orientation.z}" + if(contents.trim.equals("!loc")) { //dev hack; consider bang-commands to complement slash-commands in future + 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) } @@ -3315,6 +3331,7 @@ class WorldSessionActor extends Actor with MDCContextAware { val dcdata = packet.DetailedConstructorData(player).get sendResponse(ObjectCreateDetailedMessage(ObjectClass.avatar, player.GUID, dcdata)) avatarService ! AvatarServiceMessage(player.Continent, AvatarAction.LoadPlayer(player.GUID, packet.ConstructorData(player).get)) + continent.Population ! Zone.Population.Spawn(avatar, player) log.debug(s"ObjectCreateDetailedMessage: $dcdata") } @@ -3335,6 +3352,13 @@ class WorldSessionActor extends Actor with MDCContextAware { 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) = { log.error(error) sendResponse(ConnectionClose()) diff --git a/pslogin/src/main/scala/services/avatar/AvatarAction.scala b/pslogin/src/main/scala/services/avatar/AvatarAction.scala index 13ece68ee..54c5725f4 100644 --- a/pslogin/src/main/scala/services/avatar/AvatarAction.scala +++ b/pslogin/src/main/scala/services/avatar/AvatarAction.scala @@ -1,7 +1,9 @@ // Copyright (c) 2017 PSForever package services.avatar +import net.psforever.objects.Player import net.psforever.objects.equipment.Equipment +import net.psforever.objects.zones.Zone import net.psforever.packet.game.{PlanetSideGUID, PlayerStateMessageUpstream} import net.psforever.packet.game.objectcreate.ConstructorData 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 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 Release(player : Player, zone : Zone) 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 PlayerStateShift(killer : PlanetSideGUID, victim : PlanetSideGUID) extends Action diff --git a/pslogin/src/main/scala/services/avatar/AvatarResponse.scala b/pslogin/src/main/scala/services/avatar/AvatarResponse.scala index 0ffe562f8..f3c605ed6 100644 --- a/pslogin/src/main/scala/services/avatar/AvatarResponse.scala +++ b/pslogin/src/main/scala/services/avatar/AvatarResponse.scala @@ -1,6 +1,7 @@ // Copyright (c) 2017 PSForever package services.avatar +import net.psforever.objects.Player import net.psforever.objects.equipment.Equipment import net.psforever.packet.game.{PlanetSideGUID, PlayerStateMessageUpstream} import net.psforever.packet.game.objectcreate.ConstructorData @@ -24,6 +25,7 @@ object AvatarResponse { final case class ObjectHeld(slot : Int) 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 Release(player : Player) extends Response final case class Reload(weapon_guid : PlanetSideGUID) extends Response final case class WeaponDryFire(weapon_guid : PlanetSideGUID) extends Response // final case class PlayerStateShift(itemID : PlanetSideGUID) extends Response diff --git a/pslogin/src/main/scala/services/avatar/AvatarService.scala b/pslogin/src/main/scala/services/avatar/AvatarService.scala index dcf249020..2d8f0f8c9 100644 --- a/pslogin/src/main/scala/services/avatar/AvatarService.scala +++ b/pslogin/src/main/scala/services/avatar/AvatarService.scala @@ -1,11 +1,14 @@ // Copyright (c) 2017 PSForever package services.avatar -import akka.actor.Actor +import akka.actor.{Actor, ActorRef, Props} +import services.avatar.support.UndertakerActor import services.{GenericEventBus, Service} 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 override def preStart = { @@ -87,6 +90,11 @@ class AvatarService extends Actor { AvatarEvents.publish( 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) => AvatarEvents.publish( AvatarServiceResponse(s"/$forChannel/Avatar", player_guid, AvatarResponse.Reload(weapon_guid)) diff --git a/pslogin/src/main/scala/services/avatar/support/UndertakerActor.scala b/pslogin/src/main/scala/services/avatar/support/UndertakerActor.scala new file mode 100644 index 000000000..bc9e81220 --- /dev/null +++ b/pslogin/src/main/scala/services/avatar/support/UndertakerActor.scala @@ -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 +}