diff --git a/src/main/scala/net/psforever/actors/session/ChatActor.scala b/src/main/scala/net/psforever/actors/session/ChatActor.scala index 9a1c2719..8afe468e 100644 --- a/src/main/scala/net/psforever/actors/session/ChatActor.scala +++ b/src/main/scala/net/psforever/actors/session/ChatActor.scala @@ -7,7 +7,7 @@ import akka.actor.typed.scaladsl.{ActorContext, Behaviors, StashBuffer} import net.psforever.actors.zone.BuildingActor import net.psforever.objects.avatar.{BattleRank, Certification, CommandRank, Cosmetic} import net.psforever.objects.serverobject.pad.{VehicleSpawnControl, VehicleSpawnPad} -import net.psforever.objects.{Default, GlobalDefinitions, Player, Session} +import net.psforever.objects.{Default, Player, Session} import net.psforever.objects.serverobject.resourcesilo.ResourceSilo import net.psforever.objects.serverobject.structures.Building import net.psforever.objects.serverobject.turret.{FacilityTurret, TurretUpgrade, WeaponTurrets} @@ -342,20 +342,55 @@ class ChatActor( ) } - case (_, _, contents) if contents.startsWith("!ntu") && session.account.gm => - session.zone.Buildings.values.foreach(building => - building.Amenities.foreach(amenity => - amenity.Definition match { - case GlobalDefinitions.resource_silo => - val r = new scala.util.Random - val silo = amenity.asInstanceOf[ResourceSilo] - val ntu = 900f + r.nextFloat() * 100f - silo.NtuCapacitor - silo.Actor ! ResourceSilo.UpdateChargeLevel(ntu) - - case _ => () - } + case (_, _, contents) if contents.startsWith("!ntu") && gmCommandAllowed => + val buffer = contents.toLowerCase.split("\\s+") + val (facility, customNtuValue) = (buffer.lift(1), buffer.lift(2)) match { + case (Some(x), Some(y)) if y.toIntOption.nonEmpty => (Some(x), Some(y.toInt)) + case (Some(x), None) if x.toIntOption.nonEmpty => (None, Some(x.toInt)) + case _ => (None, None) + } + val silos = (facility match { + case Some(cur) if cur.toLowerCase().startsWith("curr") => + val position = session.player.Position + session.zone.Buildings.values + .filter { building => + val soi2 = building.Definition.SOIRadius * building.Definition.SOIRadius + Vector3.DistanceSquared(building.Position, position) < soi2 + } + case Some(x) => + session.zone.Buildings.values.find { _.Name.equalsIgnoreCase(x) }.toList + case _ => + session.zone.Buildings.values + }) + .flatMap { building => building.Amenities.filter { _.isInstanceOf[ResourceSilo] } } + if(silos.isEmpty) { + sessionActor ! SessionActor.SendResponse( + ChatMsg(UNK_229, true, "Server", s"no targets for ntu found with parameters $facility", None) ) - ) + } + customNtuValue match { + // x = n0% of maximum capacitance + case Some(value) if value > -1 && value < 11 => + silos.collect { case silo: ResourceSilo => + silo.Actor ! ResourceSilo.UpdateChargeLevel(value * silo.MaxNtuCapacitor * 0.1f - silo.NtuCapacitor) + } + // capacitance set to x (where x > 10) exactly, within limits + case Some(value) => + silos.collect { case silo: ResourceSilo => + silo.Actor ! ResourceSilo.UpdateChargeLevel(value - silo.NtuCapacitor) + } + case None => + // x >= n0% of maximum capacitance and x <= maximum capacitance + val rand = new scala.util.Random + silos.collect { case silo: ResourceSilo => + val a = 7 + val b = 10 - a + val tenth = silo.MaxNtuCapacitor * 0.1f + silo.Actor ! ResourceSilo.UpdateChargeLevel( + a * tenth + rand.nextFloat() * b * tenth - silo.NtuCapacitor + ) + } + } case _ => // unknown ! commands are ignored diff --git a/src/main/scala/net/psforever/actors/session/SessionActor.scala b/src/main/scala/net/psforever/actors/session/SessionActor.scala index 98ea9277..6d97d3f4 100644 --- a/src/main/scala/net/psforever/actors/session/SessionActor.scala +++ b/src/main/scala/net/psforever/actors/session/SessionActor.scala @@ -542,9 +542,6 @@ class SessionActor(middlewareActor: typed.ActorRef[MiddlewareActor.Command], con case ProgressEvent(delta, finishedAction, stepAction, tick) => HandleProgressChange(delta, finishedAction, stepAction, tick) - case Door.DoorMessage(tplayer, msg, order) => - HandleDoorMessage(tplayer, msg, order) - case GalaxyServiceResponse(_, reply) => reply match { case GalaxyResponse.HotSpotUpdate(zone_index, priority, hot_spot_info) => @@ -2189,36 +2186,6 @@ class SessionActor(middlewareActor: typed.ActorRef[MiddlewareActor.Command], con } } - /** - * na - * @param tplayer na - * @param msg na - * @param order na - */ - def HandleDoorMessage(tplayer: Player, msg: UseItemMessage, order: Door.Exchange): Unit = { - val door_guid = msg.object_guid - order match { - case Door.OpenEvent() => - continent.GUID(door_guid) match { - case Some(door: Door) => - sendResponse(GenericObjectStateMsg(door_guid, 16)) - continent.LocalEvents ! LocalServiceMessage( - continent.id, - LocalAction.DoorOpens(tplayer.GUID, continent, door) - ) - - case _ => - log.warn(s"door $door_guid wanted to be opened but could not be found") - } - - case Door.CloseEvent() => - sendResponse(GenericObjectStateMsg(door_guid, 17)) - continent.LocalEvents ! LocalServiceMessage(continent.id, LocalAction.DoorCloses(tplayer.GUID, door_guid)) - - case Door.NoEvent() => ; - } - } - /** * na * @param toChannel na @@ -4477,29 +4444,7 @@ class SessionActor(middlewareActor: typed.ActorRef[MiddlewareActor.Command], con } ValidObject(object_guid) match { case Some(door: Door) => - if ( - player.Faction == door.Faction || (continent.map.doorToLock.get(object_guid.guid) match { - case Some(lock_guid) => - val lock = continent.GUID(lock_guid).get.asInstanceOf[IFFLock] - val owner = lock.Owner.asInstanceOf[Building] - val playerIsOnInside = Vector3.ScalarProjection(lock.Outwards, player.Position - door.Position) < 0f - - // If an IFF lock exists and the IFF lock faction doesn't match the current player and one of the following conditions are met open the door: - // The player is on the inside of the door, determined by the lock orientation - // The lock is hacked - // A base is hacked - // A base is neutral - // todo: A base is out of power (generator down) - - playerIsOnInside || lock.HackedBy.isDefined || owner.CaptureTerminalIsHacked || lock.Faction == PlanetSideEmpire.NEUTRAL - case None => !door.isOpen // If there's no linked IFF lock just open the door if it's closed. - }) - ) { - door.Actor ! Door.Use(player, msg) - } else if (door.isOpen) { - //the door is open globally ... except on our screen - sendResponse(GenericObjectStateMsg(object_guid, 16)) - } + door.Actor ! CommonMessages.Use(player) case Some(resourceSilo: ResourceSilo) => resourceSilo.Actor ! CommonMessages.Use(player) @@ -6609,13 +6554,20 @@ class SessionActor(middlewareActor: typed.ActorRef[MiddlewareActor.Command], con */ def configZone(zone: Zone): Unit = { zone.Buildings.values.foreach(building => { - sendResponse(SetEmpireMessage(building.GUID, building.Faction)) - - // Synchronise capitol force dome state - if (building.IsCapitol && building.ForceDomeActive) { - sendResponse(GenericObjectActionMessage(building.GUID, 13)) + val guid = building.GUID + sendResponse(SetEmpireMessage(guid, building.Faction)) + // power + building.Generator match { + case Some(obj) if obj.Condition == PlanetSideGeneratorState.Destroyed || building.NtuLevel == 0 => + sendResponse(PlanetsideAttributeMessage(guid, 48, 1)) //amenities disabled; red warning lights + sendResponse(PlanetsideAttributeMessage(guid, 38, 0)) //disable spawn target on deployment map + case _ => ; } - // Synchronise amenities + // capitol force dome state + if (building.IsCapitol && building.ForceDomeActive) { + sendResponse(GenericObjectActionMessage(guid, 13)) + } + // amenities building.Amenities.collect { case obj if obj.Destroyed => configAmenityAsDestroyed(obj) case obj => configAmenityAsWorking(obj) diff --git a/src/main/scala/net/psforever/actors/zone/BuildingActor.scala b/src/main/scala/net/psforever/actors/zone/BuildingActor.scala index 60b12795..f2971965 100644 --- a/src/main/scala/net/psforever/actors/zone/BuildingActor.scala +++ b/src/main/scala/net/psforever/actors/zone/BuildingActor.scala @@ -7,14 +7,16 @@ import akka.{actor => classic} import net.psforever.actors.commands.NtuCommand import net.psforever.objects.{CommonNtuContainer, NtuContainer} import net.psforever.objects.serverobject.PlanetSideServerObject +import net.psforever.objects.serverobject.generator.{Generator, GeneratorControl} import net.psforever.objects.serverobject.structures.{Amenity, Building, StructureType, WarpGate} import net.psforever.objects.zones.Zone import net.psforever.persistence -import net.psforever.types.PlanetSideEmpire +import net.psforever.services.avatar.{AvatarAction, AvatarServiceMessage} +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} -import net.psforever.services.{InterstellarClusterService, ServiceManager} +import net.psforever.services.{InterstellarClusterService, Service, ServiceManager} import scala.concurrent.ExecutionContext.Implicits.global import scala.util.{Failure, Success} @@ -37,18 +39,86 @@ 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 final case class MapUpdate() extends Command - final case class AmenityStateChange(obj: Amenity) extends Command + final case class AmenityStateChange(obj: Amenity, data: Option[Any]) extends Command + + object AmenityStateChange{ + def apply(obj: Amenity): AmenityStateChange = AmenityStateChange(obj, None) + } final case class Ntu(command: NtuCommand.Command) extends Command final case class SuppliedWithNtu() extends Command final case class NtuDepleted() extends Command + + 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( @@ -63,6 +133,7 @@ class BuildingActor( private[this] val log = org.log4s.getLogger var galaxyService: Option[classic.ActorRef] = None var interstellarCluster: Option[ActorRef[InterstellarClusterService.Command]] = None + var hasNtuSupply: Boolean = true context.system.receptionist ! Receptionist.Find( InterstellarClusterService.InterstellarClusterServiceKey, @@ -107,69 +178,61 @@ class BuildingActor( ): Behavior[Command] = { Behaviors.receiveMessagePartial { case SetFaction(faction) => - import ctx._ - ctx - .run( - query[persistence.Building] - .filter(_.localId == lift(building.MapId)) - .filter(_.zoneId == lift(zone.Number)) - ) - .onComplete { - case Success(res) => - res.headOption match { - case Some(_) => - ctx - .run( - query[persistence.Building] - .filter(_.localId == lift(building.MapId)) - .filter(_.zoneId == lift(zone.Number)) - .update(_.factionId -> lift(building.Faction.id)) - ) - .onComplete { - case Success(_) => - case Failure(e) => log.error(e.getMessage) - } - case _ => - ctx - .run( - query[persistence.Building] - .insert( - _.localId -> lift(building.MapId), - _.factionId -> lift(building.Faction.id), - _.zoneId -> lift(zone.Number) - ) - ) - .onComplete { - case Success(_) => - case Failure(e) => log.error(e.getMessage) - } - } - case Failure(e) => log.error(e.getMessage) - } - building.Faction = faction - galaxyService ! GalaxyServiceMessage(GalaxyAction.MapUpdate(building.infoUpdateMessage())) - zone.LocalEvents ! LocalServiceMessage(zone.id, LocalAction.SetEmpire(building.GUID, faction)) + setFactionTo(faction, galaxyService) Behaviors.same case MapUpdate() => galaxyService ! GalaxyServiceMessage(GalaxyAction.MapUpdate(building.infoUpdateMessage())) Behaviors.same - case AmenityStateChange(_) => + 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 + galaxyService ! GalaxyServiceMessage(GalaxyAction.MapUpdate(building.infoUpdateMessage())) + } + Behaviors.same + + case AmenityStateChange(_, _) => //TODO when parameter object is finally immutable, perform analysis on it to determine specific actions //for now, just update the map galaxyService ! GalaxyServiceMessage(GalaxyAction.MapUpdate(building.infoUpdateMessage())) Behaviors.same - case msg @ NtuDepleted() => - log.trace(s"${building.Definition.Name} ${building.Name} ntu has been depleted") - building.Amenities.foreach { amenity => - amenity.Actor ! msg + case PowerOff() => + building.Generator match { + case Some(gen) => gen.Actor ! BuildingActor.NtuDepleted() + case _ => powerLost() } Behaviors.same + case PowerOn() => + building.Generator match { + case Some(gen) if building.NtuLevel > 0 => gen.Actor ! BuildingActor.SuppliedWithNtu() + case _ => powerRestored() + } + Behaviors.same + + case msg @ NtuDepleted() => + // Someone let the base run out of nanites. No one gets anything. + building.Amenities.foreach { amenity => + amenity.Actor ! msg + } + setFactionTo(PlanetSideEmpire.NEUTRAL, galaxyService) + hasNtuSupply = false + Behaviors.same + case msg @ SuppliedWithNtu() => - log.trace(s"ntu supply has been restored to ${building.Definition.Name} ${building.Name}") + // Auto-repair restart, mainly. If the Generator works, power should be restored too. + hasNtuSupply = true building.Amenities.foreach { amenity => amenity.Actor ! msg } @@ -180,6 +243,195 @@ class BuildingActor( } } + def generatorStateChange(generator: Generator, event: Any): Boolean = { + event match { + case Some(GeneratorControl.Event.UnderAttack) => + val events = zone.AvatarEvents + val guid = building.GUID + val msg = AvatarAction.GenericObjectAction(Service.defaultPlayerGUID, guid, 15) + building.PlayersInSOI.foreach { player => + events ! AvatarServiceMessage(player.Name, msg) + } + false + case Some(GeneratorControl.Event.Critical) => + val events = zone.AvatarEvents + val guid = building.GUID + val msg = AvatarAction.PlanetsideAttributeToAll(guid, 46, 1) + building.PlayersInSOI.foreach { player => + events ! AvatarServiceMessage(player.Name, msg) + } + true + case Some(GeneratorControl.Event.Destabilized) => + val events = zone.AvatarEvents + val guid = building.GUID + val msg = AvatarAction.GenericObjectAction(Service.defaultPlayerGUID, guid, 16) + building.PlayersInSOI.foreach { player => + events ! AvatarServiceMessage(player.Name, msg) + } + false + case Some(GeneratorControl.Event.Destroyed) => + 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) + } //??? + 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) + val msg2 = AvatarAction.GenericObjectAction(Service.defaultPlayerGUID, guid, 17) + building.PlayersInSOI.foreach { player => + val name = player.Name + events ! AvatarServiceMessage(name, msg1) //reset ???; might be global? + events ! AvatarServiceMessage(name, msg2) //This facility's generator is back on line + } + true + case _ => + false + } + } + + def setFactionTo(faction: PlanetSideEmpire.Value, galaxy: classic.ActorRef): Unit = { + if (hasNtuSupply) { + import ctx._ + ctx + .run( + query[persistence.Building] + .filter(_.localId == lift(building.MapId)) + .filter(_.zoneId == lift(zone.Number)) + ) + .onComplete { + case Success(res) => + res.headOption match { + case Some(_) => + ctx + .run( + query[persistence.Building] + .filter(_.localId == lift(building.MapId)) + .filter(_.zoneId == lift(zone.Number)) + .update(_.factionId -> lift(building.Faction.id)) + ) + .onComplete { + case Success(_) => + case Failure(e) => log.error(e.getMessage) + } + case _ => + ctx + .run( + query[persistence.Building] + .insert( + _.localId -> lift(building.MapId), + _.factionId -> lift(building.Faction.id), + _.zoneId -> lift(zone.Number) + ) + ) + .onComplete { + case Success(_) => + case Failure(e) => log.error(e.getMessage) + } + } + 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 + val events = zone.AvatarEvents + val guid = building.GUID + val powerMsg = BuildingActor.PowerOff() + building.Amenities.foreach { amenity => + amenity.Actor ! powerMsg + } + //amenities disabled; red warning lights + events ! AvatarServiceMessage(zoneId, AvatarAction.PlanetsideAttributeToAll(guid, 48, 1)) + //disable spawn target on deployment map + 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 + val events = zone.AvatarEvents + val guid = building.GUID + val powerMsg = BuildingActor.PowerOn() + building.Amenities.foreach { amenity => + amenity.Actor ! powerMsg + } + //amenities enabled; normal lights + events ! AvatarServiceMessage(zoneId, AvatarAction.PlanetsideAttributeToAll(guid, 48, 0)) + //enable spawn target on deployment map + events ! AvatarServiceMessage(zoneId, AvatarAction.PlanetsideAttributeToAll(guid, 38, 1)) + } + def ntu(msg: NtuCommand.Command): Behavior[Command] = { import NtuCommand._ msg match { 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/SpawnPoint.scala b/src/main/scala/net/psforever/objects/SpawnPoint.scala index 2860c784..fa1e8a03 100644 --- a/src/main/scala/net/psforever/objects/SpawnPoint.scala +++ b/src/main/scala/net/psforever/objects/SpawnPoint.scala @@ -50,7 +50,7 @@ trait SpawnPoint { */ def Definition: ObjectDefinition with SpawnPointDefinition - def Offline: Boolean = psso.Destroyed + def isOffline: Boolean = psso.Destroyed /** * Determine a specific position and orientation in which to spawn the target. diff --git a/src/main/scala/net/psforever/objects/serverobject/doors/Door.scala b/src/main/scala/net/psforever/objects/serverobject/doors/Door.scala index 56e7193b..b10574bf 100644 --- a/src/main/scala/net/psforever/objects/serverobject/doors/Door.scala +++ b/src/main/scala/net/psforever/objects/serverobject/doors/Door.scala @@ -25,16 +25,6 @@ class Door(private val ddef: DoorDefinition) extends Amenity { Open } - def Use(player: Player, msg: UseItemMessage): Door.Exchange = { - if (openState.isEmpty) { - openState = Some(player) - Door.OpenEvent() - } else { - openState = None - Door.CloseEvent() - } - } - def Definition: DoorDefinition = ddef } diff --git a/src/main/scala/net/psforever/objects/serverobject/doors/DoorControl.scala b/src/main/scala/net/psforever/objects/serverobject/doors/DoorControl.scala index 8614db3c..10fa58b8 100644 --- a/src/main/scala/net/psforever/objects/serverobject/doors/DoorControl.scala +++ b/src/main/scala/net/psforever/objects/serverobject/doors/DoorControl.scala @@ -1,21 +1,90 @@ // Copyright (c) 2017 PSForever package net.psforever.objects.serverobject.doors -import akka.actor.Actor +import net.psforever.objects.Player +import net.psforever.objects.serverobject.CommonMessages import net.psforever.objects.serverobject.affinity.{FactionAffinity, FactionAffinityBehavior} +import net.psforever.objects.serverobject.locks.IFFLock +import net.psforever.objects.serverobject.structures.{Building, PoweredAmenityControl} +import net.psforever.services.Service +import net.psforever.services.local.{LocalAction, LocalResponse, LocalServiceMessage, LocalServiceResponse} +import net.psforever.types.{PlanetSideEmpire, Vector3} /** * An `Actor` that handles messages being dispatched to a specific `Door`. * @param door the `Door` object being governed */ -class DoorControl(door: Door) extends Actor with FactionAffinityBehavior.Check { +class DoorControl(door: Door) + extends PoweredAmenityControl + with FactionAffinityBehavior.Check { def FactionObject: FactionAffinity = door - def receive: Receive = - checkBehavior.orElse { - case Door.Use(player, msg) => - sender() ! Door.DoorMessage(player, msg, door.Use(player, msg)) + val commonBehavior: Receive = checkBehavior - case _ => ; + def poweredStateLogic: Receive = + commonBehavior + .orElse { + case CommonMessages.Use(player, _) => + val zone = door.Zone + val doorGUID = door.GUID + if ( + player.Faction == door.Faction || (zone.GUID(zone.map.doorToLock.getOrElse(doorGUID.guid, 0)) match { + case Some(lock: IFFLock) => + val owner = lock.Owner.asInstanceOf[Building] + val playerIsOnInside = Vector3.ScalarProjection(lock.Outwards, player.Position - door.Position) < 0f + /* + If an IFF lock exists and + the IFF lock faction doesn't match the current player and + one of the following conditions are met: + 1. player is on the inside of the door (determined by the lock orientation) + 2. lock is hacked + 3. facility capture terminal has been hacked + 4. base is neutral + ... open the door. + */ + playerIsOnInside || lock.HackedBy.isDefined || owner.CaptureTerminalIsHacked || lock.Faction == PlanetSideEmpire.NEUTRAL + case _ => true // no linked IFF lock, just try open the door + }) + ) { + openDoor(player) + } + + case _ => ; + } + + def unpoweredStateLogic: Receive = { + commonBehavior + .orElse { + case CommonMessages.Use(player, _) => + //without power, the door opens freely + openDoor(player) + + case _ => ; + } + } + + def openDoor(player: Player): Unit = { + val zone = door.Zone + val doorGUID = door.GUID + if (!door.isOpen) { + //global open + door.Open = player + zone.LocalEvents ! LocalServiceMessage( + zone.id, + LocalAction.DoorOpens(Service.defaultPlayerGUID, zone, door) + ) } + else { + //the door should already open, but the requesting player does not see it as open + sender() ! LocalServiceResponse( + player.Name, + Service.defaultPlayerGUID, + LocalResponse.DoorOpens(doorGUID) + ) + } + } + + override def powerTurnOffCallback() : Unit = { } + + override def powerTurnOnCallback() : Unit = { } } diff --git a/src/main/scala/net/psforever/objects/serverobject/generator/Generator.scala b/src/main/scala/net/psforever/objects/serverobject/generator/Generator.scala index b66646cb..27de959c 100644 --- a/src/main/scala/net/psforever/objects/serverobject/generator/Generator.scala +++ b/src/main/scala/net/psforever/objects/serverobject/generator/Generator.scala @@ -24,6 +24,16 @@ class Generator(private val gdef: GeneratorDefinition) extends Amenity { Condition } + override def Destroyed_=(state : Boolean) : Boolean = { + val isDestroyed = super.Destroyed_=(state) + condition = if (isDestroyed) { + PlanetSideGeneratorState.Destroyed + } else { + PlanetSideGeneratorState.Normal + } + isDestroyed + } + def Definition: GeneratorDefinition = gdef } diff --git a/src/main/scala/net/psforever/objects/serverobject/generator/GeneratorControl.scala b/src/main/scala/net/psforever/objects/serverobject/generator/GeneratorControl.scala index 5a45f43f..503f1dec 100644 --- a/src/main/scala/net/psforever/objects/serverobject/generator/GeneratorControl.scala +++ b/src/main/scala/net/psforever/objects/serverobject/generator/GeneratorControl.scala @@ -1,9 +1,9 @@ // Copyright (c) 2020 PSForever package net.psforever.objects.serverobject.generator -import akka.actor.Actor +import akka.actor.{Actor, Cancellable} import net.psforever.actors.zone.BuildingActor -import net.psforever.objects.{Player, Tool} +import net.psforever.objects.{Default, Player, Tool} import net.psforever.objects.ballistics._ import net.psforever.objects.serverobject.affinity.FactionAffinityBehavior import net.psforever.objects.serverobject.damage.Damageable.Target @@ -34,21 +34,64 @@ class GeneratorControl(gen: Generator) def DamageableObject = gen def RepairableObject = gen def AutoRepairObject = gen + /** flagged to explode after some time */ var imminentExplosion: Boolean = false - var alarmCooldownPeriod: Boolean = false + /** explode when this timer completes */ + var queuedExplosion: Cancellable = Default.Cancellable + /** when damaged, announce that damage was dealt on a schedule */ + var alarmCooldown: Cancellable = Default.Cancellable - def receive: Receive = + /* + behavior of the generator piggybacks from the logic used in `AmenityAutoRepair` + AAR splits its logic based on whether or not it has detected a source of nanite transfer units (NTU) + this amenity is the bridge between NTU and facility power so it leverages that logic + it is split between when detecting ntu and when starved for ntu + */ + + def receive: Receive = withNtu + + /** behavior that is valid for both "with-ntu" and "without-ntu" */ + val commonBehavior: Receive = checkBehavior .orElse(takesDamage) .orElse(canBeRepairedByNanoDispenser) .orElse(autoRepairBehavior) .orElse { - case GeneratorControl.GeneratorExplodes() => //TODO this only works with projectiles right now! + case GeneratorControl.UnderThreatAlarm() => + //alert to damage and block other damage alerts for a time + if (alarmCooldown.isCancelled) { + GeneratorControl.UpdateOwner(gen, Some(GeneratorControl.Event.UnderAttack)) + alarmCooldown.cancel() + alarmCooldown = context.system.scheduler.scheduleOnce(delay = 5 seconds, self, GeneratorControl.AlarmReset()) + } + + case GeneratorControl.AlarmReset() => + //clear the blocker for alerting to damage + alarmCooldown = Default.Cancellable + } + + /* + when NTU is detected, + the generator can be properly destabilized and explode + the generator can be repaired to operational status and power the facility in which it is installed + */ + def withNtu: Receive = + commonBehavior + .orElse { + case GeneratorControl.Destabilized() => + imminentExplosion = true + //the generator's condition is technically destroyed, but avoid official reporting until the explosion + gen.Condition = PlanetSideGeneratorState.Destroyed + GeneratorControl.UpdateOwner(gen, Some(GeneratorControl.Event.Destabilized)) + queuedExplosion.cancel() + queuedExplosion = context.system.scheduler.scheduleOnce(10 seconds, self, GeneratorControl.GeneratorExplodes()) + + case GeneratorControl.GeneratorExplodes() => + //TODO this only works with projectiles right now! val zone = gen.Zone gen.Health = 0 super.DestructionAwareness(gen, gen.LastShot.get) - gen.Condition = PlanetSideGeneratorState.Destroyed - GeneratorControl.UpdateOwner(gen) + GeneratorControl.UpdateOwner(gen, Some(GeneratorControl.Event.Destroyed)) //kaboom zone.AvatarEvents ! AvatarServiceMessage( zone.id, @@ -57,6 +100,8 @@ class GeneratorControl(gen: Generator) TriggerEffectMessage(gen.GUID, "explosion_generator", None, None) ) ) + queuedExplosion.cancel() + queuedExplosion = Default.Cancellable imminentExplosion = false //kill everyone within 14m gen.Owner match { @@ -71,24 +116,46 @@ class GeneratorControl(gen: Generator) } gen.ClearHistory() - case GeneratorControl.UnderThreatAlarm() => - if (!alarmCooldownPeriod) { - alarmCooldownPeriod = true - GeneratorControl.BroadcastGeneratorEvent(gen, event = 15) - context.system.scheduler.scheduleOnce(delay = 5 seconds, self, GeneratorControl.AlarmReset()) - } - - case GeneratorControl.AlarmReset() => - alarmCooldownPeriod = false + case GeneratorControl.Restored() => + GeneratorControl.UpdateOwner(gen, Some(GeneratorControl.Event.Online)) case _ => ; } + /* + when ntu is not expected, + the generator can still be destroyed but will not explode + handles the possibility that ntu was lost during an ongoing destabilization and cancels the explosion + */ + def withoutNtu: Receive = + commonBehavior + .orElse { + case GeneratorControl.GeneratorExplodes() => + queuedExplosion.cancel() + queuedExplosion = Default.Cancellable + imminentExplosion = false + + case GeneratorControl.Destabilized() => + //if the generator is destabilized but has no ntu, it will not explode + gen.Health = 0 + super.DestructionAwareness(gen, gen.LastShot.get) + queuedExplosion.cancel() + queuedExplosion = Default.Cancellable + imminentExplosion = false + gen.Condition = PlanetSideGeneratorState.Destroyed + GeneratorControl.UpdateOwner(gen, Some(GeneratorControl.Event.Destroyed)) + gen.ClearHistory() + + case _ => + } + override protected def CanPerformRepairs(obj: Target, player: Player, item: Tool): Boolean = { + //if an explosion is queued, disallow repairs !imminentExplosion && super.CanPerformRepairs(obj, player, item) } override protected def WillAffectTarget(target: Target, damage: Int, cause: ResolvedProjectile): Boolean = { + //if an explosion is queued, disallow further damage !imminentExplosion && super.WillAffectTarget(target, damage, cause) } @@ -104,11 +171,11 @@ class GeneratorControl(gen: Generator) override protected def DestructionAwareness(target: Target, cause: ResolvedProjectile): Unit = { tryAutoRepair() + //if the target is already destroyed, do not let it be destroyed again if (!target.Destroyed) { target.Health = 1 //temporary - imminentExplosion = true - context.system.scheduler.scheduleOnce(10 seconds, self, GeneratorControl.GeneratorExplodes()) - GeneratorControl.BroadcastGeneratorEvent(gen, 16) + GeneratorControl.UpdateOwner(gen, Some(GeneratorControl.Event.Offline)) + self ! GeneratorControl.Destabilized() } } @@ -123,12 +190,40 @@ class GeneratorControl(gen: Generator) override def Restoration(obj: Repairable.Target): Unit = { super.Restoration(obj) gen.Condition = PlanetSideGeneratorState.Normal - GeneratorControl.UpdateOwner(gen) - GeneratorControl.BroadcastGeneratorEvent(gen, 17) + GeneratorControl.UpdateOwner(gen, Some(GeneratorControl.Event.Normal)) + self ! GeneratorControl.Restored() + } + + override def withNtuSupplyCallback() : Unit = { + context.become(withNtu) + super.withNtuSupplyCallback() + //if not destroyed when a source of ntu is detected, restore facility power + if(!gen.Destroyed) { + self ! GeneratorControl.Restored() + } + } + + override def noNtuSupplyCallback() : Unit = { + //auto-repair must stop naturally + context.become(withoutNtu) + super.noNtuSupplyCallback() + //if not destroyed when cutoff from a source of ntu, stop facility power generation + if(!gen.Destroyed) { + GeneratorControl.UpdateOwner(gen, Some(GeneratorControl.Event.Offline)) + } + //can any explosion (see withoutNtu->GenweratorControl.Destabilized) + if(!queuedExplosion.isCancelled) { + queuedExplosion.cancel() + self ! GeneratorControl.Destabilized() + } } } object GeneratorControl { + /** + * na + */ + private case class Destabilized() /** * na @@ -147,30 +242,31 @@ object GeneratorControl { /** * na - * @param obj na */ - private def UpdateOwner(obj: Generator): Unit = { - obj.Owner match { - case b: Building => b.Actor ! BuildingActor.AmenityStateChange(obj) - case _ => ; - } - } + private case class Restored() /** * na - * @param target the generator - * @param event the action code for the event */ - private def BroadcastGeneratorEvent(target: Generator, event: Int): Unit = { - target.Owner match { - case b: Building => - val events = target.Zone.AvatarEvents - val msg = AvatarAction.GenericObjectAction(Service.defaultPlayerGUID, target.Owner.GUID, event) - b.PlayersInSOI.foreach { player => - events ! AvatarServiceMessage(player.Name, msg) - } - case _ => ; - } + object Event extends Enumeration { + val + Critical, //PlanetSideGeneratorState.Critical + UnderAttack, + Destabilized, + Destroyed, //PlanetSideGeneratorState.Destroyed + Offline, + Normal, //PlanetSideGeneratorState.Normal + Online + = Value + } + + /** + * Send a message back to the owner for which this `Amenity` entity is installed. + * @param obj the entity doing the self-reporting + * @param data optional information that indicates the nature of the state change + */ + private def UpdateOwner(obj: Generator, data: Option[Any] = None): Unit = { + obj.Owner.Actor ! BuildingActor.AmenityStateChange(obj, data) } /** @@ -185,7 +281,7 @@ object GeneratorControl { val max: Float = target.MaxHealth.toFloat if (target.Condition != PlanetSideGeneratorState.Critical && health / max < 0.51f) { //becoming critical target.Condition = PlanetSideGeneratorState.Critical - GeneratorControl.UpdateOwner(target) + GeneratorControl.UpdateOwner(target, Some(GeneratorControl.Event.Critical)) } //the generator is under attack target.Actor ! UnderThreatAlarm() diff --git a/src/main/scala/net/psforever/objects/serverobject/implantmech/ImplantTerminalMechControl.scala b/src/main/scala/net/psforever/objects/serverobject/implantmech/ImplantTerminalMechControl.scala index 45853ba2..4400a3e5 100644 --- a/src/main/scala/net/psforever/objects/serverobject/implantmech/ImplantTerminalMechControl.scala +++ b/src/main/scala/net/psforever/objects/serverobject/implantmech/ImplantTerminalMechControl.scala @@ -1,7 +1,6 @@ // Copyright (c) 2017 PSForever package net.psforever.objects.serverobject.implantmech -import akka.actor.Actor import net.psforever.objects.ballistics.ResolvedProjectile import net.psforever.objects.{GlobalDefinitions, Player, SimpleItem} import net.psforever.objects.serverobject.{CommonMessages, PlanetSideServerObject} @@ -11,14 +10,15 @@ import net.psforever.objects.serverobject.damage.Damageable.Target import net.psforever.objects.serverobject.damage.{Damageable, DamageableEntity, DamageableMountable} import net.psforever.objects.serverobject.hackable.{GenericHackables, HackableBehavior} import net.psforever.objects.serverobject.repair.{AmenityAutoRepair, RepairableEntity} -import net.psforever.objects.serverobject.structures.Building +import net.psforever.objects.serverobject.structures.{Building, PoweredAmenityControl} +import net.psforever.services.vehicle.{VehicleAction, VehicleServiceMessage} /** * An `Actor` that handles messages being dispatched to a specific `ImplantTerminalMech`. * @param mech the "mech" object being governed */ class ImplantTerminalMechControl(mech: ImplantTerminalMech) - extends Actor + extends PoweredAmenityControl with FactionAffinityBehavior.Check with MountableBehavior.Mount with MountableBehavior.Dismount @@ -33,17 +33,19 @@ class ImplantTerminalMechControl(mech: ImplantTerminalMech) def RepairableObject = mech def AutoRepairObject = mech - def receive: Receive = + def commonBehavior: Receive = checkBehavior - .orElse(mountBehavior) .orElse(dismountBehavior) - .orElse(hackableBehavior) .orElse(takesDamage) .orElse(canBeRepairedByNanoDispenser) .orElse(autoRepairBehavior) + + def poweredStateLogic : Receive = + commonBehavior + .orElse(mountBehavior) .orElse { case CommonMessages.Use(player, Some(item: SimpleItem)) - if item.Definition == GlobalDefinitions.remote_electronics_kit => + if item.Definition == GlobalDefinitions.remote_electronics_kit => //TODO setup certifications check mech.Owner match { case b: Building if (b.Faction != player.Faction || b.CaptureTerminalIsHacked) && mech.HackedBy.isEmpty => @@ -57,6 +59,12 @@ class ImplantTerminalMechControl(mech: ImplantTerminalMech) case _ => ; } + def unpoweredStateLogic: Receive = + commonBehavior + .orElse { + case _ => ; + } + override protected def MountTest( obj: PlanetSideServerObject with Mountable, seatNumber: Int, @@ -98,4 +106,32 @@ class ImplantTerminalMechControl(mech: ImplantTerminalMech) } newHealth } + + override def tryAutoRepair() : Boolean = { + isPowered && super.tryAutoRepair() + } + + def powerTurnOffCallback(): Unit = { + stopAutoRepair() + //kick all occupants + val guid = mech.GUID + val zone = mech.Zone + val zoneId = zone.id + val events = zone.VehicleEvents + mech.Seats.values.foreach(seat => + seat.Occupant match { + case Some(player) => + seat.Occupant = None + player.VehicleSeated = None + if (player.HasGUID) { + events ! VehicleServiceMessage(zoneId, VehicleAction.KickPassenger(player.GUID, 4, false, guid)) + } + case None => ; + } + ) + } + + def powerTurnOnCallback(): Unit = { + tryAutoRepair() + } } diff --git a/src/main/scala/net/psforever/objects/serverobject/painbox/PainboxControl.scala b/src/main/scala/net/psforever/objects/serverobject/painbox/PainboxControl.scala index b3cd5816..8e09e30c 100644 --- a/src/main/scala/net/psforever/objects/serverobject/painbox/PainboxControl.scala +++ b/src/main/scala/net/psforever/objects/serverobject/painbox/PainboxControl.scala @@ -1,8 +1,9 @@ package net.psforever.objects.serverobject.painbox -import akka.actor.{Actor, Cancellable} +import akka.actor.Cancellable +import net.psforever.actors.zone.BuildingActor import net.psforever.objects.serverobject.doors.Door -import net.psforever.objects.serverobject.structures.Building +import net.psforever.objects.serverobject.structures.{Building, PoweredAmenityControl} import net.psforever.objects.{Default, GlobalDefinitions} import net.psforever.types.{PlanetSideEmpire, Vector3} import net.psforever.services.avatar.{AvatarAction, AvatarServiceMessage} @@ -10,7 +11,7 @@ import net.psforever.services.avatar.{AvatarAction, AvatarServiceMessage} import scala.concurrent.ExecutionContext.Implicits.global import scala.concurrent.duration._ -class PainboxControl(painbox: Painbox) extends Actor { +class PainboxControl(painbox: Painbox) extends PoweredAmenityControl { private[this] val log = org.log4s.getLogger(s"Painbox") var painboxTick: Cancellable = Default.Cancellable var nearestDoor: Option[Door] = None @@ -20,149 +21,167 @@ class PainboxControl(painbox: Painbox) extends Actor { var disabled = false // Temporary to disable cavern non-radius fields - def receive: Receive = { - case "startup" => - if (painbox.Definition.HasNearestDoorDependency) { - (painbox.Owner match { - case obj: Building => - obj.Amenities - .collect { case door: Door => door } - .sortBy(door => Vector3.DistanceSquared(painbox.Position, door.Position)) - .headOption - case _ => - None - }) match { - case door @ Some(_) => - nearestDoor = door - case _ => - log.error( - s"Painbox ${painbox.GUID} on ${painbox.Owner.Continent} - ${painbox.Position} can not find a door that it is dependent on" - ) + def initialStartup(): Unit = { + if (painbox.Definition.HasNearestDoorDependency) { + (painbox.Owner match { + case obj : Building => + obj.Amenities + .collect { case door : Door => door } + .sortBy(door => Vector3.DistanceSquared(painbox.Position, door.Position)) + .headOption + case _ => + None + }) match { + case door@Some(_) => + nearestDoor = door + case _ => + log.error( + s"Painbox ${painbox.GUID} on ${painbox.Owner.Continent} - ${painbox.Position} can not find a door that it is dependent on" + ) + } + } + else { + if (painbox.Definition.Radius == 0f) { + if (painbox.Owner.Continent.matches("c[0-9]")) { + // todo: handle non-radius painboxes in caverns properly + log.warn(s"Skipping initialization of ${painbox.GUID} on ${painbox.Owner.Continent} - ${painbox.Position}") + disabled = true } - } else { - if (painbox.Definition.Radius == 0f) { - if (painbox.Owner.Continent.matches("c[0-9]")) { - // todo: handle non-radius painboxes in caverns properly - log.warn(s"Skipping initialization of ${painbox.GUID} on ${painbox.Owner.Continent} - ${painbox.Position}") - disabled = true - } else { - painbox.Owner match { - case obj: Building => - val planarRange = 16.5f - val aboveRange = 5 - val belowRange = 5 + else { + painbox.Owner match { + case obj : Building => + val planarRange = 16.5f + val aboveRange = 5 + val belowRange = 5 + // Find amenities within the specified range + val nearbyAmenities = obj.Amenities + .filter(amenity => + amenity.Position != Vector3.Zero + && (amenity.Definition == GlobalDefinitions.mb_locker + || amenity.Definition == GlobalDefinitions.respawn_tube + || amenity.Definition == GlobalDefinitions.spawn_terminal + || amenity.Definition == GlobalDefinitions.order_terminal + || amenity.Definition == GlobalDefinitions.door) + && amenity.Position.x > painbox.Position.x - planarRange && amenity.Position.x < painbox.Position.x + planarRange + && amenity.Position.y > painbox.Position.y - planarRange && amenity.Position.y < painbox.Position.y + planarRange + && amenity.Position.z > painbox.Position.z - belowRange && amenity.Position.z < painbox.Position.z + aboveRange + ) - // Find amenities within the specified range - val nearbyAmenities = obj.Amenities - .filter(amenity => - amenity.Position != Vector3.Zero - && (amenity.Definition == GlobalDefinitions.mb_locker - || amenity.Definition == GlobalDefinitions.respawn_tube - || amenity.Definition == GlobalDefinitions.spawn_terminal - || amenity.Definition == GlobalDefinitions.order_terminal - || amenity.Definition == GlobalDefinitions.door) - && amenity.Position.x > painbox.Position.x - planarRange && amenity.Position.x < painbox.Position.x + planarRange - && amenity.Position.y > painbox.Position.y - planarRange && amenity.Position.y < painbox.Position.y + planarRange - && amenity.Position.z > painbox.Position.z - belowRange && amenity.Position.z < painbox.Position.z + aboveRange - ) - - // Calculate bounding box of amenities - bBoxMinCorner = Vector3( - nearbyAmenities.minBy(amenity => amenity.Position.x).Position.x, - nearbyAmenities.minBy(amenity => amenity.Position.y).Position.y, - nearbyAmenities.minBy(x => x.Position.z).Position.z - ) - bBoxMaxCorner = Vector3( - nearbyAmenities.maxBy(amenity => amenity.Position.x).Position.x, - nearbyAmenities.maxBy(amenity => amenity.Position.y).Position.y, - painbox.Position.z - ) - bBoxMidPoint = Vector3( - (bBoxMinCorner.x + bBoxMaxCorner.x) / 2, - (bBoxMinCorner.y + bBoxMaxCorner.y) / 2, - (bBoxMinCorner.z + bBoxMaxCorner.z) / 2 - ) - case _ => None - } + // Calculate bounding box of amenities + bBoxMinCorner = Vector3( + nearbyAmenities.minBy(amenity => amenity.Position.x).Position.x, + nearbyAmenities.minBy(amenity => amenity.Position.y).Position.y, + nearbyAmenities.minBy(x => x.Position.z).Position.z + ) + bBoxMaxCorner = Vector3( + nearbyAmenities.maxBy(amenity => amenity.Position.x).Position.x, + nearbyAmenities.maxBy(amenity => amenity.Position.y).Position.y, + painbox.Position.z + ) + bBoxMidPoint = Vector3( + (bBoxMinCorner.x + bBoxMaxCorner.x) / 2, + (bBoxMinCorner.y + bBoxMaxCorner.y) / 2, + (bBoxMinCorner.z + bBoxMaxCorner.z) / 2 + ) + case _ => None } } } - - if (!disabled) { - context.become(Stopped) - } - - case _ => ; + } + if (!disabled) { + self ! BuildingActor.PowerOff() + } } - def Running: Receive = { + var commonBehavior: Receive = { + case "startup" => + if (bBoxMidPoint == Vector3.Zero) { + initialStartup() + } + case Painbox.Stop() => - context.become(Stopped) painboxTick.cancel() painboxTick = Default.Cancellable + } - case Painbox.Tick() => - //todo: Account for overlapping pain fields - //todo: Pain module - //todo: REK boosting - val guid = painbox.GUID - val owner = painbox.Owner.asInstanceOf[Building] - val faction = owner.Faction - if ( - faction != PlanetSideEmpire.NEUTRAL && (nearestDoor match { - case Some(door) => door.Open.nonEmpty; - case _ => true - }) - ) { - val events = painbox.Zone.AvatarEvents - val damage = painbox.Definition.Damage - val radius = painbox.Definition.Radius * painbox.Definition.Radius - val position = painbox.Position + def poweredStateLogic: Receive = + commonBehavior + .orElse { + case Painbox.Start() if isPowered => + painboxTick.cancel() + painboxTick = context.system.scheduler.scheduleWithFixedDelay(0 seconds, 1 second, self, Painbox.Tick()) - if (painbox.Definition.Radius != 0f) { - // Spherical pain field - owner.PlayersInSOI - .collect { - case p - if p.Faction != faction - && p.Health > 0 - && Vector3.DistanceSquared(p.Position, position) < radius => - events ! AvatarServiceMessage(p.Name, AvatarAction.EnvironmentalDamage(p.GUID, guid, damage)) - } - } else { - // Bounding box pain field - owner.PlayersInSOI - .collect { - case p - if p.Faction != faction - && p.Health > 0 => - /* - This may be cpu intensive with a large number of players in SOI. Further performance tweaking may be required - The bounding box is calculated aligned to the world XY axis, instead of rotating the painbox corners to match the base rotation - we instead rotate the player's current coordinates to match the base rotation, which allows for much simplified checking of if the player is - within the bounding box - */ - val playerRot = - Vector3.PlanarRotateAroundPoint(p.Position, bBoxMidPoint, painbox.Owner.Orientation.z.toRadians) - if ( - bBoxMinCorner.x <= playerRot.x && playerRot.x <= bBoxMaxCorner.x && bBoxMinCorner.y <= playerRot.y && playerRot.y <= bBoxMaxCorner.y - && playerRot.z >= bBoxMinCorner.z && playerRot.z <= bBoxMaxCorner.z - ) { - events ! AvatarServiceMessage(p.Name, AvatarAction.EnvironmentalDamage(p.GUID, guid, damage)) + case Painbox.Tick() => + //todo: Account for overlapping pain fields + //todo: Pain module + //todo: REK boosting + val guid = painbox.GUID + val owner = painbox.Owner.asInstanceOf[Building] + val faction = owner.Faction + if ( + isPowered && faction != PlanetSideEmpire.NEUTRAL && (nearestDoor match { + case Some(door) => door.Open.nonEmpty; + case _ => true + }) + ) { + val events = painbox.Zone.AvatarEvents + val damage = painbox.Definition.Damage + val radius = painbox.Definition.Radius * painbox.Definition.Radius + val position = painbox.Position + + if (painbox.Definition.Radius != 0f) { + // Spherical pain field + owner.PlayersInSOI + .collect { + case p + if p.Faction != faction + && p.Health > 0 + && Vector3.DistanceSquared(p.Position, position) < radius => + events ! AvatarServiceMessage(p.Name, AvatarAction.EnvironmentalDamage(p.GUID, guid, damage)) + } + } else { + // Bounding box pain field + owner.PlayersInSOI + .collect { + case p + if p.Faction != faction + && p.Health > 0 => + /* + This may be cpu intensive with a large number of players in SOI. Further performance tweaking may be required + The bounding box is calculated aligned to the world XY axis, instead of rotating the painbox corners to match the base rotation + we instead rotate the player's current coordinates to match the base rotation, which allows for much simplified checking of if the player is + within the bounding box + */ + val playerRot = + Vector3.PlanarRotateAroundPoint(p.Position, bBoxMidPoint, painbox.Owner.Orientation.z.toRadians) + if ( + bBoxMinCorner.x <= playerRot.x && playerRot.x <= bBoxMaxCorner.x && bBoxMinCorner.y <= playerRot.y && playerRot.y <= bBoxMaxCorner.y + && playerRot.z >= bBoxMinCorner.z && playerRot.z <= bBoxMaxCorner.z + ) { + events ! AvatarServiceMessage(p.Name, AvatarAction.EnvironmentalDamage(p.GUID, guid, damage)) + } } } - } + } + + case _ => ; } - case _ => ; + def unpoweredStateLogic: Receive = + commonBehavior + .orElse { + case _ => ; + } + + def powerTurnOffCallback(): Unit = { + self ! Painbox.Stop() } - def Stopped: Receive = { - case Painbox.Start() => - context.become(Running) - painboxTick.cancel() - painboxTick = context.system.scheduler.scheduleWithFixedDelay(0 seconds, 1 second, self, Painbox.Tick()) - - case _ => ; + def powerTurnOnCallback(): Unit = { + painbox.Owner match { + case b: Building if b.PlayersInSOI.nonEmpty => + self ! Painbox.Start() + case _ => ; + } } } diff --git a/src/main/scala/net/psforever/objects/serverobject/repair/AmenityAutoRepair.scala b/src/main/scala/net/psforever/objects/serverobject/repair/AmenityAutoRepair.scala index 22d45abe..64b925d6 100644 --- a/src/main/scala/net/psforever/objects/serverobject/repair/AmenityAutoRepair.scala +++ b/src/main/scala/net/psforever/objects/serverobject/repair/AmenityAutoRepair.scala @@ -129,10 +129,25 @@ trait AmenityAutoRepair /** * Attempt to start auto-repair operation * only if no operation is currently being processed. + * @see `actuallyTryAutoRepair` * @return `true`, if the auto-repair process started specifically due to this call; * `false`, if it was already started, or did not start */ - final def tryAutoRepair(): Boolean = { + def tryAutoRepair(): Boolean = { + actuallyTryAutoRepair() + } + + /** + * Attempt to start auto-repair operation + * only if no operation is currently being processed. + * In case that an override to the normals operations of `tryAutoRepair` is necessary, + * but the superclass can not be invoked, + * this method is the backup of those operations to initiate auto-repair. + * @see `tryAutoRepair` + * @return `true`, if the auto-repair process started specifically due to this call; + * `false`, if it was already started, or did not start + */ + final def actuallyTryAutoRepair(): Boolean = { val before = autoRepairTimer.isCancelled autoRepairStartFunc() !(before || autoRepairTimer.isCancelled) diff --git a/src/main/scala/net/psforever/objects/serverobject/resourcesilo/ResourceSiloControl.scala b/src/main/scala/net/psforever/objects/serverobject/resourcesilo/ResourceSiloControl.scala index f726b57d..673829f8 100644 --- a/src/main/scala/net/psforever/objects/serverobject/resourcesilo/ResourceSiloControl.scala +++ b/src/main/scala/net/psforever/objects/serverobject/resourcesilo/ResourceSiloControl.scala @@ -113,18 +113,9 @@ class ResourceSiloControl(resourceSilo: ResourceSilo) LowNtuWarning(enabled = true) } if (resourceSilo.NtuCapacitor == 0 && siloChargeBeforeChange > 0) { - // Oops, someone let the base run out of power. Shut it all down. - zone.AvatarEvents ! AvatarServiceMessage(zone.id, AvatarAction.PlanetsideAttribute(building.GUID, 48, 1)) building.Actor ! BuildingActor.NtuDepleted() building.Actor ! BuildingActor.AmenityStateChange(resourceSilo) - building.Actor ! BuildingActor.SetFaction(PlanetSideEmpire.NEUTRAL) } else if (siloChargeBeforeChange == 0 && resourceSilo.NtuCapacitor > 0) { - // Power restored. Reactor Online. Sensors Online. Weapons Online. All systems nominal. - //todo: Check generator is online before starting up - zone.AvatarEvents ! AvatarServiceMessage( - zone.id, - AvatarAction.PlanetsideAttribute(building.GUID, 48, 0) - ) building.Actor ! BuildingActor.SuppliedWithNtu() building.Actor ! BuildingActor.AmenityStateChange(resourceSilo) } 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 e1ba19fa..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 } @@ -114,6 +102,13 @@ class Building( } } + def Generator: Option[Generator] = { + Amenities.find(_.isInstanceOf[Generator]) match { + case Some(obj: Generator) => Some(obj) + case _ => None + } + } + def CaptureTerminal: Option[CaptureTerminal] = { Amenities.find(_.isInstanceOf[CaptureTerminal]) match { case Some(term) => Some(term.asInstanceOf[CaptureTerminal]) @@ -129,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 { @@ -187,8 +151,8 @@ class Building( (false, PlanetSideEmpire.NEUTRAL, 0L) } //if we have no generator, assume the state is "Normal" - val (generatorState, boostGeneratorPain) = Amenities.find(x => x.isInstanceOf[Generator]) match { - case Some(obj: Generator) => + val (generatorState, boostGeneratorPain) = Generator match { + case Some(obj) => (obj.Condition, false) // todo: poll pain field strength case _ => (PlanetSideGeneratorState.Normal, false) diff --git a/src/main/scala/net/psforever/objects/serverobject/structures/PoweredAmenityControl.scala b/src/main/scala/net/psforever/objects/serverobject/structures/PoweredAmenityControl.scala new file mode 100644 index 00000000..c73401bf --- /dev/null +++ b/src/main/scala/net/psforever/objects/serverobject/structures/PoweredAmenityControl.scala @@ -0,0 +1,39 @@ +// Copyright (c) 2020 PSForever +package net.psforever.objects.serverobject.structures + +import akka.actor.Actor +import net.psforever.actors.zone.BuildingActor + +trait PoweredAmenityControl extends Actor { + private var powered: Boolean = true + + final def receive: Receive = powerOnCondition + + final def powerOnCondition: Receive = { + case BuildingActor.PowerOff() => + powered = false + context.become(powerOffCondition) + powerTurnOffCallback() + case msg => + poweredStateLogic.apply(msg) + } + + final def powerOffCondition: Receive = { + case BuildingActor.PowerOn() => + powered = true + context.become(powerOnCondition) + powerTurnOnCallback() + case msg => + unpoweredStateLogic.apply(msg) + } + + def isPowered: Boolean = powered + + def poweredStateLogic: Receive + + def unpoweredStateLogic: Receive + + def powerTurnOnCallback(): Unit + + def powerTurnOffCallback(): Unit +} diff --git a/src/main/scala/net/psforever/objects/serverobject/terminals/ProximityTerminalControl.scala b/src/main/scala/net/psforever/objects/serverobject/terminals/ProximityTerminalControl.scala index 0ff531eb..04ded0e6 100644 --- a/src/main/scala/net/psforever/objects/serverobject/terminals/ProximityTerminalControl.scala +++ b/src/main/scala/net/psforever/objects/serverobject/terminals/ProximityTerminalControl.scala @@ -1,14 +1,17 @@ // Copyright (c) 2017 PSForever package net.psforever.objects.serverobject.terminals -import akka.actor.{Actor, ActorRef, Cancellable} +import akka.actor.{ActorRef, Cancellable} import net.psforever.objects._ import net.psforever.objects.serverobject.CommonMessages import net.psforever.objects.serverobject.affinity.FactionAffinityBehavior +import net.psforever.objects.serverobject.damage.Damageable.Target import net.psforever.objects.serverobject.damage.DamageableAmenity import net.psforever.objects.serverobject.hackable.{GenericHackables, HackableBehavior} -import net.psforever.objects.serverobject.repair.RepairableAmenity -import net.psforever.objects.serverobject.structures.Building +import net.psforever.objects.serverobject.repair.{AmenityAutoRepair, RepairableAmenity} +import net.psforever.objects.serverobject.structures.{Building, PoweredAmenityControl} +import net.psforever.services.Service +import net.psforever.services.local.{LocalAction, LocalServiceMessage} import scala.collection.mutable import scala.concurrent.duration._ @@ -20,26 +23,38 @@ import scala.concurrent.duration._ * @param term the proximity unit (terminal) */ class ProximityTerminalControl(term: Terminal with ProximityUnit) - extends Actor + extends PoweredAmenityControl with FactionAffinityBehavior.Check with HackableBehavior.GenericHackable with DamageableAmenity - with RepairableAmenity { + with RepairableAmenity + with AmenityAutoRepair { def FactionObject = term def HackableObject = term def TerminalObject = term def DamageableObject = term def RepairableObject = term + def AutoRepairObject = term var terminalAction: Cancellable = Default.Cancellable val callbacks: mutable.ListBuffer[ActorRef] = new mutable.ListBuffer[ActorRef]() val log = org.log4s.getLogger - def receive: Receive = - checkBehavior + val commonBehavior: Receive = checkBehavior + .orElse(takesDamage) + .orElse(canBeRepairedByNanoDispenser) + .orElse(autoRepairBehavior) + .orElse { + case CommonMessages.Unuse(_, Some(target: PlanetSideGameObject)) => + Unuse(target, term.Continent) + + case CommonMessages.Unuse(_, _) => + log.warn(s"unexpected format for CommonMessages.Unuse in this context") + } + + def poweredStateLogic: Receive = + commonBehavior .orElse(hackableBehavior) - .orElse(takesDamage) - .orElse(canBeRepairedByNanoDispenser) .orElse { case CommonMessages.Use(player, Some(item: SimpleItem)) if item.Definition == GlobalDefinitions.remote_electronics_kit => @@ -66,12 +81,6 @@ class ProximityTerminalControl(term: Terminal with ProximityUnit) case CommonMessages.Use(_, _) => log.warn(s"unexpected format for CommonMessages.Use in this context") - case CommonMessages.Unuse(_, Some(target: PlanetSideGameObject)) => - Unuse(target, term.Continent) - - case CommonMessages.Unuse(_, _) => - log.warn(s"unexpected format for CommonMessages.Unuse in this context") - case ProximityTerminalControl.TerminalAction() => val proxDef = term.Definition.asInstanceOf[ProximityDefinition] val validateFunc: PlanetSideGameObject => Boolean = @@ -99,6 +108,31 @@ class ProximityTerminalControl(term: Terminal with ProximityUnit) case _ => } + def unpoweredStateLogic : Receive = commonBehavior + .orElse { + case CommonMessages.Use(_, _) => + log.warn(s"unexpected format for CommonMessages.Use in this context") + + case CommonMessages.Unuse(_, Some(target: PlanetSideGameObject)) => + Unuse(target, term.Continent) + + case CommonMessages.Unuse(_, _) => + log.warn(s"unexpected format for CommonMessages.Unuse in this context") + case _ => ; + } + + override def PerformRepairs(target : Target, amount : Int) : Int = { + val newHealth = super.PerformRepairs(target, amount) + if(newHealth == target.Definition.MaxHealth) { + stopAutoRepair() + } + newHealth + } + + override def tryAutoRepair() : Boolean = { + isPowered && super.tryAutoRepair() + } + def Use(target: PlanetSideGameObject, zone: String, callback: ActorRef): Unit = { val hadNoUsers = term.NumberUsers == 0 if (term.AddUser(target)) { @@ -145,6 +179,25 @@ class ProximityTerminalControl(term: Terminal with ProximityUnit) } } + def powerTurnOffCallback() : Unit = { + stopAutoRepair() + //clear effect callbacks + terminalAction.cancel() + if (callbacks.nonEmpty) { + callbacks.clear() + TerminalObject.Zone.LocalEvents ! Terminal.StopProximityEffect(term) + } + //clear hack state + if (term.HackedBy.nonEmpty) { + val zone = term.Zone + zone.LocalEvents ! LocalServiceMessage(zone.id, LocalAction.ClearTemporaryHack(Service.defaultPlayerGUID, term)) + } + } + + def powerTurnOnCallback() : Unit = { + tryAutoRepair() + } + override def toString: String = term.Definition.Name } diff --git a/src/main/scala/net/psforever/objects/serverobject/terminals/TerminalControl.scala b/src/main/scala/net/psforever/objects/serverobject/terminals/TerminalControl.scala index f052507b..fcfdba3c 100644 --- a/src/main/scala/net/psforever/objects/serverobject/terminals/TerminalControl.scala +++ b/src/main/scala/net/psforever/objects/serverobject/terminals/TerminalControl.scala @@ -1,7 +1,7 @@ // Copyright (c) 2017 PSForever package net.psforever.objects.serverobject.terminals -import akka.actor.{Actor, ActorRef} +import akka.actor.ActorRef import net.psforever.objects.ballistics.ResolvedProjectile import net.psforever.objects.{GlobalDefinitions, SimpleItem} import net.psforever.objects.serverobject.CommonMessages @@ -10,14 +10,16 @@ import net.psforever.objects.serverobject.damage.Damageable.Target import net.psforever.objects.serverobject.damage.{Damageable, DamageableAmenity} import net.psforever.objects.serverobject.hackable.{GenericHackables, HackableBehavior} import net.psforever.objects.serverobject.repair.{AmenityAutoRepair, RepairableAmenity} -import net.psforever.objects.serverobject.structures.Building +import net.psforever.objects.serverobject.structures.{Building, PoweredAmenityControl} +import net.psforever.services.Service +import net.psforever.services.local.{LocalAction, LocalServiceMessage} /** * An `Actor` that handles messages being dispatched to a specific `Terminal`. * @param term the `Terminal` object being governed */ class TerminalControl(term: Terminal) - extends Actor + extends PoweredAmenityControl with FactionAffinityBehavior.Check with HackableBehavior.GenericHackable with DamageableAmenity @@ -29,18 +31,20 @@ class TerminalControl(term: Terminal) def RepairableObject = term def AutoRepairObject = term - def receive: Receive = - checkBehavior + val commonBehavior: Receive = checkBehavior + .orElse(takesDamage) + .orElse(canBeRepairedByNanoDispenser) + .orElse(autoRepairBehavior) + + def poweredStateLogic : Receive = + commonBehavior .orElse(hackableBehavior) - .orElse(takesDamage) - .orElse(canBeRepairedByNanoDispenser) - .orElse(autoRepairBehavior) .orElse { case Terminal.Request(player, msg) => TerminalControl.Dispatch(sender(), term, Terminal.TerminalMessage(player, msg, term.Request(player, msg))) case CommonMessages.Use(player, Some(item: SimpleItem)) - if item.Definition == GlobalDefinitions.remote_electronics_kit => + if item.Definition == GlobalDefinitions.remote_electronics_kit => //TODO setup certifications check term.Owner match { case b: Building if (b.Faction != player.Faction || b.CaptureTerminalIsHacked) && term.HackedBy.isEmpty => @@ -55,6 +59,14 @@ class TerminalControl(term: Terminal) case _ => ; } + def unpoweredStateLogic : Receive = commonBehavior + .orElse { + case Terminal.Request(player, msg) => + sender() ! Terminal.TerminalMessage(player, msg, Terminal.NoDeal()) + + case _ => ; + } + override protected def DamageAwareness(target : Target, cause : ResolvedProjectile, amount : Any) : Unit = { tryAutoRepair() super.DamageAwareness(target, cause, amount) @@ -62,6 +74,10 @@ class TerminalControl(term: Terminal) override protected def DestructionAwareness(target: Damageable.Target, cause: ResolvedProjectile) : Unit = { tryAutoRepair() + if (term.HackedBy.nonEmpty) { + val zone = term.Zone + zone.LocalEvents ! LocalServiceMessage(zone.id, LocalAction.ClearTemporaryHack(Service.defaultPlayerGUID, term)) + } super.DestructionAwareness(target, cause) } @@ -73,6 +89,23 @@ class TerminalControl(term: Terminal) newHealth } + override def tryAutoRepair() : Boolean = { + isPowered && super.tryAutoRepair() + } + + def powerTurnOffCallback() : Unit = { + stopAutoRepair() + //clear hack state + if (term.HackedBy.nonEmpty) { + val zone = term.Zone + zone.LocalEvents ! LocalServiceMessage(zone.id, LocalAction.ClearTemporaryHack(Service.defaultPlayerGUID, term)) + } + } + + def powerTurnOnCallback() : Unit = { + tryAutoRepair() + } + override def toString: String = term.Definition.Name } diff --git a/src/main/scala/net/psforever/objects/serverobject/tube/SpawnTube.scala b/src/main/scala/net/psforever/objects/serverobject/tube/SpawnTube.scala index caf81802..dd754d0d 100644 --- a/src/main/scala/net/psforever/objects/serverobject/tube/SpawnTube.scala +++ b/src/main/scala/net/psforever/objects/serverobject/tube/SpawnTube.scala @@ -10,6 +10,10 @@ import net.psforever.objects.serverobject.structures.Amenity * @param tDef the `ObjectDefinition` that constructs this object and maintains some of its immutable fields */ class SpawnTube(tDef: SpawnTubeDefinition) extends Amenity with SpawnPoint { + var offline: Boolean = false + + override def isOffline: Boolean = offline || super.isOffline + def Definition: SpawnTubeDefinition = tDef } diff --git a/src/main/scala/net/psforever/objects/serverobject/tube/SpawnTubeControl.scala b/src/main/scala/net/psforever/objects/serverobject/tube/SpawnTubeControl.scala index 38a9fde8..c7607bc1 100644 --- a/src/main/scala/net/psforever/objects/serverobject/tube/SpawnTubeControl.scala +++ b/src/main/scala/net/psforever/objects/serverobject/tube/SpawnTubeControl.scala @@ -1,21 +1,20 @@ // Copyright (c) 2017 PSForever package net.psforever.objects.serverobject.tube -import akka.actor.Actor import net.psforever.actors.zone.BuildingActor import net.psforever.objects.ballistics.ResolvedProjectile import net.psforever.objects.serverobject.affinity.FactionAffinityBehavior import net.psforever.objects.serverobject.damage.Damageable.Target import net.psforever.objects.serverobject.damage.DamageableAmenity import net.psforever.objects.serverobject.repair.{AmenityAutoRepair, Repairable, RepairableAmenity} -import net.psforever.objects.serverobject.structures.Building +import net.psforever.objects.serverobject.structures.{Building, PoweredAmenityControl} /** * An `Actor` that handles messages being dispatched to a specific `SpawnTube`. * @param tube the `SpawnTube` object being governed */ class SpawnTubeControl(tube: SpawnTube) - extends Actor + extends PoweredAmenityControl with FactionAffinityBehavior.Check with DamageableAmenity with RepairableAmenity @@ -25,11 +24,19 @@ class SpawnTubeControl(tube: SpawnTube) def RepairableObject = tube def AutoRepairObject = tube - def receive: Receive = - checkBehavior - .orElse(takesDamage) - .orElse(canBeRepairedByNanoDispenser) - .orElse(autoRepairBehavior) + val commonBehavior: Receive = checkBehavior + .orElse(takesDamage) + .orElse(canBeRepairedByNanoDispenser) + .orElse(autoRepairBehavior) + + def poweredStateLogic: Receive = + commonBehavior + .orElse { + case _ => ; + } + + def unpoweredStateLogic: Receive = + commonBehavior .orElse { case _ => ; } @@ -64,5 +71,27 @@ class SpawnTubeControl(tube: SpawnTube) } } + override def tryAutoRepair() : Boolean = { + isPowered && super.tryAutoRepair() + } + + def powerTurnOffCallback(): Unit = { + tube.offline = false + stopAutoRepair() + tube.Owner match { + case b: Building => b.Actor ! BuildingActor.AmenityStateChange(tube) + case _ => ; + } + } + + def powerTurnOnCallback(): Unit = { + tube.offline = true + tryAutoRepair() + tube.Owner match { + case b: Building => b.Actor ! BuildingActor.AmenityStateChange(tube) + case _ => ; + } + } + override def toString: String = tube.Definition.Name } diff --git a/src/main/scala/net/psforever/objects/serverobject/turret/FacilityTurretControl.scala b/src/main/scala/net/psforever/objects/serverobject/turret/FacilityTurretControl.scala index 9e23e450..3ae7f160 100644 --- a/src/main/scala/net/psforever/objects/serverobject/turret/FacilityTurretControl.scala +++ b/src/main/scala/net/psforever/objects/serverobject/turret/FacilityTurretControl.scala @@ -1,7 +1,6 @@ // Copyright (c) 2017 PSForever package net.psforever.objects.serverobject.turret -import akka.actor.Actor import net.psforever.objects.ballistics.ResolvedProjectile import net.psforever.objects.{Default, GlobalDefinitions, Player, Tool} import net.psforever.objects.equipment.{Ammo, JammableMountedWeapons} @@ -11,8 +10,10 @@ import net.psforever.objects.serverobject.affinity.FactionAffinityBehavior import net.psforever.objects.serverobject.damage.{Damageable, DamageableWeaponTurret} import net.psforever.objects.serverobject.hackable.GenericHackables import net.psforever.objects.serverobject.repair.{AmenityAutoRepair, RepairableWeaponTurret} +import net.psforever.objects.serverobject.structures.PoweredAmenityControl import net.psforever.services.avatar.{AvatarAction, AvatarServiceMessage} import net.psforever.services.local.{LocalAction, LocalServiceMessage} +import net.psforever.services.vehicle.{VehicleAction, VehicleServiceMessage} import scala.concurrent.ExecutionContext.Implicits.global import scala.concurrent.duration._ @@ -27,7 +28,7 @@ import scala.concurrent.duration._ * @param turret the `MannedTurret` object being governed */ class FacilityTurretControl(turret: FacilityTurret) - extends Actor + extends PoweredAmenityControl with FactionAffinityBehavior.Check with MountableBehavior.TurretMount with MountableBehavior.Dismount @@ -51,14 +52,17 @@ class FacilityTurretControl(turret: FacilityTurret) stopAutoRepair() } - def receive: Receive = + def commonBehavior: Receive = checkBehavior .orElse(jammableBehavior) - .orElse(mountBehavior) .orElse(dismountBehavior) .orElse(takesDamage) .orElse(canBeRepairedByNanoDispenser) .orElse(autoRepairBehavior) + + def poweredStateLogic: Receive = + commonBehavior + .orElse(mountBehavior) .orElse { case CommonMessages.Use(player, Some((item: Tool, upgradeValue: Int))) if player.Faction == turret.Faction && @@ -113,6 +117,12 @@ class FacilityTurretControl(turret: FacilityTurret) case _ => ; } + def unpoweredStateLogic: Receive = + commonBehavior + .orElse { + case _ => ; + } + override protected def DamageAwareness(target : Damageable.Target, cause : ResolvedProjectile, amount : Any) : Unit = { tryAutoRepair() super.DamageAwareness(target, cause, amount) @@ -146,4 +156,32 @@ class FacilityTurretControl(turret: FacilityTurret) events ! AvatarServiceMessage(zoneId, AvatarAction.PlanetsideAttributeToAll(tguid, 50, 0)) events ! AvatarServiceMessage(zoneId, AvatarAction.PlanetsideAttributeToAll(tguid, 51, 0)) } + + override def tryAutoRepair() : Boolean = { + isPowered && super.tryAutoRepair() + } + + def powerTurnOffCallback(): Unit = { + stopAutoRepair() + //kick all occupants + val guid = turret.GUID + val zone = turret.Zone + val zoneId = zone.id + val events = zone.VehicleEvents + turret.Seats.values.foreach(seat => + seat.Occupant match { + case Some(player) => + seat.Occupant = None + player.VehicleSeated = None + if (player.HasGUID) { + events ! VehicleServiceMessage(zoneId, VehicleAction.KickPassenger(player.GUID, 4, false, guid)) + } + case None => ; + } + ) + } + + def powerTurnOnCallback(): Unit = { + tryAutoRepair() + } } diff --git a/src/main/scala/net/psforever/objects/zones/Zone.scala b/src/main/scala/net/psforever/objects/zones/Zone.scala index bc1a2e23..c9487602 100644 --- a/src/main/scala/net/psforever/objects/zones/Zone.scala +++ b/src/main/scala/net/psforever/objects/zones/Zone.scala @@ -354,8 +354,8 @@ class Zone(val id: String, val map: ZoneMap, zoneNumber: Int) { .filter { case (building, spawns) => spawns.nonEmpty && - spawns.exists(_.Offline == false) && - structures.contains(building.BuildingType) + spawns.exists(_.isOffline == false) && + structures.contains(building.BuildingType) } .filter { case (building, _) => @@ -368,7 +368,7 @@ class Zone(val id: String, val map: ZoneMap, zoneNumber: Int) { } .map { case (building, spawns: List[SpawnPoint]) => - (building, spawns.filter(!_.Offline)) + (building, spawns.filter(!_.isOffline)) } .concat( (if (ams) Vehicles else List()) diff --git a/src/main/scala/net/psforever/packet/game/PlanetsideAttributeMessage.scala b/src/main/scala/net/psforever/packet/game/PlanetsideAttributeMessage.scala index a1539e79..d3a8044f 100644 --- a/src/main/scala/net/psforever/packet/game/PlanetsideAttributeMessage.scala +++ b/src/main/scala/net/psforever/packet/game/PlanetsideAttributeMessage.scala @@ -144,7 +144,7 @@ import scodec.codecs._ * `45 - NTU charge bar 0-10, 5 = 50% full. Seems to apply to both ANT and NTU Silo (possibly siphons?)`
* `46 - Sends "Generator damage is at a critical level!" message` * `47 - Sets base NTU level to CRITICAL. MUST use base MapId not base GUID`
- * `48 - Set to 1 to send base power loss message & turns on red warning lights throughout base. MUST use base MapId not base GUID`
+ * `48 - Set to 1 to send base power loss message & turns on red warning lights throughout base. MUST use base MapId not base GUID`?
* `49 - Vehicle texture effects state? (>0 turns on ANT panel glow or ntu silo panel glow + orbs) (bit?)`
* `52 - Vehicle particle effects? (>0 turns on orbs going towards ANT. Doesn't affect silo) (bit?)`
* `53 - LFS. Value is 1 to flag LFS`
diff --git a/src/test/scala/objects/AutoRepairIntegrationTest.scala b/src/test/scala/objects/AutoRepairIntegrationTest.scala index 58194308..188847ca 100644 --- a/src/test/scala/objects/AutoRepairIntegrationTest.scala +++ b/src/test/scala/objects/AutoRepairIntegrationTest.scala @@ -4,6 +4,7 @@ package objects import akka.actor.Props import akka.testkit.TestProbe import base.FreedContextActorTest +import net.psforever.actors.zone.BuildingActor import net.psforever.objects.avatar.Avatar import net.psforever.objects.ballistics.{Projectile, ProjectileResolution, ResolvedProjectile, SourceEntry} import net.psforever.objects.guid.NumberPoolHub @@ -57,6 +58,7 @@ class AutoRepairFacilityIntegrationTest extends FreedContextActorTest { silo.NtuCapacitor = 1000 silo.Actor = system.actorOf(Props(classOf[ResourceSiloControl], silo), "test-silo") silo.Actor ! "startup" + building.Actor ! BuildingActor.PowerOn() //artificial val wep_fmode = weapon.FireMode val wep_prof = wep_fmode.Add @@ -158,7 +160,7 @@ object AutoRepairIntegrationTest { MaxHealth = 500 Damageable = true Repairable = true - autoRepair = AutoRepairStats(1, 500, 500, 1) + autoRepair = AutoRepairStats(200, 500, 500, 1) RepairIfDestroyed = true } } diff --git a/src/test/scala/objects/DoorTest.scala b/src/test/scala/objects/DoorTest.scala index e1ef43bb..bc908e66 100644 --- a/src/test/scala/objects/DoorTest.scala +++ b/src/test/scala/objects/DoorTest.scala @@ -2,17 +2,22 @@ package objects import akka.actor.{ActorSystem, Props} +import akka.testkit.TestProbe import base.ActorTest import net.psforever.objects.avatar.Avatar +import net.psforever.objects.guid.NumberPoolHub +import net.psforever.objects.guid.source.MaxNumberSource +import net.psforever.objects.serverobject.CommonMessages import net.psforever.objects.{Default, GlobalDefinitions, Player} import net.psforever.objects.serverobject.doors.{Door, DoorControl} import net.psforever.objects.serverobject.structures.{Building, StructureType} -import net.psforever.objects.zones.Zone +import net.psforever.objects.zones.{Zone, ZoneMap} import net.psforever.packet.game.UseItemMessage +import net.psforever.services.local.{LocalAction, LocalServiceMessage} import net.psforever.types._ import org.specs2.mutable.Specification -import scala.concurrent.duration.Duration +import scala.concurrent.duration._ class DoorTest extends Specification { val player = Player(Avatar(0, "test", PlanetSideEmpire.TR, CharacterGender.Male, 0, CharacterVoice.Mute)) @@ -59,10 +64,12 @@ class DoorTest extends Specification { ) val door = Door(GlobalDefinitions.door) door.Open.isEmpty mustEqual true - door.Use(player, msg) + door.Open = player + door.isOpen mustEqual true door.Open.contains(player) mustEqual true - door.Use(player, msg) + door.Open = None door.Open.isEmpty mustEqual true + door.isOpen mustEqual false } } } @@ -81,6 +88,8 @@ class DoorControl2Test extends ActorTest { "DoorControl" should { "open on use" in { val (player, door) = DoorControlTest.SetUpAgents(PlanetSideEmpire.TR) + val probe = new TestProbe(system) + door.Zone.LocalEvents = probe.ref val msg = UseItemMessage( PlanetSideGUID(1), PlanetSideGUID(0), @@ -96,13 +105,12 @@ class DoorControl2Test extends ActorTest { ) //faked assert(door.Open.isEmpty) - door.Actor ! Door.Use(player, msg) - val reply = receiveOne(Duration.create(500, "ms")) - assert(reply.isInstanceOf[Door.DoorMessage]) - val reply2 = reply.asInstanceOf[Door.DoorMessage] - assert(reply2.player == player) - assert(reply2.msg == msg) - assert(reply2.response == Door.OpenEvent()) + door.Actor ! CommonMessages.Use(player, Some(msg)) + val reply = probe.receiveOne(1000 milliseconds) + assert(reply match { + case LocalServiceMessage("test", LocalAction.DoorOpens(PlanetSideGUID(0), _, d)) => d eq door + case _ => false + }) assert(door.Open.isDefined) } } @@ -124,16 +132,24 @@ class DoorControl3Test extends ActorTest { object DoorControlTest { def SetUpAgents(faction: PlanetSideEmpire.Value)(implicit system: ActorSystem): (Player, Door) = { val door = Door(GlobalDefinitions.door) + val guid = new NumberPoolHub(new MaxNumberSource(5)) + val zone = new Zone("test", new ZoneMap("test"), 0) { + override def SetupNumberPools() = {} + GUID(guid) + } + guid.register(door, 1) door.Actor = system.actorOf(Props(classOf[DoorControl], door), "door") door.Owner = new Building( "Building", building_guid = 0, map_id = 0, - Zone.Nowhere, + zone, StructureType.Building, GlobalDefinitions.building ) door.Owner.Faction = faction - (Player(Avatar(0, "test", faction, CharacterGender.Male, 0, CharacterVoice.Mute)), door) + val player = Player(Avatar(0, "test", faction, CharacterGender.Male, 0, CharacterVoice.Mute)) + guid.register(player, 2) + (player, door) } } diff --git a/src/test/scala/objects/GeneratorTest.scala b/src/test/scala/objects/GeneratorTest.scala index 91ea0926..436164ec 100644 --- a/src/test/scala/objects/GeneratorTest.scala +++ b/src/test/scala/objects/GeneratorTest.scala @@ -11,7 +11,7 @@ import net.psforever.objects.{GlobalDefinitions, Player, Tool} import net.psforever.objects.guid.NumberPoolHub import net.psforever.objects.guid.source.MaxNumberSource import net.psforever.objects.serverobject.CommonMessages -import net.psforever.objects.serverobject.generator.{Generator, GeneratorControl} +import net.psforever.objects.serverobject.generator.{Generator, GeneratorControl, GeneratorDefinition} import net.psforever.objects.serverobject.structures.{Building, StructureType} import net.psforever.objects.vital.Vitality import net.psforever.objects.zones.{Zone, ZoneMap} @@ -25,12 +25,12 @@ import scala.concurrent.duration._ class GeneratorTest extends Specification { "Generator" should { "construct" in { - Generator(GlobalDefinitions.generator) + Generator(GeneratorTest.generator_definition) ok } "start in 'Normal' condition" in { - val obj = Generator(GlobalDefinitions.generator) + val obj = Generator(GeneratorTest.generator_definition) obj.Condition mustEqual PlanetSideGeneratorState.Normal } } @@ -39,7 +39,7 @@ class GeneratorTest extends Specification { class GeneratorControlConstructTest extends ActorTest { "GeneratorControl" should { "construct" in { - val gen = Generator(GlobalDefinitions.generator) + val gen = Generator(GeneratorTest.generator_definition) gen.Actor = system.actorOf(Props(classOf[GeneratorControl], gen), "gen-control") assert(gen.Actor != ActorRef.noSender) } @@ -57,7 +57,7 @@ class GeneratorControlDamageTest extends ActorTest { val activityProbe = TestProbe() zone.Activity = activityProbe.ref - val gen = Generator(GlobalDefinitions.generator) //guid=2 + val gen = Generator(GeneratorTest.generator_definition) //guid=2 gen.Position = Vector3(1, 0, 0) gen.Actor = system.actorOf(Props(classOf[GeneratorControl], gen), "generator-control") @@ -106,19 +106,18 @@ class GeneratorControlDamageTest extends ActorTest { assert(gen.Condition == PlanetSideGeneratorState.Normal) gen.Actor ! Vitality.Damage(applyDamageTo) - val msg_avatar = avatarProbe.receiveN(2, 500 milliseconds) - buildingProbe.expectNoMessage(200 milliseconds) + val msg_avatar = avatarProbe.receiveOne(500 milliseconds) + val msg_building = buildingProbe.receiveOne(500 milliseconds) assert( - msg_avatar.head match { + msg_avatar match { case AvatarServiceMessage("test", AvatarAction.PlanetsideAttributeToAll(PlanetSideGUID(2), 0, _)) => true case _ => false } ) assert( - msg_avatar(1) match { - case AvatarServiceMessage("TestCharacter1", AvatarAction.GenericObjectAction(_, PlanetSideGUID(1), 15)) => - true - case _ => false + msg_building match { + case BuildingActor.AmenityStateChange(_, Some(GeneratorControl.Event.UnderAttack)) => true + case _ => false } ) assert(gen.Health < gen.Definition.MaxHealth) @@ -139,7 +138,7 @@ class GeneratorControlCriticalTest extends ActorTest { val activityProbe = TestProbe() zone.Activity = activityProbe.ref - val gen = Generator(GlobalDefinitions.generator) //guid=2 + val gen = Generator(GeneratorTest.generator_definition) //guid=2 gen.Position = Vector3(1, 0, 0) gen.Actor = system.actorOf(Props(classOf[GeneratorControl], gen), "generator-control") @@ -190,25 +189,18 @@ class GeneratorControlCriticalTest extends ActorTest { assert(gen.Condition == PlanetSideGeneratorState.Normal) gen.Actor ! Vitality.Damage(applyDamageTo) - val msg_avatar = avatarProbe.receiveN(2, 500 milliseconds) + val msg_avatar = avatarProbe.receiveOne(500 milliseconds) val msg_building = buildingProbe.receiveOne(500 milliseconds) assert( - msg_avatar.head match { + msg_avatar match { case AvatarServiceMessage("test", AvatarAction.PlanetsideAttributeToAll(PlanetSideGUID(2), 0, _)) => true case _ => false } ) - assert( - msg_avatar(1) match { - case AvatarServiceMessage("TestCharacter1", AvatarAction.GenericObjectAction(_, PlanetSideGUID(1), 15)) => - true - case _ => false - } - ) assert( msg_building match { - case BuildingActor.AmenityStateChange(o) => o eq gen - case _ => false + case BuildingActor.AmenityStateChange(o, Some(GeneratorControl.Event.Critical)) => o eq gen + case _ => false } ) assert(gen.Health < halfHealth) @@ -229,7 +221,7 @@ class GeneratorControlDestroyedTest extends ActorTest { val activityProbe = TestProbe() zone.Activity = activityProbe.ref - val gen = Generator(GlobalDefinitions.generator) //guid=2 + val gen = Generator(GeneratorTest.generator_definition) //guid=2 gen.Position = Vector3(1, 0, 0) gen.Actor = system.actorOf(Props(classOf[GeneratorControl], gen), "generator-control") @@ -269,7 +261,6 @@ class GeneratorControlDestroyedTest extends ActorTest { Vector3(1, 0, 0) ) val applyDamageTo = resolved.damage_model.Calculate(resolved) - gen.Actor ! BuildingActor.NtuDepleted() //no auto-repair expectNoMessage(200 milliseconds) //we're not testing that the math is correct @@ -281,26 +272,30 @@ class GeneratorControlDestroyedTest extends ActorTest { assert(gen.Condition == PlanetSideGeneratorState.Normal) //skipped critical state because didn't transition ~50% gen.Actor ! Vitality.Damage(applyDamageTo) - val msg_avatar1 = avatarProbe.receiveOne(500 milliseconds) - buildingProbe.expectNoMessage(200 milliseconds) + val msg_building12 = buildingProbe.receiveN(2,500 milliseconds) assert( - msg_avatar1 match { - case AvatarServiceMessage("TestCharacter1", AvatarAction.GenericObjectAction(_, PlanetSideGUID(1), 16)) => - true + msg_building12.head match { + case BuildingActor.AmenityStateChange(o, Some(GeneratorControl.Event.Offline)) => o eq gen + case _ => false + } + ) + assert( + msg_building12(1) match { + case BuildingActor.AmenityStateChange(o, Some(GeneratorControl.Event.Destabilized)) => o eq gen case _ => false } ) assert(gen.Health == 1) assert(!gen.Destroyed) - assert(gen.Condition == PlanetSideGeneratorState.Normal) + assert(gen.Condition == PlanetSideGeneratorState.Destroyed) - avatarProbe.expectNoMessage(9 seconds) + avatarProbe.expectNoMessage(9500 milliseconds) val msg_avatar2 = avatarProbe.receiveN(3, 1000 milliseconds) //see DamageableEntity test file val msg_building = buildingProbe.receiveOne(200 milliseconds) assert( msg_building match { - case BuildingActor.AmenityStateChange(o) => o eq gen - case _ => false + case BuildingActor.AmenityStateChange(o, Some(GeneratorControl.Event.Destroyed)) => o eq gen + case _ => false } ) assert( @@ -352,7 +347,7 @@ class GeneratorControlKillsTest extends ActorTest { val activityProbe = TestProbe() zone.Activity = activityProbe.ref - val gen = Generator(GlobalDefinitions.generator) //guid=2 + val gen = Generator(GeneratorTest.generator_definition) //guid=2 gen.Position = Vector3(1, 0, 0) gen.Actor = system.actorOf(Props(classOf[GeneratorControl], gen), "generator-control") @@ -400,7 +395,6 @@ class GeneratorControlKillsTest extends ActorTest { Vector3(1, 0, 0) ) val applyDamageTo = resolved.damage_model.Calculate(resolved) - gen.Actor ! BuildingActor.NtuDepleted() //no auto-repair expectNoMessage(200 milliseconds) //we're not testing that the math is correct @@ -412,36 +406,30 @@ class GeneratorControlKillsTest extends ActorTest { assert(gen.Condition == PlanetSideGeneratorState.Normal) //skipped critical state because didn't transition ~50% gen.Actor ! Vitality.Damage(applyDamageTo) - val msg_avatar1 = avatarProbe.receiveN(2, 500 milliseconds) - buildingProbe.expectNoMessage(200 milliseconds) - player1Probe.expectNoMessage(200 milliseconds) - player2Probe.expectNoMessage(200 milliseconds) + val msg_building12 = buildingProbe.receiveN(2,500 milliseconds) assert( - msg_avatar1.head match { - case AvatarServiceMessage("TestCharacter1", AvatarAction.GenericObjectAction(_, PlanetSideGUID(1), 16)) => - true + msg_building12.head match { + case BuildingActor.AmenityStateChange(o, Some(GeneratorControl.Event.Offline)) => o eq gen case _ => false } ) assert( - msg_avatar1(1) match { - case AvatarServiceMessage("TestCharacter2", AvatarAction.GenericObjectAction(_, PlanetSideGUID(1), 16)) => - true + msg_building12(1) match { + case BuildingActor.AmenityStateChange(o, Some(GeneratorControl.Event.Destabilized)) => o eq gen case _ => false } ) assert(gen.Health == 1) assert(!gen.Destroyed) - assert(gen.Condition == PlanetSideGeneratorState.Normal) + assert(gen.Condition == PlanetSideGeneratorState.Destroyed) - val msg_building = buildingProbe.receiveOne(10500 milliseconds) - val msg_avatar2 = avatarProbe.receiveN(3, 200 milliseconds) - val msg_player1 = player1Probe.receiveOne(100 milliseconds) - player2Probe.expectNoMessage(200 milliseconds) + avatarProbe.expectNoMessage(9500 milliseconds) + val msg_avatar2 = avatarProbe.receiveN(3, 1000 milliseconds) //see DamageableEntity test file + val msg_building = buildingProbe.receiveOne(200 milliseconds) assert( msg_building match { - case BuildingActor.AmenityStateChange(o) => o eq gen - case _ => false + case BuildingActor.AmenityStateChange(o, Some(GeneratorControl.Event.Destroyed)) => o eq gen + case _ => false } ) assert( @@ -459,22 +447,25 @@ class GeneratorControlKillsTest extends ActorTest { assert( msg_avatar2(2) match { case AvatarServiceMessage( - "test", - AvatarAction.SendResponse(_, TriggerEffectMessage(PlanetSideGUID(2), "explosion_generator", None, None)) - ) => + "test", + AvatarAction.SendResponse(_, TriggerEffectMessage(PlanetSideGUID(2), "explosion_generator", None, None)) + ) => true case _ => false } ) + assert(gen.Health == 0) + assert(gen.Destroyed) + assert(gen.Condition == PlanetSideGeneratorState.Destroyed) + + val msg_player1 = player1Probe.receiveOne(100 milliseconds) + player2Probe.expectNoMessage(200 milliseconds) assert( msg_player1 match { case _ @Player.Die() => true case _ => false } ) - assert(gen.Health == 0) - assert(gen.Destroyed) - assert(gen.Condition == PlanetSideGeneratorState.Destroyed) } } } @@ -487,7 +478,7 @@ class GeneratorControlNotDestroyTwice extends ActorTest { GUID(guid) } val building = Building("test-building", 1, 1, zone, StructureType.Facility) //guid=1 - val gen = Generator(GlobalDefinitions.generator) //guid=2 + val gen = Generator(GeneratorTest.generator_definition) //guid=2 val player1 = Player(Avatar(0, "TestCharacter1", PlanetSideEmpire.TR, CharacterGender.Male, 0, CharacterVoice.Mute)) //guid=3 player1.Spawn() @@ -573,7 +564,7 @@ class GeneratorControlNotDamageIfExplodingTest extends ActorTest { val activityProbe = TestProbe() zone.Activity = activityProbe.ref - val gen = Generator(GlobalDefinitions.generator) //guid=2 + val gen = Generator(GeneratorTest.generator_definition) //guid=2 gen.Position = Vector3(1, 0, 0) gen.Actor = system.actorOf(Props(classOf[GeneratorControl], gen), "generator-control") @@ -625,19 +616,22 @@ class GeneratorControlNotDamageIfExplodingTest extends ActorTest { assert(gen.Condition == PlanetSideGeneratorState.Normal) //skipped critical state because didn't transition ~50% gen.Actor ! Vitality.Damage(applyDamageTo) - val msg_avatar = avatarProbe.receiveOne(500 milliseconds) - buildingProbe.expectNoMessage(200 milliseconds) - player1Probe.expectNoMessage(200 milliseconds) + val msg_building12 = buildingProbe.receiveN(2,500 milliseconds) assert( - msg_avatar match { - case AvatarServiceMessage("TestCharacter1", AvatarAction.GenericObjectAction(_, PlanetSideGUID(1), 16)) => - true + msg_building12.head match { + case BuildingActor.AmenityStateChange(o, Some(GeneratorControl.Event.Offline)) => o eq gen + case _ => false + } + ) + assert( + msg_building12(1) match { + case BuildingActor.AmenityStateChange(o, Some(GeneratorControl.Event.Destabilized)) => o eq gen case _ => false } ) assert(gen.Health == 1) assert(!gen.Destroyed) - assert(gen.Condition == PlanetSideGeneratorState.Normal) + assert(gen.Condition == PlanetSideGeneratorState.Destroyed) //going to explode state //once @@ -667,7 +661,7 @@ class GeneratorControlNotRepairIfExplodingTest extends ActorTest { val activityProbe = TestProbe() zone.Activity = activityProbe.ref - val gen = Generator(GlobalDefinitions.generator) //guid=2 + val gen = Generator(GeneratorTest.generator_definition) //guid=2 gen.Position = Vector3(1, 0, 0) gen.Actor = system.actorOf(Props(classOf[GeneratorControl], gen), "generator-control") @@ -723,19 +717,22 @@ class GeneratorControlNotRepairIfExplodingTest extends ActorTest { assert(gen.Condition == PlanetSideGeneratorState.Normal) //skipped critical state because didn't transition ~50% gen.Actor ! Vitality.Damage(applyDamageTo) - val msg_avatar1 = avatarProbe.receiveOne(500 milliseconds) - buildingProbe.expectNoMessage(200 milliseconds) - player1Probe.expectNoMessage(200 milliseconds) + val msg_building12 = buildingProbe.receiveN(2,500 milliseconds) assert( - msg_avatar1 match { - case AvatarServiceMessage("TestCharacter1", AvatarAction.GenericObjectAction(_, PlanetSideGUID(1), 16)) => - true + msg_building12.head match { + case BuildingActor.AmenityStateChange(o, Some(GeneratorControl.Event.Offline)) => o eq gen + case _ => false + } + ) + assert( + msg_building12(1) match { + case BuildingActor.AmenityStateChange(o, Some(GeneratorControl.Event.Destabilized)) => o eq gen case _ => false } ) assert(gen.Health == 1) assert(!gen.Destroyed) - assert(gen.Condition == PlanetSideGeneratorState.Normal) + assert(gen.Condition == PlanetSideGeneratorState.Destroyed) //going to explode state //once @@ -765,7 +762,7 @@ class GeneratorControlRepairPastRestorePoint extends ActorTest { val activityProbe = TestProbe() zone.Activity = activityProbe.ref - val gen = Generator(GlobalDefinitions.generator) //guid=2 + val gen = Generator(GeneratorTest.generator_definition) //guid=2 gen.Position = Vector3(1, 0, 0) gen.Actor = system.actorOf(Props(classOf[GeneratorControl], gen), "generator-control") @@ -804,7 +801,7 @@ class GeneratorControlRepairPastRestorePoint extends ActorTest { assert(gen.Destroyed) gen.Actor ! CommonMessages.Use(player1, Some(tool)) //repair - val msg_avatar = avatarProbe.receiveN(4, 500 milliseconds) //expected + val msg_avatar = avatarProbe.receiveN(3, 500 milliseconds) //expected val msg_building = buildingProbe.receiveOne(200 milliseconds) assert( msg_avatar.head match { @@ -825,13 +822,6 @@ class GeneratorControlRepairPastRestorePoint extends ActorTest { ) assert( msg_avatar(2) match { - case AvatarServiceMessage("TestCharacter1", AvatarAction.GenericObjectAction(_, PlanetSideGUID(1), 17)) => - true - case _ => false - } - ) - assert( - msg_avatar(3) match { case AvatarServiceMessage( "TestCharacter1", AvatarAction.SendResponse(_, RepairMessage(ValidPlanetSideGUID(2), _)) @@ -842,7 +832,7 @@ class GeneratorControlRepairPastRestorePoint extends ActorTest { ) assert( msg_building match { - case BuildingActor.AmenityStateChange(o) => o eq gen + case BuildingActor.AmenityStateChange(o, _) => o eq gen case _ => false } ) @@ -852,3 +842,15 @@ class GeneratorControlRepairPastRestorePoint extends ActorTest { } } } + +object GeneratorTest { + final val generator_definition = new GeneratorDefinition(352) { + MaxHealth = 4000 + Damageable = true + DamageableByFriendlyFire = false + Repairable = true + RepairDistance = 13.5f + RepairIfDestroyed = true + //note: no auto-repair + } +} diff --git a/src/test/scala/objects/ResourceSiloTest.scala b/src/test/scala/objects/ResourceSiloTest.scala index ba1fe643..36dab9b9 100644 --- a/src/test/scala/objects/ResourceSiloTest.scala +++ b/src/test/scala/objects/ResourceSiloTest.scala @@ -289,12 +289,6 @@ class ResourceSiloControlUpdate1Test extends ActorTest { case AvatarServiceMessage("nowhere", AvatarAction.PlanetsideAttribute(PlanetSideGUID(6), 47, 0)) => true case _ => false }) - - val reply4 = zoneEvents.receiveOne(500 milliseconds) - assert(reply4 match { - case AvatarServiceMessage("nowhere", AvatarAction.PlanetsideAttribute(PlanetSideGUID(6), 48, 0)) => true - case _ => false - }) } } }