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 09556202e..100e4b54b 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 @@ -69,12 +69,13 @@ class ResourceSiloControl(resourceSilo : ResourceSilo) extends Actor with Factio self ! ResourceSilo.LowNtuWarning(enabled = true) } - val building = resourceSilo.Owner + val building = resourceSilo.Owner.asInstanceOf[Building] val zone = building.Zone if(resourceSilo.ChargeLevel == 0 && siloChargeBeforeChange > 0) { // Oops, someone let the base run out of power. Shut it all down. //todo: Make base neutral if silo hits zero NTU zone.AvatarEvents ! AvatarServiceMessage(zone.Id, AvatarAction.PlanetsideAttribute(building.GUID, 48, 1)) + building.TriggerZoneMapUpdate() } else if (siloChargeBeforeChange == 0 && resourceSilo.ChargeLevel > 0) { // Power restored. Reactor Online. Sensors Online. Weapons Online. All systems nominal. //todo: Check generator is online before starting up @@ -82,6 +83,7 @@ class ResourceSiloControl(resourceSilo : ResourceSilo) extends Actor with Factio zone.Id, AvatarAction.PlanetsideAttribute(building.GUID, 48, 0) ) + building.TriggerZoneMapUpdate() } case _ => ; } 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 bb9e40199..6dcd285d6 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 @@ -3,7 +3,7 @@ package net.psforever.objects.serverobject.structures import java.util.concurrent.TimeUnit -import akka.actor.ActorContext +import akka.actor.{ActorContext, ActorRef} import net.psforever.objects.{GlobalDefinitions, Player} import net.psforever.objects.definition.ObjectDefinition import net.psforever.objects.serverobject.hackable.Hackable @@ -13,6 +13,7 @@ import net.psforever.objects.serverobject.tube.SpawnTube import net.psforever.objects.zones.Zone import net.psforever.packet.game._ import net.psforever.types.{PlanetSideEmpire, Vector3} +import scalax.collection.{Graph, GraphEdge} class Building(private val name: String, private val building_guid : Int, @@ -40,6 +41,7 @@ class Building(private val name: String, override def Faction_=(fac : PlanetSideEmpire.Value) : PlanetSideEmpire.Value = { faction = fac + TriggerZoneMapUpdate() Faction } @@ -66,6 +68,20 @@ class Building(private val name: String, } } + def NtuLevel : Int = { + //if we have a silo, get the NTU level + Amenities.find(_.Definition == GlobalDefinitions.resource_silo) match { + case Some(obj: ResourceSilo) => + obj.CapacitorDisplay.toInt + case _ => //we have no silo; we have unlimited power + 10 + } + } + + def TriggerZoneMapUpdate(): Unit = { + if(Actor != ActorRef.noSender) Actor ! Building.TriggerZoneMapUpdate(Zone.Number) + } + // Get all lattice neighbours matching the specified faction def Neighbours(faction: PlanetSideEmpire.Value): Option[Set[Building]] = { this.Neighbours match { @@ -86,13 +102,7 @@ class Building(private val name: String, Int, Option[Additional3], Boolean, Boolean ) = { - //if we have a silo, get the NTU level - val ntuLevel : Int = Amenities.find(_.Definition == GlobalDefinitions.resource_silo) match { - case Some(obj: ResourceSilo) => - obj.CapacitorDisplay.toInt - case _ => //we have no silo; we have unlimited power - 10 - } + val ntuLevel : Int = NtuLevel //if we have a capture terminal, get the hack status & time (in milliseconds) from control console if it exists val (hacking, hackingFaction, hackTime) : (Boolean, PlanetSideEmpire.Value, Long) = Amenities.find(_.Definition == GlobalDefinitions.capture_terminal) match { case Some(obj: CaptureTerminal with Hackable) => @@ -118,6 +128,46 @@ class Building(private val name: String, (true, false) } } + + val latticeBenefit : Int = { + if(Faction == PlanetSideEmpire.NEUTRAL) 0 + else { + def FindLatticeBenefit(wantedBenefit: ObjectDefinition, subGraph: Graph[Building, GraphEdge.UnDiEdge]): Boolean = { + var found = false + + subGraph find this match { + case Some(self) => + if (this.Definition == wantedBenefit) found = true + else { + self pathUntil (_.Definition == wantedBenefit) match { + case Some(_) => found = true + case None => ; + } + } + case None => ; + } + + found + } + + // Check this Building is on the lattice first + zone.Lattice find this match { + case Some(_) => + // todo: generator destruction state + val subGraph = Zone.Lattice filter ((b: Building) => b.Faction == this.Faction && !b.CaptureConsoleIsHacked && b.NtuLevel > 0) + + var stackedBenefit = 0 + if (FindLatticeBenefit(GlobalDefinitions.amp_station, subGraph)) stackedBenefit |= 1 + if (FindLatticeBenefit(GlobalDefinitions.comm_station_dsp, subGraph)) stackedBenefit |= 2 + if (FindLatticeBenefit(GlobalDefinitions.cryo_facility, subGraph)) stackedBenefit |= 4 + if (FindLatticeBenefit(GlobalDefinitions.comm_station, subGraph)) stackedBenefit |= 8 + if (FindLatticeBenefit(GlobalDefinitions.tech_plant, subGraph)) stackedBenefit |= 16 + + stackedBenefit + case None => 0; + } + } + } //out ( ntuLevel, @@ -130,7 +180,7 @@ class Building(private val name: String, generatorState, spawnTubesNormal, false, //force_dome_active - 0, //lattice_benefit + latticeBenefit, 0, //cavern_benefit; !! Field > 0 will cause malformed packet. See class def. Nil, 0, @@ -196,4 +246,5 @@ object Building { } final case class SendMapUpdate(all_clients: Boolean) + final case class TriggerZoneMapUpdate(zone_num: Int) } 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 19e1a26b3..8bd88709e 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 @@ -3,6 +3,7 @@ package net.psforever.objects.serverobject.structures import akka.actor.{Actor, ActorRef} import net.psforever.objects.serverobject.affinity.{FactionAffinity, FactionAffinityBehavior} +import net.psforever.objects.zones.InterstellarCluster import net.psforever.packet.game.BuildingInfoUpdateMessage import services.ServiceManager import services.ServiceManager.Lookup @@ -11,24 +12,31 @@ import services.galaxy.{GalaxyAction, GalaxyResponse, GalaxyServiceMessage, Gala class BuildingControl(building : Building) extends Actor with FactionAffinityBehavior.Check { def FactionObject : FactionAffinity = building var galaxyService : ActorRef = Actor.noSender + var interstellarCluster : ActorRef = Actor.noSender private[this] val log = org.log4s.getLogger override def preStart = { log.trace(s"Starting BuildingControl for ${building.GUID} / ${building.MapId}") ServiceManager.serviceManager ! Lookup("galaxy") + ServiceManager.serviceManager ! Lookup("cluster") } def receive : Receive = checkBehavior.orElse { case ServiceManager.LookupResult("galaxy", endpoint) => galaxyService = endpoint log.trace("BuildingControl: Building " + building.GUID + " Got galaxy service " + endpoint) + case ServiceManager.LookupResult("cluster", endpoint) => + interstellarCluster = endpoint + log.trace("BuildingControl: Building " + building.GUID + " Got interstellar cluster 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.TriggerZoneMapUpdate(zone_num: Int) => + if(interstellarCluster != ActorRef.noSender) interstellarCluster ! InterstellarCluster.ZoneMapUpdate(zone_num) case Building.SendMapUpdate(all_clients: Boolean) => val zoneNumber = building.Zone.Number val buildingNumber = building.MapId @@ -57,7 +65,7 @@ class BuildingControl(building : Building) extends Actor with FactionAffinityBeh ) if(all_clients) { - galaxyService ! GalaxyServiceMessage(GalaxyAction.MapUpdate(msg)) + if(galaxyService != ActorRef.noSender) galaxyService ! GalaxyServiceMessage(GalaxyAction.MapUpdate(msg)) } else { // Fake a GalaxyServiceResponse response back to just the sender sender ! GalaxyServiceResponse("", GalaxyResponse.MapUpdate(msg)) 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 a85c784a5..e47eea1b0 100644 --- a/common/src/main/scala/net/psforever/objects/zones/InterstellarCluster.scala +++ b/common/src/main/scala/net/psforever/objects/zones/InterstellarCluster.scala @@ -2,6 +2,7 @@ package net.psforever.objects.zones import akka.actor.{Actor, Props} +import net.psforever.objects.serverobject.structures.Building import scala.annotation.tailrec @@ -69,6 +70,9 @@ class InterstellarCluster(zones : List[Zone]) extends Actor { case None => //zone_number does not exist sender ! Zone.Lattice.NoValidSpawnPoint(zone_number, None) } + case InterstellarCluster.ZoneMapUpdate(zone_num: Int) => + val zone = zones.find(x => x.Number == zone_num).get + zone.Buildings.values.foreach(b => b.Actor ! Building.SendMapUpdate(all_clients = true)) case _ => log.warn(s"InterstellarCluster received unknown message"); @@ -122,6 +126,13 @@ object InterstellarCluster { * @see `WorldSessionActor` */ final case class ClientInitializationComplete() + + /** + * Requests that all buildings within a zone send a map update for the purposes of refreshing lattice benefits, such as when a base is hacked, changes faction or loses power + * @see `BuildingInfoUpdateMessage` + * @param zone_num the zone number to request building map updates for + */ + final case class ZoneMapUpdate(zone_num: Int) } /* diff --git a/common/src/main/scala/services/local/LocalService.scala b/common/src/main/scala/services/local/LocalService.scala index c46a215de..69002e53c 100644 --- a/common/src/main/scala/services/local/LocalService.scala +++ b/common/src/main/scala/services/local/LocalService.scala @@ -3,7 +3,6 @@ package services.local import akka.actor.{Actor, ActorRef, Props} import net.psforever.objects.ce.Deployable -import net.psforever.objects.serverobject.resourcesilo.ResourceSilo import net.psforever.objects.serverobject.structures.{Amenity, Building} import net.psforever.objects.serverobject.terminals.{CaptureTerminal, Terminal} import net.psforever.objects.zones.Zone @@ -87,10 +86,12 @@ class LocalService(zone : Zone) extends Actor { hackClearer ! HackClearActor.ObjectIsResecured(target) case LocalAction.HackCaptureTerminal(player_guid, _, target, unk1, unk2, isResecured) => // When a CC is hacked (or resecured) all amenities for the base should be unhacked - val hackableAmenities = target.Owner.asInstanceOf[Building].Amenities.filter(x => x.isInstanceOf[Hackable]).map(x => x.asInstanceOf[Amenity with Hackable]) + val building = target.Owner.asInstanceOf[Building] + val hackableAmenities = building.Amenities.filter(x => x.isInstanceOf[Hackable]).map(x => x.asInstanceOf[Amenity with Hackable]) hackableAmenities.foreach(amenity => if(amenity.HackedBy.isDefined) { hackClearer ! HackClearActor.ObjectIsResecured(amenity) } ) + if(isResecured){ hackCapturer ! HackCaptureActor.ClearHack(target, zone) } else { @@ -103,9 +104,16 @@ class LocalService(zone : Zone) extends Actor { 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)) ) + + // If the owner of this capture terminal is on the lattice trigger a zone wide map update to update lattice benefits + zone.Lattice find building match { + case Some(_) => building.TriggerZoneMapUpdate() + case None => ; + } case LocalAction.RouterTelepadTransport(player_guid, passenger_guid, src_guid, dest_guid) => LocalEvents.publish( LocalServiceResponse(s"/$forChannel/Local", player_guid, LocalResponse.RouterTelepadTransport(passenger_guid, src_guid, dest_guid)) @@ -163,16 +171,8 @@ class LocalService(zone : Zone) extends Actor { case HackCaptureActor.HackTimeoutReached(capture_terminal_guid, _, _, _, hackedByFaction) => val terminal = zone.GUID(capture_terminal_guid).get.asInstanceOf[CaptureTerminal] val building = terminal.Owner.asInstanceOf[Building] - // todo: Move this to a function for Building - var ntuLevel = building.Amenities.find(_.Definition == GlobalDefinitions.resource_silo) match { - case Some(obj: ResourceSilo) => - obj.CapacitorDisplay.toInt - case _ => - // Base has no NTU silo - likely a tower / cavern CC - 1 - } - if(ntuLevel > 0) { + if(building.NtuLevel > 0) { log.info(s"Setting base ${building.GUID} / MapId: ${building.MapId} as owned by $hackedByFaction") building.Faction = hackedByFaction diff --git a/common/src/main/scala/services/local/support/HackCaptureActor.scala b/common/src/main/scala/services/local/support/HackCaptureActor.scala index 0a0246160..35405d7cf 100644 --- a/common/src/main/scala/services/local/support/HackCaptureActor.scala +++ b/common/src/main/scala/services/local/support/HackCaptureActor.scala @@ -43,7 +43,7 @@ class HackCaptureActor extends Actor { // 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) + if(target.isInstanceOf[CaptureTerminal]) { target.Owner.asInstanceOf[Building].TriggerZoneMapUpdate() } case HackCaptureActor.ProcessCompleteHacks() => log.trace("Processing complete hacks") @@ -67,7 +67,7 @@ class HackCaptureActor extends Actor { case HackCaptureActor.ClearHack(target, _) => hackedObjects = hackedObjects.filterNot(x => x.target == target) - if(target.isInstanceOf[CaptureTerminal]) { target.Owner.Actor ! Building.SendMapUpdate(all_clients = true) } + if(target.isInstanceOf[CaptureTerminal]) { target.Owner.asInstanceOf[Building].TriggerZoneMapUpdate() } // Restart the timer in case the object we just removed was the next one scheduled RestartTimer() diff --git a/common/src/test/scala/objects/VehicleTest.scala b/common/src/test/scala/objects/VehicleTest.scala index 0cb21ef96..99cae7658 100644 --- a/common/src/test/scala/objects/VehicleTest.scala +++ b/common/src/test/scala/objects/VehicleTest.scala @@ -696,7 +696,7 @@ class VehicleControlShieldsNotChargingDamagedTest extends ActorTest { val fury_dm = Vehicle(GlobalDefinitions.fury).DamageModel val obj = ResolvedProjectile(ProjectileResolution.Hit, projectile, p_source, fury_dm, Vector3(1.2f, 3.4f, 5.6f), System.nanoTime) - "charge vehicle shields" in { + "not charge vehicle shields if recently damaged" in { assert(vehicle.Shields == 0) vehicle.Actor ! Vitality.Damage({case v : Vehicle => v.History(obj)})