Merge pull request #619 from Fate-JH/powered

Powered
This commit is contained in:
Fate-JH 2020-11-16 07:53:22 -05:00 committed by GitHub
commit d24fefaa91
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
26 changed files with 1178 additions and 530 deletions

View file

@ -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

View file

@ -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)

View file

@ -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 {

View file

@ -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)
}

View file

@ -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.

View file

@ -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
}

View file

@ -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 = { }
}

View file

@ -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
}

View file

@ -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()

View file

@ -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()
}
}

View file

@ -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 _ => ;
}
}
}

View file

@ -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)

View file

@ -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)
}

View file

@ -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)

View file

@ -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
}

View file

@ -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
}

View file

@ -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
}

View file

@ -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
}

View file

@ -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
}

View file

@ -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()
}
}

View file

@ -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())

View file

@ -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?)`<br>
* `46 - Sends "Generator damage is at a critical level!" message`
* `47 - Sets base NTU level to CRITICAL. MUST use base MapId not base GUID`<br>
* `48 - Set to 1 to send base power loss message & turns on red warning lights throughout base. MUST use base MapId not base GUID`<br>
* `48 - Set to 1 to send base power loss message & turns on red warning lights throughout base. MUST use base MapId not base GUID`?<br>
* `49 - Vehicle texture effects state? (>0 turns on ANT panel glow or ntu silo panel glow + orbs) (bit?)`<br>
* `52 - Vehicle particle effects? (>0 turns on orbs going towards ANT. Doesn't affect silo) (bit?)`<br>
* `53 - LFS. Value is 1 to flag LFS`<br>

View file

@ -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
}
}

View file

@ -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)
}
}

View file

@ -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
}
}

View file

@ -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
})
}
}
}