diff --git a/common/src/main/scala/net/psforever/objects/GlobalDefinitions.scala b/common/src/main/scala/net/psforever/objects/GlobalDefinitions.scala index 4fccc47f..6d961e8f 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.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.packet.game.objectcreate.ObjectClass +import net.psforever.objects.serverobject.locks.IFFLockDefinition +import net.psforever.objects.serverobject.terminals.{CertTerminalDefinition, OrderTerminalDefinition} import net.psforever.types.PlanetSideEmpire object GlobalDefinitions { @@ -1242,4 +1243,9 @@ object GlobalDefinitions { order_terminal = new OrderTerminalDefinition val cert_terminal = new CertTerminalDefinition + + val + 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 ab1cc89a..15572f51 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 new file mode 100644 index 00000000..c73b7a23 --- /dev/null +++ b/common/src/main/scala/net/psforever/objects/serverobject/CommonMessages.scala @@ -0,0 +1,10 @@ +// Copyright (c) 2017 PSForever +package net.psforever.objects.serverobject + +import net.psforever.objects.Player + +//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 new file mode 100644 index 00000000..f4497544 --- /dev/null +++ b/common/src/main/scala/net/psforever/objects/serverobject/PlanetSideServerObject.scala @@ -0,0 +1,32 @@ +// Copyright (c) 2017 PSForever +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 + } + actor + } +} diff --git a/common/src/main/scala/net/psforever/objects/serverobject/builders/DoorObjectBuilder.scala b/common/src/main/scala/net/psforever/objects/serverobject/builders/DoorObjectBuilder.scala new file mode 100644 index 00000000..d40ff826 --- /dev/null +++ b/common/src/main/scala/net/psforever/objects/serverobject/builders/DoorObjectBuilder.scala @@ -0,0 +1,34 @@ +// Copyright (c) 2017 PSForever +package net.psforever.objects.serverobject.builders + +import akka.actor.Props +import net.psforever.objects.serverobject.doors.{Door, DoorControl, 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 = context.actorOf(Props(classOf[DoorControl], obj), s"${ddef.Name}_${obj.GUID.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/serverobject/builders/IFFLockObjectBuilder.scala b/common/src/main/scala/net/psforever/objects/serverobject/builders/IFFLockObjectBuilder.scala new file mode 100644 index 00000000..26834d93 --- /dev/null +++ b/common/src/main/scala/net/psforever/objects/serverobject/builders/IFFLockObjectBuilder.scala @@ -0,0 +1,34 @@ +// Copyright (c) 2017 PSForever +package net.psforever.objects.serverobject.builders + +import akka.actor.Props +import net.psforever.objects.serverobject.locks.{IFFLock, IFFLockControl, IFFLockDefinition} + +/** + * 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] { + import akka.actor.ActorContext + import net.psforever.objects.guid.NumberPoolHub + + def Build(implicit context : ActorContext, guid : NumberPoolHub) : 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 + } +} + +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/ServerObjectBuilder.scala b/common/src/main/scala/net/psforever/objects/serverobject/builders/ServerObjectBuilder.scala similarity index 81% 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 2635df09..e1bb0ce7 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 @@ -7,9 +7,11 @@ 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` */ -trait ServerObjectBuilder { +//TODO can we changed PlanetSideGameObject -> PlanetSideServerObject? +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 +25,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/serverobject/builders/TerminalObjectBuilder.scala similarity index 74% 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 2ced4d84..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,21 +1,22 @@ // 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. * @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 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/serverobject/doors/Base.scala b/common/src/main/scala/net/psforever/objects/serverobject/doors/Base.scala new file mode 100644 index 00000000..90e6c490 --- /dev/null +++ b/common/src/main/scala/net/psforever/objects/serverobject/doors/Base.scala @@ -0,0 +1,21 @@ +// Copyright (c) 2017 PSForever +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 + + 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/serverobject/doors/Door.scala b/common/src/main/scala/net/psforever/objects/serverobject/doors/Door.scala new file mode 100644 index 00000000..ae63affd --- /dev/null +++ b/common/src/main/scala/net/psforever/objects/serverobject/doors/Door.scala @@ -0,0 +1,90 @@ +// Copyright (c) 2017 PSForever +package net.psforever.objects.serverobject.doors + +import net.psforever.objects.serverobject.PlanetSideServerObject +import net.psforever.objects.Player +import net.psforever.packet.game.UseItemMessage + +/** + * 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(private val ddef : DoorDefinition) extends PlanetSideServerObject { + private var openState : Boolean = false + private var lockState : Boolean = false + + def Open : Boolean = openState + + def Open_=(open : Boolean) : Boolean = { + openState = open + Open + } + + 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) { + openState = false + Door.CloseEvent() + } + else { + Door.NoEvent() + } + } + + def Definition : DoorDefinition = ddef +} + +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) + } +} 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 new file mode 100644 index 00000000..8f116331 --- /dev/null +++ b/common/src/main/scala/net/psforever/objects/serverobject/doors/DoorControl.scala @@ -0,0 +1,18 @@ +// Copyright (c) 2017 PSForever +package net.psforever.objects.serverobject.doors + +import akka.actor.Actor + +/** + * An `Actor` that handles messages being dispatched to a specific `Door`. + * @param door the `Door` object being governed + */ +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/serverobject/doors/DoorDefinition.scala b/common/src/main/scala/net/psforever/objects/serverobject/doors/DoorDefinition.scala new file mode 100644 index 00000000..6a22670c --- /dev/null +++ b/common/src/main/scala/net/psforever/objects/serverobject/doors/DoorDefinition.scala @@ -0,0 +1,12 @@ +// Copyright (c) 2017 PSForever +package net.psforever.objects.serverobject.doors + +import net.psforever.objects.definition.ObjectDefinition + +/** + * The definition for any `Door`. + * Object Id 242 is a generic door. + */ +class DoorDefinition extends ObjectDefinition(242) { + Name = "door" +} 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..e08587e8 --- /dev/null +++ b/common/src/main/scala/net/psforever/objects/serverobject/locks/IFFLock.scala @@ -0,0 +1,66 @@ +// Copyright (c) 2017 PSForever +package net.psforever.objects.serverobject.locks + +import net.psforever.objects.Player +import net.psforever.objects.serverobject.PlanetSideServerObject +import net.psforever.packet.game.PlanetSideGUID +import net.psforever.types.Vector3 + +/** + * 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, PlanetSideGUID, Vector3)] = hackedBy + + def HackedBy_=(agent : Player) : Option[(Player, PlanetSideGUID, Vector3)] = HackedBy_=(Some(agent)) + + /** + * 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.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.GUID, agent.get.Position) + } + } + HackedBy + } + + def Definition : IFFLockDefinition = idef +} + +object 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 new file mode 100644 index 00000000..bf9d1b81 --- /dev/null +++ b/common/src/main/scala/net/psforever/objects/serverobject/locks/IFFLockControl.scala @@ -0,0 +1,22 @@ +// Copyright (c) 2017 PSForever +package net.psforever.objects.serverobject.locks + +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 _ => ; //no default message + } +} 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 new file mode 100644 index 00000000..d8c180d8 --- /dev/null +++ b/common/src/main/scala/net/psforever/objects/serverobject/locks/IFFLockDefinition.scala @@ -0,0 +1,12 @@ +// Copyright (c) 2017 PSForever +package net.psforever.objects.serverobject.locks + +import net.psforever.objects.definition.ObjectDefinition + +/** + * 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/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/Terminal.scala b/common/src/main/scala/net/psforever/objects/serverobject/terminals/Terminal.scala similarity index 67% 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..57532c54 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,55 +1,59 @@ // 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.packet.game.ItemTransactionMessage -import net.psforever.types.{ExoSuitType, PlanetSideEmpire, TransactionType} +import net.psforever.objects.serverobject.PlanetSideServerObject +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 PlanetSideGameObject { - /** Internal reference to the `Actor` for this `Terminal`, sets up by this `Terminal`. */ - private var actor = ActorRef.noSender +class Terminal(tdef : TerminalDefinition) 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, PlanetSideGUID, Vector3)] = hackedBy + + def HackedBy_=(agent : Player) : Option[(Player, PlanetSideGUID, Vector3)] = HackedBy_=(Some(agent)) /** - * 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` + * 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 Actor(implicit context : ActorContext) : ActorRef = { - if(actor == ActorRef.noSender) { - actor = context.actorOf(Props(classOf[TerminalControl], this), s"${tdef.Name}_${GUID.guid}") + 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.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.GUID, agent.get.Position) + } } - actor + HackedBy } - //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 + //the following fields and related methods are neither finalized nor integrated; GOTO Request private var health : Int = 100 //TODO not real health value - def Faction : PlanetSideEmpire.Value = faction - - def HackedBy : Option[PlanetSideEmpire.Value] = hackedBy - def Health : Int = health - def Convert(toFaction : PlanetSideEmpire.Value) : Unit = { - hackedBy = None - 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) } @@ -148,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/terminals/TerminalControl.scala b/common/src/main/scala/net/psforever/objects/serverobject/terminals/TerminalControl.scala similarity index 52% 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..c74ada79 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,12 +1,10 @@ // Copyright (c) 2017 PSForever -package net.psforever.objects.terminals +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,18 +12,6 @@ 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.Hacked(fact) => - term.HackedBy(fact) - - case TemporaryTerminalMessages.Damaged(dam) => - term.Damaged(dam) - - case TemporaryTerminalMessages.Repaired(rep) => - term.Repair(rep) - 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/terminals/TemporaryTerminalMessages.scala b/common/src/main/scala/net/psforever/objects/terminals/TemporaryTerminalMessages.scala deleted file mode 100644 index 85ae3375..00000000 --- a/common/src/main/scala/net/psforever/objects/terminals/TemporaryTerminalMessages.scala +++ /dev/null @@ -1,13 +0,0 @@ -// Copyright (c) 2017 PSForever -package net.psforever.objects.terminals - -import net.psforever.types.PlanetSideEmpire - -//temporary location for these temporary messages -object TemporaryTerminalMessages { - //TODO send original packets along with these messages - final case class Convert(faction : PlanetSideEmpire.Value) - final case class Hacked(faction : Option[PlanetSideEmpire.Value]) - final case class Damaged(dm : Int) - final case class Repaired(rep : Int) -} 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..68415c50 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.serverobject.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..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 @@ -13,8 +14,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 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]) { + 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 locks, but it was not") + } + } + catch { + case _ : Exception => + slog.error(s"expected an IFF locks, 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 b1cf2e9a..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.
*
@@ -12,32 +14,55 @@ 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 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 - /** - * Append the builder for a server object to the list of builders known to this `ZoneMap`. - * @param obj the builder for a server object - */ - def LocalObject(obj : ServerObjectBuilder) : Unit = { - localObjects = localObjects :+ obj - } - /** * The list of all server object builder wrappers that have been assigned to this `ZoneMap`. * @return the `List` of all `ServerObjectBuilders` known to this `ZoneMap` */ - def LocalObjects : List[ServerObjectBuilder] = { + 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 + */ + def LocalObject(obj : ServerObjectBuilder[_]) : Unit = { + localObjects = localObjects :+ obj + } + + 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/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/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/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/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/AvatarService.scala b/pslogin/src/main/scala/AvatarService.scala deleted file mode 100644 index 377a9249..00000000 --- a/pslogin/src/main/scala/AvatarService.scala +++ /dev/null @@ -1,228 +0,0 @@ -// Copyright (c) 2016 PSForever.net to present -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 { - 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 { - 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 : Action) - -final case class AvatarServiceResponse(toChannel : String, avatar_guid : PlanetSideGUID, replyMessage : Response) - -/* - /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 - - override def preStart = { - log.info("Starting...") - } - - val AvatarEvents = new AvatarEventBus - - /*val channelMap = Map( - AvatarMessageType.CMT_OPEN -> AvatarPath("local") - )*/ - - def receive = { - case Join(channel) => - val path = "/Avatar/" + channel - val who = sender() - - log.info(s"$who has joined $path") - - AvatarEvents.subscribe(who, path) - case Leave() => - AvatarEvents.unsubscribe(sender()) - case 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)) - ) - case AvatarAction.EquipmentInHand(player_guid, slot, obj) => - AvatarEvents.publish( - AvatarServiceResponse("/Avatar/" + forChannel, 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)) - ) - case AvatarAction.LoadPlayer(player_guid, pdata) => - AvatarEvents.publish( - AvatarServiceResponse("/Avatar/" + forChannel, 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)) - ) - case AvatarAction.ObjectHeld(player_guid, slot) => - AvatarEvents.publish( - AvatarServiceResponse("/Avatar/" + forChannel, 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)) - ) - case AvatarAction.PlayerState(guid, msg, spectator, weapon) => - AvatarEvents.publish( - AvatarServiceResponse("/Avatar/" + forChannel, guid, AvatarServiceResponse.PlayerState(msg, spectator, weapon)) - ) - case AvatarAction.Reload(player_guid, mag) => - AvatarEvents.publish( - AvatarServiceResponse("/Avatar/" + forChannel, player_guid, AvatarServiceResponse.Reload(mag)) - ) - case _ => ; - } - - /* - case AvatarService.PlayerStateMessage(msg) => - // log.info(s"NEW: ${m}") - val playerOpt: Option[PlayerAvatar] = PlayerMasterList.getPlayer(msg.avatar_guid) - if (playerOpt.isDefined) { - val player: PlayerAvatar = playerOpt.get - AvatarEvents.publish(AvatarMessage("/Avatar/" + player.continent, msg.avatar_guid, - AvatarServiceReply.PlayerStateMessage(msg.pos, msg.vel, msg.facingYaw, msg.facingPitch, msg.facingYawUpper, msg.is_crouching, msg.is_jumping, msg.jump_thrust, msg.is_cloaked) - )) - - } - case AvatarService.LoadMap(msg) => - val playerOpt: Option[PlayerAvatar] = PlayerMasterList.getPlayer(msg.guid) - if (playerOpt.isDefined) { - val player: PlayerAvatar = playerOpt.get - AvatarEvents.publish(AvatarMessage("/Avatar/" + player.continent, PlanetSideGUID(msg.guid), - AvatarServiceReply.LoadMap() - )) - } - case AvatarService.unLoadMap(msg) => - val playerOpt: Option[PlayerAvatar] = PlayerMasterList.getPlayer(msg.guid) - if (playerOpt.isDefined) { - val player: PlayerAvatar = playerOpt.get - AvatarEvents.publish(AvatarMessage("/Avatar/" + player.continent, PlanetSideGUID(msg.guid), - AvatarServiceReply.unLoadMap() - )) - } - case AvatarService.ObjectHeld(msg) => - val playerOpt: Option[PlayerAvatar] = PlayerMasterList.getPlayer(msg.guid) - if (playerOpt.isDefined) { - val player: PlayerAvatar = playerOpt.get - AvatarEvents.publish(AvatarMessage("/Avatar/" + player.continent, PlanetSideGUID(msg.guid), - AvatarServiceReply.ObjectHeld() - )) - } - case AvatarService.PlanetsideAttribute(guid, attribute_type, attribute_value) => - val playerOpt: Option[PlayerAvatar] = PlayerMasterList.getPlayer(guid) - if (playerOpt.isDefined) { - val player: PlayerAvatar = playerOpt.get - AvatarEvents.publish(AvatarMessage("/Avatar/" + player.continent, guid, - AvatarServiceReply.PlanetSideAttribute(attribute_type, attribute_value) - )) - } - case AvatarService.PlayerStateShift(killer, guid) => - val playerOpt: Option[PlayerAvatar] = PlayerMasterList.getPlayer(guid) - if (playerOpt.isDefined) { - val player: PlayerAvatar = playerOpt.get - AvatarEvents.publish(AvatarMessage("/Avatar/" + player.continent, guid, - AvatarServiceReply.PlayerStateShift(killer) - )) - } - case AvatarService.DestroyDisplay(killer, victim) => - val playerOpt: Option[PlayerAvatar] = PlayerMasterList.getPlayer(victim) - if (playerOpt.isDefined) { - val player: PlayerAvatar = playerOpt.get - AvatarEvents.publish(AvatarMessage("/Avatar/" + player.continent, victim, - AvatarServiceReply.DestroyDisplay(killer) - )) - } - case AvatarService.HitHintReturn(source_guid,victim_guid) => - val playerOpt: Option[PlayerAvatar] = PlayerMasterList.getPlayer(source_guid) - if (playerOpt.isDefined) { - val player: PlayerAvatar = playerOpt.get - AvatarEvents.publish(AvatarMessage("/Avatar/" + player.continent, victim_guid, - AvatarServiceReply.DestroyDisplay(source_guid) - )) - } - case AvatarService.ChangeWeapon(unk1, sessionId) => - val playerOpt: Option[PlayerAvatar] = PlayerMasterList.getPlayer(sessionId) - if (playerOpt.isDefined) { - val player: PlayerAvatar = playerOpt.get - AvatarEvents.publish(AvatarMessage("/Avatar/" + player.continent, PlanetSideGUID(player.guid), - AvatarServiceReply.ChangeWeapon(unk1) - )) - } - */ - 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 f71cce11..6e172cbe 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 @@ -12,11 +12,14 @@ 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 net.psforever.objects.serverobject.builders.{DoorObjectBuilder, IFFLockObjectBuilder, TerminalObjectBuilder} 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 @@ -202,6 +205,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,14 +226,38 @@ object PsLogin { def createContinents() : List[Zone] = { val map13 = new ZoneMap("map13") { import net.psforever.objects.GlobalDefinitions._ + + LocalObject(DoorObjectBuilder(door, 330)) + LocalObject(DoorObjectBuilder(door, 332)) + LocalObject(DoorObjectBuilder(door, 372)) + LocalObject(DoorObjectBuilder(door, 373)) + 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)) 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/WorldSessionActor.scala b/pslogin/src/main/scala/WorldSessionActor.scala index 34f9cb5c..c202a572 100644 --- a/pslogin/src/main/scala/WorldSessionActor.scala +++ b/pslogin/src/main/scala/WorldSessionActor.scala @@ -2,24 +2,30 @@ 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 import MDCContextAware.Implicits._ import ServiceManager.Lookup import net.psforever.objects._ +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._ +import services._ +import services.avatar._ +import services.local._ import scala.annotation.tailrec import scala.util.Success @@ -31,18 +37,22 @@ 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 progressBarValue : Option[Float] = None var clientKeepAlive : Cancellable = WorldSessionActor.DefaultCancellable + var progressBarUpdate : Cancellable = WorldSessionActor.DefaultCancellable override def postStop() = { if(clientKeepAlive != null) clientKeepAlive.cancel() - avatarService ! Leave() + avatarService ! Service.Leave() + localService ! Service.Leave() LivePlayerList.Remove(sessionId) match { case Some(tplayer) => if(tplayer.HasGUID) { @@ -64,11 +74,13 @@ class WorldSessionActor extends Actor with MDCContextAware { if(pipe.hasNext) { rightRef = pipe.next rightRef !> HelloFriend(sessionId, pipe) - } else { + } + else { rightRef = sender() } context.become(Started) ServiceManager.serviceManager ! Lookup("avatar") + ServiceManager.serviceManager ! Lookup("local") ServiceManager.serviceManager ! Lookup("taskResolver") ServiceManager.serviceManager ! Lookup("galaxy") @@ -81,6 +93,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) @@ -206,6 +221,48 @@ class WorldSessionActor extends Actor with MDCContextAware { case _ => ; } + 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 LocalServiceResponse.HackClear(target_guid, unk1, 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, 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) => + val door_guid = msg.object_guid + order match { + case Door.OpenEvent() => + 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(door_guid, 17))) + localService ! LocalServiceMessage(continent.Id, LocalAction.DoorCloses(tplayer.GUID, door_guid)) + + case Door.NoEvent() => ; + } + case Terminal.TerminalMessage(tplayer, msg, order) => order match { case Terminal.BuyExosuit(exosuit, subtype) => @@ -532,6 +589,35 @@ class WorldSessionActor extends Actor with MDCContextAware { continent.Actor ! Zone.DropItemOnGround(item, item.Position, item.Orientation) //restore } + case ItemHacking(tplayer, target, tool_guid, delta, completeAction, tickAction) => + progressBarUpdate.cancel + if(progressBarValue.isDefined) { + val progressBarVal : Float = progressBarValue.get + delta + val vis = if(progressBarVal == 0L) { //hack state for progress bar visibility + HackState.Start + } + else if(progressBarVal > 100L) { + HackState.Finished + } + else { + HackState.Ongoing + } + 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, player.GUID, 100, 1114636288L, HackState.Hacked, 8L))) + completeAction() + } + else { //continue next tick + 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, completeAction)) + } + } + case ResponseToSelf(pkt) => log.info(s"Received a direct message: $pkt") sendResponse(pkt) @@ -714,7 +800,8 @@ class WorldSessionActor extends Actor with MDCContextAware { ) }) - avatarService ! Join(player.Continent) + 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) => @@ -776,6 +863,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) @@ -945,15 +1033,53 @@ 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) => + 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, _, _)) => + 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 locks is hacked + door.Actor ! Door.Use(player, msg) + } + case None => + if(lock_hacked) { //is lock hacked? this may be a weird case + door.Actor ! Door.Use(player, msg) + } + } + case None => + 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) { + //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, FinishHackingDoor(panel, 1114636288L)) + log.info("Hacking a door~") + } + case _ => ; + } + + 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))) + } + + case None => ; } case msg @ UnuseItemMessage(player_guid, item) => @@ -1059,6 +1185,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") } @@ -1500,6 +1629,22 @@ class WorldSessionActor extends Actor with MDCContextAware { } } + /** + * 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 + //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)) + } + def failWithError(error : String) = { log.error(error) sendResponse(PacketCoding.CreateControlPacket(ConnectionClose())) @@ -1531,6 +1676,23 @@ object WorldSessionActor { 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, + delta : Float, + completeAction : () => Unit, + tickAction : Option[() => Unit] = None) /** * A placeholder `Cancellable` object. */ diff --git a/pslogin/src/main/scala/services/Service.scala b/pslogin/src/main/scala/services/Service.scala new file mode 100644 index 00000000..eed17c79 --- /dev/null +++ b/pslogin/src/main/scala/services/Service.scala @@ -0,0 +1,34 @@ +// 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() +} + +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/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/services/avatar/AvatarService.scala b/pslogin/src/main/scala/services/avatar/AvatarService.scala new file mode 100644 index 00000000..5b656c23 --- /dev/null +++ b/pslogin/src/main/scala/services/avatar/AvatarService.scala @@ -0,0 +1,150 @@ +// Copyright (c) 2017 PSForever +package services.avatar + +import akka.actor.Actor +import services.{GenericEventBus, Service} + +class AvatarService extends Actor { + //import AvatarServiceResponse._ + private [this] val log = org.log4s.getLogger + + override def preStart = { + log.info("Starting...") + } + + val AvatarEvents = new GenericEventBus[AvatarServiceResponse] //AvatarEventBus + + def receive = { + case Service.Join(channel) => + val path = s"/$channel/Avatar" + val who = sender() + + log.info(s"$who has joined $path") + + AvatarEvents.subscribe(who, path) + case Service.Leave() => + AvatarEvents.unsubscribe(sender()) + case Service.LeaveAll() => + AvatarEvents.unsubscribe(sender()) + + case AvatarServiceMessage(forChannel, action) => + action match { + case AvatarAction.ArmorChanged(player_guid, suit, subtype) => + AvatarEvents.publish( + AvatarServiceResponse(s"/$forChannel/Avatar", player_guid, AvatarServiceResponse.ArmorChanged(suit, subtype)) + ) + case AvatarAction.EquipmentInHand(player_guid, slot, obj) => + AvatarEvents.publish( + AvatarServiceResponse(s"/$forChannel/Avatar", player_guid, AvatarServiceResponse.EquipmentInHand(slot, obj)) + ) + case AvatarAction.EquipmentOnGround(player_guid, pos, orient, obj) => + AvatarEvents.publish( + AvatarServiceResponse(s"/$forChannel/Avatar", player_guid, AvatarServiceResponse.EquipmentOnGround(pos, orient, obj)) + ) + case AvatarAction.LoadPlayer(player_guid, pdata) => + AvatarEvents.publish( + AvatarServiceResponse(s"/$forChannel/Avatar", player_guid, AvatarServiceResponse.LoadPlayer(pdata)) + ) + case AvatarAction.ObjectDelete(player_guid, item_guid, unk) => + AvatarEvents.publish( + AvatarServiceResponse(s"/$forChannel/Avatar", player_guid, AvatarServiceResponse.ObjectDelete(item_guid, unk)) + ) + case AvatarAction.ObjectHeld(player_guid, slot) => + AvatarEvents.publish( + AvatarServiceResponse(s"/$forChannel/Avatar", player_guid, AvatarServiceResponse.ObjectHeld(slot)) + ) + case AvatarAction.PlanetsideAttribute(guid, attribute_type, attribute_value) => + AvatarEvents.publish( + AvatarServiceResponse(s"/$forChannel/Avatar", guid, AvatarServiceResponse.PlanetSideAttribute(attribute_type, attribute_value)) + ) + case AvatarAction.PlayerState(guid, msg, spectator, weapon) => + AvatarEvents.publish( + AvatarServiceResponse(s"/$forChannel/Avatar", guid, AvatarServiceResponse.PlayerState(msg, spectator, weapon)) + ) + case AvatarAction.Reload(player_guid, mag) => + AvatarEvents.publish( + AvatarServiceResponse(s"/$forChannel/Avatar", player_guid, AvatarServiceResponse.Reload(mag)) + ) + case _ => ; + } + + /* + case AvatarService.PlayerStateMessage(msg) => + // log.info(s"NEW: ${m}") + val playerOpt: Option[PlayerAvatar] = PlayerMasterList.getPlayer(msg.avatar_guid) + if (playerOpt.isDefined) { + val player: PlayerAvatar = playerOpt.get + AvatarEvents.publish(AvatarMessage("/Avatar/" + player.continent, msg.avatar_guid, + AvatarServiceReply.PlayerStateMessage(msg.pos, msg.vel, msg.facingYaw, msg.facingPitch, msg.facingYawUpper, msg.is_crouching, msg.is_jumping, msg.jump_thrust, msg.is_cloaked) + )) + + } + case AvatarService.LoadMap(msg) => + val playerOpt: Option[PlayerAvatar] = PlayerMasterList.getPlayer(msg.guid) + if (playerOpt.isDefined) { + val player: PlayerAvatar = playerOpt.get + AvatarEvents.publish(AvatarMessage("/Avatar/" + player.continent, PlanetSideGUID(msg.guid), + AvatarServiceReply.LoadMap() + )) + } + case AvatarService.unLoadMap(msg) => + val playerOpt: Option[PlayerAvatar] = PlayerMasterList.getPlayer(msg.guid) + if (playerOpt.isDefined) { + val player: PlayerAvatar = playerOpt.get + AvatarEvents.publish(AvatarMessage("/Avatar/" + player.continent, PlanetSideGUID(msg.guid), + AvatarServiceReply.unLoadMap() + )) + } + case AvatarService.ObjectHeld(msg) => + val playerOpt: Option[PlayerAvatar] = PlayerMasterList.getPlayer(msg.guid) + if (playerOpt.isDefined) { + val player: PlayerAvatar = playerOpt.get + AvatarEvents.publish(AvatarMessage("/Avatar/" + player.continent, PlanetSideGUID(msg.guid), + AvatarServiceReply.ObjectHeld() + )) + } + case AvatarService.PlanetsideAttribute(guid, attribute_type, attribute_value) => + val playerOpt: Option[PlayerAvatar] = PlayerMasterList.getPlayer(guid) + if (playerOpt.isDefined) { + val player: PlayerAvatar = playerOpt.get + AvatarEvents.publish(AvatarMessage("/Avatar/" + player.continent, guid, + AvatarServiceReply.PlanetSideAttribute(attribute_type, attribute_value) + )) + } + case AvatarService.PlayerStateShift(killer, guid) => + val playerOpt: Option[PlayerAvatar] = PlayerMasterList.getPlayer(guid) + if (playerOpt.isDefined) { + val player: PlayerAvatar = playerOpt.get + AvatarEvents.publish(AvatarMessage("/Avatar/" + player.continent, guid, + AvatarServiceReply.PlayerStateShift(killer) + )) + } + case AvatarService.DestroyDisplay(killer, victim) => + val playerOpt: Option[PlayerAvatar] = PlayerMasterList.getPlayer(victim) + if (playerOpt.isDefined) { + val player: PlayerAvatar = playerOpt.get + AvatarEvents.publish(AvatarMessage("/Avatar/" + player.continent, victim, + AvatarServiceReply.DestroyDisplay(killer) + )) + } + case AvatarService.HitHintReturn(source_guid,victim_guid) => + val playerOpt: Option[PlayerAvatar] = PlayerMasterList.getPlayer(source_guid) + if (playerOpt.isDefined) { + val player: PlayerAvatar = playerOpt.get + AvatarEvents.publish(AvatarMessage("/Avatar/" + player.continent, victim_guid, + AvatarServiceReply.DestroyDisplay(source_guid) + )) + } + case AvatarService.ChangeWeapon(unk1, sessionId) => + val playerOpt: Option[PlayerAvatar] = PlayerMasterList.getPlayer(sessionId) + if (playerOpt.isDefined) { + val player: PlayerAvatar = playerOpt.get + AvatarEvents.publish(AvatarMessage("/Avatar/" + player.continent, PlanetSideGUID(player.guid), + AvatarServiceReply.ChangeWeapon(unk1) + )) + } + */ + case msg => + log.info(s"Unhandled message $msg from $sender") + } +} 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..29e05ed6 --- /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 +} 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/services/local/LocalService.scala b/pslogin/src/main/scala/services/local/LocalService.scala new file mode 100644 index 00000000..56acf25d --- /dev/null +++ b/pslogin/src/main/scala/services/local/LocalService.scala @@ -0,0 +1,73 @@ +// Copyright (c) 2017 PSForever +package services.local + +import akka.actor.{Actor, Props} +import services.local.support.{DoorCloseActor, HackClearActor} +import services.{GenericEventBus, Service} + +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 = { + 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.DoorOpens(player_guid, zone, door) => + doorCloser ! DoorCloseActor.DoorIsOpen(door, zone) + 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/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 _ => ; + } + + //response from DoorCloseActor + case DoorCloseActor.CloseTheDoor(door_guid, zone_id) => + LocalEvents.publish( + 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", Service.defaultPlayerGUID, LocalServiceResponse.HackClear(target_guid, unk1, unk2)) + ) + + case msg => + log.info(s"Unhandled message $msg from $sender") + } +} 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/pslogin/src/main/scala/services/local/support/DoorCloseActor.scala b/pslogin/src/main/scala/services/local/support/DoorCloseActor.scala new file mode 100644 index 00000000..a2ca622c --- /dev/null +++ b/pslogin/src/main/scala/services/local/support/DoorCloseActor.scala @@ -0,0 +1,135 @@ +// Copyright (c) 2017 PSForever +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 +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 { + /** 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 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(DoorCloseActor.timeout, self, DoorCloseActor.TryCloseDoors()) + } + + case DoorCloseActor.TryCloseDoors() => + doorCloserTrigger.cancel + val now : Long = System.nanoTime + val (doorsToClose, doorsLeftOpen) = PartitionEntries(openDoors, now) + openDoors = doorsLeftOpen + doorsToClose.foreach(entry => { + 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, DoorCloseActor.timeout_time - (now - doorsLeftOpen.head.time)) nanoseconds + import scala.concurrent.ExecutionContext.Implicits.global + doorCloserTrigger = context.system.scheduler.scheduleOnce(short_timeout, self, DoorCloseActor.TryCloseDoors()) + } + + case _ => ; + } + + /** + * 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`, whose qualifications are explained above + */ + 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) { + index + } + else { + val entry = iter.next() + if(now - entry.time >= DoorCloseActor.timeout_time) { + recursivePartitionEntries(iter, now, index + 1) + } + else { + index + } + } + } +} + +object DoorCloseActor { + /** 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() { + override def cancel : Boolean = true + override def isCancelled : Boolean = true + } + + /** + * 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) + /** + * 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/pslogin/src/main/scala/services/local/support/HackClearActor.scala b/pslogin/src/main/scala/services/local/support/HackClearActor.scala new file mode 100644 index 00000000..76a7e7f9 --- /dev/null +++ b/pslogin/src/main/scala/services/local/support/HackClearActor.scala @@ -0,0 +1,136 @@ +// Copyright (c) 2017 PSForever +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 +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 { + /** 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 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(HackClearActor.timeout, self, HackClearActor.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) = 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) //call up to the main event system + }) + + if(stillHackedObjects.nonEmpty) { + 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()) + } + + case _ => ; + } + + /** + * 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`, whose qualifications are explained above + */ + 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) { + index + } + else { + val entry = iter.next() + if(now - entry.time >= HackClearActor.timeout_time) { + recursivePartitionEntries(iter, now, index + 1) + } + else { + index + } + } + } +} + +object HackClearActor { + /** 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() { + override def cancel : Boolean = true + override def isCancelled : Boolean = true + } + + /** + * 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) + /** + * 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) +}