comments and documentation, mainly; adjusted the list splitting functionality in the LocalService support Actors

This commit is contained in:
FateJH 2017-10-13 14:57:28 -04:00
parent 0a4bac8ab5
commit d5f40a3d5f
22 changed files with 408 additions and 211 deletions

View file

@ -1245,7 +1245,7 @@ object GlobalDefinitions {
cert_terminal = new CertTerminalDefinition
val
external_lock = new IFFLockDefinition
lock_external = new IFFLockDefinition
val
door = new DoorDefinition
}

View file

@ -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. */

View file

@ -2,9 +2,8 @@
package net.psforever.objects.serverobject
import net.psforever.objects.Player
import net.psforever.types.PlanetSideEmpire
//temporary location for these temporary messages
//temporary location for these messages
object CommonMessages {
final case class Hack(player : Player)
final case class ClearHack()

View file

@ -4,11 +4,25 @@ package net.psforever.objects.serverobject
import akka.actor.ActorRef
import net.psforever.objects.PlanetSideGameObject
/**
* An object layered on top of the standard game object class that maintains an internal `ActorRef`.
* A measure of synchronization can be managed using this `Actor`.
*/
abstract class PlanetSideServerObject extends PlanetSideGameObject {
private var actor = ActorRef.noSender
/**
* Retrieve a reference to the internal `Actor`.
* @return the internal `ActorRef`
*/
def Actor : ActorRef = actor
/**
* Assign an `Actor` to act for this server object.
* This reference is only set once, that is, as long as the internal `ActorRef` directs to `Actor.noSender` (`null`).
* @param control the `Actor` whose functionality will govern this server object
* @return the current internal `ActorRef`
*/
def Actor_=(control : ActorRef) : ActorRef = {
if(actor == ActorRef.noSender) {
actor = control

View file

@ -5,8 +5,8 @@ import akka.actor.Props
import net.psforever.objects.serverobject.locks.{IFFLock, IFFLockControl, IFFLockDefinition}
/**
* Wrapper `Class` designed to instantiate a `Door` server object.
* @param idef a `IFFLockDefinition` object, indicating the specific functionality of the resulting `Door`
* Wrapper `Class` designed to instantiate a door lock server object that is sensitive to user faction affiliation.
* @param idef a `IFFLockDefinition` object, indicating the specific functionality
* @param id the globally unique identifier to which this `IFFLock` will be registered
*/
class IFFLockObjectBuilder(private val idef : IFFLockDefinition, private val id : Int) extends ServerObjectBuilder[IFFLock] {
@ -14,7 +14,7 @@ class IFFLockObjectBuilder(private val idef : IFFLockDefinition, private val id
import net.psforever.objects.guid.NumberPoolHub
def Build(implicit context : ActorContext, guid : NumberPoolHub) : IFFLock = {
val obj = IFFLock()
val obj = IFFLock(idef)
guid.register(obj, id) //non-Actor GUID registration
obj.Actor = context.actorOf(Props(classOf[IFFLockControl], obj), s"${idef.Name}_${obj.GUID.guid}")
obj

View file

@ -7,8 +7,10 @@ import net.psforever.objects.guid.NumberPoolHub
/**
* Wrapper `Trait` designed to be extended to implement custom object instantiation logic at the `ZoneMap` level.
* @tparam A any object that extends from PlanetSideGameObject
* @see `Zone.Init`
*/
//TODO can we changed PlanetSideGameObject -> PlanetSideServerObject?
trait ServerObjectBuilder[A <: PlanetSideGameObject] {
/**
* Instantiate and configure the given server object

View file

@ -3,6 +3,10 @@ package net.psforever.objects.serverobject.doors
import net.psforever.types.PlanetSideEmpire
/**
* A temporary class to represent "facilities" and "structures."
* @param id the map id of the base
*/
class Base(private val id : Int) {
private var faction : PlanetSideEmpire.Value = PlanetSideEmpire.NEUTRAL

View file

@ -6,10 +6,10 @@ import net.psforever.objects.Player
import net.psforever.packet.game.UseItemMessage
/**
* na
* A structure-owned server object that is a "door" that can open and can close.
* @param ddef the `ObjectDefinition` that constructs this object and maintains some of its immutable fields
*/
class Door(ddef : DoorDefinition) extends PlanetSideServerObject {
class Door(private val ddef : DoorDefinition) extends PlanetSideServerObject {
private var openState : Boolean = false
private var lockState : Boolean = false
@ -45,26 +45,46 @@ class Door(ddef : DoorDefinition) extends PlanetSideServerObject {
}
object Door {
/**
* Entry message into this `Door` that carries the request.
* @param player the player who sent this request message
* @param msg the original packet carrying the request
*/
final case class Use(player : Player, msg : UseItemMessage)
/**
* A basic `Trait` connecting all of the actionable `Door` response messages.
*/
sealed trait Exchange
/**
* Message that carries the result of the processed request message back to the original user (`player`).
* @param player the player who sent this request message
* @param msg the original packet carrying the request
* @param response the result of the processed request
*/
final case class DoorMessage(player : Player, msg : UseItemMessage, response : Exchange)
/**
* This door will open.
*/
final case class OpenEvent() extends Exchange
/**
* This door will close.
*/
final case class CloseEvent() extends Exchange
/**
* This door will do nothing.
*/
final case class NoEvent() extends Exchange
/**
* Overloaded constructor.
* @param tdef the `ObjectDefinition` that constructs this object and maintains some of its immutable fields
*/
def apply(tdef : DoorDefinition) : Door = {
new Door(tdef)
}
import net.psforever.packet.game.PlanetSideGUID
def apply(guid : PlanetSideGUID, ddef : DoorDefinition) : Door = {
val obj = new Door(ddef)
obj.GUID = guid
obj
}
}

View file

@ -1,7 +1,7 @@
// Copyright (c) 2017 PSForever
package net.psforever.objects.serverobject.doors
import akka.actor.{Actor, Cancellable}
import akka.actor.Actor
/**
* An `Actor` that handles messages being dispatched to a specific `Door`.
@ -16,10 +16,3 @@ class DoorControl(door : Door) extends Actor {
sender ! Door.NoEvent()
}
}
object DoorControl {
final val DefaultCloser : Cancellable = new Cancellable() {
override def cancel : Boolean = true
override def isCancelled : Boolean = true
}
}

View file

@ -4,7 +4,7 @@ package net.psforever.objects.serverobject.doors
import net.psforever.objects.definition.ObjectDefinition
/**
* The definition for any `door`.
* The definition for any `Door`.
* Object Id 242 is a generic door.
*/
class DoorDefinition extends ObjectDefinition(242) {

View file

@ -1,39 +1,66 @@
// Copyright (c) 2017 PSForever
package net.psforever.objects.serverobject.locks
import net.psforever.objects.{GlobalDefinitions, Player}
import net.psforever.objects.Player
import net.psforever.objects.serverobject.PlanetSideServerObject
import net.psforever.types.{PlanetSideEmpire, Vector3}
import net.psforever.packet.game.PlanetSideGUID
import net.psforever.types.Vector3
class IFFLock extends PlanetSideServerObject {
private var hackedBy : Option[(Player, Vector3)] = None
/**
* A structure-owned server object that is a "door lock."<br>
* <br>
* The "door lock" exerts an "identify friend or foe" field that detects the faction affiliation of a target player.
* It also indirectly inherits faction affiliation from the structure to which it is connected
* or it can be "hacked" whereupon the person exploiting it leaves their "faction" as the aforementioned affiliated faction.
* The `IFFLock` is ideally associated with a server map object - a `Door` - to which it acts as a gatekeeper.
* @param idef the `ObjectDefinition` that constructs this object and maintains some of its immutable fields
*/
class IFFLock(private val idef : IFFLockDefinition) extends PlanetSideServerObject {
/**
* An entry that maintains a reference to the `Player`, and the player's GUID and location when the message was received.
*/
private var hackedBy : Option[(Player, PlanetSideGUID, Vector3)] = None
def HackedBy : Option[(Player, Vector3)] = hackedBy
def HackedBy : Option[(Player, PlanetSideGUID, Vector3)] = hackedBy
def HackedBy_=(agent : Player) : Option[(Player, Vector3)] = HackedBy_=(Some(agent))
def HackedBy_=(agent : Player) : Option[(Player, PlanetSideGUID, Vector3)] = HackedBy_=(Some(agent))
def HackedBy_=(agent : Option[Player]) : Option[(Player, Vector3)] = {
/**
* Set the hack state of this object by recording important information about the player that caused it.
* Set the hack state if there is no current hack state.
* Override the hack state with a new hack state if the new user has different faction affiliation.
* @param agent a `Player`, or no player
* @return the player hack entry
*/
def HackedBy_=(agent : Option[Player]) : Option[(Player, PlanetSideGUID, Vector3)] = {
hackedBy match {
case None =>
//set the hack state if there is no current hack state
if(agent.isDefined) {
hackedBy = Some(agent.get, agent.get.Position)
hackedBy = Some(agent.get, agent.get.GUID, agent.get.Position)
}
case Some(_) =>
//clear the hack state
if(agent.isEmpty) {
hackedBy = None
}
//override the hack state with a new hack state if the new user has different faction affiliation
else if(agent.get.Faction != hackedBy.get._1.Faction) {
hackedBy = Some(agent.get, agent.get.Position) //overwrite
hackedBy = Some(agent.get, agent.get.GUID, agent.get.Position)
}
}
HackedBy
}
def Definition : IFFLockDefinition = GlobalDefinitions.external_lock
def Definition : IFFLockDefinition = idef
}
object IFFLock {
def apply() : IFFLock = {
new IFFLock
/**
* Overloaded constructor.
* @param idef the `ObjectDefinition` that constructs this object and maintains some of its immutable fields
*/
def apply(idef : IFFLockDefinition) : IFFLock = {
new IFFLock(idef)
}
}

View file

@ -1,22 +1,22 @@
// Copyright (c) 2017 PSForever
package net.psforever.objects.serverobject.locks
import akka.actor.{Actor, Cancellable}
import akka.actor.Actor
import net.psforever.objects.serverobject.CommonMessages
/**
* An `Actor` that handles messages being dispatched to a specific `IFFLock`.
* @param lock the `IFFLock` object being governed
* @see `CommonMessages`
*/
class IFFLockControl(lock : IFFLock) extends Actor {
def receive : Receive = {
case CommonMessages.Hack(player) =>
lock.HackedBy = player
case CommonMessages.ClearHack() =>
lock.HackedBy = None
case _ => ;
case _ => ; //no default message
}
}
object IFFLockControl {
final val DefaultCloser : Cancellable = new Cancellable() {
override def cancel : Boolean = true
override def isCancelled : Boolean = true
}
}

View file

@ -3,6 +3,10 @@ package net.psforever.objects.serverobject.locks
import net.psforever.objects.definition.ObjectDefinition
class IFFLockDefinition extends ObjectDefinition(0) {
/**
* The definition for any `IFFLock`.
* Object Id 451 is a generic external lock.
*/
class IFFLockDefinition extends ObjectDefinition(451) {
Name = "iff_lock"
}

View file

@ -1,16 +0,0 @@
// Copyright (c) 2017 PSForever
package net.psforever.objects.serverobject.terminals
import net.psforever.objects.Player
import net.psforever.types.PlanetSideEmpire
//temporary location for these temporary messages
object TemporaryTerminalMessages {
//TODO send original packets along with these messages
final case class UseItem(player : Player)
final case class Convert(faction : PlanetSideEmpire.Value)
final case class Hack(player : Player)
final case class ClearHack()
final case class Damaged(dm : Int)
final case class Repaired(rep : Int)
}

View file

@ -5,48 +5,54 @@ import net.psforever.objects.Player
import net.psforever.objects.equipment.Equipment
import net.psforever.objects.inventory.InventoryItem
import net.psforever.objects.serverobject.PlanetSideServerObject
import net.psforever.packet.game.ItemTransactionMessage
import net.psforever.types.{ExoSuitType, PlanetSideEmpire, TransactionType, Vector3}
import net.psforever.packet.game.{ItemTransactionMessage, PlanetSideGUID}
import net.psforever.types.{ExoSuitType, TransactionType, Vector3}
/**
* na
* A structure-owned server object that is a "terminal" that can be accessed for amenities and services.
* @param tdef the `ObjectDefinition` that constructs this object and maintains some of its immutable fields
*/
class Terminal(tdef : TerminalDefinition) extends PlanetSideServerObject {
//the following fields and related methods are neither finalized no integrated; GOTO Request
private var faction : PlanetSideEmpire.Value = PlanetSideEmpire.NEUTRAL
private var hackedBy : Option[(Player, Vector3)] = None
private var health : Int = 100 //TODO not real health value
/**
* An entry that maintains a reference to the `Player`, and the player's GUID and location when the message was received.
*/
private var hackedBy : Option[(Player, PlanetSideGUID, Vector3)] = None
def Faction : PlanetSideEmpire.Value = faction
def HackedBy : Option[(Player, PlanetSideGUID, Vector3)] = hackedBy
def HackedBy : Option[(Player, Vector3)] = hackedBy
def HackedBy_=(agent : Player) : Option[(Player, PlanetSideGUID, Vector3)] = HackedBy_=(Some(agent))
def HackedBy_=(agent : Player) : Option[(Player, Vector3)] = HackedBy_=(Some(agent))
def HackedBy_=(agent : Option[Player]) : Option[(Player, Vector3)] = {
/**
* Set the hack state of this object by recording important information about the player that caused it.
* Set the hack state if there is no current hack state.
* Override the hack state with a new hack state if the new user has different faction affiliation.
* @param agent a `Player`, or no player
* @return the player hack entry
*/
def HackedBy_=(agent : Option[Player]) : Option[(Player, PlanetSideGUID, Vector3)] = {
hackedBy match {
case None =>
//set the hack state if there is no current hack state
if(agent.isDefined) {
hackedBy = Some(agent.get, agent.get.Position)
hackedBy = Some(agent.get, agent.get.GUID, agent.get.Position)
}
case Some(_) =>
//clear the hack state
if(agent.isEmpty) {
hackedBy = None
}
//override the hack state with a new hack state if the new user has different faction affiliation
else if(agent.get.Faction != hackedBy.get._1.Faction) {
hackedBy = Some(agent.get, agent.get.Position) //overwrite
hackedBy = Some(agent.get, agent.get.GUID, agent.get.Position)
}
}
HackedBy
}
def Health : Int = health
//the following fields and related methods are neither finalized nor integrated; GOTO Request
private var health : Int = 100 //TODO not real health value
def Convert(toFaction : PlanetSideEmpire.Value) : Unit = {
hackedBy = None
faction = toFaction
}
def Health : Int = health
def Damaged(dam : Int) : Unit = {
health = Math.max(0, Health - dam)
@ -146,14 +152,11 @@ object Terminal {
*/
final case class InfantryLoadout(exosuit : ExoSuitType.Value, subtype : Int = 0, holsters : List[InventoryItem], inventory : List[InventoryItem]) extends Exchange
/**
* Overloaded constructor.
* @param tdef the `ObjectDefinition` that constructs this object and maintains some of its immutable fields
*/
def apply(tdef : TerminalDefinition) : Terminal = {
new Terminal(tdef)
}
import net.psforever.packet.game.PlanetSideGUID
def apply(guid : PlanetSideGUID, tdef : TerminalDefinition) : Terminal = {
val obj = new Terminal(tdef)
obj.GUID = guid
obj
}
}

View file

@ -4,9 +4,7 @@ package net.psforever.objects.serverobject.terminals
import akka.actor.Actor
/**
* An `Actor` that handles messages being dispatched to a specific `Terminal`.<br>
* <br>
* For now, the only important message being managed is `Terminal.Request`.
* An `Actor` that handles messages being dispatched to a specific `Terminal`.
* @param term the `Terminal` object being governed
*/
class TerminalControl(term : Terminal) extends Actor {
@ -14,12 +12,6 @@ class TerminalControl(term : Terminal) extends Actor {
case Terminal.Request(player, msg) =>
sender ! Terminal.TerminalMessage(player, msg, term.Request(player, msg))
case TemporaryTerminalMessages.Hack(player) =>
term.HackedBy = player
case TemporaryTerminalMessages.ClearHack() =>
term.HackedBy = None
case _ =>
sender ! Terminal.NoDeal()
}

View file

@ -8,33 +8,38 @@ import net.psforever.packet.game.PlanetSideGUID
import scala.annotation.tailrec
import scala.concurrent.duration._
/**
* Close an opened door after a certain amount of time has passed.
* This `Actor` is intended to sit on top of the event system that handles broadcast messaging regarding doors opening.
* @see `LocalService`
*/
class DoorCloseActor() extends Actor {
import DoorCloseActor._
private var doorCloserTrigger : Cancellable = DefaultCloser
private var openDoors : List[DoorEntry] = Nil
/** The periodic `Executor` that checks for doors to be closed */
private var doorCloserTrigger : Cancellable = DoorCloseActor.DefaultCloser
/** A `List` of currently open doors */
private var openDoors : List[DoorCloseActor.DoorEntry] = Nil
//private[this] val log = org.log4s.getLogger
def receive : Receive = {
case DoorIsOpen(door, zone, time) =>
openDoors = openDoors :+ DoorEntry(door, zone, time)
if(openDoors.size == 1) {
case DoorCloseActor.DoorIsOpen(door, zone, time) =>
openDoors = openDoors :+ DoorCloseActor.DoorEntry(door, zone, time)
if(openDoors.size == 1) { //we were the only entry so the event must be started from scratch
import scala.concurrent.ExecutionContext.Implicits.global
doorCloserTrigger = context.system.scheduler.scheduleOnce(timeout, self, DoorCloseActor.TryCloseDoors())
doorCloserTrigger = context.system.scheduler.scheduleOnce(DoorCloseActor.timeout, self, DoorCloseActor.TryCloseDoors())
}
case TryCloseDoors() =>
case DoorCloseActor.TryCloseDoors() =>
doorCloserTrigger.cancel
val now : Long = System.nanoTime
//TODO we can just walk across the list of doors and extract only the first few entries
val (doorsToClose, doorsLeftOpen) = recursivePartitionDoors(openDoors.iterator, now)
val (doorsToClose, doorsLeftOpen) = PartitionEntries(openDoors, now)
openDoors = doorsLeftOpen
doorsToClose.foreach(entry => {
entry.door.Open = false //permissible
context.parent ! DoorCloseActor.CloseTheDoor(entry.door.GUID, entry.zone.Id)
entry.door.Open = false //permissible break from synchronization
context.parent ! DoorCloseActor.CloseTheDoor(entry.door.GUID, entry.zone.Id) //call up to the main event system
})
if(doorsLeftOpen.nonEmpty) {
val short_timeout : FiniteDuration = math.max(1, timeout_time - (now - doorsLeftOpen.head.opened_at_time)) nanoseconds
val short_timeout : FiniteDuration = math.max(1, DoorCloseActor.timeout_time - (now - doorsLeftOpen.head.time)) nanoseconds
import scala.concurrent.ExecutionContext.Implicits.global
doorCloserTrigger = context.system.scheduler.scheduleOnce(short_timeout, self, DoorCloseActor.TryCloseDoors())
}
@ -43,33 +48,53 @@ class DoorCloseActor() extends Actor {
}
/**
* na
* @param iter na
* @param now na
* @param list na
* Iterate over entries in a `List` until an entry that does not exceed the time limit is discovered.
* Separate the original `List` into two:
* a `List` of elements that have exceeded the time limit,
* and a `List` of elements that still satisfy the time limit.
* As newer entries to the `List` will always resolve later than old ones,
* and newer entries are always added to the end of the main `List`,
* processing in order is always correct.
* @param list the `List` of entries to divide
* @param now the time right now (in nanoseconds)
* @see `List.partition`
* @return a `Tuple` of two `Lists`:
* the entries for all `Door`s that are closing,
* and the entries for all doors that are staying open
* @return a `Tuple` of two `Lists`, whose qualifications are explained above
*/
@tailrec private def recursivePartitionDoors(iter : Iterator[DoorEntry], now : Long, list : List[DoorEntry] = Nil) : (List[DoorEntry], List[DoorEntry]) = {
private def PartitionEntries(list : List[DoorCloseActor.DoorEntry], now : Long) : (List[DoorCloseActor.DoorEntry], List[DoorCloseActor.DoorEntry]) = {
val n : Int = recursivePartitionEntries(list.iterator, now)
(list.take(n), list.drop(n)) //take and drop so to always return new lists
}
/**
* Mark the index where the `List` of elements can be divided into two:
* a `List` of elements that have exceeded the time limit,
* and a `List` of elements that still satisfy the time limit.
* @param iter the `Iterator` of entries to divide
* @param now the time right now (in nanoseconds)
* @param index a persistent record of the index where list division should occur;
* defaults to 0
* @return the index where division will occur
*/
@tailrec private def recursivePartitionEntries(iter : Iterator[DoorCloseActor.DoorEntry], now : Long, index : Int = 0) : Int = {
if(!iter.hasNext) {
(list, iter.toList)
index
}
else {
val entry = iter.next()
if(now - entry.opened_at_time >= timeout_time) {
recursivePartitionDoors(iter, now, list :+ entry)
if(now - entry.time >= DoorCloseActor.timeout_time) {
recursivePartitionEntries(iter, now, index + 1)
}
else {
(list, entry +: iter.toList)
index
}
}
}
}
object DoorCloseActor {
private final val timeout_time : Long = 5000000000L //nanoseconds
/** The wait before an open door closes; as a Long for calculation simplicity */
private final val timeout_time : Long = 5000000000L //nanoseconds (5s)
/** The wait before an open door closes; as a `FiniteDuration` for `Executor` simplicity */
private final val timeout : FiniteDuration = timeout_time nanoseconds
private final val DefaultCloser : Cancellable = new Cancellable() {
@ -77,11 +102,33 @@ object DoorCloseActor {
override def isCancelled : Boolean = true
}
final case class DoorIsOpen(door : Door, zone : Zone, opened_at_time : Long = System.nanoTime())
/**
* Message that carries information about a door that has been opened.
* @param door the door object
* @param zone the zone in which the door resides
* @param time when the door was opened
* @see `DoorEntry`
*/
final case class DoorIsOpen(door : Door, zone : Zone, time : Long = System.nanoTime())
/**
* Message that carries information about a door that needs to close.
* Prompting, as compared to `DoorIsOpen` which is reactionary.
* @param door_guid the door
* @param zone_id the zone in which the door resides
*/
final case class CloseTheDoor(door_guid : PlanetSideGUID, zone_id : String)
private final case class DoorEntry(door : Door, zone : Zone, opened_at_time : Long)
/**
* Internal message used to signal a test of the queued door information.
*/
private final case class TryCloseDoors()
/**
* Entry of door information.
* The `zone` is maintained separately to ensure that any message resulting in an attempt to close doors is targetted.
* @param door the door object
* @param zone the zone in which the door resides
* @param time when the door was opened
* @see `DoorIsOpen`
*/
private final case class DoorEntry(door : Door, zone : Zone, time : Long)
}

View file

@ -8,33 +8,39 @@ import net.psforever.packet.game.PlanetSideGUID
import scala.annotation.tailrec
import scala.concurrent.duration._
/**
* Restore original functionality to an object that has been hacked after a certain amount of time has passed.
* This `Actor` is intended to sit on top of the event system that handles broadcast messaging regarding hacking events.
* @see `LocalService`
*/
class HackClearActor() extends Actor {
import HackClearActor._
private var clearTrigger : Cancellable = DefaultClearer
private var hackedObjects : List[HackEntry] = Nil
/** The periodic `Executor` that checks for server objects to be unhacked */
private var clearTrigger : Cancellable = HackClearActor.DefaultClearer
/** A `List` of currently hacked server objects */
private var hackedObjects : List[HackClearActor.HackEntry] = Nil
//private[this] val log = org.log4s.getLogger
def receive : Receive = {
case ObjectIsHacked(door, zone, unk1, unk2, time) =>
hackedObjects = hackedObjects :+ HackEntry(door, zone, unk1, unk2, time)
if(hackedObjects.size == 1) {
case HackClearActor.ObjectIsHacked(target, zone, unk1, unk2, time) =>
hackedObjects = hackedObjects :+ HackClearActor.HackEntry(target, zone, unk1, unk2, time)
if(hackedObjects.size == 1) { //we were the only entry so the event must be started from scratch
import scala.concurrent.ExecutionContext.Implicits.global
clearTrigger = context.system.scheduler.scheduleOnce(timeout, self, HackClearActor.TryClearHacks())
clearTrigger = context.system.scheduler.scheduleOnce(HackClearActor.timeout, self, HackClearActor.TryClearHacks())
}
case TryClearHacks() =>
case HackClearActor.TryClearHacks() =>
clearTrigger.cancel
val now : Long = System.nanoTime
//TODO we can just walk across the list of doors and extract only the first few entries
val (unhackObjects, stillHackedObjects) = recursivePartitionHacks(hackedObjects.iterator, now)
val (unhackObjects, stillHackedObjects) = PartitionEntries(hackedObjects, now)
hackedObjects = stillHackedObjects
unhackObjects.foreach(entry => {
entry.target.Actor ! CommonMessages.ClearHack()
context.parent ! HackClearActor.ClearTheHack(entry.target.GUID, entry.zone.Id, entry.unk1, entry.unk2)
context.parent ! HackClearActor.ClearTheHack(entry.target.GUID, entry.zone.Id, entry.unk1, entry.unk2) //call up to the main event system
})
if(stillHackedObjects.nonEmpty) {
val short_timeout : FiniteDuration = math.max(1, timeout_time - (now - stillHackedObjects.head.hacked_at_time)) nanoseconds
val short_timeout : FiniteDuration = math.max(1, HackClearActor.timeout_time - (now - stillHackedObjects.head.time)) nanoseconds
import scala.concurrent.ExecutionContext.Implicits.global
clearTrigger = context.system.scheduler.scheduleOnce(short_timeout, self, HackClearActor.TryClearHacks())
}
@ -43,33 +49,53 @@ class HackClearActor() extends Actor {
}
/**
* na
* @param iter na
* @param now na
* @param list na
* Iterate over entries in a `List` until an entry that does not exceed the time limit is discovered.
* Separate the original `List` into two:
* a `List` of elements that have exceeded the time limit,
* and a `List` of elements that still satisfy the time limit.
* As newer entries to the `List` will always resolve later than old ones,
* and newer entries are always added to the end of the main `List`,
* processing in order is always correct.
* @param list the `List` of entries to divide
* @param now the time right now (in nanoseconds)
* @see `List.partition`
* @return a `Tuple` of two `Lists`:
* the entries for all objects that are no longer hacked,
* and the entries for all objects that are still hacked
* @return a `Tuple` of two `Lists`, whose qualifications are explained above
*/
@tailrec private def recursivePartitionHacks(iter : Iterator[HackEntry], now : Long, list : List[HackEntry] = Nil) : (List[HackEntry], List[HackEntry]) = {
private def PartitionEntries(list : List[HackClearActor.HackEntry], now : Long) : (List[HackClearActor.HackEntry], List[HackClearActor.HackEntry]) = {
val n : Int = recursivePartitionEntries(list.iterator, now)
(list.take(n), list.drop(n)) //take and drop so to always return new lists
}
/**
* Mark the index where the `List` of elements can be divided into two:
* a `List` of elements that have exceeded the time limit,
* and a `List` of elements that still satisfy the time limit.
* @param iter the `Iterator` of entries to divide
* @param now the time right now (in nanoseconds)
* @param index a persistent record of the index where list division should occur;
* defaults to 0
* @return the index where division will occur
*/
@tailrec private def recursivePartitionEntries(iter : Iterator[HackClearActor.HackEntry], now : Long, index : Int = 0) : Int = {
if(!iter.hasNext) {
(list, iter.toList)
index
}
else {
val entry = iter.next()
if(now - entry.hacked_at_time >= timeout_time) {
recursivePartitionHacks(iter, now, list :+ entry)
if(now - entry.time >= HackClearActor.timeout_time) {
recursivePartitionEntries(iter, now, index + 1)
}
else {
(list, entry +: iter.toList)
index
}
}
}
}
object HackClearActor {
private final val timeout_time : Long = 60000000000L //nanoseconds (1 minute)
/** The wait before a server object is to unhack; as a Long for calculation simplicity */
private final val timeout_time : Long = 60000000000L //nanoseconds (60s)
/** The wait before a server object is to unhack; as a `FiniteDuration` for `Executor` simplicity */
private final val timeout : FiniteDuration = timeout_time nanoseconds
private final val DefaultClearer : Cancellable = new Cancellable() {
@ -77,11 +103,33 @@ object HackClearActor {
override def isCancelled : Boolean = true
}
final case class ObjectIsHacked(target : PlanetSideServerObject, zone : Zone, unk1 : Long, unk2 : Long, hacked_at_time : Long = System.nanoTime())
/**
* Message that carries information about a server object that has been hacked.
* @param target the server object
* @param zone the zone in which the object resides
* @param time when the object was hacked
* @see `HackEntry`
*/
final case class ObjectIsHacked(target : PlanetSideServerObject, zone : Zone, unk1 : Long, unk2 : Long, time : Long = System.nanoTime())
/**
* Message that carries information about a server object that needs its functionality restored.
* Prompting, as compared to `ObjectIsHacked` which is reactionary.
* @param door_guid the server object
* @param zone_id the zone in which the object resides
*/
final case class ClearTheHack(door_guid : PlanetSideGUID, zone_id : String, unk1 : Long, unk2 : Long)
private final case class HackEntry(target : PlanetSideServerObject, zone : Zone, unk1 : Long, unk2 : Long, hacked_at_time : Long)
/**
* Internal message used to signal a test of the queued door information.
*/
private final case class TryClearHacks()
/**
* Entry of hacked server object information.
* The `zone` is maintained separately to ensure that any message resulting in an attempt to close doors is targetted.
* @param target the server object
* @param zone the zone in which the object resides
* @param time when the object was hacked
* @see `ObjectIsHacked`
*/
private final case class HackEntry(target : PlanetSideServerObject, zone : Zone, unk1 : Long, unk2 : Long, time : Long)
}

View file

@ -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.<br>
* <br>
* `Start` initially displays the hacking progress bar.<br>
* `Ongoing` is a neutral state that keeps the progress bar displayed while its value updates. (unconfirmed?)<br>
* `Finished` disposes of the hacking progress bar. It does not, by itself, mean the hack was successful.<br>
* `Hacked` modifies the target of the hack.<br>
* `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.<br>
* <br>
* 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]
}

View file

@ -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
}

View file

@ -229,8 +229,8 @@ object PsLogin {
LocalObject(DoorObjectBuilder(door, 332))
LocalObject(DoorObjectBuilder(door, 372))
LocalObject(DoorObjectBuilder(door, 373))
LocalObject(IFFLockObjectBuilder(external_lock, 556))
LocalObject(IFFLockObjectBuilder(external_lock, 558))
LocalObject(IFFLockObjectBuilder(lock_external, 556))
LocalObject(IFFLockObjectBuilder(lock_external, 558))
LocalObject(TerminalObjectBuilder(cert_terminal, 186))
LocalObject(TerminalObjectBuilder(cert_terminal, 187))
LocalObject(TerminalObjectBuilder(cert_terminal, 188))

View file

@ -230,11 +230,11 @@ class WorldSessionActor extends Actor with MDCContextAware {
case LocalServiceResponse.HackClear(target_guid, unk1, unk2) =>
log.info(s"Clear hack of $target_guid")
sendResponse(PacketCoding.CreateGamePacket(0, HackMessage(0, target_guid.guid, guid.guid, 0, unk1, 7, unk2)))
sendResponse(PacketCoding.CreateGamePacket(0, HackMessage(0, target_guid, guid, 0, unk1, HackState.HackCleared, unk2)))
case LocalServiceResponse.HackObject(target_guid, unk1, unk2) =>
if(player.GUID != guid) {
sendResponse(PacketCoding.CreateGamePacket(0, HackMessage(0, target_guid.guid, guid.guid, 100, unk1, 6, unk2)))
sendResponse(PacketCoding.CreateGamePacket(0, HackMessage(0, target_guid, guid, 100, unk1, HackState.Hacked, unk2)))
}
}
@ -586,23 +586,23 @@ class WorldSessionActor extends Actor with MDCContextAware {
progressBarUpdate.cancel
if(progressBarValue.isDefined) {
val progressBarVal : Float = progressBarValue.get + delta
val vis = if(progressBarVal == 0L) {
1
val vis = if(progressBarVal == 0L) { //hack state for progress bar visibility
HackState.Start
}
else if(progressBarVal >= 100L) {
4
else if(progressBarVal > 100L) {
HackState.Finished
}
else {
3
HackState.Ongoing
}
sendResponse(PacketCoding.CreateGamePacket(0, HackMessage(1, target.GUID.guid, player.GUID.guid, progressBarVal.toInt, 0L, vis, 8L)))
if(progressBarVal > 100) {
sendResponse(PacketCoding.CreateGamePacket(0, HackMessage(1, target.GUID, player.GUID, progressBarVal.toInt, 0L, vis, 8L)))
if(progressBarVal > 100) { //done
progressBarValue = None
log.info(s"Hacked a $target")
sendResponse(PacketCoding.CreateGamePacket(0, HackMessage(0, target.GUID.guid, player.GUID.guid, 100, 1114636288L, 6, 8L)))
sendResponse(PacketCoding.CreateGamePacket(0, HackMessage(0, target.GUID, player.GUID, 100, 1114636288L, HackState.Hacked, 8L)))
completeAction()
}
else {
else { //continue next tick
tickAction.getOrElse(() => Unit)()
progressBarValue = Some(progressBarVal)
import scala.concurrent.duration._
@ -1028,10 +1028,10 @@ class WorldSessionActor extends Actor with MDCContextAware {
// TODO: Not all incoming UseItemMessage's respond with another UseItemMessage (i.e. doors only send out GenericObjectStateMsg)
continent.GUID(object_guid) match {
case Some(door : Door) =>
continent.Map.DoorToLock.get(object_guid.guid) match { //check for IFFLock
continent.Map.DoorToLock.get(object_guid.guid) match { //check for IFF Lock
case Some(lock_guid) =>
val lock_hacked = continent.GUID(lock_guid).get.asInstanceOf[IFFLock].HackedBy match {
case Some((tplayer, _)) =>
case Some((tplayer, _, _)) =>
tplayer.Faction == player.Faction
case None =>
false
@ -1042,7 +1042,7 @@ class WorldSessionActor extends Actor with MDCContextAware {
door.Actor ! Door.Use(player, msg)
}
case None =>
if(lock_hacked) { //is locks hacked?
if(lock_hacked) { //is lock hacked? this may be a weird case
door.Actor ! Door.Use(player, msg)
}
}
@ -1054,13 +1054,13 @@ class WorldSessionActor extends Actor with MDCContextAware {
player.Slot(player.DrawnSlot).Equipment match {
case Some(tool : SimpleItem) =>
if(tool.Definition == GlobalDefinitions.remote_electronics_kit) {
//TODO get player hack level (for now, presume 15s in intervals of 4/s)
progressBarValue = Some(-2.66f)
self ! WorldSessionActor.ItemHacking(player, panel, tool.GUID, 2.66f, HackTemporary(panel))
self ! WorldSessionActor.ItemHacking(player, panel, tool.GUID, 2.66f, FinishHackingDoor(panel, 1114636288L))
log.info("Hacking a door~")
}
case _ => ;
}
log.info("Hacking a door~")
//TODO get player hack level (for now, presume 15s in internals of 4/s)
case Some(obj : PlanetSideGameObject) =>
if(itemType != 121) {
@ -1071,10 +1071,7 @@ class WorldSessionActor extends Actor with MDCContextAware {
sendResponse(PacketCoding.CreateGamePacket(0, PlanetsideAttributeMessage(avatar_guid, 0, 100))) // avatar with 100 hp
sendResponse(PacketCoding.CreateGamePacket(0, ObjectDeleteMessage(PlanetSideGUID(unk1), 2)))
}
// if(unk1 == 0 && !unk3 && unk7 == 25) {
// // TODO: This should only actually be sent to doors upon opening; may break non-door items upon use
// sendResponse(PacketCoding.CreateGamePacket(0, GenericObjectStateMsg(object_guid, 16)))
// }
case None => ;
}
@ -1625,9 +1622,18 @@ class WorldSessionActor extends Actor with MDCContextAware {
}
}
private def HackTemporary(target : PlanetSideServerObject)() : Unit = {
/**
* The process of hacking the `Door` `IFFLock` is completed.
* Pass the message onto the lock and onto the local events system.
* @param target the `IFFLock` belonging to the door that is being hacked
* @param unk na;
* used by `HackingMessage` as `unk5`
* @see `HackMessage`
*/
//TODO add params here depending on which params in HackMessage are important
private def FinishHackingDoor(target : IFFLock, unk : Long)() : Unit = {
target.Actor ! CommonMessages.Hack(player)
localService ! LocalServiceMessage(player.Continent, LocalAction.HackTemporarily(player.GUID, continent, target, 1114636288L))
localService ! LocalServiceMessage(continent.Id, LocalAction.HackTemporarily(player.GUID, continent, target, unk))
}
def failWithError(error : String) = {
@ -1660,6 +1666,18 @@ object WorldSessionActor {
private final case class PlayerFailedToLoad(tplayer : Player)
private final case class ListAccountCharacters()
private final case class SetCurrentAvatar(tplayer : Player)
/**
* A message that indicates the user is using a remote electronics kit to hack some server object.
* Each time this message is sent for a given hack attempt counts as a single "tick" of progress.
* The process of "making progress" with a hack involves sending this message repeatedly until the progress is 100 or more.
* @param tplayer the player
* @param target the object being hacked
* @param tool_guid the REK
* @param delta how much the progress bar value changes each tick
* @param completeAction a custom action performed once the hack is completed
* @param tickAction an optional action is is performed for each tick of progress
*/
private final case class ItemHacking(tplayer : Player,
target : PlanetSideServerObject,
tool_guid : PlanetSideGUID,