From c300bce0ff731fe5e77652fb4d7918213c4c14de Mon Sep 17 00:00:00 2001 From: Mazo Date: Wed, 27 Jan 2021 22:11:13 +0000 Subject: [PATCH] CaptureFlagManager + Supporting changes --- .../actors/session/SessionActor.scala | 167 +++++++++-- .../services/galaxy/GalaxyService.scala | 7 +- .../galaxy/GalaxyServiceMessage.scala | 3 +- .../galaxy/GalaxyServiceResponse.scala | 4 +- .../services/local/LocalService.scala | 101 +++++-- .../services/local/LocalServiceMessage.scala | 31 +- .../services/local/LocalServiceResponse.scala | 15 + .../local/support/CaptureFlagManager.scala | 282 ++++++++++++++++++ .../local/support/HackCaptureActor.scala | 103 ++++++- 9 files changed, 644 insertions(+), 69 deletions(-) create mode 100644 src/main/scala/net/psforever/services/local/support/CaptureFlagManager.scala diff --git a/src/main/scala/net/psforever/actors/session/SessionActor.scala b/src/main/scala/net/psforever/actors/session/SessionActor.scala index 42c6b365..27347c78 100644 --- a/src/main/scala/net/psforever/actors/session/SessionActor.scala +++ b/src/main/scala/net/psforever/actors/session/SessionActor.scala @@ -1,23 +1,11 @@ package net.psforever.actors.session -import akka.actor.typed import akka.actor.typed.receptionist.Receptionist import akka.actor.typed.scaladsl.adapter._ -import akka.actor.{Actor, ActorRef, Cancellable, MDCContextAware} +import akka.actor.{Actor, ActorRef, Cancellable, MDCContextAware, typed} import akka.pattern.ask import akka.util.Timeout - -import java.util.concurrent.TimeUnit import net.psforever.actors.net.MiddlewareActor -import net.psforever.services.ServiceManager.Lookup -import net.psforever.objects.locker.LockerContainer -import org.log4s.MDC - -import scala.collection.mutable -import scala.concurrent.{Await, Future} -import scala.concurrent.duration._ -import scala.concurrent.ExecutionContext.Implicits.global -import scala.util.Success import net.psforever.login.WorldSession._ import net.psforever.objects._ import net.psforever.objects.avatar._ @@ -29,7 +17,7 @@ import net.psforever.objects.entity.{SimpleWorldEntity, WorldEntity} import net.psforever.objects.equipment._ import net.psforever.objects.guid.{GUIDTask, Task, TaskResolver} import net.psforever.objects.inventory.{Container, InventoryItem} -import net.psforever.objects.serverobject.{CommonMessages, PlanetSideServerObject} +import net.psforever.objects.locker.LockerContainer import net.psforever.objects.serverobject.affinity.FactionAffinity import net.psforever.objects.serverobject.containable.Containable import net.psforever.objects.serverobject.damage.Damageable @@ -37,6 +25,7 @@ import net.psforever.objects.serverobject.deploy.Deployment import net.psforever.objects.serverobject.doors.Door import net.psforever.objects.serverobject.generator.Generator import net.psforever.objects.serverobject.hackable.Hackable +import net.psforever.objects.serverobject.llu.CaptureFlag import net.psforever.objects.serverobject.locks.IFFLock import net.psforever.objects.serverobject.mblocker.Locker import net.psforever.objects.serverobject.mount.Mountable @@ -44,14 +33,15 @@ import net.psforever.objects.serverobject.pad.VehicleSpawnPad import net.psforever.objects.serverobject.resourcesilo.ResourceSilo import net.psforever.objects.serverobject.structures.{Amenity, Building, StructureType, WarpGate} import net.psforever.objects.serverobject.terminals._ -import net.psforever.objects.serverobject.terminals.capture.{CaptureTerminal, CaptureTerminals} +import net.psforever.objects.serverobject.terminals.capture.CaptureTerminal import net.psforever.objects.serverobject.terminals.implant.ImplantTerminalMech import net.psforever.objects.serverobject.tube.SpawnTube import net.psforever.objects.serverobject.turret.{FacilityTurret, WeaponTurret} import net.psforever.objects.serverobject.zipline.ZipLinePath +import net.psforever.objects.serverobject.{CommonMessages, PlanetSideServerObject} import net.psforever.objects.teamwork.Squad -import net.psforever.objects.vehicles._ import net.psforever.objects.vehicles.Utility.InternalTelepad +import net.psforever.objects.vehicles._ import net.psforever.objects.vital._ import net.psforever.objects.vital.base._ import net.psforever.objects.vital.interaction.DamageInteraction @@ -59,14 +49,14 @@ import net.psforever.objects.vital.projectile.ProjectileReason import net.psforever.objects.zones.{Zone, ZoneHotSpotProjector, Zoning} import net.psforever.packet._ import net.psforever.packet.game.PlanetsideAttributeEnum.PlanetsideAttributeEnum -import net.psforever.packet.game.{HotSpotInfo => PacketHotSpotInfo, _} import net.psforever.packet.game.objectcreate._ -import net.psforever.services.ServiceManager.LookupResult +import net.psforever.packet.game.{HotSpotInfo => PacketHotSpotInfo, _} +import net.psforever.services.ServiceManager.{Lookup, LookupResult} import net.psforever.services.account.{AccountPersistenceService, PlayerToken, ReceiveAccountData, RetrieveAccountData} import net.psforever.services.avatar.{AvatarAction, AvatarResponse, AvatarServiceMessage, AvatarServiceResponse} import net.psforever.services.chat.ChatService import net.psforever.services.galaxy.{GalaxyAction, GalaxyResponse, GalaxyServiceMessage, GalaxyServiceResponse} -import net.psforever.services.local.support.{HackCaptureActor, RouterTelepadActivation} +import net.psforever.services.local.support.{CaptureFlagManager, HackCaptureActor, RouterTelepadActivation} import net.psforever.services.local.{LocalAction, LocalResponse, LocalServiceMessage, LocalServiceResponse} import net.psforever.services.properties.PropertyOverrideManager import net.psforever.services.support.SupportActor @@ -76,6 +66,13 @@ import net.psforever.services.{InterstellarClusterService, RemoverActor, Service import net.psforever.types._ import net.psforever.util.{Config, DefinitionUtil} import net.psforever.zones.Zones +import org.log4s.MDC + +import scala.collection.mutable +import scala.concurrent.ExecutionContext.Implicits.global +import scala.concurrent.duration._ +import scala.concurrent.{Await, Future} +import scala.util.Success object SessionActor { sealed trait Command @@ -204,6 +201,7 @@ class SessionActor(middlewareActor: typed.ActorRef[MiddlewareActor.Command], con var setupAvatarFunc: () => Unit = AvatarCreate var setCurrentAvatarFunc: Player => Unit = SetCurrentAvatarNormally var persist: () => Unit = NoPersistence + var specialItemSlotGuid : Option[PlanetSideGUID] = None // If a special item (e.g. LLU) has been attached to the player the GUID should be stored here, or cleared when dropped, since the drop hotkey doesn't send the GUID of the object to be dropped. /** * used during zone transfers to maintain reference to seated vehicle (which does not yet exist in the new zone) @@ -556,6 +554,9 @@ class SessionActor(middlewareActor: typed.ActorRef[MiddlewareActor.Command], con case GalaxyResponse.MapUpdate(msg) => sendResponse(msg) + case GalaxyResponse.FlagMapUpdate(msg) => + sendResponse(msg) + case GalaxyResponse.TransferPassenger(temp_channel, vehicle, vehicle_to_delete, manifest) => (manifest.passengers.find { case (name, _) => player.Name.equals(name) } match { case Some((name, index)) if vehicle.Seats(index).Occupant.isEmpty => @@ -971,6 +972,7 @@ class SessionActor(middlewareActor: typed.ActorRef[MiddlewareActor.Command], con CancelZoningProcess() PlayerActionsToCancel() CancelAllProximityUnits() + DropSpecialSlotItem() continent.Population ! Zone.Population.Release(avatar) response match { case Some((zone, spawnPoint)) => @@ -1828,25 +1830,32 @@ class SessionActor(middlewareActor: typed.ActorRef[MiddlewareActor.Command], con DropEquipmentFromInventory(player)(item) case None => ; } + + DropSpecialSlotItem() ToggleMaxSpecialState(enable = false) + keepAliveFunc = NormalKeepAlive zoningStatus = Zoning.Status.None deadState = DeadState.Dead + continent.GUID(mount) match { case Some(obj: Vehicle) => TotalDriverVehicleControl(obj) UnaccessContainer(obj) case _ => ; } + PlayerActionsToCancel() CancelAllProximityUnits() CancelZoningProcessWithDescriptiveReason("cancel") + if (shotsWhileDead > 0) { log.warn( s"KillPlayer/SHOTS_WHILE_DEAD: client of ${avatar.name} fired $shotsWhileDead rounds while character was dead on server" ) shotsWhileDead = 0 } + reviveTimer.cancel() if (player.death_by == 0) { reviveTimer = context.system.scheduler.scheduleOnce(respawnTimer) { @@ -2150,6 +2159,28 @@ class SessionActor(middlewareActor: typed.ActorRef[MiddlewareActor.Command], con } } + def DropSpecialSlotItem(): Unit = { + specialItemSlotGuid match { + case Some(guid: PlanetSideGUID) => + specialItemSlotGuid = None + continent.GUID(guid) match { + case Some(llu: CaptureFlag) => + llu.Carrier match { + case Some(carrier: Player) if carrier.GUID == player.GUID => + continent.LocalEvents ! CaptureFlagManager.DropFlag(llu) + case Some(carrier: Player) => + log.warn(s"${player.toString} tried to drop LLU, but it is currently held by ${carrier.toString}") + case None => + log.warn(s"${player.toString} tried to drop LLU, but nobody is holding it.") + } + case _ => + log.warn(s"${player.toString} Tried to drop a special item that wasn't recognized. GUID: $guid") + } + + case _ => ; // Nothing to drop, do nothing. + } + } + /** * Enforce constraints on bulk purchases as determined by a given player's previous purchase times and hard acquisition delays. * Intended to assist in sanitizing loadout information from the perspectvie of the player, or target owner. @@ -2298,6 +2329,40 @@ class SessionActor(middlewareActor: typed.ActorRef[MiddlewareActor.Command], con case LocalResponse.SendPlanetsideAttributeMessage(target_guid, attribute_number, attribute_value) => SendPlanetsideAttributeMessage(target_guid, attribute_number, attribute_value) + case LocalResponse.SendGenericObjectActionMessage(target_guid, action_number) => + sendResponse(GenericObjectActionMessage(target_guid, action_number)) + + case LocalResponse.SendGenericActionMessage(action_number) => + sendResponse(GenericActionMessage(action_number)) + + case LocalResponse.SendChatMsg(msg) => + sendResponse(msg) + + case LocalResponse.SendPacket(packet) => + sendResponse(packet) + + case LocalResponse.LluSpawned(llu) => + // Create LLU on client + sendResponse(ObjectCreateMessage( + llu.Definition.ObjectId, + llu.GUID, + llu.Definition.Packet.ConstructorData(llu).get + )) + + sendResponse(TriggerSoundMessage(TriggeredSound.LLUMaterialize, llu.Position, unk = 20, 0.8000001f)) + + case LocalResponse.LluDespawned(llu) => + sendResponse(TriggerSoundMessage(TriggeredSound.LLUDeconstruct, llu.Position, unk = 20, 0.8000001f)) + sendResponse(ObjectDeleteMessage(llu.GUID, 0)) + + // If the player was holding the LLU, remove it from their tracked special item slot + specialItemSlotGuid match { + case Some(guid) => + if (guid == llu.GUID) { + specialItemSlotGuid = None + } + case _ => ; + } case LocalResponse.ObjectDelete(object_guid, unk) => if (tplayer_guid != guid) { sendResponse(ObjectDeleteMessage(object_guid, unk)) @@ -4076,7 +4141,7 @@ class SessionActor(middlewareActor: typed.ActorRef[MiddlewareActor.Command], con case None => log.warn(s"DropItem: ${player.Name} wanted to drop a $anItem, but it wasn't at hand") } - case Some(obj) => //TODO LLU + case Some(obj) => log.warn(s"DropItem: ${player.Name} wanted to drop a $obj, but that isn't possible") case None => sendResponse(ObjectDeleteMessage(item_guid, 0)) //this is fine; item doesn't exist to the server anyway @@ -4662,7 +4727,16 @@ class SessionActor(middlewareActor: typed.ActorRef[MiddlewareActor.Command], con case Some(item) => CancelZoningProcessWithDescriptiveReason("cancel_use") captureTerminal.Actor ! CommonMessages.Use(player, Some(item)) - case _ => ; + case _ if specialItemSlotGuid.nonEmpty => + continent.GUID(specialItemSlotGuid) match { + case Some(llu: CaptureFlag) => + if (llu.Target.GUID == captureTerminal.Owner.GUID) { + continent.LocalEvents ! LocalServiceMessage(continent.id, LocalAction.LluCaptured(llu)) + } else { + log.info(s"LLU target is not this base. Target GUID: ${llu.Target.GUID} This base: ${captureTerminal.Owner.GUID}") + } + case _ => log.warn("Item in specialItemSlotGuid is not registered with continent or is not a LLU") + } } case Some(obj: FacilityTurret) => @@ -4884,6 +4958,19 @@ class SessionActor(middlewareActor: typed.ActorRef[MiddlewareActor.Command], con case None => ; } + case Some(obj: CaptureFlag) => + // LLU can normally only be picked up the faction that owns it + if (specialItemSlotGuid.isEmpty) { + if(obj.Faction == player.Faction) { + specialItemSlotGuid = Some(obj.GUID) + continent.LocalEvents ! CaptureFlagManager.PickupFlag(obj, player) + } else { + log.warn(s"Player ${player.toString} tried to pick up LLU ${obj.GUID} - ${obj.Faction} that doesn't belong to their faction") + } + } else if(specialItemSlotGuid.get != obj.GUID) { // Ignore duplicate pickup requests + log.warn(s"Player ${player.toString} tried to pick up LLU ${obj.GUID} - ${obj.Faction} but their special slot already contains $specialItemSlotGuid") + } + case Some(obj) => CancelZoningProcessWithDescriptiveReason("cancel_use") log.warn(s"UseItem: don't know how to handle $obj") @@ -4989,6 +5076,9 @@ class SessionActor(middlewareActor: typed.ActorRef[MiddlewareActor.Command], con log.info(s"${player.Name} is back") player.AwayFromKeyboard = false } + if (action == GenericActionEnum.DropSpecialItem.id) { + DropSpecialSlotItem() + } if (action == 15) { //max deployment log.info(s"GenericObject: $player is anchored") player.UsingSpecial = SpecialExoSuitDefinition.Mode.Anchored @@ -6657,11 +6747,13 @@ class SessionActor(middlewareActor: typed.ActorRef[MiddlewareActor.Command], con //sync model access state sendResponse(PlanetsideAttributeMessage(amenityId, 50, 0)) sendResponse(PlanetsideAttributeMessage(amenityId, 51, 0)) + //sync damageable, if val health = amenity.Health if (amenity.Definition.Damageable && health < amenity.MaxHealth) { sendResponse(PlanetsideAttributeMessage(amenityId, 0, health)) } + //sync special object type cases amenity match { case silo: ResourceSilo => @@ -6675,19 +6767,30 @@ class SessionActor(middlewareActor: typed.ActorRef[MiddlewareActor.Command], con case door: Door if door.isOpen => sendResponse(GenericObjectStateMsg(amenityId, 16)) - case _ => ; - } - //sync hack state - amenity match { case obj: Hackable if obj.HackedBy.nonEmpty => + //sync hack state amenity.Definition match { - case GlobalDefinitions.capture_terminal => - SendPlanetsideAttributeMessage( - amenity.GUID, - PlanetsideAttributeEnum.ControlConsoleHackUpdate, - HackCaptureActor.GetHackUpdateAttributeValue(amenity.asInstanceOf[CaptureTerminal], isResecured = false)) - case _ => - HackObject(amenity.GUID, 1114636288L, 8L) //generic hackable object + case GlobalDefinitions.capture_terminal => + SendPlanetsideAttributeMessage( + amenity.GUID, + PlanetsideAttributeEnum.ControlConsoleHackUpdate, + HackCaptureActor.GetHackUpdateAttributeValue(amenity.asInstanceOf[CaptureTerminal], isResecured = false)) + case _ => + HackObject(amenity.GUID, 1114636288L, 8L) //generic hackable object + } + + // sync capture flags + case llu: CaptureFlag => + // Create LLU + sendResponse(ObjectCreateMessage( + llu.Definition.ObjectId, + llu.GUID, + llu.Definition.Packet.ConstructorData(llu).get + )) + + // Attach it to a player if it has a carrier + if (llu.Carrier.nonEmpty) { + continent.LocalEvents ! LocalServiceMessage(continent.id, LocalAction.SendPacket(ObjectAttachMessage(llu.Carrier.get.GUID, llu.GUID, 252))) } case _ => ; } diff --git a/src/main/scala/net/psforever/services/galaxy/GalaxyService.scala b/src/main/scala/net/psforever/services/galaxy/GalaxyService.scala index 678f99a5..be6e6cb2 100644 --- a/src/main/scala/net/psforever/services/galaxy/GalaxyService.scala +++ b/src/main/scala/net/psforever/services/galaxy/GalaxyService.scala @@ -3,7 +3,7 @@ package net.psforever.services.galaxy import akka.actor.Actor import net.psforever.objects.zones.Zone -import net.psforever.packet.game.BuildingInfoUpdateMessage +import net.psforever.packet.game.{BuildingInfoUpdateMessage, FlagInfo} import net.psforever.services.{GenericEventBus, Service} class GalaxyService extends Actor { @@ -53,6 +53,11 @@ class GalaxyService extends Actor { GalaxyServiceResponse(s"/Galaxy", GalaxyResponse.MapUpdate(msg)) ) + case GalaxyAction.FlagMapUpdate(msg) => + GalaxyEvents.publish( + GalaxyServiceResponse(s"/Galaxy", GalaxyResponse.FlagMapUpdate(msg)) + ) + case GalaxyAction.TransferPassenger(player_guid, temp_channel, vehicle, vehicle_to_delete, manifest) => GalaxyEvents.publish( GalaxyServiceResponse( diff --git a/src/main/scala/net/psforever/services/galaxy/GalaxyServiceMessage.scala b/src/main/scala/net/psforever/services/galaxy/GalaxyServiceMessage.scala index 28c27205..6f369bdb 100644 --- a/src/main/scala/net/psforever/services/galaxy/GalaxyServiceMessage.scala +++ b/src/main/scala/net/psforever/services/galaxy/GalaxyServiceMessage.scala @@ -3,7 +3,7 @@ package net.psforever.services.galaxy import net.psforever.objects.Vehicle import net.psforever.objects.vehicles.VehicleManifest -import net.psforever.packet.game.BuildingInfoUpdateMessage +import net.psforever.packet.game.{BuildingInfoUpdateMessage, CaptureFlagUpdateMessage, FlagInfo} import net.psforever.types.PlanetSideGUID final case class GalaxyServiceMessage(forChannel: String, actionMessage: GalaxyAction.Action) @@ -16,6 +16,7 @@ object GalaxyAction { trait Action final case class MapUpdate(msg: BuildingInfoUpdateMessage) extends Action + final case class FlagMapUpdate(msg: CaptureFlagUpdateMessage) extends Action final case class TransferPassenger( player_guid: PlanetSideGUID, diff --git a/src/main/scala/net/psforever/services/galaxy/GalaxyServiceResponse.scala b/src/main/scala/net/psforever/services/galaxy/GalaxyServiceResponse.scala index 8bdb55e4..ceebb9d4 100644 --- a/src/main/scala/net/psforever/services/galaxy/GalaxyServiceResponse.scala +++ b/src/main/scala/net/psforever/services/galaxy/GalaxyServiceResponse.scala @@ -4,7 +4,7 @@ package net.psforever.services.galaxy import net.psforever.objects.Vehicle import net.psforever.objects.vehicles.VehicleManifest import net.psforever.objects.zones.HotSpotInfo -import net.psforever.packet.game.BuildingInfoUpdateMessage +import net.psforever.packet.game.{BuildingInfoUpdateMessage, CaptureFlagUpdateMessage, FlagInfo} import net.psforever.types.PlanetSideGUID import net.psforever.services.GenericEventBusMsg @@ -16,6 +16,8 @@ object GalaxyResponse { final case class HotSpotUpdate(zone_id: Int, priority: Int, host_spot_info: List[HotSpotInfo]) extends Response final case class MapUpdate(msg: BuildingInfoUpdateMessage) extends Response + final case class FlagMapUpdate(msg: CaptureFlagUpdateMessage) extends Response + final case class TransferPassenger( temp_channel: String, diff --git a/src/main/scala/net/psforever/services/local/LocalService.scala b/src/main/scala/net/psforever/services/local/LocalService.scala index 8fc58145..3348f6f0 100644 --- a/src/main/scala/net/psforever/services/local/LocalService.scala +++ b/src/main/scala/net/psforever/services/local/LocalService.scala @@ -2,34 +2,26 @@ package net.psforever.services.local import akka.actor.{Actor, ActorRef, Props} -import akka.pattern.Patterns -import akka.util.Timeout -import net.psforever.actors.zone.{BuildingActor, ZoneActor} -import net.psforever.objects.ce.Deployable -import net.psforever.objects.serverobject.structures.{Amenity, Building} -import net.psforever.objects.serverobject.terminals.Terminal -import net.psforever.objects.zones.Zone import net.psforever.objects._ -import net.psforever.packet.game.{PlanetsideAttributeEnum, TriggeredEffect, TriggeredEffectLocation} +import net.psforever.objects.ce.Deployable +import net.psforever.objects.serverobject.terminals.Terminal +import net.psforever.objects.vehicles.{Utility, UtilityType} import net.psforever.objects.vital.Vitality -import net.psforever.types.{PlanetSideGUID, Vector3} -import net.psforever.services.local.support._ +import net.psforever.objects.zones.Zone +import net.psforever.packet.game.{TriggeredEffect, TriggeredEffectLocation} +import net.psforever.services.local.support.{CaptureFlagManager, _} +import net.psforever.services.support.SupportActor import net.psforever.services.vehicle.{VehicleAction, VehicleServiceMessage} import net.psforever.services.{GenericEventBus, RemoverActor, Service} +import net.psforever.types.{PlanetSideGUID, Vector3} -import scala.concurrent.duration._ -import net.psforever.objects.serverobject.hackable.Hackable -import net.psforever.objects.vehicles.{Utility, UtilityType} -import net.psforever.services.support.SupportActor - -import java.util.concurrent.TimeUnit -import scala.concurrent.Await -import scala.concurrent.duration.Duration +import scala.concurrent.duration.{Duration, _} class LocalService(zone: Zone) extends Actor { private val doorCloser = context.actorOf(Props[DoorCloseActor](), s"${zone.id}-local-door-closer") private val hackClearer = context.actorOf(Props[HackClearActor](), s"${zone.id}-local-hack-clearer") - private val hackCapturer = context.actorOf(Props[HackCaptureActor](), s"${zone.id}-local-hack-capturer") + private val hackCapturer = context.actorOf(Props(classOf[HackCaptureActor], zone.tasks), s"${zone.id}-local-hack-capturer") + private val captureFlagManager = context.actorOf(Props(classOf[CaptureFlagManager], zone.tasks, zone), s"${zone.id}-local-capture-flag-manager") private val engineer = context.actorOf(Props(classOf[DeployableRemover], zone.tasks), s"${zone.id}-deployable-remover-agent") private val teleportDeployment: ActorRef = context.actorOf(Props[RouterTelepadActivation](), s"${zone.id}-telepad-activate-agent") @@ -102,10 +94,43 @@ class LocalService(zone: Zone) extends Actor { ) case LocalAction.ClearTemporaryHack(_, target) => hackClearer ! HackClearActor.ObjectIsResecured(target) + case LocalAction.ResecureCaptureTerminal(target) => hackCapturer ! HackCaptureActor.ResecureCaptureTerminal(target, zone) case LocalAction.StartCaptureTerminalHack(target) => hackCapturer ! HackCaptureActor.StartCaptureTerminalHack(target, zone, 0, 8L) + case LocalAction.LluCaptured(llu) => + hackCapturer ! HackCaptureActor.FlagCaptured(llu) + + case LocalAction.LluSpawned(player_guid, llu) => + // Forward to all clients to create object locally + LocalEvents.publish( + LocalServiceResponse( + s"/$forChannel/Local", + player_guid, + LocalResponse.LluSpawned(llu) + ) + ) + + case LocalAction.LluDespawned(player_guid, llu) => + // Forward to all clients to destroy object locally + LocalEvents.publish( + LocalServiceResponse( + s"/$forChannel/Local", + player_guid, + LocalResponse.LluDespawned(llu) + ) + ) + + case LocalAction.SendPacket(packet) => + LocalEvents.publish( + LocalServiceResponse( + s"/$forChannel/Local", + PlanetSideGUID(-1), + LocalResponse.SendPacket(packet) + ) + ) + case LocalAction.SendPlanetsideAttributeMessage(player_guid, target_guid, attribute_number, attribute_value) => LocalEvents.publish( LocalServiceResponse( @@ -114,6 +139,34 @@ class LocalService(zone: Zone) extends Actor { LocalResponse.SendPlanetsideAttributeMessage(target_guid, attribute_number, attribute_value) ) ) + + case LocalAction.SendGenericObjectActionMessage(player_guid, target_guid, action_number) => + LocalEvents.publish( + LocalServiceResponse( + s"/$forChannel/Local", + player_guid, + LocalResponse.SendGenericObjectActionMessage(target_guid, action_number) + ) + ) + + case LocalAction.SendChatMsg(player_guid, msg) => + LocalEvents.publish( + LocalServiceResponse( + s"/$forChannel/Local", + player_guid, + LocalResponse.SendChatMsg(msg) + ) + ) + + case LocalAction.SendGenericActionMessage(player_guid, action_number) => + LocalEvents.publish( + LocalServiceResponse( + s"/$forChannel/Local", + player_guid, + LocalResponse.SendGenericActionMessage(action_number) + ) + ) + case LocalAction.RouterTelepadTransport(player_guid, passenger_guid, src_guid, dest_guid) => LocalEvents.publish( LocalServiceResponse( @@ -338,6 +391,16 @@ class LocalService(zone: Zone) extends Actor { val cause = damage_func(target) sender() ! Vitality.DamageResolution(target, cause) + // Forward all CaptureFlagManager messages + case msg @ + (CaptureFlagManager.SpawnCaptureFlag(_, _, _) + | CaptureFlagManager.PickupFlag(_, _) + | CaptureFlagManager.DropFlag(_) + | CaptureFlagManager.Captured(_) + | CaptureFlagManager.Lost(_, _) + | CaptureFlagManager) => + captureFlagManager.forward(msg) + case msg => log.warn(s"Unhandled message $msg from ${sender()}") } diff --git a/src/main/scala/net/psforever/services/local/LocalServiceMessage.scala b/src/main/scala/net/psforever/services/local/LocalServiceMessage.scala index e4ccd703..43fe2cf2 100644 --- a/src/main/scala/net/psforever/services/local/LocalServiceMessage.scala +++ b/src/main/scala/net/psforever/services/local/LocalServiceMessage.scala @@ -1,16 +1,20 @@ // Copyright (c) 2017 PSForever package net.psforever.services.local -import net.psforever.objects.{PlanetSideGameObject, TelepadDeployable, Vehicle} import net.psforever.objects.ce.Deployable 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.llu.CaptureFlag import net.psforever.objects.serverobject.terminals.capture.CaptureTerminal import net.psforever.objects.vehicles.Utility import net.psforever.objects.zones.Zone +import net.psforever.objects.{PlanetSideGameObject, TelepadDeployable, Vehicle} +import net.psforever.packet.PlanetSideGamePacket +import net.psforever.packet.game.GenericActionEnum.GenericActionEnum +import net.psforever.packet.game.GenericObjectActionEnum.GenericObjectActionEnum import net.psforever.packet.game.PlanetsideAttributeEnum.PlanetsideAttributeEnum -import net.psforever.packet.game.{DeployableInfo, DeploymentAction, TriggeredSound} +import net.psforever.packet.game.{ChatMsg, DeployableInfo, DeploymentAction, TriggeredSound} import net.psforever.types.{PlanetSideEmpire, PlanetSideGUID, Vector3} final case class LocalServiceMessage(forChannel: String, actionMessage: LocalAction.Action) @@ -46,14 +50,37 @@ object LocalAction { ) extends Action final case class ClearTemporaryHack(player_guid: PlanetSideGUID, target: PlanetSideServerObject with Hackable) extends Action + final case class ResecureCaptureTerminal(target: CaptureTerminal) extends Action final case class StartCaptureTerminalHack(target: CaptureTerminal) extends Action + final case class LluCaptured(llu: CaptureFlag) extends Action + final case class LluSpawned(player_guid: PlanetSideGUID, llu: CaptureFlag) extends Action + final case class LluDespawned(player_guid: PlanetSideGUID, llu: CaptureFlag) extends Action + + final case class SendPacket(packet: PlanetSideGamePacket) extends Action final case class SendPlanetsideAttributeMessage( player_guid: PlanetSideGUID, target: PlanetSideGUID, attribute_number: PlanetsideAttributeEnum, attribute_value: Long ) extends Action + + final case class SendGenericObjectActionMessage( + player_guid: PlanetSideGUID, + target: PlanetSideGUID, + action_number: GenericObjectActionEnum + ) extends Action + + final case class SendChatMsg( + player_guid: PlanetSideGUID, + msg: ChatMsg + ) extends Action + + final case class SendGenericActionMessage( + player_guid: PlanetSideGUID, + action_number: GenericActionEnum + ) extends Action + final case class RouterTelepadTransport( player_guid: PlanetSideGUID, passenger_guid: PlanetSideGUID, diff --git a/src/main/scala/net/psforever/services/local/LocalServiceResponse.scala b/src/main/scala/net/psforever/services/local/LocalServiceResponse.scala index 1b43cf9a..123fe140 100644 --- a/src/main/scala/net/psforever/services/local/LocalServiceResponse.scala +++ b/src/main/scala/net/psforever/services/local/LocalServiceResponse.scala @@ -3,8 +3,13 @@ package net.psforever.services.local import net.psforever.objects.{PlanetSideGameObject, TelepadDeployable, Vehicle} import net.psforever.objects.ce.Deployable +import net.psforever.objects.serverobject.llu.CaptureFlag +import net.psforever.objects.serverobject.structures.{AmenityOwner, Building} import net.psforever.objects.serverobject.terminals.{ProximityUnit, Terminal} import net.psforever.objects.vehicles.Utility +import net.psforever.packet.PlanetSideGamePacket +import net.psforever.packet.game.GenericActionEnum.GenericActionEnum +import net.psforever.packet.game.GenericObjectActionEnum.GenericObjectActionEnum import net.psforever.packet.game.PlanetsideAttributeEnum.PlanetsideAttributeEnum import net.psforever.packet.game._ import net.psforever.types.{PlanetSideEmpire, PlanetSideGUID, Vector3} @@ -31,8 +36,18 @@ object LocalResponse { ) extends Response final case class SendHackMessageHackCleared(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 SendPacket(packet: PlanetSideGamePacket) extends Response final case class SendPlanetsideAttributeMessage(target_guid: PlanetSideGUID, attribute_number: PlanetsideAttributeEnum, attribute_value: Long) extends Response + final case class SendGenericObjectActionMessage(target_guid: PlanetSideGUID, action_number: GenericObjectActionEnum) + extends Response + final case class SendChatMsg(msg: ChatMsg) extends Response + final case class SendGenericActionMessage(action_num: GenericActionEnum) extends Response + + final case class LluSpawned(llu: CaptureFlag) extends Response + final case class LluDespawned(llu: CaptureFlag) extends Response + final case class ObjectDelete(item_guid: PlanetSideGUID, unk: Int) extends Response final case class ProximityTerminalAction(terminal: Terminal with ProximityUnit, target: PlanetSideGameObject) extends Response diff --git a/src/main/scala/net/psforever/services/local/support/CaptureFlagManager.scala b/src/main/scala/net/psforever/services/local/support/CaptureFlagManager.scala new file mode 100644 index 00000000..9b12bde2 --- /dev/null +++ b/src/main/scala/net/psforever/services/local/support/CaptureFlagManager.scala @@ -0,0 +1,282 @@ +package net.psforever.services.local.support + +import akka.actor.{Actor, ActorRef, Cancellable} +import net.psforever.objects.{Default, Player} +import net.psforever.objects.guid.{GUIDTask, Task, TaskResolver} +import net.psforever.objects.serverobject.llu.CaptureFlag +import net.psforever.objects.serverobject.structures.Building +import net.psforever.objects.serverobject.terminals.capture.CaptureTerminal +import net.psforever.objects.zones.Zone +import net.psforever.packet.game._ +import net.psforever.services.ServiceManager +import net.psforever.services.ServiceManager.{Lookup, LookupResult} +import net.psforever.services.galaxy.{GalaxyAction, GalaxyServiceMessage} +import net.psforever.services.local.support.CaptureFlagLostReasonEnum.CaptureFlagLostReasonEnum +import net.psforever.services.local.{LocalAction, LocalServiceMessage} +import net.psforever.types.{ChatMessageType, PlanetSideEmpire, PlanetSideGUID, Vector3} + +import scala.concurrent.duration.DurationInt +import scala.util.Success + + +/** + * Responsible for handling capture flag related lifecycles + * @param taskResolver A reference to a zone's task resolver actor + */ +class CaptureFlagManager(val taskResolver: ActorRef, zone: Zone) extends Actor{ + private[this] val log = org.log4s.getLogger(self.path.name) + + var galaxyService: ActorRef = ActorRef.noSender + + private var mapUpdateTick: Cancellable = Default.Cancellable + + /** An internally tracked list of current flags, to avoid querying AmenityOwners each second for flag lookups */ + private var flags: List[CaptureFlag] = Nil + + private def TrackFlag(flag: CaptureFlag): Unit = { + flag.Owner.Amenities = flag + flags = flags :+ flag + + if (mapUpdateTick.isCancelled) { + // Start sending map updates periodically + import scala.concurrent.ExecutionContext.Implicits.global + mapUpdateTick = context.system.scheduler.scheduleAtFixedRate(0 seconds, 1 second, self, CaptureFlagManager.MapUpdate()) + } + } + + private def UntrackFlag(flag: CaptureFlag): Unit = { + flag.Owner.RemoveAmenity(flag) + flags = flags.filterNot(x => x == flag) + + if (flags.isEmpty) { + mapUpdateTick.cancel() + + // Send one final map update to clear the last flag from the map + self ! CaptureFlagManager.MapUpdate() + } + } + + val serviceManager = ServiceManager.serviceManager + serviceManager ! Lookup("galaxy") + + def receive: Receive = { + case LookupResult("galaxy", endpoint) => + galaxyService = endpoint + + case CaptureFlagManager.MapUpdate() => + val flagInfo = flags.map(flag => + FlagInfo( + u1 = 0, + owner_map_id = flag.Owner.asInstanceOf[Building].MapId, + target_map_id = flag.Target.MapId, + x = flag.Position.x, + y = flag.Position.y, + u6 = 0, + isMonolithUnit = false + ) + ) + + galaxyService ! GalaxyServiceMessage(GalaxyAction.FlagMapUpdate(CaptureFlagUpdateMessage(zone.Number, flagInfo))) + + case CaptureFlagManager.SpawnCaptureFlag(capture_terminal, target, hackingFaction) => + val zone = capture_terminal.Zone + val socket = capture_terminal.Owner.asInstanceOf[Building].GetFlagSocket.get + + // Override CC message when looked at + zone.LocalEvents ! LocalServiceMessage( + zone.id, + LocalAction.SendGenericObjectActionMessage( + PlanetSideGUID(-1), + capture_terminal.GUID, + GenericObjectActionEnum.FlagSpawned + ) + ) + + // Register LLU object create task and callback to create on clients + val flag: CaptureFlag = CaptureFlag.Constructor( + Vector3(socket.Position.x, socket.Position.y, socket.Position.z - 1), + socket.Orientation, + target, + socket.Owner, + hackingFaction + ) + + // Add the flag as an amenity and track it internally + TrackFlag(flag) + + taskResolver ! CallBackForTask( + TaskResolver.GiveTask(GUIDTask.RegisterObjectTask(flag)(socket.Zone.GUID).task), + socket.Zone.LocalEvents, + LocalServiceMessage( + socket.Zone.id, + LocalAction.LluSpawned(PlanetSideGUID(-1), flag) + ) + ) + + // Broadcast chat message for LLU spawn + val owner = flag.Owner.asInstanceOf[Building] + ChatBroadcast(flag.Zone, CaptureFlagChatMessageStrings.CTF_FlagSpawned(owner, flag.Target)) + + case CaptureFlagManager.Captured(flag: CaptureFlag) => + // Trigger Install sound + flag.Zone.LocalEvents ! LocalServiceMessage(flag.Zone.id, LocalAction.TriggerSound(PlanetSideGUID(-1), TriggeredSound.LLUInstall, flag.Target.CaptureTerminal.get.Position, 20, 0.8000001f)) + + // Broadcast capture chat message + ChatBroadcast(flag.Zone, CaptureFlagChatMessageStrings.CTF_Success(flag.Carrier.get, flag.Owner.asInstanceOf[Building].Name)) + + // Despawn flag + HandleFlagDespawn(flag) + + case CaptureFlagManager.Lost(flag: CaptureFlag, reason: CaptureFlagLostReasonEnum) => + val message = reason match { + case CaptureFlagLostReasonEnum.Resecured => + CaptureFlagChatMessageStrings.CTF_Failed_SourceResecured(flag.Owner.asInstanceOf[Building]) + case CaptureFlagLostReasonEnum.TimedOut => + CaptureFlagChatMessageStrings.CTF_Failed_TimedOut(flag.Owner.asInstanceOf[Building].Name, flag.Target) + } + + ChatBroadcast(flag.Zone, message) + + HandleFlagDespawn(flag) + + case CaptureFlagManager.PickupFlag(flag: CaptureFlag, player: Player) => + val continent = flag.Zone + + flag.Carrier = Some(player) + + continent.LocalEvents ! LocalServiceMessage(continent.id, LocalAction.SendPacket(ObjectAttachMessage(player.GUID, flag.GUID, 252))) + continent.LocalEvents ! LocalServiceMessage(continent.id, LocalAction.TriggerSound(PlanetSideGUID(-1), TriggeredSound.LLUPickup, player.Position, 15, volume = 0.8f)) + + ChatBroadcast(flag.Zone, CaptureFlagChatMessageStrings.CTF_FlagPickedUp(player, flag.Owner.asInstanceOf[Building].Name), fanfare = false) + + case CaptureFlagManager.DropFlag(flag: CaptureFlag) => + flag.Carrier match { + case Some(player: Player) => + // Set the flag position to where the player is that dropped it + flag.Position = player.Position + + // Remove attached player from flag + flag.Carrier = None + + // Send drop packet + flag.Zone.LocalEvents ! LocalServiceMessage(flag.Zone.id, LocalAction.SendPacket(ObjectDetachMessage(player.GUID, flag.GUID, player.Position, 0, 0, 0))) + + // Send dropped chat message + ChatBroadcast(flag.Zone, CaptureFlagChatMessageStrings.CTF_FlagDropped(player, flag.Owner.asInstanceOf[Building].Name), fanfare = false) + + case None => + log.warn("Tried to drop flag but flag has no carrier") + } + + case _ => + log.warn("Received unhandled message"); + } + + private def HandleFlagDespawn(flag: CaptureFlag): Unit = { + // Unregister LLU from clients, + flag.Zone.LocalEvents ! LocalServiceMessage(flag.Zone.id, LocalAction.LluDespawned(PlanetSideGUID(-1), flag)) + + // Then unregister it from the GUID pool + taskResolver ! TaskResolver.GiveTask(GUIDTask.UnregisterObjectTask(flag)(flag.Zone.GUID).task) + + // Remove the flag as an amenity + UntrackFlag(flag) + } + + private def ChatBroadcast(zone: Zone, message: String, fanfare: Boolean = true): Unit = { + val messageType: ChatMessageType = if (fanfare) { + ChatMessageType.UNK_223 + } else { + ChatMessageType.UNK_229 + } + + zone.LocalEvents ! LocalServiceMessage( + zone.id, + LocalAction.SendChatMsg( + PlanetSideGUID(-1), + ChatMsg(messageType, wideContents = false, "", message, None) + ) + ) + } + + // Todo: Duplicate from SessionActor. Make common. + def CallBackForTask(task: TaskResolver.GiveTask, sendTo: ActorRef, pass: Any): TaskResolver.GiveTask = { + TaskResolver.GiveTask( + new Task() { + private val localDesc = task.task.Description + private val destination = sendTo + private val passMsg = pass + + override def Description: String = s"callback for tasking $localDesc" + + def Execute(resolver: ActorRef): Unit = { + destination ! passMsg + resolver ! Success(this) + } + }, + List(task) + ) + } +} + +object CaptureFlagManager { + final case class SpawnCaptureFlag(capture_terminal: CaptureTerminal, target: Building, hackingFaction: PlanetSideEmpire.Value) + final case class PickupFlag(flag: CaptureFlag, player: Player) + final case class DropFlag(flag: CaptureFlag) + final case class Captured(flag: CaptureFlag) + final case class Lost(flag: CaptureFlag, reason: CaptureFlagLostReasonEnum) + final case class MapUpdate() +} + +object CaptureFlagChatMessageStrings { + /* + @CTF_Failed_TargetLost=%1's LLU target facility %2 was lost!\nHack canceled! + @CTF_Failed_FlagLost=The %1 lost %2's LLU!\nHack canceled! + @CTF_Warning_Carrier=%1 of the %2 has %3's LLU.\nIt must be taken to %4 within %5 minutes! + @CTF_Warning_NoCarrier=%1's LLU is in the field.\nThe %2 must take it to %3 within %4 minutes! + @CTF_Warning_Carrier1Min=%1 of the %2 has %3's LLU.\nIt must be taken to %4 within the next minute! + @CTF_Warning_NoCarrier1Min=%1's LLU is in the field.\nThe %2 must take it to %3 within the next minute! + */ + + // @CTF_Success=%1 captured %2's LLU for the %3! + /** {player.Name} captured {owner_name}'s LLU for the {player.Faction}! */ + def CTF_Success(player: Player, owner_name: String): String = s"@CTF_Success^${player.Name}~^@$owner_name~^@${GetFactionString(player.Faction)}~" + + // @CTF_Failed_TimedOut=The %1 failed to deliver %2's LLU to %3 in time!\nHack canceled! + /** The {target.Faction} failed to deliver {owner_name}'s LLU to {target.Name} in time!\nHack canceled! */ + def CTF_Failed_TimedOut(owner_name: String, target: Building): String = s"@CTF_Failed_TimedOut^@${GetFactionString(target.Faction)}~^@$owner_name~^@${target.Name}~" + + // @CTF_Failed_SourceResecured=The %1 resecured %2!\nThe LLU was lost! + /** The {owner.Faction} resecured {owner.Name}!\nThe LLU was lost! */ + def CTF_Failed_SourceResecured(owner: Building): String = s"@CTF_Failed_SourceResecured^@${CaptureFlagChatMessageStrings.GetFactionString(owner.Faction)}~^@${owner.Name}~" + + + + // @CTF_FlagSpawned=%1 %2 has spawned a LLU.\nIt must be taken to %3 %4's Control Console within %5 minutes or the hack will fail! + /** {facilityType} {facilityName} has spawned a LLU.\nIt must be taken to {targetFacilityType} {targetFacilityName}'s Control Console within 15 minutes or the hack will fail! */ + def CTF_FlagSpawned(owner: Building, target: Building): String = s"@CTF_FlagSpawned^@${owner.Definition.Name}~^@${owner.Name}~^@${target.Definition.Name}~^@${target.Name}~^15~" + + + // @CTF_FlagPickedUp=%1 of the %2 picked up %3's LLU + /** {playerName} of the {faction} picked up {facilityName}'s LLU */ + def CTF_FlagPickedUp(player: Player, owner_name: String): String = s"@CTF_FlagPickedUp^${player.Name}~^@${CaptureFlagChatMessageStrings.GetFactionString(player.Faction)}~^@$owner_name~" + + // @CTF_FlagDropped=%1 of the %2 dropped %3's LLU + /** {playerName} of the {faction} dropped {facilityName}'s LLU */ + def CTF_FlagDropped(player: Player, owner_name: String): String = s"@CTF_FlagDropped^${player.Name}~^@${CaptureFlagChatMessageStrings.GetFactionString(player.Faction)}~^@$owner_name~" + + // todo: make private + private def GetFactionString(faction: PlanetSideEmpire.Value): String = { + faction match { + case PlanetSideEmpire.TR => "TerranRepublic" + case PlanetSideEmpire.NC => "NewConglomerate" + case PlanetSideEmpire.VS => "VanuSovereigncy" // Yes, this is wrong. It is like that in packet captures. + } + } +} + +object CaptureFlagLostReasonEnum extends Enumeration { + type CaptureFlagLostReasonEnum = Value + + val Resecured, TimedOut = Value +} \ No newline at end of file diff --git a/src/main/scala/net/psforever/services/local/support/HackCaptureActor.scala b/src/main/scala/net/psforever/services/local/support/HackCaptureActor.scala index 606d3990..c829871d 100644 --- a/src/main/scala/net/psforever/services/local/support/HackCaptureActor.scala +++ b/src/main/scala/net/psforever/services/local/support/HackCaptureActor.scala @@ -1,21 +1,26 @@ package net.psforever.services.local.support -import akka.actor.{Actor, Cancellable} +import akka.actor.{Actor, ActorRef, Cancellable} import net.psforever.actors.zone.{BuildingActor, ZoneActor} import net.psforever.objects.serverobject.CommonMessages import net.psforever.objects.serverobject.hackable.Hackable +import net.psforever.objects.serverobject.llu.CaptureFlag import net.psforever.objects.serverobject.structures.Building import net.psforever.objects.serverobject.terminals.capture.CaptureTerminal import net.psforever.objects.zones.Zone import net.psforever.objects.{Default, GlobalDefinitions} -import net.psforever.packet.game.PlanetsideAttributeEnum +import net.psforever.packet.game.{GenericActionEnum, PlanetsideAttributeEnum} import net.psforever.services.local.{LocalAction, LocalServiceMessage} import net.psforever.types.{PlanetSideEmpire, PlanetSideGUID} import java.util.concurrent.TimeUnit import scala.concurrent.duration.{FiniteDuration, _} +import scala.util.Random -class HackCaptureActor extends Actor { +/** + * Responsible for handling the aspects related to hacking control consoles and capturing bases. + */ +class HackCaptureActor(val taskResolver: ActorRef) extends Actor { private[this] val log = org.log4s.getLogger private var clearTrigger: Cancellable = Default.Cancellable @@ -61,6 +66,7 @@ class HackCaptureActor extends Actor { RestartTimer() NotifyHackStateChange(target, isResecured = false) + TrySpawnCaptureFlag(target) case HackCaptureActor.ProcessCompleteHacks() => log.trace("Processing complete hacks") @@ -75,7 +81,19 @@ class HackCaptureActor extends Actor { val hackedByFaction = entry.target.HackedBy.get.hackerFaction entry.target.Actor ! CommonMessages.ClearHack() - HackCompleted(entry.target, hackedByFaction) + entry.target.Owner.asInstanceOf[Building].GetFlagSocket match { + case Some(socket) => + // LLU was not delivered in time. Send resecured notifications + entry.target.Owner.asInstanceOf[Building].GetFlag match { + case Some(flag: CaptureFlag) => entry.target.Zone.LocalEvents ! CaptureFlagManager.Lost(flag, CaptureFlagLostReasonEnum.TimedOut) + case None => log.warn(s"Failed to find capture flag matching socket ${socket.GUID}") + } + + NotifyHackStateChange(entry.target, isResecured = true) + case None => + // Timed hack finished, capture the base + HackCompleted(entry.target, hackedByFaction) + } }) // If there's hacked objects left in the list restart the timer with the shortest hack time left @@ -84,32 +102,79 @@ class HackCaptureActor extends Actor { case HackCaptureActor.ResecureCaptureTerminal(target, _) => hackedObjects = hackedObjects.filterNot(x => x.target == target) + // If LLU exists it was not delivered. Send resecured notifications + target.Owner.asInstanceOf[Building].GetFlag match { + case Some(flag: CaptureFlag) => target.Zone.LocalEvents ! CaptureFlagManager.Lost(flag, CaptureFlagLostReasonEnum.Resecured) + case None => ; + } + NotifyHackStateChange(target, isResecured = true) // Restart the timer in case the object we just removed was the next one scheduled RestartTimer() + case HackCaptureActor.FlagCaptured(flag) => + log.warn(hackedObjects.toString()) + hackedObjects.find(_.target.GUID == flag.Owner.asInstanceOf[Building].CaptureTerminal.get.GUID) match { + case Some(entry) => + val hackedByFaction = entry.target.HackedBy.get.hackerFaction + hackedObjects = hackedObjects.filterNot(x => x == entry) + HackCompleted(entry.target, hackedByFaction) + + entry.target.Actor ! CommonMessages.ClearHack() + + flag.Zone.LocalEvents ! CaptureFlagManager.Captured(flag) + + // If there's hacked objects left in the list restart the timer with the shortest hack time left + RestartTimer() + case _ => + log.error(s"Attempted LLU capture for ${flag.Owner.asInstanceOf[Building].Name} but CC GUID ${flag.Owner.asInstanceOf[Building].CaptureTerminal.get.GUID} was not in list of hacked objects") + } + case _ => ; } - private def NotifyHackStateChange(target: CaptureTerminal, isResecured: Boolean): Unit = { - val attribute_value = HackCaptureActor.GetHackUpdateAttributeValue(target, isResecured) + private def TrySpawnCaptureFlag(terminal: CaptureTerminal): Unit = { + // Handle LLUs if the base contains a LLU socket + // If there are no neighbouring bases belonging to the hacking faction this will be handled as a regular timed hack (e.g. neutral base in enemy territory) + val owner = terminal.Owner.asInstanceOf[Building] + val hackingFaction = HackCaptureActor.GetHackingFaction(terminal).get + val hackingFactionNeighbourBases = owner.Neighbours(hackingFaction) + + hackingFactionNeighbourBases match { + case Some(neighbours) => + if(owner.IsCtfBase) { + // Find a random neighbouring base matching the hacking faction + val targetBase = neighbours.toVector((new Random).nextInt(neighbours.size)) + + // Request LLU is created by CaptureFlagActor via LocalService + terminal.Zone.LocalEvents ! CaptureFlagManager.SpawnCaptureFlag(terminal, targetBase, hackingFaction) + } + case None => + log.info("Couldn't find any neighbouring bases for LLU hack.") + } + } + + private def NotifyHackStateChange(terminal: CaptureTerminal, isResecured: Boolean): Unit = { + val attribute_value = HackCaptureActor.GetHackUpdateAttributeValue(terminal, isResecured) // Notify all clients that CC has been hacked - target.Zone.LocalEvents ! LocalServiceMessage( - target.Zone.id, + terminal.Zone.LocalEvents ! LocalServiceMessage( + terminal.Zone.id, LocalAction.SendPlanetsideAttributeMessage( PlanetSideGUID(-1), - target.GUID, + terminal.GUID, PlanetsideAttributeEnum.ControlConsoleHackUpdate, attribute_value ) ) + val owner = terminal.Owner.asInstanceOf[Building] + // Notify parent building that state has changed - target.Owner.Actor ! BuildingActor.AmenityStateChange(target, Some(isResecured)) + owner.Actor ! BuildingActor.AmenityStateChange(terminal, Some(isResecured)) // Push map update to clients - target.Owner.asInstanceOf[Building].Zone.actor ! ZoneActor.ZoneMapUpdate() + owner.Zone.actor ! ZoneActor.ZoneMapUpdate() } private def HackCompleted(terminal: CaptureTerminal with Hackable, hackedByFaction: PlanetSideEmpire.Value): Unit = { @@ -117,6 +182,9 @@ class HackCaptureActor extends Actor { if (building.NtuLevel > 0) { log.info(s"Setting base ${building.GUID} / MapId: ${building.MapId} as owned by $hackedByFaction") building.Actor! BuildingActor.SetFaction(hackedByFaction) + + // todo: This should probably only go to those within the captured SOI who belong to the capturing faction + building.Zone.LocalEvents ! LocalServiceMessage(building.Zone.id, LocalAction.SendGenericActionMessage(PlanetSideGUID(-1), GenericActionEnum.BaseCaptureFanfare)) } else { log.info("Base hack completed, but base was out of NTU.") } @@ -160,6 +228,7 @@ object HackCaptureActor { ) final case class ResecureCaptureTerminal(target: CaptureTerminal, zone: Zone) + final case class FlagCaptured(flag: CaptureFlag) private final case class ProcessCompleteHacks() @@ -172,11 +241,19 @@ object HackCaptureActor { hack_timestamp: Long ) - def GetHackUpdateAttributeValue(target: CaptureTerminal, isResecured: Boolean): Long = { + def GetHackingFaction(terminal: CaptureTerminal): Option[PlanetSideEmpire.Value] = { + terminal.HackedBy match { + case Some(Hackable.HackInfo(_, _, hackingFaction, _, _, _)) => + Some(hackingFaction) + case _ => None + } + } + + def GetHackUpdateAttributeValue(terminal: CaptureTerminal, isResecured: Boolean): Long = { if (isResecured) { 17039360L } else { - target.HackedBy match { + terminal.HackedBy match { case Some(Hackable.HackInfo(_, _, hackingFaction, _, start, length)) => // See PlanetSideAttributeMessage #20 documentation for an explanation of how the timer is calculated val hack_time_remaining_ms =