xmlns:ui="clr-namespace:Content.Client.Medical.CrewMonitoring"
xmlns:controls="clr-namespace:Content.Client.UserInterface.Controls"
Title="{Loc 'crew-monitoring-user-interface-title'}"
- SetSize="1200 700"
- MinSize="1200 700">
+ SetSize="1210 700"
+ MinSize="1210 700">
<BoxContainer Orientation="Vertical">
<BoxContainer Orientation="Horizontal" VerticalExpand="True" HorizontalExpand="True">
<ui:CrewMonitoringNavMapControl Name="NavMap" HorizontalExpand="True" VerticalExpand="True" Margin="5 20"/>
- <BoxContainer Orientation="Vertical">
+ <BoxContainer Orientation="Vertical" Margin="0 0 10 0">
<controls:StripeBack>
<PanelContainer>
- <Label Name="StationName" Text="Unknown station" Align="Center" />
+ <Label Name="StationName" Text="Unknown station" Align="Center" Margin="0 5 0 3"/>
</PanelContainer>
</controls:StripeBack>
[Dependency] private readonly IEntityManager _entManager = default!;
private readonly SharedTransformSystem _transformSystem = default!;
+ public EntityUid? Owner;
public EntityUid? MapUid;
// Actions
public event Action<NetEntity?>? TrackedEntitySelectedAction;
+ public event Action<DrawingHandleScreen>? PostWallDrawingAction;
// Tracked data
public Dictionary<EntityCoordinates, (bool Visible, Color Color)> TrackedCoordinates = new();
}
}
+ if (PostWallDrawingAction != null)
+ PostWallDrawingAction.Invoke(handle);
+
// Beacons
if (_beacons.Pressed)
{
}
}
- private void UpdateNavMap()
+ protected virtual void UpdateNavMap()
{
if (_navMap == null || _grid == null)
return;
--- /dev/null
+using Content.Shared.Power;
+
+namespace Content.Client.Power;
+
+public sealed class PowerMonitoringConsoleBoundUserInterface : BoundUserInterface
+{
+ [ViewVariables]
+ private PowerMonitoringWindow? _menu;
+
+ public PowerMonitoringConsoleBoundUserInterface(EntityUid owner, Enum uiKey) : base(owner, uiKey) { }
+
+ protected override void Open()
+ {
+ _menu = new PowerMonitoringWindow(this, Owner);
+ _menu.OpenCentered();
+ _menu.OnClose += Close;
+ }
+
+ protected override void UpdateState(BoundUserInterfaceState state)
+ {
+ base.UpdateState(state);
+
+ var castState = (PowerMonitoringConsoleBoundInterfaceState) state;
+
+ if (castState == null)
+ return;
+
+ EntMan.TryGetComponent<TransformComponent>(Owner, out var xform);
+ _menu?.ShowEntites
+ (castState.TotalSources,
+ castState.TotalBatteryUsage,
+ castState.TotalLoads,
+ castState.AllEntries,
+ castState.FocusSources,
+ castState.FocusLoads,
+ xform?.Coordinates);
+ }
+
+ public void SendPowerMonitoringConsoleMessage(NetEntity? netEntity, PowerMonitoringConsoleGroup group)
+ {
+ SendMessage(new PowerMonitoringConsoleMessage(netEntity, group));
+ }
+
+ protected override void Dispose(bool disposing)
+ {
+ base.Dispose(disposing);
+ if (!disposing)
+ return;
+
+ _menu?.Dispose();
+ }
+}
--- /dev/null
+using Content.Client.Pinpointer.UI;
+using Content.Shared.Pinpointer;
+using Content.Shared.Power;
+using Robust.Client.Graphics;
+using Robust.Shared.Collections;
+using Robust.Shared.Map.Components;
+using System.Numerics;
+
+namespace Content.Client.Power;
+
+public sealed partial class PowerMonitoringConsoleNavMapControl : NavMapControl
+{
+ [Dependency] private readonly IEntityManager _entManager = default!;
+
+ // Cable indexing
+ // 0: CableType.HighVoltage
+ // 1: CableType.MediumVoltage
+ // 2: CableType.Apc
+
+ private readonly Color[] _powerCableColors = { Color.OrangeRed, Color.Yellow, Color.LimeGreen };
+ private readonly Vector2[] _powerCableOffsets = { new Vector2(-0.2f, -0.2f), Vector2.Zero, new Vector2(0.2f, 0.2f) };
+ private Dictionary<Color, Color> _sRGBLookUp = new Dictionary<Color, Color>();
+
+ public PowerMonitoringCableNetworksComponent? PowerMonitoringCableNetworks;
+ public List<PowerMonitoringConsoleLineGroup> HiddenLineGroups = new();
+ public Dictionary<Vector2i, List<PowerMonitoringConsoleLine>>? PowerCableNetwork;
+ public Dictionary<Vector2i, List<PowerMonitoringConsoleLine>>? FocusCableNetwork;
+
+ private MapGridComponent? _grid;
+
+ public PowerMonitoringConsoleNavMapControl() : base()
+ {
+ // Set colors
+ TileColor = new Color(30, 57, 67);
+ WallColor = new Color(102, 164, 217);
+
+ PostWallDrawingAction += DrawAllCableNetworks;
+ }
+
+ protected override void UpdateNavMap()
+ {
+ base.UpdateNavMap();
+
+ if (Owner == null)
+ return;
+
+ if (!_entManager.TryGetComponent<PowerMonitoringCableNetworksComponent>(Owner, out var cableNetworks))
+ return;
+
+ if (!_entManager.TryGetComponent(MapUid, out _grid))
+ return;
+
+ PowerCableNetwork = GetDecodedPowerCableChunks(cableNetworks.AllChunks, _grid);
+ FocusCableNetwork = GetDecodedPowerCableChunks(cableNetworks.FocusChunks, _grid);
+ }
+
+ public void DrawAllCableNetworks(DrawingHandleScreen handle)
+ {
+ // Draw full cable network
+ if (PowerCableNetwork != null && PowerCableNetwork.Count > 0)
+ {
+ var modulator = (FocusCableNetwork != null && FocusCableNetwork.Count > 0) ? Color.DimGray : Color.White;
+ DrawCableNetwork(handle, PowerCableNetwork, modulator);
+ }
+
+ // Draw focus network
+ if (FocusCableNetwork != null && FocusCableNetwork.Count > 0)
+ DrawCableNetwork(handle, FocusCableNetwork, Color.White);
+ }
+
+ public void DrawCableNetwork(DrawingHandleScreen handle, Dictionary<Vector2i, List<PowerMonitoringConsoleLine>> fullCableNetwork, Color modulator)
+ {
+ var offset = GetOffset();
+ var area = new Box2(-WorldRange, -WorldRange, WorldRange + 1f, WorldRange + 1f).Translated(offset);
+
+ if (WorldRange / WorldMaxRange > 0.5f)
+ {
+ var cableNetworks = new ValueList<Vector2>[3];
+
+ foreach ((var chunk, var chunkedLines) in fullCableNetwork)
+ {
+ var offsetChunk = new Vector2(chunk.X, chunk.Y) * SharedNavMapSystem.ChunkSize;
+
+ if (offsetChunk.X < area.Left - SharedNavMapSystem.ChunkSize || offsetChunk.X > area.Right)
+ continue;
+
+ if (offsetChunk.Y < area.Bottom - SharedNavMapSystem.ChunkSize || offsetChunk.Y > area.Top)
+ continue;
+
+ foreach (var chunkedLine in chunkedLines)
+ {
+ if (HiddenLineGroups.Contains(chunkedLine.Group))
+ continue;
+
+ var start = Scale(chunkedLine.Origin - new Vector2(offset.X, -offset.Y));
+ var end = Scale(chunkedLine.Terminus - new Vector2(offset.X, -offset.Y));
+
+ cableNetworks[(int) chunkedLine.Group].Add(start);
+ cableNetworks[(int) chunkedLine.Group].Add(end);
+ }
+ }
+
+ for (int cableNetworkIdx = 0; cableNetworkIdx < cableNetworks.Length; cableNetworkIdx++)
+ {
+ var cableNetwork = cableNetworks[cableNetworkIdx];
+
+ if (cableNetwork.Count > 0)
+ {
+ var color = _powerCableColors[cableNetworkIdx] * modulator;
+
+ if (!_sRGBLookUp.TryGetValue(color, out var sRGB))
+ {
+ sRGB = Color.ToSrgb(color);
+ _sRGBLookUp[color] = sRGB;
+ }
+
+ handle.DrawPrimitives(DrawPrimitiveTopology.LineList, cableNetwork.Span, sRGB);
+ }
+ }
+ }
+
+ else
+ {
+ var cableVertexUVs = new ValueList<Vector2>[3];
+
+ foreach ((var chunk, var chunkedLines) in fullCableNetwork)
+ {
+ var offsetChunk = new Vector2(chunk.X, chunk.Y) * SharedNavMapSystem.ChunkSize;
+
+ if (offsetChunk.X < area.Left - SharedNavMapSystem.ChunkSize || offsetChunk.X > area.Right)
+ continue;
+
+ if (offsetChunk.Y < area.Bottom - SharedNavMapSystem.ChunkSize || offsetChunk.Y > area.Top)
+ continue;
+
+ foreach (var chunkedLine in chunkedLines)
+ {
+ if (HiddenLineGroups.Contains(chunkedLine.Group))
+ continue;
+
+ var leftTop = Scale(new Vector2
+ (Math.Min(chunkedLine.Origin.X, chunkedLine.Terminus.X) - 0.1f,
+ Math.Min(chunkedLine.Origin.Y, chunkedLine.Terminus.Y) - 0.1f)
+ - new Vector2(offset.X, -offset.Y));
+
+ var rightTop = Scale(new Vector2
+ (Math.Max(chunkedLine.Origin.X, chunkedLine.Terminus.X) + 0.1f,
+ Math.Min(chunkedLine.Origin.Y, chunkedLine.Terminus.Y) - 0.1f)
+ - new Vector2(offset.X, -offset.Y));
+
+ var leftBottom = Scale(new Vector2
+ (Math.Min(chunkedLine.Origin.X, chunkedLine.Terminus.X) - 0.1f,
+ Math.Max(chunkedLine.Origin.Y, chunkedLine.Terminus.Y) + 0.1f)
+ - new Vector2(offset.X, -offset.Y));
+
+ var rightBottom = Scale(new Vector2
+ (Math.Max(chunkedLine.Origin.X, chunkedLine.Terminus.X) + 0.1f,
+ Math.Max(chunkedLine.Origin.Y, chunkedLine.Terminus.Y) + 0.1f)
+ - new Vector2(offset.X, -offset.Y));
+
+ cableVertexUVs[(int) chunkedLine.Group].Add(leftBottom);
+ cableVertexUVs[(int) chunkedLine.Group].Add(leftTop);
+ cableVertexUVs[(int) chunkedLine.Group].Add(rightBottom);
+ cableVertexUVs[(int) chunkedLine.Group].Add(leftTop);
+ cableVertexUVs[(int) chunkedLine.Group].Add(rightBottom);
+ cableVertexUVs[(int) chunkedLine.Group].Add(rightTop);
+ }
+ }
+
+ for (int cableNetworkIdx = 0; cableNetworkIdx < cableVertexUVs.Length; cableNetworkIdx++)
+ {
+ var cableVertexUV = cableVertexUVs[cableNetworkIdx];
+
+ if (cableVertexUV.Count > 0)
+ {
+ var color = _powerCableColors[cableNetworkIdx] * modulator;
+
+ if (!_sRGBLookUp.TryGetValue(color, out var sRGB))
+ {
+ sRGB = Color.ToSrgb(color);
+ _sRGBLookUp[color] = sRGB;
+ }
+
+ handle.DrawPrimitives(DrawPrimitiveTopology.TriangleList, cableVertexUV.Span, sRGB);
+ }
+ }
+ }
+ }
+
+ public Dictionary<Vector2i, List<PowerMonitoringConsoleLine>>? GetDecodedPowerCableChunks(Dictionary<Vector2i, PowerCableChunk>? chunks, MapGridComponent? grid)
+ {
+ if (chunks == null || grid == null)
+ return null;
+
+ var decodedOutput = new Dictionary<Vector2i, List<PowerMonitoringConsoleLine>>();
+
+ foreach ((var chunkOrigin, var chunk) in chunks)
+ {
+ var list = new List<PowerMonitoringConsoleLine>();
+
+ for (int cableIdx = 0; cableIdx < chunk.PowerCableData.Length; cableIdx++)
+ {
+ var chunkMask = chunk.PowerCableData[cableIdx];
+
+ Vector2 offset = _powerCableOffsets[cableIdx];
+
+ for (var chunkIdx = 0; chunkIdx < SharedNavMapSystem.ChunkSize * SharedNavMapSystem.ChunkSize; chunkIdx++)
+ {
+ var value = (int) Math.Pow(2, chunkIdx);
+ var mask = chunkMask & value;
+
+ if (mask == 0x0)
+ continue;
+
+ var relativeTile = SharedNavMapSystem.GetTile(mask);
+ var tile = (chunk.Origin * SharedNavMapSystem.ChunkSize + relativeTile) * grid.TileSize;
+ var position = new Vector2(tile.X, -tile.Y);
+
+ PowerCableChunk neighborChunk;
+ bool neighbor;
+
+ // Note: we only check the north and east neighbors
+
+ // East
+ if (relativeTile.X == SharedNavMapSystem.ChunkSize - 1)
+ {
+ neighbor = chunks.TryGetValue(chunkOrigin + new Vector2i(1, 0), out neighborChunk) &&
+ (neighborChunk.PowerCableData[cableIdx] & SharedNavMapSystem.GetFlag(new Vector2i(0, relativeTile.Y))) != 0x0;
+ }
+ else
+ {
+ var flag = SharedNavMapSystem.GetFlag(relativeTile + new Vector2i(1, 0));
+ neighbor = (chunkMask & flag) != 0x0;
+ }
+
+ if (neighbor)
+ {
+ // Add points
+ var line = new PowerMonitoringConsoleLine
+ (position + offset + new Vector2(grid.TileSize * 0.5f, -grid.TileSize * 0.5f),
+ position + new Vector2(1f, 0f) + offset + new Vector2(grid.TileSize * 0.5f, -grid.TileSize * 0.5f),
+ (PowerMonitoringConsoleLineGroup) cableIdx);
+
+ list.Add(line);
+ }
+
+ // North
+ if (relativeTile.Y == SharedNavMapSystem.ChunkSize - 1)
+ {
+ neighbor = chunks.TryGetValue(chunkOrigin + new Vector2i(0, 1), out neighborChunk) &&
+ (neighborChunk.PowerCableData[cableIdx] & SharedNavMapSystem.GetFlag(new Vector2i(relativeTile.X, 0))) != 0x0;
+ }
+ else
+ {
+ var flag = SharedNavMapSystem.GetFlag(relativeTile + new Vector2i(0, 1));
+ neighbor = (chunkMask & flag) != 0x0;
+ }
+
+ if (neighbor)
+ {
+ // Add points
+ var line = new PowerMonitoringConsoleLine
+ (position + offset + new Vector2(grid.TileSize * 0.5f, -grid.TileSize * 0.5f),
+ position + new Vector2(0f, -1f) + offset + new Vector2(grid.TileSize * 0.5f, -grid.TileSize * 0.5f),
+ (PowerMonitoringConsoleLineGroup) cableIdx);
+
+ list.Add(line);
+ }
+ }
+
+ }
+
+ if (list.Count > 0)
+ decodedOutput.Add(chunkOrigin, list);
+ }
+
+ return decodedOutput;
+ }
+}
+
+public struct PowerMonitoringConsoleLine
+{
+ public readonly Vector2 Origin;
+ public readonly Vector2 Terminus;
+ public readonly PowerMonitoringConsoleLineGroup Group;
+
+ public PowerMonitoringConsoleLine(Vector2 origin, Vector2 terminus, PowerMonitoringConsoleLineGroup group)
+ {
+ Origin = origin;
+ Terminus = terminus;
+ Group = group;
+ }
+}
+
+public enum PowerMonitoringConsoleLineGroup : byte
+{
+ HighVoltage,
+ MediumVoltage,
+ Apc,
+}
-<DefaultWindow
- xmlns="https://spacestation14.io"
- Title="{Loc 'power-monitoring-window-title'}">
- <BoxContainer Orientation="Vertical" VerticalExpand="True">
- <GridContainer Columns="2">
- <!-- Grid is used here to align things. -->
- <Label Text="{Loc 'power-monitoring-window-total-sources'}"/><Label Name="TotalSourcesNum" Text="?"/>
- <Label Text="{Loc 'power-monitoring-window-total-loads'}"/><Label Name="TotalLoadsNum" Text="?"/>
- </GridContainer>
- <TabContainer Name="MasterTabContainer" VerticalExpand="True">
- <ItemList Name="SourcesList" VerticalExpand="True">
- </ItemList>
- <BoxContainer Orientation="Vertical" VerticalExpand="True">
- <CheckBox Margin="8 8" Name="ShowInactiveConsumersCheckBox" Text="{Loc 'power-monitoring-window-show-inactive-consumers'}" />
- <ItemList Name="LoadsList" VerticalExpand="True">
- </ItemList>
+<controls:FancyWindow xmlns="https://spacestation14.io"
+ xmlns:ui="clr-namespace:Content.Client.Power"
+ xmlns:controls="clr-namespace:Content.Client.UserInterface.Controls"
+ xmlns:gfx="clr-namespace:Robust.Client.Graphics;assembly=Robust.Client"
+ Title="{Loc 'power-monitoring-window-title'}"
+ SetSize="1120 750"
+ MinSize="1120 750">
+ <BoxContainer Orientation="Vertical">
+ <!-- Main display -->
+ <BoxContainer Orientation="Horizontal" VerticalExpand="True" HorizontalExpand="True">
+ <!-- Nav map -->
+ <BoxContainer Orientation="Vertical" VerticalExpand="True" HorizontalExpand="True">
+ <ui:PowerMonitoringConsoleNavMapControl Name="NavMap" Margin="5 5" VerticalExpand="True" HorizontalExpand="True">
+
+ <!-- System warning -->
+ <PanelContainer Name="SystemWarningPanel"
+ HorizontalAlignment="Center"
+ VerticalAlignment="Top"
+ HorizontalExpand="True"
+ Margin="0 48 0 0"
+ Visible="False">
+ <RichTextLabel Name="SystemWarningLabel" Margin="12 8 12 8"/>
+ </PanelContainer>
+
+
+ </ui:PowerMonitoringConsoleNavMapControl>
+
+ <!-- Nav map legend -->
+ <BoxContainer Orientation="Horizontal" Margin="0 10 0 10">
+ <TextureRect Stretch="KeepAspectCentered"
+ TexturePath="/Textures/Interface/NavMap/beveled_circle.png"
+ Modulate="#800080"
+ SetSize="16 16"
+ Margin="20 0 5 0"/>
+ <Label Text="{Loc 'power-monitoring-window-label-sources'}"/>
+ <TextureRect Stretch="KeepAspectCentered"
+ TexturePath="/Textures/Interface/NavMap/beveled_hexagon.png"
+ SetSize="16 16"
+ Modulate="#ff4500"
+ Margin="20 0 5 0"/>
+ <Label Text="{Loc 'power-monitoring-window-label-smes'}"/>
+ <TextureRect Stretch="KeepAspectCentered"
+ TexturePath="/Textures/Interface/NavMap/beveled_square.png"
+ SetSize="16 16"
+ Modulate="#ffff00"
+ Margin="20 0 5 0"/>
+ <Label Text="{Loc 'power-monitoring-window-label-substation'}"/>
+ <TextureRect Stretch="KeepAspectCentered"
+ TexturePath="/Textures/Interface/NavMap/beveled_triangle.png"
+ SetSize="16 16"
+ Modulate="#32cd32"
+ Margin="20 0 5 0"/>
+ <Label Text="{Loc 'power-monitoring-window-label-apc'}"/>
+ </BoxContainer>
</BoxContainer>
- </TabContainer>
+
+ <!-- Power status -->
+ <BoxContainer Orientation="Vertical" VerticalExpand="True" SetWidth="440" Margin="0 0 10 10">
+
+ <!-- Station name -->
+ <controls:StripeBack>
+ <PanelContainer>
+ <RichTextLabel Name="StationName" HorizontalAlignment="Center" VerticalAlignment="Center" Margin="0 5 0 3"/>
+ </PanelContainer>
+ </controls:StripeBack>
+
+ <!-- Power overview -->
+ <GridContainer Columns="2">
+ <Label StyleClasses="StatusFieldTitle" Text="{Loc 'power-monitoring-window-total-sources'}"/>
+ <Label Name="TotalSources" Text="?" Margin="10 0 0 0"/>
+ <Label StyleClasses="StatusFieldTitle" Text="{Loc 'power-monitoring-window-total-battery-usage'}"/>
+ <Label Name="TotalBatteryUsage" Text="?" Margin="10 0 0 0"/>
+ <Label StyleClasses="StatusFieldTitle" Text="{Loc 'power-monitoring-window-total-loads'}"/>
+ <Label Name="TotalLoads" Text="?" Margin="10 0 0 0"/>
+ </GridContainer>
+
+ <!-- Loads / Sources (entries added by C# code) -->
+ <TabContainer Name="MasterTabContainer" VerticalExpand="True" HorizontalExpand="True" Margin="0 10 0 0">
+ <ScrollContainer HorizontalExpand="True" Margin="8, 8, 8, 8">
+ <BoxContainer Name="SourcesList" Orientation="Vertical" VerticalExpand="True" HorizontalExpand="True" Margin="0 0 10 0"/>
+ </ScrollContainer>
+ <ScrollContainer HorizontalExpand="True" Margin="8, 8, 8, 8">
+ <BoxContainer Name="SMESList" Orientation="Vertical" VerticalExpand="True" HorizontalExpand="True" Margin="0 0 10 0"/>
+ </ScrollContainer>
+ <ScrollContainer HorizontalExpand="True" Margin="8, 8, 8, 8">
+ <BoxContainer Name="SubstationList" Orientation="Vertical" VerticalExpand="True" HorizontalExpand="True" Margin="0 0 10 0"/>
+ </ScrollContainer>
+ <ScrollContainer HorizontalExpand="True" Margin="8, 8, 8, 8">
+ <BoxContainer Name="ApcList" Orientation="Vertical" VerticalExpand="True" HorizontalExpand="True" Margin="0 0 10 0"/>
+ </ScrollContainer>
+ </TabContainer>
+
+ <!-- Cable network toggles -->
+ <BoxContainer Orientation="Vertical" Margin="0 10 0 0">
+ <Label Text="{Loc 'power-monitoring-window-show-cable-networks'}" Margin="0 0 0 5"/>
+ <BoxContainer Orientation="Horizontal" HorizontalExpand="True">
+ <CheckBox Name="ShowHVCable" Text="{Loc 'power-monitoring-window-show-hv-cable'}" Pressed="True" Margin="0 0 0 0"/>
+ <CheckBox Name="ShowMVCable" Text="{Loc 'power-monitoring-window-show-mv-cable'}" Pressed="True" Margin="10 0 0 0"/>
+ <CheckBox Name="ShowLVCable" Text="{Loc 'power-monitoring-window-show-lv-cable'}" Pressed="True" Margin="10 0 0 0"/>
+ </BoxContainer>
+ </BoxContainer>
+ </BoxContainer>
+ </BoxContainer>
+
+ <!-- Footer -->
+ <BoxContainer Orientation="Vertical">
+ <PanelContainer StyleClasses="LowDivider" />
+ <BoxContainer Orientation="Horizontal" Margin="10 2 5 0" VerticalAlignment="Bottom">
+ <Label Text="{Loc 'power-monitoring-window-flavor-left'}" StyleClasses="WindowFooterText" />
+ <Label Text="{Loc 'power-monitoring-window-flavor-right'}" StyleClasses="WindowFooterText"
+ HorizontalAlignment="Right" HorizontalExpand="True" Margin="0 0 5 0" />
+ <TextureRect StyleClasses="NTLogoDark" Stretch="KeepAspectCentered"
+ VerticalAlignment="Center" HorizontalAlignment="Right" SetSize="19 19"/>
+ </BoxContainer>
+ </BoxContainer>
</BoxContainer>
-</DefaultWindow>
+</controls:FancyWindow>
--- /dev/null
+using Content.Client.Stylesheets;
+using Content.Shared.Power;
+using Robust.Client.Graphics;
+using Robust.Client.UserInterface.Controls;
+using Robust.Shared.Utility;
+using System.Diagnostics.CodeAnalysis;
+using System.Linq;
+using System.Numerics;
+
+namespace Content.Client.Power;
+
+public sealed partial class PowerMonitoringWindow
+{
+ private SpriteSpecifier.Texture _sourceIcon = new SpriteSpecifier.Texture(new ResPath("/Textures/Interface/PowerMonitoring/source_arrow.png"));
+ private SpriteSpecifier.Texture _loadIconPath = new SpriteSpecifier.Texture(new ResPath("/Textures/Interface/PowerMonitoring/load_arrow.png"));
+
+ private bool _autoScrollActive = false;
+ private bool _autoScrollAwaitsUpdate = false;
+
+ private void UpdateWindowConsoleEntry
+ (BoxContainer masterContainer,
+ int index,
+ PowerMonitoringConsoleEntry entry,
+ PowerMonitoringConsoleEntry[] focusSources,
+ PowerMonitoringConsoleEntry[] focusLoads)
+ {
+ UpdateWindowConsoleEntry(masterContainer, index, entry);
+
+ var windowEntry = masterContainer.GetChild(index) as PowerMonitoringWindowEntry;
+
+ // If we exit here, something was added to the container that shouldn't have been added
+ if (windowEntry == null)
+ return;
+
+ // Update sources and loads
+ UpdateEntrySourcesOrLoads(masterContainer, windowEntry.SourcesContainer, focusSources, _sourceIcon);
+ UpdateEntrySourcesOrLoads(masterContainer, windowEntry.LoadsContainer, focusLoads, _loadIconPath);
+
+ windowEntry.MainContainer.Visible = true;
+ }
+
+ private void UpdateWindowConsoleEntry(BoxContainer masterContainer, int index, PowerMonitoringConsoleEntry entry)
+ {
+ PowerMonitoringWindowEntry? windowEntry;
+
+ // Add missing children
+ if (index >= masterContainer.ChildCount)
+ {
+ // Add basic entry
+ windowEntry = new PowerMonitoringWindowEntry(entry);
+ masterContainer.AddChild(windowEntry);
+
+ // Selection action
+ windowEntry.Button.OnButtonUp += args =>
+ {
+ windowEntry.SourcesContainer.DisposeAllChildren();
+ windowEntry.LoadsContainer.DisposeAllChildren();
+ ButtonAction(windowEntry, masterContainer);
+ };
+ }
+
+ else
+ {
+ windowEntry = masterContainer.GetChild(index) as PowerMonitoringWindowEntry;
+ }
+
+ // If we exit here, something was added to the container that shouldn't have been added
+ if (windowEntry == null)
+ return;
+
+ windowEntry.NetEntity = entry.NetEntity;
+ windowEntry.Entry = entry;
+ windowEntry.MainContainer.Visible = false;
+
+ UpdateWindowEntryButton(entry.NetEntity, windowEntry.Button, entry);
+ }
+
+ public void UpdateWindowEntryButton(NetEntity netEntity, PowerMonitoringButton button, PowerMonitoringConsoleEntry entry)
+ {
+ if (!netEntity.IsValid())
+ return;
+
+ if (entry.MetaData == null)
+ return;
+
+ // Update button style
+ if (netEntity == _focusEntity)
+ button.AddStyleClass(StyleNano.StyleClassButtonColorGreen);
+
+ else
+ button.RemoveStyleClass(StyleNano.StyleClassButtonColorGreen);
+
+ // Update sprite
+ if (entry.MetaData.Value.SpritePath != string.Empty && entry.MetaData.Value.SpriteState != string.Empty)
+ button.TextureRect.Texture = _spriteSystem.Frame0(new SpriteSpecifier.Rsi(new ResPath(entry.MetaData.Value.SpritePath), entry.MetaData.Value.SpriteState));
+
+ // Update name
+ var name = Loc.GetString(entry.MetaData.Value.EntityName);
+ button.NameLocalized.Text = name;
+
+ // Update tool tip
+ button.ToolTip = Loc.GetString(name);
+
+ // Update power value
+ button.PowerValue.Text = Loc.GetString("power-monitoring-window-value", ("value", entry.PowerValue));
+ }
+
+ private void UpdateEntrySourcesOrLoads(BoxContainer masterContainer, BoxContainer currentContainer, PowerMonitoringConsoleEntry[]? entries, SpriteSpecifier.Texture icon)
+ {
+ if (currentContainer == null)
+ return;
+
+ if (entries == null || entries.Length == 0)
+ {
+ currentContainer.RemoveAllChildren();
+ return;
+ }
+
+ // Remove excess children
+ while (currentContainer.ChildCount > entries.Length)
+ {
+ currentContainer.RemoveChild(currentContainer.GetChild(currentContainer.ChildCount - 1));
+ }
+
+ // Add missing children
+ while (currentContainer.ChildCount < entries.Length)
+ {
+ var entry = entries[currentContainer.ChildCount];
+ var subEntry = new PowerMonitoringWindowSubEntry(entry);
+ currentContainer.AddChild(subEntry);
+
+ // Selection action
+ subEntry.Button.OnButtonUp += args => { ButtonAction(subEntry, masterContainer); };
+ }
+
+ if (!_entManager.TryGetComponent<PowerMonitoringConsoleComponent>(_owner, out var console))
+ return;
+
+ // Update all children
+ foreach (var child in currentContainer.Children)
+ {
+ if (child is not PowerMonitoringWindowSubEntry)
+ continue;
+
+ var castChild = (PowerMonitoringWindowSubEntry) child;
+
+ if (castChild == null)
+ continue;
+
+ if (castChild.Icon != null)
+ castChild.Icon.Texture = _spriteSystem.Frame0(icon);
+
+ var entry = entries[child.GetPositionInParent()];
+
+ castChild.NetEntity = entry.NetEntity;
+ castChild.Entry = entry;
+
+ UpdateWindowEntryButton(entry.NetEntity, castChild.Button, entries.ElementAt(child.GetPositionInParent()));
+ }
+ }
+
+ private void ButtonAction(PowerMonitoringWindowBaseEntry entry, BoxContainer masterContainer)
+ {
+ // Toggle off button?
+ if (entry.NetEntity == _focusEntity)
+ {
+ entry.Button.RemoveStyleClass(StyleNano.StyleClassButtonColorGreen);
+ _focusEntity = null;
+
+ // Request an update from the power monitoring system
+ SendPowerMonitoringConsoleMessageAction?.Invoke(null, entry.Entry.Group);
+
+ return;
+ }
+
+ // Otherwise, toggle on
+ entry.Button.AddStyleClass(StyleNano.StyleClassButtonColorGreen);
+
+ ActivateAutoScrollToFocus();
+
+ // Toggle off the old button (if applicable)
+ if (_focusEntity != null)
+ {
+ foreach (PowerMonitoringWindowEntry sibling in masterContainer.Children)
+ {
+ if (sibling.NetEntity == _focusEntity)
+ {
+ sibling.Button.RemoveStyleClass(StyleNano.StyleClassButtonColorGreen);
+ break;
+ }
+ }
+ }
+
+ // Center the nav map on selected entity
+ _focusEntity = entry.NetEntity;
+
+ if (!NavMap.TrackedEntities.TryGetValue(entry.NetEntity, out var blip))
+ return;
+
+ NavMap.CenterToCoordinates(blip.Coordinates);
+
+ // Switch tabs
+ SwitchTabsBasedOnPowerMonitoringConsoleGroup(entry.Entry.Group);
+
+ // Send an update from the power monitoring system
+ SendPowerMonitoringConsoleMessageAction?.Invoke(_focusEntity, entry.Entry.Group);
+ }
+
+ private void ActivateAutoScrollToFocus()
+ {
+ _autoScrollActive = false;
+ _autoScrollAwaitsUpdate = true;
+ }
+
+ private bool TryGetNextScrollPosition([NotNullWhen(true)] out float? nextScrollPosition)
+ {
+ nextScrollPosition = null;
+
+ var scroll = MasterTabContainer.Children.ElementAt(MasterTabContainer.CurrentTab) as ScrollContainer;
+ if (scroll == null)
+ return false;
+
+ var container = scroll.Children.ElementAt(0) as BoxContainer;
+ if (container == null || container.Children.Count() == 0)
+ return false;
+
+ // Exit if the heights of the children haven't been initialized yet
+ if (!container.Children.Any(x => x.Height > 0))
+ return false;
+
+ nextScrollPosition = 0;
+
+ foreach (var control in container.Children)
+ {
+ if (control == null || control is not PowerMonitoringWindowEntry)
+ continue;
+
+ if (((PowerMonitoringWindowEntry) control).NetEntity == _focusEntity)
+ return true;
+
+ nextScrollPosition += control.Height;
+ }
+
+ // Failed to find control
+ nextScrollPosition = null;
+
+ return false;
+ }
+
+ private bool TryGetVerticalScrollbar(ScrollContainer scroll, [NotNullWhen(true)] out VScrollBar? vScrollBar)
+ {
+ vScrollBar = null;
+
+ foreach (var child in scroll.Children)
+ {
+ if (child is not VScrollBar)
+ continue;
+
+ var castChild = child as VScrollBar;
+
+ if (castChild != null)
+ {
+ vScrollBar = castChild;
+ return true;
+ }
+ }
+
+ return false;
+ }
+
+ private void AutoScrollToFocus()
+ {
+ if (!_autoScrollActive)
+ return;
+
+ var scroll = MasterTabContainer.Children.ElementAt(MasterTabContainer.CurrentTab) as ScrollContainer;
+ if (scroll == null)
+ return;
+
+ if (!TryGetVerticalScrollbar(scroll, out var vScrollbar))
+ return;
+
+ if (!TryGetNextScrollPosition(out float? nextScrollPosition))
+ return;
+
+ vScrollbar.ValueTarget = nextScrollPosition.Value;
+
+ if (MathHelper.CloseToPercent(vScrollbar.Value, vScrollbar.ValueTarget))
+ _autoScrollActive = false;
+ }
+
+ private void UpdateWarningLabel(PowerMonitoringFlags flags)
+ {
+ if (flags == PowerMonitoringFlags.None)
+ {
+ SystemWarningPanel.Visible = false;
+ return;
+ }
+
+ var msg = new FormattedMessage();
+
+ if ((flags & PowerMonitoringFlags.RoguePowerConsumer) != 0)
+ {
+ SystemWarningPanel.PanelOverride = new StyleBoxFlat
+ {
+ BackgroundColor = Color.Red,
+ BorderColor = Color.DarkRed,
+ BorderThickness = new Thickness(2),
+ };
+
+ msg.AddMarkup(Loc.GetString("power-monitoring-window-rogue-power-consumer"));
+ SystemWarningPanel.Visible = true;
+ }
+
+ else if ((flags & PowerMonitoringFlags.PowerNetAbnormalities) != 0)
+ {
+ SystemWarningPanel.PanelOverride = new StyleBoxFlat
+ {
+ BackgroundColor = Color.Orange,
+ BorderColor = Color.DarkOrange,
+ BorderThickness = new Thickness(2),
+ };
+
+ msg.AddMarkup(Loc.GetString("power-monitoring-window-power-net-abnormalities"));
+ SystemWarningPanel.Visible = true;
+ }
+
+ SystemWarningLabel.SetMessage(msg);
+ }
+
+ private void SwitchTabsBasedOnPowerMonitoringConsoleGroup(PowerMonitoringConsoleGroup group)
+ {
+ switch (group)
+ {
+ case PowerMonitoringConsoleGroup.Generator:
+ MasterTabContainer.CurrentTab = 0; break;
+ case PowerMonitoringConsoleGroup.SMES:
+ MasterTabContainer.CurrentTab = 1; break;
+ case PowerMonitoringConsoleGroup.Substation:
+ MasterTabContainer.CurrentTab = 2; break;
+ case PowerMonitoringConsoleGroup.APC:
+ MasterTabContainer.CurrentTab = 3; break;
+ }
+ }
+
+ private PowerMonitoringConsoleGroup GetCurrentPowerMonitoringConsoleGroup()
+ {
+ return (PowerMonitoringConsoleGroup) MasterTabContainer.CurrentTab;
+ }
+}
+
+public sealed class PowerMonitoringWindowEntry : PowerMonitoringWindowBaseEntry
+{
+ public BoxContainer MainContainer;
+ public BoxContainer SourcesContainer;
+ public BoxContainer LoadsContainer;
+
+ public PowerMonitoringWindowEntry(PowerMonitoringConsoleEntry entry) : base(entry)
+ {
+ Entry = entry;
+
+ // Alignment
+ Orientation = LayoutOrientation.Vertical;
+ HorizontalExpand = true;
+
+ // Update selection button
+ Button.StyleClasses.Add("OpenLeft");
+ AddChild(Button);
+
+ // Grid container to hold sub containers
+ MainContainer = new BoxContainer()
+ {
+ Orientation = LayoutOrientation.Vertical,
+ HorizontalExpand = true,
+ Margin = new Thickness(8, 0, 0, 0),
+ Visible = false,
+ };
+
+ AddChild(MainContainer);
+
+ // Grid container to hold the list of sources when selected
+ SourcesContainer = new BoxContainer()
+ {
+ Orientation = LayoutOrientation.Vertical,
+ HorizontalExpand = true,
+ };
+
+ MainContainer.AddChild(SourcesContainer);
+
+ // Grid container to hold the list of loads when selected
+ LoadsContainer = new BoxContainer()
+ {
+ Orientation = LayoutOrientation.Vertical,
+ HorizontalExpand = true,
+ };
+
+ MainContainer.AddChild(LoadsContainer);
+ }
+}
+
+public sealed class PowerMonitoringWindowSubEntry : PowerMonitoringWindowBaseEntry
+{
+ public TextureRect? Icon;
+
+ public PowerMonitoringWindowSubEntry(PowerMonitoringConsoleEntry entry) : base(entry)
+ {
+ Orientation = LayoutOrientation.Horizontal;
+ HorizontalExpand = true;
+
+ // Source/load icon
+ Icon = new TextureRect()
+ {
+ VerticalAlignment = VAlignment.Center,
+ Margin = new Thickness(0, 0, 2, 0),
+ };
+
+ AddChild(Icon);
+
+ // Selection button
+ Button.StyleClasses.Add("OpenBoth");
+ AddChild(Button);
+ }
+}
+
+public abstract class PowerMonitoringWindowBaseEntry : BoxContainer
+{
+ public NetEntity NetEntity;
+ public PowerMonitoringConsoleEntry Entry;
+ public PowerMonitoringButton Button;
+
+ public PowerMonitoringWindowBaseEntry(PowerMonitoringConsoleEntry entry)
+ {
+ Entry = entry;
+
+ // Add selection button (properties set by derivative classes)
+ Button = new PowerMonitoringButton();
+ }
+}
+
+public sealed class PowerMonitoringButton : Button
+{
+ public BoxContainer MainContainer;
+ public TextureRect TextureRect;
+ public Label NameLocalized;
+ public Label PowerValue;
+
+ public PowerMonitoringButton()
+ {
+ HorizontalExpand = true;
+ VerticalExpand = true;
+ Margin = new Thickness(0f, 1f, 0f, 1f);
+
+ MainContainer = new BoxContainer()
+ {
+ Orientation = BoxContainer.LayoutOrientation.Horizontal,
+ HorizontalExpand = true,
+ SetHeight = 32f,
+ };
+
+ AddChild(MainContainer);
+
+ TextureRect = new TextureRect()
+ {
+ HorizontalAlignment = HAlignment.Center,
+ VerticalAlignment = VAlignment.Center,
+ SetSize = new Vector2(32f, 32f),
+ Margin = new Thickness(0f, 0f, 5f, 0f),
+ };
+
+ MainContainer.AddChild(TextureRect);
+
+ NameLocalized = new Label()
+ {
+ HorizontalExpand = true,
+ ClipText = true,
+ };
+
+ MainContainer.AddChild(NameLocalized);
+
+ PowerValue = new Label()
+ {
+ HorizontalAlignment = HAlignment.Right,
+ SetWidth = 72f,
+ Margin = new Thickness(10, 0, 0, 0),
+ ClipText = true,
+ };
+
+ MainContainer.AddChild(PowerValue);
+ }
+}
-using System.Linq;
-using System.Numerics;
-using Content.Client.Computer;
+using Content.Client.Pinpointer.UI;
+using Content.Client.UserInterface.Controls;
using Content.Shared.Power;
-using JetBrains.Annotations;
using Robust.Client.AutoGenerated;
using Robust.Client.GameObjects;
-using Robust.Client.Graphics;
using Robust.Client.UserInterface.Controls;
-using Robust.Client.UserInterface.CustomControls;
using Robust.Client.UserInterface.XAML;
-using Robust.Shared.Graphics;
-using Robust.Shared.Graphics.RSI;
-using Robust.Shared.Prototypes;
+using Robust.Shared.Map;
+using Robust.Shared.Timing;
+using Robust.Shared.Utility;
+using System.Linq;
namespace Content.Client.Power;
[GenerateTypedNameReferences]
-public sealed partial class PowerMonitoringWindow : DefaultWindow, IComputerWindow<PowerMonitoringConsoleBoundInterfaceState>
+public sealed partial class PowerMonitoringWindow : FancyWindow
{
- [Dependency] private readonly IPrototypeManager _prototypeManager = default!;
- private readonly SpriteSystem _spriteSystem = default!;
+ private readonly IEntityManager _entManager;
+ private readonly SpriteSystem _spriteSystem;
+ private readonly IGameTiming _gameTiming;
+
+ private const float BlinkFrequency = 1f;
- public PowerMonitoringWindow()
+ private EntityUid? _owner;
+ private NetEntity? _focusEntity;
+
+ public event Action<NetEntity?, PowerMonitoringConsoleGroup>? SendPowerMonitoringConsoleMessageAction;
+
+ private Dictionary<PowerMonitoringConsoleGroup, (SpriteSpecifier.Texture, Color)> _groupBlips = new()
{
- RobustXamlLoader.Load(this);
- SetSize = MinSize = new Vector2(300, 450);
- IoCManager.InjectDependencies(this);
- _spriteSystem = IoCManager.Resolve<IEntityManager>().System<SpriteSystem>();
- MasterTabContainer.SetTabTitle(0, Loc.GetString("power-monitoring-window-tab-sources"));
- MasterTabContainer.SetTabTitle(1, Loc.GetString("power-monitoring-window-tab-loads"));
- }
+ { PowerMonitoringConsoleGroup.Generator, (new SpriteSpecifier.Texture(new ResPath("/Textures/Interface/NavMap/beveled_circle.png")), Color.Purple) },
+ { PowerMonitoringConsoleGroup.SMES, (new SpriteSpecifier.Texture(new ResPath("/Textures/Interface/NavMap/beveled_hexagon.png")), Color.OrangeRed) },
+ { PowerMonitoringConsoleGroup.Substation, (new SpriteSpecifier.Texture(new ResPath("/Textures/Interface/NavMap/beveled_square.png")), Color.Yellow) },
+ { PowerMonitoringConsoleGroup.APC, (new SpriteSpecifier.Texture(new ResPath("/Textures/Interface/NavMap/beveled_triangle.png")), Color.LimeGreen) },
+ };
- public void UpdateState(PowerMonitoringConsoleBoundInterfaceState scc)
+ public PowerMonitoringWindow(PowerMonitoringConsoleBoundUserInterface userInterface, EntityUid? owner)
{
- UpdateList(TotalSourcesNum, scc.TotalSources, SourcesList, scc.Sources);
- var loads = scc.Loads;
- if (!ShowInactiveConsumersCheckBox.Pressed)
+ RobustXamlLoader.Load(this);
+ _entManager = IoCManager.Resolve<IEntityManager>();
+ _gameTiming = IoCManager.Resolve<IGameTiming>();
+
+ _spriteSystem = _entManager.System<SpriteSystem>();
+ _owner = owner;
+
+ // Pass owner to nav map
+ NavMap.Owner = _owner;
+
+ // Set nav map grid uid
+ var stationName = Loc.GetString("power-monitoring-window-unknown-location");
+
+ if (_entManager.TryGetComponent<TransformComponent>(owner, out var xform))
{
- // Not showing inactive consumers, so hiding them.
- // This means filtering out loads that are not either:
- // + Batteries (always important)
- // + Meaningful (size above 0)
- loads = loads.Where(a => a.IsBattery || a.Size > 0.0f).ToArray();
+ NavMap.MapUid = xform.GridUid;
+
+ // Assign station name
+ if (_entManager.TryGetComponent<MetaDataComponent>(xform.GridUid, out var stationMetaData))
+ stationName = stationMetaData.EntityName;
+
+ var msg = new FormattedMessage();
+ msg.AddMarkup(Loc.GetString("power-monitoring-window-station-name", ("stationName", stationName)));
+
+ StationName.SetMessage(msg);
+ }
+
+ else
+ {
+ StationName.SetMessage(stationName);
+ NavMap.Visible = false;
}
- UpdateList(TotalLoadsNum, scc.TotalLoads, LoadsList, loads);
+
+ // Set trackable entity selected action
+ NavMap.TrackedEntitySelectedAction += SetTrackedEntityFromNavMap;
+
+ // Update nav map
+ NavMap.ForceNavMapUpdate();
+
+ // Set UI tab titles
+ MasterTabContainer.SetTabTitle(0, Loc.GetString("power-monitoring-window-label-sources"));
+ MasterTabContainer.SetTabTitle(1, Loc.GetString("power-monitoring-window-label-smes"));
+ MasterTabContainer.SetTabTitle(2, Loc.GetString("power-monitoring-window-label-substation"));
+ MasterTabContainer.SetTabTitle(3, Loc.GetString("power-monitoring-window-label-apc"));
+
+ // Track when the MasterTabContainer changes its tab
+ MasterTabContainer.OnTabChanged += OnTabChanged;
+
+ // Set UI toggles
+ ShowHVCable.OnToggled += _ => OnShowCableToggled(PowerMonitoringConsoleLineGroup.HighVoltage);
+ ShowMVCable.OnToggled += _ => OnShowCableToggled(PowerMonitoringConsoleLineGroup.MediumVoltage);
+ ShowLVCable.OnToggled += _ => OnShowCableToggled(PowerMonitoringConsoleLineGroup.Apc);
+
+ // Set power monitoring message action
+ SendPowerMonitoringConsoleMessageAction += userInterface.SendPowerMonitoringConsoleMessage;
}
- public void UpdateList(Label number, double numberVal, ItemList list, PowerMonitoringConsoleEntry[] listVal)
+ private void OnTabChanged(int tab)
{
- number.Text = Loc.GetString("power-monitoring-window-value", ("value", numberVal));
- // This magic is important to prevent scrolling issues.
- while (list.Count > listVal.Length)
+ SendPowerMonitoringConsoleMessageAction?.Invoke(_focusEntity, (PowerMonitoringConsoleGroup) tab);
+ }
+
+ private void OnShowCableToggled(PowerMonitoringConsoleLineGroup lineGroup)
+ {
+ if (!NavMap.HiddenLineGroups.Remove(lineGroup))
+ NavMap.HiddenLineGroups.Add(lineGroup);
+ }
+
+ public void ShowEntites
+ (double totalSources,
+ double totalBatteryUsage,
+ double totalLoads,
+ PowerMonitoringConsoleEntry[] allEntries,
+ PowerMonitoringConsoleEntry[] focusSources,
+ PowerMonitoringConsoleEntry[] focusLoads,
+ EntityCoordinates? monitorCoords)
+ {
+ if (_owner == null)
+ return;
+
+ if (!_entManager.TryGetComponent<PowerMonitoringConsoleComponent>(_owner.Value, out var console))
+ return;
+
+ // Update power status text
+ TotalSources.Text = Loc.GetString("power-monitoring-window-value", ("value", totalSources));
+ TotalBatteryUsage.Text = Loc.GetString("power-monitoring-window-value", ("value", totalBatteryUsage));
+ TotalLoads.Text = Loc.GetString("power-monitoring-window-value", ("value", totalLoads));
+
+ // 10+% of station power is being drawn from batteries
+ TotalBatteryUsage.FontColorOverride = (totalSources * 0.1111f) < totalBatteryUsage ? new Color(180, 0, 0) : Color.White;
+
+ // Station generator and battery output is less than the current demand
+ TotalLoads.FontColorOverride = (totalSources + totalBatteryUsage) < totalLoads &&
+ !MathHelper.CloseToPercent(totalSources + totalBatteryUsage, totalLoads, 0.1f) ? new Color(180, 0, 0) : Color.White;
+
+ // Update system warnings
+ UpdateWarningLabel(console.Flags);
+
+ // Reset nav map values
+ NavMap.TrackedCoordinates.Clear();
+ NavMap.TrackedEntities.Clear();
+
+ // Draw entities on the nav map
+ var entitiesOfInterest = new List<NetEntity>();
+
+ if (_focusEntity != null)
{
- list.RemoveAt(list.Count - 1);
+ entitiesOfInterest.Add(_focusEntity.Value);
+
+ foreach (var entry in focusSources)
+ entitiesOfInterest.Add(entry.NetEntity);
+
+ foreach (var entry in focusLoads)
+ entitiesOfInterest.Add(entry.NetEntity);
}
- while (list.Count < listVal.Length)
+
+ focusSources.Concat(focusLoads);
+
+ foreach ((var netEntity, var metaData) in console.PowerMonitoringDeviceMetaData)
{
- list.AddItem("YOU SHOULD NEVER SEE THIS (REALLY!)", null, false);
+ if (NavMap.Visible)
+ AddTrackedEntityToNavMap(netEntity, metaData, entitiesOfInterest);
}
- // Now overwrite the items properly...
- for (var i = 0; i < listVal.Length; i++)
+
+ // Show monitor location
+ var mon = _entManager.GetNetEntity(_owner);
+
+ if (monitorCoords != null && mon != null)
{
- var ent = listVal[i];
- _prototypeManager.TryIndex(ent.IconEntityPrototypeId, out EntityPrototype? entityPrototype);
- IRsiStateLike? iconState = null;
- if (entityPrototype != null)
- iconState = _spriteSystem.GetPrototypeIcon(entityPrototype);
- var icon = iconState?.GetFrame(RsiDirection.South, 0);
- var item = list[i];
- item.Text = $"{ent.NameLocalized} {Loc.GetString("power-monitoring-window-value", ("value", ent.Size))}";
- item.Icon = icon;
+ var texture = _spriteSystem.Frame0(new SpriteSpecifier.Texture(new ResPath("/Textures/Interface/NavMap/beveled_circle.png")));
+ var blip = new NavMapBlip(monitorCoords.Value, texture, Color.Cyan, true, false);
+ NavMap.TrackedEntities[mon.Value] = blip;
}
+
+ // Update nav map
+ NavMap.ForceNavMapUpdate();
+
+ // If the entry group doesn't match the current tab, the data is out dated, do not use it
+ if (allEntries.Length > 0 && allEntries[0].Group != GetCurrentPowerMonitoringConsoleGroup())
+ return;
+
+ // Assign meta data to the console entries and sort them
+ allEntries = GetUpdatedPowerMonitoringConsoleEntries(allEntries, console);
+ focusSources = GetUpdatedPowerMonitoringConsoleEntries(focusSources, console);
+ focusLoads = GetUpdatedPowerMonitoringConsoleEntries(focusLoads, console);
+
+ // Get current console entry container
+ BoxContainer currentContainer = SourcesList;
+ switch (GetCurrentPowerMonitoringConsoleGroup())
+ {
+ case PowerMonitoringConsoleGroup.SMES:
+ currentContainer = SMESList; break;
+ case PowerMonitoringConsoleGroup.Substation:
+ currentContainer = SubstationList; break;
+ case PowerMonitoringConsoleGroup.APC:
+ currentContainer = ApcList; break;
+ }
+
+ // Clear excess children from the container
+ while (currentContainer.ChildCount > allEntries.Length)
+ currentContainer.RemoveChild(currentContainer.GetChild(currentContainer.ChildCount - 1));
+
+ // Update the remaining children
+ for (var index = 0; index < allEntries.Length; index++)
+ {
+ var entry = allEntries[index];
+
+ if (entry.NetEntity == _focusEntity)
+ UpdateWindowConsoleEntry(currentContainer, index, entry, focusSources, focusLoads);
+
+ else
+ UpdateWindowConsoleEntry(currentContainer, index, entry);
+ }
+
+ // Auto-scroll renable
+ if (_autoScrollAwaitsUpdate)
+ {
+ _autoScrollActive = true;
+ _autoScrollAwaitsUpdate = false;
+ }
+ }
+
+ private void AddTrackedEntityToNavMap(NetEntity netEntity, PowerMonitoringDeviceMetaData metaData, List<NetEntity> entitiesOfInterest)
+ {
+ if (!_groupBlips.TryGetValue(metaData.Group, out var data))
+ return;
+
+ var usedEntity = (metaData.CollectionMaster != null) ? metaData.CollectionMaster : netEntity;
+ var coords = _entManager.GetCoordinates(metaData.Coordinates);
+ var texture = data.Item1;
+ var color = data.Item2;
+ var blink = usedEntity == _focusEntity;
+ var modulator = Color.White;
+
+ if (_focusEntity != null && usedEntity != _focusEntity && !entitiesOfInterest.Contains(usedEntity.Value))
+ modulator = Color.DimGray;
+
+ var blip = new NavMapBlip(coords, _spriteSystem.Frame0(texture), color * modulator, blink);
+ NavMap.TrackedEntities[netEntity] = blip;
+ }
+
+ private void SetTrackedEntityFromNavMap(NetEntity? netEntity)
+ {
+ if (netEntity == null)
+ return;
+
+ if (!_entManager.TryGetComponent<PowerMonitoringConsoleComponent>(_owner, out var console))
+ return;
+
+ if (!console.PowerMonitoringDeviceMetaData.TryGetValue(netEntity.Value, out var metaData))
+ return;
+
+ // Switch entity for master, if applicable
+ // The master will always be in the same group as the entity
+ if (metaData.CollectionMaster != null)
+ netEntity = metaData.CollectionMaster;
+
+ _focusEntity = netEntity;
+
+ // Switch tabs
+ SwitchTabsBasedOnPowerMonitoringConsoleGroup(metaData.Group);
+
+ // Get the scroll position of the selected entity on the selected button the UI
+ ActivateAutoScrollToFocus();
+
+ // Send message to console that the focus has changed
+ SendPowerMonitoringConsoleMessageAction?.Invoke(_focusEntity, metaData.Group);
+ }
+
+ protected override void FrameUpdate(FrameEventArgs args)
+ {
+ AutoScrollToFocus();
+
+ // Warning sign pulse
+ var lit = _gameTiming.RealTime.TotalSeconds % BlinkFrequency > BlinkFrequency / 2f;
+ SystemWarningPanel.Modulate = lit ? Color.White : new Color(178, 178, 178);
+ }
+
+ private PowerMonitoringConsoleEntry[] GetUpdatedPowerMonitoringConsoleEntries(PowerMonitoringConsoleEntry[] entries, PowerMonitoringConsoleComponent console)
+ {
+ for (int i = 0; i < entries.Length; i++)
+ {
+ var entry = entries[i];
+
+ if (!console.PowerMonitoringDeviceMetaData.TryGetValue(entry.NetEntity, out var metaData))
+ continue;
+
+ entries[i].MetaData = metaData;
+ }
+
+ // Sort all devices alphabetically by their entity name (not by power usage; otherwise their position on the UI will shift)
+ Array.Sort(entries, AlphabeticalSort);
+
+ return entries;
+ }
+
+ private int AlphabeticalSort(PowerMonitoringConsoleEntry x, PowerMonitoringConsoleEntry y)
+ {
+ if (x.MetaData?.EntityName == null)
+ return -1;
+
+ if (y.MetaData?.EntityName == null)
+ return 1;
+
+ return x.MetaData.Value.EntityName.CompareTo(y.MetaData.Value.EntityName);
}
}
-[UsedImplicitly]
-public sealed class PowerMonitoringConsoleBoundUserInterface : ComputerBoundUserInterface<PowerMonitoringWindow, PowerMonitoringConsoleBoundInterfaceState>
+public struct PowerMonitoringConsoleTrackable
{
- public PowerMonitoringConsoleBoundUserInterface(EntityUid owner, Enum uiKey) : base(owner, uiKey)
+ public EntityUid EntityUid;
+ public PowerMonitoringConsoleGroup Group;
+
+ public PowerMonitoringConsoleTrackable(EntityUid uid, PowerMonitoringConsoleGroup group)
{
+ EntityUid = uid;
+ Group = group;
}
}
-
-using Content.Server.Station.Components;
using Content.Server.Station.Systems;
using Content.Server.Warps;
using Content.Shared.Pinpointer;
SubscribeLocalEvent<StationGridAddedEvent>(OnStationInit);
SubscribeLocalEvent<NavMapComponent, ComponentStartup>(OnNavMapStartup);
SubscribeLocalEvent<NavMapComponent, ComponentGetState>(OnGetState);
- SubscribeLocalEvent<NavMapComponent, GridSplitEvent>(OnNavMapSplit);
+ SubscribeLocalEvent<GridSplitEvent>(OnNavMapSplit);
SubscribeLocalEvent<NavMapBeaconComponent, ComponentStartup>(OnNavMapBeaconStartup);
SubscribeLocalEvent<NavMapBeaconComponent, AnchorStateChangedEvent>(OnNavMapBeaconAnchor);
RefreshGrid(component, grid);
}
- private void OnNavMapSplit(EntityUid uid, NavMapComponent component, ref GridSplitEvent args)
+ private void OnNavMapSplit(ref GridSplitEvent args)
{
var gridQuery = GetEntityQuery<MapGridComponent>();
RefreshGrid(newComp, gridQuery.GetComponent(grid));
}
- RefreshGrid(component, gridQuery.GetComponent(uid));
+ RefreshGrid(Comp<NavMapComponent>(args.Grid), gridQuery.GetComponent(args.Grid));
}
private void RefreshGrid(NavMapComponent component, MapGridComponent grid)
private void RefreshTile(MapGridComponent grid, NavMapComponent component, NavMapChunk chunk, Vector2i tile)
{
var relative = SharedMapSystem.GetChunkRelative(tile, ChunkSize);
-
var existing = chunk.TileData;
var flag = GetFlag(relative);
using Content.Server.Power.EntitySystems;
+using Content.Shared.Power;
using Content.Shared.Tools;
using Robust.Shared.Prototypes;
using Robust.Shared.Serialization.TypeSerializers.Implementations.Custom.Prototype;
+using System.Diagnostics.Tracing;
-namespace Content.Server.Power.Components
+namespace Content.Server.Power.Components;
+
+/// <summary>
+/// Allows the attached entity to be destroyed by a cutting tool, dropping a piece of cable.
+/// </summary>
+[RegisterComponent]
+[Access(typeof(CableSystem))]
+public sealed partial class CableComponent : Component
{
+ [DataField("cableDroppedOnCutPrototype", customTypeSerializer: typeof(PrototypeIdSerializer<EntityPrototype>))]
+ public string CableDroppedOnCutPrototype = "CableHVStack1";
+
+ [DataField("cuttingQuality", customTypeSerializer:typeof(PrototypeIdSerializer<ToolQualityPrototype>))]
+ public string CuttingQuality = "Cutting";
+
/// <summary>
- /// Allows the attached entity to be destroyed by a cutting tool, dropping a piece of cable.
+ /// Checked by <see cref="CablePlacerComponent"/> to determine if there is
+ /// already a cable of a type on a tile.
/// </summary>
- [RegisterComponent]
- [Access(typeof(CableSystem))]
- public sealed partial class CableComponent : Component
- {
- [DataField("cableDroppedOnCutPrototype", customTypeSerializer: typeof(PrototypeIdSerializer<EntityPrototype>))]
- public string CableDroppedOnCutPrototype = "CableHVStack1";
+ [DataField("cableType")]
+ public CableType CableType = CableType.HighVoltage;
- [DataField("cuttingQuality", customTypeSerializer:typeof(PrototypeIdSerializer<ToolQualityPrototype>))]
- public string CuttingQuality = "Cutting";
+ [DataField("cuttingDelay")]
+ public float CuttingDelay = 1f;
+}
- /// <summary>
- /// Checked by <see cref="CablePlacerComponent"/> to determine if there is
- /// already a cable of a type on a tile.
- /// </summary>
- [DataField("cableType")]
- public CableType CableType = CableType.HighVoltage;
+/// <summary>
+/// Event to be raised when a cable is anchored / unanchored
+/// </summary>
+[ByRefEvent]
+public readonly struct CableAnchorStateChangedEvent
+{
+ public readonly TransformComponent Transform;
+ public EntityUid Entity => Transform.Owner;
+ public bool Anchored => Transform.Anchored;
- [DataField("cuttingDelay")]
- public float CuttingDelay = 1f;
- }
+ /// <summary>
+ /// If true, the entity is being detached to null-space
+ /// </summary>
+ public readonly bool Detaching;
- public enum CableType
+ public CableAnchorStateChangedEvent(TransformComponent transform, bool detaching = false)
{
- HighVoltage,
- MediumVoltage,
- Apc,
+ Detaching = detaching;
+ Transform = transform;
}
}
using Robust.Shared.Prototypes;
using Robust.Shared.Serialization.TypeSerializers.Implementations.Custom.Prototype;
+using Content.Shared.Power;
namespace Content.Server.Power.Components
{
+++ /dev/null
-namespace Content.Server.Power.Components;
-
-[RegisterComponent]
-public sealed partial class PowerMonitoringConsoleComponent : Component
-{
-}
-
--- /dev/null
+using Content.Server.NodeContainer;
+using Content.Server.NodeContainer.NodeGroups;
+using Content.Server.Power.EntitySystems;
+using Content.Shared.Power;
+
+namespace Content.Server.Power.Components;
+
+/// <summary>
+/// Used to flag any entities that should appear on a power monitoring console
+/// </summary>
+[RegisterComponent, Access(typeof(PowerMonitoringConsoleSystem))]
+public sealed partial class PowerMonitoringDeviceComponent : Component
+{
+ /// <summary>
+ /// Name of the node that this device draws its power from (see <see cref="NodeContainerComponent"/>)
+ /// </summary>
+ [DataField("sourceNode"), ViewVariables]
+ public string SourceNode = string.Empty;
+
+ /// <summary>
+ /// Name of the node that this device distributes power to (see <see cref="NodeContainerComponent"/>)
+ /// </summary>
+ [DataField("loadNode"), ViewVariables]
+ public string LoadNode = string.Empty;
+
+ /// <summary>
+ /// Names of the nodes that this device can potentially distributes power to (see <see cref="NodeContainerComponent"/>)
+ /// </summary>
+ [DataField("loadNodes"), ViewVariables]
+ public List<string>? LoadNodes;
+
+ /// <summary>
+ /// This entity will be grouped with entities that have the same collection name
+ /// </summary>
+ [DataField("collectionName"), ViewVariables]
+ public string CollectionName = string.Empty;
+
+ [ViewVariables]
+ public BaseNodeGroup? NodeGroup = null;
+
+ /// <summary>
+ /// Indicates whether the entity is/should be part of a collection
+ /// </summary>
+ public bool IsCollectionMasterOrChild { get { return CollectionName != string.Empty; } }
+
+ /// <summary>
+ /// Specifies the uid of the master that represents this entity
+ /// </summary>
+ /// <remarks>
+ /// Used when grouping multiple entities into a single power monitoring console entry
+ /// </remarks>
+ [ViewVariables]
+ public EntityUid CollectionMaster;
+
+ /// <summary>
+ /// Indicates if this entity represents a group of entities
+ /// </summary>
+ /// <remarks>
+ /// Used when grouping multiple entities into a single power monitoring console entry
+ /// </remarks>
+ public bool IsCollectionMaster { get { return Owner == CollectionMaster; } }
+
+ /// <summary>
+ /// A list of other entities that are to be represented by this entity
+ /// </summary>
+ /// /// <remarks>
+ /// Used when grouping multiple entities into a single power monitoring console entry
+ /// </remarks>
+ [ViewVariables]
+ public Dictionary<EntityUid, PowerMonitoringDeviceComponent> ChildDevices = new();
+
+ /// <summary>
+ /// Path to the .rsi folder
+ /// </summary>
+ [DataField("sprite"), ViewVariables]
+ public string SpritePath = string.Empty;
+
+ /// <summary>
+ /// The .rsi state
+ /// </summary>
+ [DataField("state"), ViewVariables]
+ public string SpriteState = string.Empty;
+
+ /// <summary>
+ /// Determines what power monitoring group this entity should belong to
+ /// </summary>
+ [DataField("group", required: true), ViewVariables]
+ public PowerMonitoringConsoleGroup Group;
+}
using Content.Shared.Tools;
using Content.Shared.Tools.Components;
using Robust.Shared.Map;
+using System.Xml.Schema;
using CableCuttingFinishedEvent = Content.Shared.Tools.Systems.CableCuttingFinishedEvent;
using SharedToolSystem = Content.Shared.Tools.Systems.SharedToolSystem;
[Dependency] private readonly StackSystem _stack = default!;
[Dependency] private readonly ElectrocutionSystem _electrocutionSystem = default!;
[Dependency] private readonly IAdminLogManager _adminLogs = default!;
+ [Dependency] private readonly PowerMonitoringConsoleSystem _powerMonitoringSystem = default!;
public override void Initialize()
{
if (args.Cancelled)
return;
+ var xform = Transform(uid);
+ var ev = new CableAnchorStateChangedEvent(xform);
+ RaiseLocalEvent(uid, ref ev);
+
if (_electrocutionSystem.TryDoElectrifiedAct(uid, args.User))
return;
- _adminLogs.Add(LogType.CableCut, LogImpact.Medium, $"The {ToPrettyString(uid)} at {Transform(uid).Coordinates} was cut by {ToPrettyString(args.User)}.");
+ _adminLogs.Add(LogType.CableCut, LogImpact.Medium, $"The {ToPrettyString(uid)} at {xform.Coordinates} was cut by {ToPrettyString(args.User)}.");
- Spawn(cable.CableDroppedOnCutPrototype, Transform(uid).Coordinates);
+ Spawn(cable.CableDroppedOnCutPrototype, xform.Coordinates);
QueueDel(uid);
}
private void OnAnchorChanged(EntityUid uid, CableComponent cable, ref AnchorStateChangedEvent args)
{
+ var ev = new CableAnchorStateChangedEvent(args.Transform, args.Detaching);
+ RaiseLocalEvent(uid, ref ev);
+
if (args.Anchored)
return; // huh? it wasn't anchored?
-using Content.Shared.Power;
+using Content.Server.GameTicking.Rules.Components;
using Content.Server.NodeContainer;
using Content.Server.NodeContainer.EntitySystems;
+using Content.Server.NodeContainer.NodeGroups;
using Content.Server.NodeContainer.Nodes;
using Content.Server.Power.Components;
+using Content.Server.Power.Nodes;
using Content.Server.Power.NodeGroups;
+using Content.Server.Station.Components;
+using Content.Server.StationEvents.Components;
+using Content.Shared.Pinpointer;
+using Content.Shared.Power;
using JetBrains.Annotations;
using Robust.Server.GameObjects;
+using Robust.Shared.Map.Components;
+using Robust.Shared.Player;
+using Robust.Shared.Utility;
+using System.Linq;
+using System.Diagnostics.CodeAnalysis;
namespace Content.Server.Power.EntitySystems;
[UsedImplicitly]
-internal sealed class PowerMonitoringConsoleSystem : EntitySystem
+internal sealed partial class PowerMonitoringConsoleSystem : SharedPowerMonitoringConsoleSystem
{
- private float _updateTimer = 0.0f;
+ [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;
- [Dependency] private readonly NodeContainerSystem _nodeContainer = default!;
- [Dependency] private readonly UserInterfaceSystem _userInterfaceSystem = default!;
+ 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, SharedNavMapSystem.ChunkSize);
+
+ if (!allChunks.TryGetValue(chunkOrigin, out var chunk))
+ {
+ chunk = new PowerCableChunk(chunkOrigin);
+ allChunks[chunkOrigin] = chunk;
+ }
+
+ var relative = SharedMapSystem.GetChunkRelative(tile, SharedNavMapSystem.ChunkSize);
+ var flag = SharedNavMapSystem.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 = EntityQueryEnumerator<PowerMonitoringConsoleComponent>();
- while (query.MoveNext(out var uid, out var component))
+ var query = AllEntityQuery<PowerMonitoringConsoleComponent>();
+ while (query.MoveNext(out var ent, out var console))
{
- UpdateUIState(uid, component);
+ if (!_userInterfaceSystem.TryGetUi(ent, PowerMonitoringConsoleUiKey.Key, out var bui))
+ continue;
+
+ foreach (var session in bui.SubscribedSessions)
+ UpdateUIState(ent, console, session);
}
}
}
- public void UpdateUIState(EntityUid target, PowerMonitoringConsoleComponent? pmcComp = null, NodeContainerComponent? ncComp = null)
+ public void UpdateUIState(EntityUid uid, PowerMonitoringConsoleComponent component, ICommonSession session)
{
- if (!Resolve(target, ref pmcComp))
+ if (!_userInterfaceSystem.TryGetUi(uid, PowerMonitoringConsoleUiKey.Key, out var bui))
+ return;
+
+ var consoleXform = Transform(uid);
+
+ if (consoleXform?.GridUid == null)
return;
- if (!Resolve(target, ref ncComp))
+
+ var gridUid = consoleXform.GridUid.Value;
+
+ if (!TryComp<MapGridComponent>(gridUid, out var mapGrid))
return;
- var totalSources = 0.0d;
- var totalLoads = 0.0d;
- var sources = new List<PowerMonitoringConsoleEntry>();
- var loads = new List<PowerMonitoringConsoleEntry>();
- PowerMonitoringConsoleEntry LoadOrSource(Component comp, double rate, bool isBattery)
+ // 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 powerValue = GetPrimaryPowerValues(ent, device, out var powerSupplied, out var powerUsage, out var batteryUsage);
+
+ // Update all running totals
+ totalSources += powerSupplied;
+ totalLoads += powerUsage;
+ totalBatteryUsage += 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, powerValue);
+ 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(bui,
+ new PowerMonitoringConsoleBoundInterfaceState
+ (totalSources,
+ totalBatteryUsage,
+ totalLoads,
+ allEntries.ToArray(),
+ sourcesForFocus.ToArray(),
+ loadsForFocus.ToArray()),
+ session);
+ }
+
+ private double GetPrimaryPowerValues(EntityUid uid, PowerMonitoringDeviceComponent device, out double powerSupplied, out double powerUsage, out double batteryUsage)
+ {
+ var powerValue = 0d;
+ powerSupplied = 0d;
+ powerUsage = 0d;
+ batteryUsage = 0d;
+
+ if (device.Group == PowerMonitoringConsoleGroup.Generator)
{
- var md = MetaData(comp.Owner);
- var prototype = md.EntityPrototype?.ID ?? "";
- return new PowerMonitoringConsoleEntry(md.EntityName, prototype, rate, isBattery);
+ // This covers most power sources
+ if (TryComp<PowerSupplierComponent>(uid, out var supplier))
+ {
+ powerValue = supplier.CurrentSupply;
+ powerSupplied += powerValue;
+ }
+
+ // Edge case: radiation collectors
+ else if (TryComp<BatteryDischargerComponent>(uid, out var _) &&
+ TryComp<PowerNetworkBatteryComponent>(uid, out var battery))
+ {
+ powerValue = battery.NetworkBattery.CurrentSupply;
+ powerSupplied += powerValue;
+ }
}
- // Right, so, here's what needs to be considered here.
- if (!_nodeContainer.TryGetNode<Node>(ncComp, "hv", out var node))
+
+ else if (device.Group == PowerMonitoringConsoleGroup.SMES ||
+ device.Group == PowerMonitoringConsoleGroup.Substation ||
+ device.Group == PowerMonitoringConsoleGroup.APC)
+ {
+
+ if (TryComp<PowerNetworkBatteryComponent>(uid, out var battery))
+ {
+ powerValue = battery.CurrentSupply;
+
+ // Load due to network battery recharging
+ powerUsage += Math.Max(battery.CurrentReceiving - battery.CurrentSupply, 0d);
+
+ // Track battery usage
+ batteryUsage += Math.Max(battery.CurrentSupply - battery.CurrentReceiving, 0d);
+
+ // Records loads attached to APCs
+ if (device.Group == PowerMonitoringConsoleGroup.APC && battery.Enabled)
+ {
+ 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 childPowerValue = GetPrimaryPowerValues(child, childDevice, out var childPowerSupplied, out var childPowerUsage, out var childBatteryUsage);
+
+ powerValue += childPowerValue;
+ powerSupplied += childPowerSupplied;
+ powerUsage += childPowerUsage;
+ batteryUsage += childBatteryUsage;
+ }
+ }
+
+ return powerValue;
+ }
+
+ private void GetSourcesForNode(EntityUid uid, Node node, out List<PowerMonitoringConsoleEntry> sources)
+ {
+ sources = new List<PowerMonitoringConsoleEntry>();
+
+ if (node.NodeGroup is not PowerNet netQ)
return;
- if (node.NodeGroup is PowerNet netQ)
+ var indexedSources = new Dictionary<EntityUid, PowerMonitoringConsoleEntry>();
+ var currentSupply = 0f;
+ var currentDemand = 0f;
+
+ foreach (var powerSupplier in netQ.Suppliers)
{
- foreach (PowerConsumerComponent pcc in netQ.Consumers)
+ var ent = powerSupplier.Owner;
+
+ if (uid == ent)
+ continue;
+
+ currentSupply += powerSupplier.CurrentSupply;
+
+ if (TryComp<PowerMonitoringDeviceComponent>(ent, out var entDevice))
{
- if (!pcc.ShowInMonitor)
+ // 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;
+ }
- loads.Add(LoadOrSource(pcc, pcc.DrawRate, false));
- totalLoads += pcc.DrawRate;
+ indexedSources.Add(ent, new PowerMonitoringConsoleEntry(EntityManager.GetNetEntity(ent), entDevice.Group, powerSupplier.CurrentSupply));
}
- foreach (BatteryChargerComponent pcc in netQ.Chargers)
+ }
+
+ 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))
{
- if (!TryComp(pcc.Owner, out PowerNetworkBatteryComponent? batteryComp))
+ // 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;
}
- var rate = batteryComp.NetworkBattery.CurrentReceiving;
- loads.Add(LoadOrSource(pcc, rate, true));
- totalLoads += rate;
+
+ indexedSources.Add(ent, new PowerMonitoringConsoleEntry(EntityManager.GetNetEntity(ent), entDevice.Group, entBattery.CurrentSupply));
+ }
+ }
+
+ 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;
}
- foreach (PowerSupplierComponent pcc in netQ.Suppliers)
+ }
+
+ // 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);
+ }
+ }
+
+ 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))
{
- var supply = pcc.Enabled
- ? pcc.MaxSupply
- : 0f;
+ // Combine entities represented by an master into a single entry
+ if (entDevice.IsCollectionMasterOrChild && !entDevice.IsCollectionMaster)
+ ent = entDevice.CollectionMaster;
- sources.Add(LoadOrSource(pcc, supply, false));
- totalSources += supply;
+ 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));
}
- foreach (BatteryDischargerComponent pcc in netQ.Dischargers)
+ }
+
+ 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))
{
- if (!TryComp(pcc.Owner, out PowerNetworkBatteryComponent? batteryComp))
+ // 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;
}
- var rate = batteryComp.NetworkBattery.CurrentSupply;
- sources.Add(LoadOrSource(pcc, rate, true));
- totalSources += rate;
+
+ indexedLoads.Add(ent, new PowerMonitoringConsoleEntry(EntityManager.GetNetEntity(ent), entDevice.Group, battery.CurrentReceiving));
}
}
- // Sort
- loads.Sort(CompareLoadOrSources);
- sources.Sort(CompareLoadOrSources);
- // Actually set state.
- if (_userInterfaceSystem.TryGetUi(target, PowerMonitoringConsoleUiKey.Key, out var bui))
- _userInterfaceSystem.SetUiState(bui, new PowerMonitoringConsoleBoundInterfaceState(totalSources, totalLoads, sources.ToArray(), loads.ToArray()));
+ 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);
+ }
}
- private int CompareLoadOrSources(PowerMonitoringConsoleEntry x, PowerMonitoringConsoleEntry y)
+ // 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)
{
- return -x.Size.CompareTo(y.Size);
+ 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, SharedNavMapSystem.ChunkSize);
+
+ if (!allChunks.TryGetValue(chunkOrigin, out var chunk))
+ {
+ chunk = new PowerCableChunk(chunkOrigin);
+ allChunks[chunkOrigin] = chunk;
+ }
+
+ var relative = SharedMapSystem.GetChunkRelative(tile.GridIndices, SharedNavMapSystem.ChunkSize);
+ var flag = SharedNavMapSystem.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, SharedNavMapSystem.ChunkSize);
+
+ if (!component.FocusChunks.TryGetValue(chunkOrigin, out var chunk))
+ {
+ chunk = new PowerCableChunk(chunkOrigin);
+ component.FocusChunks[chunkOrigin] = chunk;
+ }
+
+ var relative = SharedMapSystem.GetChunkRelative(gridIndices, SharedNavMapSystem.ChunkSize);
+ var flag = SharedNavMapSystem.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);
}
}
-using System.Threading;
+using System.Threading;
using Content.Server.StationEvents.Events;
namespace Content.Server.StationEvents.Components;
{
public CancellationTokenSource? AnnounceCancelToken;
+ public EntityUid AffectedStation;
public readonly List<EntityUid> Powered = new();
public readonly List<EntityUid> Unpowered = new();
if (!TryGetRandomStation(out var chosenStation))
return;
+ component.AffectedStation = chosenStation.Value;
+
var query = AllEntityQuery<ApcComponent, TransformComponent>();
while (query.MoveNext(out var apcUid ,out var apc, out var transform))
{
-using Robust.Shared.GameStates;
-
namespace Content.Shared.Pinpointer;
/// <summary>
using Robust.Shared.GameStates;
-using Robust.Shared.Timing;
namespace Content.Shared.Pinpointer;
--- /dev/null
+using Robust.Shared.GameStates;
+using Robust.Shared.Serialization;
+
+namespace Content.Shared.Power;
+
+[RegisterComponent, NetworkedComponent, AutoGenerateComponentState]
+[Access(typeof(SharedPowerMonitoringConsoleSystem))]
+public sealed partial class PowerMonitoringCableNetworksComponent : Component
+{
+ /// <summary>
+ /// A dictionary of the all the nav map chunks that contain anchored power cables
+ /// </summary>
+ [ViewVariables, AutoNetworkedField]
+ public Dictionary<Vector2i, PowerCableChunk> AllChunks = new();
+
+ /// <summary>
+ /// A dictionary of the all the nav map chunks that contain anchored power cables
+ /// that are directly connected to the console's current focus
+ /// </summary>
+ [ViewVariables, AutoNetworkedField]
+ public Dictionary<Vector2i, PowerCableChunk> FocusChunks = new();
+}
+
+[Serializable, NetSerializable]
+public struct PowerCableChunk
+{
+ public readonly Vector2i Origin;
+
+ /// <summary>
+ /// Bitmask dictionary for power cables, 1 for occupied and 0 for empty.
+ /// </summary>
+ public int[] PowerCableData;
+
+ public PowerCableChunk(Vector2i origin)
+ {
+ Origin = origin;
+ PowerCableData = new int[3];
+ }
+}
-using Robust.Shared.Serialization;
+using Robust.Shared.Serialization;
namespace Content.Shared.Power
{
WireCount,
CutWires
}
+
+ [Serializable, NetSerializable]
+ public enum CableType
+ {
+ HighVoltage,
+ MediumVoltage,
+ Apc,
+ }
}
-#nullable enable
+using Robust.Shared.GameStates;
+using Robust.Shared.Map;
using Robust.Shared.Serialization;
namespace Content.Shared.Power;
+/// <summary>
+/// Flags an entity as being a power monitoring console
+/// </summary>
+[RegisterComponent, NetworkedComponent, AutoGenerateComponentState]
+[Access(typeof(SharedPowerMonitoringConsoleSystem), Other = AccessPermissions.ReadExecute)]
+public sealed partial class PowerMonitoringConsoleComponent : Component
+{
+ /// <summary>
+ /// The EntityUid of the device that is the console's current focus
+ /// </summary>
+ /// <remarks>
+ /// Not-networked - set by the console UI
+ /// </remarks>
+ [ViewVariables]
+ public EntityUid? Focus;
+
+ /// <summary>
+ /// The group that the device that is the console's current focus belongs to
+ /// </summary>
+ /// /// <remarks>
+ /// Not-networked - set by the console UI
+ /// </remarks>
+ [ViewVariables]
+ public PowerMonitoringConsoleGroup FocusGroup = PowerMonitoringConsoleGroup.Generator;
+
+ /// <summary>
+ /// A list of flags relating to currently active events of interest to the console.
+ /// E.g., power sinks, power net anomalies
+ /// </summary>
+ [ViewVariables, AutoNetworkedField]
+ public PowerMonitoringFlags Flags = PowerMonitoringFlags.None;
+
+ /// <summary>
+ /// A dictionary containing all the meta data for tracked power monitoring devices
+ /// </summary>
+ [ViewVariables, AutoNetworkedField]
+ public Dictionary<NetEntity, PowerMonitoringDeviceMetaData> PowerMonitoringDeviceMetaData = new();
+}
+
+[Serializable, NetSerializable]
+public struct PowerMonitoringDeviceMetaData
+{
+ public string EntityName;
+ public NetCoordinates Coordinates;
+ public PowerMonitoringConsoleGroup Group;
+ public string SpritePath;
+ public string SpriteState;
+ public NetEntity? CollectionMaster;
+
+ public PowerMonitoringDeviceMetaData(string name, NetCoordinates coordinates, PowerMonitoringConsoleGroup group, string spritePath, string spriteState)
+ {
+ EntityName = name;
+ Coordinates = coordinates;
+ Group = group;
+ SpritePath = spritePath;
+ SpriteState = spriteState;
+ }
+}
+
+/// <summary>
+/// Data from by the server to the client for the power monitoring console UI
+/// </summary>
[Serializable, NetSerializable]
public sealed class PowerMonitoringConsoleBoundInterfaceState : BoundUserInterfaceState
{
public double TotalSources;
+ public double TotalBatteryUsage;
public double TotalLoads;
- public PowerMonitoringConsoleEntry[] Sources;
- public PowerMonitoringConsoleEntry[] Loads;
- public PowerMonitoringConsoleBoundInterfaceState(double totalSources, double totalLoads, PowerMonitoringConsoleEntry[] sources, PowerMonitoringConsoleEntry[] loads)
+ public PowerMonitoringConsoleEntry[] AllEntries;
+ public PowerMonitoringConsoleEntry[] FocusSources;
+ public PowerMonitoringConsoleEntry[] FocusLoads;
+
+ public PowerMonitoringConsoleBoundInterfaceState
+ (double totalSources,
+ double totalBatteryUsage,
+ double totalLoads,
+ PowerMonitoringConsoleEntry[] allEntries,
+ PowerMonitoringConsoleEntry[] focusSources,
+ PowerMonitoringConsoleEntry[] focusLoads)
{
TotalSources = totalSources;
+ TotalBatteryUsage = totalBatteryUsage;
TotalLoads = totalLoads;
- Sources = sources;
- Loads = loads;
+ AllEntries = allEntries;
+ FocusSources = focusSources;
+ FocusLoads = focusLoads;
+ }
+}
+
+/// <summary>
+/// Contains all the data needed to update a single device on the power monitoring UI
+/// </summary>
+[Serializable, NetSerializable]
+public struct PowerMonitoringConsoleEntry
+{
+ public NetEntity NetEntity;
+ public PowerMonitoringConsoleGroup Group;
+ public double PowerValue;
+
+ [NonSerialized] public PowerMonitoringDeviceMetaData? MetaData = null;
+
+ public PowerMonitoringConsoleEntry(NetEntity netEntity, PowerMonitoringConsoleGroup group, double powerValue = 0d)
+ {
+ NetEntity = netEntity;
+ Group = group;
+ PowerValue = powerValue;
}
}
+/// <summary>
+/// Triggers the server to send updated power monitoring console data to the client for the single player session
+/// </summary>
[Serializable, NetSerializable]
-public sealed class PowerMonitoringConsoleEntry
+public sealed class PowerMonitoringConsoleMessage : BoundUserInterfaceMessage
{
- public string NameLocalized;
- public string IconEntityPrototypeId;
- public double Size;
- public bool IsBattery;
- public PowerMonitoringConsoleEntry(string nl, string ipi, double size, bool isBattery)
+ public NetEntity? FocusDevice;
+ public PowerMonitoringConsoleGroup FocusGroup;
+
+ public PowerMonitoringConsoleMessage(NetEntity? focusDevice, PowerMonitoringConsoleGroup focusGroup)
{
- NameLocalized = nl;
- IconEntityPrototypeId = ipi;
- Size = size;
- IsBattery = isBattery;
+ FocusDevice = focusDevice;
+ FocusGroup = focusGroup;
}
}
+/// <summary>
+/// Determines how entities are grouped and color coded on the power monitor
+/// </summary>
+public enum PowerMonitoringConsoleGroup : byte
+{
+ Generator,
+ SMES,
+ Substation,
+ APC,
+ Consumer,
+}
+
+[Flags]
+public enum PowerMonitoringFlags : byte
+{
+ None = 0,
+ RoguePowerConsumer = 1,
+ PowerNetAbnormalities = 2,
+}
+
+/// <summary>
+/// UI key associated with the power monitoring console
+/// </summary>
[Serializable, NetSerializable]
public enum PowerMonitoringConsoleUiKey
{
Key
}
-
--- /dev/null
+using JetBrains.Annotations;
+
+namespace Content.Shared.Power;
+
+[UsedImplicitly]
+public abstract class SharedPowerMonitoringConsoleSystem : EntitySystem
+{
+}
power-monitoring-window-title = Power Monitoring Console
-power-monitoring-window-tab-sources = Sources
-power-monitoring-window-tab-loads = Loads
-power-monitoring-window-total-sources = Total Sources:
-power-monitoring-window-total-loads = Total Loads:
+
+power-monitoring-window-label-sources = Sources
+power-monitoring-window-label-smes = SMES
+power-monitoring-window-label-substation = Substation
+power-monitoring-window-label-apc = APC
+power-monitoring-window-label-misc = Misc
+
+power-monitoring-window-object-array = {$name} array [{$count}]
+
+power-monitoring-window-station-name = [color=white][font size=14]{$stationName}[/font][/color]
+power-monitoring-window-unknown-location = Unknown location
+power-monitoring-window-total-sources = Total generator output
+power-monitoring-window-total-battery-usage = Total battery usage
+power-monitoring-window-total-loads = Total network loads
power-monitoring-window-value = { POWERWATTS($value) }
power-monitoring-window-show-inactive-consumers = Show Inactive Consumers
+power-monitoring-window-show-cable-networks = Toggle cable networks
+power-monitoring-window-show-hv-cable = High voltage
+power-monitoring-window-show-mv-cable = Medium voltage
+power-monitoring-window-show-lv-cable = Low voltage
+
+power-monitoring-window-flavor-left = [user@nanotrasen] $run power_net_query
+power-monitoring-window-flavor-right = v1.3
+power-monitoring-window-rogue-power-consumer = [color=white][font size=14][bold]! WARNING - ROGUE POWER CONSUMING DEVICE DETECTED ![/bold][/font][/color]
+power-monitoring-window-power-net-abnormalities = [color=white][font size=14][bold]CAUTION - ABNORMAL ACTIVITY IN POWER NET[/bold][/font][/color]
- type: Computer
board: PowerComputerCircuitboard
- type: PowerMonitoringConsole
+ - type: PowerMonitoringCableNetworks
- type: NodeContainer
examinable: true
nodes:
!type:CableDeviceNode
nodeGroupID: HVPower
- type: ActivatableUI
+ singleUser: true
key: enum.PowerMonitoringConsoleUiKey.Key
- type: UserInterface
interfaces:
input:
!type:CableDeviceNode
nodeGroupID: HVPower
+ - type: PowerMonitoringDevice
+ group: Generator
+ loadNode: input
+ collectionName: radiationCollector
+ sprite: Structures/Power/Generation/Singularity/collector.rsi
+ state: static
- type: RadiationCollector
chargeModifier: 7500
radiationReactiveGases:
input:
!type:CableDeviceNode
nodeGroupID: HVPower
-# - type: ApcPowerReceiver
-# - type: ExtensionCableReceiver
+ - type: PowerMonitoringDevice
+ group: Generator
+ loadNode: input
+ sprite: Structures/Power/Generation/ame.rsi
+ state: static
- type: PowerSupplier
supplyRate: 0
- type: ContainerContainer
output:
!type:CableDeviceNode
nodeGroupID: HVPower
+ - type: PowerMonitoringDevice
+ group: Generator
+ loadNode: output
+ sprite: Structures/Power/power.rsi
+ state: generator
- type: PowerSupplier
supplyRate: 3000
supplyRampRate: 500
output:
!type:CableDeviceNode
nodeGroupID: HVPower
+ - type: PowerMonitoringDevice
+ group: Generator
+ loadNode: output
+ sprite: Structures/Power/Generation/wallmount_generator.rsi
+ state: static
- type: PowerSupplier
supplyRate: 3000
supplyRampRate: 500
- type: Sprite
sprite: Structures/Power/Generation/rtg.rsi
state: rtg
+ - type: PowerMonitoringDevice
+ sprite: Structures/Power/Generation/rtg.rsi
+ state: rtg
- type: AmbientSound
range: 5
sound:
layers:
- state: rtg_damaged
- state: rtg_glow
+ - type: PowerMonitoringDevice
+ sprite: Structures/Power/Generation/rtg.rsi
+ state: rtg_damaged
- type: RadiationSource # ideally only when opened.
intensity: 2
- type: Destructible
gasType: CarbonDioxide
# 2 moles of gas for every sheet of plasma.
moleRatio: 2
-
+ - type: PowerMonitoringDevice
+ group: Generator
+ loadNodes:
+ - output_hv
+ - output_mv
+ sprite: Structures/Power/Generation/portable_generator.rsi
+ state: portgen0
+
- type: entity
name: S.U.P.E.R.P.A.C.M.A.N.-type portable generator
description: |-
- type: UpgradePowerSupplier
powerSupplyMultiplier: 1.25
scaling: Exponential
-
+ - type: PowerMonitoringDevice
+ group: Generator
+ loadNodes:
+ - output_hv
+ - output_mv
+ sprite: Structures/Power/Generation/portable_generator.rsi
+ state: portgen1
+
- type: entity
name: J.R.P.A.C.M.A.N.-type portable generator
description: |-
nodes:
output:
!type:CableDeviceNode
- nodeGroupID: Apc
+ nodeGroupID: Apc
+ - type: PowerMonitoringDevice
+ group: Generator
+ loadNode: output
+ sprite: Structures/Power/Generation/portable_generator.rsi
+ state: portgen3
- type: PowerSupplier
# No ramping needed on this bugger.
voltage: Apc
output:
!type:CableDeviceNode
nodeGroupID: HVPower
+ - type: PowerMonitoringDevice
+ group: Generator
+ loadNode: output
+ sprite: Structures/Power/Generation/solar_panel.rsi
+ state: static
+ collectionName: SolarPanel
- type: Anchorable
- type: Pullable
- type: Electrified
teg:
!type:TegNodeGenerator
nodeGroupID: Teg
+ - type: PowerMonitoringDevice
+ group: Generator
+ loadNode: output
+ sprite: Structures/Power/Generation/teg.rsi
+ state: static
- type: Rotatable
# Note that only the TEG center is an AtmosDevice.
output:
!type:CableDeviceNode
nodeGroupID: Apc
+ - type: PowerMonitoringDevice
+ group: APC
+ sourceNode: input
+ loadNode: output
+ collectionName: apc
+ sprite: Structures/Power/apc.rsi
+ state: static
- type: BatteryCharger
voltage: Medium
- type: PowerProvider
examinable: true
nodes:
input:
- !type:CableDeviceNode
+ !type:CableTerminalPortNode
nodeGroupID: HVPower
output:
- !type:CableTerminalPortNode
+ !type:CableDeviceNode
nodeGroupID: HVPower
- - type: BatteryCharger
+ - type: PowerMonitoringDevice
+ group: SMES
+ sourceNode: input
+ loadNode: output
+ collectionName: smes
+ sprite: Structures/Power/smes.rsi
+ state: static
+ - type: BatteryDischarger
voltage: High
node: output
- - type: BatteryDischarger
+ - type: BatteryCharger
voltage: High
node: input
- type: PowerNetworkBattery
output:
!type:CableDeviceNode
nodeGroupID: MVPower
+ - type: PowerMonitoringDevice
+ group: Substation
+ sourceNode: input
+ loadNode: output
+ collectionName: substation
+ sprite: Structures/Power/substation.rsi
+ state: substation_static
- type: BatteryCharger
voltage: High
- type: BatteryDischarger
output:
!type:CableDeviceNode
nodeGroupID: MVPower
+ - type: PowerMonitoringDevice
+ group: Substation
+ sourceNode: input
+ loadNode: output
+ sprite: Structures/Power/substation.rsi
+ state: substation_wall_static
- type: BatteryCharger
voltage: High
- type: BatteryDischarger
},
{
"name": "ca_on"
+ },
+ {
+ "name": "static"
},
{
"name": "ca_active",
"y": 32
},
"states": [
- {
+ {
"name": "shield_0"
},
{
},
{
"name": "control"
+ },
+ {
+ "name": "static"
},
{
"name": "control_critical",
-{"version":1,"license":"CC-BY-SA-3.0","copyright":"Taken from https://github.com/discordia-space/CEV-Eris/blob/d1e0161af146835f4fb79d21a6200caa9cc842d0/icons/obj/power.dmi and modified.","size":{"x":32,"y":32},"states":[{"name":"normal","select":[],"flags":{},"directions":8},{"name":"broken","select":[],"flags":{},"directions":1},{"name": "solar_assembly"},{"name": "solar_tracker"}]}
+{
+ "version":1,
+ "license":"CC-BY-SA-3.0",
+ "copyright":"Taken from https://github.com/discordia-space/CEV-Eris/blob/d1e0161af146835f4fb79d21a6200caa9cc842d0/icons/obj/power.dmi and modified.",
+ "size":{"x":32,"y":32},
+ "states":
+ [
+ {
+ "name": "normal",
+ "select": [],
+ "flags": {},
+ "directions": 8
+ },
+ {
+ "name": "broken",
+ "select": [],
+ "flags": {},
+ "directions": 1
+ },
+ {
+ "name": "static"
+ },
+ {
+ "name": "solar_assembly"
+ },
+ {
+ "name": "solar_tracker"
+ }
+ ]
+}
"y": 32
},
"states": [
- {
+ {
"name": "teg",
"directions": 4
+ },
+ {
+ "name": "static"
},
{
"name": "teg-op1",
"states": [
{
"name": "panel"
+ },
+ {
+ "name": "static"
},
{
"name": "on",
"states": [
{
"name": "base"
+ },
+ {
+ "name": "static"
},
{
"name": "broken"
"states": [
{
"name": "smes"
+ },
+ {
+ "name": "static"
},
{
"name": "smes-open"
{
"name": "substation"
},
- {
+ {
+ "name": "substation_static"
+ },
+ {
"name": "substation_wall"
},
+ {
+ "name": "substation_wall_static"
+ },
{
"name": "full"
},