Milestone fixes (#376)

* Fix resource silos not showing the correct charge levels after server startup

* Make all continents have broadcast warpgates, disable geowarps, common initialization functions for zones

* Make instant action respect spawn point overrides e.g. z offset

* /warp and /zone restrictions from PTSv3

* Allow players hit by EMPs to disable implants correctly to prevent infinite stamina drain

* Quick fix to stop VehicleRemover from crashing if the target vehicle has no Actor assigned
This commit is contained in:
Mazo 2020-04-20 23:19:50 +01:00 committed by GitHub
parent 484fcbf56d
commit 140c2ccbc3
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
8 changed files with 152 additions and 97 deletions

View file

@ -45,7 +45,8 @@ class PlayerControl(player : Player) extends Actor
// todo: disable implants with stamina cost when changing armour type
val implantSlot = player.ImplantSlot(slot)
if(status == 0 && implantSlot.Active) {
// Allow uninitialized implants to be deactivated in case they're stuck in a state where they are no longer active or initialized but still draining stamina (e.g. by EMP)
if(status == 0 && (implantSlot.Active || !implantSlot.Initialized)) {
// Cancel stamina drain timer
implantSlotStaminaDrainTimers(slot).cancel()
implantSlotStaminaDrainTimers(slot) = DefaultCancellable.obj
@ -261,7 +262,7 @@ class PlayerControl(player : Player) extends Actor
for(slot <- 0 to player.Implants.length - 1) { // Deactivate & uninitialize all implants
player.Zone.AvatarEvents ! AvatarServiceMessage(player.Zone.Id, AvatarAction.PlanetsideAttribute(player.GUID, 28, player.Implant(slot).id * 2)) // Deactivation sound / effect
events ! AvatarServiceMessage(player.Name, AvatarAction.DeactivateImplantSlot(guid, slot))
self ! Player.ImplantActivation(slot, 0)
PlayerControl.UninitializeImplant(player, slot)
}

View file

@ -39,11 +39,7 @@ class ResourceSilo extends Amenity {
LowNtuWarningOn
}
def CapacitorDisplay : Long = capacitorDisplay
def CapacitorDisplay_=(value: Long) : Long = {
capacitorDisplay = value
CapacitorDisplay
}
def CapacitorDisplay : Long = scala.math.ceil((ChargeLevel.toFloat / MaximumCharge.toFloat) * 10).toInt
def Definition : ResourceSiloDefinition = GlobalDefinitions.resource_silo

View file

@ -50,6 +50,7 @@ class ResourceSiloControl(resourceSilo : ResourceSilo) extends Actor with Factio
case ResourceSilo.UpdateChargeLevel(amount: Int) =>
val siloChargeBeforeChange = resourceSilo.ChargeLevel
val siloDisplayBeforeChange = resourceSilo.CapacitorDisplay
val building = resourceSilo.Owner.asInstanceOf[Building]
val zone = building.Zone
@ -59,11 +60,9 @@ class ResourceSiloControl(resourceSilo : ResourceSilo) extends Actor with Factio
log.trace(s"UpdateChargeLevel: Silo ${resourceSilo.GUID} set to ${resourceSilo.ChargeLevel}")
}
val ntuBarLevel = scala.math.ceil((resourceSilo.ChargeLevel.toFloat / resourceSilo.MaximumCharge.toFloat) * 10).toInt
// Only send updated capacitor display value to all clients if it has actually changed
if(resourceSilo.CapacitorDisplay != ntuBarLevel) {
log.trace(s"Silo ${resourceSilo.GUID} NTU bar level has changed from ${resourceSilo.CapacitorDisplay} to $ntuBarLevel")
resourceSilo.CapacitorDisplay = ntuBarLevel
if(resourceSilo.CapacitorDisplay != siloDisplayBeforeChange) {
log.trace(s"Silo ${resourceSilo.GUID} NTU bar level has changed from $siloDisplayBeforeChange to ${resourceSilo.CapacitorDisplay}")
resourceSilo.Owner.Actor ! Building.SendMapUpdate(all_clients = true)
zone.AvatarEvents ! AvatarServiceMessage(
zone.Id,

View file

@ -8,7 +8,13 @@ import scodec.codecs._
/**
* Dispatched to the client in regards to cavern connections via geowarp gates.
* @param zone the zone
* @param unk na
* @param unk determines the number and composition of cavern links
* assuming two geowarps per continent the logic seems to be roughly:
* 0 - gate A disabled, B active
* 1 - gate A active, B disabled
* 2 - gate A and B active
* 3 - same as 2 (no change in destination)
* Destinations also change (north/south/east/west), but seemingly only to two of the currently active caverns can be linked to?
*/
final case class ZoneForcedCavernConnectionsMessage(zone : Int,
unk : Int)

View file

@ -1,6 +1,7 @@
// Copyright (c) 2017 PSForever
package services.vehicle.support
import akka.actor.ActorRef
import net.psforever.objects.Vehicle
import net.psforever.objects.guid.{GUIDTask, TaskResolver}
import net.psforever.objects.zones.Zone
@ -26,7 +27,9 @@ class VehicleRemover extends RemoverActor {
entry.zone.GUID(vehicleGUID) match {
case Some(vehicle : Vehicle) if vehicle.HasGUID =>
val zoneId = entry.zone.Id
vehicle.Actor ! Vehicle.PrepareForDeletion()
if(vehicle.Actor != ActorRef.noSender) {
vehicle.Actor ! Vehicle.PrepareForDeletion()
}
//escape being someone else's cargo
(vehicle.MountedIn match {
case Some(carrierGUID) =>

View file

@ -36,10 +36,9 @@ class ResourceSiloTest extends Specification {
//
obj.ChargeLevel = 50
obj.LowNtuWarningOn = false
obj.CapacitorDisplay = 75
obj.ChargeLevel mustEqual 50
obj.LowNtuWarningOn mustEqual false
obj.CapacitorDisplay mustEqual 75
obj.CapacitorDisplay mustEqual 1
}
"charge level can not exceed limits(0 to maximum)" in {
@ -237,7 +236,6 @@ class ResourceSiloControlUpdate2Test extends ActorTest {
bldg.Actor = buildingEvents.ref
obj.ChargeLevel = 100
obj.CapacitorDisplay = 1
obj.LowNtuWarningOn = true
assert(obj.ChargeLevel == 100)
assert(obj.CapacitorDisplay == 1)
@ -295,7 +293,6 @@ class ResourceSiloControlNoUpdateTest extends ActorTest {
bldg.Actor = buildingEvents.ref
obj.ChargeLevel = 250
obj.CapacitorDisplay = 3
obj.LowNtuWarningOn = false
assert(obj.ChargeLevel == 250)
assert(obj.CapacitorDisplay == 3)

View file

@ -1157,7 +1157,8 @@ class WorldSessionActor extends Actor
//in between subsequent reply messages, it does not matter if the destination changes
//so long as there is at least one destination at all (including the fallback)
if(ContemplateZoningResponse(Zoning.InstantAction.Request(player.Faction))) {
SpawnThroughZoningProcess(zone, spawn_point.Position, spawn_point)
val (pos, ori) = spawn_point.SpecificPoint(player)
SpawnThroughZoningProcess(zone, pos, ori)
}
else if(zoningStatus != Zoning.Status.None) {
instantActionFallbackDestination = Some(msg)
@ -1167,7 +1168,8 @@ class WorldSessionActor extends Actor
instantActionFallbackDestination match {
case Some(Zoning.InstantAction.Located(zone, _, spawn_point)) if spawn_point.Owner.Faction == player.Faction && !spawn_point.Offline =>
if(ContemplateZoningResponse(Zoning.InstantAction.Request(player.Faction))) {
SpawnThroughZoningProcess(zone, spawn_point.Position, spawn_point)
val (pos, ori) = spawn_point.SpecificPoint(player)
SpawnThroughZoningProcess(zone, pos, ori)
}
else if(zoningCounter == 0) {
CancelZoningProcessWithReason("@InstantActionNoHotspotsAvailable")
@ -1179,7 +1181,8 @@ class WorldSessionActor extends Actor
case Zoning.Recall.Located(zone, spawn_point) =>
if(ContemplateZoningResponse(Zoning.Recall.Request(player.Faction, zone.Id))) {
SpawnThroughZoningProcess(zone, spawn_point.Position, spawn_point)
val (pos, ori) = spawn_point.SpecificPoint(player)
SpawnThroughZoningProcess(zone, pos, ori)
}
case Zoning.Recall.Denied(reason) =>
@ -1591,10 +1594,10 @@ class WorldSessionActor extends Actor
/**
* Use the zoning process using some spawnable entity in the destination zone.
* @param zone the destination zone
* @param spawnPosition the destination spawn position (may not be related to `spawnPoint`)
* @param spawnPoint a `SpawntPoint` entity that is the target of our spawning in the destination zone
* @param spawnPosition the destination spawn position
* @param spawnOrientation the destination spawn orientation
*/
def SpawnThroughZoningProcess(zone : Zone, spawnPosition : Vector3, spawnPoint : SpawnPoint) : Unit = {
def SpawnThroughZoningProcess(zone : Zone, spawnPosition : Vector3, spawnOrientation : Vector3) : Unit = {
CancelZoningProcess()
PlayerActionsToCancel()
CancelAllProximityUnits()
@ -1608,7 +1611,7 @@ class WorldSessionActor extends Actor
//zone loading will take long enough
0L
}
LoadZonePhysicalSpawnPoint(zone.Id, spawnPosition, spawnPoint.Orientation, respawnTime)
LoadZonePhysicalSpawnPoint(zone.Id, spawnPosition, spawnOrientation, respawnTime)
}
/**
@ -4329,55 +4332,59 @@ class WorldSessionActor extends Actor
}
}
CSRZone.read(traveler, msg) match {
case (true, zone, pos) if player.isAlive =>
deadState = DeadState.Release //cancel movement updates
PlayerActionsToCancel()
continent.GUID(player.VehicleSeated) match {
case Some(vehicle : Vehicle) if vehicle.MountedIn.isEmpty =>
vehicle.PassengerInSeat(player) match {
case Some(0) =>
vehicle.Position = pos
CancelAllProximityUnits()
LoadZonePhysicalSpawnPoint(zone, pos, Vector3.Zero, 0)
case _ => //not seated as the driver, in which case we can't move
deadState = DeadState.Alive
}
case None =>
player.Position = pos
CancelAllProximityUnits()
//continent.AvatarEvents ! AvatarServiceMessage(continent.Id, AvatarAction.ObjectDelete(player.GUID, player.GUID))
CancelZoningProcess()
LoadZonePhysicalSpawnPoint(zone, pos, Vector3.Zero, 0)
case _ => //seated in something that is not a vehicle or the vehicle is cargo, in which case we can't move
deadState = DeadState.Alive
case (true, zone, pos) =>
if (player.isAlive && zone != player.Continent && (admin || zone == "z8" || zone == "c1" || zone == "c2" || zone == "c3" || zone == "c4" || zone == "c5" || zone == "c6" ||
zone == "tzshtr" || zone == "tzcotr" || zone == "tzdrtr" ||
zone == "tzshnc" || zone == "tzconc" || zone == "tzdrnc" ||
zone == "tzshvs" || zone == "tzcovs" || zone == "tzdrvs")) {
deadState = DeadState.Release //cancel movement updates
PlayerActionsToCancel()
continent.GUID(player.VehicleSeated) match {
case Some(vehicle : Vehicle) if vehicle.MountedIn.isEmpty =>
vehicle.PassengerInSeat(player) match {
case Some(0) =>
vehicle.Position = pos
LoadZonePhysicalSpawnPoint(zone, pos, Vector3.Zero, 0)
case _ => //not seated as the driver, in which case we can't move
deadState = DeadState.Alive
}
case None =>
player.Position = pos
//continent.AvatarEvents ! AvatarServiceMessage(continent.Id, AvatarAction.ObjectDelete(player.GUID, player.GUID))
LoadZonePhysicalSpawnPoint(zone, pos, Vector3.Zero, 0)
case _ => //seated in something that is not a vehicle or the vehicle is cargo, in which case we can't move
deadState = DeadState.Alive
}
}
case (_, _, _) => ;
}
CSRWarp.read(traveler, msg) match {
case (true, pos) if player.isAlive =>
deadState = DeadState.Release //cancel movement updates
PlayerActionsToCancel()
continent.GUID(player.VehicleSeated) match {
case Some(vehicle : Vehicle) if vehicle.MountedIn.isEmpty =>
vehicle.PassengerInSeat(player) match {
case Some(0) =>
vehicle.Position = pos
CancelAllProximityUnits()
LoadZonePhysicalSpawnPoint(continent.Id, pos, Vector3.z(vehicle.Orientation.z), 0)
case _ => //not seated as the driver, in which case we can't move
deadState = DeadState.Alive
}
case None =>
player.Position = pos
CancelAllProximityUnits()
CancelZoningProcessWithDescriptiveReason("cancel_motion")
sendResponse(PlayerStateShiftMessage(ShiftState(0, pos, player.Orientation.z, None)))
deadState = DeadState.Alive //must be set here
case _ => //seated in something that is not a vehicle or the vehicle is cargo, in which case we can't move
deadState = DeadState.Alive
}
CSRWarp.read(traveler, msg) match {
case (true, pos) =>
// continent.Id == "c1" || continent.Id == "c2" || continent.Id == "c3" || continent.Id == "c4" || continent.Id == "c5" || continent.Id == "c6" ||
if (player.isAlive && (admin || continent.Id == "z8" ||
continent.Id == "tzshtr" || continent.Id == "tzcotr" || continent.Id == "tzdrtr" ||
continent.Id == "tzshnc" || continent.Id == "tzconc" || continent.Id == "tzdrnc" ||
continent.Id == "tzshvs" || continent.Id == "tzcovs" || continent.Id == "tzdrvs")) {
deadState = DeadState.Release //cancel movement updates
PlayerActionsToCancel()
continent.GUID(player.VehicleSeated) match {
case Some(vehicle : Vehicle) if vehicle.MountedIn.isEmpty =>
vehicle.PassengerInSeat(player) match {
case Some(0) =>
vehicle.Position = pos
LoadZonePhysicalSpawnPoint(continent.Id, pos, Vector3.z(vehicle.Orientation.z), 0)
case _ => //not seated as the driver, in which case we can't move
deadState = DeadState.Alive
}
case None =>
player.Position = pos
sendResponse(PlayerStateShiftMessage(ShiftState(0, pos, player.Orientation.z, None)))
deadState = DeadState.Alive //must be set here
case _ => //seated in something that is not a vehicle or the vehicle is cargo, in which case we can't move
deadState = DeadState.Alive
}
}
case (_, _) => ;
}

View file

@ -14,10 +14,7 @@ object Zones {
HotSpotCoordinateFunction = Zones.HotSpots.StandardRemapping(Map.Scale, 80, 80)
HotSpotTimeFunction = Zones.HotSpots.StandardTimeRules
BuildingByMapId(1).get.asInstanceOf[WarpGate].BroadcastFor = PlanetSideEmpire.TR
BuildingByMapId(2).get.asInstanceOf[WarpGate].BroadcastFor = PlanetSideEmpire.TR
BuildingByMapId(3).get.asInstanceOf[WarpGate].BroadcastFor = PlanetSideEmpire.TR
BuildingByMapId(4).get.asInstanceOf[WarpGate].BroadcastFor = PlanetSideEmpire.TR
InitZoneAmenities(zone = this)
}
}
@ -26,6 +23,8 @@ object Zones {
super.Init(context)
HotSpotCoordinateFunction = Zones.HotSpots.StandardRemapping(Map.Scale, 80, 80)
HotSpotTimeFunction = Zones.HotSpots.StandardTimeRules
InitZoneAmenities(zone = this)
}
}
@ -34,6 +33,8 @@ object Zones {
super.Init(context)
HotSpotCoordinateFunction = Zones.HotSpots.StandardRemapping(Map.Scale, 80, 80)
HotSpotTimeFunction = Zones.HotSpots.StandardTimeRules
InitZoneAmenities(zone = this)
}
}
@ -43,13 +44,8 @@ object Zones {
HotSpotCoordinateFunction = Zones.HotSpots.StandardRemapping(Map.Scale, 80, 80)
HotSpotTimeFunction = Zones.HotSpots.StandardTimeRules
Buildings.values.flatMap {
_.Amenities.collect {
case amenity if amenity.Definition == GlobalDefinitions.resource_silo =>
val silo = amenity.asInstanceOf[ResourceSilo]
silo.ChargeLevel = silo.MaximumCharge
}
}
InitZoneAmenities(zone = this)
BuildingByMapId(5).get.Faction = PlanetSideEmpire.TR //Akkan
BuildingByMapId(6).get.Faction = PlanetSideEmpire.TR //Baal
BuildingByMapId(7).get.Faction = PlanetSideEmpire.TR //Dagon
@ -119,6 +115,8 @@ object Zones {
super.Init(context)
HotSpotCoordinateFunction = Zones.HotSpots.StandardRemapping(Map.Scale, 80, 80)
HotSpotTimeFunction = Zones.HotSpots.StandardTimeRules
InitZoneAmenities(zone = this)
}
}
@ -128,21 +126,12 @@ object Zones {
HotSpotCoordinateFunction = Zones.HotSpots.StandardRemapping(Map.Scale, 80, 80)
HotSpotTimeFunction = Zones.HotSpots.StandardTimeRules
GUID(2094) match {
case Some(silo : ResourceSilo) =>
silo.ChargeLevel = silo.MaximumCharge
case _ => ;
}
import net.psforever.types.PlanetSideEmpire
BuildingByMapId(2).get.Faction = PlanetSideEmpire.VS
BuildingByMapId(10).get.asInstanceOf[WarpGate].BroadcastFor = PlanetSideEmpire.VS
BuildingByMapId(11).get.asInstanceOf[WarpGate].BroadcastFor = PlanetSideEmpire.VS
BuildingByMapId(12).get.asInstanceOf[WarpGate].BroadcastFor = PlanetSideEmpire.VS
BuildingByMapId(13).get.asInstanceOf[WarpGate].BroadcastFor = PlanetSideEmpire.VS
BuildingByMapId(48).get.Faction = PlanetSideEmpire.VS
BuildingByMapId(49).get.Faction = PlanetSideEmpire.VS
BuildingByMapId(18657).get.asInstanceOf[WarpGate].Active = false
BuildingByMapId(18658).get.asInstanceOf[WarpGate].Active = false
InitZoneAmenities(zone = this)
}
}
@ -151,6 +140,8 @@ object Zones {
super.Init(context)
HotSpotCoordinateFunction = Zones.HotSpots.StandardRemapping(Map.Scale, 80, 80)
HotSpotTimeFunction = Zones.HotSpots.StandardTimeRules
InitZoneAmenities(zone = this)
}
}
@ -159,6 +150,8 @@ object Zones {
super.Init(context)
HotSpotCoordinateFunction = Zones.HotSpots.StandardRemapping(Map.Scale, 80, 80)
HotSpotTimeFunction = Zones.HotSpots.StandardTimeRules
InitZoneAmenities(zone = this)
}
}
@ -167,6 +160,8 @@ object Zones {
super.Init(context)
HotSpotCoordinateFunction = Zones.HotSpots.StandardRemapping(Map.Scale, 80, 80)
HotSpotTimeFunction = Zones.HotSpots.StandardTimeRules
InitZoneAmenities(zone = this)
}
}
@ -175,6 +170,8 @@ object Zones {
super.Init(context)
HotSpotCoordinateFunction = Zones.HotSpots.StandardRemapping(Map.Scale, 80, 80)
HotSpotTimeFunction = Zones.HotSpots.StandardTimeRules
InitZoneAmenities(zone = this)
}
}
@ -184,6 +181,8 @@ object Zones {
import net.psforever.types.PlanetSideEmpire
Buildings.values.foreach { _.Faction = PlanetSideEmpire.NC }
InitZoneAmenities(zone = this)
}
}
@ -193,9 +192,8 @@ object Zones {
import net.psforever.types.PlanetSideEmpire
Buildings.values.foreach { _.Faction = PlanetSideEmpire.TR }
BuildingByMapId(1).get.asInstanceOf[WarpGate].Broadcast = true
BuildingByMapId(2).get.asInstanceOf[WarpGate].Broadcast = true
BuildingByMapId(3).get.asInstanceOf[WarpGate].Broadcast = true
InitZoneAmenities(zone = this)
}
}
@ -205,9 +203,8 @@ object Zones {
import net.psforever.types.PlanetSideEmpire
Buildings.values.foreach { _.Faction = PlanetSideEmpire.VS }
BuildingByMapId(60).get.Faction = PlanetSideEmpire.NC //South Villa Gun Tower
BuildingByMapId(1).get.asInstanceOf[WarpGate].Broadcast = true
BuildingByMapId(3).get.asInstanceOf[WarpGate].Broadcast = true
InitZoneAmenities(zone = this)
}
}
@ -234,6 +231,8 @@ object Zones {
super.Init(context)
HotSpotCoordinateFunction = Zones.HotSpots.StandardRemapping(Map.Scale, 80, 80)
HotSpotTimeFunction = Zones.HotSpots.StandardTimeRules
InitZoneAmenities(zone = this)
}
}
@ -242,6 +241,8 @@ object Zones {
super.Init(context)
HotSpotCoordinateFunction = Zones.HotSpots.StandardRemapping(Map.Scale, 80, 80)
HotSpotTimeFunction = Zones.HotSpots.StandardTimeRules
InitZoneAmenities(zone = this)
}
}
@ -250,6 +251,8 @@ object Zones {
super.Init(context)
HotSpotCoordinateFunction = Zones.HotSpots.StandardRemapping(Map.Scale, 80, 80)
HotSpotTimeFunction = Zones.HotSpots.StandardTimeRules
InitZoneAmenities(zone = this)
}
}
@ -258,6 +261,8 @@ object Zones {
super.Init(context)
HotSpotCoordinateFunction = Zones.HotSpots.StandardRemapping(Map.Scale, 80, 80)
HotSpotTimeFunction = Zones.HotSpots.StandardTimeRules
InitZoneAmenities(zone = this)
}
}
@ -266,6 +271,8 @@ object Zones {
super.Init(context)
HotSpotCoordinateFunction = Zones.HotSpots.StandardRemapping(Map.Scale, 80, 80)
HotSpotTimeFunction = Zones.HotSpots.StandardTimeRules
InitZoneAmenities(zone = this)
}
}
@ -274,6 +281,8 @@ object Zones {
super.Init(context)
HotSpotCoordinateFunction = Zones.HotSpots.StandardRemapping(Map.Scale, 80, 80)
HotSpotTimeFunction = Zones.HotSpots.StandardTimeRules
InitZoneAmenities(zone = this)
}
}
@ -282,6 +291,8 @@ object Zones {
super.Init(context)
HotSpotCoordinateFunction = Zones.HotSpots.StandardRemapping(Map.Scale, 80, 80)
HotSpotTimeFunction = Zones.HotSpots.StandardTimeRules
InitZoneAmenities(zone = this)
}
}
@ -290,6 +301,8 @@ object Zones {
super.Init(context)
HotSpotCoordinateFunction = Zones.HotSpots.StandardRemapping(Map.Scale, 80, 80)
HotSpotTimeFunction = Zones.HotSpots.StandardTimeRules
InitZoneAmenities(zone = this)
}
}
@ -298,6 +311,8 @@ object Zones {
super.Init(context)
HotSpotCoordinateFunction = Zones.HotSpots.StandardRemapping(Map.Scale, 80, 80)
HotSpotTimeFunction = Zones.HotSpots.StandardTimeRules
InitZoneAmenities(zone = this)
}
}
@ -306,6 +321,37 @@ object Zones {
super.Init(context)
HotSpotCoordinateFunction = Zones.HotSpots.StandardRemapping(Map.Scale, 80, 80)
HotSpotTimeFunction = Zones.HotSpots.StandardTimeRules
InitZoneAmenities(zone = this)
}
}
def InitZoneAmenities(zone: Zone): Unit = {
InitResourceSilos(zone)
InitWarpGates(zone)
def InitWarpGates(zone: Zone): Unit = {
// todo: work out which faction owns links to this warpgate and if they should be marked as broadcast or not
// todo: enable geowarps to go to the correct cave
zone.Buildings.values.collect {
case wg : WarpGate if wg.Definition == GlobalDefinitions.warpgate || wg.Definition == GlobalDefinitions.warpgate_small =>
wg.Active = true
wg.Faction = PlanetSideEmpire.NEUTRAL
wg.Broadcast = true
case geowarp : WarpGate if geowarp.Definition == GlobalDefinitions.warpgate_cavern || geowarp.Definition == GlobalDefinitions.hst =>
geowarp.Faction = PlanetSideEmpire.NEUTRAL
geowarp.Active = false
}
}
def InitResourceSilos(zone: Zone): Unit = {
// todo: load silo charge from database
zone.Buildings.values.flatMap {
_.Amenities.collect {
case silo : ResourceSilo =>
silo.Actor ! ResourceSilo.UpdateChargeLevel(silo.MaximumCharge)
}
}
}
}