diff --git a/common/src/main/scala/net/psforever/objects/ObjectType.scala b/common/src/main/scala/net/psforever/objects/ObjectType.scala new file mode 100644 index 00000000..b7df4a72 --- /dev/null +++ b/common/src/main/scala/net/psforever/objects/ObjectType.scala @@ -0,0 +1,96 @@ +// Copyright (c) 2017 PSForever +package net.psforever.objects + +object ObjectType extends Enumeration { + type Value = String + + val AmbientSoundSource = "ambient_sound_source" + val Ammunition = "ammunition" + val AnimatedBarrier = "animated_barrier" + val Applicator = "applicator" + val Armor = "armor" + val ArmorSiphon = "armor_siphon" + val AwardStatistic = "award_statistic" + val Avatar = "avatar" + val AvatarBot = "avatar_bot" + val Ball = "ball" + val Bank = "bank" + val Barrier = "barrier" + val BfrTerminal = "bfr_terminal" + val Billboard = "billboard" + val Boomer = "boomer" + val BoomerTrigger = "boomer_trigger" + val Building = "building" + val CaptureFlag = "capture_flag" + val CaptureFlagSocket = "capture_flag_socket" + val CaptureTerminal = "capture_terminal" + val CertTerminal = "cert_terminal" + val ChainLashDamager = "chain_lash_damager" + val Dispenser = "dispenser" + val Door = "door" + val EmpBlast = "emp_blast" + val FrameVehicle = "framevehicle" + val Flag = "flag" + val FlightVehicle = "flightvehicle" + val ForceDome = "forcedome" + val ForceDomeGenerator = "forcedomegenerator" + val Game = "game" + val Generic = "generic" + val GenericTeleportion = "generic_teleportation" + val GeneratorTerminal = "generator_terminal" + val GsGenbase = "GS_genbase" + val HandGrenade = "hand_grenade" + val HeMine = "he_mine" + val HeavyWeapon = "heavy_weapon" + val HoverVehicle = "hovervehicle" + val Implant = "implant" + val ImplantInterfaceTerminal = "implant_terminal_interface" + val Lazer = "lazer" + val Locker = "locker" + val LockerContainer = "locker_container" + val LockExternal = "lock_external" + val LockSmall = "lock_small" + val MainTerminal = "main_terminal" + val Map = "map" + val MedicalTerminal = "medical_terminal" + val Medkit = "medkit" + val Monolith = "monolith" + val MonolithUnit = "monolith_unit" + val MotionAlarmSensorDest = "motion_alarm_sensor_dest" + val NanoDispenser = "nano_dispenser" + val NtuSipon = "ntu_siphon" + val OrbitalShuttlePad = "orbital_shuttle_pad" + val OrbitalStrike = "orbital_strike" + val OrderTerminal = "order_terminal" + val PainTerminal = "pain_terminal" + val Projectile = "projectile" + val RadiationCloud = "radiation_cloud" + val RearmTerminal = "rearm_terminal" + val RechargeTerminal = "recharge_terminal" + val Rek = "rek" + val RepairTerminal = "repair_terminal" + val ResourceSilo = "resource_silo" + val RespawnTube = "respawn_tube" + val SensorShield = "sensor_shield" + val ShieldGenerator = "shield_generator" + val Shifter = "shifter" + val SkyDome = "skydome" + val SpawnPlayer = "spawn_player" + val SpawnPoint = "spawn_point" + val SpawnTerminal = "spawn_terminal" + val TeleportPad = "teleport_pad" + val Terminal = "terminal" + val TradeContainer = "trade_container" + val UplinkDevice = "uplink_device" + val VanuCradleClass = "vanu_cradle_class" + val VanuModuleClass = "vanu_module_class" + val VanuModuleFactory = "vanu_module_factory" + val VanuReceptacleClass = "vanu_receptacle_class" + val Vehicle = "vehicle" + val VehicleCreationPad = "vehicle_creation_pad" + val VehicleLandingPad = "vehicle_landing_pad" + val VehicleTerminal = "vehicle_terminal" + val Warpgate = "waprgate" + val WarpZone = "warp_zone" + val Weapon = "weapon" +} diff --git a/common/src/main/scala/net/psforever/objects/guid/NumberPoolHub.scala b/common/src/main/scala/net/psforever/objects/guid/NumberPoolHub.scala index e360df53..39054335 100644 --- a/common/src/main/scala/net/psforever/objects/guid/NumberPoolHub.scala +++ b/common/src/main/scala/net/psforever/objects/guid/NumberPoolHub.scala @@ -60,14 +60,18 @@ class NumberPoolHub(private val source : NumberSource) { * @param name the name of the pool * @param pool the `List` of numbers that will belong to the pool * @return the newly-created number pool - * @throws IllegalArgumentException if the pool is already defined; - * if the pool contains numbers the source does not + * @throws IllegalArgumentException if the pool's name is already defined; + * if the pool is (already) empty; + * if the pool contains numbers the source does not; * if the pool contains numbers from already existing pools */ def AddPool(name : String, pool : List[Int]) : NumberPool = { if(hash.get(name).isDefined) { throw new IllegalArgumentException(s"can not add pool $name - name already known to this hub?") } + if(pool.isEmpty) { + throw new IllegalArgumentException(s"can not add empty pool $name") + } if(source.Size <= pool.max) { throw new IllegalArgumentException(s"can not add pool $name - max(pool) is greater than source.size") } @@ -203,8 +207,8 @@ class NumberPoolHub(private val source : NumberSource) { val slctr = pool.Selector import net.psforever.objects.guid.selector.SpecificSelector val specific = new SpecificSelector - specific.SelectionIndex = number pool.Selector = specific + specific.SelectionIndex = number pool.Get() pool.Selector = slctr register_GetAvailableNumberFromSource(number) diff --git a/common/src/main/scala/net/psforever/objects/guid/actor/IsRegistered.scala b/common/src/main/scala/net/psforever/objects/guid/actor/IsRegistered.scala deleted file mode 100644 index b680c469..00000000 --- a/common/src/main/scala/net/psforever/objects/guid/actor/IsRegistered.scala +++ /dev/null @@ -1,31 +0,0 @@ -// Copyright (c) 2017 PSForever -package net.psforever.objects.guid.actor - -import net.psforever.objects.entity.IdentifiableEntity - -/** - * A message for requesting information about the registration status of an object or a number. - * @param obj the optional object - * @param number the optional number - */ -final case class IsRegistered(obj : Option[IdentifiableEntity], number : Option[Int]) - -object IsRegistered { - /** - * Overloaded constructor for querying an object's status. - * @param obj the object - * @return an `IsRegistered` object - */ - def apply(obj : IdentifiableEntity) : IsRegistered = { - new IsRegistered(Some(obj), None) - } - - /** - * Overloaded constructor for querying a number's status. - * @param number the number - * @return an `IsRegistered` object - */ - def apply(number : Int) : IsRegistered = { - new IsRegistered(None, Some(number)) - } -} diff --git a/common/src/main/scala/net/psforever/objects/guid/actor/NumberPoolAccessorActor.scala b/common/src/main/scala/net/psforever/objects/guid/actor/NumberPoolAccessorActor.scala deleted file mode 100644 index ec35632c..00000000 --- a/common/src/main/scala/net/psforever/objects/guid/actor/NumberPoolAccessorActor.scala +++ /dev/null @@ -1,216 +0,0 @@ -// Copyright (c) 2017 PSForever -package net.psforever.objects.guid.actor - -import akka.actor.{Actor, ActorRef} -import akka.pattern.ask -import akka.util.Timeout -import net.psforever.objects.entity.IdentifiableEntity -import net.psforever.objects.guid.NumberPoolHub -import net.psforever.objects.guid.pool.NumberPool - -import scala.concurrent.duration._ -import scala.util.{Failure, Success} - -/** - * An `Actor` that wraps around the `Actor` for a `NumberPool` and automates a portion of the number registration process.
- *
- * The `NumberPoolActor` that is created is used as the synchronized "gate" through which the number selection process occurs. - * This `Actor` `ask`s the internal `Actor` and then waits on that `Future` to resolve. - * For the registration process, once it resolves, a number for the accompanying object has been chosen. - * The last part involves configuring the `NumberSource` of the hub so that it knows. - * For the process of revoking registration, the number from the object is returned to the pool. - * Like during the registration process, the `NumberSource` is then also updated.
- *
- * The object is always registered using the underlying governed `NumberPool`. - * The object will not unregister if the object or its number are not recognized as members previously registered to the `NumberPool`.
- * Whether or not an object or a specific number has been registered is always possible. - * The scope encompasses the whole of the associated `NumberSource` as opposed to just this `NumberPool`. - * @param hub the `NumberPoolHub` this `Actor` manipulates - * @param pool the specific `NumberPool` this `Actor` maintains - * @param poolActor a shared `Actor` that governs this `NumberPool` - */ -class NumberPoolAccessorActor(private val hub : NumberPoolHub, private val pool : NumberPool, private val poolActor : ActorRef) extends Actor { - //the timeout is for when we ask the poolActor - private implicit val timeout = Timeout(50 milliseconds) - private[this] val log = org.log4s.getLogger - - private final case class GUIDRequest(obj : IdentifiableEntity, replyTo : ActorRef) - private val requestQueue : collection.mutable.LongMap[GUIDRequest] = new collection.mutable.LongMap() - private var index : Long = Long.MinValue - - def receive : Receive = { - //register - case Register(obj, _, None, call) => - try { - obj.GUID //stop if object has a GUID; sometimes this happens - log.warn(s"$obj already registered") - } - catch { - case _ : Exception => - val id : Long = index - index += 1 - requestQueue += id -> GUIDRequest(obj, call.getOrElse(sender())) - poolActor ! NumberPoolActor.GetAnyNumber(Some(id)) - } - - case Register(obj, _, Some(number), call) => - try { - obj.GUID //stop if object has a GUID; sometimes this happens - log.warn(s"$obj already registered") - } - catch { - case _ : Exception => - val id : Long = index - index += 1 - requestQueue += id -> GUIDRequest(obj, call.getOrElse(sender())) - poolActor ! NumberPoolActor.GetSpecificNumber(number, Some(id)) - } - - case NumberPoolActor.GiveNumber(number, id) => - id match { - case Some(nid : Long) => - Register(nid, requestQueue.remove(nid), number) - case _ => - pool.Return(number) //recovery? - log.warn(s"received a number but there is no request to process it; returning number to pool") - } - - case NumberPoolActor.NoNumber(ex, id) => - val req = id match { - case Some(nid : Long) => - val req = requestQueue.remove(nid) - if(req.isDefined) { s"$req" } else { s"a corresponding request $nid was not found;" } - case _ => - "generic request;" //should be unreachable - } - log.warn(s"a number was not drawn from the pool; $req $ex") - - //unregister - case Unregister(obj, call) => - val callback = call.getOrElse(sender()) - try { - val number = obj.GUID.guid - if(pool.Numbers.contains(number) && hub.WhichPool(obj).isDefined) { - val id : Long = index - index += 1 - requestQueue += id -> GUIDRequest(obj, callback) - poolActor ! NumberPoolActor.ReturnNumber(number, Some(id)) - } - else { - callback ! Failure(new Exception(s"the GUID of object $obj - $number - is not a part of this number pool")) - } - } - catch { - case msg : Exception => - callback ! Failure(msg) - } - - case NumberPoolActor.ReturnNumberResult(number, None, id) => - id match { - case Some(nid : Long) => - Unregister(nid, requestQueue.remove(nid), number) - case _ => - NumberPoolActor.GetSpecificNumber(pool, number) //recovery? - log.error(s"returned a number but there is no request to process it; recovering the number from pool") - } - - case NumberPoolActor.ReturnNumberResult(number, ex, id) => - val req = id match { - case Some(nid : Long) => - val req = requestQueue.remove(nid) - if(req.isDefined) { s"$req" } else { s"a corresponding request $nid was not found;" } - case _ => - "generic request;" //should be unreachable - } - log.warn(s"a number $number was not returned to the pool; $req $ex") - - //common - case IsRegistered(Some(obj), None) => - sender ! hub.isRegistered(obj) - - case IsRegistered(None, Some(number)) => - sender ! hub.isRegistered(number) - - case NumberPoolActor.ReturnNumber(number, _) => - sender ! (poolActor ? NumberPoolActor.ReturnNumber(number)) - - case msg => - log.warn(s"unexpected message received - $msg") - } - - /** - * A step of the object registration process. - * If there is a successful request object to be found, complete the registration request. - * @param id the identifier of this request - * @param request the request data - * @param number the number that was drawn from the `NumberPool` - */ - private def Register(id : Long, request : Option[GUIDRequest], number : Int) : Unit = { - request match { - case Some(GUIDRequest(obj, replyTo)) => - processRegisterResult(obj, number, replyTo) - case None => - pool.Return(number) //recovery? - log.warn(s"received a number but the request for it is missing; returning number to pool") - } - } - - /** - * A step of the object registration process. - * This step completes the registration by consulting the `NumberSource`. - * @param obj the object - * @param number the number to use - * @param callback an optional callback `ActorRef` - */ - private def processRegisterResult(obj : IdentifiableEntity, number : Int, callback : ActorRef) : Unit = { - try { - obj.GUID - pool.Return(number) //recovery? - callback ! Success(obj) - } - catch { - case _ : Exception => - hub.latterPartRegister(obj, number) match { - case Success(_) => - callback ! Success(obj) - case Failure(ex) => - pool.Return(number) //recovery? - callback ! Failure(ex) - } - } - } - - /** - * A step of the object un-registration process. - * If there is a successful request object to be found, complete the registration request. - * @param id the identifier of this request - * @param request the request data - * @param number the number that was drawn from the `NumberPool` - */ - private def Unregister(id : Long, request : Option[GUIDRequest], number : Int) : Unit = { - request match { - case Some(GUIDRequest(obj, replyTo)) => - processUnregisterResult(obj, obj.GUID.guid, replyTo) - case None => - NumberPoolActor.GetSpecificNumber(pool, number) //recovery? - log.error(s"returned a number but the rest of the request is missing; recovering the number from pool") - } - } - - /** - * A step of the object un-registration process. - * This step completes revoking the object's registration by consulting the `NumberSource`. - * @param obj the object - * @param callback an optional callback `ActorRef` - */ - private def processUnregisterResult(obj : IdentifiableEntity, number : Int, callback : ActorRef) : Unit = { - hub.latterPartUnregister(number) match { - case Some(_) => - obj.Invalidate() - callback ! Success(obj) - case None => - NumberPoolActor.GetSpecificNumber(pool, number) //recovery? - callback ! Failure(new Exception(s"failed to unregister a number; this may be a critical error")) - } - } -} diff --git a/common/src/main/scala/net/psforever/objects/guid/actor/NumberPoolHubActor.scala b/common/src/main/scala/net/psforever/objects/guid/actor/NumberPoolHubActor.scala deleted file mode 100644 index d1a9af12..00000000 --- a/common/src/main/scala/net/psforever/objects/guid/actor/NumberPoolHubActor.scala +++ /dev/null @@ -1,212 +0,0 @@ -// Copyright (c) 2017 PSForever -package net.psforever.objects.guid.actor - -import akka.pattern.ask -import akka.util.Timeout -import akka.actor.{Actor, ActorRef, Props} - -import net.psforever.objects.entity.IdentifiableEntity -import net.psforever.objects.guid.NumberPoolHub -import net.psforever.objects.guid.pool.NumberPool - -import scala.collection.mutable -import scala.concurrent.Future -import scala.concurrent.duration._ -import scala.util.{Failure, Success, Try} - -/** - * An incoming message for retrieving a specific `NumberPoolAccessorActor`. - * @param name the name of the accessor's `NumberPool` - */ -final case class RequestPoolActor(name : String) - -/** - * An outgoing message for giving a specific `NumberPoolAccessorActor`. - * @param name the name of the accessor's `NumberPool`, for reference - * @param actor the accessor - */ -final case class DeliverPoolActor(name : String, actor : ActorRef) - -/** - * An `Actor` that wraps around the management system for `NumberPools`.
- *
- * By just instantiating, this object builds and stores a `NumberPoolAccessorActor` for each `NumberPool` known to the `hub`. - * Additional `NumberPool`s created by the `hub` need to be paired with a created accessor manually. - * Each accessor is the primary entry point to a registration process for the specific `NumberPool` it represents. - * The `hub` `Actor` itself distribute any registration task it receives out to an applicable accessor of which it is aware. - * It will attempt to revoke registration on its own, without relying on the functionality from any accessor.
- *
- * In the same way that `NumberPoolHub` is a tool for keeping track of `NumberPool` objects, - * its `Actor` is a tool for keeping track of accessors created from `NumberPool` objects. - * It is very, however, for handling unspecific revoke tasks. - * @param hub the central `NumberPool` management object for an embedded `NumberSource` object - */ -class NumberPoolHubActor(private val hub : NumberPoolHub) extends Actor { - private val actorHash : mutable.HashMap[String, ActorRef] = mutable.HashMap[String, ActorRef]() - hub.Pools.foreach({ case(name, pool) => CreatePoolActor(name, pool) }) - implicit val timeout = Timeout(50 milliseconds) - private[this] val log = org.log4s.getLogger - - def receive : Receive = { - case RequestPoolActor(name) => - sender ! (GetPoolActor(name) match { - case Success(poolActor) => - DeliverPoolActor(name, poolActor) - case Failure(ex) => - Failure(ex) - }) - - case Register(obj, name, None, callback) => - HubRegister(obj, name, callback) - - case Register(obj, name, Some(number), callback) => - HubRegister(obj, name, number, callback) - - //common - case IsRegistered(Some(obj), None) => - sender ! hub.isRegistered(obj) - - case IsRegistered(None, Some(number)) => - sender ! hub.isRegistered(number) - - case Unregister(obj, callback) => - Unregister(obj, if(callback.isEmpty) { sender } else { callback.get }) - - case msg => - log.warn(s"unexpected message received - ${msg.toString}") - } - - /** - * From a name, find an existing `NumberPoolAccessorActor`. - * @param name the accessor's name - * @return the accessor that was requested - */ - private def GetPoolActor(name : String) : Try[ActorRef] = { - actorHash.get(name) match { - case Some(actor) => - Success(actor) - case _ => - Failure(new Exception(s"number pool $name not defined")) - } - } - - /** - * Create a new `NumberPoolAccessorActor` and add it to the local collection of accessors. - * @param name the accessor's name - * @param pool the underlying `NumberPool` - */ - private def CreatePoolActor(name : String, pool : NumberPool) : Unit = { - actorHash.get(name) match { - case None => - actorHash += name -> context.actorOf(Props(classOf[NumberPoolAccessorActor], hub, pool), s"${name}Actor") - case Some(_) => - //TODO complain? - } - } - - /** - * A step of the object registration process. - * Select a valid `NumberPoolAccessorActor` and pass a task onto it. - * @param obj an object - * @param name a potential accessor pool - * @param callback an optional callback `ActorRef` - */ - private def HubRegister(obj : IdentifiableEntity, name : Option[String], callback : Option[ActorRef]) : Unit = { - val genericPool = actorHash("generic") - val pool = if(name.isDefined) { actorHash.get(name.get).orElse(Some(genericPool)).get } else { genericPool } - pool ! Register(obj, None, None, callback) - } - - /** - * A step of the object registration process. - * Determine to which `NumberPool` the `number` belongs. - * @param obj an object - * @param name a potential accessor pool - * @param number a potential number - * @param callback an optional callback `ActorRef` - */ - private def HubRegister(obj : IdentifiableEntity, name : Option[String], number : Int, callback : Option[ActorRef]) : Unit = { - hub.WhichPool(number) match { - case Some(poolname) => - HubRegister_GetActor(obj, name, poolname, number, callback) - case None => - self ! Register(obj, name, None, callback) - } - } - - /** - * A step of the object registration process. - * Pass a task onto an accessor or, if the accessor can not be found, attempt to recover. - * @param obj an object - * @param name a potential accessor pool - * @param poolname the suggested accessor pool - * @param number a potential number - * @param callback an optional callback `ActorRef` - */ - private def HubRegister_GetActor(obj : IdentifiableEntity, name : Option[String], poolname : String, number : Int, callback : Option[ActorRef]) : Unit = { - actorHash.get(poolname) match { - case Some(pool) => - pool ! Register(obj, None, Some(number), callback) - case None => - HubRegister_MissingActor(obj, name, poolname, number, callback) - } - } - - /** - * A step of the object registration process. - * If an accessor could not be found in the last step, attempt to create the accessor. - * If the accessor can not be created, the `number` can not be used; - * fall back on the original pool (`name`). - * @param obj an object - * @param name a potential accessor pool - * @param poolname the suggested accessor pool - * @param number a potential number - * @param callback an optional callback `ActorRef` - */ - private def HubRegister_MissingActor(obj : IdentifiableEntity, name : Option[String], poolname : String, number : Int, callback : Option[ActorRef]) : Unit = { - hub.GetPool(poolname) match { - case Some(pool) => - CreatePoolActor(poolname, pool) - actorHash(poolname) ! Register(obj, None, Some(number), callback) - case None => - log.error(s"matched number $number to pool $poolname, but could not find $poolname when asked") - self ! Register(obj, name, None, callback) - } - } - - /** - * A step of the object un-registration process. - * This step locates the `NumberPool` to which this object is a member. - * If found, it prepares a `Future` to resolve later regarding whether the `NumberPool` accepted the number. - * @param obj the object - * @param callback a callback `ActorRef` - */ - private def Unregister(obj : IdentifiableEntity, callback : ActorRef) : Unit = { - hub.WhichPool(obj) match { - case Some(name) => - val objToUnregister = obj - val poolName = name - processUnregisterResult(objToUnregister, (actorHash(poolName) ? NumberPoolActor.ReturnNumber(objToUnregister.GUID.guid)).mapTo[Boolean], callback) - case None => - callback ! UnregisterFailure(obj, new Exception("could not find pool object is member of")) - } - } - - /** - * A step of the object un-registration process. - * This step completes revoking the object's registration by consulting the `NumberSource`. - * @param obj the object - * @param result whether the number was returned in the last step - * @param callback a callback `ActorRef` - */ - private def processUnregisterResult(obj : IdentifiableEntity, result : Future[Boolean], callback : ActorRef) : Unit = { - import scala.concurrent.ExecutionContext.Implicits.global - result.foreach { - case true => - hub.latterPartUnregister(obj.GUID.guid) - callback ! UnregisterSuccess(obj) - case false => - callback ! UnregisterFailure(obj, new Exception("could not find object to remove")) - } - } -} diff --git a/common/src/main/scala/net/psforever/objects/guid/actor/UniqueNumberSystem.scala b/common/src/main/scala/net/psforever/objects/guid/actor/UniqueNumberSystem.scala new file mode 100644 index 00000000..fcd16c17 --- /dev/null +++ b/common/src/main/scala/net/psforever/objects/guid/actor/UniqueNumberSystem.scala @@ -0,0 +1,348 @@ +// Copyright (c) 2017 PSForever +package net.psforever.objects.guid.actor + +import akka.actor.{Actor, ActorContext, ActorRef, Props} +import net.psforever.objects.entity.IdentifiableEntity +import net.psforever.objects.guid.NumberPoolHub + +import scala.util.{Failure, Success} + +/** + * An `Actor` that wraps around converted `NumberPool`s and synchronizes a portion of the number registration process. + * The ultimate goal is to manage a coherent group of unique identifiers for a given "region" (`Zone`). + * Both parts of the UID system sit atop the `Zone` for easy external access. + * The plain part - the `NumberPoolHub` here - is used for low-priority requests such as checking for existing associations. + * This `Actor` is the involved portion that paces registration and unregistration.
+ *
+ * A four part process is used for object registration tasks. + * First, the requested `NumberPool` is located among the list of known `NumberPool`s. + * Second, an asynchronous request is sent to that pool to retrieve a number. + * (Only any number. Only a failing case allows for selection of a specific number.) + * Third, the asynchronous request returns and the original information about the request is recovered. + * Fourth, both sides of the contract are completed by the object being assigned the number and + * the underlying "number source" is made to remember an association between the object and the number. + * Short circuits and recoveries as available on all steps though reporting is split between logging and callbacks. + * The process of removing the association between a number and object (unregistering) is a similar four part process.
+ *
+ * The important relationship between this `Actor` and the `Map` of `NumberPoolActors` is an "gate." + * A single `Map` is constructed and shared between multiple entry points to the UID system where requests are messaged. + * Multiple entry points send messages to the same `NumberPool`. + * That `NumberPool` deals with the messages one at a time and sends reply to each entry point that communicated with it. + * This process is almost as fast as the process of the `NumberPool` selecting a number. + * (At least, both should be fast.) + * @param guid the `NumberPoolHub` that is partially manipulated by this `Actor` + * @param poolActors a common mapping created from the `NumberPool`s in `guid`; + * there is currently no check for this condition save for requests failing + */ +class UniqueNumberSystem(private val guid : NumberPoolHub, private val poolActors : Map[String, ActorRef]) extends Actor { + /** Information about Register and Unregister requests that persists between messages to a specific `NumberPool`. */ + private val requestQueue : collection.mutable.LongMap[UniqueNumberSystem.GUIDRequest] = new collection.mutable.LongMap() + /** The current value for the next request entry's index. */ + private var index : Long = Long.MinValue + private[this] val log = org.log4s.getLogger + + def receive : Receive = { + case Register(obj, Some(pname), None, call) => + val callback = call.getOrElse(sender()) + try { + obj.GUID //stop if object already has a GUID; sometimes this happens + AlreadyRegistered(obj, pname) + callback ! Success(obj) + } + catch { + case _ : Exception => + val id : Long = index + index += 1 + requestQueue += id -> UniqueNumberSystem.GUIDRequest(obj, pname, callback) + RegistrationProcess(pname, id) + } + + //this message is automatically sent by NumberPoolActor + case NumberPoolActor.GiveNumber(number, id) => + id match { + case Some(nid : Long) => + RegistrationProcess(requestQueue.remove(nid), number, nid) + case _ => + log.warn(s"received a number but there is no request to process it; returning number to pool") + NoCallbackReturnNumber(number) //recovery? + //no callback is possible + } + + //this message is automatically sent by NumberPoolActor + case NumberPoolActor.NoNumber(ex, id) => + id match { + case Some(nid : Long) => + requestQueue.remove(nid) match { + case Some(entry) => + entry.replyTo ! Failure(ex) //ONLY callback that is possible + case None => ; + log.warn(s"failed number request and no record of number request - $ex") //neither a successful request nor an entry of making the request + } + case None => ; + log.warn(s"failed number request and no record of number request - $ex") //neither a successful request nor an entry of making the request + case _ => ; + log.warn(s"unrecognized request $id accompanying a failed number request - $ex") + } + + case Unregister(obj, call) => + val callback = call.getOrElse(sender()) + try { + val number = obj.GUID.guid + guid.WhichPool(number) match { + case Some(pname) => + val id : Long = index + index += 1 + requestQueue += id -> UniqueNumberSystem.GUIDRequest(obj, pname, callback) + UnregistrationProcess(pname, number, id) + case None => + callback ! Failure(new Exception(s"the GUID of object $obj - $number - is not a part of this number pool")) + } + } + catch { + case _ : Exception => + log.info(s"$obj is already unregistered") + callback ! Success(obj) + } + + //this message is automatically sent by NumberPoolActor + case NumberPoolActor.ReturnNumberResult(number, None, id) => + id match { + case Some(nid : Long) => + UnregistrationProcess(requestQueue.remove(nid), number, nid) + case _ => + log.error(s"returned a number but there is no request to process it; recovering the number from pool") + NoCallbackGetSpecificNumber(number) //recovery? + //no callback is possible + } + + //this message is automatically sent by NumberPoolActor + case NumberPoolActor.ReturnNumberResult(number, Some(ex), id) => //if there is a problem when returning the number + id match { + case Some(nid : Long) => + requestQueue.remove(nid) match { + case Some(entry) => + entry.replyTo ! Failure(new Exception(s"for ${entry.target} with number $number, ${ex.getMessage}")) + case None => ; + log.error(s"could not find original request $nid that caused error $ex, but pool was $sender") + //no callback is possible + } + case _ => ; + log.error(s"could not find original request $id that caused error $ex, but pool was $sender") + //no callback is possible + } + + case msg => + log.warn(s"unexpected message received - $msg") + } + + /** + * A step of the object registration process. + * Send a message to the `NumberPool` to request a number back. + * @param poolName the pool to which the object is trying to register + * @param id a potential identifier to associate this request + */ + private def RegistrationProcess(poolName : String, id : Long) : Unit = { + poolActors.get(poolName) match { + case Some(pool) => + pool ! NumberPoolActor.GetAnyNumber(Some(id)) + case None => + //do not log; use callback + requestQueue.remove(id).get.replyTo ! Failure(new Exception(s"can not find pool $poolName; nothing was registered")) + } + } + + /** + * A step of the object registration process. + * If there is a successful request object to be found, continue the registration request. + * @param request the original request data + * @param number the number that was drawn from a `NumberPool` + */ + private def RegistrationProcess(request : Option[UniqueNumberSystem.GUIDRequest], number : Int, id : Long) : Unit = { + request match { + case Some(entry) => + processRegisterResult(entry, number) + case None => + log.error(s"returned a number but the rest of the request is missing (id:$id)") + if(id != Long.MinValue) { //check to ignore endless loop of error-catching + log.warn("returning number to pool") + NoCallbackReturnNumber(number) //recovery? + //no callback is possible + } + } + } + + /** + * A step of the object registration process. + * This step completes the registration by asking the `NumberPoolHub` to sort out its `NumberSource`. + * @param entry the original request data + * @param number the number to use + */ + private def processRegisterResult(entry : UniqueNumberSystem.GUIDRequest, number : Int) : Unit = { + val obj = entry.target + guid.latterPartRegister(obj, number) match { + case Success(_) => + entry.replyTo ! Success(obj) + case Failure(ex) => + //do not log; use callback + NoCallbackReturnNumber(number, entry.targetPool) //recovery? + entry.replyTo ! Failure(ex) + } + } + + /** + * A step of the object unregistration process. + * Send a message to the `NumberPool` to restore the availability of one of its numbers. + * @param poolName the pool to which the number will try to be returned + * @param number the number that was previously drawn from the specified `NumberPool` + * @param id a potential identifier to associate this request + */ + private def UnregistrationProcess(poolName : String, number : Int, id : Long) : Unit = { + poolActors.get(poolName) match { + case Some(pool) => + pool ! NumberPoolActor.ReturnNumber(number, Some(id)) + case None => + //do not log; use callback + requestQueue.remove(id).get.replyTo ! Failure(new Exception(s"can not find pool $poolName; nothing was de-registered")) + } + } + + /** + * A step of the object unregistration process. + * If there is a successful request object to be found, continue the registration request. + * @param request the original request data + * @param number the number that was drawn from the `NumberPool` + */ + private def UnregistrationProcess(request : Option[UniqueNumberSystem.GUIDRequest], number : Int, id : Long) : Unit = { + request match { + case Some(entry) => + processUnregisterResult(entry, number) + case None => + log.error(s"returned a number but the rest of the request is missing (id:$id)") + if(id != Long.MinValue) { //check to ignore endless loop of error-catching + log.error("recovering the number from pool") + NoCallbackGetSpecificNumber(number) //recovery? + //no callback is possible + } + } + } + + /** + * A step of the object unregistration process. + * This step completes revoking of the object's registration by consulting the `NumberSource`. + * @param entry the original request data + * @param number the number to use + */ + private def processUnregisterResult(entry : UniqueNumberSystem.GUIDRequest, number : Int) : Unit = { + val obj = entry.target + guid.latterPartUnregister(number) match { + case Some(_) => + obj.Invalidate() + entry.replyTo ! Success(obj) + case None => + //do not log; use callback + NoCallbackGetSpecificNumber(number, entry.targetPool) //recovery? + entry.replyTo ! Failure(new Exception(s"failed to unregister a number; this may be a critical error")) + } + } + + /** + * Generate a relevant logging message for an object that is trying to register is actually already registered. + * @param obj the object that was trying to register + * @param poolName the pool to which the object was trying to register + */ + private def AlreadyRegistered(obj : IdentifiableEntity, poolName : String) : Unit = { + val msg = + guid.WhichPool(obj) match { + case Some(pname) => + if(poolName.equals(pname)) { + s"to pool $poolName" + } + else { + s"but to different pool $pname" + } + case None => + "but not to any pool known to this system" + } + log.warn(s"$obj already registered $msg") + } + + /** + * Access a specific `NumberPool` in a way that doesn't invoke a callback and reset one of its numbers. + * @param number the number that was drawn from a `NumberPool` + */ + private def NoCallbackReturnNumber(number : Int) : Unit = { + guid.WhichPool(number) match { + case Some(pname) => + NoCallbackReturnNumber(number, pname) + case None => + log.error(s"critical: tried to return number $number but could not find containing pool") + } + } + + /** + * Access a specific `NumberPool` in a way that doesn't invoke a callback and reset one of its numbers. + * To avoid fully processing the callback, an id of `Long.MinValue` is used to short circuit the routine. + * @param number the number that was drawn from a `NumberPool` + * @param poolName the `NumberPool` from which the `number` was drawn + * @see `UniqueNumberSystem.UnregistrationProcess(Option[GUIDRequest], Int, Int)` + */ + private def NoCallbackReturnNumber(number : Int, poolName : String) : Unit = { + poolActors.get(poolName) match { + case Some(pool) => + pool ! NumberPoolActor.ReturnNumber(number, Some(Long.MinValue)) + case None => + log.error(s"critical: tried to return number $number but did not find pool $poolName") + } + } + + /** + * Access a specific `NumberPool` in a way that doesn't invoke a callback and claim one of its numbers. + * @param number the number to be drawn from a `NumberPool` + */ + private def NoCallbackGetSpecificNumber(number : Int) : Unit = { + guid.WhichPool(number) match { + case Some(pname) => + NoCallbackGetSpecificNumber(number, pname) + case None => + log.error(s"critical: tried to re-register number $number but could not find containing pool") + } + } + + /** + * Access a specific `NumberPool` in a way that doesn't invoke a callback and claim one of its numbers. + * To avoid fully processing the callback, an id of `Long.MinValue` is used to short circuit the routine. + * @param number the number to be drawn from a `NumberPool` + * @param poolName the `NumberPool` from which the `number` is to be drawn + * @see `UniqueNumberSystem.RegistrationProcess(Option[GUIDRequest], Int, Int)` + */ + private def NoCallbackGetSpecificNumber(number : Int, poolName : String) : Unit = { + poolActors.get(poolName) match { + case Some(pool) => + pool ! NumberPoolActor.GetSpecificNumber(number, Some(Long.MinValue)) + case None => + log.error(s"critical: tried to re-register number $number but did not find pool $poolName") + } + } +} + +object UniqueNumberSystem { + /** + * Persistent record of the important information between the time fo request and the time of reply. + * @param target the object + * @param targetPool the name of the `NumberPool` being used + * @param replyTo the callback `ActorRef` + */ + private final case class GUIDRequest(target : IdentifiableEntity, targetPool : String, replyTo : ActorRef) + + /** + * Transform `NumberPool`s into `NumberPoolActor`s and pair them with their name. + * @param poolSource where the raw `NumberPools` are located + * @param context used to create the `NumberPoolActor` instances + * @return a `Map` of the pool names to the `ActorRef` created from the `NumberPool` + */ + def AllocateNumberPoolActors(poolSource : NumberPoolHub)(implicit context : ActorContext) : Map[String, ActorRef] = { + poolSource.Pools.map({ case ((pname, pool)) => + pname -> context.actorOf(Props(classOf[NumberPoolActor], pool), pname) + }).toMap + } +} diff --git a/common/src/main/scala/net/psforever/objects/guid/actor/UnregisterFailure.scala b/common/src/main/scala/net/psforever/objects/guid/actor/UnregisterFailure.scala deleted file mode 100644 index 60eb0ce3..00000000 --- a/common/src/main/scala/net/psforever/objects/guid/actor/UnregisterFailure.scala +++ /dev/null @@ -1,11 +0,0 @@ -// Copyright (c) 2017 PSForever -package net.psforever.objects.guid.actor - -import net.psforever.objects.entity.IdentifiableEntity - -/** - * A message for when an object has failed to be unregistered for some reason. - * @param obj the object - * @param ex the reason that the registration process failed - */ -final case class UnregisterFailure(obj : IdentifiableEntity, ex : Throwable) diff --git a/common/src/main/scala/net/psforever/objects/guid/actor/UnregisterSuccess.scala b/common/src/main/scala/net/psforever/objects/guid/actor/UnregisterSuccess.scala deleted file mode 100644 index 603de46a..00000000 --- a/common/src/main/scala/net/psforever/objects/guid/actor/UnregisterSuccess.scala +++ /dev/null @@ -1,11 +0,0 @@ -// Copyright (c) 2017 PSForever -package net.psforever.objects.guid.actor - -import net.psforever.objects.entity.IdentifiableEntity - -/** - * A message for when an object has been unregistered. - * @param obj the object - */ -final case class UnregisterSuccess(obj : IdentifiableEntity) - diff --git a/common/src/main/scala/net/psforever/objects/guid/misc/AscendingNumberSource.scala b/common/src/main/scala/net/psforever/objects/guid/misc/AscendingNumberSource.scala deleted file mode 100644 index c4534fab..00000000 --- a/common/src/main/scala/net/psforever/objects/guid/misc/AscendingNumberSource.scala +++ /dev/null @@ -1,32 +0,0 @@ -// Copyright (c) 2017 PSForever -package net.psforever.objects.guid.misc - -/** - * This class is just a proof of concept model of a self-contained system. - */ -class AscendingNumberSource { - val pool : Array[Int] = Array.ofDim[Int](65536) - (0 to 65535).foreach(x => { pool(x) = x }) - var head : Int = 0 - - def Get() : Int = { - val start : Int = head - if(pool(head) == -1) { - do { - head = (head + 1) % pool.length - } - while(pool(head) == -1 && head != start) - } - if(head == start) { - import net.psforever.objects.entity.NoGUIDException - throw NoGUIDException("no unused numbers available") - } - val outNumber : Int = head - pool(head) = -1 - outNumber - } - - def Return(number : Int) : Unit = { - pool(number) = number - } -} diff --git a/common/src/main/scala/net/psforever/objects/guid/misc/RegistrationTaskResolver.scala b/common/src/main/scala/net/psforever/objects/guid/misc/RegistrationTaskResolver.scala deleted file mode 100644 index f45d7e94..00000000 --- a/common/src/main/scala/net/psforever/objects/guid/misc/RegistrationTaskResolver.scala +++ /dev/null @@ -1,137 +0,0 @@ -// Copyright (c) 2017 PSForever -package net.psforever.objects.guid.misc - -import java.util.concurrent.TimeoutException - -import akka.actor.{Actor, ActorRef, Cancellable} -import net.psforever.objects.entity.IdentifiableEntity - -import scala.annotation.tailrec -import scala.concurrent.ExecutionContext.Implicits.global -import scala.concurrent.duration._ -import scala.util.{Failure, Success, Try} - -/** - * Accept a task in waiting and series of lesser tasks that complete the provided primary task. - * Receive periodic updates on the states of the lesser tasks and, when these sub-tasks have been accomplished, - * declare the primary task accomplished as well.
- *
- * This ia admittedly a simplistic model of task resolution, currently, and is rather specific and limited. - * Generalizing and expanding on this class in the future might be beneficial. - * @param obj the primary task - * @param list a series of sub-tasks that need to be completed before the pimrary task can be completed - * @param callback where to report about the pirmary task having succeeded or failed - * @param timeoutDuration a delay during which sub-tasks are permitted to be accomplished; - * after this grave period is over, the task has failed - */ -class RegistrationTaskResolver[T <: IdentifiableEntity](private val obj : T, private val list : List[T], callback : ActorRef, timeoutDuration : FiniteDuration) extends Actor { - /** sub-tasks that contribute to completion of the task */ - private val checklist : Array[Boolean] = Array.fill[Boolean](list.length)(false) - /** whether or not it matters that sub-tasks are coming in */ - private var valid : Boolean = true - /** declares when the task has taken too long to complete */ - private val taskTimeout : Cancellable = context.system.scheduler.scheduleOnce(timeoutDuration, self, Failure(new TimeoutException(s"a task for $obj has timed out"))) - private[this] val log = org.log4s.getLogger - ConfirmTask(Success(true)) //check for auto-completion - - def receive : Receive = { - case Success(objn)=> - ConfirmTask(ConfirmSubtask(objn.asInstanceOf[T])) - - case Failure(ex)=> - FailedTask(ex) - - case msg => - log.warn(s"unexpected message received - ${msg.toString}") - } - - /** - * If this object is still accepting task resolutions, determine if that sub-task can be checked off. - * @param objn the sub-task entry - * @return a successful pass or a failure if the task can't be found; - * a "successful failure" if task resolutions are no longer accepted - */ - private def ConfirmSubtask(objn : T) : Try[Boolean] = { - if(valid) { - if(MatchSubtask(objn, list.iterator)) { - Success(true) - } - else { - Failure(new Exception(s"can not find a subtask to check off - ${objn.toString}")) - } - } - else { - Success(false) - } - } - - /** - * Find a sub-task from a `List` of sub-tasks and mark it as completed, if found. - * @param objn the sub-task entry - * @param iter_list an `Iterator` to the list of sub-tasks - * @param index the index of this entry; - * defaults to zero - * @return whether or not the subtask has been marked as completed - */ - @tailrec private def MatchSubtask(objn : T, iter_list : Iterator[T], index : Int = 0) : Boolean = { - if(!iter_list.hasNext) { - false - } - else { - val subtask = iter_list.next - if(subtask.equals(objn)) { - checklist(index) = true - true - } - else { - MatchSubtask(objn, iter_list, index + 1) - } - } - } - - /** - * Determine whether all sub-tasks have been completed successfully. - * If so, complete the primary task. - * @param subtaskComplete the status of the recent sub-task confirmation that triggered this confirmation request - */ - private def ConfirmTask(subtaskComplete : Try[Boolean]) : Unit = { - if(valid) { - subtaskComplete match { - case Success(true) => - if(!checklist.contains(false)) { - FulfillTask() - } - case Success(false) => - log.warn(s"when checking a task for ${obj.toString}, arrived at a state where we previously failed a subtask but main task still valid") - case Failure(ex) => - FailedTask(ex) - } - } - } - - /** - * All sub-tasks have been completed; the main task can also be completed. - * Alert interested parties that the task is performed successfully. - * Stop as soon as possible. - */ - private def FulfillTask() : Unit = { - valid = false - callback ! Success(obj) - taskTimeout.cancel() - context.stop(self) - } - - /** - * The main task can not be completed. - * Clean up as much as possible and alert interested parties that the task has been dropped. - * Let this `Actor` stop gracefully. - * @param ex why the main task can not be completed - */ - private def FailedTask(ex : Throwable) : Unit = { - valid = false - callback ! Failure(ex) - taskTimeout.cancel() - import akka.pattern.gracefulStop - gracefulStop(self, 2 seconds) //give time for any other messages; avoid dead letters - } -} diff --git a/common/src/main/scala/net/psforever/objects/guid/source/MaxNumberSource.scala b/common/src/main/scala/net/psforever/objects/guid/source/MaxNumberSource.scala deleted file mode 100644 index ea5c969b..00000000 --- a/common/src/main/scala/net/psforever/objects/guid/source/MaxNumberSource.scala +++ /dev/null @@ -1,119 +0,0 @@ -// Copyright (c) 2017 PSForever -package net.psforever.objects.guid.source - -import net.psforever.objects.entity.IdentifiableEntity -import net.psforever.objects.guid.key.{LoanedKey, SecureKey} -import net.psforever.objects.guid.AvailabilityPolicy - -/** - * A `NumberSource` is considered a master "pool" of numbers from which all numbers are available to be drawn. - * The numbers are considered to be exclusive.
- *
- * This source utilizes all positive integers (to `Int.MaxValue`, anyway) and zero. - * It allocates number `Monitors` as it needs them. - * While this allows for a wide range of possible numbers, the internal structure expands and contracts as needed. - * The underlying flexible structure is a `LongMap` and is subject to constraints regarding `LongMap` growth. - */ -class MaxNumberSource() extends NumberSource { - import scala.collection.mutable - private val hash : mutable.LongMap[Key] = mutable.LongMap[Key]() //TODO consider seeding an initialBufferSize - private var allowRestrictions : Boolean = true - - def Size : Int = Int.MaxValue - - def CountAvailable : Int = Size - CountUsed - - def CountUsed : Int = hash.size - - override def Test(guid : Int) : Boolean = guid > -1 - - def Get(number : Int) : Option[SecureKey] = { - if(!Test(number)) { - None - } - else { - val existing : Option[Key] = hash.get(number).orElse({ - val key : Key = new Key - key.Policy = AvailabilityPolicy.Available - hash.put(number, key) - Some(key) - }) - Some(new SecureKey(number, existing.get)) - } - } - -// def GetAll(list : List[Int]) : List[SecureKey] = { -// list.map(number => -// hash.get(number) match { -// case Some(key) => -// new SecureKey(number, key) -// case _ => -// new SecureKey(number, new Key { Policy = AvailabilityPolicy.Available }) -// } -// ) -// } -// -// def GetAll( p : Key => Boolean ) : List[SecureKey] = { -// hash.filter(entry => p.apply(entry._2)).map(entry => new SecureKey(entry._1.toInt, entry._2)).toList -// } - - def Available(number : Int) : Option[LoanedKey] = { - if(!Test(number)) { - throw new IndexOutOfBoundsException("number can not be negative") - } - hash.get(number) match { - case Some(_) => - None - case _ => - val key : Key = new Key - key.Policy = AvailabilityPolicy.Leased - hash.put(number, key) - Some(new LoanedKey(number, key)) - } - } - - def Return(number : Int) : Option[IdentifiableEntity] = { - val existing = hash.get(number) - if(existing.isDefined && existing.get.Policy == AvailabilityPolicy.Leased) { - hash -= number - val obj = existing.get.Object - existing.get.Object = None - obj - } - else { - None - } - } - - def Restrict(number : Int) : Option[LoanedKey] = { - if(allowRestrictions) { - val existing : Key = hash.get(number).orElse({ - val key : Key = new Key - hash.put(number, key) - Some(key) - }).get - existing.Policy = AvailabilityPolicy.Restricted - Some(new LoanedKey(number, existing)) - } - else { - None - } - } - - def FinalizeRestrictions : List[Int] = { - allowRestrictions = false - hash.filter(entry => entry._2.Policy == AvailabilityPolicy.Restricted).map(entry => entry._1.toInt).toList - } - - def Clear() : List[IdentifiableEntity] = { - val list : List[IdentifiableEntity] = hash.values.filter(key => key.Object.isDefined).map(key => key.Object.get).toList - hash.clear() - list - } -} - -object MaxNumberSource { - def apply() : MaxNumberSource = { - new MaxNumberSource() - } -} \ No newline at end of file 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 21497a74..617fbb59 100644 --- a/common/src/main/scala/net/psforever/objects/zones/Zone.scala +++ b/common/src/main/scala/net/psforever/objects/zones/Zone.scala @@ -5,7 +5,7 @@ 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.actor.UniqueNumberSystem import net.psforever.objects.guid.selector.RandomSelector import net.psforever.objects.guid.source.LimitedNumberSource import net.psforever.packet.GamePacket @@ -41,6 +41,8 @@ class Zone(private val zoneId : String, zoneMap : ZoneMap, zoneNumber : Int) { 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)) + guid.AddPool("environment", (0 to 2000).toList) + guid.AddPool("dynamic", (2001 to 10000).toList).Selector = new RandomSelector //TODO unlump pools later; do not make too big /** 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. */ @@ -60,13 +62,8 @@ class Zone(private val zoneId : String, zoneMap : ZoneMap, zoneNumber : Int) { */ 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") + implicit val guid : NumberPoolHub = this.guid //passed into builderObject.Build implicitly + accessor = context.actorOf(Props(classOf[UniqueNumberSystem], guid, UniqueNumberSystem.AllocateNumberPoolActors(guid)), s"$Id-uns") ground = context.actorOf(Props(classOf[ZoneGroundActor], equipmentOnGround), s"$Id-ground") Map.LocalObjects.foreach({ builderObject => diff --git a/common/src/test/scala/objects/ActorTest.scala b/common/src/test/scala/objects/ActorTest.scala new file mode 100644 index 00000000..5b08b920 --- /dev/null +++ b/common/src/test/scala/objects/ActorTest.scala @@ -0,0 +1,13 @@ +// Copyright (c) 2017 PSForever +package objects + +import akka.actor.ActorSystem +import akka.testkit.{ImplicitSender, TestKit} +import org.scalatest.{BeforeAndAfterAll, Matchers, WordSpecLike} +import org.specs2.specification.Scope + +abstract class ActorTest(sys : ActorSystem) extends TestKit(sys) with Scope with ImplicitSender with WordSpecLike with Matchers with BeforeAndAfterAll { + override def afterAll { + TestKit.shutdownActorSystem(system) + } +} diff --git a/common/src/test/scala/objects/NumberPoolActorTest.scala b/common/src/test/scala/objects/NumberPoolActorTest.scala index 52787c81..f40eac43 100644 --- a/common/src/test/scala/objects/NumberPoolActorTest.scala +++ b/common/src/test/scala/objects/NumberPoolActorTest.scala @@ -2,24 +2,11 @@ package objects import akka.actor.{ActorSystem, Props} -import akka.testkit.{ImplicitSender, TestKit, TestProbe} -import net.psforever.objects.entity.IdentifiableEntity -import net.psforever.objects.guid.NumberPoolHub -import org.scalatest.{BeforeAndAfterAll, Matchers, WordSpecLike} -import net.psforever.objects.guid.actor.{NumberPoolAccessorActor, NumberPoolActor, Register} +import net.psforever.objects.guid.actor.NumberPoolActor import net.psforever.objects.guid.pool.ExclusivePool import net.psforever.objects.guid.selector.RandomSelector -import net.psforever.objects.guid.source.LimitedNumberSource -import org.specs2.specification.Scope import scala.concurrent.duration.Duration -import scala.util.Success - -abstract class ActorTest(sys : ActorSystem) extends TestKit(sys) with Scope with ImplicitSender with WordSpecLike with Matchers with BeforeAndAfterAll { - override def afterAll { - TestKit.shutdownActorSystem(system) - } -} class NumberPoolActorTest extends ActorTest(ActorSystem("test")) { "NumberPoolActor" should { @@ -61,23 +48,3 @@ class NumberPoolActorTest2 extends ActorTest(ActorSystem("test")) { } } } - -class NumberPoolActorTest3 extends ActorTest(ActorSystem("test")) { - "NumberPoolAccessorActor" should { - class TestEntity extends IdentifiableEntity - - "register" in { - val hub = new NumberPoolHub(new LimitedNumberSource(51)) - val pool = hub.AddPool("test", (25 to 50).toList) - pool.Selector = new RandomSelector - val poolActor = system.actorOf(Props(classOf[NumberPoolActor], pool), name = "poolActor") - val poolAccessor = system.actorOf(Props(classOf[NumberPoolAccessorActor], hub, pool, poolActor), name = "accessor") - - val obj : TestEntity = new TestEntity - val probe = new TestProbe(system) - poolAccessor ! Register(obj, probe.ref) - probe.expectMsg(Success(obj)) - assert({obj.GUID; true}) //NoGUIDException if failure - } - } -} diff --git a/common/src/test/scala/objects/NumberSourceTest.scala b/common/src/test/scala/objects/NumberSourceTest.scala index dc466061..08c9f48a 100644 --- a/common/src/test/scala/objects/NumberSourceTest.scala +++ b/common/src/test/scala/objects/NumberSourceTest.scala @@ -9,180 +9,6 @@ class NumberSourceTest extends Specification { import net.psforever.objects.entity.IdentifiableEntity private class TestClass extends IdentifiableEntity - "MaxNumberSource" should { - import net.psforever.objects.guid.source.MaxNumberSource - "construct" in { - val obj = MaxNumberSource() - obj.Size mustEqual Int.MaxValue - obj.CountAvailable mustEqual Int.MaxValue - obj.CountUsed mustEqual 0 - } - - "get a number" in { - val obj = MaxNumberSource() - val result : Option[LoanedKey] = obj.Available(5) - result.isDefined mustEqual true - result.get.GUID mustEqual 5 - result.get.Policy mustEqual AvailabilityPolicy.Leased - result.get.Object mustEqual None - obj.Size mustEqual Int.MaxValue - obj.CountAvailable mustEqual Int.MaxValue - 1 - obj.CountUsed mustEqual 1 - } - - "assign the number" in { - val obj = MaxNumberSource() - val result : Option[LoanedKey] = obj.Available(5) - result.isDefined mustEqual true - result.get.Object = new TestClass() - ok - } - - "return a number (unused)" in { - val obj = MaxNumberSource() - val result : Option[LoanedKey] = obj.Available(5) - result.isDefined mustEqual true - result.get.GUID mustEqual 5 - obj.CountUsed mustEqual 1 - val ret = obj.Return(result.get) - ret mustEqual None - obj.CountUsed mustEqual 0 - } - - "return a number (assigned)" in { - val obj = MaxNumberSource() - val test = new TestClass() - val result : Option[LoanedKey] = obj.Available(5) - result.isDefined mustEqual true - result.get.GUID mustEqual 5 - result.get.Object = test - obj.CountUsed mustEqual 1 - val ret = obj.Return(result.get) - ret mustEqual Some(test) - obj.CountUsed mustEqual 0 - } - - "restrict a number (unassigned)" in { - val obj = MaxNumberSource() - val result : Option[LoanedKey] = obj.Restrict(5) - result.isDefined mustEqual true - result.get.GUID mustEqual 5 - result.get.Policy mustEqual AvailabilityPolicy.Restricted - result.get.Object mustEqual None - } - - "restrict a number (assigned + multiple assignments)" in { - val obj = MaxNumberSource() - val test1 = new TestClass() - val test2 = new TestClass() - val result : Option[LoanedKey] = obj.Restrict(5) - result.get.GUID mustEqual 5 - result.get.Policy mustEqual AvailabilityPolicy.Restricted - result.get.Object mustEqual None - result.get.Object = None //assignment 1 - result.get.Object mustEqual None //still unassigned - result.get.Object = test1 //assignment 2 - result.get.Object mustEqual Some(test1) - result.get.Object = test2 //assignment 3 - result.get.Object mustEqual Some(test1) //same as above - } - - "return a restricted number (correctly fail)" in { - val obj = MaxNumberSource() - val test = new TestClass() - val result : Option[LoanedKey] = obj.Restrict(5) - result.get.GUID mustEqual 5 - result.get.Policy mustEqual AvailabilityPolicy.Restricted - result.get.Object = test - - obj.Return(5) - val result2 : Option[SecureKey] = obj.Get(5) - result2.get.GUID mustEqual 5 - result2.get.Policy mustEqual AvailabilityPolicy.Restricted - result2.get.Object mustEqual Some(test) - } - - "restrict a previously-assigned number" in { - val obj = MaxNumberSource() - val test = new TestClass() - val result1 : Option[LoanedKey] = obj.Available(5) - result1.isDefined mustEqual true - result1.get.Policy mustEqual AvailabilityPolicy.Leased - result1.get.Object = test - val result2 : Option[LoanedKey] = obj.Restrict(5) - result2.isDefined mustEqual true - result2.get.Policy mustEqual AvailabilityPolicy.Restricted - result2.get.Object mustEqual Some(test) - } - - "check a number (not previously gotten)" in { - val obj = MaxNumberSource() - val result2 : Option[SecureKey] = obj.Get(5) - result2.get.GUID mustEqual 5 - result2.get.Policy mustEqual AvailabilityPolicy.Available - result2.get.Object mustEqual None - } - - "check a number (previously gotten)" in { - val obj = MaxNumberSource() - val result : Option[LoanedKey] = obj.Available(5) - result.isDefined mustEqual true - result.get.GUID mustEqual 5 - result.get.Policy mustEqual AvailabilityPolicy.Leased - result.get.Object mustEqual None - val result2 : Option[SecureKey] = obj.Get(5) - result2.get.GUID mustEqual 5 - result2.get.Policy mustEqual AvailabilityPolicy.Leased - result2.get.Object mustEqual None - } - - "check a number (assigned)" in { - val obj = MaxNumberSource() - val result : Option[LoanedKey] = obj.Available(5) - result.isDefined mustEqual true - result.get.GUID mustEqual 5 - result.get.Policy mustEqual AvailabilityPolicy.Leased - result.get.Object = new TestClass() - val result2 : Option[SecureKey] = obj.Get(5) - result2.get.GUID mustEqual 5 - result2.get.Policy mustEqual AvailabilityPolicy.Leased - result2.get.Object mustEqual result.get.Object - } - - "check a number (assigned and returned)" in { - val obj = MaxNumberSource() - val test = new TestClass() - val result : Option[LoanedKey] = obj.Available(5) - result.get.Policy mustEqual AvailabilityPolicy.Leased - result.get.Object = test - val result2 : Option[SecureKey] = obj.Get(5) - result2.get.Policy mustEqual AvailabilityPolicy.Leased - result2.get.Object.get === test - obj.Return(5) mustEqual Some(test) - val result3 : Option[SecureKey] = obj.Get(5) - result3.get.Policy mustEqual AvailabilityPolicy.Available - result3.get.Object mustEqual None - } - - "clear" in { - val obj = MaxNumberSource() - val test1 = new TestClass() - val test2 = new TestClass() - obj.Available(5) //no assignment - obj.Available(10).get.Object = test1 - obj.Available(15).get.Object = test2 - obj.Restrict(15) - obj.Restrict(20).get.Object = test1 - obj.CountUsed mustEqual 4 - - val list : List[IdentifiableEntity] = obj.Clear() - obj.CountUsed mustEqual 0 - list.size mustEqual 3 - list.count(obj => { obj == test1 }) mustEqual 2 - list.count(obj => { obj == test2 }) mustEqual 1 - } - } - "LimitedNumberSource" should { import net.psforever.objects.guid.source.LimitedNumberSource "construct" in { diff --git a/common/src/test/scala/objects/UniqueNumberSystemTest.scala b/common/src/test/scala/objects/UniqueNumberSystemTest.scala new file mode 100644 index 00000000..a685b035 --- /dev/null +++ b/common/src/test/scala/objects/UniqueNumberSystemTest.scala @@ -0,0 +1,288 @@ +// Copyright (c) 2017 PSForever +package objects + +import akka.actor.{ActorRef, ActorSystem, Props} +import net.psforever.objects.entity.IdentifiableEntity +import net.psforever.objects.guid.NumberPoolHub +import net.psforever.objects.guid.actor.{NumberPoolActor, Register, UniqueNumberSystem, Unregister} +import net.psforever.objects.guid.selector.RandomSelector +import net.psforever.objects.guid.source.LimitedNumberSource + +import scala.concurrent.duration.Duration +import scala.util.{Failure, Success} + +class AllocateNumberPoolActors extends ActorTest(ActorSystem("test")) { + "AllocateNumberPoolActors" in { + val src : LimitedNumberSource = LimitedNumberSource(6000) + val guid : NumberPoolHub = new NumberPoolHub(src) + guid.AddPool("pool1", (1001 to 2000).toList) + guid.AddPool("pool2", (3001 to 4000).toList) + guid.AddPool("pool3", (5001 to 6000).toList) + val actorMap = UniqueNumberSystemTest.AllocateNumberPoolActors(guid) + assert(actorMap.size == 4) + assert(actorMap.get("generic").isDefined) //automatically generated + assert(actorMap.get("pool1").isDefined) + assert(actorMap.get("pool2").isDefined) + assert(actorMap.get("pool3").isDefined) + } +} + +class UniqueNumberSystemTest extends ActorTest(ActorSystem("test")) { + "UniqueNumberSystem" should { + "constructor" in { + val src : LimitedNumberSource = LimitedNumberSource(6000) + val guid : NumberPoolHub = new NumberPoolHub(src) + guid.AddPool("pool1", (1001 to 2000).toList) + guid.AddPool("pool2", (3001 to 4000).toList) + guid.AddPool("pool3", (5001 to 6000).toList) + system.actorOf(Props(classOf[UniqueNumberSystem], guid, UniqueNumberSystemTest.AllocateNumberPoolActors(guid)), "uns") + //as long as it constructs ... + + } + } +} + +class UniqueNumberSystemTest1 extends ActorTest(ActorSystem("test")) { + class EntityTestClass extends IdentifiableEntity + + "UniqueNumberSystem" should { + "Register (success)" in { + val src : LimitedNumberSource = LimitedNumberSource(6000) + val guid : NumberPoolHub = new NumberPoolHub(src) + val pool1 = (1001 to 2000).toList + val pool2 = (3001 to 4000).toList + val pool3 = (5001 to 6000).toList + guid.AddPool("pool1", pool1).Selector = new RandomSelector + guid.AddPool("pool2", pool2).Selector = new RandomSelector + guid.AddPool("pool3", pool3).Selector = new RandomSelector + val uns = system.actorOf(Props(classOf[UniqueNumberSystem], guid, UniqueNumberSystemTest.AllocateNumberPoolActors(guid)), "uns") + assert(src.CountUsed == 0) + //pool1 + for(_ <- 1 to 100) { + val testObj = new EntityTestClass() + uns ! Register(testObj, "pool1") + val msg = receiveOne(Duration.create(100, "ms")) + assert(msg.isInstanceOf[Success[_]]) + assert(pool1.contains(testObj.GUID.guid)) + } + //pool2 + for(_ <- 1 to 100) { + val testObj = new EntityTestClass() + uns ! Register(testObj, "pool2") + val msg = receiveOne(Duration.create(100, "ms")) + assert(msg.isInstanceOf[Success[_]]) + assert(pool2.contains(testObj.GUID.guid)) + } + //pool3 + for(_ <- 1 to 100) { + val testObj = new EntityTestClass() + uns ! Register(testObj, "pool3") + val msg = receiveOne(Duration.create(100, "ms")) + assert(msg.isInstanceOf[Success[_]]) + assert(pool3.contains(testObj.GUID.guid)) + } + assert(src.CountUsed == 300) + } + } +} + +class UniqueNumberSystemTest2 extends ActorTest(ActorSystem("test")) { + class EntityTestClass extends IdentifiableEntity + + "UniqueNumberSystem" should { + "Register (success; already registered)" in { + val src : LimitedNumberSource = LimitedNumberSource(6000) + val guid : NumberPoolHub = new NumberPoolHub(src) + guid.AddPool("pool1", (1001 to 2000).toList).Selector = new RandomSelector + guid.AddPool("pool2", (3001 to 4000).toList).Selector = new RandomSelector + guid.AddPool("pool3", (5001 to 6000).toList).Selector = new RandomSelector + val uns = system.actorOf(Props(classOf[UniqueNumberSystem], guid, UniqueNumberSystemTest.AllocateNumberPoolActors(guid)), "uns") + val testObj = new EntityTestClass() + assert(!testObj.HasGUID) + assert(src.CountUsed == 0) + + uns ! Register(testObj, "pool1") + val msg1 = receiveOne(Duration.create(100, "ms")) + assert(msg1.isInstanceOf[Success[_]]) + assert(testObj.HasGUID) + assert(src.CountUsed == 1) + + val id = testObj.GUID.guid + uns ! Register(testObj, "pool2") //different pool; makes no difference + val msg2 = receiveOne(Duration.create(100, "ms")) + assert(msg2.isInstanceOf[Success[_]]) + assert(testObj.HasGUID) + assert(src.CountUsed == 1) + assert(testObj.GUID.guid == id) //unchanged + } + } + //a log.warn should have been generated during this test +} + +class UniqueNumberSystemTest3 extends ActorTest(ActorSystem("test")) { + class EntityTestClass extends IdentifiableEntity + + "UniqueNumberSystem" should { + "Register (failure; no pool)" in { + val src : LimitedNumberSource = LimitedNumberSource(6000) + val guid : NumberPoolHub = new NumberPoolHub(src) + guid.AddPool("pool1", (1001 to 2000).toList).Selector = new RandomSelector + guid.AddPool("pool2", (3001 to 4000).toList).Selector = new RandomSelector + guid.AddPool("pool3", (5001 to 6000).toList).Selector = new RandomSelector + val uns = system.actorOf(Props(classOf[UniqueNumberSystem], guid, UniqueNumberSystemTest.AllocateNumberPoolActors(guid)), "uns") + val testObj = new EntityTestClass() + assert(!testObj.HasGUID) + assert(src.CountUsed == 0) + + uns ! Register(testObj, "pool4") + val msg1 = receiveOne(Duration.create(100, "ms")) + assert(msg1.isInstanceOf[Failure[_]]) + assert(!testObj.HasGUID) + assert(src.CountUsed == 0) + } + } +} + +class UniqueNumberSystemTest4 extends ActorTest(ActorSystem("test")) { + class EntityTestClass extends IdentifiableEntity + + "UniqueNumberSystem" should { + "Register (failure; empty pool)" in { + val src : LimitedNumberSource = LimitedNumberSource(6000) + val guid : NumberPoolHub = new NumberPoolHub(src) + guid.AddPool("pool1", (1001 to 2000).toList).Selector = new RandomSelector + guid.AddPool("pool2", (3001 to 4000).toList).Selector = new RandomSelector + guid.AddPool("pool3", (5001 to 6000).toList).Selector = new RandomSelector + guid.AddPool("pool4", 50 :: Nil).Selector = new RandomSelector //list of one element; can not add an empty list + val uns = system.actorOf(Props(classOf[UniqueNumberSystem], guid, UniqueNumberSystemTest.AllocateNumberPoolActors(guid)), "uns") + + val testObj1 = new EntityTestClass() + uns ! Register(testObj1, "pool4") + val msg1 = receiveOne(Duration.create(100, "ms")) + assert(msg1.isInstanceOf[Success[_]]) //pool4 is now empty + + val testObj2 = new EntityTestClass() + uns ! Register(testObj2, "pool4") + val msg2 = receiveOne(Duration.create(100, "ms")) + assert(msg2.isInstanceOf[Failure[_]]) + } + } +} + +class UniqueNumberSystemTest5 extends ActorTest(ActorSystem("test")) { + class EntityTestClass extends IdentifiableEntity + + "UniqueNumberSystem" should { + "Unregister (success)" in { + val src : LimitedNumberSource = LimitedNumberSource(6000) + val guid : NumberPoolHub = new NumberPoolHub(src) + val pool2 = (3001 to 4000).toList + guid.AddPool("pool1", (1001 to 2000).toList).Selector = new RandomSelector + guid.AddPool("pool2", pool2).Selector = new RandomSelector + guid.AddPool("pool3", (5001 to 6000).toList).Selector = new RandomSelector + val uns = system.actorOf(Props(classOf[UniqueNumberSystem], guid, UniqueNumberSystemTest.AllocateNumberPoolActors(guid)), "uns") + val testObj = new EntityTestClass() + assert(!testObj.HasGUID) + assert(src.CountUsed == 0) + + uns ! Register(testObj, "pool2") + val msg1 = receiveOne(Duration.create(100, "ms")) + assert(msg1.isInstanceOf[Success[_]]) + assert(testObj.HasGUID) + assert(pool2.contains(testObj.GUID.guid)) + assert(src.CountUsed == 1) + + uns ! Unregister(testObj) + val msg2 = receiveOne(Duration.create(100, "ms")) + assert(msg2.isInstanceOf[Success[_]]) + assert(!testObj.HasGUID) + assert(src.CountUsed == 0) + } + } +} + +class UniqueNumberSystemTest6 extends ActorTest(ActorSystem("test")) { + class EntityTestClass extends IdentifiableEntity + + "UniqueNumberSystem" should { + "Unregister (success; object not registered at all)" in { + val src : LimitedNumberSource = LimitedNumberSource(6000) + val guid : NumberPoolHub = new NumberPoolHub(src) + guid.AddPool("pool1", (1001 to 2000).toList).Selector = new RandomSelector + guid.AddPool("pool2", (3001 to 4000).toList).Selector = new RandomSelector + guid.AddPool("pool3", (5001 to 6000).toList).Selector = new RandomSelector + val uns = system.actorOf(Props(classOf[UniqueNumberSystem], guid, UniqueNumberSystemTest.AllocateNumberPoolActors(guid)), "uns") + val testObj = new EntityTestClass() + assert(!testObj.HasGUID) + assert(src.CountUsed == 0) + + uns ! Unregister(testObj) + val msg1 = receiveOne(Duration.create(100, "ms")) + assert(msg1.isInstanceOf[Success[_]]) + assert(!testObj.HasGUID) + assert(src.CountUsed == 0) + } + } +} + +class UniqueNumberSystemTest7 extends ActorTest(ActorSystem("test")) { + class EntityTestClass extends IdentifiableEntity + + "UniqueNumberSystem" should { + "Unregister (failure; number not in system)" in { + val src : LimitedNumberSource = LimitedNumberSource(6000) + val guid : NumberPoolHub = new NumberPoolHub(src) + guid.AddPool("pool1", (1001 to 2000).toList).Selector = new RandomSelector + guid.AddPool("pool2", (3001 to 4000).toList).Selector = new RandomSelector + guid.AddPool("pool3", (5001 to 6000).toList).Selector = new RandomSelector + val uns = system.actorOf(Props(classOf[UniqueNumberSystem], guid, UniqueNumberSystemTest.AllocateNumberPoolActors(guid)), "uns") + val testObj = new EntityTestClass() + testObj.GUID = net.psforever.packet.game.PlanetSideGUID(6001) //fake registering; number too high + assert(testObj.HasGUID) + assert(src.CountUsed == 0) + + uns ! Unregister(testObj) + val msg1 = receiveOne(Duration.create(100, "ms")) + assert(msg1.isInstanceOf[Failure[_]]) + assert(testObj.HasGUID) + assert(src.CountUsed == 0) + } + } +} + +class UniqueNumberSystemTest8 extends ActorTest(ActorSystem("test")) { + class EntityTestClass extends IdentifiableEntity + + "UniqueNumberSystem" should { + "Unregister (failure; object is not registered to that number)" in { + val src : LimitedNumberSource = LimitedNumberSource(6000) + val guid : NumberPoolHub = new NumberPoolHub(src) + guid.AddPool("pool1", (1001 to 2000).toList).Selector = new RandomSelector + guid.AddPool("pool2", (3001 to 4000).toList).Selector = new RandomSelector + guid.AddPool("pool3", (5001 to 6000).toList).Selector = new RandomSelector + val uns = system.actorOf(Props(classOf[UniqueNumberSystem], guid, UniqueNumberSystemTest.AllocateNumberPoolActors(guid)), "uns") + val testObj = new EntityTestClass() + testObj.GUID = net.psforever.packet.game.PlanetSideGUID(3500) //fake registering + assert(testObj.HasGUID) + assert(src.CountUsed == 0) + + uns ! Unregister(testObj) + val msg1 = receiveOne(Duration.create(100, "ms")) + assert(msg1.isInstanceOf[Failure[_]]) + assert(testObj.HasGUID) + assert(src.CountUsed == 0) + } + } +} + +object UniqueNumberSystemTest { + /** + * @see `UniqueNumberSystem.AllocateNumberPoolActors(NumberPoolHub)(implicit ActorContext)` + */ + def AllocateNumberPoolActors(poolSource : NumberPoolHub)(implicit system : ActorSystem) : Map[String, ActorRef] = { + poolSource.Pools.map({ case ((pname, pool)) => + pname -> system.actorOf(Props(classOf[NumberPoolActor], pool), pname) + }).toMap + } +} + diff --git a/pslogin/src/main/scala/WorldSessionActor.scala b/pslogin/src/main/scala/WorldSessionActor.scala index 87bbdde9..07393e4c 100644 --- a/pslogin/src/main/scala/WorldSessionActor.scala +++ b/pslogin/src/main/scala/WorldSessionActor.scala @@ -1175,7 +1175,7 @@ class WorldSessionActor extends Actor with MDCContextAware { } def Execute(resolver : ActorRef) : Unit = { - localAccessor ! Register(localObject, resolver) + localAccessor ! Register(localObject, "dynamic", resolver) } }) }