From cc383420c82109584ba3792960d45a092ef8991a Mon Sep 17 00:00:00 2001 From: FateJH Date: Tue, 26 Sep 2017 15:51:37 -0400 Subject: [PATCH 01/23] combined NumberPoolHubActor and NumberPoolAccessorActor into UniqueNumberSystem; fixed issue with using NumberPoolHub straight-up --- .../objects/guid/NumberPoolHub.scala | 2 +- .../objects/guid/NumberPoolHub2.scala | 313 ++++++++++++++++ .../guid/actor/UniqueNumberSystem.scala | 348 ++++++++++++++++++ 3 files changed, 662 insertions(+), 1 deletion(-) create mode 100644 common/src/main/scala/net/psforever/objects/guid/NumberPoolHub2.scala create mode 100644 common/src/main/scala/net/psforever/objects/guid/actor/UniqueNumberSystem.scala 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..e574c0ec 100644 --- a/common/src/main/scala/net/psforever/objects/guid/NumberPoolHub.scala +++ b/common/src/main/scala/net/psforever/objects/guid/NumberPoolHub.scala @@ -203,8 +203,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/NumberPoolHub2.scala b/common/src/main/scala/net/psforever/objects/guid/NumberPoolHub2.scala new file mode 100644 index 00000000..7769b696 --- /dev/null +++ b/common/src/main/scala/net/psforever/objects/guid/NumberPoolHub2.scala @@ -0,0 +1,313 @@ +// Copyright (c) 2017 PSForever +package net.psforever.objects.guid + +import net.psforever.objects.entity.{IdentifiableEntity, NoGUIDException} +import net.psforever.objects.guid.key.LoanedKey +import net.psforever.objects.guid.pool.{ExclusivePool, GenericPool, NumberPool} +import net.psforever.objects.guid.source.NumberSource +import net.psforever.packet.game.PlanetSideGUID + +import scala.util.{Failure, Success, Try} + +class NumberPoolHub2(private val source : NumberSource) { + import scala.collection.mutable + private val hash : mutable.HashMap[String, NumberPool] = mutable.HashMap[String, NumberPool]() + private val bigpool : mutable.LongMap[String] = mutable.LongMap[String]() + hash += "generic" -> new GenericPool(bigpool, source.Size) + source.FinalizeRestrictions.foreach(i => bigpool += i.toLong -> "") //these numbers can never be pooled; the source can no longer restrict numbers + + def apply(number : PlanetSideGUID) : Option[IdentifiableEntity] = this(number.guid) + + def apply(number : Int) : Option[IdentifiableEntity] = source.Get(number).orElse(return None).get.Object + + def Numbers : List[Int] = bigpool.keys.map(key => key.toInt).toList + + 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(source.Size <= pool.max) { + throw new IllegalArgumentException(s"can not add pool $name - max(pool) is greater than source.size") + } + val collision = bigpool.keys.map(n => n.toInt).toSet.intersect(pool.toSet) + if(collision.nonEmpty) { + throw new IllegalArgumentException(s"can not add pool $name - it contains the following redundant numbers: ${collision.toString}") + } + pool.foreach(i => bigpool += i.toLong -> name) + hash += name -> new ExclusivePool(pool) + hash(name) + } + + def RemovePool(name : String) : List[Int] = { + if(name.equals("generic") || name.equals("")) { + throw new IllegalArgumentException("can not remove pool - generic or restricted") + } + val pool = hash.get(name).orElse({ + throw new IllegalArgumentException(s"can not remove pool - $name does not exist") + }).get + if(pool.Count > 0) { + throw new IllegalArgumentException(s"can not remove pool - $name is being used") + } + + hash.remove(name) + pool.Numbers.foreach(number => bigpool -= number) + pool.Numbers + } + + def GetPool(name : String) : Option[NumberPool] = if(name.equals("")) { None } else { hash.get(name) } + + def Pools : mutable.HashMap[String, NumberPool] = hash + + def WhichPool(number : Int) : Option[String] = { + val name = bigpool.get(number) + if(name.contains("")) { None } else { name } + } + + def WhichPool(obj : IdentifiableEntity) : Option[String] = { + try { + val number : Int = obj.GUID.guid + val entry = source.Get(number) + if(entry.isDefined && entry.get.Object.contains(obj)) { WhichPool(number) } else { None } + } + catch { + case _ : Exception => + None + } + } + + def register(obj : IdentifiableEntity) : Try[Int] = register(obj, "generic") + + def register(obj : IdentifiableEntity, number : Int) : Try[Int] = { + bigpool.get(number.toLong) match { + case Some(name) => + register_GetSpecificNumberFromPool(name, number) match { + case Success(key) => + key.Object = obj + Success(obj.GUID.guid) + case Failure(ex) => + Failure(new Exception(s"trying to register an object to a specific number but, ${ex.getMessage}")) + } + case None => + import net.psforever.objects.guid.selector.SpecificSelector + hash("generic").Selector.asInstanceOf[SpecificSelector].SelectionIndex = number + register(obj, "generic") + } + } + + private def register_GetSpecificNumberFromPool(name : String, number : Int) : Try[LoanedKey]= { + hash.get(name) match { + case Some(pool) => + val slctr = pool.Selector + import net.psforever.objects.guid.selector.SpecificSelector + val specific = new SpecificSelector + specific.SelectionIndex = number + pool.Selector = specific + pool.Get() + pool.Selector = slctr + register_GetAvailableNumberFromSource(number) + case None => + Failure(new Exception(s"number pool $name not defined")) + } + } + + private def register_GetAvailableNumberFromSource(number : Int) : Try[LoanedKey] = { + source.Available(number) match { + case Some(key) => + Success(key) + case None => + Failure(new Exception(s"number $number is unavailable")) + } + } + + def register(obj : IdentifiableEntity, name : String) : Try[Int] = { + try { + register_CheckNumberAgainstDesiredPool(obj, name, obj.GUID.guid) + } + catch { + case _ : Exception => + register_GetPool(name) match { + case Success(key) => + key.Object = obj + Success(obj.GUID.guid) + case Failure(ex) => + Failure(new Exception(s"trying to register an object but, ${ex.getMessage}")) + } + } + } + + private def register_CheckNumberAgainstDesiredPool(obj : IdentifiableEntity, name : String, number : Int) : Try[Int] = { + val directKey = source.Get(number) + if(directKey.isEmpty || !directKey.get.Object.contains(obj)) { + Failure(new Exception("object already registered, but not to this source")) + } + else if(!WhichPool(number).contains(name)) { + //TODO obj is not registered to the desired pool; is this okay? + Success(number) + } + else { + Success(number) + } + } + + private def register_GetPool(name : String) : Try[LoanedKey] = { + hash.get(name) match { + case Some(pool) => + register_GetNumberFromDesiredPool(pool) + case _ => + Failure(new Exception(s"number pool $name not defined")) + } + } + + private def register_GetNumberFromDesiredPool(pool : NumberPool) : Try[LoanedKey] = { + pool.Get() match { + case Success(number) => + register_GetMonitorFromSource(number) + case Failure(ex) => + Failure(ex) + } + } + + private def register_GetMonitorFromSource(number : Int) : Try[LoanedKey] = { + source.Available(number) match { + case Some(key) => + Success(key) + case _ => + throw NoGUIDException(s"a pool gave us a number $number that is actually unavailable") //stop the show; this is terrible! + } + } + + def register(number : Int) : Try[LoanedKey] = { + WhichPool(number) match { + case None => + import net.psforever.objects.guid.selector.SpecificSelector + hash("generic").Selector.asInstanceOf[SpecificSelector].SelectionIndex = number + register_GetPool("generic") + case Some(name) => + register_GetSpecificNumberFromPool(name, number) + } + } + + def register(name : String) : Try[LoanedKey] = register_GetPool(name) + + def latterPartRegister(obj : IdentifiableEntity, number : Int) : Try[IdentifiableEntity] = { + register_GetMonitorFromSource(number) match { + case Success(monitor) => + monitor.Object = obj + Success(obj) + case Failure(ex) => + Failure(ex) + } + } + + def unregister(obj : IdentifiableEntity) : Try[Int] = { + unregister_GetPoolFromObject(obj) match { + case Success(pool) => + val number = obj.GUID.guid + pool.Return(number) + source.Return(number) + obj.Invalidate() + Success(number) + case Failure(ex) => + Failure(new Exception(s"can not unregister this object: ${ex.getMessage}")) + } + } + + def unregister_GetPoolFromObject(obj : IdentifiableEntity) : Try[NumberPool] = { + WhichPool(obj) match { + case Some(name) => + unregister_GetPool(name) + case None => + Failure(throw new Exception("can not find a pool for this object")) + } + } + + private def unregister_GetPool(name : String) : Try[NumberPool] = { + hash.get(name) match { + case Some(pool) => + Success(pool) + case None => + Failure(new Exception(s"no pool by the name of '$name'")) + } + } + + def unregister(number : Int) : Try[Option[IdentifiableEntity]] = { + if(source.Test(number)) { + unregister_GetObjectFromSource(number) + } + else { + Failure(new Exception(s"can not unregister a number $number that this source does not own") ) + } + } + + private def unregister_GetObjectFromSource(number : Int) : Try[Option[IdentifiableEntity]] = { + source.Return(number) match { + case Some(obj) => + unregister_ReturnObjectToPool(obj) + case None => + unregister_ReturnNumberToPool(number) //nothing is wrong, but we'll check the pool + } + } + + private def unregister_ReturnObjectToPool(obj : IdentifiableEntity) : Try[Option[IdentifiableEntity]] = { + val number = obj.GUID.guid + unregister_GetPoolFromNumber(number) match { + case Success(pool) => + pool.Return(number) + obj.Invalidate() + Success(Some(obj)) + case Failure(ex) => + source.Available(number) //undo + Failure(new Exception(s"started unregistering, but ${ex.getMessage}")) + } + } + + private def unregister_ReturnNumberToPool(number : Int) : Try[Option[IdentifiableEntity]] = { + unregister_GetPoolFromNumber(number) match { + case Success(pool) => + pool.Return(number) + Success(None) + case _ => //though everything else went fine, we must still fail if this number was restricted all along + if(!bigpool.get(number).contains("")) { + Success(None) + } + else { + Failure(new Exception(s"can not unregister this number $number")) + } + } + } + + private def unregister_GetPoolFromNumber(number : Int) : Try[NumberPool] = { + WhichPool(number) match { + case Some(name) => + unregister_GetPool(name) + case None => + Failure(new Exception(s"no pool using number $number")) + } + } + + def latterPartUnregister(number : Int) : Option[IdentifiableEntity] = source.Return(number) + + def isRegistered(obj : IdentifiableEntity) : Boolean = { + try { + source.Get(obj.GUID.guid) match { + case Some(monitor) => + monitor.Object.contains(obj) + case None => + false + } + } + catch { + case _ : NoGUIDException => + false + } + } + + def isRegistered(number : Int) : Boolean = { + source.Get(number) match { + case Some(monitor) => + monitor.Policy == AvailabilityPolicy.Leased + case None => + false + } + } +} 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..9e37624f --- /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"awkward no number error $ex") //neither a successful request nor an entry of making the request + } + case None => ; + log.warn(s"awkward no number error $ex") //neither a successful request nor an entry of making the request + case _ => ; + log.warn(s"unrecognized request $id accompanying a no number error $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 None => ; + 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 + } +} From 0a0a41658510cbfccedd898e823a1b3f5604afd7 Mon Sep 17 00:00:00 2001 From: FateJH Date: Tue, 26 Sep 2017 23:33:08 -0400 Subject: [PATCH 02/23] standardized UNS reporting and recovery; added tests for UNS and better accommodated future Actor testing by splitting out ActorTest into its own file --- .../objects/guid/NumberPoolHub.scala | 8 +- .../guid/actor/UniqueNumberSystem.scala | 8 +- common/src/test/scala/objects/ActorTest.scala | 13 + .../scala/objects/NumberPoolActorTest.scala | 10 +- .../objects/UniqueNumberSystemTest.scala | 288 ++++++++++++++++++ 5 files changed, 312 insertions(+), 15 deletions(-) create mode 100644 common/src/test/scala/objects/ActorTest.scala create mode 100644 common/src/test/scala/objects/UniqueNumberSystemTest.scala 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 e574c0ec..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") } 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 index 9e37624f..fcd16c17 100644 --- a/common/src/main/scala/net/psforever/objects/guid/actor/UniqueNumberSystem.scala +++ b/common/src/main/scala/net/psforever/objects/guid/actor/UniqueNumberSystem.scala @@ -76,12 +76,12 @@ class UniqueNumberSystem(private val guid : NumberPoolHub, private val poolActor case Some(entry) => entry.replyTo ! Failure(ex) //ONLY callback that is possible case None => ; - log.warn(s"awkward no number error $ex") //neither a successful request nor an entry of making the request + 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"awkward no number error $ex") //neither a successful request nor an entry of making the request + 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 no number error $ex") + log.warn(s"unrecognized request $id accompanying a failed number request - $ex") } case Unregister(obj, call) => @@ -126,7 +126,7 @@ class UniqueNumberSystem(private val guid : NumberPoolHub, private val poolActor log.error(s"could not find original request $nid that caused error $ex, but pool was $sender") //no callback is possible } - case None => ; + case _ => ; log.error(s"could not find original request $id that caused error $ex, but pool was $sender") //no callback is possible } 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..4fec728b 100644 --- a/common/src/test/scala/objects/NumberPoolActorTest.scala +++ b/common/src/test/scala/objects/NumberPoolActorTest.scala @@ -2,25 +2,17 @@ package objects import akka.actor.{ActorSystem, Props} -import akka.testkit.{ImplicitSender, TestKit, TestProbe} +import akka.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.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 { "GetAnyNumber" 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 + } +} + From 92b6883fa0fbb92943bf3f068a633965bcf5cf4f Mon Sep 17 00:00:00 2001 From: FateJH Date: Wed, 27 Sep 2017 20:18:37 -0400 Subject: [PATCH 03/23] switched NumberPoolAccessorActor out for UniqueNumberSystem; created file that enumerates object types for future reference cleaned up folder object/guid, except for contents of objects/guid/selectors --- .../net/psforever/objects/ObjectType.scala | 96 ++++++ .../objects/guid/NumberPoolHub2.scala | 313 ------------------ .../objects/guid/actor/IsRegistered.scala | 31 -- .../guid/actor/NumberPoolAccessorActor.scala | 216 ------------ .../guid/actor/NumberPoolHubActor.scala | 212 ------------ .../guid/actor/UnregisterFailure.scala | 11 - .../guid/actor/UnregisterSuccess.scala | 11 - .../guid/misc/AscendingNumberSource.scala | 32 -- .../guid/misc/RegistrationTaskResolver.scala | 137 -------- .../objects/guid/source/MaxNumberSource.scala | 119 ------- .../net/psforever/objects/zones/Zone.scala | 13 +- .../scala/objects/NumberPoolActorTest.scala | 27 +- .../test/scala/objects/NumberSourceTest.scala | 174 ---------- .../src/main/scala/WorldSessionActor.scala | 2 +- 14 files changed, 103 insertions(+), 1291 deletions(-) create mode 100644 common/src/main/scala/net/psforever/objects/ObjectType.scala delete mode 100644 common/src/main/scala/net/psforever/objects/guid/NumberPoolHub2.scala delete mode 100644 common/src/main/scala/net/psforever/objects/guid/actor/IsRegistered.scala delete mode 100644 common/src/main/scala/net/psforever/objects/guid/actor/NumberPoolAccessorActor.scala delete mode 100644 common/src/main/scala/net/psforever/objects/guid/actor/NumberPoolHubActor.scala delete mode 100644 common/src/main/scala/net/psforever/objects/guid/actor/UnregisterFailure.scala delete mode 100644 common/src/main/scala/net/psforever/objects/guid/actor/UnregisterSuccess.scala delete mode 100644 common/src/main/scala/net/psforever/objects/guid/misc/AscendingNumberSource.scala delete mode 100644 common/src/main/scala/net/psforever/objects/guid/misc/RegistrationTaskResolver.scala delete mode 100644 common/src/main/scala/net/psforever/objects/guid/source/MaxNumberSource.scala 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/NumberPoolHub2.scala b/common/src/main/scala/net/psforever/objects/guid/NumberPoolHub2.scala deleted file mode 100644 index 7769b696..00000000 --- a/common/src/main/scala/net/psforever/objects/guid/NumberPoolHub2.scala +++ /dev/null @@ -1,313 +0,0 @@ -// Copyright (c) 2017 PSForever -package net.psforever.objects.guid - -import net.psforever.objects.entity.{IdentifiableEntity, NoGUIDException} -import net.psforever.objects.guid.key.LoanedKey -import net.psforever.objects.guid.pool.{ExclusivePool, GenericPool, NumberPool} -import net.psforever.objects.guid.source.NumberSource -import net.psforever.packet.game.PlanetSideGUID - -import scala.util.{Failure, Success, Try} - -class NumberPoolHub2(private val source : NumberSource) { - import scala.collection.mutable - private val hash : mutable.HashMap[String, NumberPool] = mutable.HashMap[String, NumberPool]() - private val bigpool : mutable.LongMap[String] = mutable.LongMap[String]() - hash += "generic" -> new GenericPool(bigpool, source.Size) - source.FinalizeRestrictions.foreach(i => bigpool += i.toLong -> "") //these numbers can never be pooled; the source can no longer restrict numbers - - def apply(number : PlanetSideGUID) : Option[IdentifiableEntity] = this(number.guid) - - def apply(number : Int) : Option[IdentifiableEntity] = source.Get(number).orElse(return None).get.Object - - def Numbers : List[Int] = bigpool.keys.map(key => key.toInt).toList - - 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(source.Size <= pool.max) { - throw new IllegalArgumentException(s"can not add pool $name - max(pool) is greater than source.size") - } - val collision = bigpool.keys.map(n => n.toInt).toSet.intersect(pool.toSet) - if(collision.nonEmpty) { - throw new IllegalArgumentException(s"can not add pool $name - it contains the following redundant numbers: ${collision.toString}") - } - pool.foreach(i => bigpool += i.toLong -> name) - hash += name -> new ExclusivePool(pool) - hash(name) - } - - def RemovePool(name : String) : List[Int] = { - if(name.equals("generic") || name.equals("")) { - throw new IllegalArgumentException("can not remove pool - generic or restricted") - } - val pool = hash.get(name).orElse({ - throw new IllegalArgumentException(s"can not remove pool - $name does not exist") - }).get - if(pool.Count > 0) { - throw new IllegalArgumentException(s"can not remove pool - $name is being used") - } - - hash.remove(name) - pool.Numbers.foreach(number => bigpool -= number) - pool.Numbers - } - - def GetPool(name : String) : Option[NumberPool] = if(name.equals("")) { None } else { hash.get(name) } - - def Pools : mutable.HashMap[String, NumberPool] = hash - - def WhichPool(number : Int) : Option[String] = { - val name = bigpool.get(number) - if(name.contains("")) { None } else { name } - } - - def WhichPool(obj : IdentifiableEntity) : Option[String] = { - try { - val number : Int = obj.GUID.guid - val entry = source.Get(number) - if(entry.isDefined && entry.get.Object.contains(obj)) { WhichPool(number) } else { None } - } - catch { - case _ : Exception => - None - } - } - - def register(obj : IdentifiableEntity) : Try[Int] = register(obj, "generic") - - def register(obj : IdentifiableEntity, number : Int) : Try[Int] = { - bigpool.get(number.toLong) match { - case Some(name) => - register_GetSpecificNumberFromPool(name, number) match { - case Success(key) => - key.Object = obj - Success(obj.GUID.guid) - case Failure(ex) => - Failure(new Exception(s"trying to register an object to a specific number but, ${ex.getMessage}")) - } - case None => - import net.psforever.objects.guid.selector.SpecificSelector - hash("generic").Selector.asInstanceOf[SpecificSelector].SelectionIndex = number - register(obj, "generic") - } - } - - private def register_GetSpecificNumberFromPool(name : String, number : Int) : Try[LoanedKey]= { - hash.get(name) match { - case Some(pool) => - val slctr = pool.Selector - import net.psforever.objects.guid.selector.SpecificSelector - val specific = new SpecificSelector - specific.SelectionIndex = number - pool.Selector = specific - pool.Get() - pool.Selector = slctr - register_GetAvailableNumberFromSource(number) - case None => - Failure(new Exception(s"number pool $name not defined")) - } - } - - private def register_GetAvailableNumberFromSource(number : Int) : Try[LoanedKey] = { - source.Available(number) match { - case Some(key) => - Success(key) - case None => - Failure(new Exception(s"number $number is unavailable")) - } - } - - def register(obj : IdentifiableEntity, name : String) : Try[Int] = { - try { - register_CheckNumberAgainstDesiredPool(obj, name, obj.GUID.guid) - } - catch { - case _ : Exception => - register_GetPool(name) match { - case Success(key) => - key.Object = obj - Success(obj.GUID.guid) - case Failure(ex) => - Failure(new Exception(s"trying to register an object but, ${ex.getMessage}")) - } - } - } - - private def register_CheckNumberAgainstDesiredPool(obj : IdentifiableEntity, name : String, number : Int) : Try[Int] = { - val directKey = source.Get(number) - if(directKey.isEmpty || !directKey.get.Object.contains(obj)) { - Failure(new Exception("object already registered, but not to this source")) - } - else if(!WhichPool(number).contains(name)) { - //TODO obj is not registered to the desired pool; is this okay? - Success(number) - } - else { - Success(number) - } - } - - private def register_GetPool(name : String) : Try[LoanedKey] = { - hash.get(name) match { - case Some(pool) => - register_GetNumberFromDesiredPool(pool) - case _ => - Failure(new Exception(s"number pool $name not defined")) - } - } - - private def register_GetNumberFromDesiredPool(pool : NumberPool) : Try[LoanedKey] = { - pool.Get() match { - case Success(number) => - register_GetMonitorFromSource(number) - case Failure(ex) => - Failure(ex) - } - } - - private def register_GetMonitorFromSource(number : Int) : Try[LoanedKey] = { - source.Available(number) match { - case Some(key) => - Success(key) - case _ => - throw NoGUIDException(s"a pool gave us a number $number that is actually unavailable") //stop the show; this is terrible! - } - } - - def register(number : Int) : Try[LoanedKey] = { - WhichPool(number) match { - case None => - import net.psforever.objects.guid.selector.SpecificSelector - hash("generic").Selector.asInstanceOf[SpecificSelector].SelectionIndex = number - register_GetPool("generic") - case Some(name) => - register_GetSpecificNumberFromPool(name, number) - } - } - - def register(name : String) : Try[LoanedKey] = register_GetPool(name) - - def latterPartRegister(obj : IdentifiableEntity, number : Int) : Try[IdentifiableEntity] = { - register_GetMonitorFromSource(number) match { - case Success(monitor) => - monitor.Object = obj - Success(obj) - case Failure(ex) => - Failure(ex) - } - } - - def unregister(obj : IdentifiableEntity) : Try[Int] = { - unregister_GetPoolFromObject(obj) match { - case Success(pool) => - val number = obj.GUID.guid - pool.Return(number) - source.Return(number) - obj.Invalidate() - Success(number) - case Failure(ex) => - Failure(new Exception(s"can not unregister this object: ${ex.getMessage}")) - } - } - - def unregister_GetPoolFromObject(obj : IdentifiableEntity) : Try[NumberPool] = { - WhichPool(obj) match { - case Some(name) => - unregister_GetPool(name) - case None => - Failure(throw new Exception("can not find a pool for this object")) - } - } - - private def unregister_GetPool(name : String) : Try[NumberPool] = { - hash.get(name) match { - case Some(pool) => - Success(pool) - case None => - Failure(new Exception(s"no pool by the name of '$name'")) - } - } - - def unregister(number : Int) : Try[Option[IdentifiableEntity]] = { - if(source.Test(number)) { - unregister_GetObjectFromSource(number) - } - else { - Failure(new Exception(s"can not unregister a number $number that this source does not own") ) - } - } - - private def unregister_GetObjectFromSource(number : Int) : Try[Option[IdentifiableEntity]] = { - source.Return(number) match { - case Some(obj) => - unregister_ReturnObjectToPool(obj) - case None => - unregister_ReturnNumberToPool(number) //nothing is wrong, but we'll check the pool - } - } - - private def unregister_ReturnObjectToPool(obj : IdentifiableEntity) : Try[Option[IdentifiableEntity]] = { - val number = obj.GUID.guid - unregister_GetPoolFromNumber(number) match { - case Success(pool) => - pool.Return(number) - obj.Invalidate() - Success(Some(obj)) - case Failure(ex) => - source.Available(number) //undo - Failure(new Exception(s"started unregistering, but ${ex.getMessage}")) - } - } - - private def unregister_ReturnNumberToPool(number : Int) : Try[Option[IdentifiableEntity]] = { - unregister_GetPoolFromNumber(number) match { - case Success(pool) => - pool.Return(number) - Success(None) - case _ => //though everything else went fine, we must still fail if this number was restricted all along - if(!bigpool.get(number).contains("")) { - Success(None) - } - else { - Failure(new Exception(s"can not unregister this number $number")) - } - } - } - - private def unregister_GetPoolFromNumber(number : Int) : Try[NumberPool] = { - WhichPool(number) match { - case Some(name) => - unregister_GetPool(name) - case None => - Failure(new Exception(s"no pool using number $number")) - } - } - - def latterPartUnregister(number : Int) : Option[IdentifiableEntity] = source.Return(number) - - def isRegistered(obj : IdentifiableEntity) : Boolean = { - try { - source.Get(obj.GUID.guid) match { - case Some(monitor) => - monitor.Object.contains(obj) - case None => - false - } - } - catch { - case _ : NoGUIDException => - false - } - } - - def isRegistered(number : Int) : Boolean = { - source.Get(number) match { - case Some(monitor) => - monitor.Policy == AvailabilityPolicy.Leased - case None => - false - } - } -} 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/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/NumberPoolActorTest.scala b/common/src/test/scala/objects/NumberPoolActorTest.scala index 4fec728b..f40eac43 100644 --- a/common/src/test/scala/objects/NumberPoolActorTest.scala +++ b/common/src/test/scala/objects/NumberPoolActorTest.scala @@ -2,16 +2,11 @@ package objects import akka.actor.{ActorSystem, Props} -import akka.testkit.TestProbe -import net.psforever.objects.entity.IdentifiableEntity -import net.psforever.objects.guid.NumberPoolHub -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 scala.concurrent.duration.Duration -import scala.util.Success class NumberPoolActorTest extends ActorTest(ActorSystem("test")) { "NumberPoolActor" should { @@ -53,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/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) } }) } From ddeb5b81882565f61018c888389b443434b4002a Mon Sep 17 00:00:00 2001 From: FateJH Date: Sat, 30 Sep 2017 21:31:30 -0400 Subject: [PATCH 04/23] initial work on packet and tests --- .../psforever/packet/GamePacketOpcode.scala | 2 +- .../game/DamageWithPositionMessage.scala | 33 +++++++++++++++++++ .../game/DamageWithPositionMessageTest.scala | 31 +++++++++++++++++ 3 files changed, 65 insertions(+), 1 deletion(-) create mode 100644 common/src/main/scala/net/psforever/packet/game/DamageWithPositionMessage.scala create mode 100644 common/src/test/scala/game/DamageWithPositionMessageTest.scala diff --git a/common/src/main/scala/net/psforever/packet/GamePacketOpcode.scala b/common/src/main/scala/net/psforever/packet/GamePacketOpcode.scala index 27a300a2..c8a3ffc8 100644 --- a/common/src/main/scala/net/psforever/packet/GamePacketOpcode.scala +++ b/common/src/main/scala/net/psforever/packet/GamePacketOpcode.scala @@ -515,7 +515,7 @@ object GamePacketOpcode extends Enumeration { case 0xa3 => noDecoder(UplinkResponse) case 0xa4 => game.WarpgateRequest.decode case 0xa5 => noDecoder(WarpgateResponse) - case 0xa6 => noDecoder(DamageWithPositionMessage) + case 0xa6 => game.DamageWithPositionMessage.decode case 0xa7 => game.GenericActionMessage.decode // 0xa8 case 0xa8 => game.ContinentalLockUpdateMessage.decode diff --git a/common/src/main/scala/net/psforever/packet/game/DamageWithPositionMessage.scala b/common/src/main/scala/net/psforever/packet/game/DamageWithPositionMessage.scala new file mode 100644 index 00000000..9dfe2c41 --- /dev/null +++ b/common/src/main/scala/net/psforever/packet/game/DamageWithPositionMessage.scala @@ -0,0 +1,33 @@ +// Copyright (c) 2017 PSForever +package net.psforever.packet.game + +import net.psforever.packet.{GamePacketOpcode, Marshallable, PlanetSideGamePacket} +import net.psforever.types.Vector3 +import scodec.Codec +import scodec.codecs._ + +/** + * Dispatched by the server to indicate a source of damage affecting the player. + * Unlike `HitHint` the damage source is defined by an actual coordinate location rather than a physical target.
+ *
+ * The player will be shown a fading, outwards drifting, red tick mark. + * The location will indicate a general direction towards the source. + * If the option `Game/Show Damage Flash` is set, the player's screen will flash red briefly when a mark is displayed. + * @param unk the intensity of the damage tick marks + * @param pos the position + * @see `HitHint` + */ +final case class DamageWithPositionMessage(unk : Int, + pos : Vector3) + extends PlanetSideGamePacket { + type Packet = DamageWithPositionMessage + def opcode = GamePacketOpcode.DamageWithPositionMessage + def encode = DamageWithPositionMessage.encode(this) +} + +object DamageWithPositionMessage extends Marshallable[DamageWithPositionMessage] { + implicit val codec : Codec[DamageWithPositionMessage] = ( + ("unk" | uint8L) :: + ("pos" | Vector3.codec_pos) + ).as[DamageWithPositionMessage] +} diff --git a/common/src/test/scala/game/DamageWithPositionMessageTest.scala b/common/src/test/scala/game/DamageWithPositionMessageTest.scala new file mode 100644 index 00000000..8da7c35b --- /dev/null +++ b/common/src/test/scala/game/DamageWithPositionMessageTest.scala @@ -0,0 +1,31 @@ +// Copyright (c) 2017 PSForever +package game + +import net.psforever.types.{MeritCommendation, Vector3} +import org.specs2.mutable._ +import net.psforever.packet._ +import net.psforever.packet.game._ +import scodec.bits._ + +class DamageWithPositionMessageTest extends Specification { + val string = hex"A6 11 6C2D7 65535 CA16" + + "decode" in { + PacketCoding.DecodePacket(string).require match { + case DamageWithPositionMessage(unk, pos) => + unk mustEqual 17 + pos.x mustEqual 3674.8438f + pos.y mustEqual 2726.789f + pos.z mustEqual 91.15625f + case _ => + ko + } + } + + "encode" in { + val msg = DamageWithPositionMessage(17, Vector3(3674.8438f, 2726.789f, 91.15625f)) + val pkt = PacketCoding.EncodePacket(msg).require.toByteVector + + pkt mustEqual string + } +} From 063d4967bc9acf5ee936121dd950e2c7bb0def8b Mon Sep 17 00:00:00 2001 From: SouNourS Date: Wed, 4 Oct 2017 17:15:57 +0200 Subject: [PATCH 05/23] ActionProgressMessage --- .../psforever/packet/GamePacketOpcode.scala | 2 +- .../packet/game/ActionProgressMessage.scala | 24 +++++++++++++++ .../game/ActionProgressMessageTest.scala | 29 +++++++++++++++++++ 3 files changed, 54 insertions(+), 1 deletion(-) create mode 100644 common/src/main/scala/net/psforever/packet/game/ActionProgressMessage.scala create mode 100644 common/src/test/scala/game/ActionProgressMessageTest.scala diff --git a/common/src/main/scala/net/psforever/packet/GamePacketOpcode.scala b/common/src/main/scala/net/psforever/packet/GamePacketOpcode.scala index c8a3ffc8..5b68b55c 100644 --- a/common/src/main/scala/net/psforever/packet/GamePacketOpcode.scala +++ b/common/src/main/scala/net/psforever/packet/GamePacketOpcode.scala @@ -358,7 +358,7 @@ object GamePacketOpcode extends Enumeration { // OPCODES 0x20-2f case 0x20 => noDecoder(UnknownMessage32) - case 0x21 => noDecoder(ActionProgressMessage) + case 0x21 => game.ActionProgressMessage.decode case 0x22 => noDecoder(ActionCancelMessage) case 0x23 => noDecoder(ActionCancelAcknowledgeMessage) case 0x24 => game.SetEmpireMessage.decode diff --git a/common/src/main/scala/net/psforever/packet/game/ActionProgressMessage.scala b/common/src/main/scala/net/psforever/packet/game/ActionProgressMessage.scala new file mode 100644 index 00000000..f9d3f6d9 --- /dev/null +++ b/common/src/main/scala/net/psforever/packet/game/ActionProgressMessage.scala @@ -0,0 +1,24 @@ +// Copyright (c) 2017 PSForever +package net.psforever.packet.game + +import net.psforever.packet.{GamePacketOpcode, Marshallable, PlanetSideGamePacket} +import scodec.Codec +import scodec.codecs._ + +/** + * + */ +final case class ActionProgressMessage(unk1 : Int, + unk2 : Long) + extends PlanetSideGamePacket { + type Packet = ActionProgressMessage + def opcode = GamePacketOpcode.ActionProgressMessage + def encode = ActionProgressMessage.encode(this) +} + +object ActionProgressMessage extends Marshallable[ActionProgressMessage] { + implicit val codec : Codec[ActionProgressMessage] = ( + ("unk1" | uint4L) :: + ("unk2" | uint32L) + ).as[ActionProgressMessage] +} \ No newline at end of file diff --git a/common/src/test/scala/game/ActionProgressMessageTest.scala b/common/src/test/scala/game/ActionProgressMessageTest.scala new file mode 100644 index 00000000..acbf7eb5 --- /dev/null +++ b/common/src/test/scala/game/ActionProgressMessageTest.scala @@ -0,0 +1,29 @@ +// Copyright (c) 2017 PSForever +package game + +import org.specs2.mutable._ +import net.psforever.packet._ +import net.psforever.packet.game._ +import net.psforever.types.ExoSuitType +import scodec.bits._ + +class ActionProgressMessageTest extends Specification { + val string = hex"216000000000" + + "decode" in { + PacketCoding.DecodePacket(string).require match { + case ActionProgressMessage(unk1, unk2) => + unk1 mustEqual 6 + unk2 mustEqual 0 + case _ => + ko + } + } + + "encode" in { + val msg = ActionProgressMessage(6, 0L) + val pkt = PacketCoding.EncodePacket(msg).require.toByteVector + + pkt mustEqual string + } +} From 5de1c4e202bd222c95c532ace7de30296859d7de Mon Sep 17 00:00:00 2001 From: FateJH Date: Wed, 4 Oct 2017 22:12:05 -0400 Subject: [PATCH 06/23] created Definition for cert_terminal, involving the definition itself and an entry in GlobalDefinitions; wired up the three terminals in the closest HART building such that the player can get and let go of certifications; fixed typo in certification enum; added a comment to PSAM --- .../psforever/objects/GlobalDefinitions.scala | 6 +- .../terminals/CertTerminalDefinition.scala | 100 ++++++++++++++++++ .../terminals/OrderTerminalDefinition.scala | 12 ++- .../objects/terminals/Terminal.scala | 10 +- .../terminals/TerminalDefinition.scala | 16 ++- .../game/PlanetsideAttributeMessage.scala | 3 +- .../psforever/types/CertificationType.scala | 6 +- pslogin/src/main/scala/PsLogin.scala | 9 +- .../src/main/scala/WorldSessionActor.scala | 27 +++++ 9 files changed, 166 insertions(+), 23 deletions(-) create mode 100644 common/src/main/scala/net/psforever/objects/terminals/CertTerminalDefinition.scala diff --git a/common/src/main/scala/net/psforever/objects/GlobalDefinitions.scala b/common/src/main/scala/net/psforever/objects/GlobalDefinitions.scala index 21bfbd07..4fccc47f 100644 --- a/common/src/main/scala/net/psforever/objects/GlobalDefinitions.scala +++ b/common/src/main/scala/net/psforever/objects/GlobalDefinitions.scala @@ -6,7 +6,7 @@ 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.objects.terminals.{CertTerminalDefinition, OrderTerminalDefinition} import net.psforever.packet.game.objectcreate.ObjectClass import net.psforever.types.PlanetSideEmpire @@ -1239,5 +1239,7 @@ object GlobalDefinitions { fury.TrunkOffset = 30 val - orderTerminal = new OrderTerminalDefinition + order_terminal = new OrderTerminalDefinition + val + cert_terminal = new CertTerminalDefinition } diff --git a/common/src/main/scala/net/psforever/objects/terminals/CertTerminalDefinition.scala b/common/src/main/scala/net/psforever/objects/terminals/CertTerminalDefinition.scala new file mode 100644 index 00000000..cc617ecb --- /dev/null +++ b/common/src/main/scala/net/psforever/objects/terminals/CertTerminalDefinition.scala @@ -0,0 +1,100 @@ +// Copyright (c) 2017 PSForever +package net.psforever.objects.terminals + +import net.psforever.objects.Player +import net.psforever.packet.game.ItemTransactionMessage +import net.psforever.types.CertificationType + +/** + * The definition for any `Terminal` that is of a type "cert_terminal" (certification terminal). + * `Learn` and `Sell` `CertificationType` entries, gaining access to different `Equipment` and `Vehicles`. + */ +class CertTerminalDefinition extends TerminalDefinition(171) { + Name = "cert_terminal" + + /** + * The certifications available. + * All entries are listed on page (tab) number 0. + */ + private val certificationList : Map[String, (CertificationType.Value, Int)] = Map( + "medium_assault" -> (CertificationType.MediumAssault, 2), + "reinforced_armo" -> (CertificationType.ReinforcedExoSuit, 3), + "quad_all" -> (CertificationType.ATV, 1), + "switchblade" -> (CertificationType.Switchblade, 1), + "harasser" -> (CertificationType.Harasser, 1), + "anti_vehicular" -> (CertificationType.AntiVehicular, 3), + "heavy_assault" -> (CertificationType.HeavyAssault, 4), + "sniper" -> (CertificationType.Sniping, 3), + "special_assault" -> (CertificationType.SpecialAssault, 3), + "special_assault_2" -> (CertificationType.EliteAssault, 1), + "infiltration_suit" -> (CertificationType.InfiltrationSuit, 2), + "max_anti_personnel" -> (CertificationType.AIMAX, 3), + "max_anti_vehicular" -> (CertificationType.AVMAX, 3), + "max_anti_aircraft" -> (CertificationType.AAMAX, 2), + "max_all" -> (CertificationType.UniMAX, 6), + "air_cavalry_scout" -> (CertificationType.AirCavalryScout, 3), + "air_cavalry_assault" -> (CertificationType.AirCavalryAssault, 2), + "air_cavalry_interceptor" -> (CertificationType.AirCavalryInterceptor, 2), + "air_support" -> (CertificationType.AirSupport, 3), + "gunship" -> (CertificationType.GalaxyGunship, 2), + "phantasm" -> (CertificationType.Phantasm, 3), + "armored_assault1" -> (CertificationType.ArmoredAssault1, 2), + "armored_assault2" -> (CertificationType.ArmoredAssault2, 1), + "flail" -> (CertificationType.Flail, 1), + "assault_buggy" -> (CertificationType.AssaultBuggy, 3), + "ground_support" -> (CertificationType.GroundSupport, 2), + "ground_transport" -> (CertificationType.GroundTransport, 2), + "light_scout" -> (CertificationType.LightScout, 5), + "Repair" -> (CertificationType.Engineering, 3), + "combat_engineering" -> (CertificationType.CombatEngineering, 2), + "ce_offense" -> (CertificationType.AssaultEngineering, 3), + "ce_defense" -> (CertificationType.FortificationEngineering, 3), + "ce_advanced" -> (CertificationType.AdvancedEngineering, 5), + "Hacking" -> (CertificationType.Hacking, 3), + "advanced_hacking" -> (CertificationType.AdvancedHacking, 2), + "expert_hacking" -> (CertificationType.ExpertHacking, 2), + "virus_hacking" -> (CertificationType.DataCorruption, 3), + "electronics_expert" -> (CertificationType.ElectronicsExpert, 4), + "Medical" -> (CertificationType.Medical, 3), + "advanced_medical" -> (CertificationType.AdvancedMedical, 2) + //TODO bfr certification entries + ) + + /** + * Process a `TransactionType.Learn` action by the user. + * @param player the player + * @param msg the original packet carrying the request + * @return an actionable message that explains how to process the request + */ + def Buy(player : Player, msg : ItemTransactionMessage) : Terminal.Exchange = { //Learn + certificationList.get(msg.item_name) match { + case Some((cert, cost)) => + Terminal.LearnCertification(cert, cost) + case None => + Terminal.NoDeal() + } + } + + /** + * Process a `TransactionType.Sell` action by the user. + * @param player the player + * @param msg the original packet carrying the request + * @return an actionable message that explains how to process the request + */ + def Sell(player : Player, msg : ItemTransactionMessage) : Terminal.Exchange = { + certificationList.get(msg.item_name) match { + case Some((cert, cost)) => + Terminal.SellCertification(cert, cost) + case None => + Terminal.NoDeal() + } + } + + /** + * This action is not supported by this type of `Terminal`. + * @param player the player + * @param msg the original packet carrying the request + * @return `Terminal.NoEvent` always + */ + def Loadout(player : Player, msg : ItemTransactionMessage) : Terminal.Exchange = Terminal.NoDeal() +} diff --git a/common/src/main/scala/net/psforever/objects/terminals/OrderTerminalDefinition.scala b/common/src/main/scala/net/psforever/objects/terminals/OrderTerminalDefinition.scala index ee431ff7..0eb171fc 100644 --- a/common/src/main/scala/net/psforever/objects/terminals/OrderTerminalDefinition.scala +++ b/common/src/main/scala/net/psforever/objects/terminals/OrderTerminalDefinition.scala @@ -10,6 +10,11 @@ import net.psforever.packet.game.ItemTransactionMessage import scala.annotation.switch +/** + * The definition for any `Terminal` that is of a type "order_terminal". + * `Buy` and `Sell` `Equipment` items and `AmmoBox` items. + * Change `ExoSuitType` and retrieve `InfantryLoadout` entries. + */ class OrderTerminalDefinition extends TerminalDefinition(612) { Name = "order_terminal" @@ -21,10 +26,10 @@ class OrderTerminalDefinition extends TerminalDefinition(612) { /** * Process a `TransactionType.Buy` action by the user. + * Either attempt to purchase equipment or attempt to switch directly to a different exo-suit. * @param player the player * @param msg the original packet carrying the request - * @return an actionable message that explains how to process the request; - * either you attempt to purchase equipment or attempt to switch directly to a different exo-suit + * @return an actionable message that explains how to process the request */ def Buy(player : Player, msg : ItemTransactionMessage) : Terminal.Exchange = { (msg.item_page : @switch) match { @@ -64,6 +69,7 @@ class OrderTerminalDefinition extends TerminalDefinition(612) { /** * Process a `TransactionType.Sell` action by the user. * There is no specific `order_terminal` tab associated with this action. + * Additionally, the equipment to be sold ia almost always in the player's `FreeHand` slot. * Selling `Equipment` is always permitted. * @param player the player * @param msg the original packet carrying the request @@ -81,7 +87,7 @@ class OrderTerminalDefinition extends TerminalDefinition(612) { * @param msg the original packet carrying the request * @return an actionable message that explains how to process the request */ - def InfantryLoadout(player : Player, msg : ItemTransactionMessage) : Terminal.Exchange = { + def Loadout(player : Player, msg : ItemTransactionMessage) : Terminal.Exchange = { if(msg.item_page == 4) { //Favorites tab player.LoadLoadout(msg.unk1) match { case Some(loadout) => 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 81f464fe..f5d0bd53 100644 --- a/common/src/main/scala/net/psforever/objects/terminals/Terminal.scala +++ b/common/src/main/scala/net/psforever/objects/terminals/Terminal.scala @@ -66,14 +66,14 @@ class Terminal(tdef : TerminalDefinition) extends PlanetSideGameObject { */ def Request(player : Player, msg : ItemTransactionMessage) : Terminal.Exchange = { msg.transaction_type match { - case TransactionType.Buy => + case TransactionType.Buy | TransactionType.Learn => tdef.Buy(player, msg) case TransactionType.Sell => tdef.Sell(player, msg) case TransactionType.InfantryLoadout => - tdef.InfantryLoadout(player, msg) + tdef.Loadout(player, msg) case _ => Terminal.NoDeal() @@ -132,6 +132,12 @@ object Terminal { */ //TODO if there are exceptions, find them final case class SellEquipment() extends Exchange + + import net.psforever.types.CertificationType + final case class LearnCertification(cert : CertificationType.Value, cost : Int) extends Exchange + + final case class SellCertification(cert : CertificationType.Value, cost : Int) extends Exchange + /** * Recover a former exo-suit and `Equipment` configuration that the `Player` possessed. * A result of a processed request. diff --git a/common/src/main/scala/net/psforever/objects/terminals/TerminalDefinition.scala b/common/src/main/scala/net/psforever/objects/terminals/TerminalDefinition.scala index a82fb739..a14836fa 100644 --- a/common/src/main/scala/net/psforever/objects/terminals/TerminalDefinition.scala +++ b/common/src/main/scala/net/psforever/objects/terminals/TerminalDefinition.scala @@ -7,8 +7,6 @@ import net.psforever.objects.equipment.Equipment import net.psforever.packet.game.ItemTransactionMessage import net.psforever.types.ExoSuitType -import scala.collection.immutable.HashMap - /** * The definition for any `Terminal`. * @param objectId the object's identifier number @@ -17,7 +15,7 @@ abstract class TerminalDefinition(objectId : Int) extends ObjectDefinition(objec Name = "terminal" /** - * The unimplemented functionality for this `Terminal`'s `TransactionType.Buy` activity. + * The unimplemented functionality for this `Terminal`'s `TransactionType.Buy` and `TransactionType.Learn` activity. */ def Buy(player : Player, msg : ItemTransactionMessage) : Terminal.Exchange @@ -29,7 +27,7 @@ abstract class TerminalDefinition(objectId : Int) extends ObjectDefinition(objec /** * The unimplemented functionality for this `Terminal`'s `TransactionType.InfantryLoadout` activity. */ - def InfantryLoadout(player : Player, msg : ItemTransactionMessage) : Terminal.Exchange + def Loadout(player : Player, msg : ItemTransactionMessage) : Terminal.Exchange /** * A `Map` of information for changing exo-suits. @@ -49,7 +47,7 @@ abstract class TerminalDefinition(objectId : Int) extends ObjectDefinition(objec * key - an identification string sent by the client * value - a curried function that builds the object */ - protected val infantryAmmunition : HashMap[String, ()=>Equipment] = HashMap( + protected val infantryAmmunition : Map[String, ()=>Equipment] = Map( "9mmbullet" -> MakeAmmoBox(bullet_9mm), "9mmbullet_AP" -> MakeAmmoBox(bullet_9mm_AP), "shotgun_shell" -> MakeAmmoBox(shotgun_shell), @@ -75,7 +73,7 @@ abstract class TerminalDefinition(objectId : Int) extends ObjectDefinition(objec * key - an identification string sent by the client * value - a curried function that builds the object */ - protected val supportAmmunition : HashMap[String, ()=>Equipment] = HashMap( + protected val supportAmmunition : Map[String, ()=>Equipment] = Map( "health_canister" -> MakeAmmoBox(health_canister), "armor_canister" -> MakeAmmoBox(armor_canister), "upgrade_canister" -> MakeAmmoBox(upgrade_canister) @@ -86,7 +84,7 @@ abstract class TerminalDefinition(objectId : Int) extends ObjectDefinition(objec * key - an identification string sent by the client * value - a curried function that builds the object */ - protected val vehicleAmmunition : HashMap[String, ()=>Equipment] = HashMap( + protected val vehicleAmmunition : Map[String, ()=>Equipment] = Map( "35mmbullet" -> MakeAmmoBox(bullet_35mm), "hellfire_ammo" -> MakeAmmoBox(hellfire_ammo), "liberator_bomb" -> MakeAmmoBox(liberator_bomb), @@ -129,7 +127,7 @@ abstract class TerminalDefinition(objectId : Int) extends ObjectDefinition(objec * key - an identification string sent by the client * value - a curried function that builds the object */ - protected val infantryWeapons : HashMap[String, ()=>Equipment] = HashMap( + protected val infantryWeapons : Map[String, ()=>Equipment] = Map( "ilc9" -> MakeTool(ilc9, bullet_9mm), "repeater" -> MakeTool(repeater, bullet_9mm), "isp" -> MakeTool(isp, shotgun_shell), //amp @@ -173,7 +171,7 @@ abstract class TerminalDefinition(objectId : Int) extends ObjectDefinition(objec * key - an identification string sent by the client * value - a curried function that builds the object */ - protected val supportWeapons : HashMap[String, ()=>Equipment] = HashMap( + protected val supportWeapons : Map[String, ()=>Equipment] = Map( "medkit" -> MakeKit(medkit), "super_medkit" -> MakeKit(super_medkit), "super_armorkit" -> MakeKit(super_armorkit), diff --git a/common/src/main/scala/net/psforever/packet/game/PlanetsideAttributeMessage.scala b/common/src/main/scala/net/psforever/packet/game/PlanetsideAttributeMessage.scala index 125e521d..0b640c68 100644 --- a/common/src/main/scala/net/psforever/packet/game/PlanetsideAttributeMessage.scala +++ b/common/src/main/scala/net/psforever/packet/game/PlanetsideAttributeMessage.scala @@ -20,7 +20,7 @@ import scodec.codecs._ * `17 - BEP. Value seems to be the same as BattleExperienceMessage`
* `18 - CEP.`
* `19 - Anchors. Value is 0 to disengage, 1 to engage.`
- * `24 - Certifications with value :`
+ * `24 - Learn certifications with value :`
* 01 : Medium Assault
* 02 : Heavy Assault
* 03 : Special Assault
@@ -66,6 +66,7 @@ import scodec.codecs._ * 43 : Fortification Engineering
* 44 : Assault Engineering
* 45 : Advanced Engineering (= Fortification Engineering + Assault Engineering) Must have Combat Engineering
+ * `25 - Forget certifications (same order as 24)` * `29 - Visible ?! That's not the cloaked effect, Maybe for spectator mode ?. Value is 0 to visible, 1 to invisible.`
* `31 - Info under avatar name : 0 = LFS, 1 = Looking For Squad Members`
* `32 - Info under avatar name : 0 = Looking For Squad Members, 1 = LFS`
diff --git a/common/src/main/scala/net/psforever/types/CertificationType.scala b/common/src/main/scala/net/psforever/types/CertificationType.scala index d06e2d19..ea4ca3ab 100644 --- a/common/src/main/scala/net/psforever/types/CertificationType.scala +++ b/common/src/main/scala/net/psforever/types/CertificationType.scala @@ -30,9 +30,9 @@ object CertificationType extends Enumeration { AntiVehicular, Sniping, EliteAssault, - AirCalvaryScout, - AirCalvaryInterceptor, - AirCalvaryAssault, + AirCavalryScout, + AirCavalryInterceptor, + AirCavalryAssault, //10 AirSupport, ATV, diff --git a/pslogin/src/main/scala/PsLogin.scala b/pslogin/src/main/scala/PsLogin.scala index c1403fc4..f71cce11 100644 --- a/pslogin/src/main/scala/PsLogin.scala +++ b/pslogin/src/main/scala/PsLogin.scala @@ -222,9 +222,12 @@ 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)) + LocalObject(TerminalObjectBuilder(cert_terminal, 186)) + LocalObject(TerminalObjectBuilder(cert_terminal, 187)) + LocalObject(TerminalObjectBuilder(cert_terminal, 188)) + LocalObject(TerminalObjectBuilder(order_terminal, 853)) + LocalObject(TerminalObjectBuilder(order_terminal, 855)) + LocalObject(TerminalObjectBuilder(order_terminal, 860)) } val home3 = Zone("home3", map13, 13) diff --git a/pslogin/src/main/scala/WorldSessionActor.scala b/pslogin/src/main/scala/WorldSessionActor.scala index 07393e4c..d759a8f3 100644 --- a/pslogin/src/main/scala/WorldSessionActor.scala +++ b/pslogin/src/main/scala/WorldSessionActor.scala @@ -312,6 +312,7 @@ class WorldSessionActor extends Actor with MDCContextAware { avatarService ! AvatarServiceMessage(tplayer.Continent, AvatarAction.EquipmentOnGround(tplayer.GUID, pos, orient, obj)) }) } + sendResponse(PacketCoding.CreateGamePacket(0, ItemTransactionResultMessage (msg.terminal_guid, TransactionType.Buy, true))) case Terminal.BuyEquipment(item) => ; tplayer.Fit(item) match { @@ -382,6 +383,31 @@ class WorldSessionActor extends Actor with MDCContextAware { PutEquipmentInSlot(tplayer, entry.obj, entry.start) }) //TODO drop items on ground + sendResponse(PacketCoding.CreateGamePacket(0, ItemTransactionResultMessage (msg.terminal_guid, TransactionType.InfantryLoadout, true))) + + case Terminal.LearnCertification(cert, cost) => + if(!player.Certifications.contains(cert)) { + log.info(s"$tplayer is learning the $cert certification for $cost points") + tplayer.Certifications += cert + sendResponse(PacketCoding.CreateGamePacket(0, PlanetsideAttributeMessage(tplayer.GUID, 24, cert.id.toLong))) + sendResponse(PacketCoding.CreateGamePacket(0, ItemTransactionResultMessage(msg.terminal_guid, TransactionType.Learn, true))) + } + else { + log.warn(s"$tplayer already knows the $cert certification, so he can't learn it") + sendResponse(PacketCoding.CreateGamePacket(0, ItemTransactionResultMessage(msg.terminal_guid, TransactionType.Learn, false))) + } + + case Terminal.SellCertification(cert, cost) => + if(player.Certifications.contains(cert)) { + log.info(s"$tplayer is forgetting the $cert certification for $cost points") + tplayer.Certifications -= cert + sendResponse(PacketCoding.CreateGamePacket(0, PlanetsideAttributeMessage(tplayer.GUID, 25, cert.id.toLong))) + sendResponse(PacketCoding.CreateGamePacket(0, ItemTransactionResultMessage(msg.terminal_guid, TransactionType.Sell, true))) + } + else { + log.warn(s"$tplayer doesn't know what a $cert certification is, so he can't forget it") + sendResponse(PacketCoding.CreateGamePacket(0, ItemTransactionResultMessage(msg.terminal_guid, TransactionType.Learn, false))) + } case Terminal.NoDeal() => log.warn(s"$tplayer made a request but the terminal rejected the order $msg") @@ -477,6 +503,7 @@ class WorldSessionActor extends Actor with MDCContextAware { 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))) + sendResponse(PacketCoding.CreateGamePacket(0, ChatMsg(ChatMessageType.CMT_EXPANSIONS, true, "", "1 on", None))) //CC on case Zone.ItemFromGround(tplayer, item) => val obj_guid = item.GUID From bd7d2da375460752efce3df8ca192330d1f144c2 Mon Sep 17 00:00:00 2001 From: Aphedox Date: Fri, 6 Oct 2017 23:34:37 -0500 Subject: [PATCH 07/23] Add HackMessage skeleton --- .../psforever/packet/GamePacketOpcode.scala | 2 +- .../psforever/packet/game/HackMessage.scala | 41 +++++++++++++++++++ .../src/test/scala/game/HackMessageTest.scala | 33 +++++++++++++++ 3 files changed, 75 insertions(+), 1 deletion(-) create mode 100644 common/src/main/scala/net/psforever/packet/game/HackMessage.scala create mode 100644 common/src/test/scala/game/HackMessageTest.scala diff --git a/common/src/main/scala/net/psforever/packet/GamePacketOpcode.scala b/common/src/main/scala/net/psforever/packet/GamePacketOpcode.scala index 5b68b55c..45d19e7a 100644 --- a/common/src/main/scala/net/psforever/packet/GamePacketOpcode.scala +++ b/common/src/main/scala/net/psforever/packet/GamePacketOpcode.scala @@ -418,7 +418,7 @@ object GamePacketOpcode extends Enumeration { case 0x51 => game.TriggerEffectMessage.decode case 0x52 => game.WeaponDryFireMessage.decode case 0x53 => noDecoder(DroppodLaunchRequestMessage) - case 0x54 => noDecoder(HackMessage) + case 0x54 => game.HackMessage.decode case 0x55 => noDecoder(DroppodLaunchResponseMessage) case 0x56 => noDecoder(GenericObjectActionMessage) case 0x57 => game.AvatarVehicleTimerMessage.decode diff --git a/common/src/main/scala/net/psforever/packet/game/HackMessage.scala b/common/src/main/scala/net/psforever/packet/game/HackMessage.scala new file mode 100644 index 00000000..ae3c0451 --- /dev/null +++ b/common/src/main/scala/net/psforever/packet/game/HackMessage.scala @@ -0,0 +1,41 @@ +// Copyright (c) 2017 PSForever +package net.psforever.packet.game + +import net.psforever.packet.{GamePacketOpcode, Marshallable, PacketHelpers, PlanetSideGamePacket} +import scodec.Codec +import scodec.codecs._ + +/** + * + * @param unk1 na + * @param unk2 na + * @param unk3 na + * @param unk4 na + * @param unk5 na + * @param unk6 na + * @param unk7 na + */ +final case class HackMessage(unk1 : Int, + unk2 : Int, + unk3 : Int, + unk4 : Int, + unk5 : Long, + unk6 : Int, + unk7 : Long) + extends PlanetSideGamePacket { + type Packet = HackMessage + def opcode = GamePacketOpcode.HackMessage + def encode = HackMessage.encode(this) +} + +object HackMessage extends Marshallable[HackMessage] { + implicit val codec : Codec[HackMessage] = ( + ("unk1" | uint2L) :: + ("unk2" | uint16L) :: + ("unk3" | uint16L) :: + ("unk4" | uint8L) :: + ("unk5" | uint32L) :: + ("unk6" | uint8L) :: + ("unk7" | uint32L) + ).as[HackMessage] +} diff --git a/common/src/test/scala/game/HackMessageTest.scala b/common/src/test/scala/game/HackMessageTest.scala new file mode 100644 index 00000000..8473e2c5 --- /dev/null +++ b/common/src/test/scala/game/HackMessageTest.scala @@ -0,0 +1,33 @@ +// Copyright (c) 2017 PSForever +package game + +import org.specs2.mutable._ +import net.psforever.packet._ +import net.psforever.packet.game._ +import scodec.bits._ + +class HackMessageTest extends Specification { + // Record 62 in PSCap-hack-door-tower.gcap + val string = hex"54 000105c3800000202fc04200000000" + + "decode" in { + PacketCoding.DecodePacket(string).require match { + case HackMessage(unk1, unk2, unk3, unk4, unk5, unk6, unk7) => + unk1 mustEqual 0 + unk2 mustEqual 1024 + unk3 mustEqual 3607 + unk4 mustEqual 0 + unk5 mustEqual 3212836864L + unk6 mustEqual 1 + unk7 mustEqual 8L + case _ => + ko + } + } + + "encode" in { + val msg = HackMessage(0,1024,3607,0,3212836864L,1,8L) + val pkt = PacketCoding.EncodePacket(msg).require.toByteVector + pkt mustEqual string + } +} From d2ae3dfab9a82893c5c7be5fd1be6496a2c40258 Mon Sep 17 00:00:00 2001 From: FateJH Date: Sat, 7 Oct 2017 21:57:20 -0400 Subject: [PATCH 08/23] initial TriggerSoundMessage packet work and tests --- .../psforever/packet/GamePacketOpcode.scala | 2 +- .../packet/game/TriggerSoundMessage.scala | 77 +++++++++++++++++++ .../scala/game/TriggerSoundMessageTest.scala | 33 ++++++++ 3 files changed, 111 insertions(+), 1 deletion(-) create mode 100644 common/src/main/scala/net/psforever/packet/game/TriggerSoundMessage.scala create mode 100644 common/src/test/scala/game/TriggerSoundMessageTest.scala diff --git a/common/src/main/scala/net/psforever/packet/GamePacketOpcode.scala b/common/src/main/scala/net/psforever/packet/GamePacketOpcode.scala index 45d19e7a..f4ef9765 100644 --- a/common/src/main/scala/net/psforever/packet/GamePacketOpcode.scala +++ b/common/src/main/scala/net/psforever/packet/GamePacketOpcode.scala @@ -445,7 +445,7 @@ object GamePacketOpcode extends Enumeration { case 0x68 => noDecoder(DroppodFreefallingMessage) case 0x69 => game.AvatarFirstTimeEventMessage.decode case 0x6a => noDecoder(AggravatedDamageMessage) - case 0x6b => noDecoder(TriggerSoundMessage) + case 0x6b => game.TriggerSoundMessage.decode case 0x6c => noDecoder(LootItemMessage) case 0x6d => noDecoder(VehicleSubStateMessage) case 0x6e => noDecoder(SquadMembershipRequest) diff --git a/common/src/main/scala/net/psforever/packet/game/TriggerSoundMessage.scala b/common/src/main/scala/net/psforever/packet/game/TriggerSoundMessage.scala new file mode 100644 index 00000000..24cc5fa9 --- /dev/null +++ b/common/src/main/scala/net/psforever/packet/game/TriggerSoundMessage.scala @@ -0,0 +1,77 @@ +// Copyright (c) 2017 PSForever +package net.psforever.packet.game + +import net.psforever.packet.{GamePacketOpcode, Marshallable, PacketHelpers, PlanetSideGamePacket} +import net.psforever.types.Vector3 +import scodec.Codec +import scodec.codecs._ +import shapeless.{::, HNil} + +/** + * An `Enumeration` of the sounds triggered by this packet. + * Twenty-one possible sounds are available for playback. + */ +object TriggeredSound extends Enumeration { + type Type = Value + + val + SpawnInTube, + Unknown1, + Hack, + HackDoor, + Unknown4, + LockedOut, + Unknown6, + Unknown7, + Unknown8, + Unknown9, + Unknown10, + Unknown11, + Unknown12, + Unknown13, + Unknown14, + Unknown15, + Unknown16, + Unknown17, + Unknown18, + Unknown19, + Unknown20 = Value + + implicit val codec = PacketHelpers.createEnumerationCodec(this, uintL(5)) +} + +/** + * Dispatched by the server to cause a sound to be played at a certain location in the world. + * @param sound the kind of sound + * @param pos the location where the sound gets played + * @param unk na; + * may be radius + * @param volume the volume of the sound at the origin (0.0f - 1.0f) + */ +final case class TriggerSoundMessage(sound : TriggeredSound.Value, + pos : Vector3, + unk : Int, + volume : Float) + extends PlanetSideGamePacket { + type Packet = TriggerSoundMessage + def opcode = GamePacketOpcode.TriggerSoundMessage + def encode = TriggerSoundMessage.encode(this) +} + +object TriggerSoundMessage extends Marshallable[TriggerSoundMessage] { + implicit val codec : Codec[TriggerSoundMessage] = ( + ("sound" | TriggeredSound.codec) :: + ("pos" | Vector3.codec_pos) :: + ("unk" | uintL(9)) :: + ("volume" | uint8L) + ).xmap[TriggerSoundMessage] ( + { + case a :: b :: c :: d :: HNil => + TriggerSoundMessage(a, b, c, d.toFloat * 0.0039215689f) + }, + { + case TriggerSoundMessage(a, b, c, d) => + a :: b :: c :: (d * 255f).toInt :: HNil + } + ) +} diff --git a/common/src/test/scala/game/TriggerSoundMessageTest.scala b/common/src/test/scala/game/TriggerSoundMessageTest.scala new file mode 100644 index 00000000..2db61b45 --- /dev/null +++ b/common/src/test/scala/game/TriggerSoundMessageTest.scala @@ -0,0 +1,33 @@ +// Copyright (c) 2017 PSForever +package game + +import org.specs2.mutable._ +import net.psforever.packet._ +import net.psforever.packet.game._ +import net.psforever.types.Vector3 +import scodec.bits._ + +class TriggerSoundMessageTest extends Specification { + val string = hex"6B 1FD5E1B466DB3858F1FC" + + "decode" in { + PacketCoding.DecodePacket(string).require match { + case TriggerSoundMessage(sound, pos, unk2, volume) => + sound mustEqual TriggeredSound.HackDoor + pos.x mustEqual 1913.9531f + pos.y mustEqual 6042.8125f + pos.z mustEqual 45.609375f + unk2 mustEqual 30 + volume mustEqual 0.49803925f + case _ => + ko + } + } + + "encode" in { + val msg = TriggerSoundMessage(TriggeredSound.HackDoor, Vector3(1913.9531f, 6042.8125f, 45.609375f), 30, 0.49803925f) + val pkt = PacketCoding.EncodePacket(msg).require.toByteVector + + pkt mustEqual string + } +} From fa633aa79d6507222a1254b7ef4554f9639ba727 Mon Sep 17 00:00:00 2001 From: FateJH Date: Mon, 25 Sep 2017 21:54:59 -0400 Subject: [PATCH 09/23] started doors --- .../net/psforever/objects/doors/Door.scala | 77 +++++++++++++++++++ .../objects/doors/DoorCloseControl.scala | 40 ++++++++++ .../psforever/objects/doors/DoorControl.scala | 27 +++++++ .../objects/doors/DoorDefinition.scala | 13 ++++ .../objects/zones/DoorObjectBuilder.scala | 33 ++++++++ .../objects/zones/ServerObjectBuilder.scala | 4 +- .../objects/zones/TerminalObjectBuilder.scala | 2 +- .../net/psforever/objects/zones/ZoneMap.scala | 6 +- pslogin/src/main/scala/PsLogin.scala | 8 +- .../src/main/scala/WorldSessionActor.scala | 39 +++++++--- 10 files changed, 233 insertions(+), 16 deletions(-) create mode 100644 common/src/main/scala/net/psforever/objects/doors/Door.scala create mode 100644 common/src/main/scala/net/psforever/objects/doors/DoorCloseControl.scala create mode 100644 common/src/main/scala/net/psforever/objects/doors/DoorControl.scala create mode 100644 common/src/main/scala/net/psforever/objects/doors/DoorDefinition.scala create mode 100644 common/src/main/scala/net/psforever/objects/zones/DoorObjectBuilder.scala diff --git a/common/src/main/scala/net/psforever/objects/doors/Door.scala b/common/src/main/scala/net/psforever/objects/doors/Door.scala new file mode 100644 index 00000000..623bcf42 --- /dev/null +++ b/common/src/main/scala/net/psforever/objects/doors/Door.scala @@ -0,0 +1,77 @@ +// Copyright (c) 2017 PSForever +package net.psforever.objects.doors + +import akka.actor.{ActorContext, ActorRef, Props} +import net.psforever.objects.{PlanetSideGameObject, Player} +import net.psforever.packet.game.UseItemMessage +import net.psforever.types.PlanetSideEmpire + +/** + * na + * @param ddef the `ObjectDefinition` that constructs this object and maintains some of its immutable fields + */ +class Door(ddef : DoorDefinition) extends PlanetSideGameObject { + /** Internal reference to the `Actor` for this `Door`, sets up by this `Door`. */ + private var actor = ActorRef.noSender + + def Actor(implicit context : ActorContext) : ActorRef = { + if(actor == ActorRef.noSender) { + actor = context.actorOf(Props(classOf[DoorControl], this), s"${ddef.Name}_${GUID.guid}") + } + actor + } + + private var openState : Boolean = false + private var faction : PlanetSideEmpire.Value = PlanetSideEmpire.NEUTRAL + private var hackedBy : Option[PlanetSideEmpire.Value] = None + + def Open : Boolean = openState + + def Faction : PlanetSideEmpire.Value = faction + + def Convert(toFaction : PlanetSideEmpire.Value) : Unit = { + hackedBy = None + faction = toFaction + } + + def Request(player : Player, msg : UseItemMessage) : Door.Exchange = { + if(!openState) { + if(faction == PlanetSideEmpire.NEUTRAL || player.Faction == faction) { + Door.OpenEvent() + } + else { + Door.NoEvent() + } + } + else { + Door.NoEvent() + } + } + + def Definition : DoorDefinition = ddef +} + +object Door { + final case class Request(player : Player, msg : UseItemMessage) + + sealed trait Exchange + + final case class DoorMessage(player : Player, msg : UseItemMessage, response : Exchange) + + final case class OpenEvent() extends Exchange + + final case class CloseEvent() extends Exchange + + final case class NoEvent() extends Exchange + + def apply(tdef : DoorDefinition) : Door = { + new Door(tdef) + } + + import net.psforever.packet.game.PlanetSideGUID + def apply(guid : PlanetSideGUID, ddef : DoorDefinition) : Door = { + val obj = new Door(ddef) + obj.GUID = guid + obj + } +} diff --git a/common/src/main/scala/net/psforever/objects/doors/DoorCloseControl.scala b/common/src/main/scala/net/psforever/objects/doors/DoorCloseControl.scala new file mode 100644 index 00000000..ae20b6e7 --- /dev/null +++ b/common/src/main/scala/net/psforever/objects/doors/DoorCloseControl.scala @@ -0,0 +1,40 @@ +// Copyright (c) 2017 PSForever +package net.psforever.objects.doors + +import akka.actor.{Actor, ActorRef, Cancellable} +import net.psforever.packet.game.PlanetSideGUID + +import scala.collection.mutable.ListBuffer + +class DoorCloseControl(implicit val environment : ActorRef) extends Actor { + import DoorCloseControl._ + private var doorCloser : Cancellable = DefaultCloser + private var openDoors : List[DoorEntry] = Nil + + def receive : Receive = { + case DoorIsOpen(guid, time) => + if(openDoors.isEmpty) { + //doorCloser = context.system.scheduler.scheduleOnce(timeout, environment, Door.DoorMessage()) + } + else { + openDoors = openDoors :+ DoorEntry(guid, time) + } + + case _ => ; + } +} + +object DoorCloseControl { + private final val timeout : Long = 5000L + + private final val DefaultCloser : Cancellable = new Cancellable() { + override def cancel : Boolean = true + override def isCancelled : Boolean = true + } + + private final case class DoorEntry(door_guid : PlanetSideGUID, opened_at_time : Long) + + final case class DoorIsOpen(door_guid : PlanetSideGUID, opened_at_time : Long = System.nanoTime()) + + final case class CloseTheDoor(door_guid : PlanetSideGUID) +} diff --git a/common/src/main/scala/net/psforever/objects/doors/DoorControl.scala b/common/src/main/scala/net/psforever/objects/doors/DoorControl.scala new file mode 100644 index 00000000..0491a59a --- /dev/null +++ b/common/src/main/scala/net/psforever/objects/doors/DoorControl.scala @@ -0,0 +1,27 @@ +// Copyright (c) 2017 PSForever +package net.psforever.objects.doors + +import akka.actor.{Actor, Cancellable} + +/** + * An `Actor` that handles messages being dispatched to a specific `Door`. + * @param door the `Door` object being governed + */ +class DoorControl(door : Door) extends Actor { + private var doorCloser : Cancellable = DoorControl.DefaultCloser + + def receive : Receive = { + case Door.Request(player, msg) => + sender ! Door.DoorMessage(player, msg, door.Request(player, msg)) + //doorCloser = context.system.scheduler.scheduleOnce(5000L, sender, Door.DoorMessage()) + case _ => + sender ! Door.NoEvent() + } +} + +object DoorControl { + final val DefaultCloser : Cancellable = new Cancellable() { + override def cancel : Boolean = true + override def isCancelled : Boolean = true + } +} diff --git a/common/src/main/scala/net/psforever/objects/doors/DoorDefinition.scala b/common/src/main/scala/net/psforever/objects/doors/DoorDefinition.scala new file mode 100644 index 00000000..777cccf5 --- /dev/null +++ b/common/src/main/scala/net/psforever/objects/doors/DoorDefinition.scala @@ -0,0 +1,13 @@ +// Copyright (c) 2017 PSForever +package net.psforever.objects.doors + +import net.psforever.objects.definition.ObjectDefinition + +/** + * The definition for any `door`. + * @param objectId the object's identifier number + */ +abstract class DoorDefinition(objectId : Int) extends ObjectDefinition(objectId) { + Name = "door" +} + diff --git a/common/src/main/scala/net/psforever/objects/zones/DoorObjectBuilder.scala b/common/src/main/scala/net/psforever/objects/zones/DoorObjectBuilder.scala new file mode 100644 index 00000000..8f4ead88 --- /dev/null +++ b/common/src/main/scala/net/psforever/objects/zones/DoorObjectBuilder.scala @@ -0,0 +1,33 @@ +// Copyright (c) 2017 PSForever +package net.psforever.objects.zones + +import net.psforever.objects.doors.{Door, DoorDefinition} + +/** + * Wrapper `Class` designed to instantiate a `Door` server object. + * @param ddef a `DoorDefinition` object, indicating the specific functionality of the resulting `Door` + * @param id the globally unique identifier to which this `Door` will be registered + */ +class DoorObjectBuilder(private val ddef : DoorDefinition, private val id : Int) extends ServerObjectBuilder[Door] { + import akka.actor.ActorContext + import net.psforever.objects.guid.NumberPoolHub + + def Build(implicit context : ActorContext, guid : NumberPoolHub) : Door = { + val obj = Door(ddef) + 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 DoorObjectBuilder { + /** + * Overloaded constructor for a `DoorObjectBuilder`. + * @param ddef a `DoorDefinition` object + * @param id a globally unique identifier + * @return a `DoorObjectBuilder` object + */ + def apply(ddef : DoorDefinition, id : Int) : DoorObjectBuilder = { + new DoorObjectBuilder(ddef, id) + } +} diff --git a/common/src/main/scala/net/psforever/objects/zones/ServerObjectBuilder.scala b/common/src/main/scala/net/psforever/objects/zones/ServerObjectBuilder.scala index 2635df09..391ec775 100644 --- a/common/src/main/scala/net/psforever/objects/zones/ServerObjectBuilder.scala +++ b/common/src/main/scala/net/psforever/objects/zones/ServerObjectBuilder.scala @@ -9,7 +9,7 @@ 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 { +trait ServerObjectBuilder[A <: PlanetSideGameObject] { /** * Instantiate and configure the given server object * (at a later time compared to the construction of the builder class).
@@ -23,5 +23,5 @@ trait ServerObjectBuilder { * defaults to `null` * @return the object that was created and integrated into the `Zone` */ - def Build(implicit context : ActorContext = null, guid : NumberPoolHub = null) : PlanetSideGameObject + def Build(implicit context : ActorContext = null, guid : NumberPoolHub = null) : A } diff --git a/common/src/main/scala/net/psforever/objects/zones/TerminalObjectBuilder.scala b/common/src/main/scala/net/psforever/objects/zones/TerminalObjectBuilder.scala index 2ced4d84..b78921e1 100644 --- a/common/src/main/scala/net/psforever/objects/zones/TerminalObjectBuilder.scala +++ b/common/src/main/scala/net/psforever/objects/zones/TerminalObjectBuilder.scala @@ -8,7 +8,7 @@ import net.psforever.objects.terminals.{Terminal, TerminalDefinition} * @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 { +class TerminalObjectBuilder(private val tdef : TerminalDefinition, private val id : Int) extends ServerObjectBuilder[Terminal] { import akka.actor.ActorContext import net.psforever.objects.guid.NumberPoolHub diff --git a/common/src/main/scala/net/psforever/objects/zones/ZoneMap.scala b/common/src/main/scala/net/psforever/objects/zones/ZoneMap.scala index b1cf2e9a..05ae29e0 100644 --- a/common/src/main/scala/net/psforever/objects/zones/ZoneMap.scala +++ b/common/src/main/scala/net/psforever/objects/zones/ZoneMap.scala @@ -21,7 +21,7 @@ package net.psforever.objects.zones * `LoadMapMessage` */ class ZoneMap(private val name : String) { - private var localObjects : List[ServerObjectBuilder] = List() + private var localObjects : List[ServerObjectBuilder[_]] = List() def Name : String = name @@ -29,7 +29,7 @@ class ZoneMap(private val name : String) { * 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 = { + def LocalObject(obj : ServerObjectBuilder[_]) : Unit = { localObjects = localObjects :+ obj } @@ -37,7 +37,7 @@ class ZoneMap(private val name : String) { * 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] = { + def LocalObjects : List[ServerObjectBuilder[_]] = { localObjects } } diff --git a/pslogin/src/main/scala/PsLogin.scala b/pslogin/src/main/scala/PsLogin.scala index f71cce11..d0c22dc4 100644 --- a/pslogin/src/main/scala/PsLogin.scala +++ b/pslogin/src/main/scala/PsLogin.scala @@ -12,7 +12,7 @@ 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.zones.{InterstellarCluster, TerminalObjectBuilder, Zone, ZoneMap} +import net.psforever.objects.zones._ import net.psforever.objects.guid.TaskResolver import org.slf4j import org.fusesource.jansi.Ansi._ @@ -222,6 +222,12 @@ object PsLogin { def createContinents() : List[Zone] = { val map13 = new ZoneMap("map13") { import net.psforever.objects.GlobalDefinitions._ + val ddef = new net.psforever.objects.doors.DoorDefinition(242) {} //generic door + + LocalObject(DoorObjectBuilder(ddef, 330)) + LocalObject(DoorObjectBuilder(ddef, 332)) + LocalObject(DoorObjectBuilder(ddef, 372)) + LocalObject(DoorObjectBuilder(ddef, 373)) LocalObject(TerminalObjectBuilder(cert_terminal, 186)) LocalObject(TerminalObjectBuilder(cert_terminal, 187)) LocalObject(TerminalObjectBuilder(cert_terminal, 188)) diff --git a/pslogin/src/main/scala/WorldSessionActor.scala b/pslogin/src/main/scala/WorldSessionActor.scala index d759a8f3..719c1cd1 100644 --- a/pslogin/src/main/scala/WorldSessionActor.scala +++ b/pslogin/src/main/scala/WorldSessionActor.scala @@ -11,6 +11,7 @@ import org.log4s.MDC import MDCContextAware.Implicits._ import ServiceManager.Lookup import net.psforever.objects._ +import net.psforever.objects.doors.Door import net.psforever.objects.zones.{InterstellarCluster, Zone} import net.psforever.objects.entity.IdentifiableEntity import net.psforever.objects.equipment._ @@ -206,6 +207,17 @@ class WorldSessionActor extends Actor with MDCContextAware { case _ => ; } + case Door.DoorMessage(_, msg, order) => + order match { + case Door.OpenEvent() => + sendResponse(PacketCoding.CreateGamePacket(0, GenericObjectStateMsg(msg.object_guid, 16))) + + case Door.CloseEvent() => + sendResponse(PacketCoding.CreateGamePacket(0, GenericObjectStateMsg(msg.object_guid, 17))) + + case Door.NoEvent() => ; + } + case Terminal.TerminalMessage(tplayer, msg, order) => order match { case Terminal.BuyExosuit(exosuit, subtype) => @@ -943,15 +955,24 @@ class WorldSessionActor extends Actor with MDCContextAware { log.info("UseItem: " + msg) // TODO: Not all fields in the response are identical to source in real packet logs (but seems to be ok) // TODO: Not all incoming UseItemMessage's respond with another UseItemMessage (i.e. doors only send out GenericObjectStateMsg) - if (itemType != 121) sendResponse(PacketCoding.CreateGamePacket(0, UseItemMessage(avatar_guid, unk1, object_guid, unk2, unk3, unk4, unk5, unk6, unk7, unk8, itemType))) - if (itemType == 121 && !unk3){ // TODO : medkit use ?! - sendResponse(PacketCoding.CreateGamePacket(0, UseItemMessage(avatar_guid, unk1, object_guid, 0, unk3, unk4, unk5, unk6, unk7, unk8, itemType))) - sendResponse(PacketCoding.CreateGamePacket(0, PlanetsideAttributeMessage(avatar_guid, 0, 100))) // avatar with 100 hp - sendResponse(PacketCoding.CreateGamePacket(0, ObjectDeleteMessage(PlanetSideGUID(unk1), 2))) - } - if (unk1 == 0 && !unk3 && unk7 == 25) { - // TODO: This should only actually be sent to doors upon opening; may break non-door items upon use - sendResponse(PacketCoding.CreateGamePacket(0, GenericObjectStateMsg(object_guid, 16))) + continent.GUID(object_guid) match { + case Some(door : Door) => + log.info("Door action!") + door.Actor ! Door.Request(player, msg) + case Some(obj : PlanetSideGameObject) => + if(itemType != 121) { + sendResponse(PacketCoding.CreateGamePacket(0, UseItemMessage(avatar_guid, unk1, object_guid, unk2, unk3, unk4, unk5, unk6, unk7, unk8, itemType))) + } + else if(itemType == 121 && !unk3) { // TODO : medkit use ?! + sendResponse(PacketCoding.CreateGamePacket(0, UseItemMessage(avatar_guid, unk1, object_guid, 0, unk3, unk4, unk5, unk6, unk7, unk8, itemType))) + sendResponse(PacketCoding.CreateGamePacket(0, PlanetsideAttributeMessage(avatar_guid, 0, 100))) // avatar with 100 hp + sendResponse(PacketCoding.CreateGamePacket(0, ObjectDeleteMessage(PlanetSideGUID(unk1), 2))) + } +// if(unk1 == 0 && !unk3 && unk7 == 25) { +// // TODO: This should only actually be sent to doors upon opening; may break non-door items upon use +// sendResponse(PacketCoding.CreateGamePacket(0, GenericObjectStateMsg(object_guid, 16))) +// } + case None => ; } case msg @ UnuseItemMessage(player_guid, item) => From 7fcac7fc25a76f78564dd20b2b41523dc9e6a74c Mon Sep 17 00:00:00 2001 From: FateJH Date: Fri, 29 Sep 2017 23:35:55 -0400 Subject: [PATCH 10/23] automated doors, IFF locks, and bases thus that only permissible doors can be opened by players of correct faction alignment; Base is just a prototype example, hastily created for this functionality; LocalService will eventually be used for doors messages (and other things) --- .../psforever/objects/GlobalDefinitions.scala | 10 +++ .../net/psforever/objects/doors/Base.scala | 17 +++++ .../net/psforever/objects/doors/Door.scala | 36 +++++----- .../psforever/objects/doors/DoorControl.scala | 4 +- .../objects/doors/DoorDefinition.scala | 5 +- .../net/psforever/objects/doors/IFFLock.scala | 28 ++++++++ .../objects/doors/IFFLockDefinition.scala | 8 +++ .../objects/zones/IFFLockObjectBuilder.scala | 32 +++++++++ .../net/psforever/objects/zones/Zone.scala | 14 ++++ .../psforever/objects/zones/ZoneActor.scala | 40 +++++++++++ .../net/psforever/objects/zones/ZoneMap.scala | 39 ++++++++--- pslogin/src/main/scala/AvatarService.scala | 70 ++++++------------- pslogin/src/main/scala/LocalService.scala | 58 +++++++++++++++ pslogin/src/main/scala/PsLogin.scala | 33 +++++++-- pslogin/src/main/scala/Service.scala | 29 ++++++++ .../src/main/scala/WorldSessionActor.scala | 36 +++++++--- 16 files changed, 366 insertions(+), 93 deletions(-) create mode 100644 common/src/main/scala/net/psforever/objects/doors/Base.scala create mode 100644 common/src/main/scala/net/psforever/objects/doors/IFFLock.scala create mode 100644 common/src/main/scala/net/psforever/objects/doors/IFFLockDefinition.scala create mode 100644 common/src/main/scala/net/psforever/objects/zones/IFFLockObjectBuilder.scala create mode 100644 pslogin/src/main/scala/LocalService.scala create mode 100644 pslogin/src/main/scala/Service.scala diff --git a/common/src/main/scala/net/psforever/objects/GlobalDefinitions.scala b/common/src/main/scala/net/psforever/objects/GlobalDefinitions.scala index 4fccc47f..1326f447 100644 --- a/common/src/main/scala/net/psforever/objects/GlobalDefinitions.scala +++ b/common/src/main/scala/net/psforever/objects/GlobalDefinitions.scala @@ -3,11 +3,16 @@ package net.psforever.objects import net.psforever.objects.definition._ import net.psforever.objects.definition.converter.{CommandDetonaterConverter, LockerContainerConverter, REKConverter} +import net.psforever.objects.doors.{DoorDefinition, IFFLockDefinition} import net.psforever.objects.equipment.CItem.DeployedItem import net.psforever.objects.equipment._ import net.psforever.objects.inventory.InventoryTile +<<<<<<< c5ae9e477ccade5759eac2e9526ba898d0e8f16b import net.psforever.objects.terminals.{CertTerminalDefinition, OrderTerminalDefinition} import net.psforever.packet.game.objectcreate.ObjectClass +======= +import net.psforever.objects.terminals.OrderTerminalDefinition +>>>>>>> automated doors, IFF locks, and bases thus that only permissible doors can be opened by players of correct faction alignment; Base is just a prototype example, hastily created for this functionality; LocalService will eventually be used for doors messages (and other things) import net.psforever.types.PlanetSideEmpire object GlobalDefinitions { @@ -1242,4 +1247,9 @@ object GlobalDefinitions { order_terminal = new OrderTerminalDefinition val cert_terminal = new CertTerminalDefinition + + val + external_lock = new IFFLockDefinition + val + door = new DoorDefinition } diff --git a/common/src/main/scala/net/psforever/objects/doors/Base.scala b/common/src/main/scala/net/psforever/objects/doors/Base.scala new file mode 100644 index 00000000..9fa8b3d9 --- /dev/null +++ b/common/src/main/scala/net/psforever/objects/doors/Base.scala @@ -0,0 +1,17 @@ +// Copyright (c) 2017 PSForever +package net.psforever.objects.doors + +import net.psforever.types.PlanetSideEmpire + +class Base(private val id : Int) { + private var faction : PlanetSideEmpire.Value = PlanetSideEmpire.NEUTRAL + + def Id : Int = id + + def Faction : PlanetSideEmpire.Value = faction + + def Faction_=(emp : PlanetSideEmpire.Value) : PlanetSideEmpire.Value = { + faction = emp + Faction + } +} diff --git a/common/src/main/scala/net/psforever/objects/doors/Door.scala b/common/src/main/scala/net/psforever/objects/doors/Door.scala index 623bcf42..179256f9 100644 --- a/common/src/main/scala/net/psforever/objects/doors/Door.scala +++ b/common/src/main/scala/net/psforever/objects/doors/Door.scala @@ -4,7 +4,6 @@ package net.psforever.objects.doors import akka.actor.{ActorContext, ActorRef, Props} import net.psforever.objects.{PlanetSideGameObject, Player} import net.psforever.packet.game.UseItemMessage -import net.psforever.types.PlanetSideEmpire /** * na @@ -22,26 +21,29 @@ class Door(ddef : DoorDefinition) extends PlanetSideGameObject { } private var openState : Boolean = false - private var faction : PlanetSideEmpire.Value = PlanetSideEmpire.NEUTRAL - private var hackedBy : Option[PlanetSideEmpire.Value] = None + private var lockState : Boolean = false def Open : Boolean = openState - def Faction : PlanetSideEmpire.Value = faction - - def Convert(toFaction : PlanetSideEmpire.Value) : Unit = { - hackedBy = None - faction = toFaction + def Open_=(open : Boolean) : Boolean = { + openState = open + Open } - def Request(player : Player, msg : UseItemMessage) : Door.Exchange = { - if(!openState) { - if(faction == PlanetSideEmpire.NEUTRAL || player.Faction == faction) { - Door.OpenEvent() - } - else { - Door.NoEvent() - } + def Locked : Boolean = lockState + + def Locked_=(lock : Boolean) : Boolean = { + lockState = lock + Locked + } + + def Use(player : Player, msg : UseItemMessage) : Door.Exchange = { + if(!lockState && !openState) { + openState = true + Door.OpenEvent() + } + else if(openState) { + Door.CloseEvent() } else { Door.NoEvent() @@ -52,7 +54,7 @@ class Door(ddef : DoorDefinition) extends PlanetSideGameObject { } object Door { - final case class Request(player : Player, msg : UseItemMessage) + final case class Use(player : Player, msg : UseItemMessage) sealed trait Exchange diff --git a/common/src/main/scala/net/psforever/objects/doors/DoorControl.scala b/common/src/main/scala/net/psforever/objects/doors/DoorControl.scala index 0491a59a..e98fb9b7 100644 --- a/common/src/main/scala/net/psforever/objects/doors/DoorControl.scala +++ b/common/src/main/scala/net/psforever/objects/doors/DoorControl.scala @@ -11,8 +11,8 @@ class DoorControl(door : Door) extends Actor { private var doorCloser : Cancellable = DoorControl.DefaultCloser def receive : Receive = { - case Door.Request(player, msg) => - sender ! Door.DoorMessage(player, msg, door.Request(player, msg)) + case Door.Use(player, msg) => + sender ! Door.DoorMessage(player, msg, door.Use(player, msg)) //doorCloser = context.system.scheduler.scheduleOnce(5000L, sender, Door.DoorMessage()) case _ => sender ! Door.NoEvent() diff --git a/common/src/main/scala/net/psforever/objects/doors/DoorDefinition.scala b/common/src/main/scala/net/psforever/objects/doors/DoorDefinition.scala index 777cccf5..d18c50c4 100644 --- a/common/src/main/scala/net/psforever/objects/doors/DoorDefinition.scala +++ b/common/src/main/scala/net/psforever/objects/doors/DoorDefinition.scala @@ -5,9 +5,8 @@ import net.psforever.objects.definition.ObjectDefinition /** * The definition for any `door`. - * @param objectId the object's identifier number + * Object Id 242 is a generic door. */ -abstract class DoorDefinition(objectId : Int) extends ObjectDefinition(objectId) { +class DoorDefinition extends ObjectDefinition(242) { Name = "door" } - diff --git a/common/src/main/scala/net/psforever/objects/doors/IFFLock.scala b/common/src/main/scala/net/psforever/objects/doors/IFFLock.scala new file mode 100644 index 00000000..81554a0b --- /dev/null +++ b/common/src/main/scala/net/psforever/objects/doors/IFFLock.scala @@ -0,0 +1,28 @@ +// Copyright (c) 2017 PSForever +package net.psforever.objects.doors + +import net.psforever.objects.{GlobalDefinitions, PlanetSideGameObject} +import net.psforever.types.PlanetSideEmpire + +class IFFLock extends PlanetSideGameObject { + private var hackedBy : Option[PlanetSideEmpire.Value] = None + + def Hacker : Option[PlanetSideEmpire.Value] = hackedBy + + def Hacker_=(hacker : PlanetSideEmpire.Value) : Option[PlanetSideEmpire.Value] = { + Hacker = Some(hacker) + } + + def Hacker_=(hacker : Option[PlanetSideEmpire.Value]) : Option[PlanetSideEmpire.Value] = { + hackedBy = hacker + Hacker + } + + def Definition : IFFLockDefinition = GlobalDefinitions.external_lock +} + +object IFFLock { + def apply() : IFFLock = { + new IFFLock + } +} diff --git a/common/src/main/scala/net/psforever/objects/doors/IFFLockDefinition.scala b/common/src/main/scala/net/psforever/objects/doors/IFFLockDefinition.scala new file mode 100644 index 00000000..afd6a2a7 --- /dev/null +++ b/common/src/main/scala/net/psforever/objects/doors/IFFLockDefinition.scala @@ -0,0 +1,8 @@ +// Copyright (c) 2017 PSForever +package net.psforever.objects.doors + +import net.psforever.objects.definition.ObjectDefinition + +class IFFLockDefinition extends ObjectDefinition(0) { + Name = "iff_lock" +} diff --git a/common/src/main/scala/net/psforever/objects/zones/IFFLockObjectBuilder.scala b/common/src/main/scala/net/psforever/objects/zones/IFFLockObjectBuilder.scala new file mode 100644 index 00000000..e4e55011 --- /dev/null +++ b/common/src/main/scala/net/psforever/objects/zones/IFFLockObjectBuilder.scala @@ -0,0 +1,32 @@ +// Copyright (c) 2017 PSForever +package net.psforever.objects.zones + +import net.psforever.objects.doors.{IFFLock, IFFLockDefinition} + +/** + * Wrapper `Class` designed to instantiate a `Door` server object. + * @param idef a `IFFLockDefinition` object, indicating the specific functionality of the resulting `Door` + * @param id the globally unique identifier to which this `IFFLock` will be registered + */ +class IFFLockObjectBuilder(private val idef : IFFLockDefinition, private val id : Int) extends ServerObjectBuilder[IFFLock] { + import akka.actor.ActorContext + import net.psforever.objects.guid.NumberPoolHub + + def Build(implicit context : ActorContext, guid : NumberPoolHub) : IFFLock = { + val obj = IFFLock() + guid.register(obj, id) //non-Actor GUID registration + obj + } +} + +object IFFLockObjectBuilder { + /** + * Overloaded constructor for a `IFFLockObjectBuilder`. + * @param idef an `IFFLock` object + * @param id a globally unique identifier + * @return an `IFFLockObjectBuilder` object + */ + def apply(idef : IFFLockDefinition, id : Int) : IFFLockObjectBuilder = { + new IFFLockObjectBuilder(idef, 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 index 617fbb59..ae3cf6fa 100644 --- a/common/src/main/scala/net/psforever/objects/zones/Zone.scala +++ b/common/src/main/scala/net/psforever/objects/zones/Zone.scala @@ -2,6 +2,7 @@ package net.psforever.objects.zones import akka.actor.{ActorContext, ActorRef, Props} +import net.psforever.objects.doors.Base import net.psforever.objects.{PlanetSideGameObject, Player} import net.psforever.objects.equipment.Equipment import net.psforever.objects.guid.NumberPoolHub @@ -48,6 +49,8 @@ class Zone(private val zoneId : String, zoneMap : ZoneMap, zoneNumber : Int) { /** Used by the `Zone` to coordinate `Equipment` dropping and collection requests. */ private var ground : ActorRef = ActorRef.noSender + private var bases : List[Base] = List() + /** * Establish the basic accessible conditions necessary for a functional `Zone`.
*
@@ -69,6 +72,8 @@ class Zone(private val zoneId : String, zoneMap : ZoneMap, zoneNumber : Int) { Map.LocalObjects.foreach({ builderObject => builderObject.Build }) + + MakeBases(Map.LocalBases) } } @@ -169,6 +174,15 @@ class Zone(private val zoneId : String, zoneMap : ZoneMap, zoneNumber : Int) { */ def Ground : ActorRef = ground + def MakeBases(num : Int) : List[Base] = { + bases = (0 to num).map(id => new Base(id)).toList + bases + } + + def Base(id : Int) : Option[Base] = { + bases.lift(id) + } + /** * 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:
diff --git a/common/src/main/scala/net/psforever/objects/zones/ZoneActor.scala b/common/src/main/scala/net/psforever/objects/zones/ZoneActor.scala index 811b6ef9..ba34619c 100644 --- a/common/src/main/scala/net/psforever/objects/zones/ZoneActor.scala +++ b/common/src/main/scala/net/psforever/objects/zones/ZoneActor.scala @@ -13,8 +13,48 @@ class ZoneActor(zone : Zone) extends Actor { def receive : Receive = { case Zone.Init() => zone.Init + ZoneSetupCheck() case msg => log.warn(s"Received unexpected message - $msg") } + + def ZoneSetupCheck(): Unit = { + def guid(id : Int) = zone.GUID(id) + val map = zone.Map + val slog = org.log4s.getLogger(s"zone/${zone.Id}/sanity") + + //check base to object associations + map.ObjectToBase.foreach({ case((object_guid, base_id)) => + if(zone.Base(base_id).isEmpty) { + slog.error(s"expected a base #$base_id") + } + if(guid(object_guid).isEmpty) { + slog.error(s"expected object id $object_guid to exist, but it did not") + } + }) + + //check door to lock association + import net.psforever.objects.doors.{Door, IFFLock} + map.DoorToLock.foreach({ case((door_guid, lock_guid)) => + try { + if(!guid(door_guid).get.isInstanceOf[Door]) { + slog.error(s"expected id $door_guid to be a door, but it was not") + } + } + catch { + case _ : Exception => + slog.error(s"expected a door, but looking for uninitialized object $door_guid") + } + try { + if(!guid(lock_guid).get.isInstanceOf[IFFLock]) { + slog.error(s"expected id $lock_guid to be an IFF lock, but it was not") + } + } + catch { + case _ : Exception => + slog.error(s"expected an IFF lock, but looking for uninitialized object $lock_guid") + } + }) + } } diff --git a/common/src/main/scala/net/psforever/objects/zones/ZoneMap.scala b/common/src/main/scala/net/psforever/objects/zones/ZoneMap.scala index 05ae29e0..7c56a131 100644 --- a/common/src/main/scala/net/psforever/objects/zones/ZoneMap.scala +++ b/common/src/main/scala/net/psforever/objects/zones/ZoneMap.scala @@ -12,19 +12,31 @@ package net.psforever.objects.zones * Use it as a blueprint.
*
* The "training zones" are the best example of the difference between a `ZoneMap` and a `Zone.` + * ("Course" will be used as an unofficial location and layout descriptor.) * `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`). + * While each course can have different objects and object states, i.e., a `Zone`, + * both of these courses utilize the same basic server object layout because they are built from the same blueprint, i.e., a `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() + private var linkDoorLock : Map[Int, Int] = Map() + private var linkObjectBase : Map[Int, Int] = Map() + private var numBases : Int = 0 def Name : String = name + /** + * 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 + } + /** * Append the builder for a server object to the list of builders known to this `ZoneMap`. * @param obj the builder for a server object @@ -33,11 +45,22 @@ class ZoneMap(private val name : String) { 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 + def LocalBases : Int = numBases + + def LocalBases_=(num : Int) : Int = { + numBases = math.max(0, num) + LocalBases + } + + def ObjectToBase : Map[Int, Int] = linkObjectBase + + def ObjectToBase(object_guid : Int, base_id : Int) : Unit = { + linkObjectBase = linkObjectBase ++ Map(object_guid -> base_id) + } + + def DoorToLock : Map[Int, Int] = linkDoorLock + + def DoorToLock(door_guid : Int, lock_guid : Int) = { + linkDoorLock = linkDoorLock ++ Map(door_guid -> lock_guid) } } diff --git a/pslogin/src/main/scala/AvatarService.scala b/pslogin/src/main/scala/AvatarService.scala index 377a9249..5905366f 100644 --- a/pslogin/src/main/scala/AvatarService.scala +++ b/pslogin/src/main/scala/AvatarService.scala @@ -1,22 +1,14 @@ -// Copyright (c) 2016 PSForever.net to present +// Copyright (c) 2017 PSForever import akka.actor.Actor -import akka.event.{ActorEventBus, SubchannelClassification} -import akka.util.Subclassification import net.psforever.objects.equipment.Equipment import net.psforever.packet.game.objectcreate.ConstructorData import net.psforever.types.ExoSuitType import net.psforever.packet.game.{PlanetSideGUID, PlayerStateMessageUpstream} import net.psforever.types.Vector3 -sealed trait Action - -sealed trait Response - -final case class Join(channel : String) -final case class Leave() -final case class LeaveAll() - object AvatarAction { + trait Action + final case class ArmorChanged(player_guid : PlanetSideGUID, suit : ExoSuitType.Value, subtype : Int) extends Action //final case class DropItem(pos : Vector3, orient : Vector3, item : PlanetSideGUID) extends Action final case class EquipmentInHand(player_guid : PlanetSideGUID, slot : Int, item : Equipment) extends Action @@ -36,6 +28,8 @@ object AvatarAction { } object AvatarServiceResponse { + trait Response + final case class ArmorChanged(suit : ExoSuitType.Value, subtype : Int) extends Response //final case class DropItem(pos : Vector3, orient : Vector3, item : PlanetSideGUID) extends Response final case class EquipmentInHand(slot : Int, item : Equipment) extends Response @@ -54,30 +48,14 @@ object AvatarServiceResponse { // final case class ChangeWeapon(facingYaw : Int) extends Response } -final case class AvatarServiceMessage(forChannel : String, actionMessage : Action) +final case class AvatarServiceMessage(forChannel : String, actionMessage : AvatarAction.Action) -final case class AvatarServiceResponse(toChannel : String, avatar_guid : PlanetSideGUID, replyMessage : Response) +final case class AvatarServiceResponse(toChannel : String, avatar_guid : PlanetSideGUID, replyMessage : AvatarServiceResponse.Response) extends GenericEventBusMsg /* - /avatar/ + /Avatar/ */ -class AvatarEventBus extends ActorEventBus with SubchannelClassification { - type Event = AvatarServiceResponse - type Classifier = String - - protected def classify(event: Event): Classifier = event.toChannel - - protected def subclassification = new Subclassification[Classifier] { - def isEqual(x: Classifier, y: Classifier) = x == y - def isSubclass(x: Classifier, y: Classifier) = x.startsWith(y) - } - - protected def publish(event: Event, subscriber: Subscriber): Unit = { - subscriber ! event - } -} - class AvatarService extends Actor { //import AvatarServiceResponse._ private [this] val log = org.log4s.getLogger @@ -86,62 +64,58 @@ class AvatarService extends Actor { log.info("Starting...") } - val AvatarEvents = new AvatarEventBus - - /*val channelMap = Map( - AvatarMessageType.CMT_OPEN -> AvatarPath("local") - )*/ + val AvatarEvents = new GenericEventBus[AvatarServiceResponse] //AvatarEventBus def receive = { - case Join(channel) => - val path = "/Avatar/" + channel + case Service.Join(channel) => + val path = s"/$channel/Avatar" val who = sender() log.info(s"$who has joined $path") AvatarEvents.subscribe(who, path) - case Leave() => + case Service.Leave() => AvatarEvents.unsubscribe(sender()) - case LeaveAll() => + case Service.LeaveAll() => AvatarEvents.unsubscribe(sender()) case AvatarServiceMessage(forChannel, action) => action match { case AvatarAction.ArmorChanged(player_guid, suit, subtype) => AvatarEvents.publish( - AvatarServiceResponse("/Avatar/" + forChannel, player_guid, AvatarServiceResponse.ArmorChanged(suit, subtype)) + AvatarServiceResponse(s"/$forChannel/Avatar", player_guid, AvatarServiceResponse.ArmorChanged(suit, subtype)) ) case AvatarAction.EquipmentInHand(player_guid, slot, obj) => AvatarEvents.publish( - AvatarServiceResponse("/Avatar/" + forChannel, player_guid, AvatarServiceResponse.EquipmentInHand(slot, obj)) + AvatarServiceResponse(s"/$forChannel/Avatar", player_guid, AvatarServiceResponse.EquipmentInHand(slot, obj)) ) case AvatarAction.EquipmentOnGround(player_guid, pos, orient, obj) => AvatarEvents.publish( - AvatarServiceResponse("/Avatar/" + forChannel, player_guid, AvatarServiceResponse.EquipmentOnGround(pos, orient, obj)) + AvatarServiceResponse(s"/$forChannel/Avatar", player_guid, AvatarServiceResponse.EquipmentOnGround(pos, orient, obj)) ) case AvatarAction.LoadPlayer(player_guid, pdata) => AvatarEvents.publish( - AvatarServiceResponse("/Avatar/" + forChannel, player_guid, AvatarServiceResponse.LoadPlayer(pdata)) + AvatarServiceResponse(s"/$forChannel/Avatar", player_guid, AvatarServiceResponse.LoadPlayer(pdata)) ) case AvatarAction.ObjectDelete(player_guid, item_guid, unk) => AvatarEvents.publish( - AvatarServiceResponse("/Avatar/" + forChannel, player_guid, AvatarServiceResponse.ObjectDelete(item_guid, unk)) + AvatarServiceResponse(s"/$forChannel/Avatar", player_guid, AvatarServiceResponse.ObjectDelete(item_guid, unk)) ) case AvatarAction.ObjectHeld(player_guid, slot) => AvatarEvents.publish( - AvatarServiceResponse("/Avatar/" + forChannel, player_guid, AvatarServiceResponse.ObjectHeld(slot)) + AvatarServiceResponse(s"/$forChannel/Avatar", player_guid, AvatarServiceResponse.ObjectHeld(slot)) ) case AvatarAction.PlanetsideAttribute(guid, attribute_type, attribute_value) => AvatarEvents.publish( - AvatarServiceResponse("/Avatar/" + forChannel, guid, AvatarServiceResponse.PlanetSideAttribute(attribute_type, attribute_value)) + AvatarServiceResponse(s"/$forChannel/Avatar", guid, AvatarServiceResponse.PlanetSideAttribute(attribute_type, attribute_value)) ) case AvatarAction.PlayerState(guid, msg, spectator, weapon) => AvatarEvents.publish( - AvatarServiceResponse("/Avatar/" + forChannel, guid, AvatarServiceResponse.PlayerState(msg, spectator, weapon)) + AvatarServiceResponse(s"/$forChannel/Avatar", guid, AvatarServiceResponse.PlayerState(msg, spectator, weapon)) ) case AvatarAction.Reload(player_guid, mag) => AvatarEvents.publish( - AvatarServiceResponse("/Avatar/" + forChannel, player_guid, AvatarServiceResponse.Reload(mag)) + AvatarServiceResponse(s"/$forChannel/Avatar", player_guid, AvatarServiceResponse.Reload(mag)) ) case _ => ; } diff --git a/pslogin/src/main/scala/LocalService.scala b/pslogin/src/main/scala/LocalService.scala new file mode 100644 index 00000000..5337b074 --- /dev/null +++ b/pslogin/src/main/scala/LocalService.scala @@ -0,0 +1,58 @@ +// Copyright (c) 2017 PSForever +import akka.actor.Actor +import net.psforever.packet.game.PlanetSideGUID + +object LocalAction { + trait Action + + final case class Door(player_guid : PlanetSideGUID) extends Action +} + +object LocalServiceResponse { + trait Response + + final case class Door(player_guid : PlanetSideGUID) extends Response +} + +final case class LocalServiceMessage(forChannel : String, actionMessage : LocalAction.Action) + +final case class LocalServiceResponse(toChannel : String, avatar_guid : PlanetSideGUID, replyMessage : LocalServiceResponse.Response) extends GenericEventBusMsg + +/* + /LocalEnvironment/ + */ + +class LocalService extends Actor { + //import LocalService._ + private [this] val log = org.log4s.getLogger + + override def preStart = { + log.info("Starting...") + } + + val LocalEvents = new GenericEventBus[LocalServiceResponse] + + def receive = { + case Service.Join(channel) => + val path = s"/$channel/LocalEnvironment" + val who = sender() + log.info(s"$who has joined $path") + LocalEvents.subscribe(who, path) + case Service.Leave() => + LocalEvents.unsubscribe(sender()) + case Service.LeaveAll() => + LocalEvents.unsubscribe(sender()) + + case LocalServiceMessage(forChannel, action) => + action match { + case LocalAction.Door(player_guid) => + LocalEvents.publish( + LocalServiceResponse(s"/$forChannel/LocalEnvironment" + forChannel, player_guid, LocalServiceResponse.Door(player_guid)) + ) + case _ => ; + } + + case msg => + log.info(s"Unhandled message $msg from $sender") + } +} diff --git a/pslogin/src/main/scala/PsLogin.scala b/pslogin/src/main/scala/PsLogin.scala index d0c22dc4..59edb2f7 100644 --- a/pslogin/src/main/scala/PsLogin.scala +++ b/pslogin/src/main/scala/PsLogin.scala @@ -3,7 +3,7 @@ import java.net.InetAddress import java.io.File import java.util.Locale -import akka.actor.{ActorRef, ActorSystem, Props} +import akka.actor.{ActorContext, ActorRef, ActorSystem, Props} import akka.routing.RandomPool import ch.qos.logback.classic.LoggerContext import ch.qos.logback.classic.joran.JoranConfigurator @@ -202,6 +202,7 @@ object PsLogin { val serviceManager = ServiceManager.boot serviceManager ! ServiceManager.Register(RandomPool(50).props(Props[TaskResolver]), "taskResolver") serviceManager ! ServiceManager.Register(Props[AvatarService], "avatar") + serviceManager ! ServiceManager.Register(Props[LocalService], "local") serviceManager ! ServiceManager.Register(Props(classOf[InterstellarCluster], createContinents()), "galaxy") /** Create two actors for handling the login and world server endpoints */ @@ -222,20 +223,38 @@ object PsLogin { def createContinents() : List[Zone] = { val map13 = new ZoneMap("map13") { import net.psforever.objects.GlobalDefinitions._ - val ddef = new net.psforever.objects.doors.DoorDefinition(242) {} //generic door - LocalObject(DoorObjectBuilder(ddef, 330)) - LocalObject(DoorObjectBuilder(ddef, 332)) - LocalObject(DoorObjectBuilder(ddef, 372)) - LocalObject(DoorObjectBuilder(ddef, 373)) + LocalObject(DoorObjectBuilder(door, 330)) + LocalObject(DoorObjectBuilder(door, 332)) + LocalObject(DoorObjectBuilder(door, 372)) + LocalObject(DoorObjectBuilder(door, 373)) + LocalObject(IFFLockObjectBuilder(external_lock, 556)) + LocalObject(IFFLockObjectBuilder(external_lock, 558)) LocalObject(TerminalObjectBuilder(cert_terminal, 186)) LocalObject(TerminalObjectBuilder(cert_terminal, 187)) LocalObject(TerminalObjectBuilder(cert_terminal, 188)) LocalObject(TerminalObjectBuilder(order_terminal, 853)) LocalObject(TerminalObjectBuilder(order_terminal, 855)) LocalObject(TerminalObjectBuilder(order_terminal, 860)) + + LocalBases = 30 + + ObjectToBase(330, 29) + ObjectToBase(332, 29) + ObjectToBase(556, 29) + ObjectToBase(558, 29) + DoorToLock(330, 558) + DoorToLock(332, 556) + } + val home3 = new Zone("home3", map13, 13) { + override def Init(implicit context : ActorContext) : Unit = { + super.Init(context) + + import net.psforever.types.PlanetSideEmpire + Base(2).get.Faction = PlanetSideEmpire.VS //HART building C + Base(29).get.Faction = PlanetSideEmpire.NC //South Villa Gun Tower + } } - val home3 = Zone("home3", map13, 13) home3 :: Nil diff --git a/pslogin/src/main/scala/Service.scala b/pslogin/src/main/scala/Service.scala new file mode 100644 index 00000000..37a9c87c --- /dev/null +++ b/pslogin/src/main/scala/Service.scala @@ -0,0 +1,29 @@ +// Copyright (c) 2017 PSForever +import akka.event.{ActorEventBus, SubchannelClassification} +import akka.util.Subclassification + +object Service { + final case class Join(channel : String) + final case class Leave() + final case class LeaveAll() +} + +trait GenericEventBusMsg { + def toChannel : String +} + +class GenericEventBus[A <: GenericEventBusMsg] extends ActorEventBus with SubchannelClassification { + type Event = A + type Classifier = String + + protected def classify(event: Event): Classifier = event.toChannel + + protected def subclassification = new Subclassification[Classifier] { + def isEqual(x: Classifier, y: Classifier) = x == y + def isSubclass(x: Classifier, y: Classifier) = x.startsWith(y) + } + + protected def publish(event: Event, subscriber: Subscriber): Unit = { + subscriber ! event + } +} diff --git a/pslogin/src/main/scala/WorldSessionActor.scala b/pslogin/src/main/scala/WorldSessionActor.scala index 719c1cd1..76ec1a7b 100644 --- a/pslogin/src/main/scala/WorldSessionActor.scala +++ b/pslogin/src/main/scala/WorldSessionActor.scala @@ -11,7 +11,7 @@ import org.log4s.MDC import MDCContextAware.Implicits._ import ServiceManager.Lookup import net.psforever.objects._ -import net.psforever.objects.doors.Door +import net.psforever.objects.doors.{Door, IFFLock} import net.psforever.objects.zones.{InterstellarCluster, Zone} import net.psforever.objects.entity.IdentifiableEntity import net.psforever.objects.equipment._ @@ -32,9 +32,10 @@ class WorldSessionActor extends Actor with MDCContextAware { var sessionId : Long = 0 var leftRef : ActorRef = ActorRef.noSender var rightRef : ActorRef = ActorRef.noSender - var avatarService = Actor.noSender - var taskResolver = Actor.noSender - var galaxy = Actor.noSender + var avatarService : ActorRef = ActorRef.noSender + var localService : ActorRef = ActorRef.noSender + var taskResolver : ActorRef = Actor.noSender + var galaxy : ActorRef = Actor.noSender var continent : Zone = null var clientKeepAlive : Cancellable = WorldSessionActor.DefaultCancellable @@ -43,7 +44,7 @@ class WorldSessionActor extends Actor with MDCContextAware { if(clientKeepAlive != null) clientKeepAlive.cancel() - avatarService ! Leave() + avatarService ! Service.Leave() LivePlayerList.Remove(sessionId) match { case Some(tplayer) => if(tplayer.HasGUID) { @@ -70,6 +71,7 @@ class WorldSessionActor extends Actor with MDCContextAware { } context.become(Started) ServiceManager.serviceManager ! Lookup("avatar") + ServiceManager.serviceManager ! Lookup("local") ServiceManager.serviceManager ! Lookup("taskResolver") ServiceManager.serviceManager ! Lookup("galaxy") @@ -82,6 +84,9 @@ class WorldSessionActor extends Actor with MDCContextAware { case ServiceManager.LookupResult("avatar", endpoint) => avatarService = endpoint log.info("ID: " + sessionId + " Got avatar service " + endpoint) + case ServiceManager.LookupResult("local", endpoint) => + localService = endpoint + log.info("ID: " + sessionId + " Got local service " + endpoint) case ServiceManager.LookupResult("taskResolver", endpoint) => taskResolver = endpoint log.info("ID: " + sessionId + " Got task resolver service " + endpoint) @@ -724,7 +729,7 @@ class WorldSessionActor extends Actor with MDCContextAware { ) }) - avatarService ! Join(player.Continent) + avatarService ! Service.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) => @@ -957,8 +962,23 @@ class WorldSessionActor extends Actor with MDCContextAware { // TODO: Not all incoming UseItemMessage's respond with another UseItemMessage (i.e. doors only send out GenericObjectStateMsg) continent.GUID(object_guid) match { case Some(door : Door) => - log.info("Door action!") - door.Actor ! Door.Request(player, msg) + continent.Map.DoorToLock.get(object_guid.guid) match { //check for IFFLock + case Some(lock_guid) => + val lock_hacked = continent.GUID(lock_guid).get.asInstanceOf[IFFLock].Hacker.contains(player.Faction) + continent.Map.ObjectToBase.get(lock_guid) match { //check for associated base + case Some(base_id) => + if(continent.Base(base_id).get.Faction == player.Faction || lock_hacked) { //either base allegiance aligns or lock is hacked + door.Actor ! Door.Use(player, msg) + } + case None => + if(lock_hacked) { //is lock hacked? + door.Actor ! Door.Use(player, msg) + } + } + case None => + door.Actor ! Door.Use(player, msg) //let door open freely + } + case Some(obj : PlanetSideGameObject) => if(itemType != 121) { sendResponse(PacketCoding.CreateGamePacket(0, UseItemMessage(avatar_guid, unk1, object_guid, unk2, unk3, unk4, unk5, unk6, unk7, unk8, itemType))) From 249eb96cc5eb24f0ad6872b130a38daf10be1d39 Mon Sep 17 00:00:00 2001 From: FateJH Date: Tue, 3 Oct 2017 20:32:21 -0400 Subject: [PATCH 11/23] hack testing; RepairMessage is suitable for a progress bar for now but it needs replacing with the correct operation and the last part feels wrong --- .../objects/doors/DoorCloseControl.scala | 45 +++++++++++++++---- .../src/main/scala/WorldSessionActor.scala | 35 +++++++++++++++ 2 files changed, 72 insertions(+), 8 deletions(-) diff --git a/common/src/main/scala/net/psforever/objects/doors/DoorCloseControl.scala b/common/src/main/scala/net/psforever/objects/doors/DoorCloseControl.scala index ae20b6e7..3b845804 100644 --- a/common/src/main/scala/net/psforever/objects/doors/DoorCloseControl.scala +++ b/common/src/main/scala/net/psforever/objects/doors/DoorCloseControl.scala @@ -3,29 +3,58 @@ package net.psforever.objects.doors import akka.actor.{Actor, ActorRef, Cancellable} import net.psforever.packet.game.PlanetSideGUID +import scala.concurrent.duration._ -import scala.collection.mutable.ListBuffer +import scala.annotation.tailrec class DoorCloseControl(implicit val environment : ActorRef) extends Actor { import DoorCloseControl._ - private var doorCloser : Cancellable = DefaultCloser + private var doorCloserTrigger : Cancellable = DefaultCloser private var openDoors : List[DoorEntry] = Nil def receive : Receive = { case DoorIsOpen(guid, time) => - if(openDoors.isEmpty) { - //doorCloser = context.system.scheduler.scheduleOnce(timeout, environment, Door.DoorMessage()) + openDoors = openDoors :+ DoorEntry(guid, time) + if(doorCloserTrigger.isCancelled) { + import scala.concurrent.ExecutionContext.Implicits.global + doorCloserTrigger = context.system.scheduler.scheduleOnce(timeout, self, DoorCloseControl.CloseTheDoor()) } - else { - openDoors = openDoors :+ DoorEntry(guid, time) + + case CloseTheDoor() => + doorCloserTrigger.cancel + val now : Long = System.nanoTime + recursiveCloseDoors(openDoors.iterator, now) match { + case entry :: rest => + openDoors = entry :: rest + import scala.concurrent.ExecutionContext.Implicits.global + doorCloserTrigger = context.system.scheduler.scheduleOnce((now - entry.opened_at_time + timeout_time)*1000 milliseconds, self, DoorCloseControl.CloseTheDoor()) + case Nil => + openDoors = Nil } case _ => ; } + + @tailrec private def recursiveCloseDoors(iter : Iterator[DoorEntry], now : Long) : List[DoorEntry] = { + if(!iter.hasNext) { + Nil + } + else { + val entry = iter.next + if(now - entry.opened_at_time < timeout_time) { + entry +: iter.toList + } + else { + //TODO close this door entry + recursiveCloseDoors(iter, now) + } + } + } } object DoorCloseControl { - private final val timeout : Long = 5000L + private final val timeout_time = 5000 + private final val timeout : FiniteDuration = timeout_time milliseconds private final val DefaultCloser : Cancellable = new Cancellable() { override def cancel : Boolean = true @@ -36,5 +65,5 @@ object DoorCloseControl { final case class DoorIsOpen(door_guid : PlanetSideGUID, opened_at_time : Long = System.nanoTime()) - final case class CloseTheDoor(door_guid : PlanetSideGUID) + private final case class CloseTheDoor() } diff --git a/pslogin/src/main/scala/WorldSessionActor.scala b/pslogin/src/main/scala/WorldSessionActor.scala index 76ec1a7b..9242b8fd 100644 --- a/pslogin/src/main/scala/WorldSessionActor.scala +++ b/pslogin/src/main/scala/WorldSessionActor.scala @@ -37,8 +37,10 @@ class WorldSessionActor extends Actor with MDCContextAware { var taskResolver : ActorRef = Actor.noSender var galaxy : ActorRef = Actor.noSender var continent : Zone = null + var progressBarValue : Option[Float] = None var clientKeepAlive : Cancellable = WorldSessionActor.DefaultCancellable + var progressBarUpdate : Cancellable = WorldSessionActor.DefaultCancellable override def postStop() = { if(clientKeepAlive != null) @@ -547,6 +549,25 @@ class WorldSessionActor extends Actor with MDCContextAware { continent.Actor ! Zone.DropItemOnGround(item, item.Position, item.Orientation) //restore } + case ItemHacking(tplayer, target, tool_guid, delta) => + progressBarUpdate.cancel + if(progressBarValue.isDefined) { + val progressBarVal : Float = progressBarValue.get + delta + sendResponse(PacketCoding.CreateGamePacket(0, RepairMessage(target.GUID, progressBarVal.toInt))) + if(progressBarVal > 100) { + progressBarValue = None + log.info(s"We've hacked the item $target! Now what?") + sendResponse(PacketCoding.CreateGamePacket(0, ChangeFireStateMessage_Stop(tool_guid))) + //TODO now what? + } + else { + progressBarValue = Some(progressBarVal) + import scala.concurrent.duration._ + import scala.concurrent.ExecutionContext.Implicits.global + progressBarUpdate = context.system.scheduler.scheduleOnce(250 milliseconds, self, ItemHacking(tplayer, target, tool_guid, delta)) + } + } + case ResponseToSelf(pkt) => log.info(s"Received a direct message: $pkt") sendResponse(pkt) @@ -791,6 +812,7 @@ class WorldSessionActor extends Actor with MDCContextAware { case msg @ ChangeFireStateMessage_Stop(item_guid) => log.info("ChangeFireState_Stop: " + msg) + progressBarUpdate.cancel case msg @ EmoteMsg(avatar_guid, emote) => log.info("Emote: " + msg) @@ -979,6 +1001,18 @@ class WorldSessionActor extends Actor with MDCContextAware { door.Actor ! Door.Use(player, msg) //let door open freely } + case Some(panel : IFFLock) => + player.Slot(player.DrawnSlot).Equipment match { + case Some(tool : SimpleItem) => + if(tool.Definition == GlobalDefinitions.remote_electronics_kit) { + progressBarValue = Some(-2.66f) + self ! WorldSessionActor.ItemHacking(player, panel, tool.GUID, 2.66f) + } + case _ => ; + } + log.info("Hacking a door~") + //TODO get player hack level (for now, presume 15s in internals of 4/s) + case Some(obj : PlanetSideGameObject) => if(itemType != 121) { sendResponse(PacketCoding.CreateGamePacket(0, UseItemMessage(avatar_guid, unk1, object_guid, unk2, unk3, unk4, unk5, unk6, unk7, unk8, itemType))) @@ -1569,6 +1603,7 @@ object WorldSessionActor { private final case class PlayerFailedToLoad(tplayer : Player) private final case class ListAccountCharacters() private final case class SetCurrentAvatar(tplayer : Player) + private final case class ItemHacking(tplayer : Player, target : PlanetSideGameObject, tool_guid : PlanetSideGUID, delta : Float) /** * A placeholder `Cancellable` object. From 3dd2d721174bfce624e11fde0d371f79b5767eb0 Mon Sep 17 00:00:00 2001 From: FateJH Date: Sat, 7 Oct 2017 16:37:36 -0400 Subject: [PATCH 12/23] initial ActionCancelMessage packet work and tests; borrowed aphedox hack message to pry at door --- .../psforever/objects/GlobalDefinitions.scala | 5 ---- .../psforever/packet/GamePacketOpcode.scala | 2 +- .../packet/game/ActionCancelMessage.scala | 29 +++++++++++++++++++ .../scala/game/ActionCancelMessageTest.scala | 29 +++++++++++++++++++ .../src/main/scala/WorldSessionActor.scala | 15 ++++++++-- 5 files changed, 72 insertions(+), 8 deletions(-) create mode 100644 common/src/main/scala/net/psforever/packet/game/ActionCancelMessage.scala create mode 100644 common/src/test/scala/game/ActionCancelMessageTest.scala diff --git a/common/src/main/scala/net/psforever/objects/GlobalDefinitions.scala b/common/src/main/scala/net/psforever/objects/GlobalDefinitions.scala index 1326f447..bd91559a 100644 --- a/common/src/main/scala/net/psforever/objects/GlobalDefinitions.scala +++ b/common/src/main/scala/net/psforever/objects/GlobalDefinitions.scala @@ -7,12 +7,7 @@ import net.psforever.objects.doors.{DoorDefinition, IFFLockDefinition} import net.psforever.objects.equipment.CItem.DeployedItem import net.psforever.objects.equipment._ import net.psforever.objects.inventory.InventoryTile -<<<<<<< c5ae9e477ccade5759eac2e9526ba898d0e8f16b import net.psforever.objects.terminals.{CertTerminalDefinition, OrderTerminalDefinition} -import net.psforever.packet.game.objectcreate.ObjectClass -======= -import net.psforever.objects.terminals.OrderTerminalDefinition ->>>>>>> automated doors, IFF locks, and bases thus that only permissible doors can be opened by players of correct faction alignment; Base is just a prototype example, hastily created for this functionality; LocalService will eventually be used for doors messages (and other things) import net.psforever.types.PlanetSideEmpire object GlobalDefinitions { diff --git a/common/src/main/scala/net/psforever/packet/GamePacketOpcode.scala b/common/src/main/scala/net/psforever/packet/GamePacketOpcode.scala index f4ef9765..bec75305 100644 --- a/common/src/main/scala/net/psforever/packet/GamePacketOpcode.scala +++ b/common/src/main/scala/net/psforever/packet/GamePacketOpcode.scala @@ -359,7 +359,7 @@ object GamePacketOpcode extends Enumeration { // OPCODES 0x20-2f case 0x20 => noDecoder(UnknownMessage32) case 0x21 => game.ActionProgressMessage.decode - case 0x22 => noDecoder(ActionCancelMessage) + case 0x22 => game.ActionCancelMessage.decode case 0x23 => noDecoder(ActionCancelAcknowledgeMessage) case 0x24 => game.SetEmpireMessage.decode case 0x25 => game.EmoteMsg.decode diff --git a/common/src/main/scala/net/psforever/packet/game/ActionCancelMessage.scala b/common/src/main/scala/net/psforever/packet/game/ActionCancelMessage.scala new file mode 100644 index 00000000..127e2b60 --- /dev/null +++ b/common/src/main/scala/net/psforever/packet/game/ActionCancelMessage.scala @@ -0,0 +1,29 @@ +// Copyright (c) 2017 PSForever +package net.psforever.packet.game + +import net.psforever.packet.{GamePacketOpcode, Marshallable, PlanetSideGamePacket} +import scodec.Codec +import scodec.codecs._ + +/** + * na + * @param player_guid na + * @param object_guid na + * @param unk na + */ +final case class ActionCancelMessage(player_guid : PlanetSideGUID, + object_guid : PlanetSideGUID, + unk : Int) + extends PlanetSideGamePacket { + type Packet = ActionCancelMessage + def opcode = GamePacketOpcode.ActionCancelMessage + def encode = ActionCancelMessage.encode(this) +} + +object ActionCancelMessage extends Marshallable[ActionCancelMessage] { + implicit val codec : Codec[ActionCancelMessage] = ( + ("player_guid" | PlanetSideGUID.codec) :: + ("object_guid" | PlanetSideGUID.codec) :: + ("unk" | uint4L) + ).as[ActionCancelMessage] +} diff --git a/common/src/test/scala/game/ActionCancelMessageTest.scala b/common/src/test/scala/game/ActionCancelMessageTest.scala new file mode 100644 index 00000000..2c8ee068 --- /dev/null +++ b/common/src/test/scala/game/ActionCancelMessageTest.scala @@ -0,0 +1,29 @@ +// Copyright (c) 2017 PSForever +package game + +import org.specs2.mutable._ +import net.psforever.packet._ +import net.psforever.packet.game._ +import scodec.bits._ + +class ActionCancelMessageTest extends Specification { + val string = hex"22 201ee01a10" + + "decode" in { + PacketCoding.DecodePacket(string).require match { + case ActionCancelMessage(player_guid, object_guid, unk) => + player_guid mustEqual PlanetSideGUID(7712) + object_guid mustEqual PlanetSideGUID(6880) + unk mustEqual 1 + case _ => + ko + } + } + + "encode" in { + val msg = ActionCancelMessage(PlanetSideGUID(7712), PlanetSideGUID(6880), 1) + val pkt = PacketCoding.EncodePacket(msg).require.toByteVector + + pkt mustEqual string + } +} diff --git a/pslogin/src/main/scala/WorldSessionActor.scala b/pslogin/src/main/scala/WorldSessionActor.scala index 9242b8fd..8d634f71 100644 --- a/pslogin/src/main/scala/WorldSessionActor.scala +++ b/pslogin/src/main/scala/WorldSessionActor.scala @@ -553,11 +553,19 @@ class WorldSessionActor extends Actor with MDCContextAware { progressBarUpdate.cancel if(progressBarValue.isDefined) { val progressBarVal : Float = progressBarValue.get + delta - sendResponse(PacketCoding.CreateGamePacket(0, RepairMessage(target.GUID, progressBarVal.toInt))) + val vis = if(progressBarVal == 0L) { + 1 + } + else if(progressBarVal >= 100L) { + 4 + } + else { + 3 + } + sendResponse(PacketCoding.CreateGamePacket(0, HackMessage(1, target.GUID.guid, player.GUID.guid, progressBarVal.toInt, 0L, vis, 8L))) if(progressBarVal > 100) { progressBarValue = None log.info(s"We've hacked the item $target! Now what?") - sendResponse(PacketCoding.CreateGamePacket(0, ChangeFireStateMessage_Stop(tool_guid))) //TODO now what? } else { @@ -1132,6 +1140,9 @@ class WorldSessionActor extends Actor with MDCContextAware { case msg @ TargetingImplantRequest(list) => log.info("TargetingImplantRequest: "+msg) + case msg @ ActionCancelMessage(u1, u2, u3) => + log.info("Cancelled: "+msg) + case default => log.error(s"Unhandled GamePacket $pkt") } From 1c41972d69581df185d0b41969bb8e28d49b446d Mon Sep 17 00:00:00 2001 From: FateJH Date: Mon, 9 Oct 2017 20:35:13 -0400 Subject: [PATCH 13/23] moved terminals, doors, and locks, into their own package; iff panels hack and doors open --- .../psforever/objects/GlobalDefinitions.scala | 5 +- .../net/psforever/objects/doors/IFFLock.scala | 28 ---------- .../objects/serverobject/CommonMessages.scala | 11 ++++ .../serverobject/PlanetSideServerObject.scala | 18 +++++++ .../builders}/DoorObjectBuilder.scala | 7 +-- .../builders}/IFFLockObjectBuilder.scala | 6 ++- .../builders}/ServerObjectBuilder.scala | 2 +- .../builders}/TerminalObjectBuilder.scala | 7 +-- .../{ => serverobject}/doors/Base.scala | 2 +- .../{ => serverobject}/doors/Door.scala | 18 ++----- .../doors/DoorControl.scala | 5 +- .../doors/DoorDefinition.scala | 2 +- .../objects/serverobject/locks/IFFLock.scala | 39 ++++++++++++++ .../serverobject/locks/IFFLockControl.scala | 24 +++++++++ .../locks}/IFFLockDefinition.scala | 2 +- .../terminals/CertTerminalDefinition.scala | 2 +- .../terminals/OrderTerminalDefinition.scala | 2 +- .../terminals/TemporaryTerminalMessages.scala | 7 ++- .../terminals/Terminal.scala | 54 +++++++++---------- .../terminals/TerminalControl.scala | 16 ++---- .../terminals/TerminalDefinition.scala | 2 +- .../net/psforever/objects/zones/Zone.scala | 18 ++++++- .../psforever/objects/zones/ZoneActor.scala | 9 ++-- .../ZoneDoorActor.scala} | 22 ++++---- .../net/psforever/objects/zones/ZoneMap.scala | 2 + pslogin/src/main/scala/AvatarService.scala | 7 +++ pslogin/src/main/scala/PsLogin.scala | 1 + .../src/main/scala/WorldSessionActor.scala | 27 +++++++--- 28 files changed, 219 insertions(+), 126 deletions(-) delete mode 100644 common/src/main/scala/net/psforever/objects/doors/IFFLock.scala create mode 100644 common/src/main/scala/net/psforever/objects/serverobject/CommonMessages.scala create mode 100644 common/src/main/scala/net/psforever/objects/serverobject/PlanetSideServerObject.scala rename common/src/main/scala/net/psforever/objects/{zones => serverobject/builders}/DoorObjectBuilder.scala (79%) rename common/src/main/scala/net/psforever/objects/{zones => serverobject/builders}/IFFLockObjectBuilder.scala (79%) rename common/src/main/scala/net/psforever/objects/{zones => serverobject/builders}/ServerObjectBuilder.scala (95%) rename common/src/main/scala/net/psforever/objects/{zones => serverobject/builders}/TerminalObjectBuilder.scala (79%) rename common/src/main/scala/net/psforever/objects/{ => serverobject}/doors/Base.scala (88%) rename common/src/main/scala/net/psforever/objects/{ => serverobject}/doors/Door.scala (72%) rename common/src/main/scala/net/psforever/objects/{ => serverobject}/doors/DoorControl.scala (75%) rename common/src/main/scala/net/psforever/objects/{ => serverobject}/doors/DoorDefinition.scala (83%) create mode 100644 common/src/main/scala/net/psforever/objects/serverobject/locks/IFFLock.scala create mode 100644 common/src/main/scala/net/psforever/objects/serverobject/locks/IFFLockControl.scala rename common/src/main/scala/net/psforever/objects/{doors => serverobject/locks}/IFFLockDefinition.scala (77%) rename common/src/main/scala/net/psforever/objects/{ => serverobject}/terminals/CertTerminalDefinition.scala (98%) rename common/src/main/scala/net/psforever/objects/{ => serverobject}/terminals/OrderTerminalDefinition.scala (99%) rename common/src/main/scala/net/psforever/objects/{ => serverobject}/terminals/TemporaryTerminalMessages.scala (63%) rename common/src/main/scala/net/psforever/objects/{ => serverobject}/terminals/Terminal.scala (80%) rename common/src/main/scala/net/psforever/objects/{ => serverobject}/terminals/TerminalControl.scala (63%) rename common/src/main/scala/net/psforever/objects/{ => serverobject}/terminals/TerminalDefinition.scala (99%) rename common/src/main/scala/net/psforever/objects/{doors/DoorCloseControl.scala => zones/ZoneDoorActor.scala} (84%) diff --git a/common/src/main/scala/net/psforever/objects/GlobalDefinitions.scala b/common/src/main/scala/net/psforever/objects/GlobalDefinitions.scala index bd91559a..13073dad 100644 --- a/common/src/main/scala/net/psforever/objects/GlobalDefinitions.scala +++ b/common/src/main/scala/net/psforever/objects/GlobalDefinitions.scala @@ -3,11 +3,12 @@ package net.psforever.objects import net.psforever.objects.definition._ import net.psforever.objects.definition.converter.{CommandDetonaterConverter, LockerContainerConverter, REKConverter} -import net.psforever.objects.doors.{DoorDefinition, IFFLockDefinition} +import net.psforever.objects.serverobject.doors.DoorDefinition import net.psforever.objects.equipment.CItem.DeployedItem import net.psforever.objects.equipment._ import net.psforever.objects.inventory.InventoryTile -import net.psforever.objects.terminals.{CertTerminalDefinition, OrderTerminalDefinition} +import net.psforever.objects.serverobject.locks.IFFLockDefinition +import net.psforever.objects.serverobject.terminals.{CertTerminalDefinition, OrderTerminalDefinition} import net.psforever.types.PlanetSideEmpire object GlobalDefinitions { diff --git a/common/src/main/scala/net/psforever/objects/doors/IFFLock.scala b/common/src/main/scala/net/psforever/objects/doors/IFFLock.scala deleted file mode 100644 index 81554a0b..00000000 --- a/common/src/main/scala/net/psforever/objects/doors/IFFLock.scala +++ /dev/null @@ -1,28 +0,0 @@ -// Copyright (c) 2017 PSForever -package net.psforever.objects.doors - -import net.psforever.objects.{GlobalDefinitions, PlanetSideGameObject} -import net.psforever.types.PlanetSideEmpire - -class IFFLock extends PlanetSideGameObject { - private var hackedBy : Option[PlanetSideEmpire.Value] = None - - def Hacker : Option[PlanetSideEmpire.Value] = hackedBy - - def Hacker_=(hacker : PlanetSideEmpire.Value) : Option[PlanetSideEmpire.Value] = { - Hacker = Some(hacker) - } - - def Hacker_=(hacker : Option[PlanetSideEmpire.Value]) : Option[PlanetSideEmpire.Value] = { - hackedBy = hacker - Hacker - } - - def Definition : IFFLockDefinition = GlobalDefinitions.external_lock -} - -object IFFLock { - def apply() : IFFLock = { - new IFFLock - } -} diff --git a/common/src/main/scala/net/psforever/objects/serverobject/CommonMessages.scala b/common/src/main/scala/net/psforever/objects/serverobject/CommonMessages.scala new file mode 100644 index 00000000..8f4de61c --- /dev/null +++ b/common/src/main/scala/net/psforever/objects/serverobject/CommonMessages.scala @@ -0,0 +1,11 @@ +// Copyright (c) 2017 PSForever +package net.psforever.objects.serverobject + +import net.psforever.objects.Player +import net.psforever.types.PlanetSideEmpire + +//temporary location for these temporary messages +object CommonMessages { + final case class Hack(player : Player) + final case class ClearHack() +} diff --git a/common/src/main/scala/net/psforever/objects/serverobject/PlanetSideServerObject.scala b/common/src/main/scala/net/psforever/objects/serverobject/PlanetSideServerObject.scala new file mode 100644 index 00000000..3a0e71ec --- /dev/null +++ b/common/src/main/scala/net/psforever/objects/serverobject/PlanetSideServerObject.scala @@ -0,0 +1,18 @@ +// Copyright (c) 2017 PSForever +package net.psforever.objects.serverobject + +import akka.actor.ActorRef +import net.psforever.objects.PlanetSideGameObject + +abstract class PlanetSideServerObject extends PlanetSideGameObject { + private var actor = ActorRef.noSender + + def Actor : ActorRef = actor + + def Actor_=(control : ActorRef) : ActorRef = { + if(actor == ActorRef.noSender) { + actor = control + } + actor + } +} diff --git a/common/src/main/scala/net/psforever/objects/zones/DoorObjectBuilder.scala b/common/src/main/scala/net/psforever/objects/serverobject/builders/DoorObjectBuilder.scala similarity index 79% rename from common/src/main/scala/net/psforever/objects/zones/DoorObjectBuilder.scala rename to common/src/main/scala/net/psforever/objects/serverobject/builders/DoorObjectBuilder.scala index 8f4ead88..d40ff826 100644 --- a/common/src/main/scala/net/psforever/objects/zones/DoorObjectBuilder.scala +++ b/common/src/main/scala/net/psforever/objects/serverobject/builders/DoorObjectBuilder.scala @@ -1,7 +1,8 @@ // Copyright (c) 2017 PSForever -package net.psforever.objects.zones +package net.psforever.objects.serverobject.builders -import net.psforever.objects.doors.{Door, DoorDefinition} +import akka.actor.Props +import net.psforever.objects.serverobject.doors.{Door, DoorControl, DoorDefinition} /** * Wrapper `Class` designed to instantiate a `Door` server object. @@ -15,7 +16,7 @@ class DoorObjectBuilder(private val ddef : DoorDefinition, private val id : Int) def Build(implicit context : ActorContext, guid : NumberPoolHub) : Door = { val obj = Door(ddef) guid.register(obj, id) //non-Actor GUID registration - obj.Actor //it's necessary to register beforehand because the Actor name utilizes the GUID + obj.Actor = context.actorOf(Props(classOf[DoorControl], obj), s"${ddef.Name}_${obj.GUID.guid}") obj } } diff --git a/common/src/main/scala/net/psforever/objects/zones/IFFLockObjectBuilder.scala b/common/src/main/scala/net/psforever/objects/serverobject/builders/IFFLockObjectBuilder.scala similarity index 79% rename from common/src/main/scala/net/psforever/objects/zones/IFFLockObjectBuilder.scala rename to common/src/main/scala/net/psforever/objects/serverobject/builders/IFFLockObjectBuilder.scala index e4e55011..e954eea4 100644 --- a/common/src/main/scala/net/psforever/objects/zones/IFFLockObjectBuilder.scala +++ b/common/src/main/scala/net/psforever/objects/serverobject/builders/IFFLockObjectBuilder.scala @@ -1,7 +1,8 @@ // Copyright (c) 2017 PSForever -package net.psforever.objects.zones +package net.psforever.objects.serverobject.builders -import net.psforever.objects.doors.{IFFLock, IFFLockDefinition} +import akka.actor.Props +import net.psforever.objects.serverobject.locks.{IFFLock, IFFLockControl, IFFLockDefinition} /** * Wrapper `Class` designed to instantiate a `Door` server object. @@ -15,6 +16,7 @@ class IFFLockObjectBuilder(private val idef : IFFLockDefinition, private val id def Build(implicit context : ActorContext, guid : NumberPoolHub) : IFFLock = { val obj = IFFLock() guid.register(obj, id) //non-Actor GUID registration + obj.Actor = context.actorOf(Props(classOf[IFFLockControl], obj), s"${idef.Name}_${obj.GUID.guid}") obj } } diff --git a/common/src/main/scala/net/psforever/objects/zones/ServerObjectBuilder.scala b/common/src/main/scala/net/psforever/objects/serverobject/builders/ServerObjectBuilder.scala similarity index 95% rename from common/src/main/scala/net/psforever/objects/zones/ServerObjectBuilder.scala rename to common/src/main/scala/net/psforever/objects/serverobject/builders/ServerObjectBuilder.scala index 391ec775..5b412814 100644 --- a/common/src/main/scala/net/psforever/objects/zones/ServerObjectBuilder.scala +++ b/common/src/main/scala/net/psforever/objects/serverobject/builders/ServerObjectBuilder.scala @@ -1,5 +1,5 @@ // Copyright (c) 2017 PSForever -package net.psforever.objects.zones +package net.psforever.objects.serverobject.builders import akka.actor.ActorContext import net.psforever.objects.PlanetSideGameObject diff --git a/common/src/main/scala/net/psforever/objects/zones/TerminalObjectBuilder.scala b/common/src/main/scala/net/psforever/objects/serverobject/builders/TerminalObjectBuilder.scala similarity index 79% rename from common/src/main/scala/net/psforever/objects/zones/TerminalObjectBuilder.scala rename to common/src/main/scala/net/psforever/objects/serverobject/builders/TerminalObjectBuilder.scala index b78921e1..d01022d5 100644 --- a/common/src/main/scala/net/psforever/objects/zones/TerminalObjectBuilder.scala +++ b/common/src/main/scala/net/psforever/objects/serverobject/builders/TerminalObjectBuilder.scala @@ -1,7 +1,8 @@ // Copyright (c) 2017 PSForever -package net.psforever.objects.zones +package net.psforever.objects.serverobject.builders -import net.psforever.objects.terminals.{Terminal, TerminalDefinition} +import akka.actor.Props +import net.psforever.objects.serverobject.terminals.{Terminal, TerminalControl, TerminalDefinition} /** * Wrapper `Class` designed to instantiate a `Terminal` server object. @@ -15,7 +16,7 @@ class TerminalObjectBuilder(private val tdef : TerminalDefinition, private val i 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.Actor = context.actorOf(Props(classOf[TerminalControl], obj), s"${tdef.Name}_${obj.GUID.guid}") obj } } diff --git a/common/src/main/scala/net/psforever/objects/doors/Base.scala b/common/src/main/scala/net/psforever/objects/serverobject/doors/Base.scala similarity index 88% rename from common/src/main/scala/net/psforever/objects/doors/Base.scala rename to common/src/main/scala/net/psforever/objects/serverobject/doors/Base.scala index 9fa8b3d9..17c590a7 100644 --- a/common/src/main/scala/net/psforever/objects/doors/Base.scala +++ b/common/src/main/scala/net/psforever/objects/serverobject/doors/Base.scala @@ -1,5 +1,5 @@ // Copyright (c) 2017 PSForever -package net.psforever.objects.doors +package net.psforever.objects.serverobject.doors import net.psforever.types.PlanetSideEmpire diff --git a/common/src/main/scala/net/psforever/objects/doors/Door.scala b/common/src/main/scala/net/psforever/objects/serverobject/doors/Door.scala similarity index 72% rename from common/src/main/scala/net/psforever/objects/doors/Door.scala rename to common/src/main/scala/net/psforever/objects/serverobject/doors/Door.scala index 179256f9..25c50e85 100644 --- a/common/src/main/scala/net/psforever/objects/doors/Door.scala +++ b/common/src/main/scala/net/psforever/objects/serverobject/doors/Door.scala @@ -1,25 +1,15 @@ // Copyright (c) 2017 PSForever -package net.psforever.objects.doors +package net.psforever.objects.serverobject.doors -import akka.actor.{ActorContext, ActorRef, Props} -import net.psforever.objects.{PlanetSideGameObject, Player} +import net.psforever.objects.serverobject.PlanetSideServerObject +import net.psforever.objects.Player import net.psforever.packet.game.UseItemMessage /** * na * @param ddef the `ObjectDefinition` that constructs this object and maintains some of its immutable fields */ -class Door(ddef : DoorDefinition) extends PlanetSideGameObject { - /** Internal reference to the `Actor` for this `Door`, sets up by this `Door`. */ - private var actor = ActorRef.noSender - - def Actor(implicit context : ActorContext) : ActorRef = { - if(actor == ActorRef.noSender) { - actor = context.actorOf(Props(classOf[DoorControl], this), s"${ddef.Name}_${GUID.guid}") - } - actor - } - +class Door(ddef : DoorDefinition) extends PlanetSideServerObject { private var openState : Boolean = false private var lockState : Boolean = false diff --git a/common/src/main/scala/net/psforever/objects/doors/DoorControl.scala b/common/src/main/scala/net/psforever/objects/serverobject/doors/DoorControl.scala similarity index 75% rename from common/src/main/scala/net/psforever/objects/doors/DoorControl.scala rename to common/src/main/scala/net/psforever/objects/serverobject/doors/DoorControl.scala index e98fb9b7..15875291 100644 --- a/common/src/main/scala/net/psforever/objects/doors/DoorControl.scala +++ b/common/src/main/scala/net/psforever/objects/serverobject/doors/DoorControl.scala @@ -1,5 +1,5 @@ // Copyright (c) 2017 PSForever -package net.psforever.objects.doors +package net.psforever.objects.serverobject.doors import akka.actor.{Actor, Cancellable} @@ -8,12 +8,9 @@ import akka.actor.{Actor, Cancellable} * @param door the `Door` object being governed */ class DoorControl(door : Door) extends Actor { - private var doorCloser : Cancellable = DoorControl.DefaultCloser - def receive : Receive = { case Door.Use(player, msg) => sender ! Door.DoorMessage(player, msg, door.Use(player, msg)) - //doorCloser = context.system.scheduler.scheduleOnce(5000L, sender, Door.DoorMessage()) case _ => sender ! Door.NoEvent() } diff --git a/common/src/main/scala/net/psforever/objects/doors/DoorDefinition.scala b/common/src/main/scala/net/psforever/objects/serverobject/doors/DoorDefinition.scala similarity index 83% rename from common/src/main/scala/net/psforever/objects/doors/DoorDefinition.scala rename to common/src/main/scala/net/psforever/objects/serverobject/doors/DoorDefinition.scala index d18c50c4..959aa6d2 100644 --- a/common/src/main/scala/net/psforever/objects/doors/DoorDefinition.scala +++ b/common/src/main/scala/net/psforever/objects/serverobject/doors/DoorDefinition.scala @@ -1,5 +1,5 @@ // Copyright (c) 2017 PSForever -package net.psforever.objects.doors +package net.psforever.objects.serverobject.doors import net.psforever.objects.definition.ObjectDefinition diff --git a/common/src/main/scala/net/psforever/objects/serverobject/locks/IFFLock.scala b/common/src/main/scala/net/psforever/objects/serverobject/locks/IFFLock.scala new file mode 100644 index 00000000..e7dad76c --- /dev/null +++ b/common/src/main/scala/net/psforever/objects/serverobject/locks/IFFLock.scala @@ -0,0 +1,39 @@ +// Copyright (c) 2017 PSForever +package net.psforever.objects.serverobject.locks + +import net.psforever.objects.{GlobalDefinitions, Player} +import net.psforever.objects.serverobject.PlanetSideServerObject +import net.psforever.types.{PlanetSideEmpire, Vector3} + +class IFFLock extends PlanetSideServerObject { + private var hackedBy : Option[(Player, Vector3)] = None + + def HackedBy : Option[(Player, Vector3)] = hackedBy + + def HackedBy_=(agent : Player) : Option[(Player, Vector3)] = HackedBy_=(Some(agent)) + + def HackedBy_=(agent : Option[Player]) : Option[(Player, Vector3)] = { + hackedBy match { + case None => + if(agent.isDefined) { + hackedBy = Some(agent.get, agent.get.Position) + } + case Some(_) => + if(agent.isEmpty) { + hackedBy = None + } + else if(agent.get.Faction != hackedBy.get._1.Faction) { + hackedBy = Some(agent.get, agent.get.Position) //overwrite + } + } + HackedBy + } + + def Definition : IFFLockDefinition = GlobalDefinitions.external_lock +} + +object IFFLock { + def apply() : IFFLock = { + new IFFLock + } +} diff --git a/common/src/main/scala/net/psforever/objects/serverobject/locks/IFFLockControl.scala b/common/src/main/scala/net/psforever/objects/serverobject/locks/IFFLockControl.scala new file mode 100644 index 00000000..571b6968 --- /dev/null +++ b/common/src/main/scala/net/psforever/objects/serverobject/locks/IFFLockControl.scala @@ -0,0 +1,24 @@ +// Copyright (c) 2017 PSForever +package net.psforever.objects.serverobject.locks + +import akka.actor.{Actor, Cancellable} +import net.psforever.objects.serverobject.CommonMessages +import net.psforever.objects.serverobject.doors.Door + +class IFFLockControl(lock : IFFLock) extends Actor { + def receive : Receive = { + case CommonMessages.Hack(player) => + lock.HackedBy = player + case CommonMessages.ClearHack() => + lock.HackedBy = None + case _ => + sender ! Door.NoEvent() + } +} + +object IFFLockControl { + final val DefaultCloser : Cancellable = new Cancellable() { + override def cancel : Boolean = true + override def isCancelled : Boolean = true + } +} \ No newline at end of file diff --git a/common/src/main/scala/net/psforever/objects/doors/IFFLockDefinition.scala b/common/src/main/scala/net/psforever/objects/serverobject/locks/IFFLockDefinition.scala similarity index 77% rename from common/src/main/scala/net/psforever/objects/doors/IFFLockDefinition.scala rename to common/src/main/scala/net/psforever/objects/serverobject/locks/IFFLockDefinition.scala index afd6a2a7..dcad4139 100644 --- a/common/src/main/scala/net/psforever/objects/doors/IFFLockDefinition.scala +++ b/common/src/main/scala/net/psforever/objects/serverobject/locks/IFFLockDefinition.scala @@ -1,5 +1,5 @@ // Copyright (c) 2017 PSForever -package net.psforever.objects.doors +package net.psforever.objects.serverobject.locks import net.psforever.objects.definition.ObjectDefinition diff --git a/common/src/main/scala/net/psforever/objects/terminals/CertTerminalDefinition.scala b/common/src/main/scala/net/psforever/objects/serverobject/terminals/CertTerminalDefinition.scala similarity index 98% rename from common/src/main/scala/net/psforever/objects/terminals/CertTerminalDefinition.scala rename to common/src/main/scala/net/psforever/objects/serverobject/terminals/CertTerminalDefinition.scala index cc617ecb..dbab72d7 100644 --- a/common/src/main/scala/net/psforever/objects/terminals/CertTerminalDefinition.scala +++ b/common/src/main/scala/net/psforever/objects/serverobject/terminals/CertTerminalDefinition.scala @@ -1,5 +1,5 @@ // Copyright (c) 2017 PSForever -package net.psforever.objects.terminals +package net.psforever.objects.serverobject.terminals import net.psforever.objects.Player import net.psforever.packet.game.ItemTransactionMessage diff --git a/common/src/main/scala/net/psforever/objects/terminals/OrderTerminalDefinition.scala b/common/src/main/scala/net/psforever/objects/serverobject/terminals/OrderTerminalDefinition.scala similarity index 99% rename from common/src/main/scala/net/psforever/objects/terminals/OrderTerminalDefinition.scala rename to common/src/main/scala/net/psforever/objects/serverobject/terminals/OrderTerminalDefinition.scala index 0eb171fc..cea418f0 100644 --- a/common/src/main/scala/net/psforever/objects/terminals/OrderTerminalDefinition.scala +++ b/common/src/main/scala/net/psforever/objects/serverobject/terminals/OrderTerminalDefinition.scala @@ -1,5 +1,5 @@ // Copyright (c) 2017 PSForever -package net.psforever.objects.terminals +package net.psforever.objects.serverobject.terminals import net.psforever.objects.InfantryLoadout.Simplification import net.psforever.objects.{Player, Tool} diff --git a/common/src/main/scala/net/psforever/objects/terminals/TemporaryTerminalMessages.scala b/common/src/main/scala/net/psforever/objects/serverobject/terminals/TemporaryTerminalMessages.scala similarity index 63% rename from common/src/main/scala/net/psforever/objects/terminals/TemporaryTerminalMessages.scala rename to common/src/main/scala/net/psforever/objects/serverobject/terminals/TemporaryTerminalMessages.scala index 85ae3375..6d264df3 100644 --- a/common/src/main/scala/net/psforever/objects/terminals/TemporaryTerminalMessages.scala +++ b/common/src/main/scala/net/psforever/objects/serverobject/terminals/TemporaryTerminalMessages.scala @@ -1,13 +1,16 @@ // Copyright (c) 2017 PSForever -package net.psforever.objects.terminals +package net.psforever.objects.serverobject.terminals +import net.psforever.objects.Player import net.psforever.types.PlanetSideEmpire //temporary location for these temporary messages object TemporaryTerminalMessages { //TODO send original packets along with these messages + final case class UseItem(player : Player) final case class Convert(faction : PlanetSideEmpire.Value) - final case class Hacked(faction : Option[PlanetSideEmpire.Value]) + final case class Hack(player : Player) + final case class ClearHack() final case class Damaged(dm : Int) final case class Repaired(rep : Int) } diff --git a/common/src/main/scala/net/psforever/objects/terminals/Terminal.scala b/common/src/main/scala/net/psforever/objects/serverobject/terminals/Terminal.scala similarity index 80% rename from common/src/main/scala/net/psforever/objects/terminals/Terminal.scala rename to common/src/main/scala/net/psforever/objects/serverobject/terminals/Terminal.scala index f5d0bd53..bccd245c 100644 --- a/common/src/main/scala/net/psforever/objects/terminals/Terminal.scala +++ b/common/src/main/scala/net/psforever/objects/serverobject/terminals/Terminal.scala @@ -1,43 +1,45 @@ // Copyright (c) 2017 PSForever -package net.psforever.objects.terminals +package net.psforever.objects.serverobject.terminals -import akka.actor.{ActorContext, ActorRef, Props} -import net.psforever.objects.{PlanetSideGameObject, Player} +import net.psforever.objects.Player import net.psforever.objects.equipment.Equipment import net.psforever.objects.inventory.InventoryItem +import net.psforever.objects.serverobject.PlanetSideServerObject import net.psforever.packet.game.ItemTransactionMessage -import net.psforever.types.{ExoSuitType, PlanetSideEmpire, TransactionType} +import net.psforever.types.{ExoSuitType, PlanetSideEmpire, TransactionType, Vector3} /** * na * @param tdef the `ObjectDefinition` that constructs this object and maintains some of its immutable fields */ -class Terminal(tdef : TerminalDefinition) extends PlanetSideGameObject { - /** Internal reference to the `Actor` for this `Terminal`, sets up by this `Terminal`. */ - private var actor = ActorRef.noSender - - /** - * Get access to the internal `TerminalControl` `Actor` for this `Terminal`. - * If called for the first time, create the said `Actor`. - * Must be called only after the globally unique identifier has been set. - * @param context the `ActorContext` under which this `Terminal`'s `Actor` will be created - * @return the `Terminal`'s `Actor` - */ - def Actor(implicit context : ActorContext) : ActorRef = { - if(actor == ActorRef.noSender) { - actor = context.actorOf(Props(classOf[TerminalControl], this), s"${tdef.Name}_${GUID.guid}") - } - actor - } - +class Terminal(tdef : TerminalDefinition) extends PlanetSideServerObject { //the following fields and related methods are neither finalized no integrated; GOTO Request private var faction : PlanetSideEmpire.Value = PlanetSideEmpire.NEUTRAL - private var hackedBy : Option[PlanetSideEmpire.Value] = None + private var hackedBy : Option[(Player, Vector3)] = None private var health : Int = 100 //TODO not real health value def Faction : PlanetSideEmpire.Value = faction - def HackedBy : Option[PlanetSideEmpire.Value] = hackedBy + def HackedBy : Option[(Player, Vector3)] = hackedBy + + def HackedBy_=(agent : Player) : Option[(Player, Vector3)] = HackedBy_=(Some(agent)) + + def HackedBy_=(agent : Option[Player]) : Option[(Player, Vector3)] = { + hackedBy match { + case None => + if(agent.isDefined) { + hackedBy = Some(agent.get, agent.get.Position) + } + case Some(_) => + if(agent.isEmpty) { + hackedBy = None + } + else if(agent.get.Faction != hackedBy.get._1.Faction) { + hackedBy = Some(agent.get, agent.get.Position) //overwrite + } + } + HackedBy + } def Health : Int = health @@ -46,10 +48,6 @@ class Terminal(tdef : TerminalDefinition) extends PlanetSideGameObject { faction = toFaction } - def HackedBy(toFaction : Option[PlanetSideEmpire.Value]) : Unit = { - hackedBy = if(toFaction.contains(faction)) { None } else { toFaction } - } - def Damaged(dam : Int) : Unit = { health = Math.max(0, Health - dam) } diff --git a/common/src/main/scala/net/psforever/objects/terminals/TerminalControl.scala b/common/src/main/scala/net/psforever/objects/serverobject/terminals/TerminalControl.scala similarity index 63% rename from common/src/main/scala/net/psforever/objects/terminals/TerminalControl.scala rename to common/src/main/scala/net/psforever/objects/serverobject/terminals/TerminalControl.scala index 7e9a71cf..8774245e 100644 --- a/common/src/main/scala/net/psforever/objects/terminals/TerminalControl.scala +++ b/common/src/main/scala/net/psforever/objects/serverobject/terminals/TerminalControl.scala @@ -1,5 +1,5 @@ // Copyright (c) 2017 PSForever -package net.psforever.objects.terminals +package net.psforever.objects.serverobject.terminals import akka.actor.Actor @@ -14,17 +14,11 @@ class TerminalControl(term : Terminal) extends Actor { case Terminal.Request(player, msg) => sender ! Terminal.TerminalMessage(player, msg, term.Request(player, msg)) - case TemporaryTerminalMessages.Convert(fact) => - term.Convert(fact) + case TemporaryTerminalMessages.Hack(player) => + term.HackedBy = player - case TemporaryTerminalMessages.Hacked(fact) => - term.HackedBy(fact) - - case TemporaryTerminalMessages.Damaged(dam) => - term.Damaged(dam) - - case TemporaryTerminalMessages.Repaired(rep) => - term.Repair(rep) + case TemporaryTerminalMessages.ClearHack() => + term.HackedBy = None case _ => sender ! Terminal.NoDeal() diff --git a/common/src/main/scala/net/psforever/objects/terminals/TerminalDefinition.scala b/common/src/main/scala/net/psforever/objects/serverobject/terminals/TerminalDefinition.scala similarity index 99% rename from common/src/main/scala/net/psforever/objects/terminals/TerminalDefinition.scala rename to common/src/main/scala/net/psforever/objects/serverobject/terminals/TerminalDefinition.scala index a14836fa..5cf8ff3d 100644 --- a/common/src/main/scala/net/psforever/objects/terminals/TerminalDefinition.scala +++ b/common/src/main/scala/net/psforever/objects/serverobject/terminals/TerminalDefinition.scala @@ -1,5 +1,5 @@ // Copyright (c) 2017 PSForever -package net.psforever.objects.terminals +package net.psforever.objects.serverobject.terminals import net.psforever.objects._ import net.psforever.objects.definition._ 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 ae3cf6fa..bdc25b9d 100644 --- a/common/src/main/scala/net/psforever/objects/zones/Zone.scala +++ b/common/src/main/scala/net/psforever/objects/zones/Zone.scala @@ -2,7 +2,7 @@ package net.psforever.objects.zones import akka.actor.{ActorContext, ActorRef, Props} -import net.psforever.objects.doors.Base +import net.psforever.objects.serverobject.doors.Base import net.psforever.objects.{PlanetSideGameObject, Player} import net.psforever.objects.equipment.Equipment import net.psforever.objects.guid.NumberPoolHub @@ -48,6 +48,10 @@ class Zone(private val zoneId : String, zoneMap : ZoneMap, zoneNumber : Int) { private val equipmentOnGround : ListBuffer[Equipment] = ListBuffer[Equipment]() /** Used by the `Zone` to coordinate `Equipment` dropping and collection requests. */ private var ground : ActorRef = ActorRef.noSender + /** */ + private var doors : ActorRef = ActorRef.noSender + /** */ + private var events : ActorRef = ActorRef.noSender private var bases : List[Base] = List() @@ -68,6 +72,7 @@ class Zone(private val zoneId : String, zoneMap : ZoneMap, zoneNumber : Int) { 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") + doors = context.actorOf(Props(classOf[ZoneDoorActor], this), s"$Id-doors") Map.LocalObjects.foreach({ builderObject => builderObject.Build @@ -174,6 +179,17 @@ class Zone(private val zoneId : String, zoneMap : ZoneMap, zoneNumber : Int) { */ def Ground : ActorRef = ground + def Doors : ActorRef = doors + + def Events : ActorRef = events + + def Events_=(zoneActor : ActorRef) : ActorRef = { + if(events == ActorRef.noSender) { + events = zoneActor + } + Events + } + def MakeBases(num : Int) : List[Base] = { bases = (0 to num).map(id => new Base(id)).toList bases diff --git a/common/src/main/scala/net/psforever/objects/zones/ZoneActor.scala b/common/src/main/scala/net/psforever/objects/zones/ZoneActor.scala index ba34619c..022aa53d 100644 --- a/common/src/main/scala/net/psforever/objects/zones/ZoneActor.scala +++ b/common/src/main/scala/net/psforever/objects/zones/ZoneActor.scala @@ -2,6 +2,7 @@ package net.psforever.objects.zones import akka.actor.Actor +import net.psforever.objects.serverobject.locks.IFFLock /** * na @@ -34,8 +35,8 @@ class ZoneActor(zone : Zone) extends Actor { } }) - //check door to lock association - import net.psforever.objects.doors.{Door, IFFLock} + //check door to locks association + import net.psforever.objects.serverobject.doors.Door map.DoorToLock.foreach({ case((door_guid, lock_guid)) => try { if(!guid(door_guid).get.isInstanceOf[Door]) { @@ -48,12 +49,12 @@ class ZoneActor(zone : Zone) extends Actor { } try { if(!guid(lock_guid).get.isInstanceOf[IFFLock]) { - slog.error(s"expected id $lock_guid to be an IFF lock, but it was not") + slog.error(s"expected id $lock_guid to be an IFF locks, but it was not") } } catch { case _ : Exception => - slog.error(s"expected an IFF lock, but looking for uninitialized object $lock_guid") + slog.error(s"expected an IFF locks, but looking for uninitialized object $lock_guid") } }) } diff --git a/common/src/main/scala/net/psforever/objects/doors/DoorCloseControl.scala b/common/src/main/scala/net/psforever/objects/zones/ZoneDoorActor.scala similarity index 84% rename from common/src/main/scala/net/psforever/objects/doors/DoorCloseControl.scala rename to common/src/main/scala/net/psforever/objects/zones/ZoneDoorActor.scala index 3b845804..b5d4ae0c 100644 --- a/common/src/main/scala/net/psforever/objects/doors/DoorCloseControl.scala +++ b/common/src/main/scala/net/psforever/objects/zones/ZoneDoorActor.scala @@ -1,14 +1,14 @@ // Copyright (c) 2017 PSForever -package net.psforever.objects.doors +package net.psforever.objects.zones -import akka.actor.{Actor, ActorRef, Cancellable} +import akka.actor.{Actor, Cancellable} import net.psforever.packet.game.PlanetSideGUID -import scala.concurrent.duration._ import scala.annotation.tailrec +import scala.concurrent.duration._ -class DoorCloseControl(implicit val environment : ActorRef) extends Actor { - import DoorCloseControl._ +class ZoneDoorActor(implicit val zone : Zone) extends Actor { + import ZoneDoorActor._ private var doorCloserTrigger : Cancellable = DefaultCloser private var openDoors : List[DoorEntry] = Nil @@ -17,7 +17,7 @@ class DoorCloseControl(implicit val environment : ActorRef) extends Actor { openDoors = openDoors :+ DoorEntry(guid, time) if(doorCloserTrigger.isCancelled) { import scala.concurrent.ExecutionContext.Implicits.global - doorCloserTrigger = context.system.scheduler.scheduleOnce(timeout, self, DoorCloseControl.CloseTheDoor()) + doorCloserTrigger = context.system.scheduler.scheduleOnce(timeout, self, ZoneDoorActor.CloseTheDoor()) } case CloseTheDoor() => @@ -25,9 +25,9 @@ class DoorCloseControl(implicit val environment : ActorRef) extends Actor { val now : Long = System.nanoTime recursiveCloseDoors(openDoors.iterator, now) match { case entry :: rest => - openDoors = entry :: rest + openDoors = rest import scala.concurrent.ExecutionContext.Implicits.global - doorCloserTrigger = context.system.scheduler.scheduleOnce((now - entry.opened_at_time + timeout_time)*1000 milliseconds, self, DoorCloseControl.CloseTheDoor()) + doorCloserTrigger = context.system.scheduler.scheduleOnce((now - entry.opened_at_time + timeout_time)*1000 milliseconds, self, ZoneDoorActor.CloseTheDoor()) case Nil => openDoors = Nil } @@ -52,7 +52,7 @@ class DoorCloseControl(implicit val environment : ActorRef) extends Actor { } } -object DoorCloseControl { +object ZoneDoorActor { private final val timeout_time = 5000 private final val timeout : FiniteDuration = timeout_time milliseconds @@ -61,9 +61,9 @@ object DoorCloseControl { override def isCancelled : Boolean = true } - private final case class DoorEntry(door_guid : PlanetSideGUID, opened_at_time : Long) - final case class DoorIsOpen(door_guid : PlanetSideGUID, opened_at_time : Long = System.nanoTime()) + private final case class DoorEntry(door_guid : PlanetSideGUID, opened_at_time : Long) + private final case class CloseTheDoor() } diff --git a/common/src/main/scala/net/psforever/objects/zones/ZoneMap.scala b/common/src/main/scala/net/psforever/objects/zones/ZoneMap.scala index 7c56a131..96cc0928 100644 --- a/common/src/main/scala/net/psforever/objects/zones/ZoneMap.scala +++ b/common/src/main/scala/net/psforever/objects/zones/ZoneMap.scala @@ -1,6 +1,8 @@ // Copyright (c) 2017 PSForever package net.psforever.objects.zones +import net.psforever.objects.serverobject.builders.ServerObjectBuilder + /** * The fixed instantiation and relation of a series of server objects.
*
diff --git a/pslogin/src/main/scala/AvatarService.scala b/pslogin/src/main/scala/AvatarService.scala index 5905366f..0287f244 100644 --- a/pslogin/src/main/scala/AvatarService.scala +++ b/pslogin/src/main/scala/AvatarService.scala @@ -1,6 +1,7 @@ // Copyright (c) 2017 PSForever import akka.actor.Actor import net.psforever.objects.equipment.Equipment +import net.psforever.objects.serverobject.PlanetSideServerObject import net.psforever.packet.game.objectcreate.ConstructorData import net.psforever.types.ExoSuitType import net.psforever.packet.game.{PlanetSideGUID, PlayerStateMessageUpstream} @@ -13,6 +14,7 @@ object AvatarAction { //final case class DropItem(pos : Vector3, orient : Vector3, item : PlanetSideGUID) extends Action final case class EquipmentInHand(player_guid : PlanetSideGUID, slot : Int, item : Equipment) extends Action final case class EquipmentOnGround(player_guid : PlanetSideGUID, pos : Vector3, orient : Vector3, item : Equipment) extends Action + final case class Hack(player_guid : PlanetSideGUID, target : PlanetSideServerObject, unk1 : Long, unk2 : Long = 8L) extends Action final case class LoadPlayer(player_guid : PlanetSideGUID, pdata : ConstructorData) extends Action // final case class LoadMap(msg : PlanetSideGUID) extends Action // final case class unLoadMap(msg : PlanetSideGUID) extends Action @@ -34,6 +36,7 @@ object AvatarServiceResponse { //final case class DropItem(pos : Vector3, orient : Vector3, item : PlanetSideGUID) extends Response final case class EquipmentInHand(slot : Int, item : Equipment) extends Response final case class EquipmentOnGround(pos : Vector3, orient : Vector3, item : Equipment) extends Response + final case class Hack(target_guid : PlanetSideGUID, unk1 : Long, unk2 : Long = 8L) extends Response final case class LoadPlayer(pdata : ConstructorData) extends Response // final case class unLoadMap() extends Response // final case class LoadMap() extends Response @@ -93,6 +96,10 @@ class AvatarService extends Actor { AvatarEvents.publish( AvatarServiceResponse(s"/$forChannel/Avatar", player_guid, AvatarServiceResponse.EquipmentOnGround(pos, orient, obj)) ) + case AvatarAction.Hack(player_guid, target, unk1, unk2) => + AvatarEvents.publish( + AvatarServiceResponse(s"/$forChannel/Avatar", player_guid, AvatarServiceResponse.Hack(target.GUID, unk1, unk2)) + ) case AvatarAction.LoadPlayer(player_guid, pdata) => AvatarEvents.publish( AvatarServiceResponse(s"/$forChannel/Avatar", player_guid, AvatarServiceResponse.LoadPlayer(pdata)) diff --git a/pslogin/src/main/scala/PsLogin.scala b/pslogin/src/main/scala/PsLogin.scala index 59edb2f7..de00fb1b 100644 --- a/pslogin/src/main/scala/PsLogin.scala +++ b/pslogin/src/main/scala/PsLogin.scala @@ -14,6 +14,7 @@ import com.typesafe.config.ConfigFactory import net.psforever.crypto.CryptoInterface import net.psforever.objects.zones._ import net.psforever.objects.guid.TaskResolver +import net.psforever.objects.serverobject.builders.{DoorObjectBuilder, IFFLockObjectBuilder, TerminalObjectBuilder} import org.slf4j import org.fusesource.jansi.Ansi._ import org.fusesource.jansi.Ansi.Color._ diff --git a/pslogin/src/main/scala/WorldSessionActor.scala b/pslogin/src/main/scala/WorldSessionActor.scala index 8d634f71..a78e611a 100644 --- a/pslogin/src/main/scala/WorldSessionActor.scala +++ b/pslogin/src/main/scala/WorldSessionActor.scala @@ -11,14 +11,16 @@ import org.log4s.MDC import MDCContextAware.Implicits._ import ServiceManager.Lookup import net.psforever.objects._ -import net.psforever.objects.doors.{Door, IFFLock} +import net.psforever.objects.serverobject.doors.Door 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.Terminal +import net.psforever.objects.serverobject.{CommonMessages, PlanetSideServerObject} +import net.psforever.objects.serverobject.locks.IFFLock +import net.psforever.objects.serverobject.terminals.Terminal import net.psforever.packet.game.objectcreate._ import net.psforever.types._ @@ -140,6 +142,11 @@ class WorldSessionActor extends Actor with MDCContextAware { ) } + case AvatarServiceResponse.Hack(target_guid, unk1, unk2) => + if(player.GUID != guid) { + sendResponse(PacketCoding.CreateGamePacket(0, HackMessage(0, target_guid.guid, guid.guid, 100, unk1, 6, unk2))) + } + case AvatarServiceResponse.LoadPlayer(pdata) => if(player.GUID != guid) { sendResponse( @@ -566,6 +573,9 @@ class WorldSessionActor extends Actor with MDCContextAware { if(progressBarVal > 100) { progressBarValue = None log.info(s"We've hacked the item $target! Now what?") + sendResponse(PacketCoding.CreateGamePacket(0, HackMessage(0, target.GUID.guid, player.GUID.guid, 100, 1114636288, 6, 8L))) + target.Actor ! CommonMessages.Hack(tplayer) + avatarService ! AvatarServiceMessage(tplayer.Continent, AvatarAction.Hack(tplayer.GUID, target, 1114636288)) //TODO now what? } else { @@ -994,14 +1004,19 @@ class WorldSessionActor extends Actor with MDCContextAware { case Some(door : Door) => continent.Map.DoorToLock.get(object_guid.guid) match { //check for IFFLock case Some(lock_guid) => - val lock_hacked = continent.GUID(lock_guid).get.asInstanceOf[IFFLock].Hacker.contains(player.Faction) + val lock_hacked = continent.GUID(lock_guid).get.asInstanceOf[IFFLock].HackedBy match { + case Some((tplayer, _)) => + tplayer.Faction == player.Faction + case None => + false + } continent.Map.ObjectToBase.get(lock_guid) match { //check for associated base case Some(base_id) => - if(continent.Base(base_id).get.Faction == player.Faction || lock_hacked) { //either base allegiance aligns or lock is hacked + if(continent.Base(base_id).get.Faction == player.Faction || lock_hacked) { //either base allegiance aligns or locks is hacked door.Actor ! Door.Use(player, msg) } case None => - if(lock_hacked) { //is lock hacked? + if(lock_hacked) { //is locks hacked? door.Actor ! Door.Use(player, msg) } } @@ -1614,7 +1629,7 @@ object WorldSessionActor { private final case class PlayerFailedToLoad(tplayer : Player) private final case class ListAccountCharacters() private final case class SetCurrentAvatar(tplayer : Player) - private final case class ItemHacking(tplayer : Player, target : PlanetSideGameObject, tool_guid : PlanetSideGUID, delta : Float) + private final case class ItemHacking(tplayer : Player, target : PlanetSideServerObject, tool_guid : PlanetSideGUID, delta : Float) /** * A placeholder `Cancellable` object. From 74b718c536474b6ea6c3220c84ef97e6296f5aee Mon Sep 17 00:00:00 2001 From: FateJH Date: Wed, 11 Oct 2017 19:50:50 -0400 Subject: [PATCH 14/23] doors open and async close now via LocalEnvironment and DoorCloseActor --- .../objects/serverobject/doors/Door.scala | 1 + .../serverobject/doors/DoorControl.scala | 1 + .../objects/zones/DoorCloseActor.scala | 87 +++++++++++++++++++ .../net/psforever/objects/zones/Zone.scala | 16 ---- .../objects/zones/ZoneDoorActor.scala | 69 --------------- pslogin/src/main/scala/LocalService.scala | 27 ++++-- .../src/main/scala/WorldSessionActor.scala | 31 ++++++- 7 files changed, 138 insertions(+), 94 deletions(-) create mode 100644 common/src/main/scala/net/psforever/objects/zones/DoorCloseActor.scala delete mode 100644 common/src/main/scala/net/psforever/objects/zones/ZoneDoorActor.scala diff --git a/common/src/main/scala/net/psforever/objects/serverobject/doors/Door.scala b/common/src/main/scala/net/psforever/objects/serverobject/doors/Door.scala index 25c50e85..5971fbaa 100644 --- a/common/src/main/scala/net/psforever/objects/serverobject/doors/Door.scala +++ b/common/src/main/scala/net/psforever/objects/serverobject/doors/Door.scala @@ -33,6 +33,7 @@ class Door(ddef : DoorDefinition) extends PlanetSideServerObject { Door.OpenEvent() } else if(openState) { + openState = false Door.CloseEvent() } else { diff --git a/common/src/main/scala/net/psforever/objects/serverobject/doors/DoorControl.scala b/common/src/main/scala/net/psforever/objects/serverobject/doors/DoorControl.scala index 15875291..ac3832f0 100644 --- a/common/src/main/scala/net/psforever/objects/serverobject/doors/DoorControl.scala +++ b/common/src/main/scala/net/psforever/objects/serverobject/doors/DoorControl.scala @@ -11,6 +11,7 @@ class DoorControl(door : Door) extends Actor { def receive : Receive = { case Door.Use(player, msg) => sender ! Door.DoorMessage(player, msg, door.Use(player, msg)) + case _ => sender ! Door.NoEvent() } diff --git a/common/src/main/scala/net/psforever/objects/zones/DoorCloseActor.scala b/common/src/main/scala/net/psforever/objects/zones/DoorCloseActor.scala new file mode 100644 index 00000000..b1604b75 --- /dev/null +++ b/common/src/main/scala/net/psforever/objects/zones/DoorCloseActor.scala @@ -0,0 +1,87 @@ +// Copyright (c) 2017 PSForever +package net.psforever.objects.zones + +import akka.actor.{Actor, Cancellable} +import net.psforever.objects.serverobject.doors.Door +import net.psforever.packet.game.PlanetSideGUID + +import scala.annotation.tailrec +import scala.concurrent.duration._ + +class DoorCloseActor() extends Actor { + import DoorCloseActor._ + private var doorCloserTrigger : Cancellable = DefaultCloser + private var openDoors : List[DoorEntry] = Nil + //private[this] val log = org.log4s.getLogger + + def receive : Receive = { + case DoorIsOpen(door, zone, time) => + openDoors = openDoors :+ DoorEntry(door, zone, time) + if(openDoors.size == 1) { + import scala.concurrent.ExecutionContext.Implicits.global + doorCloserTrigger = context.system.scheduler.scheduleOnce(timeout, self, DoorCloseActor.TryCloseDoors()) + } + + case TryCloseDoors() => + doorCloserTrigger.cancel + val now : Long = System.nanoTime + //TODO we can just walk across the list of doors and extract only the first few entries + val (doorsToClose, doorsLeftOpen) = recursivePartitionDoors(openDoors.iterator, now) + openDoors = doorsLeftOpen + doorsToClose.foreach(entry => { + entry.door.Open = false //permissible + context.parent ! DoorCloseActor.CloseTheDoor(entry.door.GUID, entry.zone.Id) + }) + + if(doorsLeftOpen.nonEmpty) { + val short_timeout : FiniteDuration = math.max(1, timeout_time - (now - doorsLeftOpen.head.opened_at_time)) nanoseconds + import scala.concurrent.ExecutionContext.Implicits.global + doorCloserTrigger = context.system.scheduler.scheduleOnce(short_timeout, self, DoorCloseActor.TryCloseDoors()) + } + + case _ => ; + } + + /** + * na + * @param iter na + * @param now na + * @param list na + * @see `List.partition` + * @return a `Tuple` of two `Lists`: + * the entries for all `Door`s that are closing, + * and the entries for all doors that are staying open + */ + @tailrec private def recursivePartitionDoors(iter : Iterator[DoorEntry], now : Long, list : List[DoorEntry] = Nil) : (List[DoorEntry], List[DoorEntry]) = { + if(!iter.hasNext) { + (list, iter.toList) + } + else { + val entry = iter.next() + if(now - entry.opened_at_time >= timeout_time) { + recursivePartitionDoors(iter, now, list :+ entry) + } + else { + (list, entry +: iter.toList) + } + } + } +} + +object DoorCloseActor { + private final val timeout_time : Long = 5000000000L //nanoseconds + private final val timeout : FiniteDuration = timeout_time nanoseconds + + private final val DefaultCloser : Cancellable = new Cancellable() { + override def cancel : Boolean = true + override def isCancelled : Boolean = true + } + + final case class DoorIsOpen(door : Door, zone : Zone, opened_at_time : Long = System.nanoTime()) + + final case class CloseTheDoor(door_guid : PlanetSideGUID, zone_id : String) + + private final case class DoorEntry(door : Door, zone : Zone, opened_at_time : Long) + + private final case class TryCloseDoors() +} 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 bdc25b9d..68415c50 100644 --- a/common/src/main/scala/net/psforever/objects/zones/Zone.scala +++ b/common/src/main/scala/net/psforever/objects/zones/Zone.scala @@ -48,10 +48,6 @@ class Zone(private val zoneId : String, zoneMap : ZoneMap, zoneNumber : Int) { private val equipmentOnGround : ListBuffer[Equipment] = ListBuffer[Equipment]() /** Used by the `Zone` to coordinate `Equipment` dropping and collection requests. */ private var ground : ActorRef = ActorRef.noSender - /** */ - private var doors : ActorRef = ActorRef.noSender - /** */ - private var events : ActorRef = ActorRef.noSender private var bases : List[Base] = List() @@ -72,7 +68,6 @@ class Zone(private val zoneId : String, zoneMap : ZoneMap, zoneNumber : Int) { 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") - doors = context.actorOf(Props(classOf[ZoneDoorActor], this), s"$Id-doors") Map.LocalObjects.foreach({ builderObject => builderObject.Build @@ -179,17 +174,6 @@ class Zone(private val zoneId : String, zoneMap : ZoneMap, zoneNumber : Int) { */ def Ground : ActorRef = ground - def Doors : ActorRef = doors - - def Events : ActorRef = events - - def Events_=(zoneActor : ActorRef) : ActorRef = { - if(events == ActorRef.noSender) { - events = zoneActor - } - Events - } - def MakeBases(num : Int) : List[Base] = { bases = (0 to num).map(id => new Base(id)).toList bases diff --git a/common/src/main/scala/net/psforever/objects/zones/ZoneDoorActor.scala b/common/src/main/scala/net/psforever/objects/zones/ZoneDoorActor.scala deleted file mode 100644 index b5d4ae0c..00000000 --- a/common/src/main/scala/net/psforever/objects/zones/ZoneDoorActor.scala +++ /dev/null @@ -1,69 +0,0 @@ -// Copyright (c) 2017 PSForever -package net.psforever.objects.zones - -import akka.actor.{Actor, Cancellable} -import net.psforever.packet.game.PlanetSideGUID - -import scala.annotation.tailrec -import scala.concurrent.duration._ - -class ZoneDoorActor(implicit val zone : Zone) extends Actor { - import ZoneDoorActor._ - private var doorCloserTrigger : Cancellable = DefaultCloser - private var openDoors : List[DoorEntry] = Nil - - def receive : Receive = { - case DoorIsOpen(guid, time) => - openDoors = openDoors :+ DoorEntry(guid, time) - if(doorCloserTrigger.isCancelled) { - import scala.concurrent.ExecutionContext.Implicits.global - doorCloserTrigger = context.system.scheduler.scheduleOnce(timeout, self, ZoneDoorActor.CloseTheDoor()) - } - - case CloseTheDoor() => - doorCloserTrigger.cancel - val now : Long = System.nanoTime - recursiveCloseDoors(openDoors.iterator, now) match { - case entry :: rest => - openDoors = rest - import scala.concurrent.ExecutionContext.Implicits.global - doorCloserTrigger = context.system.scheduler.scheduleOnce((now - entry.opened_at_time + timeout_time)*1000 milliseconds, self, ZoneDoorActor.CloseTheDoor()) - case Nil => - openDoors = Nil - } - - case _ => ; - } - - @tailrec private def recursiveCloseDoors(iter : Iterator[DoorEntry], now : Long) : List[DoorEntry] = { - if(!iter.hasNext) { - Nil - } - else { - val entry = iter.next - if(now - entry.opened_at_time < timeout_time) { - entry +: iter.toList - } - else { - //TODO close this door entry - recursiveCloseDoors(iter, now) - } - } - } -} - -object ZoneDoorActor { - private final val timeout_time = 5000 - private final val timeout : FiniteDuration = timeout_time milliseconds - - private final val DefaultCloser : Cancellable = new Cancellable() { - override def cancel : Boolean = true - override def isCancelled : Boolean = true - } - - final case class DoorIsOpen(door_guid : PlanetSideGUID, opened_at_time : Long = System.nanoTime()) - - private final case class DoorEntry(door_guid : PlanetSideGUID, opened_at_time : Long) - - private final case class CloseTheDoor() -} diff --git a/pslogin/src/main/scala/LocalService.scala b/pslogin/src/main/scala/LocalService.scala index 5337b074..ae014ab2 100644 --- a/pslogin/src/main/scala/LocalService.scala +++ b/pslogin/src/main/scala/LocalService.scala @@ -1,17 +1,21 @@ // Copyright (c) 2017 PSForever -import akka.actor.Actor +import akka.actor.{Actor, Props} +import net.psforever.objects.serverobject.doors.Door +import net.psforever.objects.zones.{DoorCloseActor, Zone} import net.psforever.packet.game.PlanetSideGUID object LocalAction { trait Action - final case class Door(player_guid : PlanetSideGUID) extends Action + final case class DoorOpens(player_guid : PlanetSideGUID, continent : Zone, door : Door) extends Action + final case class DoorCloses(player_guid : PlanetSideGUID, door_guid : PlanetSideGUID) extends Action } object LocalServiceResponse { trait Response - final case class Door(player_guid : PlanetSideGUID) extends Response + final case class DoorOpens(door_guid : PlanetSideGUID) extends Response + final case class DoorCloses(door_guid : PlanetSideGUID) extends Response } final case class LocalServiceMessage(forChannel : String, actionMessage : LocalAction.Action) @@ -24,6 +28,7 @@ final case class LocalServiceResponse(toChannel : String, avatar_guid : PlanetSi class LocalService extends Actor { //import LocalService._ + private val doorCloser = context.actorOf(Props[DoorCloseActor], "local-door-closer") private [this] val log = org.log4s.getLogger override def preStart = { @@ -45,13 +50,25 @@ class LocalService extends Actor { case LocalServiceMessage(forChannel, action) => action match { - case LocalAction.Door(player_guid) => + case LocalAction.DoorOpens(player_guid, zone, door) => + doorCloser ! DoorCloseActor.DoorIsOpen(door, zone) LocalEvents.publish( - LocalServiceResponse(s"/$forChannel/LocalEnvironment" + forChannel, player_guid, LocalServiceResponse.Door(player_guid)) + LocalServiceResponse(s"/$forChannel/LocalEnvironment", player_guid, LocalServiceResponse.DoorOpens(door.GUID)) + ) + + case LocalAction.DoorCloses(player_guid, door_guid) => + LocalEvents.publish( + LocalServiceResponse(s"/$forChannel/LocalEnvironment", player_guid, LocalServiceResponse.DoorCloses(door_guid)) ) case _ => ; } + //response from DoorCloseActor + case DoorCloseActor.CloseTheDoor(door_guid, zone_id) => + LocalEvents.publish( + LocalServiceResponse(s"/$zone_id/LocalEnvironment", PlanetSideGUID(0), LocalServiceResponse.DoorCloses(door_guid)) + ) + case msg => log.info(s"Unhandled message $msg from $sender") } diff --git a/pslogin/src/main/scala/WorldSessionActor.scala b/pslogin/src/main/scala/WorldSessionActor.scala index a78e611a..559ea213 100644 --- a/pslogin/src/main/scala/WorldSessionActor.scala +++ b/pslogin/src/main/scala/WorldSessionActor.scala @@ -49,6 +49,7 @@ class WorldSessionActor extends Actor with MDCContextAware { clientKeepAlive.cancel() avatarService ! Service.Leave() + localService ! Service.Leave() LivePlayerList.Remove(sessionId) match { case Some(tplayer) => if(tplayer.HasGUID) { @@ -70,7 +71,8 @@ class WorldSessionActor extends Actor with MDCContextAware { if(pipe.hasNext) { rightRef = pipe.next rightRef !> HelloFriend(sessionId, pipe) - } else { + } + else { rightRef = sender() } context.become(Started) @@ -221,13 +223,33 @@ class WorldSessionActor extends Actor with MDCContextAware { case _ => ; } - case Door.DoorMessage(_, msg, order) => + case LocalServiceResponse(_, guid, reply) => + reply match { + case LocalServiceResponse.DoorOpens(door_guid) => + if(player.GUID != guid) { + sendResponse(PacketCoding.CreateGamePacket(0, GenericObjectStateMsg(door_guid, 16))) + } + + case LocalServiceResponse.DoorCloses(door_guid) => //door closes for everyone + sendResponse(PacketCoding.CreateGamePacket(0, GenericObjectStateMsg(door_guid, 17))) + } + + case Door.DoorMessage(tplayer, msg, order) => + val door_guid = msg.object_guid order match { case Door.OpenEvent() => - sendResponse(PacketCoding.CreateGamePacket(0, GenericObjectStateMsg(msg.object_guid, 16))) + continent.GUID(door_guid) match { + case Some(door : Door) => + sendResponse(PacketCoding.CreateGamePacket(0, GenericObjectStateMsg(door_guid, 16))) + localService ! LocalServiceMessage (continent.Id, LocalAction.DoorOpens (tplayer.GUID, continent, door) ) + + case _ => + log.warn(s"door $door_guid wanted to be opened but could not be found") + } case Door.CloseEvent() => - sendResponse(PacketCoding.CreateGamePacket(0, GenericObjectStateMsg(msg.object_guid, 17))) + sendResponse(PacketCoding.CreateGamePacket(0, GenericObjectStateMsg(door_guid, 17))) + localService ! LocalServiceMessage(continent.Id, LocalAction.DoorCloses(tplayer.GUID, door_guid)) case Door.NoEvent() => ; } @@ -769,6 +791,7 @@ class WorldSessionActor extends Actor with MDCContextAware { }) avatarService ! Service.Join(player.Continent) + localService ! Service.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) => From 0a4bac8ab508868bc20be63243137fadd0fd6567 Mon Sep 17 00:00:00 2001 From: FateJH Date: Thu, 12 Oct 2017 21:05:38 -0400 Subject: [PATCH 15/23] door hacking now clears in 60s --- .../serverobject/locks/IFFLockControl.scala | 4 +- .../objects/zones/HackClearActor.scala | 87 +++++++++++++++++++ pslogin/src/main/scala/AvatarService.scala | 7 -- pslogin/src/main/scala/LocalService.scala | 30 ++++++- .../src/main/scala/WorldSessionActor.scala | 44 ++++++---- 5 files changed, 143 insertions(+), 29 deletions(-) create mode 100644 common/src/main/scala/net/psforever/objects/zones/HackClearActor.scala diff --git a/common/src/main/scala/net/psforever/objects/serverobject/locks/IFFLockControl.scala b/common/src/main/scala/net/psforever/objects/serverobject/locks/IFFLockControl.scala index 571b6968..e96d1bd3 100644 --- a/common/src/main/scala/net/psforever/objects/serverobject/locks/IFFLockControl.scala +++ b/common/src/main/scala/net/psforever/objects/serverobject/locks/IFFLockControl.scala @@ -3,7 +3,6 @@ package net.psforever.objects.serverobject.locks import akka.actor.{Actor, Cancellable} import net.psforever.objects.serverobject.CommonMessages -import net.psforever.objects.serverobject.doors.Door class IFFLockControl(lock : IFFLock) extends Actor { def receive : Receive = { @@ -11,8 +10,7 @@ class IFFLockControl(lock : IFFLock) extends Actor { lock.HackedBy = player case CommonMessages.ClearHack() => lock.HackedBy = None - case _ => - sender ! Door.NoEvent() + case _ => ; } } diff --git a/common/src/main/scala/net/psforever/objects/zones/HackClearActor.scala b/common/src/main/scala/net/psforever/objects/zones/HackClearActor.scala new file mode 100644 index 00000000..b595051e --- /dev/null +++ b/common/src/main/scala/net/psforever/objects/zones/HackClearActor.scala @@ -0,0 +1,87 @@ +// Copyright (c) 2017 PSForever +package net.psforever.objects.zones + +import akka.actor.{Actor, Cancellable} +import net.psforever.objects.serverobject.{CommonMessages, PlanetSideServerObject} +import net.psforever.packet.game.PlanetSideGUID + +import scala.annotation.tailrec +import scala.concurrent.duration._ + +class HackClearActor() extends Actor { + import HackClearActor._ + private var clearTrigger : Cancellable = DefaultClearer + private var hackedObjects : List[HackEntry] = Nil + //private[this] val log = org.log4s.getLogger + + def receive : Receive = { + case ObjectIsHacked(door, zone, unk1, unk2, time) => + hackedObjects = hackedObjects :+ HackEntry(door, zone, unk1, unk2, time) + if(hackedObjects.size == 1) { + import scala.concurrent.ExecutionContext.Implicits.global + clearTrigger = context.system.scheduler.scheduleOnce(timeout, self, HackClearActor.TryClearHacks()) + } + + case TryClearHacks() => + clearTrigger.cancel + val now : Long = System.nanoTime + //TODO we can just walk across the list of doors and extract only the first few entries + val (unhackObjects, stillHackedObjects) = recursivePartitionHacks(hackedObjects.iterator, now) + hackedObjects = stillHackedObjects + unhackObjects.foreach(entry => { + entry.target.Actor ! CommonMessages.ClearHack() + context.parent ! HackClearActor.ClearTheHack(entry.target.GUID, entry.zone.Id, entry.unk1, entry.unk2) + }) + + if(stillHackedObjects.nonEmpty) { + val short_timeout : FiniteDuration = math.max(1, timeout_time - (now - stillHackedObjects.head.hacked_at_time)) nanoseconds + import scala.concurrent.ExecutionContext.Implicits.global + clearTrigger = context.system.scheduler.scheduleOnce(short_timeout, self, HackClearActor.TryClearHacks()) + } + + case _ => ; + } + + /** + * na + * @param iter na + * @param now na + * @param list na + * @see `List.partition` + * @return a `Tuple` of two `Lists`: + * the entries for all objects that are no longer hacked, + * and the entries for all objects that are still hacked + */ + @tailrec private def recursivePartitionHacks(iter : Iterator[HackEntry], now : Long, list : List[HackEntry] = Nil) : (List[HackEntry], List[HackEntry]) = { + if(!iter.hasNext) { + (list, iter.toList) + } + else { + val entry = iter.next() + if(now - entry.hacked_at_time >= timeout_time) { + recursivePartitionHacks(iter, now, list :+ entry) + } + else { + (list, entry +: iter.toList) + } + } + } +} + +object HackClearActor { + private final val timeout_time : Long = 60000000000L //nanoseconds (1 minute) + private final val timeout : FiniteDuration = timeout_time nanoseconds + + private final val DefaultClearer : Cancellable = new Cancellable() { + override def cancel : Boolean = true + override def isCancelled : Boolean = true + } + + final case class ObjectIsHacked(target : PlanetSideServerObject, zone : Zone, unk1 : Long, unk2 : Long, hacked_at_time : Long = System.nanoTime()) + + final case class ClearTheHack(door_guid : PlanetSideGUID, zone_id : String, unk1 : Long, unk2 : Long) + + private final case class HackEntry(target : PlanetSideServerObject, zone : Zone, unk1 : Long, unk2 : Long, hacked_at_time : Long) + + private final case class TryClearHacks() +} diff --git a/pslogin/src/main/scala/AvatarService.scala b/pslogin/src/main/scala/AvatarService.scala index 0287f244..5905366f 100644 --- a/pslogin/src/main/scala/AvatarService.scala +++ b/pslogin/src/main/scala/AvatarService.scala @@ -1,7 +1,6 @@ // Copyright (c) 2017 PSForever import akka.actor.Actor import net.psforever.objects.equipment.Equipment -import net.psforever.objects.serverobject.PlanetSideServerObject import net.psforever.packet.game.objectcreate.ConstructorData import net.psforever.types.ExoSuitType import net.psforever.packet.game.{PlanetSideGUID, PlayerStateMessageUpstream} @@ -14,7 +13,6 @@ object AvatarAction { //final case class DropItem(pos : Vector3, orient : Vector3, item : PlanetSideGUID) extends Action final case class EquipmentInHand(player_guid : PlanetSideGUID, slot : Int, item : Equipment) extends Action final case class EquipmentOnGround(player_guid : PlanetSideGUID, pos : Vector3, orient : Vector3, item : Equipment) extends Action - final case class Hack(player_guid : PlanetSideGUID, target : PlanetSideServerObject, unk1 : Long, unk2 : Long = 8L) extends Action final case class LoadPlayer(player_guid : PlanetSideGUID, pdata : ConstructorData) extends Action // final case class LoadMap(msg : PlanetSideGUID) extends Action // final case class unLoadMap(msg : PlanetSideGUID) extends Action @@ -36,7 +34,6 @@ object AvatarServiceResponse { //final case class DropItem(pos : Vector3, orient : Vector3, item : PlanetSideGUID) extends Response final case class EquipmentInHand(slot : Int, item : Equipment) extends Response final case class EquipmentOnGround(pos : Vector3, orient : Vector3, item : Equipment) extends Response - final case class Hack(target_guid : PlanetSideGUID, unk1 : Long, unk2 : Long = 8L) extends Response final case class LoadPlayer(pdata : ConstructorData) extends Response // final case class unLoadMap() extends Response // final case class LoadMap() extends Response @@ -96,10 +93,6 @@ class AvatarService extends Actor { AvatarEvents.publish( AvatarServiceResponse(s"/$forChannel/Avatar", player_guid, AvatarServiceResponse.EquipmentOnGround(pos, orient, obj)) ) - case AvatarAction.Hack(player_guid, target, unk1, unk2) => - AvatarEvents.publish( - AvatarServiceResponse(s"/$forChannel/Avatar", player_guid, AvatarServiceResponse.Hack(target.GUID, unk1, unk2)) - ) case AvatarAction.LoadPlayer(player_guid, pdata) => AvatarEvents.publish( AvatarServiceResponse(s"/$forChannel/Avatar", player_guid, AvatarServiceResponse.LoadPlayer(pdata)) diff --git a/pslogin/src/main/scala/LocalService.scala b/pslogin/src/main/scala/LocalService.scala index ae014ab2..165016ef 100644 --- a/pslogin/src/main/scala/LocalService.scala +++ b/pslogin/src/main/scala/LocalService.scala @@ -1,7 +1,8 @@ // Copyright (c) 2017 PSForever import akka.actor.{Actor, Props} +import net.psforever.objects.serverobject.PlanetSideServerObject import net.psforever.objects.serverobject.doors.Door -import net.psforever.objects.zones.{DoorCloseActor, Zone} +import net.psforever.objects.zones.{DoorCloseActor, HackClearActor, Zone} import net.psforever.packet.game.PlanetSideGUID object LocalAction { @@ -9,6 +10,8 @@ object LocalAction { final case class DoorOpens(player_guid : PlanetSideGUID, continent : Zone, door : Door) extends Action final case class DoorCloses(player_guid : PlanetSideGUID, door_guid : PlanetSideGUID) extends Action + final case class HackClear(player_guid : PlanetSideGUID, target : PlanetSideServerObject, unk1 : Long, unk2 : Long = 8L) extends Action + final case class HackTemporarily(player_guid : PlanetSideGUID, continent : Zone, target : PlanetSideServerObject, unk1 : Long, unk2 : Long = 8L) extends Action } object LocalServiceResponse { @@ -16,6 +19,8 @@ object LocalServiceResponse { final case class DoorOpens(door_guid : PlanetSideGUID) extends Response final case class DoorCloses(door_guid : PlanetSideGUID) extends Response + final case class HackClear(target_guid : PlanetSideGUID, unk1 : Long, unk2 : Long) extends Response + final case class HackObject(target_guid : PlanetSideGUID, unk1 : Long, unk2 : Long) extends Response } final case class LocalServiceMessage(forChannel : String, actionMessage : LocalAction.Action) @@ -29,6 +34,7 @@ final case class LocalServiceResponse(toChannel : String, avatar_guid : PlanetSi class LocalService extends Actor { //import LocalService._ private val doorCloser = context.actorOf(Props[DoorCloseActor], "local-door-closer") + private val hackClearer = context.actorOf(Props[HackClearActor], "local-hack-clearer") private [this] val log = org.log4s.getLogger override def preStart = { @@ -55,21 +61,39 @@ class LocalService extends Actor { LocalEvents.publish( LocalServiceResponse(s"/$forChannel/LocalEnvironment", player_guid, LocalServiceResponse.DoorOpens(door.GUID)) ) - case LocalAction.DoorCloses(player_guid, door_guid) => LocalEvents.publish( LocalServiceResponse(s"/$forChannel/LocalEnvironment", player_guid, LocalServiceResponse.DoorCloses(door_guid)) ) + case LocalAction.HackClear(player_guid, target, unk1, unk2) => + LocalEvents.publish( + LocalServiceResponse(s"/$forChannel/LocalEnvironment", player_guid, LocalServiceResponse.HackClear(target.GUID, unk1, unk2)) + ) + case LocalAction.HackTemporarily(player_guid, zone, target, unk1, unk2) => + hackClearer ! HackClearActor.ObjectIsHacked(target, zone, unk1, unk2) + LocalEvents.publish( + LocalServiceResponse(s"/$forChannel/Avatar", player_guid, LocalServiceResponse.HackObject(target.GUID, unk1, unk2)) + ) case _ => ; } //response from DoorCloseActor case DoorCloseActor.CloseTheDoor(door_guid, zone_id) => LocalEvents.publish( - LocalServiceResponse(s"/$zone_id/LocalEnvironment", PlanetSideGUID(0), LocalServiceResponse.DoorCloses(door_guid)) + LocalServiceResponse(s"/$zone_id/LocalEnvironment", LocalService.defaultPlayerGUID, LocalServiceResponse.DoorCloses(door_guid)) + ) + + //response from HackClearActor + case HackClearActor.ClearTheHack(target_guid, zone_id, unk1, unk2) => + LocalEvents.publish( + LocalServiceResponse(s"/$zone_id/LocalEnvironment", LocalService.defaultPlayerGUID, LocalServiceResponse.HackClear(target_guid, unk1, unk2)) ) case msg => log.info(s"Unhandled message $msg from $sender") } } + +object LocalService { + final val defaultPlayerGUID : PlanetSideGUID = PlanetSideGUID(0) +} diff --git a/pslogin/src/main/scala/WorldSessionActor.scala b/pslogin/src/main/scala/WorldSessionActor.scala index 559ea213..50e8a59a 100644 --- a/pslogin/src/main/scala/WorldSessionActor.scala +++ b/pslogin/src/main/scala/WorldSessionActor.scala @@ -144,11 +144,6 @@ class WorldSessionActor extends Actor with MDCContextAware { ) } - case AvatarServiceResponse.Hack(target_guid, unk1, unk2) => - if(player.GUID != guid) { - sendResponse(PacketCoding.CreateGamePacket(0, HackMessage(0, target_guid.guid, guid.guid, 100, unk1, 6, unk2))) - } - case AvatarServiceResponse.LoadPlayer(pdata) => if(player.GUID != guid) { sendResponse( @@ -232,6 +227,15 @@ class WorldSessionActor extends Actor with MDCContextAware { case LocalServiceResponse.DoorCloses(door_guid) => //door closes for everyone sendResponse(PacketCoding.CreateGamePacket(0, GenericObjectStateMsg(door_guid, 17))) + + case LocalServiceResponse.HackClear(target_guid, unk1, unk2) => + log.info(s"Clear hack of $target_guid") + sendResponse(PacketCoding.CreateGamePacket(0, HackMessage(0, target_guid.guid, guid.guid, 0, unk1, 7, unk2))) + + case LocalServiceResponse.HackObject(target_guid, unk1, unk2) => + if(player.GUID != guid) { + sendResponse(PacketCoding.CreateGamePacket(0, HackMessage(0, target_guid.guid, guid.guid, 100, unk1, 6, unk2))) + } } case Door.DoorMessage(tplayer, msg, order) => @@ -241,7 +245,7 @@ class WorldSessionActor extends Actor with MDCContextAware { continent.GUID(door_guid) match { case Some(door : Door) => sendResponse(PacketCoding.CreateGamePacket(0, GenericObjectStateMsg(door_guid, 16))) - localService ! LocalServiceMessage (continent.Id, LocalAction.DoorOpens (tplayer.GUID, continent, door) ) + localService ! LocalServiceMessage(continent.Id, LocalAction.DoorOpens (tplayer.GUID, continent, door) ) case _ => log.warn(s"door $door_guid wanted to be opened but could not be found") @@ -578,7 +582,7 @@ class WorldSessionActor extends Actor with MDCContextAware { continent.Actor ! Zone.DropItemOnGround(item, item.Position, item.Orientation) //restore } - case ItemHacking(tplayer, target, tool_guid, delta) => + case ItemHacking(tplayer, target, tool_guid, delta, completeAction, tickAction) => progressBarUpdate.cancel if(progressBarValue.isDefined) { val progressBarVal : Float = progressBarValue.get + delta @@ -594,17 +598,16 @@ class WorldSessionActor extends Actor with MDCContextAware { sendResponse(PacketCoding.CreateGamePacket(0, HackMessage(1, target.GUID.guid, player.GUID.guid, progressBarVal.toInt, 0L, vis, 8L))) if(progressBarVal > 100) { progressBarValue = None - log.info(s"We've hacked the item $target! Now what?") - sendResponse(PacketCoding.CreateGamePacket(0, HackMessage(0, target.GUID.guid, player.GUID.guid, 100, 1114636288, 6, 8L))) - target.Actor ! CommonMessages.Hack(tplayer) - avatarService ! AvatarServiceMessage(tplayer.Continent, AvatarAction.Hack(tplayer.GUID, target, 1114636288)) - //TODO now what? + log.info(s"Hacked a $target") + sendResponse(PacketCoding.CreateGamePacket(0, HackMessage(0, target.GUID.guid, player.GUID.guid, 100, 1114636288L, 6, 8L))) + completeAction() } else { + tickAction.getOrElse(() => Unit)() progressBarValue = Some(progressBarVal) import scala.concurrent.duration._ import scala.concurrent.ExecutionContext.Implicits.global - progressBarUpdate = context.system.scheduler.scheduleOnce(250 milliseconds, self, ItemHacking(tplayer, target, tool_guid, delta)) + progressBarUpdate = context.system.scheduler.scheduleOnce(250 milliseconds, self, ItemHacking(tplayer, target, tool_guid, delta, completeAction)) } } @@ -1052,7 +1055,7 @@ class WorldSessionActor extends Actor with MDCContextAware { case Some(tool : SimpleItem) => if(tool.Definition == GlobalDefinitions.remote_electronics_kit) { progressBarValue = Some(-2.66f) - self ! WorldSessionActor.ItemHacking(player, panel, tool.GUID, 2.66f) + self ! WorldSessionActor.ItemHacking(player, panel, tool.GUID, 2.66f, HackTemporary(panel)) } case _ => ; } @@ -1622,6 +1625,11 @@ class WorldSessionActor extends Actor with MDCContextAware { } } + private def HackTemporary(target : PlanetSideServerObject)() : Unit = { + target.Actor ! CommonMessages.Hack(player) + localService ! LocalServiceMessage(player.Continent, LocalAction.HackTemporarily(player.GUID, continent, target, 1114636288L)) + } + def failWithError(error : String) = { log.error(error) sendResponse(PacketCoding.CreateControlPacket(ConnectionClose())) @@ -1652,8 +1660,12 @@ object WorldSessionActor { private final case class PlayerFailedToLoad(tplayer : Player) private final case class ListAccountCharacters() private final case class SetCurrentAvatar(tplayer : Player) - private final case class ItemHacking(tplayer : Player, target : PlanetSideServerObject, tool_guid : PlanetSideGUID, delta : Float) - + private final case class ItemHacking(tplayer : Player, + target : PlanetSideServerObject, + tool_guid : PlanetSideGUID, + delta : Float, + completeAction : () => Unit, + tickAction : Option[() => Unit] = None) /** * A placeholder `Cancellable` object. */ From d5f40a3d5f7909bf7d21d726ce514cc6e68f0acb Mon Sep 17 00:00:00 2001 From: FateJH Date: Fri, 13 Oct 2017 14:57:28 -0400 Subject: [PATCH 16/23] comments and documentation, mainly; adjusted the list splitting functionality in the LocalService support Actors --- .../psforever/objects/GlobalDefinitions.scala | 2 +- .../scala/net/psforever/objects/Player.scala | 2 - .../objects/serverobject/CommonMessages.scala | 3 +- .../serverobject/PlanetSideServerObject.scala | 14 +++ .../builders/IFFLockObjectBuilder.scala | 6 +- .../builders/ServerObjectBuilder.scala | 2 + .../objects/serverobject/doors/Base.scala | 4 + .../objects/serverobject/doors/Door.scala | 38 ++++-- .../serverobject/doors/DoorControl.scala | 9 +- .../serverobject/doors/DoorDefinition.scala | 2 +- .../objects/serverobject/locks/IFFLock.scala | 51 ++++++-- .../serverobject/locks/IFFLockControl.scala | 18 +-- .../locks/IFFLockDefinition.scala | 6 +- .../terminals/TemporaryTerminalMessages.scala | 16 --- .../serverobject/terminals/Terminal.scala | 55 ++++----- .../terminals/TerminalControl.scala | 10 +- .../objects/zones/DoorCloseActor.scala | 109 +++++++++++++----- .../objects/zones/HackClearActor.scala | 106 ++++++++++++----- .../psforever/packet/game/HackMessage.scala | 86 ++++++++++---- .../src/test/scala/game/HackMessageTest.scala | 12 +- pslogin/src/main/scala/PsLogin.scala | 4 +- .../src/main/scala/WorldSessionActor.scala | 64 ++++++---- 22 files changed, 408 insertions(+), 211 deletions(-) delete mode 100644 common/src/main/scala/net/psforever/objects/serverobject/terminals/TemporaryTerminalMessages.scala diff --git a/common/src/main/scala/net/psforever/objects/GlobalDefinitions.scala b/common/src/main/scala/net/psforever/objects/GlobalDefinitions.scala index 13073dad..6d961e8f 100644 --- a/common/src/main/scala/net/psforever/objects/GlobalDefinitions.scala +++ b/common/src/main/scala/net/psforever/objects/GlobalDefinitions.scala @@ -1245,7 +1245,7 @@ object GlobalDefinitions { cert_terminal = new CertTerminalDefinition val - external_lock = new IFFLockDefinition + lock_external = new IFFLockDefinition val door = new DoorDefinition } diff --git a/common/src/main/scala/net/psforever/objects/Player.scala b/common/src/main/scala/net/psforever/objects/Player.scala index 9d59f733..a91cc0ff 100644 --- a/common/src/main/scala/net/psforever/objects/Player.scala +++ b/common/src/main/scala/net/psforever/objects/Player.scala @@ -64,8 +64,6 @@ class Player(private val name : String, /** Last medkituse. */ var lastMedkit : Long = 0 var death_by : Int = 0 - var doors : Array[Int] = Array.ofDim(120) - var doorsTime : Array[Long] = Array.ofDim(120) var lastSeenStreamMessage : Array[Long] = Array.fill[Long](65535)(0L) var lastShotSeq_time : Int = -1 /** The player is shooting. */ diff --git a/common/src/main/scala/net/psforever/objects/serverobject/CommonMessages.scala b/common/src/main/scala/net/psforever/objects/serverobject/CommonMessages.scala index 8f4de61c..c73b7a23 100644 --- a/common/src/main/scala/net/psforever/objects/serverobject/CommonMessages.scala +++ b/common/src/main/scala/net/psforever/objects/serverobject/CommonMessages.scala @@ -2,9 +2,8 @@ package net.psforever.objects.serverobject import net.psforever.objects.Player -import net.psforever.types.PlanetSideEmpire -//temporary location for these temporary messages +//temporary location for these messages object CommonMessages { final case class Hack(player : Player) final case class ClearHack() diff --git a/common/src/main/scala/net/psforever/objects/serverobject/PlanetSideServerObject.scala b/common/src/main/scala/net/psforever/objects/serverobject/PlanetSideServerObject.scala index 3a0e71ec..f4497544 100644 --- a/common/src/main/scala/net/psforever/objects/serverobject/PlanetSideServerObject.scala +++ b/common/src/main/scala/net/psforever/objects/serverobject/PlanetSideServerObject.scala @@ -4,11 +4,25 @@ package net.psforever.objects.serverobject import akka.actor.ActorRef import net.psforever.objects.PlanetSideGameObject +/** + * An object layered on top of the standard game object class that maintains an internal `ActorRef`. + * A measure of synchronization can be managed using this `Actor`. + */ abstract class PlanetSideServerObject extends PlanetSideGameObject { private var actor = ActorRef.noSender + /** + * Retrieve a reference to the internal `Actor`. + * @return the internal `ActorRef` + */ def Actor : ActorRef = actor + /** + * Assign an `Actor` to act for this server object. + * This reference is only set once, that is, as long as the internal `ActorRef` directs to `Actor.noSender` (`null`). + * @param control the `Actor` whose functionality will govern this server object + * @return the current internal `ActorRef` + */ def Actor_=(control : ActorRef) : ActorRef = { if(actor == ActorRef.noSender) { actor = control diff --git a/common/src/main/scala/net/psforever/objects/serverobject/builders/IFFLockObjectBuilder.scala b/common/src/main/scala/net/psforever/objects/serverobject/builders/IFFLockObjectBuilder.scala index e954eea4..26834d93 100644 --- a/common/src/main/scala/net/psforever/objects/serverobject/builders/IFFLockObjectBuilder.scala +++ b/common/src/main/scala/net/psforever/objects/serverobject/builders/IFFLockObjectBuilder.scala @@ -5,8 +5,8 @@ import akka.actor.Props import net.psforever.objects.serverobject.locks.{IFFLock, IFFLockControl, IFFLockDefinition} /** - * Wrapper `Class` designed to instantiate a `Door` server object. - * @param idef a `IFFLockDefinition` object, indicating the specific functionality of the resulting `Door` + * Wrapper `Class` designed to instantiate a door lock server object that is sensitive to user faction affiliation. + * @param idef a `IFFLockDefinition` object, indicating the specific functionality * @param id the globally unique identifier to which this `IFFLock` will be registered */ class IFFLockObjectBuilder(private val idef : IFFLockDefinition, private val id : Int) extends ServerObjectBuilder[IFFLock] { @@ -14,7 +14,7 @@ class IFFLockObjectBuilder(private val idef : IFFLockDefinition, private val id import net.psforever.objects.guid.NumberPoolHub def Build(implicit context : ActorContext, guid : NumberPoolHub) : IFFLock = { - val obj = IFFLock() + val obj = IFFLock(idef) guid.register(obj, id) //non-Actor GUID registration obj.Actor = context.actorOf(Props(classOf[IFFLockControl], obj), s"${idef.Name}_${obj.GUID.guid}") obj diff --git a/common/src/main/scala/net/psforever/objects/serverobject/builders/ServerObjectBuilder.scala b/common/src/main/scala/net/psforever/objects/serverobject/builders/ServerObjectBuilder.scala index 5b412814..e1bb0ce7 100644 --- a/common/src/main/scala/net/psforever/objects/serverobject/builders/ServerObjectBuilder.scala +++ b/common/src/main/scala/net/psforever/objects/serverobject/builders/ServerObjectBuilder.scala @@ -7,8 +7,10 @@ import net.psforever.objects.guid.NumberPoolHub /** * Wrapper `Trait` designed to be extended to implement custom object instantiation logic at the `ZoneMap` level. + * @tparam A any object that extends from PlanetSideGameObject * @see `Zone.Init` */ +//TODO can we changed PlanetSideGameObject -> PlanetSideServerObject? trait ServerObjectBuilder[A <: PlanetSideGameObject] { /** * Instantiate and configure the given server object diff --git a/common/src/main/scala/net/psforever/objects/serverobject/doors/Base.scala b/common/src/main/scala/net/psforever/objects/serverobject/doors/Base.scala index 17c590a7..90e6c490 100644 --- a/common/src/main/scala/net/psforever/objects/serverobject/doors/Base.scala +++ b/common/src/main/scala/net/psforever/objects/serverobject/doors/Base.scala @@ -3,6 +3,10 @@ package net.psforever.objects.serverobject.doors import net.psforever.types.PlanetSideEmpire +/** + * A temporary class to represent "facilities" and "structures." + * @param id the map id of the base + */ class Base(private val id : Int) { private var faction : PlanetSideEmpire.Value = PlanetSideEmpire.NEUTRAL diff --git a/common/src/main/scala/net/psforever/objects/serverobject/doors/Door.scala b/common/src/main/scala/net/psforever/objects/serverobject/doors/Door.scala index 5971fbaa..ae63affd 100644 --- a/common/src/main/scala/net/psforever/objects/serverobject/doors/Door.scala +++ b/common/src/main/scala/net/psforever/objects/serverobject/doors/Door.scala @@ -6,10 +6,10 @@ import net.psforever.objects.Player import net.psforever.packet.game.UseItemMessage /** - * na + * A structure-owned server object that is a "door" that can open and can close. * @param ddef the `ObjectDefinition` that constructs this object and maintains some of its immutable fields */ -class Door(ddef : DoorDefinition) extends PlanetSideServerObject { +class Door(private val ddef : DoorDefinition) extends PlanetSideServerObject { private var openState : Boolean = false private var lockState : Boolean = false @@ -45,26 +45,46 @@ class Door(ddef : DoorDefinition) extends PlanetSideServerObject { } object Door { + /** + * Entry message into this `Door` that carries the request. + * @param player the player who sent this request message + * @param msg the original packet carrying the request + */ final case class Use(player : Player, msg : UseItemMessage) + /** + * A basic `Trait` connecting all of the actionable `Door` response messages. + */ sealed trait Exchange + /** + * Message that carries the result of the processed request message back to the original user (`player`). + * @param player the player who sent this request message + * @param msg the original packet carrying the request + * @param response the result of the processed request + */ final case class DoorMessage(player : Player, msg : UseItemMessage, response : Exchange) + /** + * This door will open. + */ final case class OpenEvent() extends Exchange + /** + * This door will close. + */ final case class CloseEvent() extends Exchange + /** + * This door will do nothing. + */ final case class NoEvent() extends Exchange + /** + * Overloaded constructor. + * @param tdef the `ObjectDefinition` that constructs this object and maintains some of its immutable fields + */ def apply(tdef : DoorDefinition) : Door = { new Door(tdef) } - - import net.psforever.packet.game.PlanetSideGUID - def apply(guid : PlanetSideGUID, ddef : DoorDefinition) : Door = { - val obj = new Door(ddef) - obj.GUID = guid - obj - } } diff --git a/common/src/main/scala/net/psforever/objects/serverobject/doors/DoorControl.scala b/common/src/main/scala/net/psforever/objects/serverobject/doors/DoorControl.scala index ac3832f0..8f116331 100644 --- a/common/src/main/scala/net/psforever/objects/serverobject/doors/DoorControl.scala +++ b/common/src/main/scala/net/psforever/objects/serverobject/doors/DoorControl.scala @@ -1,7 +1,7 @@ // Copyright (c) 2017 PSForever package net.psforever.objects.serverobject.doors -import akka.actor.{Actor, Cancellable} +import akka.actor.Actor /** * An `Actor` that handles messages being dispatched to a specific `Door`. @@ -16,10 +16,3 @@ class DoorControl(door : Door) extends Actor { sender ! Door.NoEvent() } } - -object DoorControl { - final val DefaultCloser : Cancellable = new Cancellable() { - override def cancel : Boolean = true - override def isCancelled : Boolean = true - } -} diff --git a/common/src/main/scala/net/psforever/objects/serverobject/doors/DoorDefinition.scala b/common/src/main/scala/net/psforever/objects/serverobject/doors/DoorDefinition.scala index 959aa6d2..6a22670c 100644 --- a/common/src/main/scala/net/psforever/objects/serverobject/doors/DoorDefinition.scala +++ b/common/src/main/scala/net/psforever/objects/serverobject/doors/DoorDefinition.scala @@ -4,7 +4,7 @@ package net.psforever.objects.serverobject.doors import net.psforever.objects.definition.ObjectDefinition /** - * The definition for any `door`. + * The definition for any `Door`. * Object Id 242 is a generic door. */ class DoorDefinition extends ObjectDefinition(242) { diff --git a/common/src/main/scala/net/psforever/objects/serverobject/locks/IFFLock.scala b/common/src/main/scala/net/psforever/objects/serverobject/locks/IFFLock.scala index e7dad76c..e08587e8 100644 --- a/common/src/main/scala/net/psforever/objects/serverobject/locks/IFFLock.scala +++ b/common/src/main/scala/net/psforever/objects/serverobject/locks/IFFLock.scala @@ -1,39 +1,66 @@ // Copyright (c) 2017 PSForever package net.psforever.objects.serverobject.locks -import net.psforever.objects.{GlobalDefinitions, Player} +import net.psforever.objects.Player import net.psforever.objects.serverobject.PlanetSideServerObject -import net.psforever.types.{PlanetSideEmpire, Vector3} +import net.psforever.packet.game.PlanetSideGUID +import net.psforever.types.Vector3 -class IFFLock extends PlanetSideServerObject { - private var hackedBy : Option[(Player, Vector3)] = None +/** + * A structure-owned server object that is a "door lock."
+ *
+ * The "door lock" exerts an "identify friend or foe" field that detects the faction affiliation of a target player. + * It also indirectly inherits faction affiliation from the structure to which it is connected + * or it can be "hacked" whereupon the person exploiting it leaves their "faction" as the aforementioned affiliated faction. + * The `IFFLock` is ideally associated with a server map object - a `Door` - to which it acts as a gatekeeper. + * @param idef the `ObjectDefinition` that constructs this object and maintains some of its immutable fields + */ +class IFFLock(private val idef : IFFLockDefinition) extends PlanetSideServerObject { + /** + * An entry that maintains a reference to the `Player`, and the player's GUID and location when the message was received. + */ + private var hackedBy : Option[(Player, PlanetSideGUID, Vector3)] = None - def HackedBy : Option[(Player, Vector3)] = hackedBy + def HackedBy : Option[(Player, PlanetSideGUID, Vector3)] = hackedBy - def HackedBy_=(agent : Player) : Option[(Player, Vector3)] = HackedBy_=(Some(agent)) + def HackedBy_=(agent : Player) : Option[(Player, PlanetSideGUID, Vector3)] = HackedBy_=(Some(agent)) - def HackedBy_=(agent : Option[Player]) : Option[(Player, Vector3)] = { + /** + * Set the hack state of this object by recording important information about the player that caused it. + * Set the hack state if there is no current hack state. + * Override the hack state with a new hack state if the new user has different faction affiliation. + * @param agent a `Player`, or no player + * @return the player hack entry + */ + def HackedBy_=(agent : Option[Player]) : Option[(Player, PlanetSideGUID, Vector3)] = { hackedBy match { case None => + //set the hack state if there is no current hack state if(agent.isDefined) { - hackedBy = Some(agent.get, agent.get.Position) + hackedBy = Some(agent.get, agent.get.GUID, agent.get.Position) } case Some(_) => + //clear the hack state if(agent.isEmpty) { hackedBy = None } + //override the hack state with a new hack state if the new user has different faction affiliation else if(agent.get.Faction != hackedBy.get._1.Faction) { - hackedBy = Some(agent.get, agent.get.Position) //overwrite + hackedBy = Some(agent.get, agent.get.GUID, agent.get.Position) } } HackedBy } - def Definition : IFFLockDefinition = GlobalDefinitions.external_lock + def Definition : IFFLockDefinition = idef } object IFFLock { - def apply() : IFFLock = { - new IFFLock + /** + * Overloaded constructor. + * @param idef the `ObjectDefinition` that constructs this object and maintains some of its immutable fields + */ + def apply(idef : IFFLockDefinition) : IFFLock = { + new IFFLock(idef) } } diff --git a/common/src/main/scala/net/psforever/objects/serverobject/locks/IFFLockControl.scala b/common/src/main/scala/net/psforever/objects/serverobject/locks/IFFLockControl.scala index e96d1bd3..bf9d1b81 100644 --- a/common/src/main/scala/net/psforever/objects/serverobject/locks/IFFLockControl.scala +++ b/common/src/main/scala/net/psforever/objects/serverobject/locks/IFFLockControl.scala @@ -1,22 +1,22 @@ // Copyright (c) 2017 PSForever package net.psforever.objects.serverobject.locks -import akka.actor.{Actor, Cancellable} +import akka.actor.Actor import net.psforever.objects.serverobject.CommonMessages +/** + * An `Actor` that handles messages being dispatched to a specific `IFFLock`. + * @param lock the `IFFLock` object being governed + * @see `CommonMessages` + */ class IFFLockControl(lock : IFFLock) extends Actor { def receive : Receive = { case CommonMessages.Hack(player) => lock.HackedBy = player + case CommonMessages.ClearHack() => lock.HackedBy = None - case _ => ; + + case _ => ; //no default message } } - -object IFFLockControl { - final val DefaultCloser : Cancellable = new Cancellable() { - override def cancel : Boolean = true - override def isCancelled : Boolean = true - } -} \ No newline at end of file diff --git a/common/src/main/scala/net/psforever/objects/serverobject/locks/IFFLockDefinition.scala b/common/src/main/scala/net/psforever/objects/serverobject/locks/IFFLockDefinition.scala index dcad4139..d8c180d8 100644 --- a/common/src/main/scala/net/psforever/objects/serverobject/locks/IFFLockDefinition.scala +++ b/common/src/main/scala/net/psforever/objects/serverobject/locks/IFFLockDefinition.scala @@ -3,6 +3,10 @@ package net.psforever.objects.serverobject.locks import net.psforever.objects.definition.ObjectDefinition -class IFFLockDefinition extends ObjectDefinition(0) { +/** + * The definition for any `IFFLock`. + * Object Id 451 is a generic external lock. + */ +class IFFLockDefinition extends ObjectDefinition(451) { Name = "iff_lock" } diff --git a/common/src/main/scala/net/psforever/objects/serverobject/terminals/TemporaryTerminalMessages.scala b/common/src/main/scala/net/psforever/objects/serverobject/terminals/TemporaryTerminalMessages.scala deleted file mode 100644 index 6d264df3..00000000 --- a/common/src/main/scala/net/psforever/objects/serverobject/terminals/TemporaryTerminalMessages.scala +++ /dev/null @@ -1,16 +0,0 @@ -// Copyright (c) 2017 PSForever -package net.psforever.objects.serverobject.terminals - -import net.psforever.objects.Player -import net.psforever.types.PlanetSideEmpire - -//temporary location for these temporary messages -object TemporaryTerminalMessages { - //TODO send original packets along with these messages - final case class UseItem(player : Player) - final case class Convert(faction : PlanetSideEmpire.Value) - final case class Hack(player : Player) - final case class ClearHack() - final case class Damaged(dm : Int) - final case class Repaired(rep : Int) -} diff --git a/common/src/main/scala/net/psforever/objects/serverobject/terminals/Terminal.scala b/common/src/main/scala/net/psforever/objects/serverobject/terminals/Terminal.scala index bccd245c..57532c54 100644 --- a/common/src/main/scala/net/psforever/objects/serverobject/terminals/Terminal.scala +++ b/common/src/main/scala/net/psforever/objects/serverobject/terminals/Terminal.scala @@ -5,48 +5,54 @@ import net.psforever.objects.Player import net.psforever.objects.equipment.Equipment import net.psforever.objects.inventory.InventoryItem import net.psforever.objects.serverobject.PlanetSideServerObject -import net.psforever.packet.game.ItemTransactionMessage -import net.psforever.types.{ExoSuitType, PlanetSideEmpire, TransactionType, Vector3} +import net.psforever.packet.game.{ItemTransactionMessage, PlanetSideGUID} +import net.psforever.types.{ExoSuitType, TransactionType, Vector3} /** - * na + * A structure-owned server object that is a "terminal" that can be accessed for amenities and services. * @param tdef the `ObjectDefinition` that constructs this object and maintains some of its immutable fields */ class Terminal(tdef : TerminalDefinition) extends PlanetSideServerObject { - //the following fields and related methods are neither finalized no integrated; GOTO Request - private var faction : PlanetSideEmpire.Value = PlanetSideEmpire.NEUTRAL - private var hackedBy : Option[(Player, Vector3)] = None - private var health : Int = 100 //TODO not real health value + /** + * An entry that maintains a reference to the `Player`, and the player's GUID and location when the message was received. + */ + private var hackedBy : Option[(Player, PlanetSideGUID, Vector3)] = None - def Faction : PlanetSideEmpire.Value = faction + def HackedBy : Option[(Player, PlanetSideGUID, Vector3)] = hackedBy - def HackedBy : Option[(Player, Vector3)] = hackedBy + def HackedBy_=(agent : Player) : Option[(Player, PlanetSideGUID, Vector3)] = HackedBy_=(Some(agent)) - def HackedBy_=(agent : Player) : Option[(Player, Vector3)] = HackedBy_=(Some(agent)) - - def HackedBy_=(agent : Option[Player]) : Option[(Player, Vector3)] = { + /** + * Set the hack state of this object by recording important information about the player that caused it. + * Set the hack state if there is no current hack state. + * Override the hack state with a new hack state if the new user has different faction affiliation. + * @param agent a `Player`, or no player + * @return the player hack entry + */ + def HackedBy_=(agent : Option[Player]) : Option[(Player, PlanetSideGUID, Vector3)] = { hackedBy match { case None => + //set the hack state if there is no current hack state if(agent.isDefined) { - hackedBy = Some(agent.get, agent.get.Position) + hackedBy = Some(agent.get, agent.get.GUID, agent.get.Position) } case Some(_) => + //clear the hack state if(agent.isEmpty) { hackedBy = None } + //override the hack state with a new hack state if the new user has different faction affiliation else if(agent.get.Faction != hackedBy.get._1.Faction) { - hackedBy = Some(agent.get, agent.get.Position) //overwrite + hackedBy = Some(agent.get, agent.get.GUID, agent.get.Position) } } HackedBy } - def Health : Int = health + //the following fields and related methods are neither finalized nor integrated; GOTO Request + private var health : Int = 100 //TODO not real health value - def Convert(toFaction : PlanetSideEmpire.Value) : Unit = { - hackedBy = None - faction = toFaction - } + def Health : Int = health def Damaged(dam : Int) : Unit = { health = Math.max(0, Health - dam) @@ -146,14 +152,11 @@ object Terminal { */ final case class InfantryLoadout(exosuit : ExoSuitType.Value, subtype : Int = 0, holsters : List[InventoryItem], inventory : List[InventoryItem]) extends Exchange + /** + * Overloaded constructor. + * @param tdef the `ObjectDefinition` that constructs this object and maintains some of its immutable fields + */ 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) - obj.GUID = guid - obj - } } diff --git a/common/src/main/scala/net/psforever/objects/serverobject/terminals/TerminalControl.scala b/common/src/main/scala/net/psforever/objects/serverobject/terminals/TerminalControl.scala index 8774245e..c74ada79 100644 --- a/common/src/main/scala/net/psforever/objects/serverobject/terminals/TerminalControl.scala +++ b/common/src/main/scala/net/psforever/objects/serverobject/terminals/TerminalControl.scala @@ -4,9 +4,7 @@ package net.psforever.objects.serverobject.terminals import akka.actor.Actor /** - * An `Actor` that handles messages being dispatched to a specific `Terminal`.
- *
- * For now, the only important message being managed is `Terminal.Request`. + * An `Actor` that handles messages being dispatched to a specific `Terminal`. * @param term the `Terminal` object being governed */ class TerminalControl(term : Terminal) extends Actor { @@ -14,12 +12,6 @@ class TerminalControl(term : Terminal) extends Actor { case Terminal.Request(player, msg) => sender ! Terminal.TerminalMessage(player, msg, term.Request(player, msg)) - case TemporaryTerminalMessages.Hack(player) => - term.HackedBy = player - - case TemporaryTerminalMessages.ClearHack() => - term.HackedBy = None - case _ => sender ! Terminal.NoDeal() } diff --git a/common/src/main/scala/net/psforever/objects/zones/DoorCloseActor.scala b/common/src/main/scala/net/psforever/objects/zones/DoorCloseActor.scala index b1604b75..4b2d9e9f 100644 --- a/common/src/main/scala/net/psforever/objects/zones/DoorCloseActor.scala +++ b/common/src/main/scala/net/psforever/objects/zones/DoorCloseActor.scala @@ -8,33 +8,38 @@ import net.psforever.packet.game.PlanetSideGUID import scala.annotation.tailrec import scala.concurrent.duration._ +/** + * Close an opened door after a certain amount of time has passed. + * This `Actor` is intended to sit on top of the event system that handles broadcast messaging regarding doors opening. + * @see `LocalService` + */ class DoorCloseActor() extends Actor { - import DoorCloseActor._ - private var doorCloserTrigger : Cancellable = DefaultCloser - private var openDoors : List[DoorEntry] = Nil + /** The periodic `Executor` that checks for doors to be closed */ + private var doorCloserTrigger : Cancellable = DoorCloseActor.DefaultCloser + /** A `List` of currently open doors */ + private var openDoors : List[DoorCloseActor.DoorEntry] = Nil //private[this] val log = org.log4s.getLogger def receive : Receive = { - case DoorIsOpen(door, zone, time) => - openDoors = openDoors :+ DoorEntry(door, zone, time) - if(openDoors.size == 1) { + case DoorCloseActor.DoorIsOpen(door, zone, time) => + openDoors = openDoors :+ DoorCloseActor.DoorEntry(door, zone, time) + if(openDoors.size == 1) { //we were the only entry so the event must be started from scratch import scala.concurrent.ExecutionContext.Implicits.global - doorCloserTrigger = context.system.scheduler.scheduleOnce(timeout, self, DoorCloseActor.TryCloseDoors()) + doorCloserTrigger = context.system.scheduler.scheduleOnce(DoorCloseActor.timeout, self, DoorCloseActor.TryCloseDoors()) } - case TryCloseDoors() => + case DoorCloseActor.TryCloseDoors() => doorCloserTrigger.cancel val now : Long = System.nanoTime - //TODO we can just walk across the list of doors and extract only the first few entries - val (doorsToClose, doorsLeftOpen) = recursivePartitionDoors(openDoors.iterator, now) + val (doorsToClose, doorsLeftOpen) = PartitionEntries(openDoors, now) openDoors = doorsLeftOpen doorsToClose.foreach(entry => { - entry.door.Open = false //permissible - context.parent ! DoorCloseActor.CloseTheDoor(entry.door.GUID, entry.zone.Id) + entry.door.Open = false //permissible break from synchronization + context.parent ! DoorCloseActor.CloseTheDoor(entry.door.GUID, entry.zone.Id) //call up to the main event system }) if(doorsLeftOpen.nonEmpty) { - val short_timeout : FiniteDuration = math.max(1, timeout_time - (now - doorsLeftOpen.head.opened_at_time)) nanoseconds + val short_timeout : FiniteDuration = math.max(1, DoorCloseActor.timeout_time - (now - doorsLeftOpen.head.time)) nanoseconds import scala.concurrent.ExecutionContext.Implicits.global doorCloserTrigger = context.system.scheduler.scheduleOnce(short_timeout, self, DoorCloseActor.TryCloseDoors()) } @@ -43,33 +48,53 @@ class DoorCloseActor() extends Actor { } /** - * na - * @param iter na - * @param now na - * @param list na + * Iterate over entries in a `List` until an entry that does not exceed the time limit is discovered. + * Separate the original `List` into two: + * a `List` of elements that have exceeded the time limit, + * and a `List` of elements that still satisfy the time limit. + * As newer entries to the `List` will always resolve later than old ones, + * and newer entries are always added to the end of the main `List`, + * processing in order is always correct. + * @param list the `List` of entries to divide + * @param now the time right now (in nanoseconds) * @see `List.partition` - * @return a `Tuple` of two `Lists`: - * the entries for all `Door`s that are closing, - * and the entries for all doors that are staying open + * @return a `Tuple` of two `Lists`, whose qualifications are explained above */ - @tailrec private def recursivePartitionDoors(iter : Iterator[DoorEntry], now : Long, list : List[DoorEntry] = Nil) : (List[DoorEntry], List[DoorEntry]) = { + private def PartitionEntries(list : List[DoorCloseActor.DoorEntry], now : Long) : (List[DoorCloseActor.DoorEntry], List[DoorCloseActor.DoorEntry]) = { + val n : Int = recursivePartitionEntries(list.iterator, now) + (list.take(n), list.drop(n)) //take and drop so to always return new lists + } + + /** + * Mark the index where the `List` of elements can be divided into two: + * a `List` of elements that have exceeded the time limit, + * and a `List` of elements that still satisfy the time limit. + * @param iter the `Iterator` of entries to divide + * @param now the time right now (in nanoseconds) + * @param index a persistent record of the index where list division should occur; + * defaults to 0 + * @return the index where division will occur + */ + @tailrec private def recursivePartitionEntries(iter : Iterator[DoorCloseActor.DoorEntry], now : Long, index : Int = 0) : Int = { if(!iter.hasNext) { - (list, iter.toList) + index } else { val entry = iter.next() - if(now - entry.opened_at_time >= timeout_time) { - recursivePartitionDoors(iter, now, list :+ entry) + if(now - entry.time >= DoorCloseActor.timeout_time) { + recursivePartitionEntries(iter, now, index + 1) } else { - (list, entry +: iter.toList) + index } } } } object DoorCloseActor { - private final val timeout_time : Long = 5000000000L //nanoseconds + /** The wait before an open door closes; as a Long for calculation simplicity */ + private final val timeout_time : Long = 5000000000L //nanoseconds (5s) + /** The wait before an open door closes; as a `FiniteDuration` for `Executor` simplicity */ private final val timeout : FiniteDuration = timeout_time nanoseconds private final val DefaultCloser : Cancellable = new Cancellable() { @@ -77,11 +102,33 @@ object DoorCloseActor { override def isCancelled : Boolean = true } - final case class DoorIsOpen(door : Door, zone : Zone, opened_at_time : Long = System.nanoTime()) - + /** + * Message that carries information about a door that has been opened. + * @param door the door object + * @param zone the zone in which the door resides + * @param time when the door was opened + * @see `DoorEntry` + */ + final case class DoorIsOpen(door : Door, zone : Zone, time : Long = System.nanoTime()) + /** + * Message that carries information about a door that needs to close. + * Prompting, as compared to `DoorIsOpen` which is reactionary. + * @param door_guid the door + * @param zone_id the zone in which the door resides + */ final case class CloseTheDoor(door_guid : PlanetSideGUID, zone_id : String) - - private final case class DoorEntry(door : Door, zone : Zone, opened_at_time : Long) - + /** + * Internal message used to signal a test of the queued door information. + */ private final case class TryCloseDoors() + + /** + * Entry of door information. + * The `zone` is maintained separately to ensure that any message resulting in an attempt to close doors is targetted. + * @param door the door object + * @param zone the zone in which the door resides + * @param time when the door was opened + * @see `DoorIsOpen` + */ + private final case class DoorEntry(door : Door, zone : Zone, time : Long) } diff --git a/common/src/main/scala/net/psforever/objects/zones/HackClearActor.scala b/common/src/main/scala/net/psforever/objects/zones/HackClearActor.scala index b595051e..2ec3cb05 100644 --- a/common/src/main/scala/net/psforever/objects/zones/HackClearActor.scala +++ b/common/src/main/scala/net/psforever/objects/zones/HackClearActor.scala @@ -8,33 +8,39 @@ import net.psforever.packet.game.PlanetSideGUID import scala.annotation.tailrec import scala.concurrent.duration._ +/** + * Restore original functionality to an object that has been hacked after a certain amount of time has passed. + * This `Actor` is intended to sit on top of the event system that handles broadcast messaging regarding hacking events. + * @see `LocalService` + */ class HackClearActor() extends Actor { - import HackClearActor._ - private var clearTrigger : Cancellable = DefaultClearer - private var hackedObjects : List[HackEntry] = Nil + /** The periodic `Executor` that checks for server objects to be unhacked */ + private var clearTrigger : Cancellable = HackClearActor.DefaultClearer + /** A `List` of currently hacked server objects */ + private var hackedObjects : List[HackClearActor.HackEntry] = Nil //private[this] val log = org.log4s.getLogger def receive : Receive = { - case ObjectIsHacked(door, zone, unk1, unk2, time) => - hackedObjects = hackedObjects :+ HackEntry(door, zone, unk1, unk2, time) - if(hackedObjects.size == 1) { + case HackClearActor.ObjectIsHacked(target, zone, unk1, unk2, time) => + hackedObjects = hackedObjects :+ HackClearActor.HackEntry(target, zone, unk1, unk2, time) + if(hackedObjects.size == 1) { //we were the only entry so the event must be started from scratch import scala.concurrent.ExecutionContext.Implicits.global - clearTrigger = context.system.scheduler.scheduleOnce(timeout, self, HackClearActor.TryClearHacks()) + clearTrigger = context.system.scheduler.scheduleOnce(HackClearActor.timeout, self, HackClearActor.TryClearHacks()) } - case TryClearHacks() => + case HackClearActor.TryClearHacks() => clearTrigger.cancel val now : Long = System.nanoTime //TODO we can just walk across the list of doors and extract only the first few entries - val (unhackObjects, stillHackedObjects) = recursivePartitionHacks(hackedObjects.iterator, now) + val (unhackObjects, stillHackedObjects) = PartitionEntries(hackedObjects, now) hackedObjects = stillHackedObjects unhackObjects.foreach(entry => { entry.target.Actor ! CommonMessages.ClearHack() - context.parent ! HackClearActor.ClearTheHack(entry.target.GUID, entry.zone.Id, entry.unk1, entry.unk2) + context.parent ! HackClearActor.ClearTheHack(entry.target.GUID, entry.zone.Id, entry.unk1, entry.unk2) //call up to the main event system }) if(stillHackedObjects.nonEmpty) { - val short_timeout : FiniteDuration = math.max(1, timeout_time - (now - stillHackedObjects.head.hacked_at_time)) nanoseconds + val short_timeout : FiniteDuration = math.max(1, HackClearActor.timeout_time - (now - stillHackedObjects.head.time)) nanoseconds import scala.concurrent.ExecutionContext.Implicits.global clearTrigger = context.system.scheduler.scheduleOnce(short_timeout, self, HackClearActor.TryClearHacks()) } @@ -43,33 +49,53 @@ class HackClearActor() extends Actor { } /** - * na - * @param iter na - * @param now na - * @param list na + * Iterate over entries in a `List` until an entry that does not exceed the time limit is discovered. + * Separate the original `List` into two: + * a `List` of elements that have exceeded the time limit, + * and a `List` of elements that still satisfy the time limit. + * As newer entries to the `List` will always resolve later than old ones, + * and newer entries are always added to the end of the main `List`, + * processing in order is always correct. + * @param list the `List` of entries to divide + * @param now the time right now (in nanoseconds) * @see `List.partition` - * @return a `Tuple` of two `Lists`: - * the entries for all objects that are no longer hacked, - * and the entries for all objects that are still hacked + * @return a `Tuple` of two `Lists`, whose qualifications are explained above */ - @tailrec private def recursivePartitionHacks(iter : Iterator[HackEntry], now : Long, list : List[HackEntry] = Nil) : (List[HackEntry], List[HackEntry]) = { + private def PartitionEntries(list : List[HackClearActor.HackEntry], now : Long) : (List[HackClearActor.HackEntry], List[HackClearActor.HackEntry]) = { + val n : Int = recursivePartitionEntries(list.iterator, now) + (list.take(n), list.drop(n)) //take and drop so to always return new lists + } + + /** + * Mark the index where the `List` of elements can be divided into two: + * a `List` of elements that have exceeded the time limit, + * and a `List` of elements that still satisfy the time limit. + * @param iter the `Iterator` of entries to divide + * @param now the time right now (in nanoseconds) + * @param index a persistent record of the index where list division should occur; + * defaults to 0 + * @return the index where division will occur + */ + @tailrec private def recursivePartitionEntries(iter : Iterator[HackClearActor.HackEntry], now : Long, index : Int = 0) : Int = { if(!iter.hasNext) { - (list, iter.toList) + index } else { val entry = iter.next() - if(now - entry.hacked_at_time >= timeout_time) { - recursivePartitionHacks(iter, now, list :+ entry) + if(now - entry.time >= HackClearActor.timeout_time) { + recursivePartitionEntries(iter, now, index + 1) } else { - (list, entry +: iter.toList) + index } } } } object HackClearActor { - private final val timeout_time : Long = 60000000000L //nanoseconds (1 minute) + /** The wait before a server object is to unhack; as a Long for calculation simplicity */ + private final val timeout_time : Long = 60000000000L //nanoseconds (60s) + /** The wait before a server object is to unhack; as a `FiniteDuration` for `Executor` simplicity */ private final val timeout : FiniteDuration = timeout_time nanoseconds private final val DefaultClearer : Cancellable = new Cancellable() { @@ -77,11 +103,33 @@ object HackClearActor { override def isCancelled : Boolean = true } - final case class ObjectIsHacked(target : PlanetSideServerObject, zone : Zone, unk1 : Long, unk2 : Long, hacked_at_time : Long = System.nanoTime()) - + /** + * Message that carries information about a server object that has been hacked. + * @param target the server object + * @param zone the zone in which the object resides + * @param time when the object was hacked + * @see `HackEntry` + */ + final case class ObjectIsHacked(target : PlanetSideServerObject, zone : Zone, unk1 : Long, unk2 : Long, time : Long = System.nanoTime()) + /** + * Message that carries information about a server object that needs its functionality restored. + * Prompting, as compared to `ObjectIsHacked` which is reactionary. + * @param door_guid the server object + * @param zone_id the zone in which the object resides + */ final case class ClearTheHack(door_guid : PlanetSideGUID, zone_id : String, unk1 : Long, unk2 : Long) - - private final case class HackEntry(target : PlanetSideServerObject, zone : Zone, unk1 : Long, unk2 : Long, hacked_at_time : Long) - + /** + * Internal message used to signal a test of the queued door information. + */ private final case class TryClearHacks() + + /** + * Entry of hacked server object information. + * The `zone` is maintained separately to ensure that any message resulting in an attempt to close doors is targetted. + * @param target the server object + * @param zone the zone in which the object resides + * @param time when the object was hacked + * @see `ObjectIsHacked` + */ + private final case class HackEntry(target : PlanetSideServerObject, zone : Zone, unk1 : Long, unk2 : Long, time : Long) } diff --git a/common/src/main/scala/net/psforever/packet/game/HackMessage.scala b/common/src/main/scala/net/psforever/packet/game/HackMessage.scala index ae3c0451..2225033e 100644 --- a/common/src/main/scala/net/psforever/packet/game/HackMessage.scala +++ b/common/src/main/scala/net/psforever/packet/game/HackMessage.scala @@ -6,21 +6,65 @@ import scodec.Codec import scodec.codecs._ /** - * - * @param unk1 na - * @param unk2 na - * @param unk3 na - * @param unk4 na - * @param unk5 na - * @param unk6 na - * @param unk7 na - */ + * An `Enumeration` of the various states and activities of the hacking process. + * These values are closely tied to the condition of the hacking progress bar and/or the condition of the hacked object.
+ *
+ * `Start` initially displays the hacking progress bar.
+ * `Ongoing` is a neutral state that keeps the progress bar displayed while its value updates. (unconfirmed?)
+ * `Finished` disposes of the hacking progress bar. It does not, by itself, mean the hack was successful.
+ * `Hacked` modifies the target of the hack.
+ * `HackCleared` modifies the target of the hack, opposite of `Hacked`. + */ +object HackState extends Enumeration { + type Type = Value + + val + Unknown0, + Start, + Unknown2, + Ongoing, + Finished, + Unknown5, + Hacked, + HackCleared + = Value + + implicit val codec = PacketHelpers.createEnumerationCodec(this, uint8L) +} + +/** + * Dispatched by the server to control the process of hacking.
+ *
+ * Part of the hacking process is regulated by the server while another part of it is automatically reset by the client. + * The visibility, update, and closing of the hacking progress bar must be handled manually, for each tick. + * When hacking is complete, using the appropriate `HackState` will cue the target to be affected by the hack. + * Terminals and door IFF panels will temporarily expose their functionality; + * the faction association of vehicles will be converted permanently; + * a protracted process of a base conversion will be enacted; etc.. + * This transfer of faction association occurs to align the target with the faction of the hacking player (as indicated). + * The client will select the faction without needing to be explicitly told + * and will select the appropriate action to enact upon the target. + * Upon the hack's completion, the target on the client will automatically revert back to its original state, if possible. + * (It will still be necessary to alert this change from the server's perspective.) + * @param unk1 na; + * hack type? + * @param target_guid the target of the hack + * @param player_guid the player + * @param progress the amount of progress visible; + * visible range is 0 - 100 + * @param unk5 na; + * often a large number; + * doesn't seem to be `char_id`? + * @param hack_state hack state + * @param unk7 na; + * usually, 8? + */ final case class HackMessage(unk1 : Int, - unk2 : Int, - unk3 : Int, - unk4 : Int, + target_guid : PlanetSideGUID, + player_guid : PlanetSideGUID, + progress : Int, unk5 : Long, - unk6 : Int, + hack_state : HackState.Value, unk7 : Long) extends PlanetSideGamePacket { type Packet = HackMessage @@ -30,12 +74,12 @@ final case class HackMessage(unk1 : Int, object HackMessage extends Marshallable[HackMessage] { implicit val codec : Codec[HackMessage] = ( - ("unk1" | uint2L) :: - ("unk2" | uint16L) :: - ("unk3" | uint16L) :: - ("unk4" | uint8L) :: - ("unk5" | uint32L) :: - ("unk6" | uint8L) :: - ("unk7" | uint32L) - ).as[HackMessage] + ("unk1" | uint2L) :: + ("object_guid" | PlanetSideGUID.codec) :: + ("player_guid" | PlanetSideGUID.codec) :: + ("progress" | uint8L) :: + ("unk5" | uint32L) :: + ("hack_state" | HackState.codec) :: + ("unk7" | uint32L) + ).as[HackMessage] } diff --git a/common/src/test/scala/game/HackMessageTest.scala b/common/src/test/scala/game/HackMessageTest.scala index 8473e2c5..469950d1 100644 --- a/common/src/test/scala/game/HackMessageTest.scala +++ b/common/src/test/scala/game/HackMessageTest.scala @@ -12,13 +12,13 @@ class HackMessageTest extends Specification { "decode" in { PacketCoding.DecodePacket(string).require match { - case HackMessage(unk1, unk2, unk3, unk4, unk5, unk6, unk7) => + case HackMessage(unk1, target_guid, player_guid, progress, unk5, hack_state, unk7) => unk1 mustEqual 0 - unk2 mustEqual 1024 - unk3 mustEqual 3607 - unk4 mustEqual 0 + target_guid mustEqual PlanetSideGUID(1024) + player_guid mustEqual PlanetSideGUID(3607) + progress mustEqual 0 unk5 mustEqual 3212836864L - unk6 mustEqual 1 + hack_state mustEqual HackState.Start unk7 mustEqual 8L case _ => ko @@ -26,7 +26,7 @@ class HackMessageTest extends Specification { } "encode" in { - val msg = HackMessage(0,1024,3607,0,3212836864L,1,8L) + val msg = HackMessage(0, PlanetSideGUID(1024), PlanetSideGUID(3607), 0, 3212836864L, HackState.Start, 8L) val pkt = PacketCoding.EncodePacket(msg).require.toByteVector pkt mustEqual string } diff --git a/pslogin/src/main/scala/PsLogin.scala b/pslogin/src/main/scala/PsLogin.scala index de00fb1b..94551628 100644 --- a/pslogin/src/main/scala/PsLogin.scala +++ b/pslogin/src/main/scala/PsLogin.scala @@ -229,8 +229,8 @@ object PsLogin { LocalObject(DoorObjectBuilder(door, 332)) LocalObject(DoorObjectBuilder(door, 372)) LocalObject(DoorObjectBuilder(door, 373)) - LocalObject(IFFLockObjectBuilder(external_lock, 556)) - LocalObject(IFFLockObjectBuilder(external_lock, 558)) + LocalObject(IFFLockObjectBuilder(lock_external, 556)) + LocalObject(IFFLockObjectBuilder(lock_external, 558)) LocalObject(TerminalObjectBuilder(cert_terminal, 186)) LocalObject(TerminalObjectBuilder(cert_terminal, 187)) LocalObject(TerminalObjectBuilder(cert_terminal, 188)) diff --git a/pslogin/src/main/scala/WorldSessionActor.scala b/pslogin/src/main/scala/WorldSessionActor.scala index 50e8a59a..2ed2191f 100644 --- a/pslogin/src/main/scala/WorldSessionActor.scala +++ b/pslogin/src/main/scala/WorldSessionActor.scala @@ -230,11 +230,11 @@ class WorldSessionActor extends Actor with MDCContextAware { case LocalServiceResponse.HackClear(target_guid, unk1, unk2) => log.info(s"Clear hack of $target_guid") - sendResponse(PacketCoding.CreateGamePacket(0, HackMessage(0, target_guid.guid, guid.guid, 0, unk1, 7, unk2))) + sendResponse(PacketCoding.CreateGamePacket(0, HackMessage(0, target_guid, guid, 0, unk1, HackState.HackCleared, unk2))) case LocalServiceResponse.HackObject(target_guid, unk1, unk2) => if(player.GUID != guid) { - sendResponse(PacketCoding.CreateGamePacket(0, HackMessage(0, target_guid.guid, guid.guid, 100, unk1, 6, unk2))) + sendResponse(PacketCoding.CreateGamePacket(0, HackMessage(0, target_guid, guid, 100, unk1, HackState.Hacked, unk2))) } } @@ -586,23 +586,23 @@ class WorldSessionActor extends Actor with MDCContextAware { progressBarUpdate.cancel if(progressBarValue.isDefined) { val progressBarVal : Float = progressBarValue.get + delta - val vis = if(progressBarVal == 0L) { - 1 + val vis = if(progressBarVal == 0L) { //hack state for progress bar visibility + HackState.Start } - else if(progressBarVal >= 100L) { - 4 + else if(progressBarVal > 100L) { + HackState.Finished } else { - 3 + HackState.Ongoing } - sendResponse(PacketCoding.CreateGamePacket(0, HackMessage(1, target.GUID.guid, player.GUID.guid, progressBarVal.toInt, 0L, vis, 8L))) - if(progressBarVal > 100) { + sendResponse(PacketCoding.CreateGamePacket(0, HackMessage(1, target.GUID, player.GUID, progressBarVal.toInt, 0L, vis, 8L))) + if(progressBarVal > 100) { //done progressBarValue = None log.info(s"Hacked a $target") - sendResponse(PacketCoding.CreateGamePacket(0, HackMessage(0, target.GUID.guid, player.GUID.guid, 100, 1114636288L, 6, 8L))) + sendResponse(PacketCoding.CreateGamePacket(0, HackMessage(0, target.GUID, player.GUID, 100, 1114636288L, HackState.Hacked, 8L))) completeAction() } - else { + else { //continue next tick tickAction.getOrElse(() => Unit)() progressBarValue = Some(progressBarVal) import scala.concurrent.duration._ @@ -1028,10 +1028,10 @@ class WorldSessionActor extends Actor with MDCContextAware { // TODO: Not all incoming UseItemMessage's respond with another UseItemMessage (i.e. doors only send out GenericObjectStateMsg) continent.GUID(object_guid) match { case Some(door : Door) => - continent.Map.DoorToLock.get(object_guid.guid) match { //check for IFFLock + continent.Map.DoorToLock.get(object_guid.guid) match { //check for IFF Lock case Some(lock_guid) => val lock_hacked = continent.GUID(lock_guid).get.asInstanceOf[IFFLock].HackedBy match { - case Some((tplayer, _)) => + case Some((tplayer, _, _)) => tplayer.Faction == player.Faction case None => false @@ -1042,7 +1042,7 @@ class WorldSessionActor extends Actor with MDCContextAware { door.Actor ! Door.Use(player, msg) } case None => - if(lock_hacked) { //is locks hacked? + if(lock_hacked) { //is lock hacked? this may be a weird case door.Actor ! Door.Use(player, msg) } } @@ -1054,13 +1054,13 @@ class WorldSessionActor extends Actor with MDCContextAware { player.Slot(player.DrawnSlot).Equipment match { case Some(tool : SimpleItem) => if(tool.Definition == GlobalDefinitions.remote_electronics_kit) { + //TODO get player hack level (for now, presume 15s in intervals of 4/s) progressBarValue = Some(-2.66f) - self ! WorldSessionActor.ItemHacking(player, panel, tool.GUID, 2.66f, HackTemporary(panel)) + self ! WorldSessionActor.ItemHacking(player, panel, tool.GUID, 2.66f, FinishHackingDoor(panel, 1114636288L)) + log.info("Hacking a door~") } case _ => ; } - log.info("Hacking a door~") - //TODO get player hack level (for now, presume 15s in internals of 4/s) case Some(obj : PlanetSideGameObject) => if(itemType != 121) { @@ -1071,10 +1071,7 @@ class WorldSessionActor extends Actor with MDCContextAware { sendResponse(PacketCoding.CreateGamePacket(0, PlanetsideAttributeMessage(avatar_guid, 0, 100))) // avatar with 100 hp sendResponse(PacketCoding.CreateGamePacket(0, ObjectDeleteMessage(PlanetSideGUID(unk1), 2))) } -// if(unk1 == 0 && !unk3 && unk7 == 25) { -// // TODO: This should only actually be sent to doors upon opening; may break non-door items upon use -// sendResponse(PacketCoding.CreateGamePacket(0, GenericObjectStateMsg(object_guid, 16))) -// } + case None => ; } @@ -1625,9 +1622,18 @@ class WorldSessionActor extends Actor with MDCContextAware { } } - private def HackTemporary(target : PlanetSideServerObject)() : Unit = { + /** + * The process of hacking the `Door` `IFFLock` is completed. + * Pass the message onto the lock and onto the local events system. + * @param target the `IFFLock` belonging to the door that is being hacked + * @param unk na; + * used by `HackingMessage` as `unk5` + * @see `HackMessage` + */ + //TODO add params here depending on which params in HackMessage are important + private def FinishHackingDoor(target : IFFLock, unk : Long)() : Unit = { target.Actor ! CommonMessages.Hack(player) - localService ! LocalServiceMessage(player.Continent, LocalAction.HackTemporarily(player.GUID, continent, target, 1114636288L)) + localService ! LocalServiceMessage(continent.Id, LocalAction.HackTemporarily(player.GUID, continent, target, unk)) } def failWithError(error : String) = { @@ -1660,6 +1666,18 @@ object WorldSessionActor { private final case class PlayerFailedToLoad(tplayer : Player) private final case class ListAccountCharacters() private final case class SetCurrentAvatar(tplayer : Player) + + /** + * A message that indicates the user is using a remote electronics kit to hack some server object. + * Each time this message is sent for a given hack attempt counts as a single "tick" of progress. + * The process of "making progress" with a hack involves sending this message repeatedly until the progress is 100 or more. + * @param tplayer the player + * @param target the object being hacked + * @param tool_guid the REK + * @param delta how much the progress bar value changes each tick + * @param completeAction a custom action performed once the hack is completed + * @param tickAction an optional action is is performed for each tick of progress + */ private final case class ItemHacking(tplayer : Player, target : PlanetSideServerObject, tool_guid : PlanetSideGUID, From 7845508bd3ccf40bd15745f2d538628e77baa081 Mon Sep 17 00:00:00 2001 From: FateJH Date: Fri, 13 Oct 2017 16:26:10 -0400 Subject: [PATCH 17/23] moved AvatarService and LocalService into their own package --- pslogin/src/main/scala/PsLogin.scala | 2 + .../src/main/scala/WorldSessionActor.scala | 13 ++++- .../main/scala/{ => services}/Service.scala | 5 ++ .../scala/services/avatar/AvatarAction.scala | 28 +++++++++ .../{ => services/avatar}/AvatarService.scala | 58 +------------------ .../avatar/AvatarServiceMessage.scala | 4 ++ .../avatar/AvatarServiceResponse.scala | 34 +++++++++++ .../scala/services/local/LocalAction.scala | 18 ++++++ .../{ => services/local}/LocalService.scala | 48 ++++----------- .../services/local/LocalServiceMessage.scala | 4 ++ .../services/local/LocalServiceResponse.scala | 21 +++++++ .../local/support}/DoorCloseActor.scala | 3 +- .../local/support}/HackClearActor.scala | 3 +- 13 files changed, 144 insertions(+), 97 deletions(-) rename pslogin/src/main/scala/{ => services}/Service.scala (85%) create mode 100644 pslogin/src/main/scala/services/avatar/AvatarAction.scala rename pslogin/src/main/scala/{ => services/avatar}/AvatarService.scala (64%) create mode 100644 pslogin/src/main/scala/services/avatar/AvatarServiceMessage.scala create mode 100644 pslogin/src/main/scala/services/avatar/AvatarServiceResponse.scala create mode 100644 pslogin/src/main/scala/services/local/LocalAction.scala rename pslogin/src/main/scala/{ => services/local}/LocalService.scala (53%) create mode 100644 pslogin/src/main/scala/services/local/LocalServiceMessage.scala create mode 100644 pslogin/src/main/scala/services/local/LocalServiceResponse.scala rename {common/src/main/scala/net/psforever/objects/zones => pslogin/src/main/scala/services/local/support}/DoorCloseActor.scala (98%) rename {common/src/main/scala/net/psforever/objects/zones => pslogin/src/main/scala/services/local/support}/HackClearActor.scala (98%) diff --git a/pslogin/src/main/scala/PsLogin.scala b/pslogin/src/main/scala/PsLogin.scala index 94551628..6e172cbe 100644 --- a/pslogin/src/main/scala/PsLogin.scala +++ b/pslogin/src/main/scala/PsLogin.scala @@ -18,6 +18,8 @@ import net.psforever.objects.serverobject.builders.{DoorObjectBuilder, IFFLockOb import org.slf4j import org.fusesource.jansi.Ansi._ import org.fusesource.jansi.Ansi.Color._ +import services.avatar._ +import services.local._ import scala.collection.JavaConverters._ import scala.concurrent.Await diff --git a/pslogin/src/main/scala/WorldSessionActor.scala b/pslogin/src/main/scala/WorldSessionActor.scala index 2ed2191f..7f6634d5 100644 --- a/pslogin/src/main/scala/WorldSessionActor.scala +++ b/pslogin/src/main/scala/WorldSessionActor.scala @@ -2,9 +2,9 @@ import java.util.concurrent.atomic.AtomicInteger import akka.actor.{Actor, ActorRef, Cancellable, MDCContextAware} -import net.psforever.packet.{PlanetSideGamePacket, _} +import net.psforever.packet._ import net.psforever.packet.control._ -import net.psforever.packet.game.{ObjectCreateDetailedMessage, _} +import net.psforever.packet.game._ import scodec.Attempt.{Failure, Successful} import scodec.bits._ import org.log4s.MDC @@ -23,6 +23,9 @@ import net.psforever.objects.serverobject.locks.IFFLock import net.psforever.objects.serverobject.terminals.Terminal import net.psforever.packet.game.objectcreate._ import net.psforever.types._ +import services._ +import services.avatar._ +import services.local._ import scala.annotation.tailrec import scala.util.Success @@ -229,13 +232,15 @@ class WorldSessionActor extends Actor with MDCContextAware { sendResponse(PacketCoding.CreateGamePacket(0, GenericObjectStateMsg(door_guid, 17))) case LocalServiceResponse.HackClear(target_guid, unk1, unk2) => - log.info(s"Clear hack of $target_guid") sendResponse(PacketCoding.CreateGamePacket(0, HackMessage(0, target_guid, guid, 0, unk1, HackState.HackCleared, unk2))) case LocalServiceResponse.HackObject(target_guid, unk1, unk2) => if(player.GUID != guid) { sendResponse(PacketCoding.CreateGamePacket(0, HackMessage(0, target_guid, guid, 100, unk1, HackState.Hacked, unk2))) } + + case LocalServiceResponse.TriggerSound(sound, pos, unk, volume) => + sendResponse(PacketCoding.CreateGamePacket(0, TriggerSoundMessage(sound, pos, unk, volume))) } case Door.DoorMessage(tplayer, msg, order) => @@ -1631,8 +1636,10 @@ class WorldSessionActor extends Actor with MDCContextAware { * @see `HackMessage` */ //TODO add params here depending on which params in HackMessage are important + //TODO sound should be centered on IFFLock, not on player private def FinishHackingDoor(target : IFFLock, unk : Long)() : Unit = { target.Actor ! CommonMessages.Hack(player) + localService ! LocalServiceMessage(continent.Id, LocalAction.TriggerSound(player.GUID, TriggeredSound.HackDoor, player.Position, 30, 0.49803925f)) localService ! LocalServiceMessage(continent.Id, LocalAction.HackTemporarily(player.GUID, continent, target, unk)) } diff --git a/pslogin/src/main/scala/Service.scala b/pslogin/src/main/scala/services/Service.scala similarity index 85% rename from pslogin/src/main/scala/Service.scala rename to pslogin/src/main/scala/services/Service.scala index 37a9c87c..eed17c79 100644 --- a/pslogin/src/main/scala/Service.scala +++ b/pslogin/src/main/scala/services/Service.scala @@ -1,8 +1,13 @@ // Copyright (c) 2017 PSForever +package services + import akka.event.{ActorEventBus, SubchannelClassification} import akka.util.Subclassification +import net.psforever.packet.game.PlanetSideGUID object Service { + final val defaultPlayerGUID : PlanetSideGUID = PlanetSideGUID(0) + final case class Join(channel : String) final case class Leave() final case class LeaveAll() diff --git a/pslogin/src/main/scala/services/avatar/AvatarAction.scala b/pslogin/src/main/scala/services/avatar/AvatarAction.scala new file mode 100644 index 00000000..451b3873 --- /dev/null +++ b/pslogin/src/main/scala/services/avatar/AvatarAction.scala @@ -0,0 +1,28 @@ +// Copyright (c) 2017 PSForever +package services.avatar + +import net.psforever.objects.equipment.Equipment +import net.psforever.packet.game.{PlanetSideGUID, PlayerStateMessageUpstream} +import net.psforever.packet.game.objectcreate.ConstructorData +import net.psforever.types.{ExoSuitType, Vector3} + +object AvatarAction { + trait Action + + final case class ArmorChanged(player_guid : PlanetSideGUID, suit : ExoSuitType.Value, subtype : Int) extends Action + //final case class DropItem(pos : Vector3, orient : Vector3, item : PlanetSideGUID) extends Action + final case class EquipmentInHand(player_guid : PlanetSideGUID, slot : Int, item : Equipment) extends Action + final case class EquipmentOnGround(player_guid : PlanetSideGUID, pos : Vector3, orient : Vector3, item : Equipment) extends Action + final case class LoadPlayer(player_guid : PlanetSideGUID, pdata : ConstructorData) extends Action +// final case class LoadMap(msg : PlanetSideGUID) extends Action +// final case class unLoadMap(msg : PlanetSideGUID) extends Action + final case class ObjectDelete(player_guid : PlanetSideGUID, item_guid : PlanetSideGUID, unk : Int = 0) extends Action + final case class ObjectHeld(player_guid : PlanetSideGUID, slot : Int) extends Action + final case class PlanetsideAttribute(player_guid : PlanetSideGUID, attribute_type : Int, attribute_value : Long) extends Action + final case class PlayerState(player_guid : PlanetSideGUID, msg : PlayerStateMessageUpstream, spectator : Boolean, weaponInHand : Boolean) extends Action + final case class Reload(player_guid : PlanetSideGUID, mag : Int) extends Action +// final case class PlayerStateShift(killer : PlanetSideGUID, victim : PlanetSideGUID) extends Action +// final case class DestroyDisplay(killer : PlanetSideGUID, victim : PlanetSideGUID) extends Action +// final case class HitHintReturn(killer : PlanetSideGUID, victim : PlanetSideGUID) extends Action +// final case class ChangeWeapon(unk1 : Int, sessionId : Long) extends Action +} diff --git a/pslogin/src/main/scala/AvatarService.scala b/pslogin/src/main/scala/services/avatar/AvatarService.scala similarity index 64% rename from pslogin/src/main/scala/AvatarService.scala rename to pslogin/src/main/scala/services/avatar/AvatarService.scala index 5905366f..5b656c23 100644 --- a/pslogin/src/main/scala/AvatarService.scala +++ b/pslogin/src/main/scala/services/avatar/AvatarService.scala @@ -1,60 +1,8 @@ // Copyright (c) 2017 PSForever +package services.avatar + import akka.actor.Actor -import net.psforever.objects.equipment.Equipment -import net.psforever.packet.game.objectcreate.ConstructorData -import net.psforever.types.ExoSuitType -import net.psforever.packet.game.{PlanetSideGUID, PlayerStateMessageUpstream} -import net.psforever.types.Vector3 - -object AvatarAction { - trait Action - - final case class ArmorChanged(player_guid : PlanetSideGUID, suit : ExoSuitType.Value, subtype : Int) extends Action - //final case class DropItem(pos : Vector3, orient : Vector3, item : PlanetSideGUID) extends Action - final case class EquipmentInHand(player_guid : PlanetSideGUID, slot : Int, item : Equipment) extends Action - final case class EquipmentOnGround(player_guid : PlanetSideGUID, pos : Vector3, orient : Vector3, item : Equipment) extends Action - final case class LoadPlayer(player_guid : PlanetSideGUID, pdata : ConstructorData) extends Action -// final case class LoadMap(msg : PlanetSideGUID) extends Action -// final case class unLoadMap(msg : PlanetSideGUID) extends Action - final case class ObjectDelete(player_guid : PlanetSideGUID, item_guid : PlanetSideGUID, unk : Int = 0) extends Action - final case class ObjectHeld(player_guid : PlanetSideGUID, slot : Int) extends Action - final case class PlanetsideAttribute(player_guid : PlanetSideGUID, attribute_type : Int, attribute_value : Long) extends Action - final case class PlayerState(player_guid : PlanetSideGUID, msg : PlayerStateMessageUpstream, spectator : Boolean, weaponInHand : Boolean) extends Action - final case class Reload(player_guid : PlanetSideGUID, mag : Int) extends Action -// final case class PlayerStateShift(killer : PlanetSideGUID, victim : PlanetSideGUID) extends Action -// final case class DestroyDisplay(killer : PlanetSideGUID, victim : PlanetSideGUID) extends Action -// final case class HitHintReturn(killer : PlanetSideGUID, victim : PlanetSideGUID) extends Action -// final case class ChangeWeapon(unk1 : Int, sessionId : Long) extends Action -} - -object AvatarServiceResponse { - trait Response - - final case class ArmorChanged(suit : ExoSuitType.Value, subtype : Int) extends Response - //final case class DropItem(pos : Vector3, orient : Vector3, item : PlanetSideGUID) extends Response - final case class EquipmentInHand(slot : Int, item : Equipment) extends Response - final case class EquipmentOnGround(pos : Vector3, orient : Vector3, item : Equipment) extends Response - final case class LoadPlayer(pdata : ConstructorData) extends Response -// final case class unLoadMap() extends Response -// final case class LoadMap() extends Response - final case class ObjectDelete(item_guid : PlanetSideGUID, unk : Int) extends Response - final case class ObjectHeld(slot : Int) extends Response - final case class PlanetSideAttribute(attribute_type : Int, attribute_value : Long) extends Response - final case class PlayerState(msg : PlayerStateMessageUpstream, spectator : Boolean, weaponInHand : Boolean) extends Response - final case class Reload(mag : Int) extends Response -// final case class PlayerStateShift(itemID : PlanetSideGUID) extends Response -// final case class DestroyDisplay(itemID : PlanetSideGUID) extends Response -// final case class HitHintReturn(itemID : PlanetSideGUID) extends Response -// final case class ChangeWeapon(facingYaw : Int) extends Response -} - -final case class AvatarServiceMessage(forChannel : String, actionMessage : AvatarAction.Action) - -final case class AvatarServiceResponse(toChannel : String, avatar_guid : PlanetSideGUID, replyMessage : AvatarServiceResponse.Response) extends GenericEventBusMsg - -/* - /Avatar/ - */ +import services.{GenericEventBus, Service} class AvatarService extends Actor { //import AvatarServiceResponse._ diff --git a/pslogin/src/main/scala/services/avatar/AvatarServiceMessage.scala b/pslogin/src/main/scala/services/avatar/AvatarServiceMessage.scala new file mode 100644 index 00000000..e3e35cd3 --- /dev/null +++ b/pslogin/src/main/scala/services/avatar/AvatarServiceMessage.scala @@ -0,0 +1,4 @@ +// Copyright (c) 2017 PSForever +package services.avatar + +final case class AvatarServiceMessage(forChannel : String, actionMessage : AvatarAction.Action) diff --git a/pslogin/src/main/scala/services/avatar/AvatarServiceResponse.scala b/pslogin/src/main/scala/services/avatar/AvatarServiceResponse.scala new file mode 100644 index 00000000..0bec7e74 --- /dev/null +++ b/pslogin/src/main/scala/services/avatar/AvatarServiceResponse.scala @@ -0,0 +1,34 @@ +// Copyright (c) 2017 PSForever +package services.avatar + +import net.psforever.objects.equipment.Equipment +import net.psforever.packet.game.{PlanetSideGUID, PlayerStateMessageUpstream} +import net.psforever.packet.game.objectcreate.ConstructorData +import net.psforever.types.{ExoSuitType, Vector3} +import services.GenericEventBusMsg + +final case class AvatarServiceResponse(toChannel : String, + avatar_guid : PlanetSideGUID, + replyMessage : AvatarServiceResponse.Response + ) extends GenericEventBusMsg + +object AvatarServiceResponse { + trait Response + + final case class ArmorChanged(suit : ExoSuitType.Value, subtype : Int) extends Response + //final case class DropItem(pos : Vector3, orient : Vector3, item : PlanetSideGUID) extends Response + final case class EquipmentInHand(slot : Int, item : Equipment) extends Response + final case class EquipmentOnGround(pos : Vector3, orient : Vector3, item : Equipment) extends Response + final case class LoadPlayer(pdata : ConstructorData) extends Response +// final case class unLoadMap() extends Response +// final case class LoadMap() extends Response + final case class ObjectDelete(item_guid : PlanetSideGUID, unk : Int) extends Response + final case class ObjectHeld(slot : Int) extends Response + final case class PlanetSideAttribute(attribute_type : Int, attribute_value : Long) extends Response + final case class PlayerState(msg : PlayerStateMessageUpstream, spectator : Boolean, weaponInHand : Boolean) extends Response + final case class Reload(mag : Int) extends Response +// final case class PlayerStateShift(itemID : PlanetSideGUID) extends Response +// final case class DestroyDisplay(itemID : PlanetSideGUID) extends Response +// final case class HitHintReturn(itemID : PlanetSideGUID) extends Response +// final case class ChangeWeapon(facingYaw : Int) extends Response +} \ No newline at end of file diff --git a/pslogin/src/main/scala/services/local/LocalAction.scala b/pslogin/src/main/scala/services/local/LocalAction.scala new file mode 100644 index 00000000..4003fd9b --- /dev/null +++ b/pslogin/src/main/scala/services/local/LocalAction.scala @@ -0,0 +1,18 @@ +// Copyright (c) 2017 PSForever +package services.local + +import net.psforever.objects.serverobject.PlanetSideServerObject +import net.psforever.objects.serverobject.doors.Door +import net.psforever.objects.zones.Zone +import net.psforever.packet.game.{PlanetSideGUID, TriggeredSound} +import net.psforever.types.Vector3 + +object LocalAction { + trait Action + + final case class DoorOpens(player_guid : PlanetSideGUID, continent : Zone, door : Door) extends Action + final case class DoorCloses(player_guid : PlanetSideGUID, door_guid : PlanetSideGUID) extends Action + final case class HackClear(player_guid : PlanetSideGUID, target : PlanetSideServerObject, unk1 : Long, unk2 : Long = 8L) extends Action + final case class HackTemporarily(player_guid : PlanetSideGUID, continent : Zone, target : PlanetSideServerObject, unk1 : Long, unk2 : Long = 8L) extends Action + final case class TriggerSound(player_guid : PlanetSideGUID, sound : TriggeredSound.Value, pos : Vector3, unk : Int, volume : Float) extends Action +} diff --git a/pslogin/src/main/scala/LocalService.scala b/pslogin/src/main/scala/services/local/LocalService.scala similarity index 53% rename from pslogin/src/main/scala/LocalService.scala rename to pslogin/src/main/scala/services/local/LocalService.scala index 165016ef..56acf25d 100644 --- a/pslogin/src/main/scala/LocalService.scala +++ b/pslogin/src/main/scala/services/local/LocalService.scala @@ -1,35 +1,9 @@ // Copyright (c) 2017 PSForever +package services.local + import akka.actor.{Actor, Props} -import net.psforever.objects.serverobject.PlanetSideServerObject -import net.psforever.objects.serverobject.doors.Door -import net.psforever.objects.zones.{DoorCloseActor, HackClearActor, Zone} -import net.psforever.packet.game.PlanetSideGUID - -object LocalAction { - trait Action - - final case class DoorOpens(player_guid : PlanetSideGUID, continent : Zone, door : Door) extends Action - final case class DoorCloses(player_guid : PlanetSideGUID, door_guid : PlanetSideGUID) extends Action - final case class HackClear(player_guid : PlanetSideGUID, target : PlanetSideServerObject, unk1 : Long, unk2 : Long = 8L) extends Action - final case class HackTemporarily(player_guid : PlanetSideGUID, continent : Zone, target : PlanetSideServerObject, unk1 : Long, unk2 : Long = 8L) extends Action -} - -object LocalServiceResponse { - trait Response - - final case class DoorOpens(door_guid : PlanetSideGUID) extends Response - final case class DoorCloses(door_guid : PlanetSideGUID) extends Response - final case class HackClear(target_guid : PlanetSideGUID, unk1 : Long, unk2 : Long) extends Response - final case class HackObject(target_guid : PlanetSideGUID, unk1 : Long, unk2 : Long) extends Response -} - -final case class LocalServiceMessage(forChannel : String, actionMessage : LocalAction.Action) - -final case class LocalServiceResponse(toChannel : String, avatar_guid : PlanetSideGUID, replyMessage : LocalServiceResponse.Response) extends GenericEventBusMsg - -/* - /LocalEnvironment/ - */ +import services.local.support.{DoorCloseActor, HackClearActor} +import services.{GenericEventBus, Service} class LocalService extends Actor { //import LocalService._ @@ -72,7 +46,11 @@ class LocalService extends Actor { case LocalAction.HackTemporarily(player_guid, zone, target, unk1, unk2) => hackClearer ! HackClearActor.ObjectIsHacked(target, zone, unk1, unk2) LocalEvents.publish( - LocalServiceResponse(s"/$forChannel/Avatar", player_guid, LocalServiceResponse.HackObject(target.GUID, unk1, unk2)) + LocalServiceResponse(s"/$forChannel/LocalEnvironment", player_guid, LocalServiceResponse.HackObject(target.GUID, unk1, unk2)) + ) + case LocalAction.TriggerSound(player_guid, sound, pos, unk, volume) => + LocalEvents.publish( + LocalServiceResponse(s"/$forChannel/LocalEnvironment", player_guid, LocalServiceResponse.TriggerSound(sound, pos, unk, volume)) ) case _ => ; } @@ -80,20 +58,16 @@ class LocalService extends Actor { //response from DoorCloseActor case DoorCloseActor.CloseTheDoor(door_guid, zone_id) => LocalEvents.publish( - LocalServiceResponse(s"/$zone_id/LocalEnvironment", LocalService.defaultPlayerGUID, LocalServiceResponse.DoorCloses(door_guid)) + LocalServiceResponse(s"/$zone_id/LocalEnvironment", Service.defaultPlayerGUID, LocalServiceResponse.DoorCloses(door_guid)) ) //response from HackClearActor case HackClearActor.ClearTheHack(target_guid, zone_id, unk1, unk2) => LocalEvents.publish( - LocalServiceResponse(s"/$zone_id/LocalEnvironment", LocalService.defaultPlayerGUID, LocalServiceResponse.HackClear(target_guid, unk1, unk2)) + LocalServiceResponse(s"/$zone_id/LocalEnvironment", Service.defaultPlayerGUID, LocalServiceResponse.HackClear(target_guid, unk1, unk2)) ) case msg => log.info(s"Unhandled message $msg from $sender") } } - -object LocalService { - final val defaultPlayerGUID : PlanetSideGUID = PlanetSideGUID(0) -} diff --git a/pslogin/src/main/scala/services/local/LocalServiceMessage.scala b/pslogin/src/main/scala/services/local/LocalServiceMessage.scala new file mode 100644 index 00000000..fc6dd20a --- /dev/null +++ b/pslogin/src/main/scala/services/local/LocalServiceMessage.scala @@ -0,0 +1,4 @@ +// Copyright (c) 2017 PSForever +package services.local + +final case class LocalServiceMessage(forChannel : String, actionMessage : LocalAction.Action) diff --git a/pslogin/src/main/scala/services/local/LocalServiceResponse.scala b/pslogin/src/main/scala/services/local/LocalServiceResponse.scala new file mode 100644 index 00000000..736732bc --- /dev/null +++ b/pslogin/src/main/scala/services/local/LocalServiceResponse.scala @@ -0,0 +1,21 @@ +// Copyright (c) 2017 PSForever +package services.local + +import net.psforever.packet.game.{PlanetSideGUID, TriggeredSound} +import net.psforever.types.Vector3 +import services.GenericEventBusMsg + +final case class LocalServiceResponse(toChannel : String, + avatar_guid : PlanetSideGUID, + replyMessage : LocalServiceResponse.Response + ) extends GenericEventBusMsg + +object LocalServiceResponse { + trait Response + + final case class DoorOpens(door_guid : PlanetSideGUID) extends Response + final case class DoorCloses(door_guid : PlanetSideGUID) extends Response + final case class HackClear(target_guid : PlanetSideGUID, unk1 : Long, unk2 : Long) extends Response + final case class HackObject(target_guid : PlanetSideGUID, unk1 : Long, unk2 : Long) extends Response + final case class TriggerSound(sound : TriggeredSound.Value, pos : Vector3, unk : Int, volume : Float) extends Response +} diff --git a/common/src/main/scala/net/psforever/objects/zones/DoorCloseActor.scala b/pslogin/src/main/scala/services/local/support/DoorCloseActor.scala similarity index 98% rename from common/src/main/scala/net/psforever/objects/zones/DoorCloseActor.scala rename to pslogin/src/main/scala/services/local/support/DoorCloseActor.scala index 4b2d9e9f..a2ca622c 100644 --- a/common/src/main/scala/net/psforever/objects/zones/DoorCloseActor.scala +++ b/pslogin/src/main/scala/services/local/support/DoorCloseActor.scala @@ -1,8 +1,9 @@ // Copyright (c) 2017 PSForever -package net.psforever.objects.zones +package services.local.support import akka.actor.{Actor, Cancellable} import net.psforever.objects.serverobject.doors.Door +import net.psforever.objects.zones.Zone import net.psforever.packet.game.PlanetSideGUID import scala.annotation.tailrec diff --git a/common/src/main/scala/net/psforever/objects/zones/HackClearActor.scala b/pslogin/src/main/scala/services/local/support/HackClearActor.scala similarity index 98% rename from common/src/main/scala/net/psforever/objects/zones/HackClearActor.scala rename to pslogin/src/main/scala/services/local/support/HackClearActor.scala index 2ec3cb05..76a7e7f9 100644 --- a/common/src/main/scala/net/psforever/objects/zones/HackClearActor.scala +++ b/pslogin/src/main/scala/services/local/support/HackClearActor.scala @@ -1,8 +1,9 @@ // Copyright (c) 2017 PSForever -package net.psforever.objects.zones +package services.local.support import akka.actor.{Actor, Cancellable} import net.psforever.objects.serverobject.{CommonMessages, PlanetSideServerObject} +import net.psforever.objects.zones.Zone import net.psforever.packet.game.PlanetSideGUID import scala.annotation.tailrec From 80b2f23b1dc52164b9ee642306d9fc565ae21b70 Mon Sep 17 00:00:00 2001 From: FateJH Date: Fri, 13 Oct 2017 18:28:38 -0400 Subject: [PATCH 18/23] missed newline --- .../src/main/scala/services/avatar/AvatarServiceResponse.scala | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/pslogin/src/main/scala/services/avatar/AvatarServiceResponse.scala b/pslogin/src/main/scala/services/avatar/AvatarServiceResponse.scala index 0bec7e74..29e05ed6 100644 --- a/pslogin/src/main/scala/services/avatar/AvatarServiceResponse.scala +++ b/pslogin/src/main/scala/services/avatar/AvatarServiceResponse.scala @@ -31,4 +31,4 @@ object AvatarServiceResponse { // final case class DestroyDisplay(itemID : PlanetSideGUID) extends Response // final case class HitHintReturn(itemID : PlanetSideGUID) extends Response // final case class ChangeWeapon(facingYaw : Int) extends Response -} \ No newline at end of file +} From 349ec676d9f314c34b6492c8f434fe7642cf13b8 Mon Sep 17 00:00:00 2001 From: FateJH Date: Tue, 10 Oct 2017 19:51:25 -0400 Subject: [PATCH 19/23] swapping OCM with OCDM packet during login; also needed to add functionality that temporarily registers everything on the character's person --- .../src/main/scala/WorldSessionActor.scala | 25 ++++++++++++++++++- 1 file changed, 24 insertions(+), 1 deletion(-) diff --git a/pslogin/src/main/scala/WorldSessionActor.scala b/pslogin/src/main/scala/WorldSessionActor.scala index d759a8f3..c458d112 100644 --- a/pslogin/src/main/scala/WorldSessionActor.scala +++ b/pslogin/src/main/scala/WorldSessionActor.scala @@ -424,7 +424,7 @@ class WorldSessionActor extends Actor with MDCContextAware { val armor = player.Armor player.Spawn sendResponse(PacketCoding.CreateGamePacket(0, - ObjectCreateMessage(ObjectClass.avatar, player.GUID, player.Definition.Packet.ConstructorData(player).get) + ObjectCreateDetailedMessage(ObjectClass.avatar, player.GUID, player.Definition.Packet.DetailedConstructorData(player).get) )) if(health > 0) { //player can not be dead; stay spawned as alive player.Health = health @@ -1447,6 +1447,18 @@ 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) } @@ -1478,6 +1490,17 @@ 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() } From 47adfef5c86f2dba5de191d85f651e77214fb2dd Mon Sep 17 00:00:00 2001 From: FateJH Date: Sun, 15 Oct 2017 23:01:48 -0400 Subject: [PATCH 20/23] attempting to fix BR24+ characters --- .../scala/net/psforever/objects/Player.scala | 10 +- .../converter/AvatarConverter.scala | 30 +++- .../converter/CharacterSelectConverter.scala | 144 ++++++++++++++++++ .../objectcreate/DetailedCharacterData.scala | 6 +- .../net/psforever/types/ImplantType.scala | 4 +- .../src/main/scala/WorldSessionActor.scala | 28 ++-- 6 files changed, 201 insertions(+), 21 deletions(-) create mode 100644 common/src/main/scala/net/psforever/objects/definition/converter/CharacterSelectConverter.scala diff --git a/common/src/main/scala/net/psforever/objects/Player.scala b/common/src/main/scala/net/psforever/objects/Player.scala index 9d59f733..ab1cc89a 100644 --- a/common/src/main/scala/net/psforever/objects/Player.scala +++ b/common/src/main/scala/net/psforever/objects/Player.scala @@ -13,8 +13,8 @@ import scala.collection.mutable class Player(private val name : String, private val faction : PlanetSideEmpire.Value, private val sex : CharacterGender.Value, - private val voice : Int, - private val head : Int + private val head : Int, + private val voice : Int ) extends PlanetSideGameObject { private var alive : Boolean = false private var backpack : Boolean = false @@ -521,11 +521,11 @@ object Player { final val FreeHandSlot : Int = 250 final val HandsDownSlot : Int = 255 - def apply(name : String, faction : PlanetSideEmpire.Value, sex : CharacterGender.Value, voice : Int, head : Int) : Player = { - new Player(name, faction, sex, voice, head) + def apply(name : String, faction : PlanetSideEmpire.Value, sex : CharacterGender.Value, head : Int, voice : Int) : Player = { + new Player(name, faction, sex, head, voice) } - def apply(guid : PlanetSideGUID, name : String, faction : PlanetSideEmpire.Value, sex : CharacterGender.Value, voice : Int, head : Int) : Player = { + def apply(guid : PlanetSideGUID, name : String, faction : PlanetSideEmpire.Value, sex : CharacterGender.Value, head : Int, voice : Int) : Player = { val obj = new Player(name, faction, sex, voice, head) obj.GUID = guid obj diff --git a/common/src/main/scala/net/psforever/objects/definition/converter/AvatarConverter.scala b/common/src/main/scala/net/psforever/objects/definition/converter/AvatarConverter.scala index b20812fd..7e9562d2 100644 --- a/common/src/main/scala/net/psforever/objects/definition/converter/AvatarConverter.scala +++ b/common/src/main/scala/net/psforever/objects/definition/converter/AvatarConverter.scala @@ -40,7 +40,7 @@ class AvatarConverter extends ObjectCreateConverter[Player]() { obj.Stamina, obj.Certifications.toList.sortBy(_.id), //TODO is sorting necessary? MakeImplantEntries(obj), - List.empty[String], //TODO fte list + "xpe_battle_rank_10" :: Nil, //TODO fte list List.empty[String], //TODO tutorial list InventoryData((MakeHolsters(obj, BuildDetailedEquipment) ++ MakeFifthSlot(obj) ++ MakeInventory(obj)).sortBy(_.parentSlot)), GetDrawnSlot(obj) @@ -56,7 +56,7 @@ class AvatarConverter extends ObjectCreateConverter[Player]() { private def MakeAppearanceData(obj : Player) : CharacterAppearanceData = { CharacterAppearanceData( PlacementData(obj.Position, obj.Orientation, obj.Velocity), - BasicCharacterData(obj.Name, obj.Faction, obj.Sex, obj.Voice, obj.Head), + BasicCharacterData(obj.Name, obj.Faction, obj.Sex, obj.Head, obj.Voice), 0, false, false, @@ -132,7 +132,10 @@ class AvatarConverter extends ObjectCreateConverter[Player]() { * @see `ImplantEntry` in `DetailedCharacterData` */ private def MakeImplantEntries(obj : Player) : List[ImplantEntry] = { - obj.Implants.map(slot => { + val numImplants : Int = NumberOfImplantSlots(obj.BEP) + val implants = obj.Implants + (0 until numImplants).map(index => { + val slot = implants(index) slot.Installed match { case Some(_) => if(slot.Initialized) { @@ -147,6 +150,27 @@ class AvatarConverter extends ObjectCreateConverter[Player]() { }).toList } + /** + * A player's battle rank, determined by their battle experience points, determines how many implants to which they have access. + * Starting with "no implants" at BR1, a player earns one at each of the three ranks: BR6, BR12, and BR18. + * @param bep battle experience points + * @return the number of accessible implant slots + */ + private def NumberOfImplantSlots(bep : Long) : Int = { + if(bep > 754370) { //BR18+ + 3 + } + else if(bep > 197753) { //BR12+ + 2 + } + else if(bep > 29999) { //BR6+ + 1 + } + else { //BR1+ + 0 + } + } + /** * Find an active implant whose effect will be displayed on this player. * @param iter an `Iterator` of `ImplantSlot` objects diff --git a/common/src/main/scala/net/psforever/objects/definition/converter/CharacterSelectConverter.scala b/common/src/main/scala/net/psforever/objects/definition/converter/CharacterSelectConverter.scala new file mode 100644 index 00000000..4abca57b --- /dev/null +++ b/common/src/main/scala/net/psforever/objects/definition/converter/CharacterSelectConverter.scala @@ -0,0 +1,144 @@ +// Copyright (c) 2017 PSForever +package net.psforever.objects.definition.converter + +import net.psforever.objects.GlobalDefinitions.{advanced_regen, darklight_vision, personal_shield, surge} +import net.psforever.objects.{EquipmentSlot, GlobalDefinitions, ImplantSlot, Player} +import net.psforever.objects.equipment.Equipment +import net.psforever.packet.game.objectcreate.{BasicCharacterData, CharacterAppearanceData, CharacterData, DetailedCharacterData, DrawnSlot, ImplantEffects, ImplantEntry, InternalSlot, InventoryData, PlacementData, RibbonBars} +import net.psforever.types.{GrenadeState, ImplantType} + +import scala.annotation.tailrec +import scala.util.{Failure, Success, Try} + +/** + * `CharacterSelectConverter` is based on `AvatarConverter` + * but it is tailored for appearance of the player character on the character selection screen only. + * Details that would not be apparent on that screen such as implants or certifications are ignored. + */ +class CharacterSelectConverter extends ObjectCreateConverter[Player]() { + override def ConstructorData(obj : Player) : Try[CharacterData] = Failure(new Exception("CharacterSelectConverter should not be used to generate CharacterData")) + + override def DetailedConstructorData(obj : Player) : Try[DetailedCharacterData] = { + Success( + DetailedCharacterData( + MakeAppearanceData(obj), + obj.BEP, + obj.CEP, + 1, 1, 0, 1, 1, + Nil, + MakeImplantEntries(obj), + Nil, Nil, + InventoryData(recursiveMakeHolsters(obj.Holsters().iterator)), + GetDrawnSlot(obj) + ) + ) + } + + /** + * Compose some data from a `Player` into a representation common to both `CharacterData` and `DetailedCharacterData`. + * @param obj the `Player` game object + * @see `AvatarConverter.MakeAppearanceData` + * @return the resulting `CharacterAppearanceData` + */ + private def MakeAppearanceData(obj : Player) : CharacterAppearanceData = { + CharacterAppearanceData( + PlacementData(0f, 0f, 0f), + BasicCharacterData(obj.Name, obj.Faction, obj.Sex, obj.Head, 1), + 0, + false, + false, + obj.ExoSuit, + "", + 0, + false, + 0f, + 0f, + true, + GrenadeState.None, + false, + false, + false, + RibbonBars() + ) + } + + /** + * Transform an `Array` of `Implant` objects into a `List` of `ImplantEntry` objects suitable as packet data. + * @param obj the `Player` game object + * @return the resulting implant `List` + * @see `ImplantEntry` in `DetailedCharacterData` + */ + private def MakeImplantEntries(obj : Player) : List[ImplantEntry] = { + List.fill[ImplantEntry](NumberOfImplantSlots(obj.BEP))(ImplantEntry(ImplantType.None, None)) + } + + /** + * A player's battle rank, determined by their battle experience points, determines how many implants to which they have access. + * Starting with "no implants" at BR1, a player earns one at each of the three ranks: BR6, BR12, and BR18. + * @param bep battle experience points + * @return the number of accessible implant slots + */ + private def NumberOfImplantSlots(bep : Long) : Int = { + if(bep > 754370) { //BR18+ + 3 + } + else if(bep > 197753) { //BR12+ + 2 + } + else if(bep > 29999) { //BR6+ + 1 + } + else { //BR1+ + 0 + } + } + + /** + * A builder method for turning an object into `0x18` decoded packet form. + * @param index the position of the object + * @param equip the game object + * @see `AvatarConverter.BuildDetailedEquipment` + * @return the game object in decoded packet form + */ + private def BuildDetailedEquipment(index : Int, equip : Equipment) : InternalSlot = { + InternalSlot(equip.Definition.ObjectId, equip.GUID, index, equip.Definition.Packet.DetailedConstructorData(equip).get) + } + + /** + * Given some equipment holsters, convert the contents of those holsters into converted-decoded packet data. + * @param iter an `Iterator` of `EquipmentSlot` objects that are a part of the player's holsters + * @param list the current `List` of transformed data + * @param index which holster is currently being explored + * @see `AvatarConverter.recursiveMakeHolsters` + * @return the `List` of inventory data created from the holsters + */ + @tailrec private def recursiveMakeHolsters(iter : Iterator[EquipmentSlot], list : List[InternalSlot] = Nil, index : Int = 0) : List[InternalSlot] = { + if(!iter.hasNext) { + list + } + else { + val slot : EquipmentSlot = iter.next + if(slot.Equipment.isDefined) { + val equip : Equipment = slot.Equipment.get + recursiveMakeHolsters( + iter, + list :+ BuildDetailedEquipment(index, equip), + index + 1 + ) + } + else { + recursiveMakeHolsters(iter, list, index + 1) + } + } + } + + /** + * Resolve which holster the player has drawn, if any. + * @param obj the `Player` game object + * @see `AvatarConverter.GetDrawnSlot` + * @return the holster's Enumeration value + */ + private def GetDrawnSlot(obj : Player) : DrawnSlot.Value = { + try { DrawnSlot(obj.DrawnSlot) } catch { case _ : Exception => DrawnSlot.None } + } +} diff --git a/common/src/main/scala/net/psforever/packet/game/objectcreate/DetailedCharacterData.scala b/common/src/main/scala/net/psforever/packet/game/objectcreate/DetailedCharacterData.scala index 13ccf6ee..ccb0d3f2 100644 --- a/common/src/main/scala/net/psforever/packet/game/objectcreate/DetailedCharacterData.scala +++ b/common/src/main/scala/net/psforever/packet/game/objectcreate/DetailedCharacterData.scala @@ -209,7 +209,7 @@ object DetailedCharacterData extends Marshallable[DetailedCharacterData] { implantOffset += entry.bitsize.toInt }) val resultB : Int = resultA - (implantOffset % 8) - if(resultB < 0) { 8 - resultB } else { resultB } + if(resultB < 0) { 8 + resultB } else { resultB } } /** @@ -297,8 +297,8 @@ object DetailedCharacterData extends Marshallable[DetailedCharacterData] { (("tutorial_length" | uint32L) >>:~ { len2 => conditional(len2 > 0, "tutorial_firstEntry" | PacketHelpers.encodedStringAligned(tutPadding(len, len2, implantFieldPadding(implants, CharacterAppearanceData.altModelBit(app))))) :: ("tutorial_list" | PacketHelpers.listOfNSized(len2 - 1, PacketHelpers.encodedString)) :: - ignore(207) :: - optional(bool, "inventory" | InventoryData.codec_detailed) :: + ignore(200) :: + conditional(true, "inventory" | InventoryData.codec_detailed) :: ("drawn_slot" | DrawnSlot.codec) :: bool //usually false }) diff --git a/common/src/main/scala/net/psforever/types/ImplantType.scala b/common/src/main/scala/net/psforever/types/ImplantType.scala index acd47b5b..d1c96eae 100644 --- a/common/src/main/scala/net/psforever/types/ImplantType.scala +++ b/common/src/main/scala/net/psforever/types/ImplantType.scala @@ -23,7 +23,9 @@ import scodec.codecs._ */ object ImplantType extends Enumeration { type Type = Value - val AdvancedRegen, + + val + AdvancedRegen, Targeting, AudioAmplifier, DarklightVision, diff --git a/pslogin/src/main/scala/WorldSessionActor.scala b/pslogin/src/main/scala/WorldSessionActor.scala index c458d112..5c532084 100644 --- a/pslogin/src/main/scala/WorldSessionActor.scala +++ b/pslogin/src/main/scala/WorldSessionActor.scala @@ -415,7 +415,9 @@ class WorldSessionActor extends Actor with MDCContextAware { } case ListAccountCharacters => + import net.psforever.objects.definition.converter.CharacterSelectConverter val gen : AtomicInteger = new AtomicInteger(1) + val converter : CharacterSelectConverter = new CharacterSelectConverter //load characters SetCharacterSelectScreenGUID(player, gen) @@ -423,15 +425,21 @@ class WorldSessionActor extends Actor with MDCContextAware { val stamina = player.Stamina val armor = player.Armor player.Spawn - sendResponse(PacketCoding.CreateGamePacket(0, - ObjectCreateDetailedMessage(ObjectClass.avatar, player.GUID, player.Definition.Packet.DetailedConstructorData(player).get) - )) +// sendResponse(PacketCoding.CreateGamePacket(0, +// ObjectCreateDetailedMessage(ObjectClass.avatar, player.GUID, player.Definition.Packet.DetailedConstructorData(player).get) +// )) + sendRawResponse( + objectHex + ) if(health > 0) { //player can not be dead; stay spawned as alive player.Health = health player.Stamina = stamina player.Armor = armor } - sendResponse(PacketCoding.CreateGamePacket(0, CharacterInfoMessage(15,PlanetSideZoneID(10000), 41605313, player.GUID, false, 6404428))) + log.info( + PacketCoding.DecodePacket(objectHex).require.toString + ) + sendResponse(PacketCoding.CreateGamePacket(0, CharacterInfoMessage(15,PlanetSideZoneID(10000), 41605313, PlanetSideGUID(75), false, 6404428))) RemoveCharacterSelectScreenGUID(player) sendResponse(PacketCoding.CreateGamePacket(0, CharacterInfoMessage(0, PlanetSideZoneID(1), 0, PlanetSideGUID(0), true, 0))) @@ -492,14 +500,15 @@ class WorldSessionActor extends Actor with MDCContextAware { log.info("Load the now-registered player") //load the now-registered player tplayer.Spawn - sendResponse(PacketCoding.CreateGamePacket(0, - ObjectCreateDetailedMessage(ObjectClass.avatar, tplayer.GUID, tplayer.Definition.Packet.DetailedConstructorData(tplayer).get) - )) +// sendResponse(PacketCoding.CreateGamePacket(0, +// ObjectCreateDetailedMessage(ObjectClass.avatar, tplayer.GUID, tplayer.Definition.Packet.DetailedConstructorData(tplayer).get) +// )) + sendRawResponse(objectHex) avatarService ! AvatarServiceMessage(tplayer.Continent, AvatarAction.LoadPlayer(tplayer.GUID, tplayer.Definition.Packet.ConstructorData(tplayer).get)) log.debug(s"ObjectCreateDetailedMessage: ${tplayer.Definition.Packet.DetailedConstructorData(tplayer).get}") case SetCurrentAvatar(tplayer) => - val guid = tplayer.GUID + val guid = PlanetSideGUID(75)//tplayer.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))) @@ -537,7 +546,7 @@ class WorldSessionActor extends Actor with MDCContextAware { case default => log.warn(s"Invalid packet class received: $default") } - +val objectHex = hex"18 2c e0 00 00 bc 84 B0 00 0b ea 00 6c 7d f1 10 00 00 02 40 00 08 60 4b 00 69 00 43 00 6b 00 4a 00 72 00 02 31 3a cc 82 c0 00 00 00 00 00 00 00 00 3e df 42 00 20 00 0e 00 40 43 40 4c 04 00 02 e8 00 00 03 a8 00 00 01 9c 04 00 00 b8 99 84 00 0e 68 28 00 00 00 00 00 00 00 00 00 00 00 00 01 90 01 90 00 c8 00 00 01 00 7e c8 00 5c 00 00 01 29 c1 cc 80 00 00 00 00 00 00 00 00 00 00 00 00 03 c0 00 40 81 01 c4 45 46 86 c8 88 c9 09 4a 4a 80 50 0c 13 00 00 15 00 80 00 48 00 7870655f6f766572686561645f6d6170 8d7870655f776172705f676174658f7870655f666f726d5f6f75746669748c7870655f626c61636b6f7073927870655f636f6d6d616e645f72616e6b5f35927870655f636f6d6d616e645f72616e6b5f33927870655f73616e6374756172795f68656c70927870655f626174746c655f72616e6b5f3133927870655f626174746c655f72616e6b5f3132927870655f626174746c655f72616e6b5f3130927870655f626174746c655f72616e6b5f3134927870655f626174746c655f72616e6b5f3135937870655f6f72626974616c5f73687574746c658c7870655f64726f705f706f64917870655f62696e645f666163696c697479917870655f626174746c655f72616e6b5f33917870655f626174746c655f72616e6b5f35917870655f626174746c655f72616e6b5f348e7870655f6a6f696e5f73717561648e7870655f666f726d5f7371756164927870655f696e7374616e745f616374696f6e917870655f626174746c655f72616e6b5f32937870655f776172705f676174655f7573616765917870655f626174746c655f72616e6b5f38927870655f626174746c655f72616e6b5f3131917870655f626174746c655f72616e6b5f368e7870655f6d61696c5f616c657274927870655f636f6d6d616e645f72616e6b5f31927870655f626174746c655f72616e6b5f3230927870655f626174746c655f72616e6b5f3138927870655f626174746c655f72616e6b5f3139907870655f6a6f696e5f706c61746f6f6e927870655f626174746c655f72616e6b5f3137927870655f626174746c655f72616e6b5f31368f7870655f6a6f696e5f6f7574666974927870655f626174746c655f72616e6b5f3235927870655f626174746c655f72616e6b5f3234927870655f636f6d6d616e645f72616e6b5f34907870655f666f726d5f706c61746f6f6e8c7870655f62696e645f616d73917870655f626174746c655f72616e6b5f39917870655f626174746c655f72616e6b5f378d7870655f74685f726f757465728c7870655f74685f666c61696c8a7870655f74685f616e748a7870655f74685f616d738f7870655f74685f67726f756e645f708c7870655f74685f6169725f708c7870655f74685f686f7665728d7870655f74685f67726f756e648a7870655f74685f626672927870655f74685f61667465726275726e65728a7870655f74685f6169728c7870655f74685f636c6f616b89757365645f6f69637791757365645f616476616e6365645f61636597766973697465645f73706974666972655f74757272657498766973697465645f73706974666972655f636c6f616b656493766973697465645f73706974666972655f616192766973697465645f74616e6b5f7472617073a1766973697465645f706f727461626c655f6d616e6e65645f7475727265745f6e63a1766973697465645f706f727461626c655f6d616e6e65645f7475727265745f74728e757365645f6d61676375747465728f757365645f636861696e626c6164658f757365645f666f726365626c61646593766973697465645f77616c6c5f74757272657498766973697465645f616e6369656e745f7465726d696e616c8b766973697465645f616d738b766973697465645f616e7490766973697465645f64726f707368697091766973697465645f6c6962657261746f7294766973697465645f6c6967687467756e7368697091766973697465645f6c696768746e696e6790766973697465645f6d616772696465728f766973697465645f70726f776c657293766973697465645f71756164737465616c746890766973697465645f736b7967756172649a766973697465645f74687265656d616e686561767962756767799d766973697465645f74776f5f6d616e5f61737361756c745f627567677998766973697465645f74776f6d616e6865617679627567677998766973697465645f74776f6d616e686f766572627567677990766973697465645f76616e67756172648d766973697465645f666c61696c8e766973697465645f726f7574657293766973697465645f737769746368626c6164658e766973697465645f6175726f726193766973697465645f626174746c657761676f6e8c766973697465645f6675727993766973697465645f7175616461737361756c7496766973697465645f67616c6178795f67756e736869708e766973697465645f6170635f74728e766973697465645f6170635f767390766973697465645f6c6f64657374617290766973697465645f7068616e7461736d91766973697465645f7468756e64657265728e766973697465645f6170635f6e638f766973697465645f76756c747572658c766973697465645f7761737090766973697465645f6d6f73717569746f97766973697465645f617068656c696f6e5f666c6967687497766973697465645f617068656c696f6e5f67756e6e657297766973697465645f636f6c6f737375735f666c6967687497766973697465645f636f6c6f737375735f67756e6e657298766973697465645f706572656772696e655f666c6967687498766973697465645f706572656772696e655f67756e6e657289757365645f62616e6b95766973697465645f7265736f757263655f73696c6f9e766973697465645f63657274696669636174696f6e5f7465726d696e616c94766973697465645f6d65645f7465726d696e616c93757365645f6e616e6f5f64697370656e73657295766973697465645f73656e736f725f736869656c649a766973697465645f62726f6164636173745f77617270676174658c757365645f7068616c616e7894757365645f7068616c616e785f6176636f6d626f96757365645f7068616c616e785f666c616b636f6d626f96766973697465645f77617270676174655f736d616c6c91757365645f666c616d657468726f7765729a757365645f616e6369656e745f7475727265745f776561706f6e92766973697465645f4c4c555f736f636b657492757365645f656e657267795f67756e5f6e6397766973697465645f6d656469756d7472616e73706f72749f757365645f617068656c696f6e5f696d6d6f6c6174696f6e5f63616e6e6f6e93757365645f6772656e6164655f706c61736d6193757365645f6772656e6164655f6a616d6d657298766973697465645f736869656c645f67656e657261746f7295766973697465645f6d6f74696f6e5f73656e736f7296766973697465645f6865616c74685f6372797374616c96766973697465645f7265706169725f6372797374616c97766973697465645f76656869636c655f6372797374616c91757365645f6772656e6164655f6672616788757365645f61636598766973697465645f6164765f6d65645f7465726d696e616c8b757365645f6265616d657290757365645f626f6c745f6472697665728b757365645f6379636c65728a757365645f676175737391757365645f68756e7465727365656b657288757365645f6973708b757365645f6c616e6365728b757365645f6c61736865728e757365645f6d61656c7374726f6d8c757365645f70686f656e69788b757365645f70756c7361728d757365645f70756e69736865728e757365645f725f73686f7467756e8d757365645f7261646961746f7288757365645f72656b8d757365645f72657065617465728c757365645f726f636b6c65748c757365645f737472696b65728f757365645f73757070726573736f728c757365645f7468756d7065729c766973697465645f76616e755f636f6e74726f6c5f636f6e736f6c6598766973697465645f636170747572655f7465726d696e616c92757365645f6d696e695f636861696e67756e91757365645f6c617a655f706f696e7465728c757365645f74656c657061648b757365645f7370696b657291757365645f68656176795f736e6970657293757365645f636f6d6d616e645f75706c696e6b8d757365645f66697265626972648e757365645f666c6563686574746594757365645f68656176795f7261696c5f6265616d89757365645f696c63399a766973697465645f67656e657261746f725f7465726d696e616c8e766973697465645f6c6f636b65729a766973697465645f65787465726e616c5f646f6f725f6c6f636b9c766973697465645f6169725f76656869636c655f7465726d696e616c97766973697465645f67616c6178795f7465726d696e616c98766973697465645f696d706c616e745f7465726d696e616c99766973697465645f7365636f6e646172795f6361707475726590757365645f32356d6d5f63616e6e6f6e99757365645f6c6962657261746f725f626f6d6261726469657293766973697465645f7265706169725f73696c6f93766973697465645f76616e755f6d6f64756c6591757365645f666c61696c5f776561706f6e8b757365645f73637974686598766973697465645f7265737061776e5f7465726d696e616c8c757365645f62616c6c67756e92757365645f656e657267795f67756e5f747295757365645f616e6e69766572736172795f67756e6195757365645f616e6e69766572736172795f67756e6294757365645f616e6e69766572736172795f67756e90757365645f37356d6d5f63616e6e6f6e92757365645f6170635f6e635f776561706f6e92757365645f6170635f74725f776561706f6e92757365645f6170635f76735f776561706f6e90757365645f666c75785f63616e6e6f6e9f757365645f617068656c696f6e5f706c61736d615f726f636b65745f706f6491757365645f617068656c696f6e5f7070618c757365645f666c7578706f6494766973697465645f6266725f7465726d696e616c9e757365645f636f6c6f737375735f636c75737465725f626f6d625f706f64a0757365645f636f6c6f737375735f6475616c5f3130306d6d5f63616e6e6f6e7399757365645f636f6c6f737375735f74616e6b5f63616e6e6f6e96766973697465645f656e657267795f6372797374616c9b757365645f68656176795f6772656e6164655f6c61756e6368657298757365645f33356d6d5f726f74617279636861696e67756e8b757365645f6b6174616e6190757365645f33356d6d5f63616e6e6f6e93757365645f7265617665725f776561706f6e7396757365645f6c696768746e696e675f776561706f6e738c757365645f6d65645f61707090757365645f32306d6d5f63616e6e6f6e98766973697465645f6d6f6e6f6c6974685f616d657269736899766973697465645f6d6f6e6f6c6974685f636572797368656e97766973697465645f6d6f6e6f6c6974685f637973736f7297766973697465645f6d6f6e6f6c6974685f6573616d697299766973697465645f6d6f6e6f6c6974685f666f72736572616c99766973697465645f6d6f6e6f6c6974685f697368756e64617298766973697465645f6d6f6e6f6c6974685f7365617268757397766973697465645f6d6f6e6f6c6974685f736f6c73617292757365645f6e635f6865765f66616c636f6e99757365645f6e635f6865765f7363617474657263616e6e6f6e93757365645f6e635f6865765f73706172726f7791757365645f61726d6f725f736970686f6e9f757365645f706572656772696e655f6475616c5f6d616368696e655f67756e9f757365645f706572656772696e655f6475616c5f726f636b65745f706f647399757365645f706572656772696e655f6d65636868616d6d65729e757365645f706572656772696e655f7061727469636c655f63616e6e6f6e96757365645f706572656772696e655f73706172726f7791757365645f3130356d6d5f63616e6e6f6e92757365645f31356d6d5f636861696e67756ea0757365645f70756c7365645f7061727469636c655f616363656c657261746f7293757365645f726f74617279636861696e67756e9f766973697465645f6465636f6e737472756374696f6e5f7465726d696e616c95757365645f736b7967756172645f776561706f6e7391766973697465645f67656e657261746f7291757365645f67617573735f63616e6e6f6e89757365645f7472656b95757365645f76616e67756172645f776561706f6e73a4766973697465645f616e6369656e745f6169725f76656869636c655f7465726d696e616ca2766973697465645f616e6369656e745f65717569706d656e745f7465726d696e616c96766973697465645f6f726465725f7465726d696e616ca7766973697465645f616e6369656e745f67726f756e645f76656869636c655f7465726d696e616c9f766973697465645f67726f756e645f76656869636c655f7465726d696e616c97757365645f76756c747572655f626f6d6261726469657298757365645f76756c747572655f6e6f73655f63616e6e6f6e98757365645f76756c747572655f7461696c5f63616e6e6f6e97757365645f776173705f776561706f6e5f73797374656d91766973697465645f636861726c6965303191766973697465645f636861726c6965303291766973697465645f636861726c6965303391766973697465645f636861726c6965303491766973697465645f636861726c6965303591766973697465645f636861726c6965303691766973697465645f636861726c6965303791766973697465645f636861726c6965303891766973697465645f636861726c6965303996766973697465645f67696e6765726d616e5f6174617298766973697465645f67696e6765726d616e5f646168616b6196766973697465645f67696e6765726d616e5f6876617296766973697465645f67696e6765726d616e5f697a686199766973697465645f67696e6765726d616e5f6a616d7368696498766973697465645f67696e6765726d616e5f6d697468726198766973697465645f67696e6765726d616e5f726173686e7599766973697465645f67696e6765726d616e5f7372616f73686198766973697465645f67696e6765726d616e5f79617a61746195766973697465645f67696e6765726d616e5f7a616c8e766973697465645f736c656430318e766973697465645f736c656430328e766973697465645f736c656430348e766973697465645f736c656430358e766973697465645f736c656430368e766973697465645f736c656430378e766973697465645f736c6564303897766973697465645f736e6f776d616e5f616d657269736898766973697465645f736e6f776d616e5f636572797368656e96766973697465645f736e6f776d616e5f637973736f7296766973697465645f736e6f776d616e5f6573616d697298766973697465645f736e6f776d616e5f666f72736572616c96766973697465645f736e6f776d616e5f686f7373696e98766973697465645f736e6f776d616e5f697368756e64617297766973697465645f736e6f776d616e5f7365617268757396766973697465645f736e6f776d616e5f736f6c736172857567643036857567643035857567643034857567643033857567643032857567643031856d61703939856d61703938856d61703937856d61703936856d61703135856d61703134856d61703131856d61703038856d61703034856d61703035856d61703033856d61703031856d61703036856d61703032856d61703039856d61703037856d617031300300000091747261696e696e675f73746172745f6e638b747261696e696e675f75698c747261696e696e675f6d61700000000000000000000000000000000000000000800000003d0c04d350840240000010000602429660f80c80000c8004200c1b81480000020000c046f18a47019000019000ca4644304900000040001809e6bb052032000008001a84787211200000080003010714889c06400000100320ff0a42e4000001009e95a7342e03200000080003010408c914064000000001198990c4e4000001000060223b9b2180c800000a00081c20c92c800003600414ec172d900000040001808de1284a0320000320008ef1c336b20000078011d830e6f6400000600569c417e2c80000020000c04102502f019000008c00ce31027d99000000400018099e6146203200004b0015a7d44002f720000008000301040c18dc064000023000b1240800636400000100006020e0e92280c80000c800081650c00cfc800006400ce32a1801a59000000400018099e6fc3e03200004b00058b14680463200000080003010742610c064000043000b16c8880916400000100006020e0d01580c80000c8006714e24012cc80000020000c04cf25c190190000258001032e240307900000c8019c74470061b2000000800030133ced8fc0640000960012d9a8d00f0640000010025b9c1401e4c8000002004b6b23c03d1900000040098f585007b3200000080131a58c00f864000001002536f1c01f4c8000002004a64e2a03f190000004015e1b4580873200000080003010711f8a406400000100110a00c010ee400000100006020e2a51380c8000002002218d21021ec80000020000c041c40249019000000400af18a44043f90000004000180838b44760320000008015e38c80088320000008000301071490cc064000001002bc35890110e400000100006020e2052180c800000200221f90d0222c80000020000c041c5e447019000000400442e62e044790000004000180838af032032000000800886d08c089320000008000301071738740640000010011098898112e400000100006020e2361c80c8000002002212a1b0226c80000020000c041c512170190000004004420a32044f900000040001808389104a0320000008008874c8808a3200000080003010715907c06400000100110c0898114e400000100006020e2771a80c800000200578bd13022ac80000020000c041c424330190000004004423848045790000004000180838bfc32032000000801a86506008b320000008000301071030dc06400000100129f68a0117640000010026353110232c8000002004b69438046d90000004015e2887008eb200000080003010715909406400000100350fb8e011de400000100006020e2881980c8000002005786d0f023cc80000020000c041c4cc3b019000000400af1ba1c047b90000004000180838af872032000000800886344408fb20000008000301071620d406400000100110c10b011fe400000100006020e2870d80c800000200578f30c0240c80000020000c041c5863b019000000400442ee300483900000040001808388605e032000000801a86f03c090b200000080003010712a8fc064000001002bc0d858121e400000100006020e2521c80c800000200578b7230244c80000020000c041c49629019000000400d434026048b90000004000180838afc42032000000801a86d864091b200000080003010711989c064000001003508c8c8123e400000100006020e2a82280c8000002006a14f110248c80000020000c041c4be21019000000400af12640049390000004000180838a54720320000008015e33430092b20000008000301071228cc064000001003546e8d432400000100004f34a631139000004001b0834723120000008000204000c2ed0fa1c800000200a8432234a90000004000180952b248a0320000018004024c569d20000008000250a4d0ebc480000020000c04a24bc43019000000c00e0" def handlePkt(pkt : PlanetSidePacket) : Unit = pkt match { case ctrl : PlanetSideControlPacket => handleControlPkt(ctrl) @@ -628,6 +637,7 @@ 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.BEP = 2286231 player.Certifications += CertificationType.StandardAssault player.Certifications += CertificationType.MediumAssault player.Certifications += CertificationType.StandardExoSuit From 4ac93de0659b22df04807c21ba1502fa80439ae1 Mon Sep 17 00:00:00 2001 From: FateJH Date: Mon, 16 Oct 2017 09:33:23 -0400 Subject: [PATCH 21/23] fixed OCDM for BR24+; updated AvatarConverter; moved Cosmetics into own file as a StreamBitSize; created and implemented truncated converter for character select screen; modified DetailedREKData based on potential field --- .../converter/AvatarConverter.scala | 46 +++++++-------- .../converter/CharacterSelectConverter.scala | 58 +++---------------- .../game/objectcreate/CharacterData.scala | 35 +---------- .../packet/game/objectcreate/Cosmetics.scala | 41 +++++++++++++ .../objectcreate/DetailedCharacterData.scala | 43 +++++++++----- .../game/objectcreate/DetailedREKData.scala | 17 +++--- .../ObjectCreateDetailedMessageTest.scala | 5 +- .../src/main/scala/WorldSessionActor.scala | 28 ++++----- 8 files changed, 124 insertions(+), 149 deletions(-) create mode 100644 common/src/main/scala/net/psforever/packet/game/objectcreate/Cosmetics.scala diff --git a/common/src/main/scala/net/psforever/objects/definition/converter/AvatarConverter.scala b/common/src/main/scala/net/psforever/objects/definition/converter/AvatarConverter.scala index 7e9562d2..9ceb021b 100644 --- a/common/src/main/scala/net/psforever/objects/definition/converter/AvatarConverter.scala +++ b/common/src/main/scala/net/psforever/objects/definition/converter/AvatarConverter.scala @@ -3,7 +3,7 @@ package net.psforever.objects.definition.converter import net.psforever.objects.{EquipmentSlot, GlobalDefinitions, ImplantSlot, Player} import net.psforever.objects.equipment.Equipment -import net.psforever.packet.game.objectcreate.{BasicCharacterData, CharacterAppearanceData, CharacterData, DetailedCharacterData, DrawnSlot, ImplantEffects, ImplantEntry, InternalSlot, InventoryData, PlacementData, RibbonBars, UniformStyle} +import net.psforever.packet.game.objectcreate.{BasicCharacterData, CharacterAppearanceData, CharacterData, Cosmetics, DetailedCharacterData, DrawnSlot, ImplantEffects, ImplantEntry, InternalSlot, InventoryData, PlacementData, RibbonBars, UniformStyle} import net.psforever.types.{GrenadeState, ImplantType} import scala.annotation.tailrec @@ -40,8 +40,9 @@ class AvatarConverter extends ObjectCreateConverter[Player]() { obj.Stamina, obj.Certifications.toList.sortBy(_.id), //TODO is sorting necessary? MakeImplantEntries(obj), - "xpe_battle_rank_10" :: Nil, //TODO fte list + List.empty[String], //TODO fte list List.empty[String], //TODO tutorial list + MakeCosmetics(obj.BEP), InventoryData((MakeHolsters(obj, BuildDetailedEquipment) ++ MakeFifthSlot(obj) ++ MakeInventory(obj)).sortBy(_.parentSlot)), GetDrawnSlot(obj) ) @@ -132,7 +133,7 @@ class AvatarConverter extends ObjectCreateConverter[Player]() { * @see `ImplantEntry` in `DetailedCharacterData` */ private def MakeImplantEntries(obj : Player) : List[ImplantEntry] = { - val numImplants : Int = NumberOfImplantSlots(obj.BEP) + val numImplants : Int = DetailedCharacterData.numberOfImplantSlots(obj.BEP) val implants = obj.Implants (0 until numImplants).map(index => { val slot = implants(index) @@ -150,27 +151,6 @@ class AvatarConverter extends ObjectCreateConverter[Player]() { }).toList } - /** - * A player's battle rank, determined by their battle experience points, determines how many implants to which they have access. - * Starting with "no implants" at BR1, a player earns one at each of the three ranks: BR6, BR12, and BR18. - * @param bep battle experience points - * @return the number of accessible implant slots - */ - private def NumberOfImplantSlots(bep : Long) : Int = { - if(bep > 754370) { //BR18+ - 3 - } - else if(bep > 197753) { //BR12+ - 2 - } - else if(bep > 29999) { //BR6+ - 1 - } - else { //BR1+ - 0 - } - } - /** * Find an active implant whose effect will be displayed on this player. * @param iter an `Iterator` of `ImplantSlot` objects @@ -200,6 +180,20 @@ class AvatarConverter extends ObjectCreateConverter[Player]() { } } + /** + * Should this player be of battle rank 24 or higher, they will have a mandatory cosmetics object. + * @param bep battle experience points + * @see `Cosmetics` + * @return the `Cosmetics` options + */ + protected def MakeCosmetics(bep : Long) : Option[Cosmetics] = + if(DetailedCharacterData.isBR24(bep)) { + Some(Cosmetics(false, false, false, false, false)) + } + else { + None + } + /** * Given a player with an inventory, convert the contents of that inventory into converted-decoded packet data. * The inventory is not represented in a `0x17` `Player`, so the conversion is only valid for `0x18` avatars. @@ -260,7 +254,7 @@ class AvatarConverter extends ObjectCreateConverter[Player]() { * @param equip the game object * @return the game object in decoded packet form */ - private def BuildDetailedEquipment(index : Int, equip : Equipment) : InternalSlot = { + protected def BuildDetailedEquipment(index : Int, equip : Equipment) : InternalSlot = { InternalSlot(equip.Definition.ObjectId, equip.GUID, index, equip.Definition.Packet.DetailedConstructorData(equip).get) } @@ -298,7 +292,7 @@ class AvatarConverter extends ObjectCreateConverter[Player]() { * @param obj the `Player` game object * @return the holster's Enumeration value */ - private def GetDrawnSlot(obj : Player) : DrawnSlot.Value = { + protected def GetDrawnSlot(obj : Player) : DrawnSlot.Value = { try { DrawnSlot(obj.DrawnSlot) } catch { case _ : Exception => DrawnSlot.None } } } diff --git a/common/src/main/scala/net/psforever/objects/definition/converter/CharacterSelectConverter.scala b/common/src/main/scala/net/psforever/objects/definition/converter/CharacterSelectConverter.scala index 4abca57b..293c71ce 100644 --- a/common/src/main/scala/net/psforever/objects/definition/converter/CharacterSelectConverter.scala +++ b/common/src/main/scala/net/psforever/objects/definition/converter/CharacterSelectConverter.scala @@ -1,21 +1,20 @@ // Copyright (c) 2017 PSForever package net.psforever.objects.definition.converter -import net.psforever.objects.GlobalDefinitions.{advanced_regen, darklight_vision, personal_shield, surge} -import net.psforever.objects.{EquipmentSlot, GlobalDefinitions, ImplantSlot, Player} +import net.psforever.objects.{EquipmentSlot, Player} import net.psforever.objects.equipment.Equipment -import net.psforever.packet.game.objectcreate.{BasicCharacterData, CharacterAppearanceData, CharacterData, DetailedCharacterData, DrawnSlot, ImplantEffects, ImplantEntry, InternalSlot, InventoryData, PlacementData, RibbonBars} +import net.psforever.packet.game.objectcreate.{BasicCharacterData, CharacterAppearanceData, CharacterData, DetailedCharacterData, ImplantEntry, InternalSlot, InventoryData, PlacementData, RibbonBars} import net.psforever.types.{GrenadeState, ImplantType} import scala.annotation.tailrec import scala.util.{Failure, Success, Try} /** - * `CharacterSelectConverter` is based on `AvatarConverter` - * but it is tailored for appearance of the player character on the character selection screen only. + * `CharacterSelectConverter` is a simplified `AvatarConverter` + * that is tailored for appearance of the player character on the character selection screen. * Details that would not be apparent on that screen such as implants or certifications are ignored. */ -class CharacterSelectConverter extends ObjectCreateConverter[Player]() { +class CharacterSelectConverter extends AvatarConverter { override def ConstructorData(obj : Player) : Try[CharacterData] = Failure(new Exception("CharacterSelectConverter should not be used to generate CharacterData")) override def DetailedConstructorData(obj : Player) : Try[DetailedCharacterData] = { @@ -26,8 +25,9 @@ class CharacterSelectConverter extends ObjectCreateConverter[Player]() { obj.CEP, 1, 1, 0, 1, 1, Nil, - MakeImplantEntries(obj), + MakeImplantEntries(obj), //necessary for correct stream length Nil, Nil, + MakeCosmetics(obj.BEP), InventoryData(recursiveMakeHolsters(obj.Holsters().iterator)), GetDrawnSlot(obj) ) @@ -69,39 +69,7 @@ class CharacterSelectConverter extends ObjectCreateConverter[Player]() { * @see `ImplantEntry` in `DetailedCharacterData` */ private def MakeImplantEntries(obj : Player) : List[ImplantEntry] = { - List.fill[ImplantEntry](NumberOfImplantSlots(obj.BEP))(ImplantEntry(ImplantType.None, None)) - } - - /** - * A player's battle rank, determined by their battle experience points, determines how many implants to which they have access. - * Starting with "no implants" at BR1, a player earns one at each of the three ranks: BR6, BR12, and BR18. - * @param bep battle experience points - * @return the number of accessible implant slots - */ - private def NumberOfImplantSlots(bep : Long) : Int = { - if(bep > 754370) { //BR18+ - 3 - } - else if(bep > 197753) { //BR12+ - 2 - } - else if(bep > 29999) { //BR6+ - 1 - } - else { //BR1+ - 0 - } - } - - /** - * A builder method for turning an object into `0x18` decoded packet form. - * @param index the position of the object - * @param equip the game object - * @see `AvatarConverter.BuildDetailedEquipment` - * @return the game object in decoded packet form - */ - private def BuildDetailedEquipment(index : Int, equip : Equipment) : InternalSlot = { - InternalSlot(equip.Definition.ObjectId, equip.GUID, index, equip.Definition.Packet.DetailedConstructorData(equip).get) + List.fill[ImplantEntry](DetailedCharacterData.numberOfImplantSlots(obj.BEP))(ImplantEntry(ImplantType.None, None)) } /** @@ -131,14 +99,4 @@ class CharacterSelectConverter extends ObjectCreateConverter[Player]() { } } } - - /** - * Resolve which holster the player has drawn, if any. - * @param obj the `Player` game object - * @see `AvatarConverter.GetDrawnSlot` - * @return the holster's Enumeration value - */ - private def GetDrawnSlot(obj : Player) : DrawnSlot.Value = { - try { DrawnSlot(obj.DrawnSlot) } catch { case _ : Exception => DrawnSlot.None } - } } diff --git a/common/src/main/scala/net/psforever/packet/game/objectcreate/CharacterData.scala b/common/src/main/scala/net/psforever/packet/game/objectcreate/CharacterData.scala index a3aa0009..51d08d63 100644 --- a/common/src/main/scala/net/psforever/packet/game/objectcreate/CharacterData.scala +++ b/common/src/main/scala/net/psforever/packet/game/objectcreate/CharacterData.scala @@ -41,24 +41,6 @@ object UniformStyle extends Enumeration { implicit val codec = PacketHelpers.createEnumerationCodec(this, uintL(3)) } -/** - * The different cosmetics that a player can apply to their model's head.
- *
- * The player gets the ability to apply these minor modifications at battle rank twenty-four, just one rank before the third uniform upgrade. - * @param no_helmet removes the current helmet on the reinforced exo-suit and the agile exo-suit; - * all other cosmetics require `no_helmet` to be `true` before they can be seen - * @param beret player dons a beret - * @param sunglasses player dons sunglasses - * @param earpiece player dons an earpiece on the left - * @param brimmed_cap player dons a cap; - * the cap overrides the beret, if both are selected - */ -final case class Cosmetics(no_helmet : Boolean, - beret : Boolean, - sunglasses : Boolean, - earpiece : Boolean, - brimmed_cap : Boolean) - /** * A part of a representation of the avatar portion of `ObjectCreateMessage` packet data. * This densely-packed information outlines most of the specifics of depicting some other character.
@@ -118,7 +100,7 @@ final case class CharacterData(appearance : CharacterAppearanceData, //factor guard bool values into the base size, not its corresponding optional field val appearanceSize : Long = appearance.bitsize val effectsSize : Long = if(implant_effects.isDefined) { 4L } else { 0L } - val cosmeticsSize : Long = if(cosmetics.isDefined) { 5L } else { 0L } + val cosmeticsSize : Long = if(cosmetics.isDefined) { cosmetics.get.bitsize } else { 0L } val inventorySize : Long = if(inventory.isDefined) { inventory.get.bitsize } else { 0L } 32L + appearanceSize + effectsSize + cosmeticsSize + inventorySize } @@ -141,19 +123,6 @@ object CharacterData extends Marshallable[CharacterData] { def apply(appearance : CharacterAppearanceData, health : Int, armor : Int, uniform : UniformStyle.Value, cr : Int, implant_effects : Option[ImplantEffects.Value], cosmetics : Option[Cosmetics], inv : InventoryData, drawn_slot : DrawnSlot.Value) : CharacterData = new CharacterData(appearance, health, armor, uniform, cr, implant_effects, cosmetics, Some(inv), drawn_slot) - /** - * Check for the bit flags for the cosmetic items. - * These flags are only valid if the player has acquired their third uniform upgrade. - * @see `UniformStyle.ThirdUpgrade` - */ - private val cosmeticsCodec : Codec[Cosmetics] = ( - ("no_helmet" | bool) :: - ("beret" | bool) :: - ("sunglasses" | bool) :: - ("earpiece" | bool) :: - ("brimmed_cap" | bool) - ).as[Cosmetics] - implicit val codec : Codec[CharacterData] = ( ("app" | CharacterAppearanceData.codec) :: ("health" | uint8L) :: //dead state when health == 0 @@ -163,7 +132,7 @@ object CharacterData extends Marshallable[CharacterData] { ("command_rank" | uintL(3)) :: bool :: //stream misalignment when != 1 optional(bool, "implant_effects" | ImplantEffects.codec) :: - conditional(style == UniformStyle.ThirdUpgrade, "cosmetics" | cosmeticsCodec) :: + conditional(style == UniformStyle.ThirdUpgrade, "cosmetics" | Cosmetics.codec) :: optional(bool, "inventory" | InventoryData.codec) :: ("drawn_slot" | DrawnSlot.codec) :: bool //usually false diff --git a/common/src/main/scala/net/psforever/packet/game/objectcreate/Cosmetics.scala b/common/src/main/scala/net/psforever/packet/game/objectcreate/Cosmetics.scala new file mode 100644 index 00000000..da3f85b2 --- /dev/null +++ b/common/src/main/scala/net/psforever/packet/game/objectcreate/Cosmetics.scala @@ -0,0 +1,41 @@ +// Copyright (c) 2017 PSForever +package net.psforever.packet.game.objectcreate + +import scodec.codecs._ +import scodec.Codec + +/** + * The different cosmetics that a player can apply to their character model's head.
+ *
+ * The player gets the ability to apply these minor modifications at battle rank twenty-four, just one rank before the third uniform upgrade. + * These flags are only valid if the player has: + * for `DetailedCharacterData`, achieved at least battle rank twenty-four (battle experience points greater than 2286230), + * or, for `CharacterData`, achieved at least battle rank twenty-five (acquired their third uniform upgrade). + * `CharacterData`, as implied, will not display these options until one battle rank after they would have become available. + * @param no_helmet removes the current helmet on the reinforced exo-suit and the agile exo-suit; + * all other cosmetics require `no_helmet` to be `true` before they can be seen + * @param beret player dons a beret + * @param sunglasses player dons sunglasses + * @param earpiece player dons an earpiece on the left + * @param brimmed_cap player dons a cap; + * the cap overrides the beret, if both are selected + * @see `UniformStyle.ThirdUpgrade` + */ +final case class Cosmetics(no_helmet : Boolean, + beret : Boolean, + sunglasses : Boolean, + earpiece : Boolean, + brimmed_cap : Boolean + ) extends StreamBitSize { + override def bitsize : Long = 5L +} + +object Cosmetics { + implicit val codec : Codec[Cosmetics] = ( + ("no_helmet" | bool) :: + ("beret" | bool) :: + ("sunglasses" | bool) :: + ("earpiece" | bool) :: + ("brimmed_cap" | bool) + ).as[Cosmetics] +} diff --git a/common/src/main/scala/net/psforever/packet/game/objectcreate/DetailedCharacterData.scala b/common/src/main/scala/net/psforever/packet/game/objectcreate/DetailedCharacterData.scala index ccb0d3f2..c2fa2d74 100644 --- a/common/src/main/scala/net/psforever/packet/game/objectcreate/DetailedCharacterData.scala +++ b/common/src/main/scala/net/psforever/packet/game/objectcreate/DetailedCharacterData.scala @@ -70,6 +70,9 @@ final case class ImplantEntry(implant : ImplantType.Value, * @param tutorials the `List` of tutorials completed by this avatar; * the size field is a 32-bit number; * the first entry may be padded + * @param cosmetics optional decorative features that are added to the player's head model by console/chat commands; + * they become available at battle rank 24; + * these flags do not exist if they are not applicable * @param inventory the avatar's inventory * @param drawn_slot the holster that is initially drawn * @see `CharacterAppearanceData`
@@ -93,6 +96,7 @@ final case class DetailedCharacterData(appearance : CharacterAppearanceData, implants : List[ImplantEntry], firstTimeEvents : List[String], tutorials : List[String], + cosmetics : Option[Cosmetics], inventory : Option[InventoryData], drawn_slot : DrawnSlot.Value = DrawnSlot.None ) extends ConstructorData { @@ -116,13 +120,16 @@ final case class DetailedCharacterData(appearance : CharacterAppearanceData, for(str <- tutorials) { tutorialListSize += StreamBitSize.stringBitSize(str) } + val br24 = DetailedCharacterData.isBR24(bep) //character is at least BR24 + val extraBitSize : Long = if(br24) { 33L } else { 46L } + val cosmeticsSize : Long = if(br24) { cosmetics.get.bitsize } else { 0L } val inventorySize : Long = if(inventory.isDefined) { //inventory inventory.get.bitsize } else { 0L } - 649L + appearanceSize + certSize + implantSize + eventListSize + tutorialListSize + inventorySize + 603L + appearanceSize + certSize + implantSize + eventListSize + extraBitSize + cosmeticsSize + tutorialListSize + inventorySize } } @@ -145,8 +152,8 @@ object DetailedCharacterData extends Marshallable[DetailedCharacterData] { * @param drawn_slot the holster that is initially drawn * @return a `DetailedCharacterData` object */ - def apply(appearance : CharacterAppearanceData, bep : Long, cep : Long, healthMax : Int, health : Int, armor : Int, staminaMax : Int, stamina : Int, certs : List[CertificationType.Value], implants : List[ImplantEntry], firstTimeEvents : List[String], tutorials : List[String], inventory : InventoryData, drawn_slot : DrawnSlot.Value) : DetailedCharacterData = - new DetailedCharacterData(appearance, bep, cep, healthMax, health, armor, 1, 7, 7, staminaMax, stamina, certs, implants, firstTimeEvents, tutorials, Some(inventory), drawn_slot) + def apply(appearance : CharacterAppearanceData, bep : Long, cep : Long, healthMax : Int, health : Int, armor : Int, staminaMax : Int, stamina : Int, certs : List[CertificationType.Value], implants : List[ImplantEntry], firstTimeEvents : List[String], tutorials : List[String], cosmetics : Option[Cosmetics], inventory : InventoryData, drawn_slot : DrawnSlot.Value) : DetailedCharacterData = + new DetailedCharacterData(appearance, bep, cep, healthMax, health, armor, 1, 7, 7, staminaMax, stamina, certs, implants, firstTimeEvents, tutorials, cosmetics, Some(inventory), drawn_slot) /** * `Codec` for entries in the `List` of implants. @@ -179,7 +186,7 @@ object DetailedCharacterData extends Marshallable[DetailedCharacterData] { * @param bep battle experience points * @return the number of accessible implant slots */ - private def numberOfImplantSlots(bep : Long) : Int = { + def numberOfImplantSlots(bep : Long) : Int = { if(bep > 754370) { //BR18+ 3 } @@ -269,6 +276,8 @@ object DetailedCharacterData extends Marshallable[DetailedCharacterData] { } } + def isBR24(bep : Long) : Boolean = bep > 2286230 + implicit val codec : Codec[DetailedCharacterData] = ( ("appearance" | CharacterAppearanceData.codec) >>:~ { app => ("bep" | uint32L) >>:~ { bep => @@ -297,10 +306,14 @@ object DetailedCharacterData extends Marshallable[DetailedCharacterData] { (("tutorial_length" | uint32L) >>:~ { len2 => conditional(len2 > 0, "tutorial_firstEntry" | PacketHelpers.encodedStringAligned(tutPadding(len, len2, implantFieldPadding(implants, CharacterAppearanceData.altModelBit(app))))) :: ("tutorial_list" | PacketHelpers.listOfNSized(len2 - 1, PacketHelpers.encodedString)) :: - ignore(200) :: - conditional(true, "inventory" | InventoryData.codec_detailed) :: - ("drawn_slot" | DrawnSlot.codec) :: - bool //usually false + ignore(160) :: + (bool >>:~ { br24 => //BR24+ + newcodecs.binary_choice(br24, ignore(33), ignore(46)) :: + conditional(br24, Cosmetics.codec) :: + optional(bool, "inventory" | InventoryData.codec_detailed) :: + ("drawn_slot" | DrawnSlot.codec) :: + bool //usually false + }) }) }) }) @@ -308,14 +321,14 @@ object DetailedCharacterData extends Marshallable[DetailedCharacterData] { } ).exmap[DetailedCharacterData] ( { - case app :: bep :: cep :: _ :: hpmax :: hp :: _ :: armor :: _ :: u1 :: _ :: u2 :: u3 :: stamax :: stam :: _ :: certs :: _ :: _ :: implants :: _ :: _ :: fte0 :: fte1 :: _ :: tut0 :: tut1 :: _ :: inv :: drawn :: false :: HNil => + case app :: bep :: cep :: _ :: hpmax :: hp :: _ :: armor :: _ :: u1 :: _ :: u2 :: u3 :: stamax :: stam :: _ :: certs :: _ :: _ :: implants :: _ :: _ :: fte0 :: fte1 :: _ :: tut0 :: tut1 :: _ :: _ :: _ :: cosmetics :: inv :: drawn :: false :: HNil => //prepend the displaced first elements to their lists - val fteList : List[String] = if(fte0.isDefined) { fte0.get +: fte1 } else fte1 - val tutList : List[String] = if(tut0.isDefined) { tut0.get +: tut1 } else tut1 - Attempt.successful(DetailedCharacterData(app, bep, cep, hpmax, hp, armor, u1, u2, u3, stamax, stam, certs, implants, fteList, tutList, inv, drawn)) + val fteList : List[String] = if(fte0.isDefined) { fte0.get +: fte1 } else { fte1 } + val tutList : List[String] = if(tut0.isDefined) { tut0.get +: tut1 } else { tut1 } + Attempt.successful(DetailedCharacterData(app, bep, cep, hpmax, hp, armor, u1, u2, u3, stamax, stam, certs, implants, fteList, tutList, cosmetics, inv, drawn)) }, { - case DetailedCharacterData(app, bep, cep, hpmax, hp, armor, u1, u2, u3, stamax, stam, certs, implants, fteList, tutList, inv, drawn) => + case DetailedCharacterData(app, bep, cep, hpmax, hp, armor, u1, u2, u3, stamax, stam, certs, implants, fteList, tutList, cos, inv, drawn) => val implantCapacity : Int = numberOfImplantSlots(bep) val implantList = if(implants.length > implantCapacity) { implants.slice(0, implantCapacity) @@ -334,7 +347,9 @@ object DetailedCharacterData extends Marshallable[DetailedCharacterData] { case ((f : String) +: (rest : List[String])) => (Some(f), rest) case Nil => (None, Nil) } - Attempt.successful(app :: bep :: cep :: () :: hpmax :: hp :: () :: armor :: () :: u1 :: () :: u2 :: u3 :: stamax :: stam :: () :: certs :: None :: () :: implantList :: () :: fteList.size.toLong :: firstEvent :: fteListCopy :: tutList.size.toLong :: firstTutorial :: tutListCopy :: () :: inv :: drawn :: false :: HNil) + val br24 : Boolean = isBR24(bep) + val cosmetics : Option[Cosmetics] = if(br24) { cos } else { None } + Attempt.successful(app :: bep :: cep :: () :: hpmax :: hp :: () :: armor :: () :: u1 :: () :: u2 :: u3 :: stamax :: stam :: () :: certs :: None :: () :: implantList :: () :: fteList.size.toLong :: firstEvent :: fteListCopy :: tutList.size.toLong :: firstTutorial :: tutListCopy :: () :: br24 :: () :: cosmetics :: inv :: drawn :: false :: HNil) } ) } diff --git a/common/src/main/scala/net/psforever/packet/game/objectcreate/DetailedREKData.scala b/common/src/main/scala/net/psforever/packet/game/objectcreate/DetailedREKData.scala index de0912c7..c4c19e62 100644 --- a/common/src/main/scala/net/psforever/packet/game/objectcreate/DetailedREKData.scala +++ b/common/src/main/scala/net/psforever/packet/game/objectcreate/DetailedREKData.scala @@ -11,9 +11,12 @@ import shapeless.{::, HNil} * This data will help construct the "tool" called a Remote Electronics Kit.
*
* Of note is the first portion of the data which resembles the `DetailedWeaponData` format. - * @param unk na + * @param unk1 na + * @param unk2 na */ -final case class DetailedREKData(unk : Int) extends ConstructorData { +final case class DetailedREKData(unk1 : Int, + unk2 : Int = 0 + ) extends ConstructorData { override def bitsize : Long = 67L } @@ -25,17 +28,17 @@ object DetailedREKData extends Marshallable[DetailedREKData] { uint4L :: uint16L :: uint4L :: - uintL(15) + ("unk2" | uintL(15)) ).exmap[DetailedREKData] ( { - case code :: 8 :: 0 :: 2 :: 0 :: 8 :: 0 :: HNil => - Attempt.successful(DetailedREKData(code)) + case code :: 8 :: 0 :: 2 :: 0 :: 8 :: unk2 :: HNil => + Attempt.successful(DetailedREKData(code, unk2)) case _ => Attempt.failure(Err("invalid rek data format")) }, { - case DetailedREKData(code) => - Attempt.successful(code :: 8 :: 0 :: 2 :: 0 :: 8 :: 0 :: HNil) + case DetailedREKData(code, unk2) => + Attempt.successful(code :: 8 :: 0 :: 2 :: 0 :: 8 :: unk2 :: HNil) } ) } diff --git a/common/src/test/scala/game/ObjectCreateDetailedMessageTest.scala b/common/src/test/scala/game/ObjectCreateDetailedMessageTest.scala index f6163520..6582f9ff 100644 --- a/common/src/test/scala/game/ObjectCreateDetailedMessageTest.scala +++ b/common/src/test/scala/game/ObjectCreateDetailedMessageTest.scala @@ -146,7 +146,8 @@ class ObjectCreateDetailedMessageTest extends Specification { parent.get.guid mustEqual PlanetSideGUID(75) parent.get.slot mustEqual 1 data.isDefined mustEqual true - data.get.asInstanceOf[DetailedREKData].unk mustEqual 4 + data.get.asInstanceOf[DetailedREKData].unk1 mustEqual 4 + data.get.asInstanceOf[DetailedREKData].unk2 mustEqual 0 case _ => ko } @@ -231,6 +232,7 @@ class ObjectCreateDetailedMessageTest extends Specification { char.firstTimeEvents(2) mustEqual "used_beamer" char.firstTimeEvents(3) mustEqual "map13" char.tutorials.size mustEqual 0 + char.cosmetics.isDefined mustEqual false char.inventory.isDefined mustEqual true val inventory = char.inventory.get.contents inventory.size mustEqual 10 @@ -428,6 +430,7 @@ class ObjectCreateDetailedMessageTest extends Specification { List(), "xpe_sanctuary_help" :: "xpe_th_firemodes" :: "used_beamer" :: "map13" :: Nil, List.empty, + None, Some(InventoryData(inv)), DrawnSlot.Pistol1 ) diff --git a/pslogin/src/main/scala/WorldSessionActor.scala b/pslogin/src/main/scala/WorldSessionActor.scala index 5c532084..ca7ea356 100644 --- a/pslogin/src/main/scala/WorldSessionActor.scala +++ b/pslogin/src/main/scala/WorldSessionActor.scala @@ -425,21 +425,15 @@ class WorldSessionActor extends Actor with MDCContextAware { val stamina = player.Stamina val armor = player.Armor player.Spawn -// sendResponse(PacketCoding.CreateGamePacket(0, -// ObjectCreateDetailedMessage(ObjectClass.avatar, player.GUID, player.Definition.Packet.DetailedConstructorData(player).get) -// )) - sendRawResponse( - objectHex - ) + sendResponse(PacketCoding.CreateGamePacket(0, + ObjectCreateDetailedMessage(ObjectClass.avatar, player.GUID, converter.DetailedConstructorData(player).get) + )) if(health > 0) { //player can not be dead; stay spawned as alive player.Health = health player.Stamina = stamina player.Armor = armor } - log.info( - PacketCoding.DecodePacket(objectHex).require.toString - ) - sendResponse(PacketCoding.CreateGamePacket(0, CharacterInfoMessage(15,PlanetSideZoneID(10000), 41605313, PlanetSideGUID(75), false, 6404428))) + sendResponse(PacketCoding.CreateGamePacket(0, CharacterInfoMessage(15,PlanetSideZoneID(10000), 41605313, player.GUID, false, 6404428))) RemoveCharacterSelectScreenGUID(player) sendResponse(PacketCoding.CreateGamePacket(0, CharacterInfoMessage(0, PlanetSideZoneID(1), 0, PlanetSideGUID(0), true, 0))) @@ -500,15 +494,14 @@ class WorldSessionActor extends Actor with MDCContextAware { log.info("Load the now-registered player") //load the now-registered player tplayer.Spawn -// sendResponse(PacketCoding.CreateGamePacket(0, -// ObjectCreateDetailedMessage(ObjectClass.avatar, tplayer.GUID, tplayer.Definition.Packet.DetailedConstructorData(tplayer).get) -// )) - sendRawResponse(objectHex) + sendResponse(PacketCoding.CreateGamePacket(0, + ObjectCreateDetailedMessage(ObjectClass.avatar, tplayer.GUID, tplayer.Definition.Packet.DetailedConstructorData(tplayer).get) + )) avatarService ! AvatarServiceMessage(tplayer.Continent, AvatarAction.LoadPlayer(tplayer.GUID, tplayer.Definition.Packet.ConstructorData(tplayer).get)) log.debug(s"ObjectCreateDetailedMessage: ${tplayer.Definition.Packet.DetailedConstructorData(tplayer).get}") case SetCurrentAvatar(tplayer) => - val guid = PlanetSideGUID(75)//tplayer.GUID + val guid = tplayer.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))) @@ -546,7 +539,7 @@ class WorldSessionActor extends Actor with MDCContextAware { case default => log.warn(s"Invalid packet class received: $default") } -val objectHex = hex"18 2c e0 00 00 bc 84 B0 00 0b ea 00 6c 7d f1 10 00 00 02 40 00 08 60 4b 00 69 00 43 00 6b 00 4a 00 72 00 02 31 3a cc 82 c0 00 00 00 00 00 00 00 00 3e df 42 00 20 00 0e 00 40 43 40 4c 04 00 02 e8 00 00 03 a8 00 00 01 9c 04 00 00 b8 99 84 00 0e 68 28 00 00 00 00 00 00 00 00 00 00 00 00 01 90 01 90 00 c8 00 00 01 00 7e c8 00 5c 00 00 01 29 c1 cc 80 00 00 00 00 00 00 00 00 00 00 00 00 03 c0 00 40 81 01 c4 45 46 86 c8 88 c9 09 4a 4a 80 50 0c 13 00 00 15 00 80 00 48 00 7870655f6f766572686561645f6d6170 8d7870655f776172705f676174658f7870655f666f726d5f6f75746669748c7870655f626c61636b6f7073927870655f636f6d6d616e645f72616e6b5f35927870655f636f6d6d616e645f72616e6b5f33927870655f73616e6374756172795f68656c70927870655f626174746c655f72616e6b5f3133927870655f626174746c655f72616e6b5f3132927870655f626174746c655f72616e6b5f3130927870655f626174746c655f72616e6b5f3134927870655f626174746c655f72616e6b5f3135937870655f6f72626974616c5f73687574746c658c7870655f64726f705f706f64917870655f62696e645f666163696c697479917870655f626174746c655f72616e6b5f33917870655f626174746c655f72616e6b5f35917870655f626174746c655f72616e6b5f348e7870655f6a6f696e5f73717561648e7870655f666f726d5f7371756164927870655f696e7374616e745f616374696f6e917870655f626174746c655f72616e6b5f32937870655f776172705f676174655f7573616765917870655f626174746c655f72616e6b5f38927870655f626174746c655f72616e6b5f3131917870655f626174746c655f72616e6b5f368e7870655f6d61696c5f616c657274927870655f636f6d6d616e645f72616e6b5f31927870655f626174746c655f72616e6b5f3230927870655f626174746c655f72616e6b5f3138927870655f626174746c655f72616e6b5f3139907870655f6a6f696e5f706c61746f6f6e927870655f626174746c655f72616e6b5f3137927870655f626174746c655f72616e6b5f31368f7870655f6a6f696e5f6f7574666974927870655f626174746c655f72616e6b5f3235927870655f626174746c655f72616e6b5f3234927870655f636f6d6d616e645f72616e6b5f34907870655f666f726d5f706c61746f6f6e8c7870655f62696e645f616d73917870655f626174746c655f72616e6b5f39917870655f626174746c655f72616e6b5f378d7870655f74685f726f757465728c7870655f74685f666c61696c8a7870655f74685f616e748a7870655f74685f616d738f7870655f74685f67726f756e645f708c7870655f74685f6169725f708c7870655f74685f686f7665728d7870655f74685f67726f756e648a7870655f74685f626672927870655f74685f61667465726275726e65728a7870655f74685f6169728c7870655f74685f636c6f616b89757365645f6f69637791757365645f616476616e6365645f61636597766973697465645f73706974666972655f74757272657498766973697465645f73706974666972655f636c6f616b656493766973697465645f73706974666972655f616192766973697465645f74616e6b5f7472617073a1766973697465645f706f727461626c655f6d616e6e65645f7475727265745f6e63a1766973697465645f706f727461626c655f6d616e6e65645f7475727265745f74728e757365645f6d61676375747465728f757365645f636861696e626c6164658f757365645f666f726365626c61646593766973697465645f77616c6c5f74757272657498766973697465645f616e6369656e745f7465726d696e616c8b766973697465645f616d738b766973697465645f616e7490766973697465645f64726f707368697091766973697465645f6c6962657261746f7294766973697465645f6c6967687467756e7368697091766973697465645f6c696768746e696e6790766973697465645f6d616772696465728f766973697465645f70726f776c657293766973697465645f71756164737465616c746890766973697465645f736b7967756172649a766973697465645f74687265656d616e686561767962756767799d766973697465645f74776f5f6d616e5f61737361756c745f627567677998766973697465645f74776f6d616e6865617679627567677998766973697465645f74776f6d616e686f766572627567677990766973697465645f76616e67756172648d766973697465645f666c61696c8e766973697465645f726f7574657293766973697465645f737769746368626c6164658e766973697465645f6175726f726193766973697465645f626174746c657761676f6e8c766973697465645f6675727993766973697465645f7175616461737361756c7496766973697465645f67616c6178795f67756e736869708e766973697465645f6170635f74728e766973697465645f6170635f767390766973697465645f6c6f64657374617290766973697465645f7068616e7461736d91766973697465645f7468756e64657265728e766973697465645f6170635f6e638f766973697465645f76756c747572658c766973697465645f7761737090766973697465645f6d6f73717569746f97766973697465645f617068656c696f6e5f666c6967687497766973697465645f617068656c696f6e5f67756e6e657297766973697465645f636f6c6f737375735f666c6967687497766973697465645f636f6c6f737375735f67756e6e657298766973697465645f706572656772696e655f666c6967687498766973697465645f706572656772696e655f67756e6e657289757365645f62616e6b95766973697465645f7265736f757263655f73696c6f9e766973697465645f63657274696669636174696f6e5f7465726d696e616c94766973697465645f6d65645f7465726d696e616c93757365645f6e616e6f5f64697370656e73657295766973697465645f73656e736f725f736869656c649a766973697465645f62726f6164636173745f77617270676174658c757365645f7068616c616e7894757365645f7068616c616e785f6176636f6d626f96757365645f7068616c616e785f666c616b636f6d626f96766973697465645f77617270676174655f736d616c6c91757365645f666c616d657468726f7765729a757365645f616e6369656e745f7475727265745f776561706f6e92766973697465645f4c4c555f736f636b657492757365645f656e657267795f67756e5f6e6397766973697465645f6d656469756d7472616e73706f72749f757365645f617068656c696f6e5f696d6d6f6c6174696f6e5f63616e6e6f6e93757365645f6772656e6164655f706c61736d6193757365645f6772656e6164655f6a616d6d657298766973697465645f736869656c645f67656e657261746f7295766973697465645f6d6f74696f6e5f73656e736f7296766973697465645f6865616c74685f6372797374616c96766973697465645f7265706169725f6372797374616c97766973697465645f76656869636c655f6372797374616c91757365645f6772656e6164655f6672616788757365645f61636598766973697465645f6164765f6d65645f7465726d696e616c8b757365645f6265616d657290757365645f626f6c745f6472697665728b757365645f6379636c65728a757365645f676175737391757365645f68756e7465727365656b657288757365645f6973708b757365645f6c616e6365728b757365645f6c61736865728e757365645f6d61656c7374726f6d8c757365645f70686f656e69788b757365645f70756c7361728d757365645f70756e69736865728e757365645f725f73686f7467756e8d757365645f7261646961746f7288757365645f72656b8d757365645f72657065617465728c757365645f726f636b6c65748c757365645f737472696b65728f757365645f73757070726573736f728c757365645f7468756d7065729c766973697465645f76616e755f636f6e74726f6c5f636f6e736f6c6598766973697465645f636170747572655f7465726d696e616c92757365645f6d696e695f636861696e67756e91757365645f6c617a655f706f696e7465728c757365645f74656c657061648b757365645f7370696b657291757365645f68656176795f736e6970657293757365645f636f6d6d616e645f75706c696e6b8d757365645f66697265626972648e757365645f666c6563686574746594757365645f68656176795f7261696c5f6265616d89757365645f696c63399a766973697465645f67656e657261746f725f7465726d696e616c8e766973697465645f6c6f636b65729a766973697465645f65787465726e616c5f646f6f725f6c6f636b9c766973697465645f6169725f76656869636c655f7465726d696e616c97766973697465645f67616c6178795f7465726d696e616c98766973697465645f696d706c616e745f7465726d696e616c99766973697465645f7365636f6e646172795f6361707475726590757365645f32356d6d5f63616e6e6f6e99757365645f6c6962657261746f725f626f6d6261726469657293766973697465645f7265706169725f73696c6f93766973697465645f76616e755f6d6f64756c6591757365645f666c61696c5f776561706f6e8b757365645f73637974686598766973697465645f7265737061776e5f7465726d696e616c8c757365645f62616c6c67756e92757365645f656e657267795f67756e5f747295757365645f616e6e69766572736172795f67756e6195757365645f616e6e69766572736172795f67756e6294757365645f616e6e69766572736172795f67756e90757365645f37356d6d5f63616e6e6f6e92757365645f6170635f6e635f776561706f6e92757365645f6170635f74725f776561706f6e92757365645f6170635f76735f776561706f6e90757365645f666c75785f63616e6e6f6e9f757365645f617068656c696f6e5f706c61736d615f726f636b65745f706f6491757365645f617068656c696f6e5f7070618c757365645f666c7578706f6494766973697465645f6266725f7465726d696e616c9e757365645f636f6c6f737375735f636c75737465725f626f6d625f706f64a0757365645f636f6c6f737375735f6475616c5f3130306d6d5f63616e6e6f6e7399757365645f636f6c6f737375735f74616e6b5f63616e6e6f6e96766973697465645f656e657267795f6372797374616c9b757365645f68656176795f6772656e6164655f6c61756e6368657298757365645f33356d6d5f726f74617279636861696e67756e8b757365645f6b6174616e6190757365645f33356d6d5f63616e6e6f6e93757365645f7265617665725f776561706f6e7396757365645f6c696768746e696e675f776561706f6e738c757365645f6d65645f61707090757365645f32306d6d5f63616e6e6f6e98766973697465645f6d6f6e6f6c6974685f616d657269736899766973697465645f6d6f6e6f6c6974685f636572797368656e97766973697465645f6d6f6e6f6c6974685f637973736f7297766973697465645f6d6f6e6f6c6974685f6573616d697299766973697465645f6d6f6e6f6c6974685f666f72736572616c99766973697465645f6d6f6e6f6c6974685f697368756e64617298766973697465645f6d6f6e6f6c6974685f7365617268757397766973697465645f6d6f6e6f6c6974685f736f6c73617292757365645f6e635f6865765f66616c636f6e99757365645f6e635f6865765f7363617474657263616e6e6f6e93757365645f6e635f6865765f73706172726f7791757365645f61726d6f725f736970686f6e9f757365645f706572656772696e655f6475616c5f6d616368696e655f67756e9f757365645f706572656772696e655f6475616c5f726f636b65745f706f647399757365645f706572656772696e655f6d65636868616d6d65729e757365645f706572656772696e655f7061727469636c655f63616e6e6f6e96757365645f706572656772696e655f73706172726f7791757365645f3130356d6d5f63616e6e6f6e92757365645f31356d6d5f636861696e67756ea0757365645f70756c7365645f7061727469636c655f616363656c657261746f7293757365645f726f74617279636861696e67756e9f766973697465645f6465636f6e737472756374696f6e5f7465726d696e616c95757365645f736b7967756172645f776561706f6e7391766973697465645f67656e657261746f7291757365645f67617573735f63616e6e6f6e89757365645f7472656b95757365645f76616e67756172645f776561706f6e73a4766973697465645f616e6369656e745f6169725f76656869636c655f7465726d696e616ca2766973697465645f616e6369656e745f65717569706d656e745f7465726d696e616c96766973697465645f6f726465725f7465726d696e616ca7766973697465645f616e6369656e745f67726f756e645f76656869636c655f7465726d696e616c9f766973697465645f67726f756e645f76656869636c655f7465726d696e616c97757365645f76756c747572655f626f6d6261726469657298757365645f76756c747572655f6e6f73655f63616e6e6f6e98757365645f76756c747572655f7461696c5f63616e6e6f6e97757365645f776173705f776561706f6e5f73797374656d91766973697465645f636861726c6965303191766973697465645f636861726c6965303291766973697465645f636861726c6965303391766973697465645f636861726c6965303491766973697465645f636861726c6965303591766973697465645f636861726c6965303691766973697465645f636861726c6965303791766973697465645f636861726c6965303891766973697465645f636861726c6965303996766973697465645f67696e6765726d616e5f6174617298766973697465645f67696e6765726d616e5f646168616b6196766973697465645f67696e6765726d616e5f6876617296766973697465645f67696e6765726d616e5f697a686199766973697465645f67696e6765726d616e5f6a616d7368696498766973697465645f67696e6765726d616e5f6d697468726198766973697465645f67696e6765726d616e5f726173686e7599766973697465645f67696e6765726d616e5f7372616f73686198766973697465645f67696e6765726d616e5f79617a61746195766973697465645f67696e6765726d616e5f7a616c8e766973697465645f736c656430318e766973697465645f736c656430328e766973697465645f736c656430348e766973697465645f736c656430358e766973697465645f736c656430368e766973697465645f736c656430378e766973697465645f736c6564303897766973697465645f736e6f776d616e5f616d657269736898766973697465645f736e6f776d616e5f636572797368656e96766973697465645f736e6f776d616e5f637973736f7296766973697465645f736e6f776d616e5f6573616d697298766973697465645f736e6f776d616e5f666f72736572616c96766973697465645f736e6f776d616e5f686f7373696e98766973697465645f736e6f776d616e5f697368756e64617297766973697465645f736e6f776d616e5f7365617268757396766973697465645f736e6f776d616e5f736f6c736172857567643036857567643035857567643034857567643033857567643032857567643031856d61703939856d61703938856d61703937856d61703936856d61703135856d61703134856d61703131856d61703038856d61703034856d61703035856d61703033856d61703031856d61703036856d61703032856d61703039856d61703037856d617031300300000091747261696e696e675f73746172745f6e638b747261696e696e675f75698c747261696e696e675f6d61700000000000000000000000000000000000000000800000003d0c04d350840240000010000602429660f80c80000c8004200c1b81480000020000c046f18a47019000019000ca4644304900000040001809e6bb052032000008001a84787211200000080003010714889c06400000100320ff0a42e4000001009e95a7342e03200000080003010408c914064000000001198990c4e4000001000060223b9b2180c800000a00081c20c92c800003600414ec172d900000040001808de1284a0320000320008ef1c336b20000078011d830e6f6400000600569c417e2c80000020000c04102502f019000008c00ce31027d99000000400018099e6146203200004b0015a7d44002f720000008000301040c18dc064000023000b1240800636400000100006020e0e92280c80000c800081650c00cfc800006400ce32a1801a59000000400018099e6fc3e03200004b00058b14680463200000080003010742610c064000043000b16c8880916400000100006020e0d01580c80000c8006714e24012cc80000020000c04cf25c190190000258001032e240307900000c8019c74470061b2000000800030133ced8fc0640000960012d9a8d00f0640000010025b9c1401e4c8000002004b6b23c03d1900000040098f585007b3200000080131a58c00f864000001002536f1c01f4c8000002004a64e2a03f190000004015e1b4580873200000080003010711f8a406400000100110a00c010ee400000100006020e2a51380c8000002002218d21021ec80000020000c041c40249019000000400af18a44043f90000004000180838b44760320000008015e38c80088320000008000301071490cc064000001002bc35890110e400000100006020e2052180c800000200221f90d0222c80000020000c041c5e447019000000400442e62e044790000004000180838af032032000000800886d08c089320000008000301071738740640000010011098898112e400000100006020e2361c80c8000002002212a1b0226c80000020000c041c512170190000004004420a32044f900000040001808389104a0320000008008874c8808a3200000080003010715907c06400000100110c0898114e400000100006020e2771a80c800000200578bd13022ac80000020000c041c424330190000004004423848045790000004000180838bfc32032000000801a86506008b320000008000301071030dc06400000100129f68a0117640000010026353110232c8000002004b69438046d90000004015e2887008eb200000080003010715909406400000100350fb8e011de400000100006020e2881980c8000002005786d0f023cc80000020000c041c4cc3b019000000400af1ba1c047b90000004000180838af872032000000800886344408fb20000008000301071620d406400000100110c10b011fe400000100006020e2870d80c800000200578f30c0240c80000020000c041c5863b019000000400442ee300483900000040001808388605e032000000801a86f03c090b200000080003010712a8fc064000001002bc0d858121e400000100006020e2521c80c800000200578b7230244c80000020000c041c49629019000000400d434026048b90000004000180838afc42032000000801a86d864091b200000080003010711989c064000001003508c8c8123e400000100006020e2a82280c8000002006a14f110248c80000020000c041c4be21019000000400af12640049390000004000180838a54720320000008015e33430092b20000008000301071228cc064000001003546e8d432400000100004f34a631139000004001b0834723120000008000204000c2ed0fa1c800000200a8432234a90000004000180952b248a0320000018004024c569d20000008000250a4d0ebc480000020000c04a24bc43019000000c00e0" + def handlePkt(pkt : PlanetSidePacket) : Unit = pkt match { case ctrl : PlanetSideControlPacket => handleControlPkt(ctrl) @@ -637,7 +630,6 @@ val objectHex = hex"18 2c e0 00 00 bc 84 B0 00 0b ea 00 6c 7d f1 10 00 00 02 40 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.BEP = 2286231 player.Certifications += CertificationType.StandardAssault player.Certifications += CertificationType.MediumAssault player.Certifications += CertificationType.StandardExoSuit @@ -709,7 +701,7 @@ val objectHex = hex"18 2c e0 00 00 bc 84 B0 00 0b ea 00 6c 7d f1 10 00 00 02 40 ) }) //render Equipment that was dropped into zone before the player arrived - continent.EquipmentOnGround.toList.foreach(item => { + continent.EquipmentOnGround.foreach(item => { val definition = item.Definition sendResponse( PacketCoding.CreateGamePacket(0, From 040bea8b13610d6ecc89792d4c54deba4be2f2c8 Mon Sep 17 00:00:00 2001 From: FateJH Date: Tue, 17 Oct 2017 00:27:19 -0400 Subject: [PATCH 22/23] added tests --- .../ObjectCreateDetailedMessageTest.scala | 629 +++++++++++++++++- .../test/scala/objects/ConverterTest.scala | 63 +- 2 files changed, 687 insertions(+), 5 deletions(-) diff --git a/common/src/test/scala/game/ObjectCreateDetailedMessageTest.scala b/common/src/test/scala/game/ObjectCreateDetailedMessageTest.scala index 6582f9ff..545a5fdb 100644 --- a/common/src/test/scala/game/ObjectCreateDetailedMessageTest.scala +++ b/common/src/test/scala/game/ObjectCreateDetailedMessageTest.scala @@ -3,7 +3,7 @@ package game import org.specs2.mutable._ import net.psforever.packet._ -import net.psforever.packet.game._ +import net.psforever.packet.game.{ObjectCreateDetailedMessage, _} import net.psforever.packet.game.objectcreate._ import net.psforever.types._ import scodec.bits._ @@ -21,6 +21,7 @@ class ObjectCreateDetailedMessageTest extends Specification { val string_rek = hex"18 97000000 2580 6C2 9F05 81 48000002000080000" val string_boomer_trigger = hex"18 87000000 6304CA8760B 80 C800000200008" val string_testchar = hex"18 570C0000 BC8 4B00 6C2D7 65535 CA16 0 00 01 34 40 00 0970 49006C006C006C004900490049006C006C006C0049006C0049006C006C0049006C006C006C0049006C006C004900 84 52 70 76 1E 80 80 00 00 00 00 00 3FFFC 0 00 00 00 20 00 00 0F F6 A7 03 FF FF FF FF FF FF FF FF FF FF FF FF FF FF FF FC 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 01 90 01 90 00 64 00 00 01 00 7E C8 00 C8 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 01 C0 00 42 C5 46 86 C7 00 00 00 80 00 00 12 40 78 70 65 5F 73 61 6E 63 74 75 61 72 79 5F 68 65 6C 70 90 78 70 65 5F 74 68 5F 66 69 72 65 6D 6F 64 65 73 8B 75 73 65 64 5F 62 65 61 6D 65 72 85 6D 61 70 31 33 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 01 0A 23 02 60 04 04 40 00 00 10 00 06 02 08 14 D0 08 0C 80 00 02 00 02 6B 4E 00 82 88 00 00 02 00 00 C0 41 C0 9E 01 01 90 00 00 64 00 44 2A 00 10 91 00 00 00 40 00 18 08 38 94 40 20 32 00 00 00 80 19 05 48 02 17 20 00 00 08 00 70 29 80 43 64 00 00 32 00 0E 05 40 08 9C 80 00 06 40 01 C0 AA 01 19 90 00 00 C8 00 3A 15 80 28 72 00 00 19 00 04 0A B8 05 26 40 00 03 20 06 C2 58 00 A7 88 00 00 02 00 00 80 00 00" + val string_testchar_br32 = hex"18 2c e0 00 00 bc 84 B0 00 0b ea 00 6c 7d f1 10 00 00 02 40 00 08 60 4b 00 69 00 43 00 6b 00 4a 00 72 00 02 31 3a cc 82 c0 00 00 00 00 00 00 00 00 3e df 42 00 20 00 0e 00 40 43 40 4c 04 00 02 e8 00 00 03 a8 00 00 01 9c 04 00 00 b8 99 84 00 0e 68 28 00 00 00 00 00 00 00 00 00 00 00 00 01 90 01 90 00 c8 00 00 01 00 7e c8 00 5c 00 00 01 29 c1 cc 80 00 00 00 00 00 00 00 00 00 00 00 00 03 c0 00 40 81 01 c4 45 46 86 c8 88 c9 09 4a 4a 80 50 0c 13 00 00 15 00 80 00 48 00 7870655f6f766572686561645f6d6170 8d7870655f776172705f676174658f7870655f666f726d5f6f75746669748c7870655f626c61636b6f7073927870655f636f6d6d616e645f72616e6b5f35927870655f636f6d6d616e645f72616e6b5f33927870655f73616e6374756172795f68656c70927870655f626174746c655f72616e6b5f3133927870655f626174746c655f72616e6b5f3132927870655f626174746c655f72616e6b5f3130927870655f626174746c655f72616e6b5f3134927870655f626174746c655f72616e6b5f3135937870655f6f72626974616c5f73687574746c658c7870655f64726f705f706f64917870655f62696e645f666163696c697479917870655f626174746c655f72616e6b5f33917870655f626174746c655f72616e6b5f35917870655f626174746c655f72616e6b5f348e7870655f6a6f696e5f73717561648e7870655f666f726d5f7371756164927870655f696e7374616e745f616374696f6e917870655f626174746c655f72616e6b5f32937870655f776172705f676174655f7573616765917870655f626174746c655f72616e6b5f38927870655f626174746c655f72616e6b5f3131917870655f626174746c655f72616e6b5f368e7870655f6d61696c5f616c657274927870655f636f6d6d616e645f72616e6b5f31927870655f626174746c655f72616e6b5f3230927870655f626174746c655f72616e6b5f3138927870655f626174746c655f72616e6b5f3139907870655f6a6f696e5f706c61746f6f6e927870655f626174746c655f72616e6b5f3137927870655f626174746c655f72616e6b5f31368f7870655f6a6f696e5f6f7574666974927870655f626174746c655f72616e6b5f3235927870655f626174746c655f72616e6b5f3234927870655f636f6d6d616e645f72616e6b5f34907870655f666f726d5f706c61746f6f6e8c7870655f62696e645f616d73917870655f626174746c655f72616e6b5f39917870655f626174746c655f72616e6b5f378d7870655f74685f726f757465728c7870655f74685f666c61696c8a7870655f74685f616e748a7870655f74685f616d738f7870655f74685f67726f756e645f708c7870655f74685f6169725f708c7870655f74685f686f7665728d7870655f74685f67726f756e648a7870655f74685f626672927870655f74685f61667465726275726e65728a7870655f74685f6169728c7870655f74685f636c6f616b89757365645f6f69637791757365645f616476616e6365645f61636597766973697465645f73706974666972655f74757272657498766973697465645f73706974666972655f636c6f616b656493766973697465645f73706974666972655f616192766973697465645f74616e6b5f7472617073a1766973697465645f706f727461626c655f6d616e6e65645f7475727265745f6e63a1766973697465645f706f727461626c655f6d616e6e65645f7475727265745f74728e757365645f6d61676375747465728f757365645f636861696e626c6164658f757365645f666f726365626c61646593766973697465645f77616c6c5f74757272657498766973697465645f616e6369656e745f7465726d696e616c8b766973697465645f616d738b766973697465645f616e7490766973697465645f64726f707368697091766973697465645f6c6962657261746f7294766973697465645f6c6967687467756e7368697091766973697465645f6c696768746e696e6790766973697465645f6d616772696465728f766973697465645f70726f776c657293766973697465645f71756164737465616c746890766973697465645f736b7967756172649a766973697465645f74687265656d616e686561767962756767799d766973697465645f74776f5f6d616e5f61737361756c745f627567677998766973697465645f74776f6d616e6865617679627567677998766973697465645f74776f6d616e686f766572627567677990766973697465645f76616e67756172648d766973697465645f666c61696c8e766973697465645f726f7574657293766973697465645f737769746368626c6164658e766973697465645f6175726f726193766973697465645f626174746c657761676f6e8c766973697465645f6675727993766973697465645f7175616461737361756c7496766973697465645f67616c6178795f67756e736869708e766973697465645f6170635f74728e766973697465645f6170635f767390766973697465645f6c6f64657374617290766973697465645f7068616e7461736d91766973697465645f7468756e64657265728e766973697465645f6170635f6e638f766973697465645f76756c747572658c766973697465645f7761737090766973697465645f6d6f73717569746f97766973697465645f617068656c696f6e5f666c6967687497766973697465645f617068656c696f6e5f67756e6e657297766973697465645f636f6c6f737375735f666c6967687497766973697465645f636f6c6f737375735f67756e6e657298766973697465645f706572656772696e655f666c6967687498766973697465645f706572656772696e655f67756e6e657289757365645f62616e6b95766973697465645f7265736f757263655f73696c6f9e766973697465645f63657274696669636174696f6e5f7465726d696e616c94766973697465645f6d65645f7465726d696e616c93757365645f6e616e6f5f64697370656e73657295766973697465645f73656e736f725f736869656c649a766973697465645f62726f6164636173745f77617270676174658c757365645f7068616c616e7894757365645f7068616c616e785f6176636f6d626f96757365645f7068616c616e785f666c616b636f6d626f96766973697465645f77617270676174655f736d616c6c91757365645f666c616d657468726f7765729a757365645f616e6369656e745f7475727265745f776561706f6e92766973697465645f4c4c555f736f636b657492757365645f656e657267795f67756e5f6e6397766973697465645f6d656469756d7472616e73706f72749f757365645f617068656c696f6e5f696d6d6f6c6174696f6e5f63616e6e6f6e93757365645f6772656e6164655f706c61736d6193757365645f6772656e6164655f6a616d6d657298766973697465645f736869656c645f67656e657261746f7295766973697465645f6d6f74696f6e5f73656e736f7296766973697465645f6865616c74685f6372797374616c96766973697465645f7265706169725f6372797374616c97766973697465645f76656869636c655f6372797374616c91757365645f6772656e6164655f6672616788757365645f61636598766973697465645f6164765f6d65645f7465726d696e616c8b757365645f6265616d657290757365645f626f6c745f6472697665728b757365645f6379636c65728a757365645f676175737391757365645f68756e7465727365656b657288757365645f6973708b757365645f6c616e6365728b757365645f6c61736865728e757365645f6d61656c7374726f6d8c757365645f70686f656e69788b757365645f70756c7361728d757365645f70756e69736865728e757365645f725f73686f7467756e8d757365645f7261646961746f7288757365645f72656b8d757365645f72657065617465728c757365645f726f636b6c65748c757365645f737472696b65728f757365645f73757070726573736f728c757365645f7468756d7065729c766973697465645f76616e755f636f6e74726f6c5f636f6e736f6c6598766973697465645f636170747572655f7465726d696e616c92757365645f6d696e695f636861696e67756e91757365645f6c617a655f706f696e7465728c757365645f74656c657061648b757365645f7370696b657291757365645f68656176795f736e6970657293757365645f636f6d6d616e645f75706c696e6b8d757365645f66697265626972648e757365645f666c6563686574746594757365645f68656176795f7261696c5f6265616d89757365645f696c63399a766973697465645f67656e657261746f725f7465726d696e616c8e766973697465645f6c6f636b65729a766973697465645f65787465726e616c5f646f6f725f6c6f636b9c766973697465645f6169725f76656869636c655f7465726d696e616c97766973697465645f67616c6178795f7465726d696e616c98766973697465645f696d706c616e745f7465726d696e616c99766973697465645f7365636f6e646172795f6361707475726590757365645f32356d6d5f63616e6e6f6e99757365645f6c6962657261746f725f626f6d6261726469657293766973697465645f7265706169725f73696c6f93766973697465645f76616e755f6d6f64756c6591757365645f666c61696c5f776561706f6e8b757365645f73637974686598766973697465645f7265737061776e5f7465726d696e616c8c757365645f62616c6c67756e92757365645f656e657267795f67756e5f747295757365645f616e6e69766572736172795f67756e6195757365645f616e6e69766572736172795f67756e6294757365645f616e6e69766572736172795f67756e90757365645f37356d6d5f63616e6e6f6e92757365645f6170635f6e635f776561706f6e92757365645f6170635f74725f776561706f6e92757365645f6170635f76735f776561706f6e90757365645f666c75785f63616e6e6f6e9f757365645f617068656c696f6e5f706c61736d615f726f636b65745f706f6491757365645f617068656c696f6e5f7070618c757365645f666c7578706f6494766973697465645f6266725f7465726d696e616c9e757365645f636f6c6f737375735f636c75737465725f626f6d625f706f64a0757365645f636f6c6f737375735f6475616c5f3130306d6d5f63616e6e6f6e7399757365645f636f6c6f737375735f74616e6b5f63616e6e6f6e96766973697465645f656e657267795f6372797374616c9b757365645f68656176795f6772656e6164655f6c61756e6368657298757365645f33356d6d5f726f74617279636861696e67756e8b757365645f6b6174616e6190757365645f33356d6d5f63616e6e6f6e93757365645f7265617665725f776561706f6e7396757365645f6c696768746e696e675f776561706f6e738c757365645f6d65645f61707090757365645f32306d6d5f63616e6e6f6e98766973697465645f6d6f6e6f6c6974685f616d657269736899766973697465645f6d6f6e6f6c6974685f636572797368656e97766973697465645f6d6f6e6f6c6974685f637973736f7297766973697465645f6d6f6e6f6c6974685f6573616d697299766973697465645f6d6f6e6f6c6974685f666f72736572616c99766973697465645f6d6f6e6f6c6974685f697368756e64617298766973697465645f6d6f6e6f6c6974685f7365617268757397766973697465645f6d6f6e6f6c6974685f736f6c73617292757365645f6e635f6865765f66616c636f6e99757365645f6e635f6865765f7363617474657263616e6e6f6e93757365645f6e635f6865765f73706172726f7791757365645f61726d6f725f736970686f6e9f757365645f706572656772696e655f6475616c5f6d616368696e655f67756e9f757365645f706572656772696e655f6475616c5f726f636b65745f706f647399757365645f706572656772696e655f6d65636868616d6d65729e757365645f706572656772696e655f7061727469636c655f63616e6e6f6e96757365645f706572656772696e655f73706172726f7791757365645f3130356d6d5f63616e6e6f6e92757365645f31356d6d5f636861696e67756ea0757365645f70756c7365645f7061727469636c655f616363656c657261746f7293757365645f726f74617279636861696e67756e9f766973697465645f6465636f6e737472756374696f6e5f7465726d696e616c95757365645f736b7967756172645f776561706f6e7391766973697465645f67656e657261746f7291757365645f67617573735f63616e6e6f6e89757365645f7472656b95757365645f76616e67756172645f776561706f6e73a4766973697465645f616e6369656e745f6169725f76656869636c655f7465726d696e616ca2766973697465645f616e6369656e745f65717569706d656e745f7465726d696e616c96766973697465645f6f726465725f7465726d696e616ca7766973697465645f616e6369656e745f67726f756e645f76656869636c655f7465726d696e616c9f766973697465645f67726f756e645f76656869636c655f7465726d696e616c97757365645f76756c747572655f626f6d6261726469657298757365645f76756c747572655f6e6f73655f63616e6e6f6e98757365645f76756c747572655f7461696c5f63616e6e6f6e97757365645f776173705f776561706f6e5f73797374656d91766973697465645f636861726c6965303191766973697465645f636861726c6965303291766973697465645f636861726c6965303391766973697465645f636861726c6965303491766973697465645f636861726c6965303591766973697465645f636861726c6965303691766973697465645f636861726c6965303791766973697465645f636861726c6965303891766973697465645f636861726c6965303996766973697465645f67696e6765726d616e5f6174617298766973697465645f67696e6765726d616e5f646168616b6196766973697465645f67696e6765726d616e5f6876617296766973697465645f67696e6765726d616e5f697a686199766973697465645f67696e6765726d616e5f6a616d7368696498766973697465645f67696e6765726d616e5f6d697468726198766973697465645f67696e6765726d616e5f726173686e7599766973697465645f67696e6765726d616e5f7372616f73686198766973697465645f67696e6765726d616e5f79617a61746195766973697465645f67696e6765726d616e5f7a616c8e766973697465645f736c656430318e766973697465645f736c656430328e766973697465645f736c656430348e766973697465645f736c656430358e766973697465645f736c656430368e766973697465645f736c656430378e766973697465645f736c6564303897766973697465645f736e6f776d616e5f616d657269736898766973697465645f736e6f776d616e5f636572797368656e96766973697465645f736e6f776d616e5f637973736f7296766973697465645f736e6f776d616e5f6573616d697298766973697465645f736e6f776d616e5f666f72736572616c96766973697465645f736e6f776d616e5f686f7373696e98766973697465645f736e6f776d616e5f697368756e64617297766973697465645f736e6f776d616e5f7365617268757396766973697465645f736e6f776d616e5f736f6c736172857567643036857567643035857567643034857567643033857567643032857567643031856d61703939856d61703938856d61703937856d61703936856d61703135856d61703134856d61703131856d61703038856d61703034856d61703035856d61703033856d61703031856d61703036856d61703032856d61703039856d61703037856d617031300300000091747261696e696e675f73746172745f6e638b747261696e696e675f75698c747261696e696e675f6d61700000000000000000000000000000000000000000800000003d0c04d350840240000010000602429660f80c80000c8004200c1b81480000020000c046f18a47019000019000ca4644304900000040001809e6bb052032000008001a84787211200000080003010714889c06400000100320ff0a42e4000001009e95a7342e03200000080003010408c914064000000001198990c4e4000001000060223b9b2180c800000a00081c20c92c800003600414ec172d900000040001808de1284a0320000320008ef1c336b20000078011d830e6f6400000600569c417e2c80000020000c04102502f019000008c00ce31027d99000000400018099e6146203200004b0015a7d44002f720000008000301040c18dc064000023000b1240800636400000100006020e0e92280c80000c800081650c00cfc800006400ce32a1801a59000000400018099e6fc3e03200004b00058b14680463200000080003010742610c064000043000b16c8880916400000100006020e0d01580c80000c8006714e24012cc80000020000c04cf25c190190000258001032e240307900000c8019c74470061b2000000800030133ced8fc0640000960012d9a8d00f0640000010025b9c1401e4c8000002004b6b23c03d1900000040098f585007b3200000080131a58c00f864000001002536f1c01f4c8000002004a64e2a03f190000004015e1b4580873200000080003010711f8a406400000100110a00c010ee400000100006020e2a51380c8000002002218d21021ec80000020000c041c40249019000000400af18a44043f90000004000180838b44760320000008015e38c80088320000008000301071490cc064000001002bc35890110e400000100006020e2052180c800000200221f90d0222c80000020000c041c5e447019000000400442e62e044790000004000180838af032032000000800886d08c089320000008000301071738740640000010011098898112e400000100006020e2361c80c8000002002212a1b0226c80000020000c041c512170190000004004420a32044f900000040001808389104a0320000008008874c8808a3200000080003010715907c06400000100110c0898114e400000100006020e2771a80c800000200578bd13022ac80000020000c041c424330190000004004423848045790000004000180838bfc32032000000801a86506008b320000008000301071030dc06400000100129f68a0117640000010026353110232c8000002004b69438046d90000004015e2887008eb200000080003010715909406400000100350fb8e011de400000100006020e2881980c8000002005786d0f023cc80000020000c041c4cc3b019000000400af1ba1c047b90000004000180838af872032000000800886344408fb20000008000301071620d406400000100110c10b011fe400000100006020e2870d80c800000200578f30c0240c80000020000c041c5863b019000000400442ee300483900000040001808388605e032000000801a86f03c090b200000080003010712a8fc064000001002bc0d858121e400000100006020e2521c80c800000200578b7230244c80000020000c041c49629019000000400d434026048b90000004000180838afc42032000000801a86d864091b200000080003010711989c064000001003508c8c8123e400000100006020e2a82280c8000002006a14f110248c80000020000c041c4be21019000000400af12640049390000004000180838a54720320000008015e33430092b20000008000301071228cc064000001003546e8d432400000100004f34a631139000004001b0834723120000008000204000c2ed0fa1c800000200a8432234a90000004000180952b248a0320000018004024c569d20000008000250a4d0ebc480000020000c04a24bc43019000000c00e0" "decode (2)" in { //an invalid bit representation will fail to turn into an object @@ -305,6 +306,76 @@ class ObjectCreateDetailedMessageTest extends Specification { } } + "decode (character, BR32)" in { + PacketCoding.DecodePacket(string_testchar_br32).require match { + case ObjectCreateDetailedMessage(len, cls, guid, parent, data) => + //this test is mainly for an alternate bitstream parsing order + //the object produced is massive and most of it is already covered in other tests + //only certain details towards the end of the stream will be checked + data.isDefined mustEqual true + val char = data.get.asInstanceOf[DetailedCharacterData] + DetailedCharacterData.isBR24(char.bep) mustEqual true + char.certs.size mustEqual 15 + char.certs.head mustEqual CertificationType.StandardAssault + char.certs(14) mustEqual CertificationType.CombatEngineering + char.implants.size mustEqual 3 + char.implants.head.implant mustEqual ImplantType.AudioAmplifier + char.implants.head.activation mustEqual None + char.implants(1).implant mustEqual ImplantType.Targeting + char.implants(1).activation mustEqual None + char.implants(2).implant mustEqual ImplantType.Surge + char.implants(2).activation mustEqual None + char.firstTimeEvents.size mustEqual 298 + char.firstTimeEvents.head mustEqual "xpe_overhead_map" + char.firstTimeEvents(297) mustEqual "map10" + char.tutorials.size mustEqual 3 + char.tutorials.head mustEqual "training_start_nc" + char.tutorials(1) mustEqual "training_ui" + char.tutorials(2) mustEqual "training_map" + char.cosmetics.isDefined mustEqual true + char.cosmetics.get.no_helmet mustEqual true + char.cosmetics.get.beret mustEqual true + char.cosmetics.get.earpiece mustEqual true + char.cosmetics.get.sunglasses mustEqual true + char.cosmetics.get.brimmed_cap mustEqual false + //inventory + char.inventory.isDefined mustEqual true + char.inventory.get.contents.size mustEqual 12 + //0 + char.inventory.get.contents.head.objectClass mustEqual 531 + char.inventory.get.contents.head.guid mustEqual PlanetSideGUID(4202) + char.inventory.get.contents.head.parentSlot mustEqual 0 + val wep1 = char.inventory.get.contents.head.obj.asInstanceOf[DetailedWeaponData] + wep1.unk1 mustEqual 2 + wep1.unk2 mustEqual 8 + wep1.ammo.head.objectClass mustEqual 389 + wep1.ammo.head.guid mustEqual PlanetSideGUID(3942) + wep1.ammo.head.parentSlot mustEqual 0 + wep1.ammo.head.obj.asInstanceOf[DetailedAmmoBoxData].unk mustEqual 8 + wep1.ammo.head.obj.asInstanceOf[DetailedAmmoBoxData].magazine mustEqual 100 + //4 + char.inventory.get.contents(4).objectClass mustEqual 456 + char.inventory.get.contents(4).guid mustEqual PlanetSideGUID(5374) + char.inventory.get.contents(4).parentSlot mustEqual 5 + char.inventory.get.contents(4).obj.asInstanceOf[DetailedLockerContainerData].inventory.get.contents.size mustEqual 61 + //11 + char.inventory.get.contents(11).objectClass mustEqual 673 + char.inventory.get.contents(11).guid mustEqual PlanetSideGUID(3661) + char.inventory.get.contents(11).parentSlot mustEqual 60 + val wep2 = char.inventory.get.contents(11).obj.asInstanceOf[DetailedWeaponData] + wep2.unk1 mustEqual 2 + wep2.unk2 mustEqual 8 + wep2.ammo.head.objectClass mustEqual 674 + wep2.ammo.head.guid mustEqual PlanetSideGUID(8542) + wep2.ammo.head.parentSlot mustEqual 0 + wep2.ammo.head.obj.asInstanceOf[DetailedAmmoBoxData].unk mustEqual 8 + wep2.ammo.head.obj.asInstanceOf[DetailedAmmoBoxData].magazine mustEqual 3 + char.drawn_slot mustEqual DrawnSlot.None + case _ => + ko + } + } + "encode (2)" in { //the lack of an object will fail to turn into a bad bitstream val msg = ObjectCreateDetailedMessage(0L, ObjectClass.avatar, PlanetSideGUID(2497), None, None) @@ -445,4 +516,560 @@ class ObjectCreateDetailedMessageTest extends Specification { pkt_bitv.drop(732) mustEqual ori_bitv.drop(732) //TODO work on DetailedCharacterData to make this pass as a single stream } + + "encode (character, br32)" in { + val obj = DetailedCharacterData( + CharacterAppearanceData( + PlacementData( + Vector3(5500.0f, 3800.0f, 71.484375f), + Vector3(0.0f, 0.0f, 90.0f), + None + ), + BasicCharacterData("KiCkJr", PlanetSideEmpire.NC, CharacterGender.Male, 24, 4), + 3, + false, false, + ExoSuitType.Agile, + "", + 14, + false, + 354.375f, 354.375f, + false, + GrenadeState.None, + false, false, false, + RibbonBars(MeritCommendation.Loser4, MeritCommendation.EventNCElite, MeritCommendation.HeavyAssault6, MeritCommendation.SixYearNC) + ), + 6366766, + 694787, + 100, 100, 100, + 1, 7, 7, + 100, 46, + List( + CertificationType.StandardAssault, + CertificationType.MediumAssault, + CertificationType.HeavyAssault, + CertificationType.AntiVehicular, + CertificationType.AirCavalryScout, + CertificationType.GroundSupport, + CertificationType.Harasser, + CertificationType.StandardExoSuit, + CertificationType.AgileExoSuit, + CertificationType.Medical, + CertificationType.AdvancedMedical, + CertificationType.Hacking, + CertificationType.AdvancedHacking, + CertificationType.Engineering, + CertificationType.CombatEngineering + ), + List( + ImplantEntry(ImplantType.AudioAmplifier, None), + ImplantEntry(ImplantType.Targeting, None), + ImplantEntry(ImplantType.Surge, None) + ), + List( + "xpe_overhead_map", + "xpe_warp_gate", + "xpe_form_outfit", + "xpe_blackops", + "xpe_command_rank_5", + "xpe_command_rank_3", + "xpe_sanctuary_help", + "xpe_battle_rank_13", + "xpe_battle_rank_12", + "xpe_battle_rank_10", + "xpe_battle_rank_14", + "xpe_battle_rank_15", + "xpe_orbital_shuttle", + "xpe_drop_pod", + "xpe_bind_facility", + "xpe_battle_rank_3", + "xpe_battle_rank_5", + "xpe_battle_rank_4", + "xpe_join_squad", + "xpe_form_squad", + "xpe_instant_action", + "xpe_battle_rank_2", + "xpe_warp_gate_usage", + "xpe_battle_rank_8", + "xpe_battle_rank_11", + "xpe_battle_rank_6", + "xpe_mail_alert", + "xpe_command_rank_1", + "xpe_battle_rank_20", + "xpe_battle_rank_18", + "xpe_battle_rank_19", + "xpe_join_platoon", + "xpe_battle_rank_17", + "xpe_battle_rank_16", + "xpe_join_outfit", + "xpe_battle_rank_25", + "xpe_battle_rank_24", + "xpe_command_rank_4", + "xpe_form_platoon", + "xpe_bind_ams", + "xpe_battle_rank_9", + "xpe_battle_rank_7", + "xpe_th_router", + "xpe_th_flail", + "xpe_th_ant", + "xpe_th_ams", + "xpe_th_ground_p", + "xpe_th_air_p", + "xpe_th_hover", + "xpe_th_ground", + "xpe_th_bfr", + "xpe_th_afterburner", + "xpe_th_air", + "xpe_th_cloak", + "used_oicw", + "used_advanced_ace", + "visited_spitfire_turret", + "visited_spitfire_cloaked", + "visited_spitfire_aa", + "visited_tank_traps", + "visited_portable_manned_turret_nc", + "visited_portable_manned_turret_tr", + "used_magcutter", + "used_chainblade", + "used_forceblade", + "visited_wall_turret", + "visited_ancient_terminal", + "visited_ams", + "visited_ant", + "visited_dropship", + "visited_liberator", + "visited_lightgunship", + "visited_lightning", + "visited_magrider", + "visited_prowler", + "visited_quadstealth", + "visited_skyguard", + "visited_threemanheavybuggy", + "visited_two_man_assault_buggy", + "visited_twomanheavybuggy", + "visited_twomanhoverbuggy", + "visited_vanguard", + "visited_flail", + "visited_router", + "visited_switchblade", + "visited_aurora", + "visited_battlewagon", + "visited_fury", + "visited_quadassault", + "visited_galaxy_gunship", + "visited_apc_tr", + "visited_apc_vs", + "visited_lodestar", + "visited_phantasm", + "visited_thunderer", + "visited_apc_nc", + "visited_vulture", + "visited_wasp", + "visited_mosquito", + "visited_aphelion_flight", + "visited_aphelion_gunner", + "visited_colossus_flight", + "visited_colossus_gunner", + "visited_peregrine_flight", + "visited_peregrine_gunner", + "used_bank", + "visited_resource_silo", + "visited_certification_terminal", + "visited_med_terminal", + "used_nano_dispenser", + "visited_sensor_shield", + "visited_broadcast_warpgate", + "used_phalanx", + "used_phalanx_avcombo", + "used_phalanx_flakcombo", + "visited_warpgate_small", + "used_flamethrower", + "used_ancient_turret_weapon", + "visited_LLU_socket", + "used_energy_gun_nc", + "visited_mediumtransport", + "used_aphelion_immolation_cannon", + "used_grenade_plasma", + "used_grenade_jammer", + "visited_shield_generator", + "visited_motion_sensor", + "visited_health_crystal", + "visited_repair_crystal", + "visited_vehicle_crystal", + "used_grenade_frag", + "used_ace", + "visited_adv_med_terminal", + "used_beamer", + "used_bolt_driver", + "used_cycler", + "used_gauss", + "used_hunterseeker", + "used_isp", + "used_lancer", + "used_lasher", + "used_maelstrom", + "used_phoenix", + "used_pulsar", + "used_punisher", + "used_r_shotgun", + "used_radiator", + "used_rek", + "used_repeater", + "used_rocklet", + "used_striker", + "used_suppressor", + "used_thumper", + "visited_vanu_control_console", + "visited_capture_terminal", + "used_mini_chaingun", + "used_laze_pointer", + "used_telepad", + "used_spiker", + "used_heavy_sniper", + "used_command_uplink", + "used_firebird", + "used_flechette", + "used_heavy_rail_beam", + "used_ilc9", + "visited_generator_terminal", + "visited_locker", + "visited_external_door_lock", + "visited_air_vehicle_terminal", + "visited_galaxy_terminal", + "visited_implant_terminal", + "visited_secondary_capture", + "used_25mm_cannon", + "used_liberator_bombardier", + "visited_repair_silo", + "visited_vanu_module", + "used_flail_weapon", + "used_scythe", + "visited_respawn_terminal", + "used_ballgun", + "used_energy_gun_tr", + "used_anniversary_guna", + "used_anniversary_gunb", + "used_anniversary_gun", + "used_75mm_cannon", + "used_apc_nc_weapon", + "used_apc_tr_weapon", + "used_apc_vs_weapon", + "used_flux_cannon", + "used_aphelion_plasma_rocket_pod", + "used_aphelion_ppa", + "used_fluxpod", + "visited_bfr_terminal", + "used_colossus_cluster_bomb_pod", + "used_colossus_dual_100mm_cannons", + "used_colossus_tank_cannon", + "visited_energy_crystal", + "used_heavy_grenade_launcher", + "used_35mm_rotarychaingun", + "used_katana", + "used_35mm_cannon", + "used_reaver_weapons", + "used_lightning_weapons", + "used_med_app", + "used_20mm_cannon", + "visited_monolith_amerish", + "visited_monolith_ceryshen", + "visited_monolith_cyssor", + "visited_monolith_esamir", + "visited_monolith_forseral", + "visited_monolith_ishundar", + "visited_monolith_searhus", + "visited_monolith_solsar", + "used_nc_hev_falcon", + "used_nc_hev_scattercannon", + "used_nc_hev_sparrow", + "used_armor_siphon", + "used_peregrine_dual_machine_gun", + "used_peregrine_dual_rocket_pods", + "used_peregrine_mechhammer", + "used_peregrine_particle_cannon", + "used_peregrine_sparrow", + "used_105mm_cannon", + "used_15mm_chaingun", + "used_pulsed_particle_accelerator", + "used_rotarychaingun", + "visited_deconstruction_terminal", + "used_skyguard_weapons", + "visited_generator", + "used_gauss_cannon", + "used_trek", + "used_vanguard_weapons", + "visited_ancient_air_vehicle_terminal", + "visited_ancient_equipment_terminal", + "visited_order_terminal", + "visited_ancient_ground_vehicle_terminal", + "visited_ground_vehicle_terminal", + "used_vulture_bombardier", + "used_vulture_nose_cannon", + "used_vulture_tail_cannon", + "used_wasp_weapon_system", + "visited_charlie01", + "visited_charlie02", + "visited_charlie03", + "visited_charlie04", + "visited_charlie05", + "visited_charlie06", + "visited_charlie07", + "visited_charlie08", + "visited_charlie09", + "visited_gingerman_atar", + "visited_gingerman_dahaka", + "visited_gingerman_hvar", + "visited_gingerman_izha", + "visited_gingerman_jamshid", + "visited_gingerman_mithra", + "visited_gingerman_rashnu", + "visited_gingerman_sraosha", + "visited_gingerman_yazata", + "visited_gingerman_zal", + "visited_sled01", + "visited_sled02", + "visited_sled04", + "visited_sled05", + "visited_sled06", + "visited_sled07", + "visited_sled08", + "visited_snowman_amerish", + "visited_snowman_ceryshen", + "visited_snowman_cyssor", + "visited_snowman_esamir", + "visited_snowman_forseral", + "visited_snowman_hossin", + "visited_snowman_ishundar", + "visited_snowman_searhus", + "visited_snowman_solsar", + "ugd06", + "ugd05", + "ugd04", + "ugd03", + "ugd02", + "ugd01", + "map99", + "map98", + "map97", + "map96", + "map15", + "map14", + "map11", + "map08", + "map04", + "map05", + "map03", + "map01", + "map06", + "map02", + "map09", + "map07", + "map10" + ), + List( + "training_start_nc", + "training_ui", + "training_map" + ), + Some(Cosmetics(true, true, true, true, false)), + Some( + InventoryData( + List( + InternalSlot(531, PlanetSideGUID(4202), 0, + DetailedWeaponData(2, 8, List(InternalSlot(389, PlanetSideGUID(3942), 0,DetailedAmmoBoxData(8, 100)))) + ), + InternalSlot(132, PlanetSideGUID(6924), 1, + DetailedWeaponData(2, 8, List(InternalSlot(111, PlanetSideGUID(9157), 0, DetailedAmmoBoxData(8, 100)))) + ), + InternalSlot(714, PlanetSideGUID(8498), 2, + DetailedWeaponData(2, 8, List(InternalSlot(755, PlanetSideGUID(5356), 0, DetailedAmmoBoxData(8, 16)))) + ), + InternalSlot(468, PlanetSideGUID(7198), 4, + DetailedWeaponData(2, 8, List(InternalSlot(540, PlanetSideGUID(5009), 0, DetailedAmmoBoxData(8, 1)))) + ), + InternalSlot(456, PlanetSideGUID(5374), 5, + DetailedLockerContainerData(8, Some(InventoryData(List( + InternalSlot(429, PlanetSideGUID(3021), 0, + DetailedWeaponData(6, 8, List(InternalSlot(272, PlanetSideGUID(8729), 0, DetailedAmmoBoxData(8, 0)))) + ), + InternalSlot(838, PlanetSideGUID(8467), 9, + DetailedWeaponData(6, 8, List(InternalSlot(839, PlanetSideGUID(8603), 0, DetailedAmmoBoxData(8, 5)))) + ), + InternalSlot(272, PlanetSideGUID(3266), 18, DetailedAmmoBoxData(8, 27)), + InternalSlot(577, PlanetSideGUID(2934), 22, + DetailedWeaponData(6, 8, List(InternalSlot(111, PlanetSideGUID(4682), 0, DetailedAmmoBoxData(8, 100)))) + ), + InternalSlot(839, PlanetSideGUID(3271), 90, DetailedAmmoBoxData(8, 15)), + InternalSlot(839, PlanetSideGUID(7174), 94, DetailedAmmoBoxData(8, 6)), + InternalSlot(429, PlanetSideGUID(6084), 98, + DetailedWeaponData(6, 8, List(InternalSlot(272, PlanetSideGUID(5928), 0, DetailedAmmoBoxData(8, 35)))) + ), + InternalSlot(462, PlanetSideGUID(5000), 108, + DetailedWeaponData(6, 8, List(InternalSlot(463, PlanetSideGUID(6277), 0, DetailedAmmoBoxData(8, 150)))) + ), + InternalSlot(429, PlanetSideGUID(4341), 189, + DetailedWeaponData(6, 8, List(InternalSlot(272, PlanetSideGUID(7043), 0, DetailedAmmoBoxData(8, 35)))) + ), + InternalSlot(556, PlanetSideGUID(4168), 198, + DetailedWeaponData(6, 8, List(InternalSlot(28, PlanetSideGUID(8937), 0, DetailedAmmoBoxData(8, 100)))) + ), + InternalSlot(272, PlanetSideGUID(3173), 207, DetailedAmmoBoxData(8, 50)), + InternalSlot(462, PlanetSideGUID(3221), 210, + DetailedWeaponData(6, 8, List(InternalSlot(463, PlanetSideGUID(4031), 0, DetailedAmmoBoxData(8, 150)))) + ), + InternalSlot(556, PlanetSideGUID(6853), 280, + DetailedWeaponData(6, 8, List(InternalSlot(29, PlanetSideGUID(8524), 0, DetailedAmmoBoxData(8, 67)))) + ), + InternalSlot(556, PlanetSideGUID(4569), 290, + DetailedWeaponData(6, 8, List(InternalSlot(28, PlanetSideGUID(5584), 0, DetailedAmmoBoxData(8, 100)))) + ), + InternalSlot(462, PlanetSideGUID(9294), 300, + DetailedWeaponData(6, 8, List(InternalSlot(463, PlanetSideGUID(3118), 0, DetailedAmmoBoxData(8, 150)))) + ), + InternalSlot(272, PlanetSideGUID(4759), 387, DetailedAmmoBoxData(8, 50)), + InternalSlot(462, PlanetSideGUID(7377), 390, + DetailedWeaponData(6, 8, List(InternalSlot(463, PlanetSideGUID(8155), 0, DetailedAmmoBoxData(8, 150)))) + ), + InternalSlot(843, PlanetSideGUID(6709), 480, DetailedAmmoBoxData(8, 1)), + InternalSlot(843, PlanetSideGUID(5276), 484, DetailedAmmoBoxData(8, 1)), + InternalSlot(843, PlanetSideGUID(7769), 488, DetailedAmmoBoxData(8, 1)), + InternalSlot(844, PlanetSideGUID(5334), 492, DetailedAmmoBoxData(8, 1)), + InternalSlot(844, PlanetSideGUID(6219), 496, DetailedAmmoBoxData(8, 1)), + InternalSlot(842, PlanetSideGUID(7279), 500, DetailedAmmoBoxData(8, 1)), + InternalSlot(842, PlanetSideGUID(5415), 504, DetailedAmmoBoxData(8, 1)), + InternalSlot(175, PlanetSideGUID(5741), 540, + DetailedWeaponData(6, 8, List(InternalSlot(540, PlanetSideGUID(5183), 0, DetailedAmmoBoxData(8, 1)))) + ), + InternalSlot(324, PlanetSideGUID(6208), 541, + DetailedWeaponData(6, 8, List(InternalSlot(540, PlanetSideGUID(5029), 0, DetailedAmmoBoxData(8, 1)))) + ), + InternalSlot(324, PlanetSideGUID(8589), 542, + DetailedWeaponData(6, 8, List(InternalSlot(540, PlanetSideGUID(9217), 0, DetailedAmmoBoxData(8, 1)))) + ), + InternalSlot(175, PlanetSideGUID(8901), 543, + DetailedWeaponData(6, 8, List(InternalSlot(540, PlanetSideGUID(7633), 0, DetailedAmmoBoxData(8, 1)))) + ), + InternalSlot(175, PlanetSideGUID(8419), 544, + DetailedWeaponData(6, 8, List(InternalSlot(540, PlanetSideGUID(6546), 0, DetailedAmmoBoxData(8, 1)))) + ), + InternalSlot(175, PlanetSideGUID(4715), 545, + DetailedWeaponData(6, 8, List(InternalSlot(540, PlanetSideGUID(8453), 0, DetailedAmmoBoxData(8, 1)))) + ), + InternalSlot(324, PlanetSideGUID(3577), 546, + DetailedWeaponData(6, 8, List(InternalSlot(540, PlanetSideGUID(9202), 0, DetailedAmmoBoxData(8, 1)))) + ), + InternalSlot(324, PlanetSideGUID(6003), 547, + DetailedWeaponData(6, 8, List(InternalSlot(540, PlanetSideGUID(3260), 0, DetailedAmmoBoxData(8, 1)))) + ), + InternalSlot(324, PlanetSideGUID(9140), 548, + DetailedWeaponData(6, 8, List(InternalSlot(540,PlanetSideGUID(3815),0,DetailedAmmoBoxData(8, 1)))) + ), + InternalSlot(324, PlanetSideGUID(4913), 549, + DetailedWeaponData(6, 8, List(InternalSlot(540,PlanetSideGUID(7222),0,DetailedAmmoBoxData(8, 1)))) + ), + InternalSlot(324, PlanetSideGUID(6954), 550, + DetailedWeaponData(6, 8, List(InternalSlot(540,PlanetSideGUID(2953),0,DetailedAmmoBoxData(8, 1)))) + ), + InternalSlot(324, PlanetSideGUID(6405), 551, + DetailedWeaponData(6, 8, List(InternalSlot(540,PlanetSideGUID(4676),0,DetailedAmmoBoxData(8, 1)))) + ), + InternalSlot(324, PlanetSideGUID(8915), 552, + DetailedWeaponData(6, 8, List(InternalSlot(540,PlanetSideGUID(4018),0,DetailedAmmoBoxData(8, 1)))) + ), + InternalSlot(324, PlanetSideGUID(4993), 553, + DetailedWeaponData(6, 8, List(InternalSlot(540,PlanetSideGUID(6775),0,DetailedAmmoBoxData(8, 1)))) + ), + InternalSlot(175, PlanetSideGUID(5053), 554, + DetailedWeaponData(6, 8, List(InternalSlot(540,PlanetSideGUID(6418),0,DetailedAmmoBoxData(8, 1)))) + ), + InternalSlot(324, PlanetSideGUID(9244), 555, + DetailedWeaponData(6, 8, List(InternalSlot(540,PlanetSideGUID(3327),0,DetailedAmmoBoxData(8, 1)))) + ), + InternalSlot(468, PlanetSideGUID(6292), 556, + DetailedWeaponData(6, 8, List(InternalSlot(540,PlanetSideGUID(6918),0,DetailedAmmoBoxData(8, 1)))) + ), + InternalSlot(842, PlanetSideGUID(5357), 558, DetailedAmmoBoxData(8, 1)), + InternalSlot(844, PlanetSideGUID(4435), 562, DetailedAmmoBoxData(8, 1)), + InternalSlot(843, PlanetSideGUID(7242), 566, DetailedAmmoBoxData(8, 1)), + InternalSlot(175, PlanetSideGUID(7330), 570, + DetailedWeaponData(6, 8, List(InternalSlot(540, PlanetSideGUID(4786), 0, DetailedAmmoBoxData(8, 1)))) + ), + InternalSlot(468, PlanetSideGUID(7415), 571, + DetailedWeaponData(6, 8, List(InternalSlot(540, PlanetSideGUID(6536), 0, DetailedAmmoBoxData(8, 1)))) + ), + InternalSlot(175, PlanetSideGUID(3949), 572, + DetailedWeaponData(6, 8, List(InternalSlot(540, PlanetSideGUID(7526), 0, DetailedAmmoBoxData(8, 1)))) + ), + InternalSlot(175, PlanetSideGUID(3805), 573, + DetailedWeaponData(6, 8, List(InternalSlot(540, PlanetSideGUID(7358), 0, DetailedAmmoBoxData(8, 1)))) + ), + InternalSlot(324, PlanetSideGUID(4493), 574, + DetailedWeaponData(6, 8, List(InternalSlot(540, PlanetSideGUID(6852), 0, DetailedAmmoBoxData(8, 1)))) + ), + InternalSlot(324, PlanetSideGUID(5762), 575, + DetailedWeaponData(6, 8, List(InternalSlot(540, PlanetSideGUID(3463), 0, DetailedAmmoBoxData(8, 1)))) + ), + InternalSlot(175, PlanetSideGUID(3315), 576, + DetailedWeaponData(6, 8, List(InternalSlot(540, PlanetSideGUID(7619), 0, DetailedAmmoBoxData(8, 1)))) + ), + InternalSlot(324, PlanetSideGUID(6263), 577, + DetailedWeaponData(6, 8, List(InternalSlot(540, PlanetSideGUID(5912), 0, DetailedAmmoBoxData(8, 1)))) + ), + InternalSlot(468, PlanetSideGUID(4028), 578, + DetailedWeaponData(6, 8, List(InternalSlot(540, PlanetSideGUID(8021), 0, DetailedAmmoBoxData(8, 1)))) + ), + InternalSlot(175, PlanetSideGUID(2843), 579, + DetailedWeaponData(6, 8, List(InternalSlot(540, PlanetSideGUID(7250), 0, DetailedAmmoBoxData(8, 1)))) + ), + InternalSlot(175, PlanetSideGUID(9143), 580, + DetailedWeaponData(6, 8, List(InternalSlot(540, PlanetSideGUID(5195), 0, DetailedAmmoBoxData(8, 1)))) + ), + InternalSlot(468, PlanetSideGUID(5024), 581, + DetailedWeaponData(6, 8, List(InternalSlot(540, PlanetSideGUID(4287), 0, DetailedAmmoBoxData(8, 1)))) + ), + InternalSlot(468, PlanetSideGUID(6582), 582, + DetailedWeaponData(6, 8, List(InternalSlot(540, PlanetSideGUID(4915), 0, DetailedAmmoBoxData(8, 1)))) + ), + InternalSlot(468, PlanetSideGUID(6425), 583, + DetailedWeaponData(6, 8, List(InternalSlot(540, PlanetSideGUID(8872), 0, DetailedAmmoBoxData(8, 1)))) + ), + InternalSlot(468, PlanetSideGUID(4431), 584, + DetailedWeaponData(6, 8, List(InternalSlot(540, PlanetSideGUID(4191), 0, DetailedAmmoBoxData(8, 1)))) + ), + InternalSlot(175, PlanetSideGUID(8339), 585, + DetailedWeaponData(6, 8, List(InternalSlot(540, PlanetSideGUID(7317), 0, DetailedAmmoBoxData(8, 1)))) + ), + InternalSlot(175, PlanetSideGUID(3277), 586, + DetailedWeaponData(6, 8, List(InternalSlot(540, PlanetSideGUID(6469), 0, DetailedAmmoBoxData(8, 1)))) + ) + )))) + ), + InternalSlot(213, PlanetSideGUID(6877), 6, DetailedCommandDetonaterData(4, 8)), + InternalSlot(755, PlanetSideGUID(6227), 9, DetailedAmmoBoxData(8, 16)), + InternalSlot(728, PlanetSideGUID(7181), 12, DetailedREKData(4, 16)), + InternalSlot(536, PlanetSideGUID(4077), 33, DetailedAmmoBoxData(8, 1)), + InternalSlot(680, PlanetSideGUID(4377), 37, + DetailedWeaponData(2, 8, List(InternalSlot(681, PlanetSideGUID(8905), 0, DetailedAmmoBoxData(8, 3)))) + ), + InternalSlot(32, PlanetSideGUID(5523), 39, DetailedACEData(4)), + InternalSlot(673, PlanetSideGUID(3661), 60, + DetailedWeaponData(2, 8, List(InternalSlot(674, PlanetSideGUID(8542), 0, DetailedAmmoBoxData(8, 3)))) + ) + ) + ) + ), + DrawnSlot.None + ) + val msg = ObjectCreateDetailedMessage(ObjectClass.avatar, PlanetSideGUID(75), obj) + val pkt = PacketCoding.EncodePacket(msg).require.toByteVector + + val pkt_bitv = pkt.toBitVector + val ori_bitv = string_testchar_br32.toBitVector + pkt_bitv.take(153) mustEqual ori_bitv.take(153) //skip 1 + pkt_bitv.drop(154).take(144) mustEqual ori_bitv.drop(154).take(144) //skip 24 + pkt_bitv.drop(322).take(72) mustEqual ori_bitv.drop(322).take(72) //skip 24 + pkt_bitv.drop(418).take(55) mustEqual ori_bitv.drop(418).take(55) //skip 1 + pkt_bitv.drop(474).take(102) mustEqual ori_bitv.drop(474).take(102) //skip 126 + pkt_bitv.drop(702).take(192) mustEqual ori_bitv.drop(702).take(192) //skip 36 + pkt_bitv.drop(930) mustEqual ori_bitv.drop(930) //to end + } } diff --git a/common/src/test/scala/objects/ConverterTest.scala b/common/src/test/scala/objects/ConverterTest.scala index 2bcde21f..9f53443c 100644 --- a/common/src/test/scala/objects/ConverterTest.scala +++ b/common/src/test/scala/objects/ConverterTest.scala @@ -1,7 +1,7 @@ // Copyright (c) 2017 PSForever package objects -import net.psforever.objects.definition.converter.{ACEConverter, REKConverter} +import net.psforever.objects.definition.converter.{ACEConverter, CharacterSelectConverter, REKConverter} import net.psforever.objects._ import net.psforever.objects.definition._ import net.psforever.objects.equipment.CItem.{DeployedItem, Unit} @@ -130,7 +130,7 @@ class ConverterTest extends Specification { } "Player" should { - "convert to packet" in { + val obj : Player = { /* Create an AmmoBoxDefinition with which to build two AmmoBoxes Create a ToolDefinition with which to create a Tool @@ -157,9 +157,64 @@ class ConverterTest extends Specification { obj.Slot(2).Equipment = tool obj.Slot(5).Equipment.get.GUID = PlanetSideGUID(94) obj.Inventory += 8 -> box2 + obj + } + val converter = new CharacterSelectConverter - obj.Definition.Packet.DetailedConstructorData(obj).isSuccess mustEqual true - ok //TODO write more of this test + "convert to packet (BR < 24)" in { + obj.BEP = 0 + obj.Definition.Packet.DetailedConstructorData(obj) match { + case Success(pkt) => + ok + case _ => + ko + } + obj.Definition.Packet.ConstructorData(obj) match { + case Success(pkt) => + ok + case _ => + ko + } + } + + "convert to packet (BR >= 24)" in { + obj.BEP = 10000000 + obj.Definition.Packet.DetailedConstructorData(obj) match { + case Success(pkt) => + ok + case _ => + ko + } + obj.Definition.Packet.ConstructorData(obj) match { + case Success(pkt) => + ok + case _ => + ko + } + } + + "convert to simple packet (BR < 24)" in { + obj.BEP = 0 + converter.DetailedConstructorData(obj) match { + case Success(pkt) => + ok + case _ => + ko + } + converter.ConstructorData(obj).isFailure mustEqual true + converter.ConstructorData(obj).get must throwA[Exception] + } + + "convert to simple packet (BR >= 24)" in { + obj.BEP = 10000000 + converter.DetailedConstructorData(obj) match { + case Success(pkt) => + ok + case _ => + ko + } + converter.ConstructorData(obj).isFailure mustEqual true + converter.ConstructorData(obj).get must throwA[Exception] } } From 1f5955064191fb2a3635792fdfa703fcb83a4e0c Mon Sep 17 00:00:00 2001 From: FateJH Date: Tue, 17 Oct 2017 13:21:31 -0400 Subject: [PATCH 23/23] removing excess faux-guid initialization from char select; moving client poke executor to stop server from generating new sessions when idling --- .../src/main/scala/WorldSessionActor.scala | 33 +++---------------- 1 file changed, 5 insertions(+), 28 deletions(-) diff --git a/pslogin/src/main/scala/WorldSessionActor.scala b/pslogin/src/main/scala/WorldSessionActor.scala index ca7ea356..34f9cb5c 100644 --- a/pslogin/src/main/scala/WorldSessionActor.scala +++ b/pslogin/src/main/scala/WorldSessionActor.scala @@ -654,6 +654,11 @@ class WorldSessionActor extends Actor with MDCContextAware { log.info(s"New world login to $server with Token:$token. $clientVersion") self ! ListAccountCharacters + import scala.concurrent.duration._ + import scala.concurrent.ExecutionContext.Implicits.global + clientKeepAlive.cancel + clientKeepAlive = context.system.scheduler.schedule(0 seconds, 500 milliseconds, self, PokeClient()) + case msg @ CharacterCreateRequestMessage(name, head, voice, gender, empire) => log.info("Handling " + msg) sendResponse(PacketCoding.CreateGamePacket(0, ActionResultMessage(true, None))) @@ -670,11 +675,6 @@ class WorldSessionActor extends Actor with MDCContextAware { //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) } @@ -1449,18 +1449,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) } @@ -1492,17 +1480,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() }