CatStation/Content.Server/Power/EntitySystems/PowerMonitoringConsoleSystem.cs
Dmitry 25c5e59248
Upstream (#3172)
Signed-off-by: Prole <172158352+Prole0@users.noreply.github.com>
Co-authored-by: PJBot <pieterjan.briers+bot@gmail.com>
Co-authored-by: ScarKy0 <106310278+ScarKy0@users.noreply.github.com>
Co-authored-by: Samuka-C <47865393+Samuka-C@users.noreply.github.com>
Co-authored-by: ArtisticRoomba <145879011+ArtisticRoomba@users.noreply.github.com>
Co-authored-by: Partmedia <kevinz5000@gmail.com>
Co-authored-by: slarticodefast <161409025+slarticodefast@users.noreply.github.com>
Co-authored-by: themias <89101928+themias@users.noreply.github.com>
Co-authored-by: Victor Shen <71985089+Vexerot@users.noreply.github.com>
Co-authored-by: Ed <96445749+TheShuEd@users.noreply.github.com>
Co-authored-by: Milon <milonpl.git@proton.me>
Co-authored-by: Kirus59 <145689588+Kirus59@users.noreply.github.com>
Co-authored-by: Nemanja <98561806+EmoGarbage404@users.noreply.github.com>
Co-authored-by: Stomf <5dorkydorks@gmail.com>
Co-authored-by: drakewill-CRL <46307022+drakewill-CRL@users.noreply.github.com>
Co-authored-by: PraxisMapper <praxismapper@gmail.com>
Co-authored-by: EmoGarbage404 <retron404@gmail.com>
Co-authored-by: lzk <124214523+lzk228@users.noreply.github.com>
Co-authored-by: Princess Cheeseballs <66055347+Pronana@users.noreply.github.com>
Co-authored-by: IProduceWidgets <107586145+IProduceWidgets@users.noreply.github.com>
Co-authored-by: TytosB <54259736+TytosB@users.noreply.github.com>
Co-authored-by: abadaba695 <spacestation13thingy@gmail.com>
Co-authored-by: kosticia <kosticia46@gmail.com>
Co-authored-by: Thinbug <101073555+Thinbug0@users.noreply.github.com>
Co-authored-by: pathetic meowmeow <uhhadd@gmail.com>
Co-authored-by: SlamBamActionman <83650252+SlamBamActionman@users.noreply.github.com>
Co-authored-by: Boaz1111 <149967078+Boaz1111@users.noreply.github.com>
Co-authored-by: ActiveMammmoth <140334666+ActiveMammmoth@users.noreply.github.com>
Co-authored-by: Myra <vasilis@pikachu.systems>
Co-authored-by: Whatstone <166147148+whatston3@users.noreply.github.com>
Co-authored-by: Pieter-Jan Briers <pieterjan.briers+git@gmail.com>
Co-authored-by: K-Dynamic <20566341+K-Dynamic@users.noreply.github.com>
Co-authored-by: Gentleman-Bird <dcgreen406@gmail.com>
Co-authored-by: metalgearsloth <31366439+metalgearsloth@users.noreply.github.com>
Co-authored-by: BIGZi0348 <svalker0348@gmail.com>
Co-authored-by: LaCumbiaDelCoronavirus <90893484+LaCumbiaDelCoronavirus@users.noreply.github.com>
Co-authored-by: imatsoup <93290208+imatsoup@users.noreply.github.com>
Co-authored-by: Matthew Herber <32679887+happyrobot33@users.noreply.github.com>
Co-authored-by: Ertanic <36124833+Ertanic@users.noreply.github.com>
Co-authored-by: MissKay1994 <15877268+MissKay1994@users.noreply.github.com>
Co-authored-by: Errant <35878406+Errant-4@users.noreply.github.com>
Co-authored-by: eoineoineoin <helloworld@eoinrul.es>
Co-authored-by: Tiniest Shark <head.rebel@yahoo.com>
Co-authored-by: nikitosych <boriszyn@gmail.com>
Co-authored-by: Tayrtahn <tayrtahn@gmail.com>
Co-authored-by: Perry Fraser <perryprog@users.noreply.github.com>
Co-authored-by: YoungThug <ramialanbagy@gmail.com>
Co-authored-by: beck-thompson <107373427+beck-thompson@users.noreply.github.com>
Co-authored-by: metalgearsloth <comedian_vs_clown@hotmail.com>
Co-authored-by: Southbridge <7013162+southbridge-fur@users.noreply.github.com>
Co-authored-by: Vladislav Suchkov <20380250+murolem@users.noreply.github.com>
Co-authored-by: Prole <172158352+Prole0@users.noreply.github.com>
Co-authored-by: Unkn0wn_Gh0st <shadowstalkermll@gmail.com>
Co-authored-by: 3nderall <101940324+3nderall@users.noreply.github.com>
Co-authored-by: Radezolid <snappednexus@gmail.com>
Co-authored-by: J <billsmith116@gmail.com>
Co-authored-by: Ghagliiarghii <68826635+Ghagliiarghii@users.noreply.github.com>
Co-authored-by: chromiumboy <50505512+chromiumboy@users.noreply.github.com>
Co-authored-by: youtissoum <51883137+youtissoum@users.noreply.github.com>
Co-authored-by: Minemoder5000 <minemoder50000@gmail.com>
Co-authored-by: Spanky <scott@wearejacob.com>
Co-authored-by: Spessmann <156740760+Spessmann@users.noreply.github.com>
Co-authored-by: github-actions[bot] <41898282+github-actions[bot]@users.noreply.github.com>
Co-authored-by: brainfood1183 <113240905+brainfood1183@users.noreply.github.com>
Co-authored-by: Deerstop <edainturner@gmail.com>
Co-authored-by: B_Kirill <153602297+B-Kirill@users.noreply.github.com>
Co-authored-by: archee1 <archee3@hotmail.co.uk>
Co-authored-by: Cojoke <83733158+Cojoke-dot@users.noreply.github.com>
Co-authored-by: Quantum-cross <7065792+Quantum-cross@users.noreply.github.com>
Co-authored-by: poklj <compgeek223@gmail.com>
Co-authored-by: Krunklehorn <42424291+Krunklehorn@users.noreply.github.com>
Co-authored-by: OnyxTheBrave <131422822+OnyxTheBrave@users.noreply.github.com>
Co-authored-by: UpAndLeaves <92269094+Alpha-Two@users.noreply.github.com>
Co-authored-by: Flareguy <78941145+Flareguy@users.noreply.github.com>
Co-authored-by: Zalycon <84675130+Zalycon@users.noreply.github.com>
Co-authored-by: deltanedas <39013340+deltanedas@users.noreply.github.com>
Co-authored-by: Verm <32827189+Vermidia@users.noreply.github.com>
Co-authored-by: nikthechampiongr <32041239+nikthechampiongr@users.noreply.github.com>
Co-authored-by: ScarKy0 <scarky0@onet.eu>
Co-authored-by: Dmitry <57028746+dimm00n@users.noreply.github.com>
2025-05-24 11:00:17 +03:00

1018 lines
38 KiB
C#

using Content.Server.NodeContainer;
using Content.Server.NodeContainer.EntitySystems;
using Content.Server.NodeContainer.Nodes;
using Content.Server.Power.Components;
using Content.Server.Power.Nodes;
using Content.Server.Power.NodeGroups;
using Content.Server.StationEvents.Components;
using Content.Shared.GameTicking.Components;
using Content.Shared.Pinpointer;
using Content.Shared.Station.Components;
using Content.Shared.Power;
using JetBrains.Annotations;
using Robust.Server.GameObjects;
using Robust.Shared.Map.Components;
using Robust.Shared.Utility;
using System.Linq;
using Content.Shared.NodeContainer;
namespace Content.Server.Power.EntitySystems;
[UsedImplicitly]
internal sealed partial class PowerMonitoringConsoleSystem : SharedPowerMonitoringConsoleSystem
{
[Dependency] private readonly UserInterfaceSystem _userInterfaceSystem = default!;
[Dependency] private readonly SharedMapSystem _sharedMapSystem = default!;
// Note: this data does not need to be saved
private Dictionary<EntityUid, Dictionary<Vector2i, PowerCableChunk>> _gridPowerCableChunks = new();
private float _updateTimer = 1.0f;
private const float UpdateTime = 1.0f;
private const float RoguePowerConsumerThreshold = 100000;
public override void Initialize()
{
base.Initialize();
// Console events
SubscribeLocalEvent<PowerMonitoringConsoleComponent, ComponentInit>(OnConsoleInit);
SubscribeLocalEvent<PowerMonitoringConsoleComponent, EntParentChangedMessage>(OnConsoleParentChanged);
SubscribeLocalEvent<PowerMonitoringCableNetworksComponent, ComponentInit>(OnCableNetworksInit);
SubscribeLocalEvent<PowerMonitoringCableNetworksComponent, EntParentChangedMessage>(OnCableNetworksParentChanged);
// UI events
SubscribeLocalEvent<PowerMonitoringConsoleComponent, PowerMonitoringConsoleMessage>(OnPowerMonitoringConsoleMessage);
SubscribeLocalEvent<PowerMonitoringConsoleComponent, BoundUIOpenedEvent>(OnBoundUIOpened);
// Grid events
SubscribeLocalEvent<GridSplitEvent>(OnGridSplit);
SubscribeLocalEvent<CableComponent, CableAnchorStateChangedEvent>(OnCableAnchorStateChanged);
SubscribeLocalEvent<PowerMonitoringDeviceComponent, AnchorStateChangedEvent>(OnDeviceAnchoringChanged);
SubscribeLocalEvent<PowerMonitoringDeviceComponent, NodeGroupsRebuilt>(OnNodeGroupRebuilt);
// Game rule events
SubscribeLocalEvent<GameRuleStartedEvent>(OnPowerGridCheckStarted);
SubscribeLocalEvent<GameRuleEndedEvent>(OnPowerGridCheckEnded);
}
#region EventHandling
private void OnConsoleInit(EntityUid uid, PowerMonitoringConsoleComponent component, ComponentInit args)
{
RefreshPowerMonitoringConsole(uid, component);
}
private void OnConsoleParentChanged(EntityUid uid, PowerMonitoringConsoleComponent component, EntParentChangedMessage args)
{
RefreshPowerMonitoringConsole(uid, component);
}
private void OnCableNetworksInit(EntityUid uid, PowerMonitoringCableNetworksComponent component, ComponentInit args)
{
RefreshPowerMonitoringCableNetworks(uid, component);
}
private void OnCableNetworksParentChanged(EntityUid uid, PowerMonitoringCableNetworksComponent component, EntParentChangedMessage args)
{
RefreshPowerMonitoringCableNetworks(uid, component);
}
private void OnPowerMonitoringConsoleMessage(EntityUid uid, PowerMonitoringConsoleComponent component, PowerMonitoringConsoleMessage args)
{
var focus = EntityManager.GetEntity(args.FocusDevice);
var group = args.FocusGroup;
// Update this if the focus device has changed
if (component.Focus != focus)
{
component.Focus = focus;
if (TryComp<PowerMonitoringCableNetworksComponent>(uid, out var cableNetworks))
{
cableNetworks.FocusChunks.Clear(); // Component will be dirtied when these chunks are rebuilt, unless the focus is null
if (focus == null)
Dirty(uid, cableNetworks);
}
}
// Update this if the focus group has changed
if (component.FocusGroup != group)
{
component.FocusGroup = args.FocusGroup;
Dirty(uid, component);
}
}
private void OnBoundUIOpened(EntityUid uid, PowerMonitoringConsoleComponent component, BoundUIOpenedEvent args)
{
component.Focus = null;
component.FocusGroup = PowerMonitoringConsoleGroup.Generator;
if (TryComp<PowerMonitoringCableNetworksComponent>(uid, out var cableNetworks))
{
cableNetworks.FocusChunks.Clear();
Dirty(uid, cableNetworks);
}
}
private void OnGridSplit(ref GridSplitEvent args)
{
// Collect grids
var allGrids = args.NewGrids.ToList();
if (!allGrids.Contains(args.Grid))
allGrids.Add(args.Grid);
// Refresh affected power cable grids
foreach (var grid in allGrids)
{
if (!TryComp<MapGridComponent>(grid, out var map))
continue;
RefreshPowerCableGrid(grid, map);
}
// Update power monitoring consoles that stand upon an updated grid
var query = AllEntityQuery<PowerMonitoringConsoleComponent, PowerMonitoringCableNetworksComponent, TransformComponent>();
while (query.MoveNext(out var ent, out var entConsole, out var entCableNetworks, out var entXform))
{
if (entXform.GridUid == null)
continue;
if (!allGrids.Contains(entXform.GridUid.Value))
continue;
RefreshPowerMonitoringConsole(ent, entConsole);
RefreshPowerMonitoringCableNetworks(ent, entCableNetworks);
}
}
public void OnCableAnchorStateChanged(EntityUid uid, CableComponent component, CableAnchorStateChangedEvent args)
{
var xform = args.Transform;
if (xform.GridUid == null || !TryComp<MapGridComponent>(xform.GridUid, out var grid))
return;
if (!_gridPowerCableChunks.TryGetValue(xform.GridUid.Value, out var allChunks))
allChunks = new();
var tile = _sharedMapSystem.LocalToTile(xform.GridUid.Value, grid, xform.Coordinates);
var chunkOrigin = SharedMapSystem.GetChunkIndices(tile, ChunkSize);
if (!allChunks.TryGetValue(chunkOrigin, out var chunk))
{
chunk = new PowerCableChunk(chunkOrigin);
allChunks[chunkOrigin] = chunk;
}
var relative = SharedMapSystem.GetChunkRelative(tile, ChunkSize);
var flag = GetFlag(relative);
if (args.Anchored)
chunk.PowerCableData[(int) component.CableType] |= flag;
else
chunk.PowerCableData[(int) component.CableType] &= ~flag;
var query = AllEntityQuery<PowerMonitoringCableNetworksComponent, TransformComponent>();
while (query.MoveNext(out var ent, out var entCableNetworks, out var entXform))
{
if (entXform.GridUid != xform.GridUid)
continue;
entCableNetworks.AllChunks = allChunks;
Dirty(ent, entCableNetworks);
}
}
private void OnDeviceAnchoringChanged(EntityUid uid, PowerMonitoringDeviceComponent component, AnchorStateChangedEvent args)
{
var xform = Transform(uid);
var gridUid = xform.GridUid;
if (gridUid == null)
return;
if (component.IsCollectionMasterOrChild)
AssignEntityAsCollectionMaster(uid, component, xform);
var query = AllEntityQuery<PowerMonitoringConsoleComponent, TransformComponent>();
while (query.MoveNext(out var ent, out var entConsole, out var entXform))
{
if (gridUid != entXform.GridUid)
continue;
if (!args.Anchored)
{
entConsole.PowerMonitoringDeviceMetaData.Remove(EntityManager.GetNetEntity(uid));
Dirty(ent, entConsole);
continue;
}
var name = MetaData(uid).EntityName;
var coords = EntityManager.GetNetCoordinates(xform.Coordinates);
var metaData = new PowerMonitoringDeviceMetaData(name, coords, component.Group, component.SpritePath, component.SpriteState);
entConsole.PowerMonitoringDeviceMetaData.TryAdd(EntityManager.GetNetEntity(uid), metaData);
Dirty(ent, entConsole);
}
}
public void OnNodeGroupRebuilt(EntityUid uid, PowerMonitoringDeviceComponent component, NodeGroupsRebuilt args)
{
if (component.IsCollectionMasterOrChild)
AssignEntityAsCollectionMaster(uid, component);
var query = AllEntityQuery<PowerMonitoringConsoleComponent, PowerMonitoringCableNetworksComponent>();
while (query.MoveNext(out var _, out var entConsole, out var entCableNetworks))
{
if (entConsole.Focus == uid)
entCableNetworks.FocusChunks.Clear(); // Component is dirtied when these chunks are rebuilt
}
}
private void OnPowerGridCheckStarted(ref GameRuleStartedEvent ev)
{
if (!TryComp<PowerGridCheckRuleComponent>(ev.RuleEntity, out var rule))
return;
var query = AllEntityQuery<PowerMonitoringConsoleComponent, TransformComponent>();
while (query.MoveNext(out var uid, out var console, out var xform))
{
if (CompOrNull<StationMemberComponent>(xform.GridUid)?.Station == rule.AffectedStation)
{
console.Flags |= PowerMonitoringFlags.PowerNetAbnormalities;
Dirty(uid, console);
}
}
}
private void OnPowerGridCheckEnded(ref GameRuleEndedEvent ev)
{
if (!TryComp<PowerGridCheckRuleComponent>(ev.RuleEntity, out var rule))
return;
var query = AllEntityQuery<PowerMonitoringConsoleComponent, TransformComponent>();
while (query.MoveNext(out var uid, out var console, out var xform))
{
if (CompOrNull<StationMemberComponent>(xform.GridUid)?.Station == rule.AffectedStation)
{
console.Flags &= ~PowerMonitoringFlags.PowerNetAbnormalities;
Dirty(uid, console);
}
}
}
#endregion
public override void Update(float frameTime)
{
base.Update(frameTime);
_updateTimer += frameTime;
if (_updateTimer >= UpdateTime)
{
_updateTimer -= UpdateTime;
var query = AllEntityQuery<PowerMonitoringConsoleComponent>();
while (query.MoveNext(out var ent, out var console))
{
if (!_userInterfaceSystem.IsUiOpen(ent, PowerMonitoringConsoleUiKey.Key))
continue;
UpdateUIState(ent, console);
}
}
}
private void UpdateUIState(EntityUid uid, PowerMonitoringConsoleComponent component)
{
var consoleXform = Transform(uid);
if (consoleXform?.GridUid == null)
return;
var gridUid = consoleXform.GridUid.Value;
if (!TryComp<MapGridComponent>(gridUid, out var mapGrid))
return;
// The grid must have a NavMapComponent to visualize the map in the UI
EnsureComp<NavMapComponent>(gridUid);
// Initializing data to be send to the client
var totalSources = 0d;
var totalBatteryUsage = 0d;
var totalLoads = 0d;
var allEntries = new List<PowerMonitoringConsoleEntry>();
var sourcesForFocus = new List<PowerMonitoringConsoleEntry>();
var loadsForFocus = new List<PowerMonitoringConsoleEntry>();
var flags = component.Flags;
// Reset RoguePowerConsumer flag
component.Flags &= ~PowerMonitoringFlags.RoguePowerConsumer;
// Record the load value of all non-tracked power consumers on the same grid as the console
var powerConsumerQuery = AllEntityQuery<PowerConsumerComponent, TransformComponent>();
while (powerConsumerQuery.MoveNext(out var ent, out var powerConsumer, out var xform))
{
if (xform.Anchored == false || xform.GridUid != gridUid)
continue;
if (TryComp<PowerMonitoringDeviceComponent>(ent, out var device))
continue;
// Flag an alert if power consumption is ridiculous
if (powerConsumer.ReceivedPower >= RoguePowerConsumerThreshold)
component.Flags |= PowerMonitoringFlags.RoguePowerConsumer;
totalLoads += powerConsumer.DrawRate;
}
if (component.Flags != flags)
Dirty(uid, component);
// Loop over all tracked devices
var powerMonitoringDeviceQuery = AllEntityQuery<PowerMonitoringDeviceComponent, TransformComponent>();
while (powerMonitoringDeviceQuery.MoveNext(out var ent, out var device, out var xform))
{
// Ignore joint, non-master entities
if (device.IsCollectionMasterOrChild && !device.IsCollectionMaster)
continue;
if (xform.Anchored == false || xform.GridUid != gridUid)
continue;
// Get the device power stats
var powerStats = GetPowerStats(ent, device);
//, out var powerSupplied, out var powerUsage, out var batteryUsage);
// Update all running totals
totalSources += powerStats.PowerSupplied;
totalLoads += powerStats.PowerUsage;
totalBatteryUsage += powerStats.BatteryUsage;
// Continue on if the device is not in the current focus group
if (device.Group != component.FocusGroup)
continue;
// Generate a new console entry with which to populate the UI
var entry = new PowerMonitoringConsoleEntry(EntityManager.GetNetEntity(ent), device.Group, powerStats.PowerValue, powerStats.BatteryLevel);
allEntries.Add(entry);
}
// Update the UI focus data (if applicable)
if (component.Focus != null)
{
if (TryComp<NodeContainerComponent>(component.Focus, out var nodeContainer) &&
TryComp<PowerMonitoringDeviceComponent>(component.Focus, out var device))
{
// Record the tracked sources powering the device
if (nodeContainer.Nodes.TryGetValue(device.SourceNode, out var sourceNode))
GetSourcesForNode(component.Focus.Value, sourceNode, out sourcesForFocus);
// Search for the enabled load node (required for portable generators)
var loadNodeName = device.LoadNode;
if (device.LoadNodes != null)
{
var foundNode = nodeContainer.Nodes.FirstOrNull(x => x.Value is CableDeviceNode && (x.Value as CableDeviceNode)?.Enabled == true);
if (foundNode != null)
loadNodeName = foundNode.Value.Key;
}
// Record the tracked loads on the device
if (nodeContainer.Nodes.TryGetValue(loadNodeName, out var loadNode))
GetLoadsForNode(component.Focus.Value, loadNode, out loadsForFocus);
// If the UI focus changed, update the highlighted power network
if (TryComp<PowerMonitoringCableNetworksComponent>(uid, out var cableNetworks) &&
cableNetworks.FocusChunks.Count == 0)
{
var reachableEntities = new List<EntityUid>();
if (sourceNode?.NodeGroup != null)
{
foreach (var node in sourceNode.NodeGroup.Nodes)
reachableEntities.Add(node.Owner);
}
if (loadNode?.NodeGroup != null)
{
foreach (var node in loadNode.NodeGroup.Nodes)
reachableEntities.Add(node.Owner);
}
UpdateFocusNetwork(uid, cableNetworks, gridUid, mapGrid, reachableEntities);
}
}
}
// Set the UI state
_userInterfaceSystem.SetUiState(uid,
PowerMonitoringConsoleUiKey.Key,
new PowerMonitoringConsoleBoundInterfaceState
(totalSources,
totalBatteryUsage,
totalLoads,
allEntries.ToArray(),
sourcesForFocus.ToArray(),
loadsForFocus.ToArray()));
}
private PowerStats GetPowerStats(EntityUid uid, PowerMonitoringDeviceComponent device)
{
var stats = new PowerStats();
if (device.Group == PowerMonitoringConsoleGroup.Generator)
{
// This covers most power sources
if (TryComp<PowerSupplierComponent>(uid, out var supplier))
{
stats.PowerValue = supplier.CurrentSupply;
stats.PowerSupplied += stats.PowerValue;
}
// Edge case: radiation collectors
else if (TryComp<BatteryDischargerComponent>(uid, out var _) &&
TryComp<PowerNetworkBatteryComponent>(uid, out var battery))
{
stats.PowerValue = battery.NetworkBattery.CurrentSupply;
stats.PowerSupplied += stats.PowerValue;
stats.BatteryLevel = GetBatteryLevel(uid);
}
}
else if (device.Group == PowerMonitoringConsoleGroup.SMES ||
device.Group == PowerMonitoringConsoleGroup.Substation ||
device.Group == PowerMonitoringConsoleGroup.APC)
{
if (TryComp<PowerNetworkBatteryComponent>(uid, out var battery))
{
stats.BatteryLevel = GetBatteryLevel(uid);
stats.PowerValue = battery.CurrentSupply;
// Load due to network battery recharging
stats.PowerUsage += Math.Max(battery.CurrentReceiving - battery.CurrentSupply, 0d);
// Track battery usage
stats.BatteryUsage += Math.Max(battery.CurrentSupply - battery.CurrentReceiving, 0d);
// Records loads attached to APCs
if (device.Group == PowerMonitoringConsoleGroup.APC && battery.Enabled)
{
stats.PowerUsage += battery.NetworkBattery.LoadingNetworkDemand;
}
}
}
// Master devices add the power values from all entities they represent (if applicable)
if (device.IsCollectionMasterOrChild && device.IsCollectionMaster)
{
foreach ((var child, var childDevice) in device.ChildDevices)
{
if (child == uid)
continue;
// Safeguard to prevent infinite loops
if (childDevice.IsCollectionMaster && childDevice.ChildDevices.ContainsKey(uid))
continue;
var childResult = GetPowerStats(child, childDevice);
stats.PowerValue += childResult.PowerValue;
stats.PowerSupplied += childResult.PowerSupplied;
stats.PowerUsage += childResult.PowerUsage;
stats.BatteryUsage += childResult.BatteryUsage;
}
}
return stats;
}
private float? GetBatteryLevel(EntityUid uid)
{
if (!TryComp<BatteryComponent>(uid, out var battery))
return null;
var effectiveMax = battery.MaxCharge;
if (effectiveMax == 0)
effectiveMax = 1;
return battery.CurrentCharge / effectiveMax;
}
private void GetSourcesForNode(EntityUid uid, Node node, out List<PowerMonitoringConsoleEntry> sources)
{
sources = new List<PowerMonitoringConsoleEntry>();
if (node.NodeGroup is not PowerNet netQ)
return;
var indexedSources = new Dictionary<EntityUid, PowerMonitoringConsoleEntry>();
var currentSupply = 0f;
var currentDemand = 0f;
foreach (var powerSupplier in netQ.Suppliers)
{
var ent = powerSupplier.Owner;
if (uid == ent)
continue;
currentSupply += powerSupplier.CurrentSupply;
if (TryComp<PowerMonitoringDeviceComponent>(ent, out var entDevice))
{
// Combine entities represented by an master into a single entry
if (entDevice.IsCollectionMasterOrChild && !entDevice.IsCollectionMaster)
ent = entDevice.CollectionMaster;
if (indexedSources.TryGetValue(ent, out var entry))
{
entry.PowerValue += powerSupplier.CurrentSupply;
indexedSources[ent] = entry;
continue;
}
indexedSources.Add(ent, new PowerMonitoringConsoleEntry(EntityManager.GetNetEntity(ent), entDevice.Group, powerSupplier.CurrentSupply, GetBatteryLevel(ent)));
}
}
foreach (var batteryDischarger in netQ.Dischargers)
{
var ent = batteryDischarger.Owner;
if (uid == ent)
continue;
if (!TryComp<PowerNetworkBatteryComponent>(ent, out var entBattery))
continue;
currentSupply += entBattery.CurrentSupply;
if (TryComp<PowerMonitoringDeviceComponent>(ent, out var entDevice))
{
// Combine entities represented by an master into a single entry
if (entDevice.IsCollectionMasterOrChild && !entDevice.IsCollectionMaster)
ent = entDevice.CollectionMaster;
if (indexedSources.TryGetValue(ent, out var entry))
{
entry.PowerValue += entBattery.CurrentSupply;
indexedSources[ent] = entry;
continue;
}
indexedSources.Add(ent, new PowerMonitoringConsoleEntry(EntityManager.GetNetEntity(ent), entDevice.Group, entBattery.CurrentSupply, GetBatteryLevel(ent)));
}
}
sources = indexedSources.Values.ToList();
// Get the total demand for the network
foreach (var powerConsumer in netQ.Consumers)
{
currentDemand += powerConsumer.ReceivedPower;
}
foreach (var batteryCharger in netQ.Chargers)
{
var ent = batteryCharger.Owner;
if (!TryComp<PowerNetworkBatteryComponent>(ent, out var entBattery))
continue;
currentDemand += entBattery.CurrentReceiving;
}
// Exit if supply / demand is negligible
if (MathHelper.CloseTo(currentDemand, 0) || MathHelper.CloseTo(currentSupply, 0))
return;
// Work out how much power this device (and those it represents) is actually receiving
if (!TryComp<PowerNetworkBatteryComponent>(uid, out var battery))
return;
var powerUsage = battery.CurrentReceiving;
if (TryComp<PowerMonitoringDeviceComponent>(uid, out var device) && device.IsCollectionMaster)
{
foreach ((var child, var _) in device.ChildDevices)
{
if (TryComp<PowerNetworkBatteryComponent>(child, out var childBattery))
powerUsage += childBattery.CurrentReceiving;
}
}
// Update the power value for each source based on the fraction of power the entity is actually draining from each
var powerFraction = Math.Min(powerUsage / currentSupply, 1f) * Math.Min(currentSupply / currentDemand, 1f);
for (int i = 0; i < sources.Count; i++)
{
var entry = sources[i];
sources[i] = new PowerMonitoringConsoleEntry(entry.NetEntity, entry.Group, entry.PowerValue * powerFraction, entry.BatteryLevel);
}
}
private void GetLoadsForNode(EntityUid uid, Node node, out List<PowerMonitoringConsoleEntry> loads, List<EntityUid>? children = null)
{
loads = new List<PowerMonitoringConsoleEntry>();
if (node.NodeGroup is not PowerNet netQ)
return;
var indexedLoads = new Dictionary<EntityUid, PowerMonitoringConsoleEntry>();
var currentDemand = 0f;
foreach (var powerConsumer in netQ.Consumers)
{
var ent = powerConsumer.Owner;
if (uid == ent)
continue;
currentDemand += powerConsumer.ReceivedPower;
if (TryComp<PowerMonitoringDeviceComponent>(ent, out var entDevice))
{
// Combine entities represented by an master into a single entry
if (entDevice.IsCollectionMasterOrChild && !entDevice.IsCollectionMaster)
ent = entDevice.CollectionMaster;
if (indexedLoads.TryGetValue(ent, out var entry))
{
entry.PowerValue += powerConsumer.ReceivedPower;
indexedLoads[ent] = entry;
continue;
}
indexedLoads.Add(ent, new PowerMonitoringConsoleEntry(EntityManager.GetNetEntity(ent), entDevice.Group, powerConsumer.ReceivedPower, GetBatteryLevel(ent)));
}
}
foreach (var batteryCharger in netQ.Chargers)
{
var ent = batteryCharger.Owner;
if (uid == ent)
continue;
if (!TryComp<PowerNetworkBatteryComponent>(ent, out var battery))
continue;
currentDemand += battery.CurrentReceiving;
if (TryComp<PowerMonitoringDeviceComponent>(ent, out var entDevice))
{
// Combine entities represented by an master into a single entry
if (entDevice.IsCollectionMasterOrChild && !entDevice.IsCollectionMaster)
ent = entDevice.CollectionMaster;
if (indexedLoads.TryGetValue(ent, out var entry))
{
entry.PowerValue += battery.CurrentReceiving;
indexedLoads[ent] = entry;
continue;
}
indexedLoads.Add(ent, new PowerMonitoringConsoleEntry(EntityManager.GetNetEntity(ent), entDevice.Group, battery.CurrentReceiving, GetBatteryLevel(ent)));
}
}
loads = indexedLoads.Values.ToList();
// Exit if demand is negligible
if (MathHelper.CloseTo(currentDemand, 0))
return;
var supplying = 0f;
// Work out how much power this device (and those it represents) is actually supplying
if (TryComp<PowerNetworkBatteryComponent>(uid, out var entBattery))
supplying = entBattery.CurrentSupply;
else if (TryComp<PowerSupplierComponent>(uid, out var entSupplier))
supplying = entSupplier.CurrentSupply;
if (TryComp<PowerMonitoringDeviceComponent>(uid, out var device) && device.IsCollectionMaster)
{
foreach ((var child, var _) in device.ChildDevices)
{
if (TryComp<PowerNetworkBatteryComponent>(child, out var childBattery))
supplying += childBattery.CurrentSupply;
else if (TryComp<PowerSupplierComponent>(child, out var childSupplier))
supplying += childSupplier.CurrentSupply;
}
}
// Update the power value for each load based on the fraction of power these entities are actually draining from this device
var powerFraction = Math.Min(supplying / currentDemand, 1f);
for (int i = 0; i < indexedLoads.Values.Count; i++)
{
var entry = loads[i];
loads[i] = new PowerMonitoringConsoleEntry(entry.NetEntity, entry.Group, entry.PowerValue * powerFraction, entry.BatteryLevel);
}
}
// Designates a supplied entity as a 'collection master'. Other entities which share this
// entities collection name and are attached on the same load network are assigned this entity
// as the master that represents them on the console UI. This way you can have one device
// represent multiple connected devices
private void AssignEntityAsCollectionMaster
(EntityUid uid,
PowerMonitoringDeviceComponent? device = null,
TransformComponent? xform = null,
NodeContainerComponent? nodeContainer = null)
{
if (!Resolve(uid, ref device, ref nodeContainer, ref xform, false))
return;
// If the device is not attached to a network, exit
var nodeName = device.SourceNode == string.Empty ? device.LoadNode : device.SourceNode;
if (!nodeContainer.Nodes.TryGetValue(nodeName, out var node) ||
node.ReachableNodes.Count == 0)
{
// Make a child the new master of the collection if necessary
if (device.ChildDevices.TryFirstOrNull(out var kvp))
{
var newMaster = kvp.Value.Key;
var newMasterDevice = kvp.Value.Value;
newMasterDevice.CollectionMaster = newMaster;
newMasterDevice.ChildDevices.Clear();
foreach ((var child, var childDevice) in device.ChildDevices)
{
newMasterDevice.ChildDevices.Add(child, childDevice);
childDevice.CollectionMaster = newMaster;
UpdateCollectionChildMetaData(child, newMaster);
}
UpdateCollectionMasterMetaData(newMaster, newMasterDevice.ChildDevices.Count);
}
device.CollectionMaster = uid;
device.ChildDevices.Clear();
UpdateCollectionMasterMetaData(uid, 0);
return;
}
// Check to see if the device has a valid existing master
if (!device.IsCollectionMaster &&
device.CollectionMaster.IsValid() &&
TryComp<NodeContainerComponent>(device.CollectionMaster, out var masterNodeContainer) &&
DevicesHaveMatchingNodes(nodeContainer, masterNodeContainer))
return;
// If not, make this a new master
device.CollectionMaster = uid;
device.ChildDevices.Clear();
// Search for children
var query = AllEntityQuery<PowerMonitoringDeviceComponent, TransformComponent, NodeContainerComponent>();
while (query.MoveNext(out var ent, out var entDevice, out var entXform, out var entNodeContainer))
{
if (entDevice.CollectionName != device.CollectionName)
continue;
if (ent == uid)
continue;
if (entXform.GridUid != xform.GridUid)
continue;
if (!DevicesHaveMatchingNodes(nodeContainer, entNodeContainer))
continue;
device.ChildDevices.Add(ent, entDevice);
entDevice.CollectionMaster = uid;
UpdateCollectionChildMetaData(ent, uid);
}
UpdateCollectionMasterMetaData(uid, device.ChildDevices.Count);
}
private bool DevicesHaveMatchingNodes(NodeContainerComponent nodeContainerA, NodeContainerComponent nodeContainerB)
{
foreach ((var key, var nodeA) in nodeContainerA.Nodes)
{
if (!nodeContainerB.Nodes.TryGetValue(key, out var nodeB))
return false;
if (nodeA.NodeGroup != nodeB.NodeGroup)
return false;
}
return true;
}
private void UpdateCollectionChildMetaData(EntityUid child, EntityUid master)
{
var netEntity = EntityManager.GetNetEntity(child);
var xform = Transform(child);
var query = AllEntityQuery<PowerMonitoringConsoleComponent, TransformComponent>();
while (query.MoveNext(out var ent, out var entConsole, out var entXform))
{
if (entXform.GridUid != xform.GridUid)
continue;
if (!entConsole.PowerMonitoringDeviceMetaData.TryGetValue(netEntity, out var metaData))
continue;
metaData.CollectionMaster = EntityManager.GetNetEntity(master);
entConsole.PowerMonitoringDeviceMetaData[netEntity] = metaData;
Dirty(ent, entConsole);
}
}
private void UpdateCollectionMasterMetaData(EntityUid master, int childCount)
{
var netEntity = EntityManager.GetNetEntity(master);
var xform = Transform(master);
var query = AllEntityQuery<PowerMonitoringConsoleComponent, TransformComponent>();
while (query.MoveNext(out var ent, out var entConsole, out var entXform))
{
if (entXform.GridUid != xform.GridUid)
continue;
if (!entConsole.PowerMonitoringDeviceMetaData.TryGetValue(netEntity, out var metaData))
continue;
if (childCount > 0)
{
var name = MetaData(master).EntityPrototype?.Name ?? MetaData(master).EntityName;
metaData.EntityName = Loc.GetString("power-monitoring-window-object-array", ("name", name), ("count", childCount + 1));
}
else
{
metaData.EntityName = MetaData(master).EntityName;
}
metaData.CollectionMaster = null;
entConsole.PowerMonitoringDeviceMetaData[netEntity] = metaData;
Dirty(ent, entConsole);
}
}
private Dictionary<Vector2i, PowerCableChunk> RefreshPowerCableGrid(EntityUid gridUid, MapGridComponent grid)
{
// Clears all chunks for the associated grid
var allChunks = new Dictionary<Vector2i, PowerCableChunk>();
_gridPowerCableChunks[gridUid] = allChunks;
// Adds all power cables to the grid
var query = AllEntityQuery<CableComponent, TransformComponent>();
while (query.MoveNext(out var ent, out var cable, out var entXform))
{
if (entXform.GridUid != gridUid)
continue;
var tile = _sharedMapSystem.GetTileRef(gridUid, grid, entXform.Coordinates);
var chunkOrigin = SharedMapSystem.GetChunkIndices(tile.GridIndices, ChunkSize);
if (!allChunks.TryGetValue(chunkOrigin, out var chunk))
{
chunk = new PowerCableChunk(chunkOrigin);
allChunks[chunkOrigin] = chunk;
}
var relative = SharedMapSystem.GetChunkRelative(tile.GridIndices, ChunkSize);
var flag = GetFlag(relative);
chunk.PowerCableData[(int) cable.CableType] |= flag;
}
return allChunks;
}
private void UpdateFocusNetwork(EntityUid uid, PowerMonitoringCableNetworksComponent component, EntityUid gridUid, MapGridComponent grid, List<EntityUid> nodeList)
{
component.FocusChunks.Clear();
foreach (var ent in nodeList)
{
var xform = Transform(ent);
var tile = _sharedMapSystem.GetTileRef(gridUid, grid, xform.Coordinates);
var gridIndices = tile.GridIndices;
var chunkOrigin = SharedMapSystem.GetChunkIndices(gridIndices, ChunkSize);
if (!component.FocusChunks.TryGetValue(chunkOrigin, out var chunk))
{
chunk = new PowerCableChunk(chunkOrigin);
component.FocusChunks[chunkOrigin] = chunk;
}
var relative = SharedMapSystem.GetChunkRelative(gridIndices, ChunkSize);
var flag = GetFlag(relative);
if (TryComp<CableComponent>(ent, out var cable))
chunk.PowerCableData[(int) cable.CableType] |= flag;
}
Dirty(uid, component);
}
private void RefreshPowerMonitoringConsole(EntityUid uid, PowerMonitoringConsoleComponent component)
{
component.Focus = null;
component.FocusGroup = PowerMonitoringConsoleGroup.Generator;
component.PowerMonitoringDeviceMetaData.Clear();
component.Flags = 0;
var xform = Transform(uid);
if (xform.GridUid == null)
return;
var grid = xform.GridUid.Value;
var query = AllEntityQuery<PowerMonitoringDeviceComponent, TransformComponent>();
while (query.MoveNext(out var ent, out var entDevice, out var entXform))
{
if (grid != entXform.GridUid)
continue;
var netEntity = EntityManager.GetNetEntity(ent);
var name = MetaData(ent).EntityName;
var netCoords = EntityManager.GetNetCoordinates(entXform.Coordinates);
var metaData = new PowerMonitoringDeviceMetaData(name, netCoords, entDevice.Group, entDevice.SpritePath, entDevice.SpriteState);
if (entDevice.IsCollectionMasterOrChild)
{
if (!entDevice.IsCollectionMaster)
{
metaData.CollectionMaster = EntityManager.GetNetEntity(entDevice.CollectionMaster);
}
else if (entDevice.ChildDevices.Count > 0)
{
name = MetaData(ent).EntityPrototype?.Name ?? MetaData(ent).EntityName;
metaData.EntityName = Loc.GetString("power-monitoring-window-object-array", ("name", name), ("count", entDevice.ChildDevices.Count + 1));
}
}
component.PowerMonitoringDeviceMetaData.Add(netEntity, metaData);
}
Dirty(uid, component);
}
private void RefreshPowerMonitoringCableNetworks(EntityUid uid, PowerMonitoringCableNetworksComponent component)
{
var xform = Transform(uid);
if (xform.GridUid == null)
return;
var grid = xform.GridUid.Value;
if (!TryComp<MapGridComponent>(grid, out var map))
return;
if (!_gridPowerCableChunks.TryGetValue(grid, out var allChunks))
allChunks = RefreshPowerCableGrid(grid, map);
component.AllChunks = allChunks;
component.FocusChunks.Clear();
Dirty(uid, component);
}
private struct PowerStats
{
public double PowerValue { get; set; }
public double PowerSupplied { get; set; }
public double PowerUsage { get; set; }
public double BatteryUsage { get; set; }
public float? BatteryLevel { get; set; }
}
}