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)
+}