diff --git a/common/src/main/scala/net/psforever/objects/GlobalDefinitions.scala b/common/src/main/scala/net/psforever/objects/GlobalDefinitions.scala index eec948f9c..46049f60e 100644 --- a/common/src/main/scala/net/psforever/objects/GlobalDefinitions.scala +++ b/common/src/main/scala/net/psforever/objects/GlobalDefinitions.scala @@ -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 diff --git a/common/src/main/scala/net/psforever/objects/serverobject/hackable/Hackable.scala b/common/src/main/scala/net/psforever/objects/serverobject/hackable/Hackable.scala index ef5bbd3f3..e51f0804c 100644 --- a/common/src/main/scala/net/psforever/objects/serverobject/hackable/Hackable.scala +++ b/common/src/main/scala/net/psforever/objects/serverobject/hackable/Hackable.scala @@ -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 + } } diff --git a/common/src/main/scala/net/psforever/objects/serverobject/locks/IFFLock.scala b/common/src/main/scala/net/psforever/objects/serverobject/locks/IFFLock.scala index 6c21bdc41..e7c305233 100644 --- a/common/src/main/scala/net/psforever/objects/serverobject/locks/IFFLock.scala +++ b/common/src/main/scala/net/psforever/objects/serverobject/locks/IFFLock.scala @@ -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 { diff --git a/common/src/main/scala/net/psforever/objects/serverobject/mblocker/Locker.scala b/common/src/main/scala/net/psforever/objects/serverobject/mblocker/Locker.scala index c91c787d3..a7b9f52e5 100644 --- a/common/src/main/scala/net/psforever/objects/serverobject/mblocker/Locker.scala +++ b/common/src/main/scala/net/psforever/objects/serverobject/mblocker/Locker.scala @@ -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 { diff --git a/common/src/main/scala/net/psforever/objects/serverobject/resourcesilo/ResourceSiloControl.scala b/common/src/main/scala/net/psforever/objects/serverobject/resourcesilo/ResourceSiloControl.scala index 1de8894aa..4045c8bd6 100644 --- a/common/src/main/scala/net/psforever/objects/serverobject/resourcesilo/ResourceSiloControl.scala +++ b/common/src/main/scala/net/psforever/objects/serverobject/resourcesilo/ResourceSiloControl.scala @@ -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)) } diff --git a/common/src/main/scala/net/psforever/objects/serverobject/structures/Building.scala b/common/src/main/scala/net/psforever/objects/serverobject/structures/Building.scala index 416b86626..8c7cfa881 100644 --- a/common/src/main/scala/net/psforever/objects/serverobject/structures/Building.scala +++ b/common/src/main/scala/net/psforever/objects/serverobject/structures/Building.scala @@ -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) } diff --git a/common/src/main/scala/net/psforever/objects/serverobject/structures/BuildingControl.scala b/common/src/main/scala/net/psforever/objects/serverobject/structures/BuildingControl.scala index f2daedbaa..27c9ccbed 100644 --- a/common/src/main/scala/net/psforever/objects/serverobject/structures/BuildingControl.scala +++ b/common/src/main/scala/net/psforever/objects/serverobject/structures/BuildingControl.scala @@ -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}") } diff --git a/common/src/main/scala/net/psforever/objects/serverobject/terminals/CaptureTerminal.scala b/common/src/main/scala/net/psforever/objects/serverobject/terminals/CaptureTerminal.scala new file mode 100644 index 000000000..74ec2df2e --- /dev/null +++ b/common/src/main/scala/net/psforever/objects/serverobject/terminals/CaptureTerminal.scala @@ -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 + } +} + diff --git a/common/src/main/scala/net/psforever/objects/serverobject/terminals/CaptureTerminalControl.scala b/common/src/main/scala/net/psforever/objects/serverobject/terminals/CaptureTerminalControl.scala new file mode 100644 index 000000000..f3bb0b986 --- /dev/null +++ b/common/src/main/scala/net/psforever/objects/serverobject/terminals/CaptureTerminalControl.scala @@ -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 + } +} diff --git a/common/src/main/scala/net/psforever/objects/serverobject/terminals/CaptureTerminalDefinition.scala b/common/src/main/scala/net/psforever/objects/serverobject/terminals/CaptureTerminalDefinition.scala new file mode 100644 index 000000000..4f654802d --- /dev/null +++ b/common/src/main/scala/net/psforever/objects/serverobject/terminals/CaptureTerminalDefinition.scala @@ -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") + } +} diff --git a/common/src/main/scala/net/psforever/objects/serverobject/terminals/Terminal.scala b/common/src/main/scala/net/psforever/objects/serverobject/terminals/Terminal.scala index eb0c56a55..6aab6d0df 100644 --- a/common/src/main/scala/net/psforever/objects/serverobject/terminals/Terminal.scala +++ b/common/src/main/scala/net/psforever/objects/serverobject/terminals/Terminal.scala @@ -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 diff --git a/common/src/main/scala/net/psforever/objects/zones/InterstellarCluster.scala b/common/src/main/scala/net/psforever/objects/zones/InterstellarCluster.scala index a1b79e7ce..bb7314667 100644 --- a/common/src/main/scala/net/psforever/objects/zones/InterstellarCluster.scala +++ b/common/src/main/scala/net/psforever/objects/zones/InterstellarCluster.scala @@ -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"); } /** diff --git a/common/src/main/scala/net/psforever/packet/game/HackMessage.scala b/common/src/main/scala/net/psforever/packet/game/HackMessage.scala index 1966f05c6..72cc53bed 100644 --- a/common/src/main/scala/net/psforever/packet/game/HackMessage.scala +++ b/common/src/main/scala/net/psforever/packet/game/HackMessage.scala @@ -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, diff --git a/common/src/main/scala/net/psforever/packet/game/PlanetsideAttributeMessage.scala b/common/src/main/scala/net/psforever/packet/game/PlanetsideAttributeMessage.scala index 09b6d289e..c4b6640e5 100644 --- a/common/src/main/scala/net/psforever/packet/game/PlanetsideAttributeMessage.scala +++ b/common/src/main/scala/net/psforever/packet/game/PlanetsideAttributeMessage.scala @@ -51,6 +51,15 @@ import scodec.codecs._ * `17 - BEP. Value seems to be the same as BattleExperienceMessage`
* `18 - CEP.`
* `19 - Anchors. Value is 0 to disengage, 1 to engage.`
+ * `20 - Control console hacking. "The FactionName has hacked into BaseName` - also sets timer on CC and yellow base warning lights on
+ * * `24 - Learn certifications with value :`
* 01 : Medium Assault
* 02 : Heavy Assault
@@ -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)`
* Client to Server :
* `106 - Custom Head`
+ * `224 - Player/vehicle joins black ops`
+ * `228 - Player/vehicle leaves black ops`
*
* `Vehicles:`
* `10 - Driver seat permissions (0 = Locked, 1 = Group, 3 = Empire)`
diff --git a/common/src/main/scala/net/psforever/packet/game/UseItemMessage.scala b/common/src/main/scala/net/psforever/packet/game/UseItemMessage.scala index f7dd19ca8..be4eadad0 100644 --- a/common/src/main/scala/net/psforever/packet/game/UseItemMessage.scala +++ b/common/src/main/scala/net/psforever/packet/game/UseItemMessage.scala @@ -10,22 +10,26 @@ import scodec.codecs._ * (Where the child object was before it was moved is not specified or important.) * @see `Definition.ObjectId`
* `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, diff --git a/common/src/main/scala/services/local/LocalAction.scala b/common/src/main/scala/services/local/LocalAction.scala index 1c04f2e7b..b20c51436 100644 --- a/common/src/main/scala/services/local/LocalAction.scala +++ b/common/src/main/scala/services/local/LocalAction.scala @@ -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 } diff --git a/common/src/main/scala/services/local/LocalResponse.scala b/common/src/main/scala/services/local/LocalResponse.scala index fdc2aa37c..c2d997a57 100644 --- a/common/src/main/scala/services/local/LocalResponse.scala +++ b/common/src/main/scala/services/local/LocalResponse.scala @@ -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 } diff --git a/common/src/main/scala/services/local/LocalService.scala b/common/src/main/scala/services/local/LocalService.scala index 2d2570ce9..be44d0d8c 100644 --- a/common/src/main/scala/services/local/LocalService.scala +++ b/common/src/main/scala/services/local/LocalService.scala @@ -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") } diff --git a/common/src/main/scala/services/local/support/HackCapturerActor.scala b/common/src/main/scala/services/local/support/HackCapturerActor.scala new file mode 100644 index 000000000..a358ae52c --- /dev/null +++ b/common/src/main/scala/services/local/support/HackCapturerActor.scala @@ -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) +} \ No newline at end of file diff --git a/common/src/main/scala/services/local/support/HackClearActor.scala b/common/src/main/scala/services/local/support/HackClearActor.scala index 52a402804..e76d687a0 100644 --- a/common/src/main/scala/services/local/support/HackClearActor.scala +++ b/common/src/main/scala/services/local/support/HackClearActor.scala @@ -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) } diff --git a/common/src/test/scala/objects/ResourceSiloTest.scala b/common/src/test/scala/objects/ResourceSiloTest.scala index 958478df3..62bd39080 100644 --- a/common/src/test/scala/objects/ResourceSiloTest.scala +++ b/common/src/test/scala/objects/ResourceSiloTest.scala @@ -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) diff --git a/pslogin/src/main/scala/Maps.scala b/pslogin/src/main/scala/Maps.scala index 85208ccfe..4263cbffc 100644 --- a/pslogin/src/main/scala/Maps.scala +++ b/pslogin/src/main/scala/Maps.scala @@ -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) diff --git a/pslogin/src/main/scala/WorldSessionActor.scala b/pslogin/src/main/scala/WorldSessionActor.scala index 4d94c6af6..0e680447f 100644 --- a/pslogin/src/main/scala/WorldSessionActor.scala +++ b/pslogin/src/main/scala/WorldSessionActor.scala @@ -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