Timed capture consoles & towers / hacking improvements (#228)

* Add capture terminal definitions

* Logging / documentation

* Functionality for timed base hacks

* Disable IFF locks while base is hacked

* Hacking speed based on player's hacking certification level (and hack effect duration data - currently not functional)

* Hack effect duration functionality

* Sync hack states with clients joining the zone

* Whitespace / comments

* Allow IFF locks to be resecured by the owning faction if it has been hacked

* Fix bases with no NTU silo failing to capture

* Reset CC properly on hack that expires with no NTU in silo

* Capture towers instantly and improve handling of hacked objects queue

* Fix handling of neutral IFF Locks and remove unnecessary casting

* Move HackCaptureActor to correct location

* Re-enable Anguta door locks for air pad now hacking/resecuring IFF locks work
Add capture terminals (timed CC only for now) for a few bases & a tower
Add a few missed locks/doors
Add a resource silo to Girru

* Fix merge issues & missing documentation
This commit is contained in:
Mazo 2018-08-10 23:21:15 +01:00 committed by Fate-JH
parent 8b5073dcbc
commit abdebf09ba
23 changed files with 649 additions and 168 deletions

View file

@ -864,6 +864,10 @@ object GlobalDefinitions {
val resource_silo = new ResourceSiloDefinition
val capture_terminal = new CaptureTerminalDefinition(158) // Base CC
val secondary_capture = new CaptureTerminalDefinition(751) // Tower CC
val manned_turret = new MannedTurretDefinition(480) {
Name = "manned_turret"
MaxHealth = 3600

View file

@ -7,13 +7,8 @@ import net.psforever.types.Vector3
trait Hackable {
/** 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
private var hackSound : TriggeredSound.Value = TriggeredSound.HackDoor
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.
@ -41,9 +36,27 @@ trait Hackable {
HackedBy
}
/** The sound made when the object is hacked */
private var hackSound : TriggeredSound.Value = TriggeredSound.HackDoor
def HackSound : TriggeredSound.Value = hackSound
def HackSound_=(sound : TriggeredSound.Value) : TriggeredSound.Value = {
hackSound = sound
hackSound
}
/** The duration in seconds a hack lasts for, based on the hacker's certification level */
private var hackEffectDuration = Array(0, 0, 0 , 0)
def HackEffectDuration: Array[Int] = hackEffectDuration
def HackEffectDuration_=(arr: Array[Int]) : Array[Int] = {
hackEffectDuration = arr
arr
}
/** How long it takes to hack the object in seconds, based on the hacker's certification level */
private var hackDuration = Array(0, 0, 0, 0)
def HackDuration: Array[Int] = hackDuration
def HackDuration_=(arr: Array[Int]) : Array[Int] = {
hackDuration = arr
arr
}
}

View file

@ -17,6 +17,8 @@ import net.psforever.packet.game.TriggeredSound
class IFFLock(private val idef : IFFLockDefinition) extends Amenity with Hackable {
def Definition : IFFLockDefinition = idef
HackSound = TriggeredSound.HackDoor
HackEffectDuration = Array(60, 180, 300, 360)
HackDuration = Array(5, 3, 1, 1)
}
object IFFLock {

View file

@ -10,6 +10,8 @@ import net.psforever.packet.game.TriggeredSound
class Locker extends Amenity with Hackable {
def Definition : LockerDefinition = GlobalDefinitions.mb_locker
HackSound = TriggeredSound.HackTerminal
HackEffectDuration = Array(0, 30, 60, 90)
HackDuration = Array(0, 10, 5, 3)
}
object Locker {

View file

@ -60,7 +60,7 @@ class ResourceSiloControl(resourceSilo : ResourceSilo) extends Actor with Factio
if(resourceSilo.CapacitorDisplay != ntuBarLevel) {
log.trace(s"Silo ${resourceSilo.GUID} NTU bar level has changed from ${resourceSilo.CapacitorDisplay} to ${ntuBarLevel}")
resourceSilo.CapacitorDisplay = ntuBarLevel
resourceSilo.Owner.Actor ! Building.SendMapUpdateToAllClients()
resourceSilo.Owner.Actor ! Building.SendMapUpdate(all_clients = true)
avatarService ! AvatarServiceMessage(resourceSilo.Owner.asInstanceOf[Building].Zone.Id, AvatarAction.PlanetsideAttribute(resourceSilo.GUID, 45, resourceSilo.CapacitorDisplay))
}

View file

@ -68,7 +68,6 @@ object Building {
val obj = new Building(id, zone, buildingType)
obj.Position = location
obj.Actor = context.actorOf(Props(classOf[BuildingControl], obj), s"$id-$buildingType-building")
obj.Actor ! "startup"
obj
}
@ -76,9 +75,8 @@ object Building {
import akka.actor.Props
val obj = new Building(id, zone, buildingType)
obj.Actor = context.actorOf(Props(classOf[BuildingControl], obj), s"$id-$buildingType-building")
obj.Actor ! "startup"
obj
}
final case class SendMapUpdateToAllClients()
final case class SendMapUpdate(all_clients: Boolean)
}

View file

@ -1,74 +1,114 @@
// Copyright (c) 2017 PSForever
package net.psforever.objects.serverobject.structures
import akka.actor.{Actor, ActorRef}
import java.util.concurrent.TimeUnit
import akka.actor.{Actor, ActorRef, Props}
import net.psforever.objects.GlobalDefinitions
import net.psforever.objects.serverobject.affinity.{FactionAffinity, FactionAffinityBehavior}
import net.psforever.objects.serverobject.hackable.Hackable
import net.psforever.objects.serverobject.resourcesilo.ResourceSilo
import net.psforever.objects.serverobject.terminals.CaptureTerminal
import net.psforever.packet.game.{BuildingInfoUpdateMessage, PlanetSideGeneratorState}
import net.psforever.types.PlanetSideEmpire
import services.ServiceManager
import services.ServiceManager.Lookup
import services.galaxy.{GalaxyAction, GalaxyServiceMessage}
import services.galaxy.{GalaxyAction, GalaxyResponse, GalaxyServiceMessage, GalaxyServiceResponse}
import services.local.support.HackCaptureActor
import scala.util.Success
import scala.concurrent.duration._
import akka.pattern.ask
import scala.concurrent.{Await, Future}
class BuildingControl(building : Building) extends Actor with FactionAffinityBehavior.Check {
def FactionObject : FactionAffinity = building
var galaxyService : ActorRef = Actor.noSender
var localService : ActorRef = Actor.noSender
private[this] val log = org.log4s.getLogger
def receive : Receive = {
case "startup" =>
ServiceManager.serviceManager ! Lookup("galaxy") //ask for a resolver to deal with the GUID system
case ServiceManager.LookupResult("galaxy", endpoint) =>
galaxyService = endpoint
log.trace("BuildingControl: Building " + building.ModelId + " Got galaxy service " + endpoint)
// todo: This is just a temporary solution to drain NTU over time. When base object destruction is properly implemented NTU should be deducted when base objects repair themselves
context.become(Processing)
case _ => log.warn("Message received before startup called");
override def preStart = {
log.info(s"Starting BuildingControl for ${building.GUID} / ${building.ModelId}")
ServiceManager.serviceManager ! Lookup("galaxy")
ServiceManager.serviceManager ! Lookup("local")
}
def Processing : Receive = checkBehavior.orElse {
def receive : Receive = checkBehavior.orElse {
case ServiceManager.LookupResult("galaxy", endpoint) =>
galaxyService = endpoint
log.info("BuildingControl: Building " + building.ModelId + " Got galaxy service " + endpoint)
case ServiceManager.LookupResult("local", endpoint) =>
localService = endpoint
log.info("BuildingControl: Building " + building.ModelId + " Got local service " + endpoint)
case FactionAffinity.ConvertFactionAffinity(faction) =>
val originalAffinity = building.Faction
if(originalAffinity != (building.Faction = faction)) {
building.Amenities.foreach(_.Actor forward FactionAffinity.ConfirmFactionAffinity())
}
sender ! FactionAffinity.AssertFactionAffinity(building, faction)
case Building.SendMapUpdateToAllClients() =>
log.info(s"Sending BuildingInfoUpdateMessage update to all clients. Zone: ${building.Zone.Number} - Building: ${building.ModelId}")
case Building.SendMapUpdate(all_clients: Boolean) =>
log.info(s"Sending BuildingInfoUpdateMessage update. Zone: ${building.Zone.Number} - Building: ${building.ModelId}")
var ntuLevel = 0
var is_hacked = false
var hack_time_remaining_ms = 0L;
var hacked_by_faction = PlanetSideEmpire.NEUTRAL
// Get Ntu level from silo if it exists
building.Amenities.filter(x => (x.Definition == GlobalDefinitions.resource_silo)).headOption.asInstanceOf[Option[ResourceSilo]] match {
case Some(obj: ResourceSilo) =>
ntuLevel = obj.CapacitorDisplay.toInt
case _ => ;
}
galaxyService ! GalaxyServiceMessage(GalaxyAction.MapUpdate(
BuildingInfoUpdateMessage(
// Get hack status & time from control console if it exists
building.Amenities.filter(x => x.Definition == GlobalDefinitions.capture_terminal).headOption.asInstanceOf[Option[CaptureTerminal with Hackable]] match {
case Some(obj: CaptureTerminal with Hackable) =>
if(!obj.HackedBy.isEmpty) {
is_hacked = true
hacked_by_faction = obj.HackedBy.get._1.Faction
}
import scala.concurrent.ExecutionContext.Implicits.global
val future = ask(localService, HackCaptureActor.GetHackTimeRemainingNanos(obj.GUID))(1 second)
//todo: this is blocking. Not so bad when we're only retrieving one piece of data but as more functionality is added we'll need to change this to be async but wait for all replies before sending BIUM to clients
val time = Await.result(future, 1 second).asInstanceOf[Long]
hack_time_remaining_ms = TimeUnit.MILLISECONDS.convert(time, TimeUnit.NANOSECONDS)
case _ => ;
}
val msg = BuildingInfoUpdateMessage(
continent_id = building.Zone.Number, //Zone
building_id = building.Id, //Facility
ntu_level = ntuLevel,
is_hacked = false, //Hacked
PlanetSideEmpire.NEUTRAL, //Base hacked by
hack_time_remaining = 0, //Time remaining for hack (ms)
empire_own = building.Faction, //Base owned by
is_hacked,
hacked_by_faction,
hack_time_remaining_ms,
empire_own = building.Faction,
unk1 = 0, //!! Field != 0 will cause malformed packet. See class def.
unk1x = None,
generator_state = PlanetSideGeneratorState.Normal, //Generator state
spawn_tubes_normal = true, //Respawn tubes operating state
force_dome_active = false, //Force dome state
lattice_benefit = 0, //Lattice benefits
generator_state = PlanetSideGeneratorState.Normal,
spawn_tubes_normal = true,
force_dome_active = false,
lattice_benefit = 0,
cavern_benefit = 0, //!! Field > 0 will cause malformed packet. See class def.
unk4 = Nil,
unk5 = 0,
unk6 = false,
unk7 = 8, //!! Field != 8 will cause malformed packet. See class def.
unk7x = None,
boost_spawn_pain = false, //Boosted spawn room pain field
boost_generator_pain = false //Boosted generator room pain field
)))
boost_spawn_pain = false,
boost_generator_pain = false
)
if(all_clients) {
galaxyService ! GalaxyServiceMessage(GalaxyAction.MapUpdate(msg))
} else {
// Fake a GalaxyServiceResponse response back to just the sender
sender ! GalaxyServiceResponse("", GalaxyResponse.MapUpdate(msg))
}
case default =>
log.warn(s"BuildingControl: Unknown message ${default} received from ${sender().path}")
}

View file

@ -0,0 +1,31 @@
package net.psforever.objects.serverobject.terminals
import net.psforever.objects.serverobject.hackable.Hackable
import net.psforever.objects.serverobject.structures.Amenity
import net.psforever.packet.game.TriggeredSound
class CaptureTerminal(private val idef : CaptureTerminalDefinition) extends Amenity with Hackable {
def Definition : CaptureTerminalDefinition = idef
HackDuration = Array(60, 40, 20, 15)
HackSound = TriggeredSound.HackTerminal
}
object CaptureTerminal {
/**
* Overloaded constructor.
* @param tdef the `ObjectDefinition` that constructs this object and maintains some of its immutable fields
*/
def apply(tdef : CaptureTerminalDefinition) : CaptureTerminal = {
new CaptureTerminal(tdef)
}
import akka.actor.ActorContext
def Constructor(tdef: CaptureTerminalDefinition)(id : Int, context : ActorContext) : CaptureTerminal = {
import akka.actor.Props
val obj = CaptureTerminal(tdef)
obj.Actor = context.actorOf(Props(classOf[CaptureTerminalControl], obj), s"${tdef.Name}_$id")
obj
}
}

View file

@ -0,0 +1,21 @@
// Copyright (c) 2017 PSForever
package net.psforever.objects.serverobject.terminals
import akka.actor.Actor
import net.psforever.objects.serverobject.CommonMessages
import net.psforever.objects.serverobject.affinity.{FactionAffinity, FactionAffinityBehavior}
class CaptureTerminalControl(terminal : CaptureTerminal) extends Actor with FactionAffinityBehavior.Check {
def FactionObject : FactionAffinity = terminal
def receive : Receive = checkBehavior.orElse {
case CommonMessages.Hack(player) =>
terminal.HackedBy = player
sender ! true
case CommonMessages.ClearHack() =>
terminal.HackedBy = None
case _ => ; //no default message
}
}

View file

@ -0,0 +1,13 @@
package net.psforever.objects.serverobject.terminals
import net.psforever.objects.definition.ObjectDefinition
class CaptureTerminalDefinition(objectId : Int) extends ObjectDefinition(objectId) {
Name = if(objectId == 158) {
"capture_terminal"
} else if (objectId == 751) {
"secondary_capture"
} else {
throw new IllegalArgumentException("Not a valid capture terminal object id")
}
}

View file

@ -14,6 +14,8 @@ import net.psforever.types.TransactionType
*/
class Terminal(tdef : TerminalDefinition) extends Amenity with Hackable {
HackSound = TriggeredSound.HackTerminal
HackEffectDuration = Array(0, 30, 60, 90)
HackDuration = Array(0, 10, 5, 3)
//the following fields and related methods are neither finalized nor integrated; GOTO Request
private var health : Int = 100 //TODO not real health value

View file

@ -61,7 +61,8 @@ class InterstellarCluster(zones : List[Zone]) extends Actor {
sender ! Zone.Lattice.NoValidSpawnPoint(zone_number, None)
}
case _ => ;
case _ =>
log.warn(s"InterstellarCluster received unknown message");
}
/**

View file

@ -56,6 +56,7 @@ object HackState extends Enumeration {
* 2 when performing (phalanx) upgrades;
* 3 for building objects during login phase;
* hack type?
* possibly player hacking level 0-3?
* @param target_guid the target of the hack
* @param player_guid the player
* @param progress the amount of progress visible;
@ -65,6 +66,7 @@ object HackState extends Enumeration {
* doesn't seem to be `char_id`?
* @param hack_state hack state
* @param unk7 na;
* 5 - boost pain field at matrixing terminal?
* usually, 8?
*/
final case class HackMessage(unk1 : Int,

View file

@ -51,6 +51,15 @@ import scodec.codecs._
* `17 - BEP. Value seems to be the same as BattleExperienceMessage`<br>
* `18 - CEP.`<br>
* `19 - Anchors. Value is 0 to disengage, 1 to engage.`<br>
* `20 - Control console hacking. "The FactionName has hacked into BaseName` - also sets timer on CC and yellow base warning lights on<br>
* <ul>
* <li>65535 segments per faction in deciseconds (seconds * 10)</li>
* <li>0-65535 = Neutral 0 seconds to 1h 49m 14s</li>
* <li>65536 - 131071 - TR</li>
* <li>131072 - 196607 - NC</li>
* <li>196608 - 262143 - VS</li>
* <li>17039360 - CC Resecured</li>
* </ul>
* `24 - Learn certifications with value :`<br>
* 01 : Medium Assault<br>
* 02 : Heavy Assault<br>
@ -127,6 +136,8 @@ import scodec.codecs._
* `116 - Apply colour to REK beam and REK icon above players (0 = yellow, 1 = red, 2 = purple, 3 = blue)`<br>
* Client to Server : <br>
* `106 - Custom Head`<br>
* `224 - Player/vehicle joins black ops`<br>
* `228 - Player/vehicle leaves black ops`<br>
* <br>
* `Vehicles:`<br>
* `10 - Driver seat permissions (0 = Locked, 1 = Group, 3 = Empire)`<br>

View file

@ -10,22 +10,26 @@ import scodec.codecs._
* (Where the child object was before it was moved is not specified or important.)
* @see `Definition.ObjectId`<br>
* `TurretUpgrade`
* @param avatar_guid the player
* @param item_used_guid the item used;
* e.g., a REK to hack, or a medkit to heal
* @param object_guid the object affected;
* e.g., door when opened, terminal when accessed, avatar when medkit used
* @param unk2 na;
* when upgrading phalanx turrets, 1 for `AVCombo` and 2 for `FlakCombo`
* @param unk3 using tools, e.g., a REK or nano-dispenser
* @param unk4 na
* @param unk5 na
* @param unk6 na
* @param unk7 na;
* 25 when door 223 when terminal
* @param unk8 na;
* 0 when door 1 when use rek (252 then equipment term)
* @param object_id he object id for `object_guid`'s object
* @param avatar_guid the player
* @param item_used_guid the item used;
* e.g. a REK to hack or a medkit to heal.
* @param object_guid the object affected;
* e.g., door when opened, terminal when accessed, avatar when medkit used
* @param unk2 na;
* when upgrading phalanx turrets, 1 for `AVCombo` and 2 for `FlakCombo`
* @param unk3 using tools, e.g., a REK or nano-dispenser
* @param unk4 seems to be related to T-REK viruses.
* 0 - unlock all doors
* 1 - disable linked benefits
* 2 - double ntu drain
* 3 - disable enemy radar
* 4 - access equipment terminals
* @param unk6 na
* @param unk7 na;
* 25 when door 223 when terminal
* @param unk8 na;
* 0 when door 1 when use rek (252 then equipment term)
* @param object_id the object id `object_guid`'s object
*/
final case class UseItemMessage(avatar_guid : PlanetSideGUID,
item_used_guid : PlanetSideGUID,

View file

@ -3,9 +3,11 @@ package services.local
import net.psforever.objects.serverobject.PlanetSideServerObject
import net.psforever.objects.serverobject.doors.Door
import net.psforever.objects.serverobject.hackable.Hackable
import net.psforever.objects.serverobject.terminals.CaptureTerminal
import net.psforever.objects.zones.Zone
import net.psforever.packet.game.{PlanetSideGUID, TriggeredSound}
import net.psforever.types.Vector3
import net.psforever.types.{PlanetSideEmpire, Vector3}
object LocalAction {
trait Action
@ -13,7 +15,10 @@ object LocalAction {
final case class DoorOpens(player_guid : PlanetSideGUID, continent : Zone, door : Door) extends Action
final case class DoorCloses(player_guid : PlanetSideGUID, door_guid : PlanetSideGUID) extends Action
final case class HackClear(player_guid : PlanetSideGUID, target : PlanetSideServerObject, unk1 : Long, unk2 : Long = 8L) extends Action
final case class HackTemporarily(player_guid : PlanetSideGUID, continent : Zone, target : PlanetSideServerObject, unk1 : Long, unk2 : Long = 8L) extends Action
final case class HackTemporarily(player_guid : PlanetSideGUID, continent : Zone, target : PlanetSideServerObject, unk1 : Long, duration: Int, unk2 : Long = 8L) extends Action
final case class ClearTemporaryHack(player_guid: PlanetSideGUID, target: PlanetSideServerObject with Hackable) extends Action
final case class HackCaptureTerminal(player_guid : PlanetSideGUID, continent : Zone, target : CaptureTerminal, unk1 : Long, unk2 : Long = 8L, isResecured : Boolean) extends Action
final case class ProximityTerminalEffect(player_guid : PlanetSideGUID, object_guid : PlanetSideGUID, effectState : Boolean) extends Action
final case class TriggerSound(player_guid : PlanetSideGUID, sound : TriggeredSound.Value, pos : Vector3, unk : Int, volume : Float) extends Action
final case class SetEmpire(object_guid: PlanetSideGUID, empire: PlanetSideEmpire.Value) extends Action
}

View file

@ -2,7 +2,7 @@
package services.local
import net.psforever.packet.game.{PlanetSideGUID, TriggeredSound}
import net.psforever.types.Vector3
import net.psforever.types.{PlanetSideEmpire, Vector3}
object LocalResponse {
trait Response
@ -11,6 +11,8 @@ object LocalResponse {
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 HackCaptureTerminal(target_guid : PlanetSideGUID, unk1 : Long, unk2 : Long, isResecured: Boolean) extends Response
final case class ProximityTerminalEffect(object_guid : PlanetSideGUID, effectState : Boolean) extends Response
final case class TriggerSound(sound : TriggeredSound.Value, pos : Vector3, unk : Int, volume : Float) extends Response
final case class SetEmpire(object_guid: PlanetSideGUID, empire: PlanetSideEmpire.Value) extends Response
}

View file

@ -1,22 +1,44 @@
// Copyright (c) 2017 PSForever
package services.local
import akka.actor.{Actor, Props}
import services.{GenericEventBus, Service}
import akka.actor.{Actor, ActorRef, Props}
import net.psforever.objects.serverobject.CommonMessages
import net.psforever.objects.zones.{InterstellarCluster, Zone}
import net.psforever.objects.zones.InterstellarCluster.GetWorld
import services.local.support.{DoorCloseActor, HackCaptureActor, HackClearActor}
import services.{GenericEventBus, Service, ServiceManager}
import services.local.support.{DoorCloseActor, HackClearActor}
import scala.util.Success
import scala.concurrent.duration._
import akka.pattern.ask
import net.psforever.objects.GlobalDefinitions
import net.psforever.objects.serverobject.hackable.Hackable
import net.psforever.objects.serverobject.resourcesilo.ResourceSilo
import net.psforever.objects.serverobject.structures.{Amenity, Building}
import net.psforever.objects.serverobject.terminals.CaptureTerminal
import net.psforever.packet.game.PlanetSideGUID
import services.ServiceManager.Lookup
class LocalService extends Actor {
private val doorCloser = context.actorOf(Props[DoorCloseActor], "local-door-closer")
private val hackClearer = context.actorOf(Props[HackClearActor], "local-hack-clearer")
private val hackCapturer = context.actorOf(Props[HackCaptureActor], "local-hack-capturer")
private [this] val log = org.log4s.getLogger
var cluster : ActorRef = Actor.noSender
override def preStart = {
log.info("Starting...")
ServiceManager.serviceManager ! Lookup("cluster")
}
val LocalEvents = new GenericEventBus[LocalServiceResponse]
def receive = {
case ServiceManager.LookupResult("cluster", endpoint) =>
cluster = endpoint
log.trace("LocalService got cluster service " + endpoint)
case Service.Join(channel) =>
val path = s"/$channel/Local"
val who = sender()
@ -50,11 +72,31 @@ class LocalService extends Actor {
LocalEvents.publish(
LocalServiceResponse(s"/$forChannel/Local", player_guid, LocalResponse.HackClear(target.GUID, unk1, unk2))
)
case LocalAction.HackTemporarily(player_guid, zone, target, unk1, unk2) =>
hackClearer ! HackClearActor.ObjectIsHacked(target, zone, unk1, unk2)
case LocalAction.HackTemporarily(player_guid, zone, target, unk1, duration, unk2) =>
hackClearer ! HackClearActor.ObjectIsHacked(target, zone, unk1, unk2, duration)
LocalEvents.publish(
LocalServiceResponse(s"/$forChannel/Local", player_guid, LocalResponse.HackObject(target.GUID, unk1, unk2))
)
case LocalAction.ClearTemporaryHack(player_guid, target) =>
hackClearer ! HackClearActor.ObjectIsResecured(target)
case LocalAction.HackCaptureTerminal(player_guid, zone, target, unk1, unk2, isResecured) =>
if(isResecured){
hackCapturer ! HackCaptureActor.ClearHack(target, zone)
} else {
target.Definition match {
case GlobalDefinitions.capture_terminal =>
// Base CC
hackCapturer ! HackCaptureActor.ObjectIsHacked(target, zone, unk1, unk2, duration = 15 minutes)
case GlobalDefinitions.secondary_capture =>
// Tower CC
hackCapturer ! HackCaptureActor.ObjectIsHacked(target, zone, unk1, unk2, duration = 1 nanosecond)
}
}
LocalEvents.publish(
LocalServiceResponse(s"/$forChannel/Local", player_guid, LocalResponse.HackCaptureTerminal(target.GUID, unk1, unk2, isResecured))
)
case LocalAction.ProximityTerminalEffect(player_guid, object_guid, effectState) =>
LocalEvents.publish(
LocalServiceResponse(s"/$forChannel/Local", player_guid, LocalResponse.ProximityTerminalEffect(object_guid, effectState))
@ -63,6 +105,10 @@ class LocalService extends Actor {
LocalEvents.publish(
LocalServiceResponse(s"/$forChannel/Local", player_guid, LocalResponse.TriggerSound(sound, pos, unk, volume))
)
case LocalAction.SetEmpire(object_guid, empire) =>
LocalEvents.publish(
LocalServiceResponse(s"/$forChannel/Local", PlanetSideGUID(-1), LocalResponse.SetEmpire(object_guid, empire))
)
case _ => ;
}
@ -74,10 +120,48 @@ class LocalService extends Actor {
//response from HackClearActor
case HackClearActor.ClearTheHack(target_guid, zone_id, unk1, unk2) =>
log.warn(s"Clearing hack for ${target_guid}")
LocalEvents.publish(
LocalServiceResponse(s"/$zone_id/Local", Service.defaultPlayerGUID, LocalResponse.HackClear(target_guid, unk1, unk2))
)
case HackCaptureActor.HackTimeoutReached(capture_terminal_guid, zone_id, unk1, unk2, hackedByFaction) =>
import scala.concurrent.ExecutionContext.Implicits.global
ask(cluster, InterstellarCluster.GetWorld(zone_id))(1 seconds).onComplete {
case Success(InterstellarCluster.GiveWorld(zoneId, zone)) =>
val terminal = zone.asInstanceOf[Zone].GUID(capture_terminal_guid).get.asInstanceOf[CaptureTerminal]
val building = terminal.Owner.asInstanceOf[Building]
// todo: Move this to a function for Building
var ntuLevel = 0
building.Amenities.filter(x => (x.Definition == GlobalDefinitions.resource_silo)).headOption.asInstanceOf[Option[ResourceSilo]] match {
case Some(obj: ResourceSilo) =>
ntuLevel = obj.CapacitorDisplay.toInt
case _ =>
// Base has no NTU silo - likely a tower / cavern CC
ntuLevel = 1
}
if(ntuLevel > 0) {
log.info(s"Setting base ${building.ModelId} as owned by ${hackedByFaction}")
building.Faction = hackedByFaction
self ! LocalServiceMessage(zone.Id, LocalAction.SetEmpire(PlanetSideGUID(building.ModelId), hackedByFaction))
} else {
log.info("Base hack completed, but base was out of NTU.")
}
// Reset CC back to normal operation
self ! LocalServiceMessage(zone.Id, LocalAction.HackCaptureTerminal(PlanetSideGUID(-1), zone, terminal, 0, 8L, isResecured = true))
//todo: this appears to be the way to reset the base warning lights after the hack finishes but it doesn't seem to work. The attribute above is a workaround
self ! HackClearActor.ClearTheHack(PlanetSideGUID(building.ModelId), zone.Id, 3212836864L, 8L)
case Success(_) =>
log.warn("Got success from InterstellarCluster.GetWorld but didn't know how to handle it")
case scala.util.Failure(_) => log.warn(s"LocalService Failed to get zone when hack timeout was reached")
}
case HackCaptureActor.GetHackTimeRemainingNanos(capture_console_guid) =>
hackCapturer forward HackCaptureActor.GetHackTimeRemainingNanos(capture_console_guid)
case msg =>
log.info(s"Unhandled message $msg from $sender")
}

View file

@ -0,0 +1,112 @@
package services.local.support
import akka.actor.{Actor, Cancellable}
import net.psforever.objects.DefaultCancellable
import net.psforever.objects.serverobject.hackable.Hackable
import net.psforever.objects.serverobject.structures.Building
import net.psforever.objects.serverobject.terminals.CaptureTerminal
import net.psforever.objects.serverobject.{CommonMessages, PlanetSideServerObject}
import net.psforever.objects.zones.Zone
import net.psforever.packet.game.PlanetSideGUID
import net.psforever.types.PlanetSideEmpire
import scala.concurrent.duration.{FiniteDuration, _}
class HackCaptureActor extends Actor {
private [this] val log = org.log4s.getLogger
private var clearTrigger : Cancellable = DefaultCancellable.obj
/** A `List` of currently hacked server objects */
private var hackedObjects : List[HackCaptureActor.HackEntry] = Nil
def receive : Receive = {
case HackCaptureActor.ObjectIsHacked(target, zone, unk1, unk2, duration, time) =>
log.trace(s"${target.GUID} is hacked.")
hackedObjects.filter(x => x.target == target).headOption match {
case Some(x) =>
log.trace(s"${target.GUID} was already hacked - removing it from the hacked objects list before re-adding it.")
hackedObjects = hackedObjects.filterNot(x => x.target == target)
log.warn(s"len: ${hackedObjects.length}")
case _ => ;
}
hackedObjects = hackedObjects :+ HackCaptureActor.HackEntry(target, zone, unk1, unk2, duration, time)
// Restart the timer, in case this is the first object in the hacked objects list or the object was removed and re-added
RestartTimer()
target.Owner.Actor ! Building.SendMapUpdate(all_clients = true)
case HackCaptureActor.ProcessCompleteHacks() =>
log.trace("Processing complete hacks")
clearTrigger.cancel
val now : Long = System.nanoTime
val stillHacked = hackedObjects.filter(x => now - x.hack_timestamp <= x.duration.toNanos)
val unhackObjects = hackedObjects.filter(x => now - x.hack_timestamp >= x.duration.toNanos)
hackedObjects = stillHacked
unhackObjects.foreach(entry => {
log.trace(s"Capture terminal hack timeout reached for terminal ${entry.target.GUID}")
val hackedByFaction = entry.target.HackedBy.get._1.Faction
entry.target.Actor ! CommonMessages.ClearHack()
context.parent ! HackCaptureActor.HackTimeoutReached(entry.target.GUID, entry.zone.Id, entry.unk1, entry.unk2, hackedByFaction) //call up to the main event system
})
// If there's hacked objects left in the list restart the timer with the shortest hack time left
RestartTimer()
case HackCaptureActor.ClearHack(target, zone) =>
hackedObjects = hackedObjects.filterNot(x => x.target == target)
target.Owner.Actor ! Building.SendMapUpdate(all_clients = true)
// Restart the timer in case the object we just removed was the next one scheduled
RestartTimer()
case HackCaptureActor.GetHackTimeRemainingNanos(capture_console_guid) =>
hackedObjects.filter(x => x.target.GUID == capture_console_guid).headOption match {
case Some(obj: HackCaptureActor.HackEntry) =>
val time_left: Long = obj.duration.toNanos - (System.nanoTime - obj.hack_timestamp)
sender ! time_left
case _ =>
log.warn(s"Couldn't find capture terminal guid ${capture_console_guid} in hackedObjects list")
sender ! 0L
}
case _ => ;
}
private def RestartTimer(): Unit = {
if(hackedObjects.length != 0) {
val now = System.nanoTime()
def minTimeLeft(entry1: HackCaptureActor.HackEntry, entry2: HackCaptureActor.HackEntry): HackCaptureActor.HackEntry = {
val entry1TimeLeft = entry1.duration.toNanos - (now - entry1.hack_timestamp)
val entry2TimeLeft = entry2.duration.toNanos - (now - entry2.hack_timestamp)
if(entry1TimeLeft < entry2TimeLeft) entry1 else entry2
}
val hackEntry = hackedObjects.reduceLeft(minTimeLeft)
val short_timeout : FiniteDuration = math.max(1, hackEntry.duration.toNanos - (System.nanoTime - hackEntry.hack_timestamp)) nanoseconds
log.trace(s"Still items left in hacked objects list. Checking again in ${short_timeout}")
import scala.concurrent.ExecutionContext.Implicits.global
clearTrigger = context.system.scheduler.scheduleOnce(short_timeout, self, HackCaptureActor.ProcessCompleteHacks())
}
}
}
object HackCaptureActor {
final case class ObjectIsHacked(target : CaptureTerminal, zone : Zone, unk1 : Long, unk2 : Long, duration: FiniteDuration, time : Long = System.nanoTime())
final case class HackTimeoutReached(capture_terminal_guid : PlanetSideGUID, zone_id : String, unk1 : Long, unk2 : Long, hackedByFaction : PlanetSideEmpire.Value)
final case class ClearHack(target : CaptureTerminal, zone : Zone)
final case class GetHackTimeRemainingNanos(capture_console_guid: PlanetSideGUID)
private final case class ProcessCompleteHacks()
private final case class HackEntry(target : PlanetSideServerObject with Hackable, zone : Zone, unk1 : Long, unk2 : Long, duration: FiniteDuration, hack_timestamp : Long)
}

View file

@ -1,8 +1,11 @@
// Copyright (c) 2017 PSForever
package services.local.support
import java.util.concurrent.TimeUnit
import akka.actor.{Actor, Cancellable}
import net.psforever.objects.DefaultCancellable
import net.psforever.objects.serverobject.hackable.Hackable
import net.psforever.objects.serverobject.{CommonMessages, PlanetSideServerObject}
import net.psforever.objects.zones.Zone
import net.psforever.packet.game.PlanetSideGUID
@ -20,15 +23,15 @@ class HackClearActor() extends Actor {
private var clearTrigger : Cancellable = DefaultCancellable.obj
/** A `List` of currently hacked server objects */
private var hackedObjects : List[HackClearActor.HackEntry] = Nil
//private[this] val log = org.log4s.getLogger
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.ObjectIsHacked(target, zone, unk1, unk2, duration, time) =>
val durationNanos = TimeUnit.NANOSECONDS.convert(duration, TimeUnit.SECONDS)
hackedObjects = hackedObjects :+ HackClearActor.HackEntry(target, zone, unk1, unk2, time, durationNanos)
// Restart the timer, in case this is the first object in the hacked objects list
RestartTimer()
case HackClearActor.TryClearHacks() =>
clearTrigger.cancel
@ -41,15 +44,36 @@ class HackClearActor() extends Actor {
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())
RestartTimer()
case HackClearActor.ObjectIsResecured(target) =>
val obj = hackedObjects.filter(x => x.target == target).headOption
obj match {
case Some(entry: HackClearActor.HackEntry) =>
hackedObjects = hackedObjects.filterNot(x => x.target == target)
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
// Restart the timer in case the object we just removed was the next one scheduled
RestartTimer()
case None => ;
}
case _ => ;
}
private def RestartTimer(): Unit = {
if(hackedObjects.length != 0) {
val now = System.nanoTime()
val (unhackObjects, stillHackedObjects) = PartitionEntries(hackedObjects, now)
val short_timeout : FiniteDuration = math.max(1, stillHackedObjects.head.duration - (now - stillHackedObjects.head.time)) nanoseconds
log.warn(s"Still items left in hacked objects list. Checking again in ${short_timeout}")
import scala.concurrent.ExecutionContext.Implicits.global
clearTrigger = context.system.scheduler.scheduleOnce(short_timeout, self, HackClearActor.TryClearHacks())
}
}
/**
* Iterate over entries in a `List` until an entry that does not exceed the time limit is discovered.
* Separate the original `List` into two:
@ -84,7 +108,7 @@ class HackClearActor() extends Actor {
}
else {
val entry = iter.next()
if(now - entry.time >= HackClearActor.timeout_time) {
if(now - entry.time >= entry.duration) {
recursivePartitionEntries(iter, now, index + 1)
}
else {
@ -95,26 +119,31 @@ class HackClearActor() extends Actor {
}
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
/**
* 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
* @param duration how long the object is to stay hacked for in seconds
* @see `HackEntry`
*/
final case class ObjectIsHacked(target : PlanetSideServerObject, zone : Zone, unk1 : Long, unk2 : Long, time : Long = System.nanoTime())
final case class ObjectIsHacked(target : PlanetSideServerObject, zone : Zone, unk1 : Long, unk2 : Long, duration: Int, time : Long = System.nanoTime())
/**
* Message used to request that a hack is cleared from the hacked objects list and the unhacked status returned to all clients
*
*/
final case class ObjectIsResecured(target: PlanetSideServerObject with Hackable)
/**
* 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 obj 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)
final case class ClearTheHack(obj : PlanetSideGUID, zone_id : String, unk1 : Long, unk2 : Long)
/**
* Internal message used to signal a test of the queued door information.
*/
@ -122,11 +151,12 @@ object HackClearActor {
/**
* 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.
* The `zone` is maintained separately to ensure that any message resulting in an attempt to close doors is targeted.
* @param target the server object
* @param zone the zone in which the object resides
* @param time when the object was hacked
* @param duration The hack duration in nanoseconds
* @see `ObjectIsHacked`
*/
private final case class HackEntry(target : PlanetSideServerObject, zone : Zone, unk1 : Long, unk2 : Long, time : Long)
private final case class HackEntry(target : PlanetSideServerObject, zone : Zone, unk1 : Long, unk2 : Long, time : Long, duration: Long)
}

View file

@ -176,7 +176,7 @@ class ResourceSiloControlUpdate1Test extends ActorTest {
assert(reply1.asInstanceOf[AvatarServiceMessage]
.actionMessage.asInstanceOf[AvatarAction.PlanetsideAttribute].attribute_value == 3)
assert(reply2.isInstanceOf[Building.SendMapUpdateToAllClients])
assert(reply2.isInstanceOf[Building.SendMapUpdate])
val reply3 = probe1.receiveOne(500 milliseconds)
assert(reply3.isInstanceOf[AvatarServiceMessage])
@ -249,7 +249,7 @@ class ResourceSiloControlUpdate2Test extends ActorTest {
assert(reply1.asInstanceOf[AvatarServiceMessage]
.actionMessage.asInstanceOf[AvatarAction.PlanetsideAttribute].attribute_value == 2)
assert(reply2.isInstanceOf[Building.SendMapUpdateToAllClients])
assert(reply2.isInstanceOf[Building.SendMapUpdate])
val reply3 = probe1.receiveOne(500 milliseconds)
assert(obj.LowNtuWarningOn == false)

View file

@ -10,6 +10,7 @@ import net.psforever.objects.serverobject.mblocker.Locker
import net.psforever.objects.serverobject.pad.VehicleSpawnPad
import net.psforever.objects.serverobject.structures.{Building, FoundationBuilder, StructureType, WarpGate}
import net.psforever.objects.serverobject.terminals.{ProximityTerminal, Terminal}
import net.psforever.objects.serverobject.terminals.{CaptureTerminal, ProximityTerminal, Terminal}
import net.psforever.objects.serverobject.tube.SpawnTube
import net.psforever.objects.serverobject.resourcesilo.ResourceSilo
import net.psforever.objects.serverobject.turret.MannedTurret
@ -94,8 +95,8 @@ object Maps {
DoorToLock(384, 1036)
DoorToLock(386, 1038)
DoorToLock(388, 1039)
// DoorToLock(394, 1047)
// DoorToLock(395, 1049)
DoorToLock(394, 1047)
DoorToLock(395, 1049)
DoorToLock(401, 1053)
DoorToLock(920, 968)
@ -173,6 +174,7 @@ object Maps {
def Building9() : Unit = { // Girru
LocalBuilding(9, FoundationBuilder(Building.Structure(StructureType.Facility, Vector3(4397f, 5895f, 0)))) // Todo change pos
LocalObject(225, CaptureTerminal.Constructor(capture_terminal))
LocalObject(513, Door.Constructor)
LocalObject(514, Door.Constructor)
LocalObject(515, Door.Constructor)
@ -225,6 +227,7 @@ object Maps {
LocalObject(2015, Terminal.Constructor(order_terminal))
LocalObject(2016, Terminal.Constructor(order_terminal))
LocalObject(2017, Terminal.Constructor(order_terminal))
LocalObject(2658, ResourceSilo.Constructor)
LocalObject(2724, SpawnTube.Constructor(Vector3(4396.7656f, 5888.258f, 71.15625f), Vector3(0, 0, 92)))
LocalObject(2725, SpawnTube.Constructor(Vector3(4397.211f, 5895.547f, 71.15625f), Vector3(0, 0, 92)))
LocalObject(2726, SpawnTube.Constructor(Vector3(4397.2344f, 5902.8203f, 71.15625f), Vector3(0, 0, 92)))
@ -244,6 +247,7 @@ object Maps {
// LocalObject(1909, ProximityTerminal.Constructor(medical_terminal))
// LocalObject(1910, ProximityTerminal.Constructor(medical_terminal))
ObjectToBuilding(225, 9)
ObjectToBuilding(513, 9)
ObjectToBuilding(514, 9)
ObjectToBuilding(515, 9)
@ -298,6 +302,7 @@ object Maps {
ObjectToBuilding(2015, 9)
ObjectToBuilding(2016, 9)
ObjectToBuilding(2017, 9)
ObjectToBuilding(2658, 9)
ObjectToBuilding(2724, 9)
ObjectToBuilding(2725, 9)
ObjectToBuilding(2726, 9)
@ -332,6 +337,7 @@ object Maps {
def Building10() : Unit = { // Hanish
LocalBuilding(10, FoundationBuilder(Building.Structure(StructureType.Facility, Vector3(3749f, 5477f, 0)))) // Todo change pos
LocalObject(223, CaptureTerminal.Constructor(capture_terminal))
LocalObject(464, Door.Constructor)
LocalObject(470, Door.Constructor(Vector3(3645.3984f, 5451.9688f, 88.890625f), Vector3(0, 0, 182)))
LocalObject(471, Door.Constructor)
@ -374,6 +380,7 @@ object Maps {
LocalObject(971, IFFLock.Constructor)
LocalObject(1105, IFFLock.Constructor)
LocalObject(1106, IFFLock.Constructor)
LocalObject(1107, IFFLock.Constructor)
LocalObject(1108, IFFLock.Constructor)
LocalObject(1113, IFFLock.Constructor)
LocalObject(1114, IFFLock.Constructor)
@ -431,6 +438,7 @@ object Maps {
// ObjectToBuilding(169, 10)
// ObjectToBuilding(1906, 10)
ObjectToBuilding(223, 10)
ObjectToBuilding(464, 10)
ObjectToBuilding(470, 10)
ObjectToBuilding(471, 10)
@ -469,10 +477,12 @@ object Maps {
ObjectToBuilding(923, 10)
ObjectToBuilding(932, 10)
ObjectToBuilding(933, 10)
ObjectToBuilding(959, 10)
ObjectToBuilding(971, 10)
ObjectToBuilding(1105, 10)
ObjectToBuilding(1106, 10)
ObjectToBuilding(1107, 10)
ObjectToBuilding(1108, 10)
ObjectToBuilding(1113, 10)
ObjectToBuilding(1114, 10)
@ -528,6 +538,7 @@ object Maps {
DoorToLock(475, 1108)
DoorToLock(481, 1113)
DoorToLock(771, 1106)
DoorToLock(774, 1107)
DoorToLock(779, 1114)
DoorToLock(784, 1116)
DoorToLock(785, 1115)
@ -761,6 +772,7 @@ object Maps {
}
def Building33() : Unit = { // East Girru Gun Tower, Ishundar (ID: 62)
LocalBuilding(33, FoundationBuilder(Building.Structure(StructureType.Tower, Vector3(4624f, 5915f, 0)))) // TODO loc
LocalObject(2792, CaptureTerminal.Constructor(secondary_capture))
LocalObject(2957, Door.Constructor)
LocalObject(2958, Door.Constructor)
LocalObject(542, Door.Constructor(Vector3(4625.9844f, 5910.211f, 55.75f), Vector3(0, 0, 180)))
@ -777,6 +789,7 @@ object Maps {
LocalObject(2733, SpawnTube.Constructor(respawn_tube_tower, Vector3(4624.758f, 5905.7344f, 45.984375f), Vector3(0, 0, 90)))
LocalObject(2734, SpawnTube.Constructor(respawn_tube_tower, Vector3(4624.7266f, 5922.1484f, 45.984375f), Vector3(0, 0, 90)))
ObjectToBuilding(2792, 33)
ObjectToBuilding(2957, 33)
ObjectToBuilding(2958, 33)
ObjectToBuilding(542, 33)
@ -1694,6 +1707,7 @@ object Maps {
LocalObject(396, Door.Constructor)
LocalObject(397, Door.Constructor)
LocalObject(398, Door.Constructor)
LocalObject(399, Door.Constructor)
LocalObject(462, Door.Constructor)
LocalObject(463, Door.Constructor)
LocalObject(522, ImplantTerminalMech.Constructor)
@ -1741,6 +1755,7 @@ object Maps {
ObjectToBuilding(396, 2)
ObjectToBuilding(397, 2)
ObjectToBuilding(398, 2)
ObjectToBuilding(399, 2)
ObjectToBuilding(462, 2)
ObjectToBuilding(463, 2)
ObjectToBuilding(522, 2)

View file

@ -1,4 +1,5 @@
// Copyright (c) 2017 PSForever
import java.util.concurrent.TimeUnit
import java.util.concurrent.atomic.AtomicInteger
import akka.actor.{Actor, ActorRef, Cancellable, MDCContextAware}
@ -53,10 +54,11 @@ import services.vehicle.{VehicleAction, VehicleResponse, VehicleServiceMessage,
import scala.concurrent.duration._
import scala.concurrent.ExecutionContext.Implicits.global
import scala.annotation.tailrec
import scala.concurrent.Future
import scala.concurrent.{Await, Future}
import scala.concurrent.duration._
import scala.util.Success
import akka.pattern.ask
import services.local.support.HackCaptureActor
class WorldSessionActor extends Actor with MDCContextAware {
import WorldSessionActor._
@ -560,10 +562,16 @@ class WorldSessionActor extends Actor with MDCContextAware {
sendResponse(GenericObjectStateMsg(door_guid, 17))
case LocalResponse.HackClear(target_guid, unk1, unk2) =>
log.trace(s"Clearing hack for ${target_guid}")
// Reset hack state for all players
sendResponse(HackMessage(0, target_guid, guid, 0, unk1, HackState.HackCleared, unk2))
// Set the object faction displayed back to it's original owner faction
sendResponse(SetEmpireMessage(target_guid, continent.GUID(target_guid).get.asInstanceOf[FactionAffinity].Faction))
continent.GUID(target_guid) match {
case Some(obj) =>
sendResponse(SetEmpireMessage(target_guid, obj.asInstanceOf[FactionAffinity].Faction))
case None => ;
}
case LocalResponse.HackObject(target_guid, unk1, unk2) =>
if(tplayer_guid != guid && continent.GUID(target_guid).get.asInstanceOf[Hackable].HackedBy.get._1.Faction != player.Faction) {
@ -576,7 +584,31 @@ class WorldSessionActor extends Actor with MDCContextAware {
// Make the hacked object look like it belongs to the hacking empire, but only for that empire's players (so that infiltrators on stealth missions won't be given away to opposing factions)
sendResponse(SetEmpireMessage(target_guid, player.Faction))
}
case LocalResponse.HackCaptureTerminal(target_guid, unk1, unk2, isResecured) =>
var value = 0L
if(isResecured) {
value = 17039360L
} else {
import scala.concurrent.ExecutionContext.Implicits.global
val future = ask(localService, HackCaptureActor.GetHackTimeRemainingNanos(target_guid))(1 second)
val time = Await.result(future, 1 second).asInstanceOf[Long] // todo: blocking call. Not good.
val hack_time_remaining_ms = TimeUnit.MILLISECONDS.convert(time, TimeUnit.NANOSECONDS)
val deciseconds_remaining = (hack_time_remaining_ms / 100)
val hacking_faction = continent.GUID(target_guid).get.asInstanceOf[Hackable].HackedBy.get._1.Faction
// See PlanetSideAttributeMessage #20 documentation for an explanation of how the timer is calculated
val start_num = hacking_faction match {
case PlanetSideEmpire.TR => 65536L
case PlanetSideEmpire.NC => 131072L
case PlanetSideEmpire.VS => 196608L
}
value = start_num + deciseconds_remaining
}
sendResponse(PlanetsideAttributeMessage(target_guid, 20, value))
case LocalResponse.ProximityTerminalEffect(object_guid, effectState) =>
if(tplayer_guid != guid) {
sendResponse(ProximityTerminalUseMessage(PlanetSideGUID(0), object_guid, effectState))
@ -585,6 +617,8 @@ class WorldSessionActor extends Actor with MDCContextAware {
case LocalResponse.TriggerSound(sound, pos, unk, volume) =>
sendResponse(TriggerSoundMessage(sound, pos, unk, volume))
case LocalResponse.SetEmpire(object_guid, empire) =>
sendResponse(SetEmpireMessage(object_guid, empire))
case _ => ;
}
@ -1451,6 +1485,7 @@ class WorldSessionActor extends Actor with MDCContextAware {
val popNC = poplist.count(_.faction == PlanetSideEmpire.NC)
val popVS = poplist.count(_.faction == PlanetSideEmpire.VS)
// StopBundlingPackets() is called on ClientInitializationComplete
StartBundlingPackets()
zone.Buildings.foreach({ case(id, building) => initBuilding(continentNumber, id, building) })
sendResponse(ZonePopulationUpdateMessage(continentNumber, 414, 138, popTR, 138, popNC, 138, popVS, 138, popBO))
@ -2764,7 +2799,7 @@ class WorldSessionActor extends Actor with MDCContextAware {
case Some(unholsteredItem : Equipment) =>
if(unholsteredItem.Definition == GlobalDefinitions.remote_electronics_kit) {
// Player has ulholstered a REK - we need to set an atttribute on the REK itself to change the beam/icon colour to the correct one for the player's hack level
avatarService ! AvatarServiceMessage(player.Continent, AvatarAction.PlanetsideAttribute(unholsteredItem.GUID, 116, GetPlayerHackData().hackLevel))
avatarService ! AvatarServiceMessage(player.Continent, AvatarAction.PlanetsideAttribute(unholsteredItem.GUID, 116, GetPlayerHackLevel()))
}
case None => ;
}
@ -2971,9 +3006,21 @@ class WorldSessionActor extends Actor with MDCContextAware {
continent.GUID(object_guid) match {
case Some(door : Door) =>
if(player.Faction == door.Faction || ((continent.Map.DoorToLock.get(object_guid.guid) match {
case Some(lock_guid) => continent.GUID(lock_guid).get.asInstanceOf[IFFLock].HackedBy.isDefined
case Some(lock_guid) =>
val lock = continent.GUID(lock_guid).get.asInstanceOf[IFFLock]
var baseIsHacked = false
lock.Owner.asInstanceOf[Building].Amenities.filter(x => x.Definition == GlobalDefinitions.capture_terminal).headOption.asInstanceOf[Option[CaptureTerminal]] match {
case Some(obj: CaptureTerminal) =>
baseIsHacked = obj.HackedBy.isDefined
case None => ;
}
// If the IFF lock has been hacked OR the base is neutral OR the base linked to the lock is hacked then open the door
lock.HackedBy.isDefined || baseIsHacked || lock.Faction == PlanetSideEmpire.NEUTRAL
case None => !door.isOpen
}) || Vector3.ScalarProjection(door.Outwards, player.Position - door.Position) < 0f)) {
// We're on the inside of the door - open the door
door.Actor ! Door.Use(player, msg)
}
else if(door.isOpen) {
@ -2995,13 +3042,25 @@ class WorldSessionActor extends Actor with MDCContextAware {
}
case Some(panel : IFFLock) =>
if(panel.Faction != player.Faction && panel.HackedBy.isEmpty) {
if((panel.Faction != player.Faction && panel.HackedBy.isEmpty) || (panel.Faction == player.Faction && panel.HackedBy.isDefined)) {
player.Slot(player.DrawnSlot).Equipment match {
case Some(tool : SimpleItem) =>
if(tool.Definition == GlobalDefinitions.remote_electronics_kit) {
progressBarValue = Some(-GetPlayerHackSpeed())
self ! WorldSessionActor.HackingProgress(1, player, panel, tool.GUID, GetPlayerHackSpeed(), FinishHacking(panel, 1114636288L))
log.info("Hacking a door~")
val hackSpeed = GetPlayerHackSpeed(panel)
if(hackSpeed > 0) {
progressBarValue = Some(-hackSpeed)
if(panel.Faction != player.Faction) {
// Enemy faction is hacking this IFF lock
self ! WorldSessionActor.HackingProgress(progressType = 1, player, panel, tool.GUID, hackSpeed, FinishHacking(panel, 1114636288L))
log.info("Hacking an IFF lock")
} else {
// IFF Lock is being resecured by it's owner faction
self ! WorldSessionActor.HackingProgress(progressType = 1, player, panel, tool.GUID, hackSpeed, FinishResecuringIFFLock(panel))
log.info("Resecuring an IFF lock")
}
}
}
case _ => ;
}
@ -3056,18 +3115,22 @@ class WorldSessionActor extends Actor with MDCContextAware {
}
}
case Some(obj : Locker) =>
if(obj.Faction != player.Faction && obj.HackedBy.isEmpty) {
case Some(locker : Locker) =>
if(locker.Faction != player.Faction && locker.HackedBy.isEmpty) {
player.Slot(player.DrawnSlot).Equipment match {
case Some(tool: SimpleItem) =>
if (tool.Definition == GlobalDefinitions.remote_electronics_kit) {
progressBarValue = Some(-GetPlayerHackSpeed())
self ! WorldSessionActor.HackingProgress(1, player, obj, tool.GUID, GetPlayerHackSpeed(), FinishHacking(obj, 3212836864L))
log.info("Hacking a locker")
val hackSpeed = GetPlayerHackSpeed(locker)
if(hackSpeed > 0) {
progressBarValue = Some(-hackSpeed)
self ! WorldSessionActor.HackingProgress(progressType = 1, player, locker, tool.GUID, hackSpeed, FinishHacking(locker, 3212836864L))
log.info("Hacking a locker")
}
}
case _ => ;
}
} else if(player.Faction == obj.Faction || !obj.HackedBy.isEmpty) {
} else if(player.Faction == locker.Faction || !locker.HackedBy.isEmpty) {
log.info(s"UseItem: $player accessing a locker")
val container = player.Locker
accessedContainer = Some(container)
@ -3077,6 +3140,25 @@ class WorldSessionActor extends Actor with MDCContextAware {
log.info(s"UseItem: not $player's locker")
}
case Some(captureTerminal : CaptureTerminal) =>
val hackedByCurrentFaction = (captureTerminal.Faction != player.Faction && !captureTerminal.HackedBy.isEmpty && captureTerminal.HackedBy.head._1.Faction == player.Faction)
val ownedByPlayerFactionAndHackedByEnemyFaction = (captureTerminal.Faction == player.Faction && !captureTerminal.HackedBy.isEmpty)
if(!hackedByCurrentFaction || ownedByPlayerFactionAndHackedByEnemyFaction) {
player.Slot(player.DrawnSlot).Equipment match {
case Some(tool: SimpleItem) =>
if (tool.Definition == GlobalDefinitions.remote_electronics_kit) {
val hackSpeed = GetPlayerHackSpeed(captureTerminal)
if(hackSpeed > 0) {
progressBarValue = Some(-hackSpeed)
self ! WorldSessionActor.HackingProgress(progressType = 1, player, captureTerminal, tool.GUID, hackSpeed, FinishHacking(captureTerminal, 3212836864L))
log.info("Hacking a capture terminal")
}
}
case _ => ;
}
}
case Some(obj : MannedTurret) =>
player.Slot(player.DrawnSlot).Equipment match {
case Some(tool : Tool) =>
@ -3085,11 +3167,11 @@ class WorldSessionActor extends Actor with MDCContextAware {
if(ammo == Ammo.upgrade_canister && obj.Seats.values.count(_.isOccupied) == 0) {
progressBarValue = Some(-1.25f)
self ! WorldSessionActor.HackingProgress(
2,
progressType = 2,
player,
obj,
tool.GUID,
1.25f,
delta = 1.25f,
FinishUpgradingMannedTurret(obj, tool, TurretUpgrade(unk2.toInt))
)
}
@ -3145,12 +3227,12 @@ class WorldSessionActor extends Actor with MDCContextAware {
}
}
case Some(obj : Terminal) =>
if(obj.Definition.isInstanceOf[MatrixTerminalDefinition]) {
case Some(terminal : Terminal) =>
if(terminal.Definition.isInstanceOf[MatrixTerminalDefinition]) {
//TODO matrix spawn point; for now, just blindly bind to show work (and hope nothing breaks)
sendResponse(BindPlayerMessage(1, "@ams", true, true, 0, 0, 0, obj.Position))
sendResponse(BindPlayerMessage(1, "@ams", true, true, 0, 0, 0, terminal.Position))
}
else if(obj.Definition.isInstanceOf[RepairRearmSiloDefinition]) {
else if(terminal.Definition.isInstanceOf[RepairRearmSiloDefinition]) {
FindLocalVehicle match {
case Some(vehicle) =>
sendResponse(UseItemMessage(avatar_guid, item_used_guid, object_guid, unk2, unk3, unk4, unk5, unk6, unk7, unk8, itemType))
@ -3160,17 +3242,21 @@ class WorldSessionActor extends Actor with MDCContextAware {
}
}
else {
if(obj.Faction != player.Faction && obj.HackedBy.isEmpty) {
if(terminal.Faction != player.Faction && terminal.HackedBy.isEmpty) {
player.Slot(player.DrawnSlot).Equipment match {
case Some(tool: SimpleItem) =>
if (tool.Definition == GlobalDefinitions.remote_electronics_kit) {
progressBarValue = Some(-GetPlayerHackSpeed())
self ! WorldSessionActor.HackingProgress(1, player, obj, tool.GUID, GetPlayerHackSpeed(), FinishHacking(obj, 3212836864L))
log.info("Hacking a terminal")
val hackSpeed = GetPlayerHackSpeed(terminal)
if(hackSpeed > 0) {
progressBarValue = Some(-hackSpeed)
self ! WorldSessionActor.HackingProgress(progressType = 1, player, terminal, tool.GUID, hackSpeed, FinishHacking(terminal, 3212836864L))
log.info("Hacking a terminal")
}
}
case _ => ;
}
} else if (obj.Faction == player.Faction || !obj.HackedBy.isEmpty) {
} else if (terminal.Faction == player.Faction || !terminal.HackedBy.isEmpty) {
// If hacked only allow access to the faction that hacked it
// Otherwise allow the faction that owns the terminal to use it
sendResponse(UseItemMessage(avatar_guid, item_used_guid, object_guid, unk2, unk3, unk4, unk5, unk6, unk7, unk8, itemType))
@ -4135,10 +4221,23 @@ class WorldSessionActor extends Actor with MDCContextAware {
ask(target.Actor, CommonMessages.Hack(player))(1 second).mapTo[Boolean].onComplete {
case Success(_) =>
localService ! LocalServiceMessage(continent.Id, LocalAction.TriggerSound(player.GUID, target.HackSound, player.Position, 30, 0.49803925f))
localService ! LocalServiceMessage(continent.Id, LocalAction.HackTemporarily(player.GUID, continent, target, unk))
case scala.util.Failure(_) =>
log.warn(s"Hack message failed on target guid: ${target.GUID}")
}
target match {
case term : CaptureTerminal =>
val isResecured = player.Faction == target.Faction
localService ! LocalServiceMessage(continent.Id, LocalAction.HackCaptureTerminal(player.GUID, continent, term, unk, 8L, isResecured))
case _ =>localService ! LocalServiceMessage(continent.Id, LocalAction.HackTemporarily(player.GUID, continent, target, unk, target.HackEffectDuration(GetPlayerHackLevel())))
}
case scala.util.Failure(_) => log.warn(s"Hack message failed on target guid: ${target.GUID}")
}
}
/**
* The process of resecuring an IFF lock is finished
* Clear the hack state and send to clients
* @param lock the `IFFLock` object that has been resecured
*/
private def FinishResecuringIFFLock(lock: IFFLock)() : Unit = {
localService ! LocalServiceMessage(continent.Id, LocalAction.ClearTemporaryHack(player.GUID, lock))
}
/**
@ -4963,38 +5062,7 @@ class WorldSessionActor extends Actor with MDCContextAware {
* @param building the building object
*/
def initFacility(continentNumber : Int, buildingNumber : Int, building : Building) : Unit = {
var ntuLevel = 0
building.Amenities.filter(x => (x.Definition == GlobalDefinitions.resource_silo)).headOption.asInstanceOf[Option[ResourceSilo]] match {
case Some(obj: ResourceSilo) =>
ntuLevel = obj.CapacitorDisplay.toInt
case _ => ;
}
sendResponse(
BuildingInfoUpdateMessage(
continent_id = continentNumber,
building_id = buildingNumber,
ntu_level = ntuLevel,
is_hacked = false,
empire_hack = PlanetSideEmpire.NEUTRAL,
hack_time_remaining = 0, // milliseconds
empire_own = building.Faction,
unk1 = 0, //!! Field != 0 will cause malformed packet. See class def.
unk1x = None,
generator_state = PlanetSideGeneratorState.Normal,
spawn_tubes_normal = true,
force_dome_active = false,
lattice_benefit = 0,
cavern_benefit = 0, //!! Field > 0 will cause malformed packet. See class def.
unk4 = Nil,
unk5 = 0,
unk6 = false,
unk7 = 8, //!! Field != 8 will cause malformed packet. See class def.
unk7x = None,
boost_spawn_pain = false,
boost_generator_pain = false
)
)
building.Actor ! Building.SendMapUpdate(all_clients = false)
sendResponse(DensityLevelUpdateMessage(continentNumber, buildingNumber, List(0,0, 0,0, 0,0, 0,0)))
}
@ -5058,18 +5126,35 @@ class WorldSessionActor extends Actor with MDCContextAware {
amenity.Definition match {
case GlobalDefinitions.resource_silo =>
// Synchronise warning light & silo capacity
var silo = amenity.asInstanceOf[ResourceSilo]
val silo = amenity.asInstanceOf[ResourceSilo]
sendResponse(PlanetsideAttributeMessage(amenityId, 45, silo.CapacitorDisplay))
sendResponse(PlanetsideAttributeMessage(amenityId, 47, if(silo.LowNtuWarningOn) 1 else 0))
if(silo.ChargeLevel == 0) {
// temporarily disabled until warpgates can bring ANTs from sanctuary, otherwise we'd be stuck in a situation with an unpowered base and no way to get an ANT to refill it.
// sendResponse(PlanetsideAttributeMessage(PlanetSideGUID(silo.Owner.asInstanceOf[Building].ModelId), 48, 1))
//todo: temporarily disabled until warpgates can bring ANTs from sanctuary, otherwise we'd be stuck in a situation with an unpowered base and no way to get an ANT to refill it.
//sendResponse(PlanetsideAttributeMessage(PlanetSideGUID(silo.Owner.asInstanceOf[Building].ModelId), 48, 1))
}
case _ => ;
}
// Synchronise hack states to clients joining the zone.
// We'll have to fake LocalServiceResponse messages to self, otherwise it means duplicating the same hack handling code twice
if(amenity.isInstanceOf[Hackable]) {
val hackable = amenity.asInstanceOf[Hackable]
if(hackable.HackedBy.isDefined) {
amenity.Definition match {
case GlobalDefinitions.capture_terminal =>
self ! LocalServiceResponse("", PlanetSideGUID(0), LocalResponse.HackCaptureTerminal(amenity.GUID, 0L, 0L, false))
case _ =>
// Generic hackable object
self ! LocalServiceResponse("", PlanetSideGUID(0), LocalResponse.HackObject(amenity.GUID, 1114636288L, 8L))
}
}
}
})
sendResponse(HackMessage(3, PlanetSideGUID(building.ModelId), PlanetSideGUID(0), 0, 3212836864L, HackState.HackCleared, 8))
// sendResponse(HackMessage(3, PlanetSideGUID(building.ModelId), PlanetSideGUID(0), 0, 3212836864L, HackState.HackCleared, 8))
})
}
@ -5844,29 +5929,31 @@ class WorldSessionActor extends Actor with MDCContextAware {
sendResponse(RawPacket(pkt))
}
def GetPlayerHackSpeed(): Float = {
if(player.Certifications.contains(CertificationType.ExpertHacking) || player.Certifications.contains(CertificationType.ElectronicsExpert)) {
10.64f
} else if(player.Certifications.contains(CertificationType.AdvancedHacking)) {
5.32f
} else {
2.66f
def GetPlayerHackSpeed(obj: PlanetSideServerObject with Hackable): Float = {
val playerHackLevel = GetPlayerHackLevel()
val timeToHack = obj.HackDuration(playerHackLevel)
if(timeToHack == 0) {
log.warn(s"Player ${player.GUID} tried to hack an object ${obj.GUID} - ${obj.Definition.Name} that they don't have the correct hacking level for")
0f
}
// 250 ms per tick on the hacking progress bar
val ticks = (timeToHack * 1000) / 250
100f / ticks
}
def GetPlayerHackData(): PlayerHackData = {
def GetPlayerHackLevel(): Int = {
if(player.Certifications.contains(CertificationType.ExpertHacking) || player.Certifications.contains(CertificationType.ElectronicsExpert)) {
PlayerHackData(3, 10.64f)
3
} else if(player.Certifications.contains(CertificationType.AdvancedHacking)) {
PlayerHackData(2, 7.98f)
2
} else if (player.Certifications.contains(CertificationType.Hacking)) {
PlayerHackData(1, 5.32f)
1
} else {
PlayerHackData(0, 2.66f)
0
}
}
case class PlayerHackData(hackLevel: Int, hackSpeed: Float)
}
object WorldSessionActor {
@ -5893,6 +5980,8 @@ object WorldSessionActor {
* The process of "making progress" with a hack involves sending this message repeatedly until the progress is 100 or more.
* To calculate the actual amount of change in the progress `delta`,
* start with 100, divide by the length of time in seconds, then divide once more by 4.
* @param progressType 1 - REK hack
* 2 - Turret upgrade with glue gun + upgrade cannister
* @param tplayer the player
* @param target the object being hacked
* @param tool_guid the REK