diff --git a/common/src/main/scala/net/psforever/objects/GlobalDefinitions.scala b/common/src/main/scala/net/psforever/objects/GlobalDefinitions.scala
index 330cba5b..21bfbd07 100644
--- a/common/src/main/scala/net/psforever/objects/GlobalDefinitions.scala
+++ b/common/src/main/scala/net/psforever/objects/GlobalDefinitions.scala
@@ -6,6 +6,8 @@ import net.psforever.objects.definition.converter.{CommandDetonaterConverter, Lo
import net.psforever.objects.equipment.CItem.DeployedItem
import net.psforever.objects.equipment._
import net.psforever.objects.inventory.InventoryTile
+import net.psforever.objects.terminals.OrderTerminalDefinition
+import net.psforever.packet.game.objectcreate.ObjectClass
import net.psforever.types.PlanetSideEmpire
object GlobalDefinitions {
@@ -1235,4 +1237,7 @@ object GlobalDefinitions {
fury.Weapons += 1 -> fury_weapon_systema
fury.TrunkSize = InventoryTile(11, 11)
fury.TrunkOffset = 30
+
+ val
+ orderTerminal = new OrderTerminalDefinition
}
diff --git a/common/src/main/scala/net/psforever/objects/LivePlayerList.scala b/common/src/main/scala/net/psforever/objects/LivePlayerList.scala
index c0ad0e8f..c0e39856 100644
--- a/common/src/main/scala/net/psforever/objects/LivePlayerList.scala
+++ b/common/src/main/scala/net/psforever/objects/LivePlayerList.scala
@@ -3,6 +3,7 @@ package net.psforever.objects
import net.psforever.packet.game.PlanetSideGUID
+import scala.annotation.tailrec
import scala.collection.concurrent.{Map, TrieMap}
/**
@@ -12,18 +13,28 @@ 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]
- /** key - the global unique identifier; value - the session id */
- private val playerMap : Map[Int, Long] = new TrieMap[Int, Long]
+ /** 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] = {
- sessionMap.filter(predicate).map({ case(_, char) => char }).toList
+ 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
+ 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
- true
case Some(_) =>
false
}
@@ -32,46 +43,62 @@ private class LivePlayerList {
def Remove(sessionId : Long) : Option[Player] = {
sessionMap.remove(sessionId) match {
case Some(char) =>
- playerMap.find({ case(_, sess) => sess == sessionId }) match {
- case Some((guid, _)) =>
- playerMap.remove(guid)
- case None => ;
- }
+ zoneMap.foreach(zone => {
+ recursiveRemoveSession(zone.iterator, sessionId) match {
+ case Some(guid) =>
+ zone.remove(guid)
+ case None => ;
+ }
+ })
Some(char)
case None =>
None
}
}
- def Get(guid : PlanetSideGUID) : Option[Player] = {
- Get(guid.guid)
+ @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(guid : Int) : Option[Player] = {
- playerMap.get(guid) match {
- case Some(sess) =>
- sessionMap.get(sess)
- case _ =>
+ 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(sessionId : Long, guid : PlanetSideGUID) : Boolean = Assign(sessionId, guid.guid)
+ def Assign(zone: Int, sessionId : Long, guid : PlanetSideGUID) : Boolean = Assign(zone, sessionId, guid.guid)
- def Assign(sessionId : Long, guid : Int) : Boolean = {
- sessionMap.find({ case(sess, _) => sess == sessionId}) match {
- case Some((_, char)) =>
- if(char.GUID.guid == guid) {
- playerMap.find({ case(_, sess) => sess == sessionId }) match {
- case Some((id, _)) =>
- playerMap.remove(id)
- case None => ;
- }
- playerMap.put(guid, sessionId)
- true
- }
- else {
- false
+ 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 =>
@@ -79,10 +106,36 @@ private class LivePlayerList {
}
}
+ 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
sessionMap.clear
- playerMap.clear
+ zoneMap.foreach(map => map.clear())
list
}
}
@@ -90,20 +143,26 @@ private class LivePlayerList {
/**
* 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`.
- * A loose coupling between the current globally unique identifier (GUID) and the user is also present.
+ * 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.
*
* 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(session, 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(guid)`
+ * `LivePlayerList.Get(zone, guid)`
* 4b) Also in between those same previous steps, a range of characters may be queried based on provided statistics.
* `LivePlayerList.WorldPopulation(...)`
- * 5) When the user leaves the game, his character's entries are removed from the mappings.
+ * `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.
* `LivePlayerList.Remove(session)`
*/
object LivePlayerList {
@@ -114,7 +173,7 @@ object LivePlayerList {
* Given some criteria, examine the mapping of user characters 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 character - is eligible for being queried.
+ * 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 predicate the conditions for filtering the live `Player`s
@@ -122,6 +181,19 @@ object LivePlayerList {
*/
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)
+
/**
* 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.
@@ -142,39 +214,61 @@ object LivePlayerList {
/**
* 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(guid : PlanetSideGUID) : Option[Player] = Instance.Get(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(guid : Int) : Option[Player] = Instance.Get(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(sessionId : Long, guid : PlanetSideGUID) : Boolean = Instance.Assign(sessionId, guid)
+ 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(sessionId : Long, guid : Int) : Boolean = Instance.Assign(sessionId, guid)
+ 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.
diff --git a/common/src/main/scala/net/psforever/objects/terminals/Terminal.scala b/common/src/main/scala/net/psforever/objects/terminals/Terminal.scala
index 7c19d142..81f464fe 100644
--- a/common/src/main/scala/net/psforever/objects/terminals/Terminal.scala
+++ b/common/src/main/scala/net/psforever/objects/terminals/Terminal.scala
@@ -142,6 +142,10 @@ object Terminal {
*/
final case class InfantryLoadout(exosuit : ExoSuitType.Value, subtype : Int = 0, holsters : List[InventoryItem], inventory : List[InventoryItem]) extends Exchange
+ def apply(tdef : TerminalDefinition) : Terminal = {
+ new Terminal(tdef)
+ }
+
import net.psforever.packet.game.PlanetSideGUID
def apply(guid : PlanetSideGUID, tdef : TerminalDefinition) : Terminal = {
val obj = new Terminal(tdef)
diff --git a/common/src/main/scala/net/psforever/objects/zones/InterstellarCluster.scala b/common/src/main/scala/net/psforever/objects/zones/InterstellarCluster.scala
new file mode 100644
index 00000000..e09d55c2
--- /dev/null
+++ b/common/src/main/scala/net/psforever/objects/zones/InterstellarCluster.scala
@@ -0,0 +1,113 @@
+// Copyright (c) 2017 PSForever
+package net.psforever.objects.zones
+
+import akka.actor.{Actor, Props}
+import net.psforever.objects.Player
+
+import scala.annotation.tailrec
+
+/**
+ * The root of the universe of one-continent planets, codified by the game's "Interstellar Map."
+ * Constructs each zone and thus instigates the construction of every server object in the game world.
+ * The nanite flow connecting all of these `Zone`s is called the "Intercontinental Lattice."
+ *
+ * The process of "construction" and "initialization" and "configuration" are referenced at this level.
+ * These concepts are not the same thing;
+ * the distinction is important.
+ * "Construction" and "instantiation" of the cluster merely produces the "facade" of the different `Zone` entities.
+ * In such a `List`, every built `Zone` is capable of being a destination on the "Intercontinental lattice."
+ * "Initialization" and "configuration" of the cluster refers to the act of completing the "Intercontinental Lattice"
+ * by connecting different terminus warp gates together.
+ * Other activities involve event management and managing wide-reaching and factional attributes.
+ * @param zones a `List` of continental `Zone` arenas
+ */
+class InterstellarCluster(zones : List[Zone]) extends Actor {
+ private[this] val log = org.log4s.getLogger
+ log.info("Starting interplanetary cluster ...")
+
+ /**
+ * Create a `ZoneActor` for each `Zone`.
+ * That `Actor` is sent a packet that would start the construction of the `Zone`'s server objects.
+ * The process is maintained this way to allow every planet to be created and configured in separate stages.
+ */
+ override def preStart() : Unit = {
+ super.preStart()
+ for(zone <- zones) {
+ log.info(s"Built continent ${zone.Id}")
+ zone.Actor = context.actorOf(Props(classOf[ZoneActor], zone), s"${zone.Id}-actor")
+ zone.Actor ! Zone.Init()
+ }
+ }
+
+ def receive : Receive = {
+ case InterstellarCluster.GetWorld(zoneId) =>
+ log.info(s"Asked to find $zoneId")
+ findWorldInCluster(zones.iterator, zoneId) match {
+ case Some(continent) =>
+ sender ! InterstellarCluster.GiveWorld(zoneId, continent)
+ case None =>
+ log.error(s"Requested zone $zoneId could not be found")
+ }
+
+ case InterstellarCluster.RequestClientInitialization(tplayer) =>
+ zones.foreach(zone => {
+ sender ! Zone.ClientInitialization(zone.ClientInitialization()) //do this for each Zone
+ })
+ sender ! InterstellarCluster.ClientInitializationComplete(tplayer) //will be processed after all Zones
+
+ case _ => ;
+ }
+
+ /**
+ * Search through the `List` of `Zone` entities and find the one with the matching designation.
+ * @param iter an `Iterator` of `Zone` entities
+ * @param zoneId the name of the `Zone`
+ * @return the discovered `Zone`
+ */
+ @tailrec private def findWorldInCluster(iter : Iterator[Zone], zoneId : String) : Option[Zone] = {
+ if(!iter.hasNext) {
+ None
+ }
+ else {
+ val cont = iter.next
+ if(cont.Id == zoneId) {
+ Some(cont)
+ }
+ else {
+ findWorldInCluster(iter, zoneId)
+ }
+ }
+ }
+}
+
+object InterstellarCluster {
+
+ /**
+ * Request a hard reference to a `Zone`.
+ * @param zoneId the name of the `Zone`
+ */
+ final case class GetWorld(zoneId : String)
+
+ /**
+ * Provide a hard reference to a `Zone`.
+ * @param zoneId the name of the `Zone`
+ * @param zone the `Zone`
+ */
+ final case class GiveWorld(zoneId : String, zone : Zone)
+
+ /**
+ * Signal to the cluster that a new client needs to be initialized for all listed `Zone` destinations.
+ * @param tplayer the `Player` belonging to the client;
+ * may be superfluous
+ * @see `Zone`
+ */
+ final case class RequestClientInitialization(tplayer : Player)
+
+ /**
+ * Return signal intended to inform the original sender that all `Zone`s have finished being initialized.
+ * @param tplayer the `Player` belonging to the client;
+ * may be superfluous
+ * @see `WorldSessionActor`
+ */
+ final case class ClientInitializationComplete(tplayer : Player)
+}
diff --git a/common/src/main/scala/net/psforever/objects/zones/ServerObjectBuilder.scala b/common/src/main/scala/net/psforever/objects/zones/ServerObjectBuilder.scala
new file mode 100644
index 00000000..2635df09
--- /dev/null
+++ b/common/src/main/scala/net/psforever/objects/zones/ServerObjectBuilder.scala
@@ -0,0 +1,27 @@
+// Copyright (c) 2017 PSForever
+package net.psforever.objects.zones
+
+import akka.actor.ActorContext
+import net.psforever.objects.PlanetSideGameObject
+import net.psforever.objects.guid.NumberPoolHub
+
+/**
+ * Wrapper `Trait` designed to be extended to implement custom object instantiation logic at the `ZoneMap` level.
+ * @see `Zone.Init`
+ */
+trait ServerObjectBuilder {
+ /**
+ * Instantiate and configure the given server object
+ * (at a later time compared to the construction of the builder class).
+ *
+ * Externally, it expects a `context` to properly integrate within an `ActorSystem`
+ * and is provided with a source for globally unique identifiers to integrate into the `Zone`.
+ * Neither is required of the `return` type, however.
+ * @param context a context to allow the object to properly set up `ActorSystem` functionality;
+ * defaults to `null`
+ * @param guid the local globally unique identifier system to complete the process of object introduction;
+ * defaults to `null`
+ * @return the object that was created and integrated into the `Zone`
+ */
+ def Build(implicit context : ActorContext = null, guid : NumberPoolHub = null) : PlanetSideGameObject
+}
diff --git a/common/src/main/scala/net/psforever/objects/zones/TerminalObjectBuilder.scala b/common/src/main/scala/net/psforever/objects/zones/TerminalObjectBuilder.scala
new file mode 100644
index 00000000..2ced4d84
--- /dev/null
+++ b/common/src/main/scala/net/psforever/objects/zones/TerminalObjectBuilder.scala
@@ -0,0 +1,33 @@
+// Copyright (c) 2017 PSForever
+package net.psforever.objects.zones
+
+import net.psforever.objects.terminals.{Terminal, TerminalDefinition}
+
+/**
+ * Wrapper `Class` designed to instantiate a `Terminal` server object.
+ * @param tdef a `TerminalDefinition` object, indicating the specific functionality of the resulting `Terminal`
+ * @param id the globally unique identifier to which this `Terminal` will be registered
+ */
+class TerminalObjectBuilder(private val tdef : TerminalDefinition, private val id : Int) extends ServerObjectBuilder {
+ import akka.actor.ActorContext
+ import net.psforever.objects.guid.NumberPoolHub
+
+ def Build(implicit context : ActorContext, guid : NumberPoolHub) : Terminal = {
+ val obj = Terminal(tdef)
+ guid.register(obj, id) //non-Actor GUID registration
+ obj.Actor //it's necessary to register beforehand because the Actor name utilizes the GUID
+ obj
+ }
+}
+
+object TerminalObjectBuilder {
+ /**
+ * Overloaded constructor for a `TerminalObjectBuilder`.
+ * @param tdef a `TerminalDefinition` object
+ * @param id a globally unique identifier
+ * @return a `TerminalObjectBuilder` object
+ */
+ def apply(tdef : TerminalDefinition, id : Int) : TerminalObjectBuilder = {
+ new TerminalObjectBuilder(tdef, id)
+ }
+}
diff --git a/common/src/main/scala/net/psforever/objects/zones/Zone.scala b/common/src/main/scala/net/psforever/objects/zones/Zone.scala
new file mode 100644
index 00000000..21497a74
--- /dev/null
+++ b/common/src/main/scala/net/psforever/objects/zones/Zone.scala
@@ -0,0 +1,259 @@
+// Copyright (c) 2017 PSForever
+package net.psforever.objects.zones
+
+import akka.actor.{ActorContext, ActorRef, Props}
+import net.psforever.objects.{PlanetSideGameObject, Player}
+import net.psforever.objects.equipment.Equipment
+import net.psforever.objects.guid.NumberPoolHub
+import net.psforever.objects.guid.actor.{NumberPoolAccessorActor, NumberPoolActor}
+import net.psforever.objects.guid.selector.RandomSelector
+import net.psforever.objects.guid.source.LimitedNumberSource
+import net.psforever.packet.GamePacket
+import net.psforever.packet.game.PlanetSideGUID
+import net.psforever.types.Vector3
+
+import scala.collection.mutable.ListBuffer
+
+/**
+ * A server object representing the one-landmass planets as well as the individual subterranean caverns.
+ *
+ * The concept of a "zone" is synonymous to the common vernacular "continent,"
+ * commonly referred by names such as Hossin or Ishundar and internally identified as c2 and c7, respectively.
+ * A `Zone` is composed of the abstracted concept of all the information pertinent for the simulation of the environment.
+ * That is, "everything about the continent."
+ * Physically, server objects and dynamic game objects are maintained through a local unique identifier system.
+ * Static server objects originate from the `ZoneMap`.
+ * Dynamic game objects originate from player characters.
+ * (Write more later.)
+ * @param zoneId the privileged name that can be used as the second parameter in the packet `LoadMapMessage`
+ * @param zoneMap the map of server objects upon which this `Zone` is based
+ * @param zoneNumber the numerical index of the `Zone` as it is recognized in a variety of packets;
+ * also used by `LivePlayerList` to indicate a specific `Zone`
+ * @see `ZoneMap`
+ * `LoadMapMessage`
+ * `LivePlayerList`
+ */
+class Zone(private val zoneId : String, zoneMap : ZoneMap, zoneNumber : Int) {
+ /** Governs general synchronized external requests. */
+ private var actor = ActorRef.noSender
+
+ /** Used by the globally unique identifier system to coordinate requests. */
+ private var accessor : ActorRef = ActorRef.noSender
+ /** The basic support structure for the globally unique number system used by this `Zone`. */
+ private var guid : NumberPoolHub = new NumberPoolHub(new LimitedNumberSource(65536))
+ /** A synchronized `List` of items (`Equipment`) dropped by players on the ground and can be collected again. */
+ private val equipmentOnGround : ListBuffer[Equipment] = ListBuffer[Equipment]()
+ /** Used by the `Zone` to coordinate `Equipment` dropping and collection requests. */
+ private var ground : ActorRef = ActorRef.noSender
+
+ /**
+ * Establish the basic accessible conditions necessary for a functional `Zone`.
+ *
+ * Called from the `Actor` that governs this `Zone` when it is passed a constructor reference to the `Zone`.
+ * Specifically, the order of calling follows: `InterstellarCluster.preStart -> ZoneActor.receive(Zone.Init()) -> Zone.Init`.
+ * The basic method performs three main operations.
+ * First, the `Actor`-driven aspect of the globally unique identifier system for this `Zone` is finalized.
+ * Second, all supporting `Actor` agents are created, e.g., `ground`.
+ * Third, the `ZoneMap` server objects are loaded and constructed within that aforementioned system.
+ * To avoid being called more than once, there is a test whether the `accessor` for the globally unique identifier system has been changed.
+ * @param context a reference to an `ActorContext` necessary for `Props`
+ */
+ def Init(implicit context : ActorContext) : Unit = {
+ if(accessor == ActorRef.noSender) {
+ //TODO wrong initialization for GUID
+ implicit val guid = this.guid
+ //passed into builderObject.Build implicitly
+ val pool = guid.AddPool("pool", (200 to 1000).toList)
+ pool.Selector = new RandomSelector
+ val poolActor = context.actorOf(Props(classOf[NumberPoolActor], pool), name = s"$Id-poolActor")
+ accessor = context.actorOf(Props(classOf[NumberPoolAccessorActor], guid, pool, poolActor), s"$Id-accessor")
+ ground = context.actorOf(Props(classOf[ZoneGroundActor], equipmentOnGround), s"$Id-ground")
+
+ Map.LocalObjects.foreach({ builderObject =>
+ builderObject.Build
+ })
+ }
+ }
+
+ /**
+ * A reference to the primary `Actor` that governs this `Zone`.
+ * @return an `ActorRef`
+ * @see `ZoneActor`
+ * `Zone.Init`
+ */
+ def Actor : ActorRef = actor
+
+ /**
+ * Give this `Zone` an `Actor` that will govern its interactions sequentially.
+ * @param zoneActor an `ActorRef` for this `Zone`;
+ * will not overwrite any existing governance unless `noSender`
+ * @return an `ActorRef`
+ * @see `ZoneActor`
+ */
+ def Actor_=(zoneActor : ActorRef) : ActorRef = {
+ if(actor == ActorRef.noSender) {
+ actor = zoneActor
+ }
+ Actor
+ }
+
+ /**
+ * The privileged name that can be used as the second parameter in the packet `LoadMapMessage`.
+ * @return the name
+ */
+ def Id : String = zoneId
+
+ /**
+ * The map of server objects upon which this `Zone` is based
+ * @return the map
+ */
+ def Map : ZoneMap = zoneMap
+
+ /**
+ * The numerical index of the `Zone` as it is recognized in a variety of packets.
+ * @return the abstract index position of this `Zone`
+ */
+ def Number : Int = zoneNumber
+
+ /**
+ * The globally unique identifier system is synchronized via an `Actor` to ensure that concurrent requests do not clash.
+ * A clash is merely when the same number is produced more than once by the same system due to concurrent requests.
+ * @return synchronized reference to the globally unique identifier system
+ */
+ def GUID : ActorRef = accessor
+
+ /**
+ * Replace the current globally unique identifier system with a new one.
+ * The replacement will not occur if the current system is populated or if its synchronized reference has been created.
+ * @return synchronized reference to the globally unique identifier system
+ */
+ def GUID(hub : NumberPoolHub) : ActorRef = {
+ if(actor == ActorRef.noSender && guid.Pools.map({case ((_, pool)) => pool.Count}).sum == 0) {
+ guid = hub
+ }
+ Actor
+ }
+
+ /**
+ * Recover an object from the globally unique identifier system by the number that was assigned previously.
+ * @param object_guid the globally unique identifier requested
+ * @return the associated object, if it exists
+ * @see `GUID(Int)`
+ */
+ def GUID(object_guid : PlanetSideGUID) : Option[PlanetSideGameObject] = GUID(object_guid.guid)
+
+ /**
+ * Recover an object from the globally unique identifier system by the number that was assigned previously.
+ * The object must be upcast into due to the differtence between the storage type and the return type.
+ * @param object_guid the globally unique identifier requested
+ * @return the associated object, if it exists
+ * @see `NumberPoolHub(Int)`
+ */
+ def GUID(object_guid : Int) : Option[PlanetSideGameObject] = guid(object_guid) match {
+ case Some(obj) =>
+ Some(obj.asInstanceOf[PlanetSideGameObject])
+ case None =>
+ None
+ }
+
+ /**
+ * The `List` of items (`Equipment`) dropped by players on the ground and can be collected again.
+ * @return the `List` of `Equipment`
+ */
+ def EquipmentOnGround : List[Equipment] = equipmentOnGround.toList
+
+ /**
+ * Coordinate `Equipment` that has been dropped on the ground or to-be-dropped on the ground.
+ * @return synchronized reference to the ground
+ * @see `ZoneGroundActor`
+ * `Zone.DropItemOnGround`
+ * `Zone.GetItemOnGround`
+ * `Zone.ItemFromGround`
+ */
+ def Ground : ActorRef = ground
+
+ /**
+ * Provide bulk correspondence on all map entities that can be composed into packet messages and reported to a client.
+ * These messages are sent in this fashion at the time of joining the server:
+ * - `BroadcastWarpgateUpdateMessage`
+ * - `BuildingInfoUpdateMessage`
+ * - `CaptureFlagUpdateMessage`
+ * - `ContinentalLockUpdateMessage`
+ * - `DensityLevelUpdateMessage`
+ * - `ModuleLimitsMessage`
+ * - `VanuModuleUpdateMessage`
+ * - `ZoneForcedCavernConnectionMessage`
+ * - `ZoneInfoMessage`
+ * - `ZoneLockInfoMessage`
+ * - `ZonePopulationUpdateMessage`
+ * @return a `List` of `GamePacket` messages
+ */
+ def ClientInitialization() : List[GamePacket] = {
+ //TODO unimplemented
+ List.empty[GamePacket]
+ }
+
+ /**
+ * Provide bulk correspondence on all server objects that can be composed into packet messages and reported to a client.
+ * These messages are sent in this fashion at the time of joining a specific `Zone`:
+ * - `HackMessage`
+ * - `PlanetsideAttributeMessage`
+ * - `SetEmpireMessage`
+ * - `TimeOfDayMessage`
+ * - `WeatherMessage`
+ * @return a `List` of `GamePacket` messages
+ */
+ def ClientConfiguration() : List[GamePacket] = {
+ //TODO unimplemented
+ List.empty[GamePacket]
+ }
+}
+
+object Zone {
+ /**
+ * Message to initialize the `Zone`.
+ * @see `Zone.Init(implicit ActorContext)`
+ */
+ final case class Init()
+
+ /**
+ * Message to relinguish an item and place in on the ground.
+ * @param item the piece of `Equipment`
+ * @param pos where it is dropped
+ * @param orient in which direction it is facing when dropped
+ */
+ final case class DropItemOnGround(item : Equipment, pos : Vector3, orient : Vector3)
+
+ /**
+ * Message to attempt to acquire an item from the ground (before somoene else?).
+ * @param player who wants the piece of `Equipment`
+ * @param item_guid the unique identifier of the piece of `Equipment`
+ */
+ final case class GetItemOnGround(player : Player, item_guid : PlanetSideGUID)
+
+ /**
+ * Message to give an item from the ground to a specific user.
+ * @param player who wants the piece of `Equipment`
+ * @param item the piece of `Equipment`
+ */
+ final case class ItemFromGround(player : Player, item : Equipment)
+
+ /**
+ * Message to report the packet messages that initialize the client.
+ * @param list a `List` of `GamePacket` messages
+ * @see `Zone.ClientInitialization()`
+ * `InterstallarCluster`
+ */
+ final case class ClientInitialization(list : List[GamePacket])
+
+ /**
+ * Overloaded constructor.
+ * @param id the privileged name that can be used as the second parameter in the packet `LoadMapMessage`
+ * @param map the map of server objects upon which this `Zone` is based
+ * @param number the numerical index of the `Zone` as it is recognized in a variety of packets
+ * @return a `Zone` object
+ */
+ def apply(id : String, map : ZoneMap, number : Int) : Zone = {
+ new Zone(id, map, number)
+ }
+}
diff --git a/common/src/main/scala/net/psforever/objects/zones/ZoneActor.scala b/common/src/main/scala/net/psforever/objects/zones/ZoneActor.scala
new file mode 100644
index 00000000..811b6ef9
--- /dev/null
+++ b/common/src/main/scala/net/psforever/objects/zones/ZoneActor.scala
@@ -0,0 +1,20 @@
+// Copyright (c) 2017 PSForever
+package net.psforever.objects.zones
+
+import akka.actor.Actor
+
+/**
+ * na
+ * @param zone the `Zone` governed by this `Actor`
+ */
+class ZoneActor(zone : Zone) extends Actor {
+ private[this] val log = org.log4s.getLogger
+
+ def receive : Receive = {
+ case Zone.Init() =>
+ zone.Init
+
+ case msg =>
+ log.warn(s"Received unexpected message - $msg")
+ }
+}
diff --git a/common/src/main/scala/net/psforever/objects/zones/ZoneGroundActor.scala b/common/src/main/scala/net/psforever/objects/zones/ZoneGroundActor.scala
new file mode 100644
index 00000000..5a001e47
--- /dev/null
+++ b/common/src/main/scala/net/psforever/objects/zones/ZoneGroundActor.scala
@@ -0,0 +1,72 @@
+// Copyright (c) 2017 PSForever
+package net.psforever.objects.zones
+
+import akka.actor.Actor
+import net.psforever.objects.equipment.Equipment
+import net.psforever.packet.game.PlanetSideGUID
+
+import scala.annotation.tailrec
+import scala.collection.mutable.ListBuffer
+
+/**
+ * na
+ * @param equipmentOnGround a `List` of items (`Equipment`) dropped by players on the ground and can be collected again
+ */
+class ZoneGroundActor(equipmentOnGround : ListBuffer[Equipment]) extends Actor {
+ //private[this] val log = org.log4s.getLogger
+
+ def receive : Receive = {
+ case Zone.DropItemOnGround(item, pos, orient) =>
+ item.Position = pos
+ item.Orientation = orient
+ equipmentOnGround += item
+
+ case Zone.GetItemOnGround(player, item_guid) =>
+ FindItemOnGround(item_guid) match {
+ case Some(item) =>
+ sender ! Zone.ItemFromGround(player, item)
+ case None =>
+ org.log4s.getLogger.warn(s"item on ground $item_guid was requested by $player for pickup but was not found")
+ }
+
+ case _ => ;
+ }
+
+ /**
+ * Shift through objects on the ground to find the location of a specific item.
+ * @param item_guid the global unique identifier of the piece of `Equipment` being sought
+ * @return the index of the object matching `item_guid`, if found;
+ * `None`, otherwise
+ */
+ private def FindItemOnGround(item_guid : PlanetSideGUID) : Option[Equipment] = {
+ recursiveFindItemOnGround(equipmentOnGround.iterator, item_guid) match {
+ case Some(index) =>
+ Some(equipmentOnGround.remove(index))
+ case None =>
+ None
+ }
+ }
+
+ /**
+ * Shift through objects on the ground to find the location of a specific item.
+ * @param iter an `Iterator` of `Equipment`
+ * @param item_guid the global unique identifier of the piece of `Equipment` being sought
+ * @param index the current position in the array-list structure used to create the `Iterator`
+ * @return the index of the object matching `item_guid`, if found;
+ * `None`, otherwise
+ */
+ @tailrec private def recursiveFindItemOnGround(iter : Iterator[Equipment], item_guid : PlanetSideGUID, index : Int = 0) : Option[Int] = {
+ if(!iter.hasNext) {
+ None
+ }
+ else {
+ val item : Equipment = iter.next
+ if(item.GUID == item_guid) {
+ Some(index)
+ }
+ else {
+ recursiveFindItemOnGround(iter, item_guid, index + 1)
+ }
+ }
+ }
+}
diff --git a/common/src/main/scala/net/psforever/objects/zones/ZoneMap.scala b/common/src/main/scala/net/psforever/objects/zones/ZoneMap.scala
new file mode 100644
index 00000000..b1cf2e9a
--- /dev/null
+++ b/common/src/main/scala/net/psforever/objects/zones/ZoneMap.scala
@@ -0,0 +1,43 @@
+// Copyright (c) 2017 PSForever
+package net.psforever.objects.zones
+
+/**
+ * The fixed instantiation and relation of a series of server objects.
+ *
+ * Asides from a `List` of server objects to be built, the operation between any server objects
+ * and the connected functionality emerging from more complex data structures is codified by this object.
+ * In the former case, all `Terminal` server objects for a `Zone` are to be defined herein.
+ * In the latter case, the arrangement of server objects into groups called facilities is also to be defined herein.
+ * Much like a `BasicDefinition` to an object, `ZoneMap` should not maintain mutable information for the companion `Zone`.
+ * Use it as a blueprint.
+ *
+ * The "training zones" are the best example of the difference between a `ZoneMap` and a `Zone.`
+ * `tzdrtr` is the Terran Republic driving course.
+ * `tzdrvs` is the Vanu Sovereignty driving course.
+ * While each course can have different objects and object states (`Zone`),
+ * both courses have the same basic server objects because they are built from the same blueprint (`ZoneMap`).
+ * @param name the privileged name that can be used as the first parameter in the packet `LoadMapMessage`
+ * @see `ServerObjectBuilder`
+ * `LoadMapMessage`
+ */
+class ZoneMap(private val name : String) {
+ private var localObjects : List[ServerObjectBuilder] = List()
+
+ def Name : String = name
+
+ /**
+ * Append the builder for a server object to the list of builders known to this `ZoneMap`.
+ * @param obj the builder for a server object
+ */
+ def LocalObject(obj : ServerObjectBuilder) : Unit = {
+ localObjects = localObjects :+ obj
+ }
+
+ /**
+ * The list of all server object builder wrappers that have been assigned to this `ZoneMap`.
+ * @return the `List` of all `ServerObjectBuilders` known to this `ZoneMap`
+ */
+ def LocalObjects : List[ServerObjectBuilder] = {
+ localObjects
+ }
+}
diff --git a/pslogin/src/main/scala/PsLogin.scala b/pslogin/src/main/scala/PsLogin.scala
index fab1d3e7..c1403fc4 100644
--- a/pslogin/src/main/scala/PsLogin.scala
+++ b/pslogin/src/main/scala/PsLogin.scala
@@ -12,10 +12,8 @@ import ch.qos.logback.core.status._
import ch.qos.logback.core.util.StatusPrinter
import com.typesafe.config.ConfigFactory
import net.psforever.crypto.CryptoInterface
-import net.psforever.objects.guid.{NumberPoolHub, TaskResolver}
-import net.psforever.objects.guid.actor.{NumberPoolAccessorActor, NumberPoolActor}
-import net.psforever.objects.guid.selector.RandomSelector
-import net.psforever.objects.guid.source.LimitedNumberSource
+import net.psforever.objects.zones.{InterstellarCluster, TerminalObjectBuilder, Zone, ZoneMap}
+import net.psforever.objects.guid.TaskResolver
import org.slf4j
import org.fusesource.jansi.Ansi._
import org.fusesource.jansi.Ansi.Color._
@@ -89,7 +87,7 @@ object PsLogin {
configurator.doConfigure(logfile)
}
catch {
- case je : JoranException => ;
+ case _ : JoranException => ;
}
if(loggerHasErrors(lc)) {
@@ -202,23 +200,9 @@ object PsLogin {
*/
val serviceManager = ServiceManager.boot
-
- //experimental guid code
- val hub = new NumberPoolHub(new LimitedNumberSource(65536))
- val pool1 = hub.AddPool("test1", (400 to 599).toList)
- val poolActor1 = system.actorOf(Props(classOf[NumberPoolActor], pool1), name = "poolActor1")
- pool1.Selector = new RandomSelector
- val pool2 = hub.AddPool("test2", (600 to 799).toList)
- val poolActor2 = system.actorOf(Props(classOf[NumberPoolActor], pool2), name = "poolActor2")
- pool2.Selector = new RandomSelector
-
- serviceManager ! ServiceManager.Register(Props(classOf[NumberPoolAccessorActor], hub, pool1, poolActor1), "accessor1")
- serviceManager ! ServiceManager.Register(Props(classOf[NumberPoolAccessorActor], hub, pool2, poolActor2), "accessor2")
-
- //task resolver
serviceManager ! ServiceManager.Register(RandomPool(50).props(Props[TaskResolver]), "taskResolver")
-
serviceManager ! ServiceManager.Register(Props[AvatarService], "avatar")
+ serviceManager ! ServiceManager.Register(Props(classOf[InterstellarCluster], createContinents()), "galaxy")
/** Create two actors for handling the login and world server endpoints */
loginRouter = Props(new SessionRouter("Login", loginTemplate))
@@ -235,6 +219,19 @@ object PsLogin {
}
}
+ def createContinents() : List[Zone] = {
+ val map13 = new ZoneMap("map13") {
+ import net.psforever.objects.GlobalDefinitions._
+ LocalObject(TerminalObjectBuilder(orderTerminal, 853))
+ LocalObject(TerminalObjectBuilder(orderTerminal, 855))
+ LocalObject(TerminalObjectBuilder(orderTerminal, 860))
+ }
+ val home3 = Zone("home3", map13, 13)
+
+ home3 ::
+ Nil
+ }
+
def main(args : Array[String]) : Unit = {
Locale.setDefault(Locale.US); // to have floats with dots, not comma...
this.args = args
diff --git a/pslogin/src/main/scala/WorldSessionActor.scala b/pslogin/src/main/scala/WorldSessionActor.scala
index c69ead66..87bbdde9 100644
--- a/pslogin/src/main/scala/WorldSessionActor.scala
+++ b/pslogin/src/main/scala/WorldSessionActor.scala
@@ -11,12 +11,13 @@ import org.log4s.MDC
import MDCContextAware.Implicits._
import ServiceManager.Lookup
import net.psforever.objects._
+import net.psforever.objects.zones.{InterstellarCluster, Zone}
import net.psforever.objects.entity.IdentifiableEntity
import net.psforever.objects.equipment._
import net.psforever.objects.guid.{Task, TaskResolver}
import net.psforever.objects.guid.actor.{Register, Unregister}
import net.psforever.objects.inventory.{GridInventory, InventoryItem}
-import net.psforever.objects.terminals.{OrderTerminalDefinition, Terminal}
+import net.psforever.objects.terminals.Terminal
import net.psforever.packet.game.objectcreate._
import net.psforever.types._
@@ -24,21 +25,16 @@ import scala.annotation.tailrec
import scala.util.Success
class WorldSessionActor extends Actor with MDCContextAware {
+ import WorldSessionActor._
private[this] val log = org.log4s.getLogger
- private final case class PokeClient()
- private final case class ServerLoaded()
- private final case class PlayerLoaded(tplayer : Player)
- private final case class ListAccountCharacters()
- private final case class SetCurrentAvatar(tplayer : Player)
- private final case class Continent_GiveItemFromGround(tplyaer : Player, item : Option[Equipment]) //TODO wrong place, move later
-
var sessionId : Long = 0
var leftRef : ActorRef = ActorRef.noSender
var rightRef : ActorRef = ActorRef.noSender
var avatarService = Actor.noSender
- var accessor = Actor.noSender
var taskResolver = Actor.noSender
+ var galaxy = Actor.noSender
+ var continent : Zone = null
var clientKeepAlive : Cancellable = WorldSessionActor.DefaultCancellable
@@ -49,10 +45,12 @@ class WorldSessionActor extends Actor with MDCContextAware {
avatarService ! Leave()
LivePlayerList.Remove(sessionId) match {
case Some(tplayer) =>
- val guid = tplayer.GUID
- avatarService ! AvatarServiceMessage(tplayer.Continent, AvatarAction.ObjectDelete(guid, guid))
- taskResolver ! UnregisterAvatar(tplayer)
- //TODO normally, the actual player avatar persists a minute or so after the user disconnects
+ if(tplayer.HasGUID) {
+ val guid = tplayer.GUID
+ avatarService ! AvatarServiceMessage(tplayer.Continent, AvatarAction.ObjectDelete(guid, guid))
+ taskResolver ! UnregisterAvatar(tplayer)
+ //TODO normally, the actual player avatar persists a minute or so after the user disconnects
+ }
case None => ;
}
}
@@ -71,8 +69,8 @@ class WorldSessionActor extends Actor with MDCContextAware {
}
context.become(Started)
ServiceManager.serviceManager ! Lookup("avatar")
- ServiceManager.serviceManager ! Lookup("accessor1")
ServiceManager.serviceManager ! Lookup("taskResolver")
+ ServiceManager.serviceManager ! Lookup("galaxy")
case _ =>
log.error("Unknown message")
@@ -83,12 +81,12 @@ class WorldSessionActor extends Actor with MDCContextAware {
case ServiceManager.LookupResult("avatar", endpoint) =>
avatarService = endpoint
log.info("ID: " + sessionId + " Got avatar service " + endpoint)
- case ServiceManager.LookupResult("accessor1", endpoint) =>
- accessor = endpoint
- log.info("ID: " + sessionId + " Got guid service " + endpoint)
case ServiceManager.LookupResult("taskResolver", endpoint) =>
taskResolver = endpoint
log.info("ID: " + sessionId + " Got task resolver service " + endpoint)
+ case ServiceManager.LookupResult("galaxy", endpoint) =>
+ galaxy = endpoint
+ log.info("ID: " + sessionId + " Got galaxy service " + endpoint)
case ctrl @ ControlPacket(_, _) =>
handlePktContainer(ctrl)
@@ -360,7 +358,6 @@ class WorldSessionActor extends Actor with MDCContextAware {
avatarService ! AvatarServiceMessage(tplayer.Continent, AvatarAction.PlanetsideAttribute(tplayer.GUID, 4, tplayer.Armor))
//re-draw equipment held in free hand
beforeFreeHand match {
- //TODO was any previous free hand item deleted?
case Some(item) =>
tplayer.FreeHand.Equipment = beforeFreeHand
val definition = item.Definition
@@ -413,10 +410,25 @@ class WorldSessionActor extends Actor with MDCContextAware {
sendResponse(PacketCoding.CreateGamePacket(0, CharacterInfoMessage(0, PlanetSideZoneID(1), 0, PlanetSideGUID(0), true, 0)))
+ case InterstellarCluster.GiveWorld(zoneId, zone) =>
+ log.info(s"Zone $zoneId has been loaded")
+ player.Continent = zoneId
+ continent = zone
+ taskResolver ! RegisterAvatar(player)
+
case PlayerLoaded(tplayer) =>
log.info(s"Player $tplayer has been loaded")
//init for whole server
- //...
+ galaxy ! InterstellarCluster.RequestClientInitialization(tplayer)
+
+ case PlayerFailedToLoad(tplayer) =>
+ player.Continent match {
+ case _ =>
+ failWithError(s"$tplayer failed to load anywhere")
+ }
+
+ case Zone.ClientInitialization(/*initList*/_) =>
+ //TODO iterate over initList; for now, just do this
sendResponse(
PacketCoding.CreateGamePacket(0,
BuildingInfoUpdateMessage(
@@ -446,8 +458,12 @@ class WorldSessionActor extends Actor with MDCContextAware {
)
sendResponse(PacketCoding.CreateGamePacket(0, ContinentalLockUpdateMessage(PlanetSideGUID(13), PlanetSideEmpire.VS))) // "The VS have captured the VS Sanctuary."
sendResponse(PacketCoding.CreateGamePacket(0, BroadcastWarpgateUpdateMessage(PlanetSideGUID(13), PlanetSideGUID(1), false, false, true))) // VS Sanctuary: Inactive Warpgate -> Broadcast Warpgate
- //LoadMapMessage -> BeginZoningMessage
- sendResponse(PacketCoding.CreateGamePacket(0, LoadMapMessage("map13","home3",40100,25,true,3770441820L))) //VS Sanctuary
+ sendResponse(PacketCoding.CreateGamePacket(0, ZonePopulationUpdateMessage(PlanetSideGUID(13), 414, 138, 0, 138, 0, 138, 0, 138, 0)))
+
+ case InterstellarCluster.ClientInitializationComplete(tplayer)=>
+ //this will cause the client to send back a BeginZoningMessage packet (see below)
+ sendResponse(PacketCoding.CreateGamePacket(0, LoadMapMessage(continent.Map.Name, continent.Id, 40100,25,true,3770441820L))) //VS Sanctuary
+ log.info("Load the now-registered player")
//load the now-registered player
tplayer.Spawn
sendResponse(PacketCoding.CreateGamePacket(0,
@@ -457,38 +473,42 @@ class WorldSessionActor extends Actor with MDCContextAware {
log.debug(s"ObjectCreateDetailedMessage: ${tplayer.Definition.Packet.DetailedConstructorData(tplayer).get}")
case SetCurrentAvatar(tplayer) =>
- //avatar-specific
val guid = tplayer.GUID
- LivePlayerList.Assign(sessionId, guid)
+ LivePlayerList.Assign(continent.Number, sessionId, guid)
sendResponse(PacketCoding.CreateGamePacket(0, SetCurrentAvatarMessage(guid,0,0)))
sendResponse(PacketCoding.CreateGamePacket(0, CreateShortcutMessage(guid, 1, 0, true, Shortcut.MEDKIT)))
- //temporary location
- case Continent_GiveItemFromGround(tplayer, item) =>
- item match {
- case Some(obj) =>
- val obj_guid = obj.GUID
- tplayer.Fit(obj) match {
- case Some(slot) =>
- PickupItemFromGround(obj_guid)
- tplayer.Slot(slot).Equipment = item
- sendResponse(PacketCoding.CreateGamePacket(0, ObjectAttachMessage(tplayer.GUID, obj_guid, slot)))
- avatarService ! AvatarServiceMessage(tplayer.Continent, AvatarAction.ObjectDelete(tplayer.GUID, obj_guid))
- if(-1 < slot && slot < 5) {
- avatarService ! AvatarServiceMessage(tplayer.Continent, AvatarAction.EquipmentInHand(tplayer.GUID, slot, obj))
- }
- case None =>
- DropItemOnGround(obj, obj.Position, obj.Orientation) //restore
+ case Zone.ItemFromGround(tplayer, item) =>
+ val obj_guid = item.GUID
+ val player_guid = tplayer.GUID
+ tplayer.Fit(item) match {
+ case Some(slot) =>
+ tplayer.Slot(slot).Equipment = item
+ avatarService ! AvatarServiceMessage(tplayer.Continent, AvatarAction.ObjectDelete(player_guid, obj_guid))
+ val definition = item.Definition
+ sendResponse(
+ PacketCoding.CreateGamePacket(0,
+ ObjectCreateDetailedMessage(
+ definition.ObjectId,
+ obj_guid,
+ ObjectCreateMessageParent(player_guid, slot),
+ definition.Packet.DetailedConstructorData(item).get
+ )
+ )
+ )
+ if(-1 < slot && slot < 5) {
+ avatarService ! AvatarServiceMessage(tplayer.Continent, AvatarAction.EquipmentInHand(player_guid, slot, item))
}
- case None => ;
+ case None =>
+ continent.Actor ! Zone.DropItemOnGround(item, item.Position, item.Orientation) //restore
}
- case WorldSessionActor.ResponseToSelf(pkt) =>
+ case ResponseToSelf(pkt) =>
log.info(s"Received a direct message: $pkt")
sendResponse(pkt)
case default =>
- failWithError(s"Invalid packet class received: $default")
+ log.warn(s"Invalid packet class received: $default")
}
def handlePkt(pkt : PlanetSidePacket) : Unit = pkt match {
@@ -496,7 +516,7 @@ class WorldSessionActor extends Actor with MDCContextAware {
handleControlPkt(ctrl)
case game : PlanetSideGamePacket =>
handleGamePkt(game)
- case default => failWithError(s"Invalid packet class received: $default")
+ case default => log.error(s"Invalid packet class received: $default")
}
def handlePktContainer(pkt : PlanetSidePacketContainer) : Unit = pkt match {
@@ -504,7 +524,7 @@ class WorldSessionActor extends Actor with MDCContextAware {
handleControlPkt(ctrlPkt)
case game @ GamePacket(opcode, seq, gamePkt) =>
handleGamePkt(gamePkt)
- case default => failWithError(s"Invalid packet container class received: $default")
+ case default => log.warn(s"Invalid packet container class received: $default")
}
def handleControlPkt(pkt : PlanetSideControlPacket) = {
@@ -556,10 +576,8 @@ class WorldSessionActor extends Actor with MDCContextAware {
}
}
- val terminal = Terminal(PlanetSideGUID(55000), new OrderTerminalDefinition)
-
import net.psforever.objects.GlobalDefinitions._
- //this part is created by the player (should be in case of ConnectToWorldRequestMessage, maybe)
+ //this part is created by WSA based on the database query (should be in case of ConnectToWorldRequestMessage, maybe)
val energy_cell_box1 = AmmoBox(energy_cell)
val energy_cell_box2 = AmmoBox(energy_cell, 16)
val bullet_9mm_box1 = AmmoBox(bullet_9mm)
@@ -583,7 +601,6 @@ class WorldSessionActor extends Actor with MDCContextAware {
player = Player("IlllIIIlllIlIllIlllIllI", PlanetSideEmpire.VS, CharacterGender.Female, 41, 1)
player.Position = Vector3(3674.8438f, 2726.789f, 91.15625f)
player.Orientation = Vector3(0f, 0f, 90f)
- player.Continent = "home3"
player.Certifications += CertificationType.StandardAssault
player.Certifications += CertificationType.MediumAssault
player.Certifications += CertificationType.StandardExoSuit
@@ -602,57 +619,6 @@ class WorldSessionActor extends Actor with MDCContextAware {
player.Slot(39).Equipment = rek
player.Slot(5).Equipment.get.asInstanceOf[LockerContainer].Inventory += 0 -> extra_rek
- //for player2
- val energy_cell_box3 = AmmoBox(PlanetSideGUID(187), energy_cell)
- val energy_cell_box4 = AmmoBox(PlanetSideGUID(177), energy_cell, 16)
- val bullet_9mm_box5 = AmmoBox(PlanetSideGUID(183), bullet_9mm)
- val bullet_9mm_box6 = AmmoBox(PlanetSideGUID(184), bullet_9mm)
- val bullet_9mm_box7 = AmmoBox(PlanetSideGUID(185), bullet_9mm)
- val bullet_9mm_box8 = AmmoBox(PlanetSideGUID(179), bullet_9mm, 25)
- val bullet_9mm_AP_box2 = AmmoBox(PlanetSideGUID(186), bullet_9mm_AP)
- val melee_ammo_box2 = AmmoBox(PlanetSideGUID(181), melee_ammo)
-
- val
- beamer2 = Tool(PlanetSideGUID(176), beamer)
- beamer2.AmmoSlots.head.Box = energy_cell_box4
- val
- suppressor2 = Tool(PlanetSideGUID(178), suppressor)
- suppressor2.AmmoSlots.head.Box = bullet_9mm_box8
- val
- forceblade2 = Tool(PlanetSideGUID(180), forceblade)
- forceblade2.AmmoSlots.head.Box = melee_ammo_box2
- val
- rek2 = SimpleItem(PlanetSideGUID(188), remote_electronics_kit)
- val
- player2 = Player(PlanetSideGUID(275), "Doppelganger", PlanetSideEmpire.NC, CharacterGender.Female, 41, 1)
- player2.Position = Vector3(3680f, 2726.789f, 91.15625f)
- player2.Orientation = Vector3(0f, 0f, 0f)
- player2.Continent = "home3"
- player2.Slot(0).Equipment = beamer2
- player2.Slot(2).Equipment = suppressor2
- player2.Slot(4).Equipment = forceblade2
- player2.Slot(5).Equipment.get.GUID = PlanetSideGUID(182)
- player2.Slot(6).Equipment = bullet_9mm_box5
- player2.Slot(9).Equipment = bullet_9mm_box6
- player2.Slot(12).Equipment = bullet_9mm_box7
- player2.Slot(33).Equipment = bullet_9mm_AP_box2
- player2.Slot(36).Equipment = energy_cell_box3
- player2.Slot(39).Equipment = rek2
- player2.Spawn
-
- val hellfire_ammo_box = AmmoBox(PlanetSideGUID(432), hellfire_ammo)
-
- val
- fury1 = Vehicle(PlanetSideGUID(313), fury)
- fury1.Faction = PlanetSideEmpire.VS
- fury1.Position = Vector3(3674.8438f, 2732f, 91.15625f)
- fury1.Orientation = Vector3(0.0f, 0.0f, 90.0f)
- fury1.WeaponControlledFromSeat(0).get.GUID = PlanetSideGUID(300)
- fury1.WeaponControlledFromSeat(0).get.AmmoSlots.head.Box = hellfire_ammo_box
-
- val object2Hex = ObjectCreateMessage(ObjectClass.avatar, PlanetSideGUID(275), player2.Definition.Packet.ConstructorData(player2).get)
- val furyHex = ObjectCreateMessage(ObjectClass.fury, PlanetSideGUID(313), fury1.Definition.Packet.ConstructorData(fury1).get)
-
def handleGamePkt(pkt : PlanetSideGamePacket) = pkt match {
case ConnectToWorldRequestMessage(server, token, majorVersion, minorVersion, revision, buildDate, unk) =>
val clientVersion = s"Client Version: $majorVersion.$minorVersion.$revision, $buildDate"
@@ -671,13 +637,14 @@ class WorldSessionActor extends Actor with MDCContextAware {
sendResponse(PacketCoding.CreateGamePacket(0, ActionResultMessage(false, Some(1))))
case CharacterRequestAction.Select =>
LivePlayerList.Add(sessionId, player)
- //check can spawn on last continent/location from player
- //if yes, get continent guid accessors
- //if no, get sanctuary guid accessors and reset the player's expectations
- taskResolver ! RegisterAvatar(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
+ galaxy ! InterstellarCluster.GetWorld("home3")
import scala.concurrent.duration._
import scala.concurrent.ExecutionContext.Implicits.global
+ clientKeepAlive.cancel
clientKeepAlive = context.system.scheduler.schedule(0 seconds, 500 milliseconds, self, PokeClient())
case default =>
log.error("Unsupported " + default + " in " + msg)
@@ -688,28 +655,24 @@ class WorldSessionActor extends Actor with MDCContextAware {
case msg @ BeginZoningMessage() =>
log.info("Reticulating splines ...")
- //map-specific initializations (VS sanctuary)
+ //map-specific initializations
+ //TODO continent.ClientConfiguration()
sendResponse(PacketCoding.CreateGamePacket(0, SetEmpireMessage(PlanetSideGUID(2), PlanetSideEmpire.VS))) //HART building C
sendResponse(PacketCoding.CreateGamePacket(0, SetEmpireMessage(PlanetSideGUID(29), PlanetSideEmpire.NC))) //South Villa Gun Tower
- sendResponse(PacketCoding.CreateGamePacket(0, object2Hex))
- //sendResponse(PacketCoding.CreateGamePacket(0, furyHex))
- sendResponse(PacketCoding.CreateGamePacket(0, ZonePopulationUpdateMessage(PlanetSideGUID(13), 414, 138, 0, 138, 0, 138, 0, 138, 0)))
sendResponse(PacketCoding.CreateGamePacket(0, TimeOfDayMessage(1191182336)))
sendResponse(PacketCoding.CreateGamePacket(0, ReplicationStreamMessage(5, Some(6), Vector(SquadListing())))) //clear squad list
- //all players are part of the same zone right now, so don't expect much
- val continent = player.Continent
- val player_guid = player.GUID
- LivePlayerList.WorldPopulation({ case (_, char : Player) => char.Continent == continent && char.HasGUID && char.GUID != player_guid}).foreach(char => {
+ //load active players in zone
+ LivePlayerList.ZonePopulation(continent.Number, _ => true).foreach(char => {
sendResponse(
PacketCoding.CreateGamePacket(0,
ObjectCreateMessage(ObjectClass.avatar, char.GUID, char.Definition.Packet.ConstructorData(char).get)
)
)
})
- //all items are part of a single zone right now, so don't expect much
- WorldSessionActor.equipmentOnGround.foreach(item => {
+ //render Equipment that was dropped into zone before the player arrived
+ continent.EquipmentOnGround.toList.foreach(item => {
val definition = item.Definition
sendResponse(
PacketCoding.CreateGamePacket(0,
@@ -722,7 +685,7 @@ class WorldSessionActor extends Actor with MDCContextAware {
)
})
- avatarService ! Join("home3")
+ avatarService ! Join(player.Continent)
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) =>
@@ -794,10 +757,11 @@ class WorldSessionActor extends Actor with MDCContextAware {
player.FreeHand.Equipment match {
case Some(item) =>
if(item.GUID == item_guid) {
+ val orient : Vector3 = Vector3(0f, 0f, player.Orientation.z)
player.FreeHand.Equipment = None
- DropItemOnGround(item, player.Position, player.Orientation)
+ continent.Ground ! Zone.DropItemOnGround(item, player.Position, orient)
sendResponse(PacketCoding.CreateGamePacket(0, ObjectDetachMessage(player.GUID, item.GUID, player.Position, 0f, 0f, player.Orientation.z)))
- avatarService ! AvatarServiceMessage(player.Continent, AvatarAction.EquipmentOnGround(player.GUID, player.Position, player.Orientation, item))
+ avatarService ! AvatarServiceMessage(player.Continent, AvatarAction.EquipmentOnGround(player.GUID, player.Position, orient, item))
}
else {
log.warn(s"item in hand was ${item.GUID} but trying to drop $item_guid; nothing will be dropped")
@@ -808,7 +772,7 @@ class WorldSessionActor extends Actor with MDCContextAware {
case msg @ PickupItemMessage(item_guid, player_guid, unk1, unk2) =>
log.info("PickupItem: " + msg)
- self ! Continent_GiveItemFromGround(player, PickupItemFromGround(item_guid))
+ continent.Ground ! Zone.GetItemOnGround(player, item_guid)
case msg @ ReloadMessage(item_guid, ammo_clip, unk1) =>
log.info("Reload: " + msg)
@@ -923,9 +887,10 @@ class WorldSessionActor extends Actor with MDCContextAware {
case None => //item2 does not fit; drop on ground
val pos = player.Position
- val orient = player.Orientation
- DropItemOnGround(item2, pos, player.Orientation)
- sendResponse(PacketCoding.CreateGamePacket(0, ObjectDetachMessage(player.GUID, item2.GUID, pos, 0f, 0f, orient.z))) //ground
+ val playerOrient = player.Orientation
+ val orient : Vector3 = Vector3(0f, 0f, playerOrient.z)
+ continent.Actor ! Zone.DropItemOnGround(item2, pos, orient)
+ sendResponse(PacketCoding.CreateGamePacket(0, ObjectDetachMessage(player.GUID, item2.GUID, pos, 0f, 0f, playerOrient.z))) //ground
avatarService ! AvatarServiceMessage(player.Continent, AvatarAction.EquipmentOnGround(player.GUID, pos, orient, item2))
}
@@ -971,9 +936,14 @@ class WorldSessionActor extends Actor with MDCContextAware {
case msg @ GenericObjectStateMsg(object_guid, unk1) =>
log.info("GenericObjectState: " + msg)
- case msg @ ItemTransactionMessage(terminal_guid, transaction_type, item_page, item_name, unk1, item_guid) =>
- terminal.Actor ! Terminal.Request(player, msg)
+ case msg @ ItemTransactionMessage(terminal_guid, _, _, _, _, _) =>
log.info("ItemTransaction: " + msg)
+ continent.GUID(terminal_guid) match {
+ case Some(term : Terminal) =>
+ term.Actor ! Terminal.Request(player, msg)
+ case Some(obj : PlanetSideGameObject) => ;
+ case None => ;
+ }
case msg @ FavoritesRequest(player_guid, unk, action, line, label) =>
if(player.GUID == player_guid) {
@@ -1191,7 +1161,7 @@ class WorldSessionActor extends Actor with MDCContextAware {
TaskResolver.GiveTask(
new Task() {
private val localObject = obj
- private val localAccessor = accessor
+ private val localAccessor = continent.GUID
override def isComplete : Task.Resolution.Value = {
try {
@@ -1267,7 +1237,7 @@ class WorldSessionActor extends Actor with MDCContextAware {
override def onSuccess() : Unit = {
val definition = localObject.Definition
- localAnnounce ! WorldSessionActor.ResponseToSelf(
+ localAnnounce ! ResponseToSelf(
PacketCoding.CreateGamePacket(0,
ObjectCreateDetailedMessage(
definition.ObjectId,
@@ -1305,13 +1275,22 @@ class WorldSessionActor extends Actor with MDCContextAware {
private val localAnnounce = self
override def isComplete : Task.Resolution.Value = {
- Task.Resolution.Incomplete
+ if(localPlayer.HasGUID) {
+ Task.Resolution.Success
+ }
+ else {
+ Task.Resolution.Incomplete
+ }
}
def Execute(resolver : ActorRef) : Unit = {
localAnnounce ! PlayerLoaded(localPlayer) //alerts WSA
resolver ! scala.util.Success(localPlayer)
}
+
+ override def onFailure(ex : Throwable) : Unit = {
+ localAnnounce ! PlayerFailedToLoad(localPlayer) //alerts WSA
+ }
}, RegisterObjectTask(tplayer) +: (holsterTasks ++ fifthHolsterTask ++ inventoryTasks)
)
}
@@ -1326,7 +1305,7 @@ class WorldSessionActor extends Actor with MDCContextAware {
TaskResolver.GiveTask(
new Task() {
private val localObject = obj
- private val localAccessor = accessor
+ private val localAccessor = continent.GUID
override def isComplete : Task.Resolution.Value = {
try {
@@ -1403,7 +1382,7 @@ class WorldSessionActor extends Actor with MDCContextAware {
}
override def onSuccess() : Unit = {
- localAnnounce ! WorldSessionActor.ResponseToSelf(PacketCoding.CreateGamePacket(0, ObjectDeleteMessage(localObjectGUID, 0)))
+ localAnnounce ! ResponseToSelf(PacketCoding.CreateGamePacket(0, ObjectDeleteMessage(localObjectGUID, 0)))
if(0 <= localIndex && localIndex < 5) {
avatarService ! AvatarServiceMessage(localTarget.Continent, AvatarAction.ObjectDelete(localTarget.GUID, localObjectGUID))
}
@@ -1441,17 +1420,6 @@ class WorldSessionActor extends Actor with MDCContextAware {
tplayer.Holsters().foreach(holster => {
SetCharacterSelectScreenGUID_SelectEquipment(holster.Equipment, gen)
})
-// tplayer.Inventory.Items.foreach({ case((_, entry : InventoryItem)) =>
-// SetCharacterSelectScreenGUID_SelectEquipment(Some(entry.obj), gen)
-// })
-// tplayer.Slot(5).Equipment match {
-// case Some(locker) =>
-// locker.GUID = PlanetSideGUID(gen.getAndIncrement)
-// locker.asInstanceOf[LockerContainer].Inventory.Items.foreach({ case((_, entry : InventoryItem)) =>
-// SetCharacterSelectScreenGUID_SelectEquipment(Some(entry.obj), gen)
-// })
-// case None => ;
-// }
tplayer.GUID = PlanetSideGUID(gen.getAndIncrement)
}
@@ -1483,17 +1451,6 @@ class WorldSessionActor extends Actor with MDCContextAware {
tplayer.Holsters().foreach(holster => {
RemoveCharacterSelectScreenGUID_SelectEquipment(holster.Equipment)
})
-// tplayer.Inventory.Items.foreach({ case((_, entry : InventoryItem)) =>
-// RemoveCharacterSelectScreenGUID_SelectEquipment(Some(entry.obj))
-// })
-// tplayer.Slot(5).Equipment match {
-// case Some(locker) =>
-// locker.Invalidate()
-// locker.asInstanceOf[LockerContainer].Inventory.Items.foreach({ case((_, entry : InventoryItem)) =>
-// RemoveCharacterSelectScreenGUID_SelectEquipment(Some(entry.obj))
-// })
-// case None => ;
-// }
tplayer.Invalidate()
}
@@ -1514,64 +1471,9 @@ class WorldSessionActor extends Actor with MDCContextAware {
}
}
- /**
- * Add an object to the local `List` of objects on the ground.
- * @param item the `Equipment` to be dropped
- * @param pos where the `item` will be dropped
- * @param orient in what direction the item will face when dropped
- * @return the global unique identifier of the object
- */
- private def DropItemOnGround(item : Equipment, pos : Vector3, orient : Vector3) : PlanetSideGUID = {
- item.Position = pos
- item.Orientation = orient
- WorldSessionActor.equipmentOnGround += item
- item.GUID
- }
-
- // private def FindItemOnGround(item_guid : PlanetSideGUID) : Option[Equipment] = {
- // equipmentOnGround.find(item => item.GUID == item_guid)
- // }
-
- /**
- * Remove an object from the local `List` of objects on the ground.
- * @param item_guid the `Equipment` to be picked up
- * @return the object being picked up
- */
- private def PickupItemFromGround(item_guid : PlanetSideGUID) : Option[Equipment] = {
- recursiveFindItemOnGround(WorldSessionActor.equipmentOnGround.iterator, item_guid) match {
- case Some(index) =>
- Some(WorldSessionActor.equipmentOnGround.remove(index))
- case None =>
- None
- }
- }
-
- /**
- * Shift through objects on the ground to find the location of a specific item.
- * @param iter an `Iterator` of `Equipment`
- * @param item_guid the global unique identifier of the piece of `Equipment` being sought
- * @param index the current position in the array-list structure used to create the `Iterator`
- * @return the index of the object matching `item_guid`, if found;
- * `None`, otherwise
- */
- @tailrec private def recursiveFindItemOnGround(iter : Iterator[Equipment], item_guid : PlanetSideGUID, index : Int = 0) : Option[Int] = {
- if(!iter.hasNext) {
- None
- }
- else {
- val item : Equipment = iter.next
- if(item.GUID == item_guid) {
- Some(index)
- }
- else {
- recursiveFindItemOnGround(iter, item_guid, index + 1)
- }
- }
- }
-
def failWithError(error : String) = {
log.error(error)
- //sendResponse(PacketCoding.CreateControlPacket(ConnectionClose()))
+ sendResponse(PacketCoding.CreateControlPacket(ConnectionClose()))
}
def sendResponse(cont : PlanetSidePacketContainer) : Unit = {
@@ -1593,6 +1495,13 @@ class WorldSessionActor extends Actor with MDCContextAware {
object WorldSessionActor {
final case class ResponseToSelf(pkt : GamePacket)
+ private final case class PokeClient()
+ private final case class ServerLoaded()
+ private final case class PlayerLoaded(tplayer : Player)
+ private final case class PlayerFailedToLoad(tplayer : Player)
+ private final case class ListAccountCharacters()
+ private final case class SetCurrentAvatar(tplayer : Player)
+
/**
* A placeholder `Cancellable` object.
*/
@@ -1601,12 +1510,6 @@ object WorldSessionActor {
def isCancelled() : Boolean = true
}
- //TODO this is a temporary local system; replace it in the future
- //in the future, items dropped on the ground will be managed by a data structure on an external Actor representing the continent
- //like so: WSA -> /GetItemOnGround/ -> continent -> /GiveItemFromGround/ -> WSA
- import scala.collection.mutable.ListBuffer
- private val equipmentOnGround : ListBuffer[Equipment] = ListBuffer[Equipment]()
-
def Distance(pos1 : Vector3, pos2 : Vector3) : Float = {
math.sqrt(DistanceSquared(pos1, pos2)).toFloat
}