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.actors.zone.BuildingActor
import net.psforever.objects.avatar.{BattleRank, Certification, CommandRank, Cosmetic} import net.psforever.objects.avatar.{BattleRank, Certification, CommandRank, Cosmetic}
import net.psforever.objects.serverobject.pad.{VehicleSpawnControl, VehicleSpawnPad} 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.resourcesilo.ResourceSilo
import net.psforever.objects.serverobject.structures.Building import net.psforever.objects.serverobject.structures.Building
import net.psforever.objects.serverobject.turret.{FacilityTurret, TurretUpgrade, WeaponTurrets} import net.psforever.objects.serverobject.turret.{FacilityTurret, TurretUpgrade, WeaponTurrets}
@ -342,20 +342,55 @@ class ChatActor(
) )
} }
case (_, _, contents) if contents.startsWith("!ntu") && session.account.gm => case (_, _, contents) if contents.startsWith("!ntu") && gmCommandAllowed =>
session.zone.Buildings.values.foreach(building => val buffer = contents.toLowerCase.split("\\s+")
building.Amenities.foreach(amenity => val (facility, customNtuValue) = (buffer.lift(1), buffer.lift(2)) match {
amenity.Definition match { case (Some(x), Some(y)) if y.toIntOption.nonEmpty => (Some(x), Some(y.toInt))
case GlobalDefinitions.resource_silo => case (Some(x), None) if x.toIntOption.nonEmpty => (None, Some(x.toInt))
val r = new scala.util.Random case _ => (None, None)
val silo = amenity.asInstanceOf[ResourceSilo] }
val ntu = 900f + r.nextFloat() * 100f - silo.NtuCapacitor val silos = (facility match {
silo.Actor ! ResourceSilo.UpdateChargeLevel(ntu) case Some(cur) if cur.toLowerCase().startsWith("curr") =>
val position = session.player.Position
case _ => () 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 _ => case _ =>
// unknown ! commands are ignored // unknown ! commands are ignored

View file

@ -542,9 +542,6 @@ class SessionActor(middlewareActor: typed.ActorRef[MiddlewareActor.Command], con
case ProgressEvent(delta, finishedAction, stepAction, tick) => case ProgressEvent(delta, finishedAction, stepAction, tick) =>
HandleProgressChange(delta, finishedAction, stepAction, tick) HandleProgressChange(delta, finishedAction, stepAction, tick)
case Door.DoorMessage(tplayer, msg, order) =>
HandleDoorMessage(tplayer, msg, order)
case GalaxyServiceResponse(_, reply) => case GalaxyServiceResponse(_, reply) =>
reply match { reply match {
case GalaxyResponse.HotSpotUpdate(zone_index, priority, hot_spot_info) => 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 * na
* @param toChannel na * @param toChannel na
@ -4477,29 +4444,7 @@ class SessionActor(middlewareActor: typed.ActorRef[MiddlewareActor.Command], con
} }
ValidObject(object_guid) match { ValidObject(object_guid) match {
case Some(door: Door) => case Some(door: Door) =>
if ( door.Actor ! CommonMessages.Use(player)
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))
}
case Some(resourceSilo: ResourceSilo) => case Some(resourceSilo: ResourceSilo) =>
resourceSilo.Actor ! CommonMessages.Use(player) resourceSilo.Actor ! CommonMessages.Use(player)
@ -6609,13 +6554,20 @@ class SessionActor(middlewareActor: typed.ActorRef[MiddlewareActor.Command], con
*/ */
def configZone(zone: Zone): Unit = { def configZone(zone: Zone): Unit = {
zone.Buildings.values.foreach(building => { zone.Buildings.values.foreach(building => {
sendResponse(SetEmpireMessage(building.GUID, building.Faction)) val guid = building.GUID
sendResponse(SetEmpireMessage(guid, building.Faction))
// Synchronise capitol force dome state // power
if (building.IsCapitol && building.ForceDomeActive) { building.Generator match {
sendResponse(GenericObjectActionMessage(building.GUID, 13)) 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 { building.Amenities.collect {
case obj if obj.Destroyed => configAmenityAsDestroyed(obj) case obj if obj.Destroyed => configAmenityAsDestroyed(obj)
case obj => configAmenityAsWorking(obj) case obj => configAmenityAsWorking(obj)

View file

@ -7,14 +7,16 @@ import akka.{actor => classic}
import net.psforever.actors.commands.NtuCommand import net.psforever.actors.commands.NtuCommand
import net.psforever.objects.{CommonNtuContainer, NtuContainer} import net.psforever.objects.{CommonNtuContainer, NtuContainer}
import net.psforever.objects.serverobject.PlanetSideServerObject 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.serverobject.structures.{Amenity, Building, StructureType, WarpGate}
import net.psforever.objects.zones.Zone import net.psforever.objects.zones.Zone
import net.psforever.persistence 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.util.Database._
import net.psforever.services.galaxy.{GalaxyAction, GalaxyServiceMessage} import net.psforever.services.galaxy.{GalaxyAction, GalaxyServiceMessage}
import net.psforever.services.local.{LocalAction, LocalServiceMessage} 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.concurrent.ExecutionContext.Implicits.global
import scala.util.{Failure, Success} import scala.util.{Failure, Success}
@ -37,18 +39,86 @@ object BuildingActor {
final case class SetFaction(faction: PlanetSideEmpire.Value) extends Command 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 // TODO remove
// Changes to building objects should go through BuildingActor // Changes to building objects should go through BuildingActor
// Once they do, we won't need this anymore // Once they do, we won't need this anymore
final case class MapUpdate() extends Command 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 Ntu(command: NtuCommand.Command) extends Command
final case class SuppliedWithNtu() extends Command final case class SuppliedWithNtu() extends Command
final case class NtuDepleted() 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( class BuildingActor(
@ -63,6 +133,7 @@ class BuildingActor(
private[this] val log = org.log4s.getLogger private[this] val log = org.log4s.getLogger
var galaxyService: Option[classic.ActorRef] = None var galaxyService: Option[classic.ActorRef] = None
var interstellarCluster: Option[ActorRef[InterstellarClusterService.Command]] = None var interstellarCluster: Option[ActorRef[InterstellarClusterService.Command]] = None
var hasNtuSupply: Boolean = true
context.system.receptionist ! Receptionist.Find( context.system.receptionist ! Receptionist.Find(
InterstellarClusterService.InterstellarClusterServiceKey, InterstellarClusterService.InterstellarClusterServiceKey,
@ -107,69 +178,61 @@ class BuildingActor(
): Behavior[Command] = { ): Behavior[Command] = {
Behaviors.receiveMessagePartial { Behaviors.receiveMessagePartial {
case SetFaction(faction) => case SetFaction(faction) =>
import ctx._ setFactionTo(faction, galaxyService)
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))
Behaviors.same Behaviors.same
case MapUpdate() => case MapUpdate() =>
galaxyService ! GalaxyServiceMessage(GalaxyAction.MapUpdate(building.infoUpdateMessage())) galaxyService ! GalaxyServiceMessage(GalaxyAction.MapUpdate(building.infoUpdateMessage()))
Behaviors.same 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 //TODO when parameter object is finally immutable, perform analysis on it to determine specific actions
//for now, just update the map //for now, just update the map
galaxyService ! GalaxyServiceMessage(GalaxyAction.MapUpdate(building.infoUpdateMessage())) galaxyService ! GalaxyServiceMessage(GalaxyAction.MapUpdate(building.infoUpdateMessage()))
Behaviors.same Behaviors.same
case msg @ NtuDepleted() => case PowerOff() =>
log.trace(s"${building.Definition.Name} ${building.Name} ntu has been depleted") building.Generator match {
building.Amenities.foreach { amenity => case Some(gen) => gen.Actor ! BuildingActor.NtuDepleted()
amenity.Actor ! msg case _ => powerLost()
} }
Behaviors.same 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() => 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 => building.Amenities.foreach { amenity =>
amenity.Actor ! msg 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] = { def ntu(msg: NtuCommand.Command): Behavior[Command] = {
import NtuCommand._ import NtuCommand._
msg match { 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.ballistics.SourceEntry
import net.psforever.objects.ce.Deployable import net.psforever.objects.ce.Deployable
import net.psforever.objects.equipment.Equipment 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.zones.Zone
import net.psforever.objects.{ConstructionItem, PlanetSideGameObject, Player, Vehicle} import net.psforever.objects.{ConstructionItem, PlanetSideGameObject, Player, Vehicle}
import net.psforever.types.{PlanetSideEmpire, PlanetSideGUID, Vector3} 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 { ctx.run(query[persistence.Building].filter(_.zoneId == lift(zone.Number))).onComplete {
case Success(buildings) => case Success(buildings) =>
var capitol: Option[Building] = None
buildings.foreach { building => buildings.foreach { building =>
zone.BuildingByMapId(building.localId) match { zone.BuildingByMapId(building.localId) match {
case Some(b) => b.Faction = PlanetSideEmpire(building.factionId) case Some(b) =>
case None => // TODO this happens during testing, need a way to not always persist during tests 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) case Failure(e) => log.error(e.getMessage)
} }

View file

@ -50,7 +50,7 @@ trait SpawnPoint {
*/ */
def Definition: ObjectDefinition with SpawnPointDefinition 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. * 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 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 def Definition: DoorDefinition = ddef
} }

View file

@ -1,21 +1,90 @@
// Copyright (c) 2017 PSForever // Copyright (c) 2017 PSForever
package net.psforever.objects.serverobject.doors 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.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`. * An `Actor` that handles messages being dispatched to a specific `Door`.
* @param door the `Door` object being governed * @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 FactionObject: FactionAffinity = door
def receive: Receive = val commonBehavior: Receive = checkBehavior
checkBehavior.orElse {
case Door.Use(player, msg) =>
sender() ! Door.DoorMessage(player, msg, door.Use(player, msg))
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 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 def Definition: GeneratorDefinition = gdef
} }

View file

@ -1,9 +1,9 @@
// Copyright (c) 2020 PSForever // Copyright (c) 2020 PSForever
package net.psforever.objects.serverobject.generator package net.psforever.objects.serverobject.generator
import akka.actor.Actor import akka.actor.{Actor, Cancellable}
import net.psforever.actors.zone.BuildingActor 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.ballistics._
import net.psforever.objects.serverobject.affinity.FactionAffinityBehavior import net.psforever.objects.serverobject.affinity.FactionAffinityBehavior
import net.psforever.objects.serverobject.damage.Damageable.Target import net.psforever.objects.serverobject.damage.Damageable.Target
@ -34,21 +34,64 @@ class GeneratorControl(gen: Generator)
def DamageableObject = gen def DamageableObject = gen
def RepairableObject = gen def RepairableObject = gen
def AutoRepairObject = gen def AutoRepairObject = gen
/** flagged to explode after some time */
var imminentExplosion: Boolean = false 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 checkBehavior
.orElse(takesDamage) .orElse(takesDamage)
.orElse(canBeRepairedByNanoDispenser) .orElse(canBeRepairedByNanoDispenser)
.orElse(autoRepairBehavior) .orElse(autoRepairBehavior)
.orElse { .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 val zone = gen.Zone
gen.Health = 0 gen.Health = 0
super.DestructionAwareness(gen, gen.LastShot.get) super.DestructionAwareness(gen, gen.LastShot.get)
gen.Condition = PlanetSideGeneratorState.Destroyed GeneratorControl.UpdateOwner(gen, Some(GeneratorControl.Event.Destroyed))
GeneratorControl.UpdateOwner(gen)
//kaboom //kaboom
zone.AvatarEvents ! AvatarServiceMessage( zone.AvatarEvents ! AvatarServiceMessage(
zone.id, zone.id,
@ -57,6 +100,8 @@ class GeneratorControl(gen: Generator)
TriggerEffectMessage(gen.GUID, "explosion_generator", None, None) TriggerEffectMessage(gen.GUID, "explosion_generator", None, None)
) )
) )
queuedExplosion.cancel()
queuedExplosion = Default.Cancellable
imminentExplosion = false imminentExplosion = false
//kill everyone within 14m //kill everyone within 14m
gen.Owner match { gen.Owner match {
@ -71,24 +116,46 @@ class GeneratorControl(gen: Generator)
} }
gen.ClearHistory() gen.ClearHistory()
case GeneratorControl.UnderThreatAlarm() => case GeneratorControl.Restored() =>
if (!alarmCooldownPeriod) { GeneratorControl.UpdateOwner(gen, Some(GeneratorControl.Event.Online))
alarmCooldownPeriod = true
GeneratorControl.BroadcastGeneratorEvent(gen, event = 15)
context.system.scheduler.scheduleOnce(delay = 5 seconds, self, GeneratorControl.AlarmReset())
}
case GeneratorControl.AlarmReset() =>
alarmCooldownPeriod = false
case _ => ; 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 = { override protected def CanPerformRepairs(obj: Target, player: Player, item: Tool): Boolean = {
//if an explosion is queued, disallow repairs
!imminentExplosion && super.CanPerformRepairs(obj, player, item) !imminentExplosion && super.CanPerformRepairs(obj, player, item)
} }
override protected def WillAffectTarget(target: Target, damage: Int, cause: ResolvedProjectile): Boolean = { 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) !imminentExplosion && super.WillAffectTarget(target, damage, cause)
} }
@ -104,11 +171,11 @@ class GeneratorControl(gen: Generator)
override protected def DestructionAwareness(target: Target, cause: ResolvedProjectile): Unit = { override protected def DestructionAwareness(target: Target, cause: ResolvedProjectile): Unit = {
tryAutoRepair() tryAutoRepair()
//if the target is already destroyed, do not let it be destroyed again
if (!target.Destroyed) { if (!target.Destroyed) {
target.Health = 1 //temporary target.Health = 1 //temporary
imminentExplosion = true GeneratorControl.UpdateOwner(gen, Some(GeneratorControl.Event.Offline))
context.system.scheduler.scheduleOnce(10 seconds, self, GeneratorControl.GeneratorExplodes()) self ! GeneratorControl.Destabilized()
GeneratorControl.BroadcastGeneratorEvent(gen, 16)
} }
} }
@ -123,12 +190,40 @@ class GeneratorControl(gen: Generator)
override def Restoration(obj: Repairable.Target): Unit = { override def Restoration(obj: Repairable.Target): Unit = {
super.Restoration(obj) super.Restoration(obj)
gen.Condition = PlanetSideGeneratorState.Normal gen.Condition = PlanetSideGeneratorState.Normal
GeneratorControl.UpdateOwner(gen) GeneratorControl.UpdateOwner(gen, Some(GeneratorControl.Event.Normal))
GeneratorControl.BroadcastGeneratorEvent(gen, 17) 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 { object GeneratorControl {
/**
* na
*/
private case class Destabilized()
/** /**
* na * na
@ -147,30 +242,31 @@ object GeneratorControl {
/** /**
* na * na
* @param obj na
*/ */
private def UpdateOwner(obj: Generator): Unit = { private case class Restored()
obj.Owner match {
case b: Building => b.Actor ! BuildingActor.AmenityStateChange(obj)
case _ => ;
}
}
/** /**
* na * na
* @param target the generator
* @param event the action code for the event
*/ */
private def BroadcastGeneratorEvent(target: Generator, event: Int): Unit = { object Event extends Enumeration {
target.Owner match { val
case b: Building => Critical, //PlanetSideGeneratorState.Critical
val events = target.Zone.AvatarEvents UnderAttack,
val msg = AvatarAction.GenericObjectAction(Service.defaultPlayerGUID, target.Owner.GUID, event) Destabilized,
b.PlayersInSOI.foreach { player => Destroyed, //PlanetSideGeneratorState.Destroyed
events ! AvatarServiceMessage(player.Name, msg) Offline,
} Normal, //PlanetSideGeneratorState.Normal
case _ => ; 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 val max: Float = target.MaxHealth.toFloat
if (target.Condition != PlanetSideGeneratorState.Critical && health / max < 0.51f) { //becoming critical if (target.Condition != PlanetSideGeneratorState.Critical && health / max < 0.51f) { //becoming critical
target.Condition = PlanetSideGeneratorState.Critical target.Condition = PlanetSideGeneratorState.Critical
GeneratorControl.UpdateOwner(target) GeneratorControl.UpdateOwner(target, Some(GeneratorControl.Event.Critical))
} }
//the generator is under attack //the generator is under attack
target.Actor ! UnderThreatAlarm() target.Actor ! UnderThreatAlarm()

View file

@ -1,7 +1,6 @@
// Copyright (c) 2017 PSForever // Copyright (c) 2017 PSForever
package net.psforever.objects.serverobject.implantmech package net.psforever.objects.serverobject.implantmech
import akka.actor.Actor
import net.psforever.objects.ballistics.ResolvedProjectile import net.psforever.objects.ballistics.ResolvedProjectile
import net.psforever.objects.{GlobalDefinitions, Player, SimpleItem} import net.psforever.objects.{GlobalDefinitions, Player, SimpleItem}
import net.psforever.objects.serverobject.{CommonMessages, PlanetSideServerObject} 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.damage.{Damageable, DamageableEntity, DamageableMountable}
import net.psforever.objects.serverobject.hackable.{GenericHackables, HackableBehavior} import net.psforever.objects.serverobject.hackable.{GenericHackables, HackableBehavior}
import net.psforever.objects.serverobject.repair.{AmenityAutoRepair, RepairableEntity} 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`. * An `Actor` that handles messages being dispatched to a specific `ImplantTerminalMech`.
* @param mech the "mech" object being governed * @param mech the "mech" object being governed
*/ */
class ImplantTerminalMechControl(mech: ImplantTerminalMech) class ImplantTerminalMechControl(mech: ImplantTerminalMech)
extends Actor extends PoweredAmenityControl
with FactionAffinityBehavior.Check with FactionAffinityBehavior.Check
with MountableBehavior.Mount with MountableBehavior.Mount
with MountableBehavior.Dismount with MountableBehavior.Dismount
@ -33,17 +33,19 @@ class ImplantTerminalMechControl(mech: ImplantTerminalMech)
def RepairableObject = mech def RepairableObject = mech
def AutoRepairObject = mech def AutoRepairObject = mech
def receive: Receive = def commonBehavior: Receive =
checkBehavior checkBehavior
.orElse(mountBehavior)
.orElse(dismountBehavior) .orElse(dismountBehavior)
.orElse(hackableBehavior)
.orElse(takesDamage) .orElse(takesDamage)
.orElse(canBeRepairedByNanoDispenser) .orElse(canBeRepairedByNanoDispenser)
.orElse(autoRepairBehavior) .orElse(autoRepairBehavior)
def poweredStateLogic : Receive =
commonBehavior
.orElse(mountBehavior)
.orElse { .orElse {
case CommonMessages.Use(player, Some(item: SimpleItem)) 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 //TODO setup certifications check
mech.Owner match { mech.Owner match {
case b: Building if (b.Faction != player.Faction || b.CaptureTerminalIsHacked) && mech.HackedBy.isEmpty => case b: Building if (b.Faction != player.Faction || b.CaptureTerminalIsHacked) && mech.HackedBy.isEmpty =>
@ -57,6 +59,12 @@ class ImplantTerminalMechControl(mech: ImplantTerminalMech)
case _ => ; case _ => ;
} }
def unpoweredStateLogic: Receive =
commonBehavior
.orElse {
case _ => ;
}
override protected def MountTest( override protected def MountTest(
obj: PlanetSideServerObject with Mountable, obj: PlanetSideServerObject with Mountable,
seatNumber: Int, seatNumber: Int,
@ -98,4 +106,32 @@ class ImplantTerminalMechControl(mech: ImplantTerminalMech)
} }
newHealth 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 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.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.objects.{Default, GlobalDefinitions}
import net.psforever.types.{PlanetSideEmpire, Vector3} import net.psforever.types.{PlanetSideEmpire, Vector3}
import net.psforever.services.avatar.{AvatarAction, AvatarServiceMessage} 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.ExecutionContext.Implicits.global
import scala.concurrent.duration._ 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") private[this] val log = org.log4s.getLogger(s"Painbox")
var painboxTick: Cancellable = Default.Cancellable var painboxTick: Cancellable = Default.Cancellable
var nearestDoor: Option[Door] = None 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 var disabled = false // Temporary to disable cavern non-radius fields
def receive: Receive = { def initialStartup(): Unit = {
case "startup" => if (painbox.Definition.HasNearestDoorDependency) {
if (painbox.Definition.HasNearestDoorDependency) { (painbox.Owner match {
(painbox.Owner match { case obj : Building =>
case obj: Building => obj.Amenities
obj.Amenities .collect { case door : Door => door }
.collect { case door: Door => door } .sortBy(door => Vector3.DistanceSquared(painbox.Position, door.Position))
.sortBy(door => Vector3.DistanceSquared(painbox.Position, door.Position)) .headOption
.headOption case _ =>
case _ => None
None }) match {
}) match { case door@Some(_) =>
case door @ Some(_) => nearestDoor = door
nearestDoor = door case _ =>
case _ => log.error(
log.error( s"Painbox ${painbox.GUID} on ${painbox.Owner.Continent} - ${painbox.Position} can not find a door that it is dependent on"
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 { else {
if (painbox.Definition.Radius == 0f) { painbox.Owner match {
if (painbox.Owner.Continent.matches("c[0-9]")) { case obj : Building =>
// todo: handle non-radius painboxes in caverns properly val planarRange = 16.5f
log.warn(s"Skipping initialization of ${painbox.GUID} on ${painbox.Owner.Continent} - ${painbox.Position}") val aboveRange = 5
disabled = true val belowRange = 5
} else { // Find amenities within the specified range
painbox.Owner match { val nearbyAmenities = obj.Amenities
case obj: Building => .filter(amenity =>
val planarRange = 16.5f amenity.Position != Vector3.Zero
val aboveRange = 5 && (amenity.Definition == GlobalDefinitions.mb_locker
val belowRange = 5 || 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 // Calculate bounding box of amenities
val nearbyAmenities = obj.Amenities bBoxMinCorner = Vector3(
.filter(amenity => nearbyAmenities.minBy(amenity => amenity.Position.x).Position.x,
amenity.Position != Vector3.Zero nearbyAmenities.minBy(amenity => amenity.Position.y).Position.y,
&& (amenity.Definition == GlobalDefinitions.mb_locker nearbyAmenities.minBy(x => x.Position.z).Position.z
|| amenity.Definition == GlobalDefinitions.respawn_tube )
|| amenity.Definition == GlobalDefinitions.spawn_terminal bBoxMaxCorner = Vector3(
|| amenity.Definition == GlobalDefinitions.order_terminal nearbyAmenities.maxBy(amenity => amenity.Position.x).Position.x,
|| amenity.Definition == GlobalDefinitions.door) nearbyAmenities.maxBy(amenity => amenity.Position.y).Position.y,
&& amenity.Position.x > painbox.Position.x - planarRange && amenity.Position.x < painbox.Position.x + planarRange painbox.Position.z
&& 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 bBoxMidPoint = Vector3(
) (bBoxMinCorner.x + bBoxMaxCorner.x) / 2,
(bBoxMinCorner.y + bBoxMaxCorner.y) / 2,
// Calculate bounding box of amenities (bBoxMinCorner.z + bBoxMaxCorner.z) / 2
bBoxMinCorner = Vector3( )
nearbyAmenities.minBy(amenity => amenity.Position.x).Position.x, case _ => None
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) { if (!disabled) {
context.become(Stopped) self ! BuildingActor.PowerOff()
} }
case _ => ;
} }
def Running: Receive = { var commonBehavior: Receive = {
case "startup" =>
if (bBoxMidPoint == Vector3.Zero) {
initialStartup()
}
case Painbox.Stop() => case Painbox.Stop() =>
context.become(Stopped)
painboxTick.cancel() painboxTick.cancel()
painboxTick = Default.Cancellable painboxTick = Default.Cancellable
}
case Painbox.Tick() => def poweredStateLogic: Receive =
//todo: Account for overlapping pain fields commonBehavior
//todo: Pain module .orElse {
//todo: REK boosting case Painbox.Start() if isPowered =>
val guid = painbox.GUID painboxTick.cancel()
val owner = painbox.Owner.asInstanceOf[Building] painboxTick = context.system.scheduler.scheduleWithFixedDelay(0 seconds, 1 second, self, Painbox.Tick())
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
if (painbox.Definition.Radius != 0f) { case Painbox.Tick() =>
// Spherical pain field //todo: Account for overlapping pain fields
owner.PlayersInSOI //todo: Pain module
.collect { //todo: REK boosting
case p val guid = painbox.GUID
if p.Faction != faction val owner = painbox.Owner.asInstanceOf[Building]
&& p.Health > 0 val faction = owner.Faction
&& Vector3.DistanceSquared(p.Position, position) < radius => if (
events ! AvatarServiceMessage(p.Name, AvatarAction.EnvironmentalDamage(p.GUID, guid, damage)) isPowered && faction != PlanetSideEmpire.NEUTRAL && (nearestDoor match {
} case Some(door) => door.Open.nonEmpty;
} else { case _ => true
// Bounding box pain field })
owner.PlayersInSOI ) {
.collect { val events = painbox.Zone.AvatarEvents
case p val damage = painbox.Definition.Damage
if p.Faction != faction val radius = painbox.Definition.Radius * painbox.Definition.Radius
&& p.Health > 0 => val position = painbox.Position
/*
This may be cpu intensive with a large number of players in SOI. Further performance tweaking may be required if (painbox.Definition.Radius != 0f) {
The bounding box is calculated aligned to the world XY axis, instead of rotating the painbox corners to match the base rotation // Spherical pain field
we instead rotate the player's current coordinates to match the base rotation, which allows for much simplified checking of if the player is owner.PlayersInSOI
within the bounding box .collect {
*/ case p
val playerRot = if p.Faction != faction
Vector3.PlanarRotateAroundPoint(p.Position, bBoxMidPoint, painbox.Owner.Orientation.z.toRadians) && p.Health > 0
if ( && Vector3.DistanceSquared(p.Position, position) < radius =>
bBoxMinCorner.x <= playerRot.x && playerRot.x <= bBoxMaxCorner.x && bBoxMinCorner.y <= playerRot.y && playerRot.y <= bBoxMaxCorner.y events ! AvatarServiceMessage(p.Name, AvatarAction.EnvironmentalDamage(p.GUID, guid, damage))
&& playerRot.z >= bBoxMinCorner.z && playerRot.z <= bBoxMaxCorner.z }
) { } else {
events ! AvatarServiceMessage(p.Name, AvatarAction.EnvironmentalDamage(p.GUID, guid, damage)) // 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 = { def powerTurnOnCallback(): Unit = {
case Painbox.Start() => painbox.Owner match {
context.become(Running) case b: Building if b.PlayersInSOI.nonEmpty =>
painboxTick.cancel() self ! Painbox.Start()
painboxTick = context.system.scheduler.scheduleWithFixedDelay(0 seconds, 1 second, self, Painbox.Tick()) case _ => ;
}
case _ => ;
} }
} }

View file

@ -129,10 +129,25 @@ trait AmenityAutoRepair
/** /**
* Attempt to start auto-repair operation * Attempt to start auto-repair operation
* only if no operation is currently being processed. * only if no operation is currently being processed.
* @see `actuallyTryAutoRepair`
* @return `true`, if the auto-repair process started specifically due to this call; * @return `true`, if the auto-repair process started specifically due to this call;
* `false`, if it was already started, or did not start * `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 val before = autoRepairTimer.isCancelled
autoRepairStartFunc() autoRepairStartFunc()
!(before || autoRepairTimer.isCancelled) !(before || autoRepairTimer.isCancelled)

View file

@ -113,18 +113,9 @@ class ResourceSiloControl(resourceSilo: ResourceSilo)
LowNtuWarning(enabled = true) LowNtuWarning(enabled = true)
} }
if (resourceSilo.NtuCapacitor == 0 && siloChargeBeforeChange > 0) { 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.NtuDepleted()
building.Actor ! BuildingActor.AmenityStateChange(resourceSilo) building.Actor ! BuildingActor.AmenityStateChange(resourceSilo)
building.Actor ! BuildingActor.SetFaction(PlanetSideEmpire.NEUTRAL)
} else if (siloChargeBeforeChange == 0 && resourceSilo.NtuCapacitor > 0) { } 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.SuppliedWithNtu()
building.Actor ! BuildingActor.AmenityStateChange(resourceSilo) building.Actor ! BuildingActor.AmenityStateChange(resourceSilo)
} }

View file

@ -4,8 +4,8 @@ package net.psforever.objects.serverobject.structures
import java.util.concurrent.TimeUnit import java.util.concurrent.TimeUnit
import akka.actor.ActorContext import akka.actor.ActorContext
import net.psforever.actors.zone.{BuildingActor, ZoneActor} import net.psforever.actors.zone.BuildingActor
import net.psforever.objects.{Default, GlobalDefinitions, Player} import net.psforever.objects.{GlobalDefinitions, Player}
import net.psforever.objects.definition.ObjectDefinition import net.psforever.objects.definition.ObjectDefinition
import net.psforever.objects.serverobject.generator.Generator import net.psforever.objects.serverobject.generator.Generator
import net.psforever.objects.serverobject.hackable.Hackable 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.packet.game.BuildingInfoUpdateMessage
import net.psforever.types.{PlanetSideEmpire, PlanetSideGUID, PlanetSideGeneratorState, Vector3} import net.psforever.types.{PlanetSideEmpire, PlanetSideGUID, PlanetSideGeneratorState, Vector3}
import scalax.collection.{Graph, GraphEdge} import scalax.collection.{Graph, GraphEdge}
import net.psforever.services.Service
import net.psforever.services.local.{LocalAction, LocalServiceMessage}
import akka.actor.typed.scaladsl.adapter._ import akka.actor.typed.scaladsl.adapter._
class Building( class Building(
@ -65,16 +63,6 @@ class Building(
override def Faction_=(fac: PlanetSideEmpire.Value): PlanetSideEmpire.Value = { override def Faction_=(fac: PlanetSideEmpire.Value): PlanetSideEmpire.Value = {
faction = fac 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 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] = { def CaptureTerminal: Option[CaptureTerminal] = {
Amenities.find(_.isInstanceOf[CaptureTerminal]) match { Amenities.find(_.isInstanceOf[CaptureTerminal]) match {
case Some(term) => Some(term.asInstanceOf[CaptureTerminal]) 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 // Get all lattice neighbours matching the specified faction
def Neighbours(faction: PlanetSideEmpire.Value): Option[Set[Building]] = { def Neighbours(faction: PlanetSideEmpire.Value): Option[Set[Building]] = {
this.Neighbours match { this.Neighbours match {
@ -187,8 +151,8 @@ class Building(
(false, PlanetSideEmpire.NEUTRAL, 0L) (false, PlanetSideEmpire.NEUTRAL, 0L)
} }
//if we have no generator, assume the state is "Normal" //if we have no generator, assume the state is "Normal"
val (generatorState, boostGeneratorPain) = Amenities.find(x => x.isInstanceOf[Generator]) match { val (generatorState, boostGeneratorPain) = Generator match {
case Some(obj: Generator) => case Some(obj) =>
(obj.Condition, false) // todo: poll pain field strength (obj.Condition, false) // todo: poll pain field strength
case _ => case _ =>
(PlanetSideGeneratorState.Normal, false) (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 // Copyright (c) 2017 PSForever
package net.psforever.objects.serverobject.terminals 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._
import net.psforever.objects.serverobject.CommonMessages import net.psforever.objects.serverobject.CommonMessages
import net.psforever.objects.serverobject.affinity.FactionAffinityBehavior 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.damage.DamageableAmenity
import net.psforever.objects.serverobject.hackable.{GenericHackables, HackableBehavior} import net.psforever.objects.serverobject.hackable.{GenericHackables, HackableBehavior}
import net.psforever.objects.serverobject.repair.RepairableAmenity 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}
import scala.collection.mutable import scala.collection.mutable
import scala.concurrent.duration._ import scala.concurrent.duration._
@ -20,26 +23,38 @@ import scala.concurrent.duration._
* @param term the proximity unit (terminal) * @param term the proximity unit (terminal)
*/ */
class ProximityTerminalControl(term: Terminal with ProximityUnit) class ProximityTerminalControl(term: Terminal with ProximityUnit)
extends Actor extends PoweredAmenityControl
with FactionAffinityBehavior.Check with FactionAffinityBehavior.Check
with HackableBehavior.GenericHackable with HackableBehavior.GenericHackable
with DamageableAmenity with DamageableAmenity
with RepairableAmenity { with RepairableAmenity
with AmenityAutoRepair {
def FactionObject = term def FactionObject = term
def HackableObject = term def HackableObject = term
def TerminalObject = term def TerminalObject = term
def DamageableObject = term def DamageableObject = term
def RepairableObject = term def RepairableObject = term
def AutoRepairObject = term
var terminalAction: Cancellable = Default.Cancellable var terminalAction: Cancellable = Default.Cancellable
val callbacks: mutable.ListBuffer[ActorRef] = new mutable.ListBuffer[ActorRef]() val callbacks: mutable.ListBuffer[ActorRef] = new mutable.ListBuffer[ActorRef]()
val log = org.log4s.getLogger val log = org.log4s.getLogger
def receive: Receive = val commonBehavior: Receive = checkBehavior
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(hackableBehavior)
.orElse(takesDamage)
.orElse(canBeRepairedByNanoDispenser)
.orElse { .orElse {
case CommonMessages.Use(player, Some(item: SimpleItem)) case CommonMessages.Use(player, Some(item: SimpleItem))
if item.Definition == GlobalDefinitions.remote_electronics_kit => if item.Definition == GlobalDefinitions.remote_electronics_kit =>
@ -66,12 +81,6 @@ class ProximityTerminalControl(term: Terminal with ProximityUnit)
case CommonMessages.Use(_, _) => case CommonMessages.Use(_, _) =>
log.warn(s"unexpected format for CommonMessages.Use in this context") 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() => case ProximityTerminalControl.TerminalAction() =>
val proxDef = term.Definition.asInstanceOf[ProximityDefinition] val proxDef = term.Definition.asInstanceOf[ProximityDefinition]
val validateFunc: PlanetSideGameObject => Boolean = val validateFunc: PlanetSideGameObject => Boolean =
@ -99,6 +108,31 @@ class ProximityTerminalControl(term: Terminal with ProximityUnit)
case _ => 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 = { def Use(target: PlanetSideGameObject, zone: String, callback: ActorRef): Unit = {
val hadNoUsers = term.NumberUsers == 0 val hadNoUsers = term.NumberUsers == 0
if (term.AddUser(target)) { 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 override def toString: String = term.Definition.Name
} }

View file

@ -1,7 +1,7 @@
// Copyright (c) 2017 PSForever // Copyright (c) 2017 PSForever
package net.psforever.objects.serverobject.terminals 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.ballistics.ResolvedProjectile
import net.psforever.objects.{GlobalDefinitions, SimpleItem} import net.psforever.objects.{GlobalDefinitions, SimpleItem}
import net.psforever.objects.serverobject.CommonMessages 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.damage.{Damageable, DamageableAmenity}
import net.psforever.objects.serverobject.hackable.{GenericHackables, HackableBehavior} import net.psforever.objects.serverobject.hackable.{GenericHackables, HackableBehavior}
import net.psforever.objects.serverobject.repair.{AmenityAutoRepair, RepairableAmenity} 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`. * An `Actor` that handles messages being dispatched to a specific `Terminal`.
* @param term the `Terminal` object being governed * @param term the `Terminal` object being governed
*/ */
class TerminalControl(term: Terminal) class TerminalControl(term: Terminal)
extends Actor extends PoweredAmenityControl
with FactionAffinityBehavior.Check with FactionAffinityBehavior.Check
with HackableBehavior.GenericHackable with HackableBehavior.GenericHackable
with DamageableAmenity with DamageableAmenity
@ -29,18 +31,20 @@ class TerminalControl(term: Terminal)
def RepairableObject = term def RepairableObject = term
def AutoRepairObject = term def AutoRepairObject = term
def receive: Receive = val commonBehavior: Receive = checkBehavior
checkBehavior .orElse(takesDamage)
.orElse(canBeRepairedByNanoDispenser)
.orElse(autoRepairBehavior)
def poweredStateLogic : Receive =
commonBehavior
.orElse(hackableBehavior) .orElse(hackableBehavior)
.orElse(takesDamage)
.orElse(canBeRepairedByNanoDispenser)
.orElse(autoRepairBehavior)
.orElse { .orElse {
case Terminal.Request(player, msg) => case Terminal.Request(player, msg) =>
TerminalControl.Dispatch(sender(), term, Terminal.TerminalMessage(player, msg, term.Request(player, msg))) TerminalControl.Dispatch(sender(), term, Terminal.TerminalMessage(player, msg, term.Request(player, msg)))
case CommonMessages.Use(player, Some(item: SimpleItem)) 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 //TODO setup certifications check
term.Owner match { term.Owner match {
case b: Building if (b.Faction != player.Faction || b.CaptureTerminalIsHacked) && term.HackedBy.isEmpty => case b: Building if (b.Faction != player.Faction || b.CaptureTerminalIsHacked) && term.HackedBy.isEmpty =>
@ -55,6 +59,14 @@ class TerminalControl(term: Terminal)
case _ => ; 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 = { override protected def DamageAwareness(target : Target, cause : ResolvedProjectile, amount : Any) : Unit = {
tryAutoRepair() tryAutoRepair()
super.DamageAwareness(target, cause, amount) super.DamageAwareness(target, cause, amount)
@ -62,6 +74,10 @@ class TerminalControl(term: Terminal)
override protected def DestructionAwareness(target: Damageable.Target, cause: ResolvedProjectile) : Unit = { override protected def DestructionAwareness(target: Damageable.Target, cause: ResolvedProjectile) : Unit = {
tryAutoRepair() tryAutoRepair()
if (term.HackedBy.nonEmpty) {
val zone = term.Zone
zone.LocalEvents ! LocalServiceMessage(zone.id, LocalAction.ClearTemporaryHack(Service.defaultPlayerGUID, term))
}
super.DestructionAwareness(target, cause) super.DestructionAwareness(target, cause)
} }
@ -73,6 +89,23 @@ class TerminalControl(term: Terminal)
newHealth 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 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 * @param tDef the `ObjectDefinition` that constructs this object and maintains some of its immutable fields
*/ */
class SpawnTube(tDef: SpawnTubeDefinition) extends Amenity with SpawnPoint { class SpawnTube(tDef: SpawnTubeDefinition) extends Amenity with SpawnPoint {
var offline: Boolean = false
override def isOffline: Boolean = offline || super.isOffline
def Definition: SpawnTubeDefinition = tDef def Definition: SpawnTubeDefinition = tDef
} }

View file

@ -1,21 +1,20 @@
// Copyright (c) 2017 PSForever // Copyright (c) 2017 PSForever
package net.psforever.objects.serverobject.tube package net.psforever.objects.serverobject.tube
import akka.actor.Actor
import net.psforever.actors.zone.BuildingActor import net.psforever.actors.zone.BuildingActor
import net.psforever.objects.ballistics.ResolvedProjectile import net.psforever.objects.ballistics.ResolvedProjectile
import net.psforever.objects.serverobject.affinity.FactionAffinityBehavior import net.psforever.objects.serverobject.affinity.FactionAffinityBehavior
import net.psforever.objects.serverobject.damage.Damageable.Target import net.psforever.objects.serverobject.damage.Damageable.Target
import net.psforever.objects.serverobject.damage.DamageableAmenity import net.psforever.objects.serverobject.damage.DamageableAmenity
import net.psforever.objects.serverobject.repair.{AmenityAutoRepair, Repairable, RepairableAmenity} 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`. * An `Actor` that handles messages being dispatched to a specific `SpawnTube`.
* @param tube the `SpawnTube` object being governed * @param tube the `SpawnTube` object being governed
*/ */
class SpawnTubeControl(tube: SpawnTube) class SpawnTubeControl(tube: SpawnTube)
extends Actor extends PoweredAmenityControl
with FactionAffinityBehavior.Check with FactionAffinityBehavior.Check
with DamageableAmenity with DamageableAmenity
with RepairableAmenity with RepairableAmenity
@ -25,11 +24,19 @@ class SpawnTubeControl(tube: SpawnTube)
def RepairableObject = tube def RepairableObject = tube
def AutoRepairObject = tube def AutoRepairObject = tube
def receive: Receive = val commonBehavior: Receive = checkBehavior
checkBehavior .orElse(takesDamage)
.orElse(takesDamage) .orElse(canBeRepairedByNanoDispenser)
.orElse(canBeRepairedByNanoDispenser) .orElse(autoRepairBehavior)
.orElse(autoRepairBehavior)
def poweredStateLogic: Receive =
commonBehavior
.orElse {
case _ => ;
}
def unpoweredStateLogic: Receive =
commonBehavior
.orElse { .orElse {
case _ => ; 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 override def toString: String = tube.Definition.Name
} }

View file

@ -1,7 +1,6 @@
// Copyright (c) 2017 PSForever // Copyright (c) 2017 PSForever
package net.psforever.objects.serverobject.turret package net.psforever.objects.serverobject.turret
import akka.actor.Actor
import net.psforever.objects.ballistics.ResolvedProjectile import net.psforever.objects.ballistics.ResolvedProjectile
import net.psforever.objects.{Default, GlobalDefinitions, Player, Tool} import net.psforever.objects.{Default, GlobalDefinitions, Player, Tool}
import net.psforever.objects.equipment.{Ammo, JammableMountedWeapons} 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.damage.{Damageable, DamageableWeaponTurret}
import net.psforever.objects.serverobject.hackable.GenericHackables import net.psforever.objects.serverobject.hackable.GenericHackables
import net.psforever.objects.serverobject.repair.{AmenityAutoRepair, RepairableWeaponTurret} 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.avatar.{AvatarAction, AvatarServiceMessage}
import net.psforever.services.local.{LocalAction, LocalServiceMessage} import net.psforever.services.local.{LocalAction, LocalServiceMessage}
import net.psforever.services.vehicle.{VehicleAction, VehicleServiceMessage}
import scala.concurrent.ExecutionContext.Implicits.global import scala.concurrent.ExecutionContext.Implicits.global
import scala.concurrent.duration._ import scala.concurrent.duration._
@ -27,7 +28,7 @@ import scala.concurrent.duration._
* @param turret the `MannedTurret` object being governed * @param turret the `MannedTurret` object being governed
*/ */
class FacilityTurretControl(turret: FacilityTurret) class FacilityTurretControl(turret: FacilityTurret)
extends Actor extends PoweredAmenityControl
with FactionAffinityBehavior.Check with FactionAffinityBehavior.Check
with MountableBehavior.TurretMount with MountableBehavior.TurretMount
with MountableBehavior.Dismount with MountableBehavior.Dismount
@ -51,14 +52,17 @@ class FacilityTurretControl(turret: FacilityTurret)
stopAutoRepair() stopAutoRepair()
} }
def receive: Receive = def commonBehavior: Receive =
checkBehavior checkBehavior
.orElse(jammableBehavior) .orElse(jammableBehavior)
.orElse(mountBehavior)
.orElse(dismountBehavior) .orElse(dismountBehavior)
.orElse(takesDamage) .orElse(takesDamage)
.orElse(canBeRepairedByNanoDispenser) .orElse(canBeRepairedByNanoDispenser)
.orElse(autoRepairBehavior) .orElse(autoRepairBehavior)
def poweredStateLogic: Receive =
commonBehavior
.orElse(mountBehavior)
.orElse { .orElse {
case CommonMessages.Use(player, Some((item: Tool, upgradeValue: Int))) case CommonMessages.Use(player, Some((item: Tool, upgradeValue: Int)))
if player.Faction == turret.Faction && if player.Faction == turret.Faction &&
@ -113,6 +117,12 @@ class FacilityTurretControl(turret: FacilityTurret)
case _ => ; case _ => ;
} }
def unpoweredStateLogic: Receive =
commonBehavior
.orElse {
case _ => ;
}
override protected def DamageAwareness(target : Damageable.Target, cause : ResolvedProjectile, amount : Any) : Unit = { override protected def DamageAwareness(target : Damageable.Target, cause : ResolvedProjectile, amount : Any) : Unit = {
tryAutoRepair() tryAutoRepair()
super.DamageAwareness(target, cause, amount) 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, 50, 0))
events ! AvatarServiceMessage(zoneId, AvatarAction.PlanetsideAttributeToAll(tguid, 51, 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 { .filter {
case (building, spawns) => case (building, spawns) =>
spawns.nonEmpty && spawns.nonEmpty &&
spawns.exists(_.Offline == false) && spawns.exists(_.isOffline == false) &&
structures.contains(building.BuildingType) structures.contains(building.BuildingType)
} }
.filter { .filter {
case (building, _) => case (building, _) =>
@ -368,7 +368,7 @@ class Zone(val id: String, val map: ZoneMap, zoneNumber: Int) {
} }
.map { .map {
case (building, spawns: List[SpawnPoint]) => case (building, spawns: List[SpawnPoint]) =>
(building, spawns.filter(!_.Offline)) (building, spawns.filter(!_.isOffline))
} }
.concat( .concat(
(if (ams) Vehicles else List()) (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> * `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` * `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> * `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> * `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> * `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> * `53 - LFS. Value is 1 to flag LFS`<br>

View file

@ -4,6 +4,7 @@ package objects
import akka.actor.Props import akka.actor.Props
import akka.testkit.TestProbe import akka.testkit.TestProbe
import base.FreedContextActorTest import base.FreedContextActorTest
import net.psforever.actors.zone.BuildingActor
import net.psforever.objects.avatar.Avatar import net.psforever.objects.avatar.Avatar
import net.psforever.objects.ballistics.{Projectile, ProjectileResolution, ResolvedProjectile, SourceEntry} import net.psforever.objects.ballistics.{Projectile, ProjectileResolution, ResolvedProjectile, SourceEntry}
import net.psforever.objects.guid.NumberPoolHub import net.psforever.objects.guid.NumberPoolHub
@ -57,6 +58,7 @@ class AutoRepairFacilityIntegrationTest extends FreedContextActorTest {
silo.NtuCapacitor = 1000 silo.NtuCapacitor = 1000
silo.Actor = system.actorOf(Props(classOf[ResourceSiloControl], silo), "test-silo") silo.Actor = system.actorOf(Props(classOf[ResourceSiloControl], silo), "test-silo")
silo.Actor ! "startup" silo.Actor ! "startup"
building.Actor ! BuildingActor.PowerOn() //artificial
val wep_fmode = weapon.FireMode val wep_fmode = weapon.FireMode
val wep_prof = wep_fmode.Add val wep_prof = wep_fmode.Add
@ -158,7 +160,7 @@ object AutoRepairIntegrationTest {
MaxHealth = 500 MaxHealth = 500
Damageable = true Damageable = true
Repairable = true Repairable = true
autoRepair = AutoRepairStats(1, 500, 500, 1) autoRepair = AutoRepairStats(200, 500, 500, 1)
RepairIfDestroyed = true RepairIfDestroyed = true
} }
} }

View file

@ -2,17 +2,22 @@
package objects package objects
import akka.actor.{ActorSystem, Props} import akka.actor.{ActorSystem, Props}
import akka.testkit.TestProbe
import base.ActorTest import base.ActorTest
import net.psforever.objects.avatar.Avatar 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.{Default, GlobalDefinitions, Player}
import net.psforever.objects.serverobject.doors.{Door, DoorControl} import net.psforever.objects.serverobject.doors.{Door, DoorControl}
import net.psforever.objects.serverobject.structures.{Building, StructureType} 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.packet.game.UseItemMessage
import net.psforever.services.local.{LocalAction, LocalServiceMessage}
import net.psforever.types._ import net.psforever.types._
import org.specs2.mutable.Specification import org.specs2.mutable.Specification
import scala.concurrent.duration.Duration import scala.concurrent.duration._
class DoorTest extends Specification { class DoorTest extends Specification {
val player = Player(Avatar(0, "test", PlanetSideEmpire.TR, CharacterGender.Male, 0, CharacterVoice.Mute)) 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) val door = Door(GlobalDefinitions.door)
door.Open.isEmpty mustEqual true door.Open.isEmpty mustEqual true
door.Use(player, msg) door.Open = player
door.isOpen mustEqual true
door.Open.contains(player) mustEqual true door.Open.contains(player) mustEqual true
door.Use(player, msg) door.Open = None
door.Open.isEmpty mustEqual true door.Open.isEmpty mustEqual true
door.isOpen mustEqual false
} }
} }
} }
@ -81,6 +88,8 @@ class DoorControl2Test extends ActorTest {
"DoorControl" should { "DoorControl" should {
"open on use" in { "open on use" in {
val (player, door) = DoorControlTest.SetUpAgents(PlanetSideEmpire.TR) val (player, door) = DoorControlTest.SetUpAgents(PlanetSideEmpire.TR)
val probe = new TestProbe(system)
door.Zone.LocalEvents = probe.ref
val msg = UseItemMessage( val msg = UseItemMessage(
PlanetSideGUID(1), PlanetSideGUID(1),
PlanetSideGUID(0), PlanetSideGUID(0),
@ -96,13 +105,12 @@ class DoorControl2Test extends ActorTest {
) //faked ) //faked
assert(door.Open.isEmpty) assert(door.Open.isEmpty)
door.Actor ! Door.Use(player, msg) door.Actor ! CommonMessages.Use(player, Some(msg))
val reply = receiveOne(Duration.create(500, "ms")) val reply = probe.receiveOne(1000 milliseconds)
assert(reply.isInstanceOf[Door.DoorMessage]) assert(reply match {
val reply2 = reply.asInstanceOf[Door.DoorMessage] case LocalServiceMessage("test", LocalAction.DoorOpens(PlanetSideGUID(0), _, d)) => d eq door
assert(reply2.player == player) case _ => false
assert(reply2.msg == msg) })
assert(reply2.response == Door.OpenEvent())
assert(door.Open.isDefined) assert(door.Open.isDefined)
} }
} }
@ -124,16 +132,24 @@ class DoorControl3Test extends ActorTest {
object DoorControlTest { object DoorControlTest {
def SetUpAgents(faction: PlanetSideEmpire.Value)(implicit system: ActorSystem): (Player, Door) = { def SetUpAgents(faction: PlanetSideEmpire.Value)(implicit system: ActorSystem): (Player, Door) = {
val door = Door(GlobalDefinitions.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.Actor = system.actorOf(Props(classOf[DoorControl], door), "door")
door.Owner = new Building( door.Owner = new Building(
"Building", "Building",
building_guid = 0, building_guid = 0,
map_id = 0, map_id = 0,
Zone.Nowhere, zone,
StructureType.Building, StructureType.Building,
GlobalDefinitions.building GlobalDefinitions.building
) )
door.Owner.Faction = faction 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.NumberPoolHub
import net.psforever.objects.guid.source.MaxNumberSource import net.psforever.objects.guid.source.MaxNumberSource
import net.psforever.objects.serverobject.CommonMessages 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.serverobject.structures.{Building, StructureType}
import net.psforever.objects.vital.Vitality import net.psforever.objects.vital.Vitality
import net.psforever.objects.zones.{Zone, ZoneMap} import net.psforever.objects.zones.{Zone, ZoneMap}
@ -25,12 +25,12 @@ import scala.concurrent.duration._
class GeneratorTest extends Specification { class GeneratorTest extends Specification {
"Generator" should { "Generator" should {
"construct" in { "construct" in {
Generator(GlobalDefinitions.generator) Generator(GeneratorTest.generator_definition)
ok ok
} }
"start in 'Normal' condition" in { "start in 'Normal' condition" in {
val obj = Generator(GlobalDefinitions.generator) val obj = Generator(GeneratorTest.generator_definition)
obj.Condition mustEqual PlanetSideGeneratorState.Normal obj.Condition mustEqual PlanetSideGeneratorState.Normal
} }
} }
@ -39,7 +39,7 @@ class GeneratorTest extends Specification {
class GeneratorControlConstructTest extends ActorTest { class GeneratorControlConstructTest extends ActorTest {
"GeneratorControl" should { "GeneratorControl" should {
"construct" in { "construct" in {
val gen = Generator(GlobalDefinitions.generator) val gen = Generator(GeneratorTest.generator_definition)
gen.Actor = system.actorOf(Props(classOf[GeneratorControl], gen), "gen-control") gen.Actor = system.actorOf(Props(classOf[GeneratorControl], gen), "gen-control")
assert(gen.Actor != ActorRef.noSender) assert(gen.Actor != ActorRef.noSender)
} }
@ -57,7 +57,7 @@ class GeneratorControlDamageTest extends ActorTest {
val activityProbe = TestProbe() val activityProbe = TestProbe()
zone.Activity = activityProbe.ref 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.Position = Vector3(1, 0, 0)
gen.Actor = system.actorOf(Props(classOf[GeneratorControl], gen), "generator-control") gen.Actor = system.actorOf(Props(classOf[GeneratorControl], gen), "generator-control")
@ -106,19 +106,18 @@ class GeneratorControlDamageTest extends ActorTest {
assert(gen.Condition == PlanetSideGeneratorState.Normal) assert(gen.Condition == PlanetSideGeneratorState.Normal)
gen.Actor ! Vitality.Damage(applyDamageTo) gen.Actor ! Vitality.Damage(applyDamageTo)
val msg_avatar = avatarProbe.receiveN(2, 500 milliseconds) val msg_avatar = avatarProbe.receiveOne(500 milliseconds)
buildingProbe.expectNoMessage(200 milliseconds) val msg_building = buildingProbe.receiveOne(500 milliseconds)
assert( assert(
msg_avatar.head match { msg_avatar match {
case AvatarServiceMessage("test", AvatarAction.PlanetsideAttributeToAll(PlanetSideGUID(2), 0, _)) => true case AvatarServiceMessage("test", AvatarAction.PlanetsideAttributeToAll(PlanetSideGUID(2), 0, _)) => true
case _ => false case _ => false
} }
) )
assert( assert(
msg_avatar(1) match { msg_building match {
case AvatarServiceMessage("TestCharacter1", AvatarAction.GenericObjectAction(_, PlanetSideGUID(1), 15)) => case BuildingActor.AmenityStateChange(_, Some(GeneratorControl.Event.UnderAttack)) => true
true case _ => false
case _ => false
} }
) )
assert(gen.Health < gen.Definition.MaxHealth) assert(gen.Health < gen.Definition.MaxHealth)
@ -139,7 +138,7 @@ class GeneratorControlCriticalTest extends ActorTest {
val activityProbe = TestProbe() val activityProbe = TestProbe()
zone.Activity = activityProbe.ref 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.Position = Vector3(1, 0, 0)
gen.Actor = system.actorOf(Props(classOf[GeneratorControl], gen), "generator-control") gen.Actor = system.actorOf(Props(classOf[GeneratorControl], gen), "generator-control")
@ -190,25 +189,18 @@ class GeneratorControlCriticalTest extends ActorTest {
assert(gen.Condition == PlanetSideGeneratorState.Normal) assert(gen.Condition == PlanetSideGeneratorState.Normal)
gen.Actor ! Vitality.Damage(applyDamageTo) 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) val msg_building = buildingProbe.receiveOne(500 milliseconds)
assert( assert(
msg_avatar.head match { msg_avatar match {
case AvatarServiceMessage("test", AvatarAction.PlanetsideAttributeToAll(PlanetSideGUID(2), 0, _)) => true case AvatarServiceMessage("test", AvatarAction.PlanetsideAttributeToAll(PlanetSideGUID(2), 0, _)) => true
case _ => false case _ => false
} }
) )
assert(
msg_avatar(1) match {
case AvatarServiceMessage("TestCharacter1", AvatarAction.GenericObjectAction(_, PlanetSideGUID(1), 15)) =>
true
case _ => false
}
)
assert( assert(
msg_building match { msg_building match {
case BuildingActor.AmenityStateChange(o) => o eq gen case BuildingActor.AmenityStateChange(o, Some(GeneratorControl.Event.Critical)) => o eq gen
case _ => false case _ => false
} }
) )
assert(gen.Health < halfHealth) assert(gen.Health < halfHealth)
@ -229,7 +221,7 @@ class GeneratorControlDestroyedTest extends ActorTest {
val activityProbe = TestProbe() val activityProbe = TestProbe()
zone.Activity = activityProbe.ref 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.Position = Vector3(1, 0, 0)
gen.Actor = system.actorOf(Props(classOf[GeneratorControl], gen), "generator-control") gen.Actor = system.actorOf(Props(classOf[GeneratorControl], gen), "generator-control")
@ -269,7 +261,6 @@ class GeneratorControlDestroyedTest extends ActorTest {
Vector3(1, 0, 0) Vector3(1, 0, 0)
) )
val applyDamageTo = resolved.damage_model.Calculate(resolved) val applyDamageTo = resolved.damage_model.Calculate(resolved)
gen.Actor ! BuildingActor.NtuDepleted() //no auto-repair
expectNoMessage(200 milliseconds) expectNoMessage(200 milliseconds)
//we're not testing that the math is correct //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% assert(gen.Condition == PlanetSideGeneratorState.Normal) //skipped critical state because didn't transition ~50%
gen.Actor ! Vitality.Damage(applyDamageTo) gen.Actor ! Vitality.Damage(applyDamageTo)
val msg_avatar1 = avatarProbe.receiveOne(500 milliseconds) val msg_building12 = buildingProbe.receiveN(2,500 milliseconds)
buildingProbe.expectNoMessage(200 milliseconds)
assert( assert(
msg_avatar1 match { msg_building12.head match {
case AvatarServiceMessage("TestCharacter1", AvatarAction.GenericObjectAction(_, PlanetSideGUID(1), 16)) => case BuildingActor.AmenityStateChange(o, Some(GeneratorControl.Event.Offline)) => o eq gen
true case _ => false
}
)
assert(
msg_building12(1) match {
case BuildingActor.AmenityStateChange(o, Some(GeneratorControl.Event.Destabilized)) => o eq gen
case _ => false case _ => false
} }
) )
assert(gen.Health == 1) assert(gen.Health == 1)
assert(!gen.Destroyed) 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_avatar2 = avatarProbe.receiveN(3, 1000 milliseconds) //see DamageableEntity test file
val msg_building = buildingProbe.receiveOne(200 milliseconds) val msg_building = buildingProbe.receiveOne(200 milliseconds)
assert( assert(
msg_building match { msg_building match {
case BuildingActor.AmenityStateChange(o) => o eq gen case BuildingActor.AmenityStateChange(o, Some(GeneratorControl.Event.Destroyed)) => o eq gen
case _ => false case _ => false
} }
) )
assert( assert(
@ -352,7 +347,7 @@ class GeneratorControlKillsTest extends ActorTest {
val activityProbe = TestProbe() val activityProbe = TestProbe()
zone.Activity = activityProbe.ref 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.Position = Vector3(1, 0, 0)
gen.Actor = system.actorOf(Props(classOf[GeneratorControl], gen), "generator-control") gen.Actor = system.actorOf(Props(classOf[GeneratorControl], gen), "generator-control")
@ -400,7 +395,6 @@ class GeneratorControlKillsTest extends ActorTest {
Vector3(1, 0, 0) Vector3(1, 0, 0)
) )
val applyDamageTo = resolved.damage_model.Calculate(resolved) val applyDamageTo = resolved.damage_model.Calculate(resolved)
gen.Actor ! BuildingActor.NtuDepleted() //no auto-repair
expectNoMessage(200 milliseconds) expectNoMessage(200 milliseconds)
//we're not testing that the math is correct //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% assert(gen.Condition == PlanetSideGeneratorState.Normal) //skipped critical state because didn't transition ~50%
gen.Actor ! Vitality.Damage(applyDamageTo) gen.Actor ! Vitality.Damage(applyDamageTo)
val msg_avatar1 = avatarProbe.receiveN(2, 500 milliseconds) val msg_building12 = buildingProbe.receiveN(2,500 milliseconds)
buildingProbe.expectNoMessage(200 milliseconds)
player1Probe.expectNoMessage(200 milliseconds)
player2Probe.expectNoMessage(200 milliseconds)
assert( assert(
msg_avatar1.head match { msg_building12.head match {
case AvatarServiceMessage("TestCharacter1", AvatarAction.GenericObjectAction(_, PlanetSideGUID(1), 16)) => case BuildingActor.AmenityStateChange(o, Some(GeneratorControl.Event.Offline)) => o eq gen
true
case _ => false case _ => false
} }
) )
assert( assert(
msg_avatar1(1) match { msg_building12(1) match {
case AvatarServiceMessage("TestCharacter2", AvatarAction.GenericObjectAction(_, PlanetSideGUID(1), 16)) => case BuildingActor.AmenityStateChange(o, Some(GeneratorControl.Event.Destabilized)) => o eq gen
true
case _ => false case _ => false
} }
) )
assert(gen.Health == 1) assert(gen.Health == 1)
assert(!gen.Destroyed) assert(!gen.Destroyed)
assert(gen.Condition == PlanetSideGeneratorState.Normal) assert(gen.Condition == PlanetSideGeneratorState.Destroyed)
val msg_building = buildingProbe.receiveOne(10500 milliseconds) avatarProbe.expectNoMessage(9500 milliseconds)
val msg_avatar2 = avatarProbe.receiveN(3, 200 milliseconds) val msg_avatar2 = avatarProbe.receiveN(3, 1000 milliseconds) //see DamageableEntity test file
val msg_player1 = player1Probe.receiveOne(100 milliseconds) val msg_building = buildingProbe.receiveOne(200 milliseconds)
player2Probe.expectNoMessage(200 milliseconds)
assert( assert(
msg_building match { msg_building match {
case BuildingActor.AmenityStateChange(o) => o eq gen case BuildingActor.AmenityStateChange(o, Some(GeneratorControl.Event.Destroyed)) => o eq gen
case _ => false case _ => false
} }
) )
assert( assert(
@ -459,22 +447,25 @@ class GeneratorControlKillsTest extends ActorTest {
assert( assert(
msg_avatar2(2) match { msg_avatar2(2) match {
case AvatarServiceMessage( case AvatarServiceMessage(
"test", "test",
AvatarAction.SendResponse(_, TriggerEffectMessage(PlanetSideGUID(2), "explosion_generator", None, None)) AvatarAction.SendResponse(_, TriggerEffectMessage(PlanetSideGUID(2), "explosion_generator", None, None))
) => ) =>
true true
case _ => false 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( assert(
msg_player1 match { msg_player1 match {
case _ @Player.Die() => true case _ @Player.Die() => true
case _ => false case _ => false
} }
) )
assert(gen.Health == 0)
assert(gen.Destroyed)
assert(gen.Condition == PlanetSideGeneratorState.Destroyed)
} }
} }
} }
@ -487,7 +478,7 @@ class GeneratorControlNotDestroyTwice extends ActorTest {
GUID(guid) GUID(guid)
} }
val building = Building("test-building", 1, 1, zone, StructureType.Facility) //guid=1 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 = val player1 =
Player(Avatar(0, "TestCharacter1", PlanetSideEmpire.TR, CharacterGender.Male, 0, CharacterVoice.Mute)) //guid=3 Player(Avatar(0, "TestCharacter1", PlanetSideEmpire.TR, CharacterGender.Male, 0, CharacterVoice.Mute)) //guid=3
player1.Spawn() player1.Spawn()
@ -573,7 +564,7 @@ class GeneratorControlNotDamageIfExplodingTest extends ActorTest {
val activityProbe = TestProbe() val activityProbe = TestProbe()
zone.Activity = activityProbe.ref 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.Position = Vector3(1, 0, 0)
gen.Actor = system.actorOf(Props(classOf[GeneratorControl], gen), "generator-control") 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% assert(gen.Condition == PlanetSideGeneratorState.Normal) //skipped critical state because didn't transition ~50%
gen.Actor ! Vitality.Damage(applyDamageTo) gen.Actor ! Vitality.Damage(applyDamageTo)
val msg_avatar = avatarProbe.receiveOne(500 milliseconds) val msg_building12 = buildingProbe.receiveN(2,500 milliseconds)
buildingProbe.expectNoMessage(200 milliseconds)
player1Probe.expectNoMessage(200 milliseconds)
assert( assert(
msg_avatar match { msg_building12.head match {
case AvatarServiceMessage("TestCharacter1", AvatarAction.GenericObjectAction(_, PlanetSideGUID(1), 16)) => case BuildingActor.AmenityStateChange(o, Some(GeneratorControl.Event.Offline)) => o eq gen
true case _ => false
}
)
assert(
msg_building12(1) match {
case BuildingActor.AmenityStateChange(o, Some(GeneratorControl.Event.Destabilized)) => o eq gen
case _ => false case _ => false
} }
) )
assert(gen.Health == 1) assert(gen.Health == 1)
assert(!gen.Destroyed) assert(!gen.Destroyed)
assert(gen.Condition == PlanetSideGeneratorState.Normal) assert(gen.Condition == PlanetSideGeneratorState.Destroyed)
//going to explode state //going to explode state
//once //once
@ -667,7 +661,7 @@ class GeneratorControlNotRepairIfExplodingTest extends ActorTest {
val activityProbe = TestProbe() val activityProbe = TestProbe()
zone.Activity = activityProbe.ref 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.Position = Vector3(1, 0, 0)
gen.Actor = system.actorOf(Props(classOf[GeneratorControl], gen), "generator-control") 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% assert(gen.Condition == PlanetSideGeneratorState.Normal) //skipped critical state because didn't transition ~50%
gen.Actor ! Vitality.Damage(applyDamageTo) gen.Actor ! Vitality.Damage(applyDamageTo)
val msg_avatar1 = avatarProbe.receiveOne(500 milliseconds) val msg_building12 = buildingProbe.receiveN(2,500 milliseconds)
buildingProbe.expectNoMessage(200 milliseconds)
player1Probe.expectNoMessage(200 milliseconds)
assert( assert(
msg_avatar1 match { msg_building12.head match {
case AvatarServiceMessage("TestCharacter1", AvatarAction.GenericObjectAction(_, PlanetSideGUID(1), 16)) => case BuildingActor.AmenityStateChange(o, Some(GeneratorControl.Event.Offline)) => o eq gen
true case _ => false
}
)
assert(
msg_building12(1) match {
case BuildingActor.AmenityStateChange(o, Some(GeneratorControl.Event.Destabilized)) => o eq gen
case _ => false case _ => false
} }
) )
assert(gen.Health == 1) assert(gen.Health == 1)
assert(!gen.Destroyed) assert(!gen.Destroyed)
assert(gen.Condition == PlanetSideGeneratorState.Normal) assert(gen.Condition == PlanetSideGeneratorState.Destroyed)
//going to explode state //going to explode state
//once //once
@ -765,7 +762,7 @@ class GeneratorControlRepairPastRestorePoint extends ActorTest {
val activityProbe = TestProbe() val activityProbe = TestProbe()
zone.Activity = activityProbe.ref 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.Position = Vector3(1, 0, 0)
gen.Actor = system.actorOf(Props(classOf[GeneratorControl], gen), "generator-control") gen.Actor = system.actorOf(Props(classOf[GeneratorControl], gen), "generator-control")
@ -804,7 +801,7 @@ class GeneratorControlRepairPastRestorePoint extends ActorTest {
assert(gen.Destroyed) assert(gen.Destroyed)
gen.Actor ! CommonMessages.Use(player1, Some(tool)) //repair 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) val msg_building = buildingProbe.receiveOne(200 milliseconds)
assert( assert(
msg_avatar.head match { msg_avatar.head match {
@ -825,13 +822,6 @@ class GeneratorControlRepairPastRestorePoint extends ActorTest {
) )
assert( assert(
msg_avatar(2) match { msg_avatar(2) match {
case AvatarServiceMessage("TestCharacter1", AvatarAction.GenericObjectAction(_, PlanetSideGUID(1), 17)) =>
true
case _ => false
}
)
assert(
msg_avatar(3) match {
case AvatarServiceMessage( case AvatarServiceMessage(
"TestCharacter1", "TestCharacter1",
AvatarAction.SendResponse(_, RepairMessage(ValidPlanetSideGUID(2), _)) AvatarAction.SendResponse(_, RepairMessage(ValidPlanetSideGUID(2), _))
@ -842,7 +832,7 @@ class GeneratorControlRepairPastRestorePoint extends ActorTest {
) )
assert( assert(
msg_building match { msg_building match {
case BuildingActor.AmenityStateChange(o) => o eq gen case BuildingActor.AmenityStateChange(o, _) => o eq gen
case _ => false 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 AvatarServiceMessage("nowhere", AvatarAction.PlanetsideAttribute(PlanetSideGUID(6), 47, 0)) => true
case _ => false case _ => false
}) })
val reply4 = zoneEvents.receiveOne(500 milliseconds)
assert(reply4 match {
case AvatarServiceMessage("nowhere", AvatarAction.PlanetsideAttribute(PlanetSideGUID(6), 48, 0)) => true
case _ => false
})
} }
} }
} }