diff --git a/src/main/scala/net/psforever/actors/zone/BuildingActor.scala b/src/main/scala/net/psforever/actors/zone/BuildingActor.scala index cf1383f5..f2971965 100644 --- a/src/main/scala/net/psforever/actors/zone/BuildingActor.scala +++ b/src/main/scala/net/psforever/actors/zone/BuildingActor.scala @@ -12,7 +12,7 @@ import net.psforever.objects.serverobject.structures.{Amenity, Building, Structu import net.psforever.objects.zones.Zone import net.psforever.persistence import net.psforever.services.avatar.{AvatarAction, AvatarServiceMessage} -import net.psforever.types.PlanetSideEmpire +import net.psforever.types.{PlanetSideEmpire, PlanetSideGeneratorState} import net.psforever.util.Database._ import net.psforever.services.galaxy.{GalaxyAction, GalaxyServiceMessage} import net.psforever.services.local.{LocalAction, LocalServiceMessage} @@ -39,6 +39,14 @@ object BuildingActor { final case class SetFaction(faction: PlanetSideEmpire.Value) extends Command + final case class UpdateForceDome(state: Option[Boolean]) extends Command + + object UpdateForceDome { + def apply(): UpdateForceDome = UpdateForceDome(None) + + def apply(state: Boolean): UpdateForceDome = UpdateForceDome(Some(state)) + } + // TODO remove // Changes to building objects should go through BuildingActor // Once they do, we won't need this anymore @@ -59,6 +67,58 @@ object BuildingActor { final case class PowerOn() extends Command final case class PowerOff() extends Command + + /** + * The natural conditions of a facility that is not eligible for its capitol force dome to be expanded. + * The only test not employed is whether or not the target building is a capitol. + * Ommission of this condition makes this test capable of evaluating subcapitol eligibility + * for capitol force dome expansion. + * @param building the target building + * @return `true`, if the conditions for capitol force dome are not met; + * `false`, otherwise + */ + def invalidBuildingCapitolForceDomeConditions(building: Building): Boolean = { + building.Faction == PlanetSideEmpire.NEUTRAL || + building.NtuLevel == 0 || + (building.Generator match { + case Some(o) => o.Condition == PlanetSideGeneratorState.Destroyed + case _ => false + }) + } + + /** + * If this building is a capitol major facility, + * use the faction affinity, the generator status, and the resource silo's capacitance level + * to determine if the capitol force dome should be active. + * @param building the building being evaluated + * @return the condition of the capitol force dome; + * `None`, if the facility is not a capitol building; + * `Some(true|false)` to indicate the state of the force dome + */ + def checkForceDomeStatus(building: Building): Option[Boolean] = { + if (building.IsCapitol) { + val originalStatus = building.ForceDomeActive + val faction = building.Faction + val updatedStatus = if (invalidBuildingCapitolForceDomeConditions(building)) { + false + } else { + val ownedSubCapitols = building.Neighbours(faction) match { + case Some(buildings: Set[Building]) => buildings.count { b => !invalidBuildingCapitolForceDomeConditions(b) } + case None => 0 + } + if (originalStatus && ownedSubCapitols <= 1) { + false + } else if (!originalStatus && ownedSubCapitols > 1) { + true + } else { + originalStatus + } + } + Some(updatedStatus) + } else { + None + } + } } class BuildingActor( @@ -125,6 +185,15 @@ class BuildingActor( galaxyService ! GalaxyServiceMessage(GalaxyAction.MapUpdate(building.infoUpdateMessage())) Behaviors.same + case UpdateForceDome(stateOpt) => + stateOpt match { + case Some(updatedStatus) if building.IsCapitol && updatedStatus != building.ForceDomeActive => + updateForceDomeStatus(updatedStatus, mapUpdateOnChange = true) + case _ => + alignForceDomeStatus() + } + Behaviors.same + case AmenityStateChange(gen: Generator, data) => if (generatorStateChange(gen, data)) { //update the map @@ -139,11 +208,17 @@ class BuildingActor( Behaviors.same case PowerOff() => - powerLost() + building.Generator match { + case Some(gen) => gen.Actor ! BuildingActor.NtuDepleted() + case _ => powerLost() + } Behaviors.same case PowerOn() => - powerRestored() + building.Generator match { + case Some(gen) if building.NtuLevel > 0 => gen.Actor ! BuildingActor.SuppliedWithNtu() + case _ => powerRestored() + } Behaviors.same case msg @ NtuDepleted() => @@ -198,17 +273,19 @@ class BuildingActor( true case Some(GeneratorControl.Event.Offline) => powerLost() + alignForceDomeStatus(mapUpdateOnChange = false) val zone = building.Zone val msg = AvatarAction.PlanetsideAttributeToAll(building.GUID, 46, 2) building.PlayersInSOI.foreach { player => zone.AvatarEvents ! AvatarServiceMessage(player.Name, msg) } //??? - false + true case Some(GeneratorControl.Event.Normal) => true case Some(GeneratorControl.Event.Online) => // Power restored. Reactor Online. Sensors Online. Weapons Online. All systems nominal. powerRestored() + alignForceDomeStatus(mapUpdateOnChange = false) val events = zone.AvatarEvents val guid = building.GUID val msg1 = AvatarAction.PlanetsideAttributeToAll(guid, 46, 0) @@ -266,11 +343,59 @@ class BuildingActor( case Failure(e) => log.error(e.getMessage) } building.Faction = faction + alignForceDomeStatus(mapUpdateOnChange = false) galaxy ! GalaxyServiceMessage(GalaxyAction.MapUpdate(building.infoUpdateMessage())) zone.LocalEvents ! LocalServiceMessage(zone.id, LocalAction.SetEmpire(building.GUID, faction)) } } + /** + * Evaluate the conditions of the building + * and determine if its capitol force dome state should be updated + * to reflect the actual conditions of the base or its surrounding bases. + * If this building is considered a subcapitol facility to the zone's actual capitol facility, + * and has the capitol force dome has a dependency upon it, + * pass a message onto that facility that it should check its own state alignment. + * @param mapUpdateOnChange if `true`, dispatch a `MapUpdate` message for this building + */ + def alignForceDomeStatus(mapUpdateOnChange: Boolean = true): Unit = { + BuildingActor.checkForceDomeStatus(building) match { + case Some(updatedStatus) if updatedStatus != building.ForceDomeActive => + updateForceDomeStatus(updatedStatus, mapUpdateOnChange) + case None if building.IsSubCapitol => + building.Neighbours match { + case Some(buildings: Set[Building]) => + buildings + .filter { _.IsCapitol } + .foreach { _.Actor ! BuildingActor.UpdateForceDome() } + case None => ; + } + case _ => ; //building is neither a capitol nor a subcapitol + } + } + + /** + * Dispatch a message to update the state of the clients with the server state of the capitol force dome. + * @param updatedStatus the new capitol force dome status + * @param mapUpdateOnChange if `true`, dispatch a `MapUpdate` message for this building + */ + def updateForceDomeStatus(updatedStatus: Boolean, mapUpdateOnChange: Boolean): Unit = { + building.ForceDomeActive = updatedStatus + zone.LocalEvents ! LocalServiceMessage( + zone.id, + LocalAction.UpdateForceDomeStatus(Service.defaultPlayerGUID, building.GUID, updatedStatus) + ) + if (mapUpdateOnChange) { + context.self ! BuildingActor.MapUpdate() + } + } + + /** + * Power has been severed. + * All installed amenities are distributed a `PowerOff` message + * and are instructed to display their "unpowered" model. + * Additionally, the facility is now rendered unspawnable regardless of its player spawning amenities. + */ def powerLost(): Unit = { val zone = building.Zone val zoneId = zone.id @@ -286,6 +411,12 @@ class BuildingActor( events ! AvatarServiceMessage(zoneId, AvatarAction.PlanetsideAttributeToAll(guid, 38, 0)) } + /** + * Power has been restored. + * All installed amenities are distributed a `PowerOn` message + * and are instructed to display their "powered" model. + * Additionally, the facility is now rendered spawnable if its player spawning amenities are online. + */ def powerRestored(): Unit = { val zone = building.Zone val zoneId = zone.id diff --git a/src/main/scala/net/psforever/actors/zone/ZoneActor.scala b/src/main/scala/net/psforever/actors/zone/ZoneActor.scala index 492b8b36..9b8546df 100644 --- a/src/main/scala/net/psforever/actors/zone/ZoneActor.scala +++ b/src/main/scala/net/psforever/actors/zone/ZoneActor.scala @@ -5,7 +5,7 @@ import akka.actor.typed.scaladsl.{AbstractBehavior, ActorContext, Behaviors} import net.psforever.objects.ballistics.SourceEntry import net.psforever.objects.ce.Deployable import net.psforever.objects.equipment.Equipment -import net.psforever.objects.serverobject.structures.StructureType +import net.psforever.objects.serverobject.structures.{Building, StructureType} import net.psforever.objects.zones.Zone import net.psforever.objects.{ConstructionItem, PlanetSideGameObject, Player, Vehicle} import net.psforever.types.{PlanetSideEmpire, PlanetSideGUID, Vector3} @@ -76,12 +76,21 @@ class ZoneActor(context: ActorContext[ZoneActor.Command], zone: Zone) ctx.run(query[persistence.Building].filter(_.zoneId == lift(zone.Number))).onComplete { case Success(buildings) => + var capitol: Option[Building] = None buildings.foreach { building => zone.BuildingByMapId(building.localId) match { - case Some(b) => b.Faction = PlanetSideEmpire(building.factionId) - case None => // TODO this happens during testing, need a way to not always persist during tests + case Some(b) => + b.Faction = PlanetSideEmpire(building.factionId) + if(b.IsCapitol) { + capitol = Some(b) + } + case None => + // TODO this happens during testing, need a way to not always persist during tests } - + } + capitol match { + case Some(b) => b.ForceDomeActive = BuildingActor.checkForceDomeStatus(b).getOrElse(false) + case None => ; } case Failure(e) => log.error(e.getMessage) } diff --git a/src/main/scala/net/psforever/objects/serverobject/structures/Building.scala b/src/main/scala/net/psforever/objects/serverobject/structures/Building.scala index 39ab6939..9ed800f4 100644 --- a/src/main/scala/net/psforever/objects/serverobject/structures/Building.scala +++ b/src/main/scala/net/psforever/objects/serverobject/structures/Building.scala @@ -4,8 +4,8 @@ package net.psforever.objects.serverobject.structures import java.util.concurrent.TimeUnit import akka.actor.ActorContext -import net.psforever.actors.zone.{BuildingActor, ZoneActor} -import net.psforever.objects.{Default, GlobalDefinitions, Player} +import net.psforever.actors.zone.BuildingActor +import net.psforever.objects.{GlobalDefinitions, Player} import net.psforever.objects.definition.ObjectDefinition import net.psforever.objects.serverobject.generator.Generator import net.psforever.objects.serverobject.hackable.Hackable @@ -17,8 +17,6 @@ import net.psforever.objects.zones.Zone import net.psforever.packet.game.BuildingInfoUpdateMessage import net.psforever.types.{PlanetSideEmpire, PlanetSideGUID, PlanetSideGeneratorState, Vector3} import scalax.collection.{Graph, GraphEdge} -import net.psforever.services.Service -import net.psforever.services.local.{LocalAction, LocalServiceMessage} import akka.actor.typed.scaladsl.adapter._ class Building( @@ -65,16 +63,6 @@ class Building( override def Faction_=(fac: PlanetSideEmpire.Value): PlanetSideEmpire.Value = { faction = fac - if (IsSubCapitol) { - Neighbours match { - case Some(buildings: Set[Building]) => buildings.filter(x => x.IsCapitol).head.UpdateForceDomeStatus() - case None => ; - } - } else if (IsCapitol) { - UpdateForceDomeStatus() - } - // FIXME null check is a bad idea but tests rely on it - if (Zone.actor != null) Zone.actor ! ZoneActor.ZoneMapUpdate() Faction } @@ -136,37 +124,6 @@ class Building( } } - def UpdateForceDomeStatus(): Unit = { - if (IsCapitol) { - val originalStatus = ForceDomeActive - - if (Faction == PlanetSideEmpire.NEUTRAL) { - ForceDomeActive = false - } else { - val ownedSubCapitols = Neighbours(Faction) match { - case Some(buildings: Set[Building]) => buildings.size - case None => 0 - } - - if (ForceDomeActive && ownedSubCapitols <= 1) { - ForceDomeActive = false - } else if (!ForceDomeActive && ownedSubCapitols > 1) { - ForceDomeActive = true - } - } - - if (originalStatus != ForceDomeActive) { - if (Actor != Default.Actor) { - Zone.LocalEvents ! LocalServiceMessage( - Zone.id, - LocalAction.UpdateForceDomeStatus(Service.defaultPlayerGUID, GUID, ForceDomeActive) - ) - Actor ! BuildingActor.MapUpdate() - } - } - } - } - // Get all lattice neighbours matching the specified faction def Neighbours(faction: PlanetSideEmpire.Value): Option[Set[Building]] = { this.Neighbours match {