]> git.smokeofanarchy.ru Git - space-station-14.git/commitdiff
Power monitoring console overhaul (#20927)
authorchromiumboy <50505512+chromiumboy@users.noreply.github.com>
Sun, 24 Dec 2023 06:07:41 +0000 (00:07 -0600)
committerGitHub <noreply@github.com>
Sun, 24 Dec 2023 06:07:41 +0000 (17:07 +1100)
* Prototyping whole station wire map

* More prototyping

* Added icons for the different power distributors and toggleable cable displays

* Power cable layouts are now only sent to the client when the power monitor is open

* UI prototyping

* Power monitors can now see the sprites of distant entities, long entity names are truncated

* Updated how network devices are added to the player's PVS

* More feature prototypes

* Added source / load symbols

* Final prototype! Time to actually code it properly...

* Start of code clean up

* Continuing code clean up

* Fixed UI appearance

* Code clean up complete

* Removed unnecessary changes

* Updated how power values are calculated, added UI warnings for power sinks and power net checks

* Updated how power values are calculated again, added support for portable generators

* Removed unnecessary files

* Map beacons start toggled off, console map now works outside the station, fixed substation icon

* Made some of Sloth's requested changes. Power distributors don't blink anymore, unless selected

* Moved a number of static variables in PowerMonitoringHelper to sensible places in the main files. Added a NavMapTrackableComponent so that you can specify how individual entities appear on the navmap

* Updated the colors/positions of HV cables and SMESes to improve contrast

* Fixed SMES color in map legend

* Partially fixed auto-scrolling on device selection, made sublists alphabetical

* Changed how auto-scroll is handled

* Changed the font color of the console warning messages

* Reduced the font size of beacon labels

* Added the station name to the console

* Organized references

* Removed unwanted changes to RobustToolbox

* Fix merge conflict

* Fix merge conflict, maybe

* Fix merge conflict

* Updated outdated reference

* Fixed portable_generator.yml

* Implemented a number of requested changes, move bit masks to a shared component

* Navigate listings via the navmap

* First attempt at improving efficiency

* Second attempt at optimization, entity grouping added for solar panels

* Finished solar panel entity joining

* Finished major revisions, code clean up needed

* Finializing optimizations

* Made requested changes

* Bug fix, removed obsolete code

* Bug fixes

* Bug fixes

* STarted revisions

* Further revisions

* More revision

* Finalizing revisions. Need to make RT PR

* Code tidying

* More code tidying

* Trying to avoid merge conflicts

* Trying to avoid merge conflicts

* Removed use of PVS

* Improving efficiency

* Addressed a bunch of outstanding issues

* Clear old data on console refresh

* UI adjustments

* Made node comparison more robust. More devices can be combined into one entry

* Added missing component 'dirty'

55 files changed:
Content.Client/Medical/CrewMonitoring/CrewMonitoringWindow.xaml
Content.Client/Pinpointer/UI/NavMapControl.cs
Content.Client/Power/PowerMonitoringConsoleBoundUserInterface.cs [new file with mode: 0644]
Content.Client/Power/PowerMonitoringConsoleNavMapControl.cs [new file with mode: 0644]
Content.Client/Power/PowerMonitoringWindow.xaml
Content.Client/Power/PowerMonitoringWindow.xaml.Widgets.cs [new file with mode: 0644]
Content.Client/Power/PowerMonitoringWindow.xaml.cs
Content.Server/Pinpointer/NavMapSystem.cs
Content.Server/Power/Components/CableComponent.cs
Content.Server/Power/Components/CablePlacerComponent.cs
Content.Server/Power/Components/PowerMonitoringConsoleComponent.cs [deleted file]
Content.Server/Power/Components/PowerMonitoringDeviceComponent.cs [new file with mode: 0644]
Content.Server/Power/EntitySystems/CableSystem.cs
Content.Server/Power/EntitySystems/PowerMonitoringConsoleSystem.cs
Content.Server/StationEvents/Components/PowerGridCheckRuleComponent.cs
Content.Server/StationEvents/Events/PowerGridCheckRule.cs
Content.Shared/Pinpointer/NavMapBeaconComponent.cs
Content.Shared/Pinpointer/NavMapComponent.cs
Content.Shared/Power/PowerMonitoringCableNetworksComponent.cs [new file with mode: 0644]
Content.Shared/Power/SharedPower.cs
Content.Shared/Power/SharedPowerMonitoringConsoleComponent.cs
Content.Shared/Power/SharedPowerMonitoringConsoleSystem.cs [new file with mode: 0644]
Resources/Locale/en-US/components/power-monitoring-component.ftl
Resources/Prototypes/Entities/Structures/Machines/Computers/computers.yml
Resources/Prototypes/Entities/Structures/Power/Generation/Singularity/collector.yml
Resources/Prototypes/Entities/Structures/Power/Generation/ame.yml
Resources/Prototypes/Entities/Structures/Power/Generation/generators.yml
Resources/Prototypes/Entities/Structures/Power/Generation/portable_generator.yml
Resources/Prototypes/Entities/Structures/Power/Generation/solar.yml
Resources/Prototypes/Entities/Structures/Power/Generation/teg.yml
Resources/Prototypes/Entities/Structures/Power/apc.yml
Resources/Prototypes/Entities/Structures/Power/smes.yml
Resources/Prototypes/Entities/Structures/Power/substation.yml
Resources/Textures/Interface/NavMap/beveled_hexagon.png [new file with mode: 0644]
Resources/Textures/Interface/NavMap/beveled_square.png [new file with mode: 0644]
Resources/Textures/Interface/NavMap/beveled_triangle.png [new file with mode: 0644]
Resources/Textures/Interface/PowerMonitoring/load_arrow.png [new file with mode: 0644]
Resources/Textures/Interface/PowerMonitoring/source_arrow.png [new file with mode: 0644]
Resources/Textures/Structures/Power/Generation/Singularity/collector.rsi/meta.json
Resources/Textures/Structures/Power/Generation/Singularity/collector.rsi/static.png [new file with mode: 0644]
Resources/Textures/Structures/Power/Generation/ame.rsi/meta.json
Resources/Textures/Structures/Power/Generation/ame.rsi/static.png [new file with mode: 0644]
Resources/Textures/Structures/Power/Generation/solar_panel.rsi/meta.json
Resources/Textures/Structures/Power/Generation/solar_panel.rsi/static.png [new file with mode: 0644]
Resources/Textures/Structures/Power/Generation/teg.rsi/meta.json
Resources/Textures/Structures/Power/Generation/teg.rsi/static.png [new file with mode: 0644]
Resources/Textures/Structures/Power/Generation/wallmount_generator.rsi/meta.json
Resources/Textures/Structures/Power/Generation/wallmount_generator.rsi/static.png [new file with mode: 0644]
Resources/Textures/Structures/Power/apc.rsi/meta.json
Resources/Textures/Structures/Power/apc.rsi/static.png [new file with mode: 0644]
Resources/Textures/Structures/Power/smes.rsi/meta.json
Resources/Textures/Structures/Power/smes.rsi/static.png [new file with mode: 0644]
Resources/Textures/Structures/Power/substation.rsi/meta.json
Resources/Textures/Structures/Power/substation.rsi/substation_static.png [new file with mode: 0644]
Resources/Textures/Structures/Power/substation.rsi/substation_wall_static.png [new file with mode: 0644]

index 80bf5a3f8b91f6605614e1fa752e92ddbd5a1c7b..b4bd76c32831b2a6cee8965a7bf088bbef2bccde 100644 (file)
@@ -2,15 +2,15 @@
                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>
                 
index 438c06f7f2ee752e0ebff283e6df658923d22bc5..a748dc4a6d54a39851902653d03a5f40da635ac8 100644 (file)
@@ -27,10 +27,12 @@ public partial class NavMapControl : MapGridControl
     [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();
@@ -358,6 +360,9 @@ public partial class NavMapControl : MapGridControl
             }
         }
 
+        if (PostWallDrawingAction != null)
+            PostWallDrawingAction.Invoke(handle);
+
         // Beacons
         if (_beacons.Pressed)
         {
@@ -455,7 +460,7 @@ public partial class NavMapControl : MapGridControl
         }
     }
 
-    private void UpdateNavMap()
+    protected virtual void UpdateNavMap()
     {
         if (_navMap == null || _grid == null)
             return;
diff --git a/Content.Client/Power/PowerMonitoringConsoleBoundUserInterface.cs b/Content.Client/Power/PowerMonitoringConsoleBoundUserInterface.cs
new file mode 100644 (file)
index 0000000..dc1dcd0
--- /dev/null
@@ -0,0 +1,52 @@
+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();
+    }
+}
diff --git a/Content.Client/Power/PowerMonitoringConsoleNavMapControl.cs b/Content.Client/Power/PowerMonitoringConsoleNavMapControl.cs
new file mode 100644 (file)
index 0000000..ac27005
--- /dev/null
@@ -0,0 +1,300 @@
+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,
+}
index 826da19d90cd9fa29ed1d7146b58d8fb007d6de9..cdd5b8f13faf79dc55cb6ab6ba5fa2be124785da 100644 (file)
-<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>
diff --git a/Content.Client/Power/PowerMonitoringWindow.xaml.Widgets.cs b/Content.Client/Power/PowerMonitoringWindow.xaml.Widgets.cs
new file mode 100644 (file)
index 0000000..25a586a
--- /dev/null
@@ -0,0 +1,490 @@
+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);
+    }
+}
index 5cc48395a33e7dca863220b96fa2b5654fe056bb..edc0eaa18a851054f4dd76864a22f5be59f06e15 100644 (file)
-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;
     }
 }
-
index a62c5155202e50b41379ca7226cc5eef80423356..a97e664d02880ed8bf6a6942d4755e4be4e1e9be 100644 (file)
@@ -1,4 +1,3 @@
-using Content.Server.Station.Components;
 using Content.Server.Station.Systems;
 using Content.Server.Warps;
 using Content.Shared.Pinpointer;
@@ -32,7 +31,7 @@ public sealed class NavMapSystem : SharedNavMapSystem
         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);
@@ -84,7 +83,7 @@ public sealed class NavMapSystem : SharedNavMapSystem
         RefreshGrid(component, grid);
     }
 
-    private void OnNavMapSplit(EntityUid uid, NavMapComponent component, ref GridSplitEvent args)
+    private void OnNavMapSplit(ref GridSplitEvent args)
     {
         var gridQuery = GetEntityQuery<MapGridComponent>();
 
@@ -94,7 +93,7 @@ public sealed class NavMapSystem : SharedNavMapSystem
             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)
@@ -201,7 +200,6 @@ public sealed class NavMapSystem : SharedNavMapSystem
     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);
 
index 306c9f732e65e4f4ac5af3e3d57137f5d2fbf85c..a2a02a60f681b5db2f9e71945d2f1bf727a90c62 100644 (file)
@@ -1,38 +1,54 @@
 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;
     }
 }
index affe3c77a4859cd61fab02bb52fafad73030630f..d52cfa118a446ddeaa7e409425211e5c0f0dd59f 100644 (file)
@@ -1,5 +1,6 @@
 using Robust.Shared.Prototypes;
 using Robust.Shared.Serialization.TypeSerializers.Implementations.Custom.Prototype;
+using Content.Shared.Power;
 
 namespace Content.Server.Power.Components
 {
diff --git a/Content.Server/Power/Components/PowerMonitoringConsoleComponent.cs b/Content.Server/Power/Components/PowerMonitoringConsoleComponent.cs
deleted file mode 100644 (file)
index 7bf8a3b..0000000
+++ /dev/null
@@ -1,7 +0,0 @@
-namespace Content.Server.Power.Components;
-
-[RegisterComponent]
-public sealed partial class PowerMonitoringConsoleComponent : Component
-{
-}
-
diff --git a/Content.Server/Power/Components/PowerMonitoringDeviceComponent.cs b/Content.Server/Power/Components/PowerMonitoringDeviceComponent.cs
new file mode 100644 (file)
index 0000000..c8f90d9
--- /dev/null
@@ -0,0 +1,89 @@
+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;
+}
index a5c9591d9ad64ea7166f33ba362dfcac344646d9..dd478753be3f3bb42c5a9b2faea2bb34205edde0 100644 (file)
@@ -8,6 +8,7 @@ using Content.Shared.Interaction;
 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;
 
@@ -21,6 +22,7 @@ public sealed partial class CableSystem : EntitySystem
     [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()
     {
@@ -47,17 +49,24 @@ public sealed partial class CableSystem : EntitySystem
         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?
 
index 6bf4e69334b9606cd29418e5466c7436a4656ae1..107d09c89806fdf609c1faa851d0f7582aacfebd 100644 (file)
-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);
     }
 }
index 509c5a8ecf7345b16f6201f4f47a9d36be528c0c..98c60346c7f5dd4ddb54f132b31768625508b25d 100644 (file)
@@ -1,4 +1,4 @@
-using System.Threading;
+using System.Threading;
 using Content.Server.StationEvents.Events;
 
 namespace Content.Server.StationEvents.Components;
@@ -8,6 +8,7 @@ public sealed partial class PowerGridCheckRuleComponent : Component
 {
     public CancellationTokenSource? AnnounceCancelToken;
 
+    public EntityUid AffectedStation;
     public readonly List<EntityUid> Powered = new();
     public readonly List<EntityUid> Unpowered = new();
 
index fdd3e30d437ad5e5e07c3aee9f8afb2f8a023d3c..5503438df8adc441f3764e44fbdf358f4ae4059c 100644 (file)
@@ -24,6 +24,8 @@ namespace Content.Server.StationEvents.Events
             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))
             {
index b9cb8d4488b83a2e7fb18e44153f087ebbd39f56..2415f92f581c157dc615ea7ecfc1b31ca6e29d3f 100644 (file)
@@ -1,5 +1,3 @@
-using Robust.Shared.GameStates;
-
 namespace Content.Shared.Pinpointer;
 
 /// <summary>
index 86a0beef18a55b13f2aa12ba7e8f9624ff125c5e..57f2d004d5573b0ba166648ef2e5f522085012fc 100644 (file)
@@ -1,5 +1,4 @@
 using Robust.Shared.GameStates;
-using Robust.Shared.Timing;
 
 namespace Content.Shared.Pinpointer;
 
diff --git a/Content.Shared/Power/PowerMonitoringCableNetworksComponent.cs b/Content.Shared/Power/PowerMonitoringCableNetworksComponent.cs
new file mode 100644 (file)
index 0000000..75ac886
--- /dev/null
@@ -0,0 +1,39 @@
+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];
+    }
+}
index 5dd366fd6855c43ed5b772d2dd01727eef0186dd..da88198825a4d6b26b983cbb081cb4ee19a6fb09 100644 (file)
@@ -1,4 +1,4 @@
-using Robust.Shared.Serialization;
+using Robust.Shared.Serialization;
 
 namespace Content.Shared.Power
 {
@@ -23,4 +23,12 @@ namespace Content.Shared.Power
         WireCount,
         CutWires
     }
+
+    [Serializable, NetSerializable]
+    public enum CableType
+    {
+        HighVoltage,
+        MediumVoltage,
+        Apc,
+    }
 }
index 9fdedc5a385e08309ac8dddd4151b824f9ed30c6..4d404f209cd75fa8e6fb49f99eed9f48812d3e1c 100644 (file)
-#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
 }
-
diff --git a/Content.Shared/Power/SharedPowerMonitoringConsoleSystem.cs b/Content.Shared/Power/SharedPowerMonitoringConsoleSystem.cs
new file mode 100644 (file)
index 0000000..dc4af23
--- /dev/null
@@ -0,0 +1,8 @@
+using JetBrains.Annotations;
+
+namespace Content.Shared.Power;
+
+[UsedImplicitly]
+public abstract class SharedPowerMonitoringConsoleSystem : EntitySystem
+{
+}
index ade9e2d93321241d6f265bcf7895116d89811617..e84c09e60d5bcd3d5d077b673cefe04082111179 100644 (file)
@@ -1,8 +1,27 @@
 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]
index 24e828bf847337dea9e9777335c115bcccb43c5c..51bf12f8ef54b1907f275e1f1cef10de30829007 100644 (file)
   - 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:
index acd1b80fb4fc69dfc9c8c505a73c78a4ab3085c4..e43c80400e49c0bd96ba972c0535b8be7bcacfbb 100644 (file)
       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:
index a356bfe285a5eec1798dc0fc9d6116adc1003b9c..d12301b3f30d6e36d0c40421459db6aaff4a4ada 100644 (file)
       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
index 65571f80ce5c9cb7906e086ed1d0100bd6380feb..ad227956a7d85b857301f2ab0c2d5a74d822cb2c 100644 (file)
       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
index c49497b774073f7e7f9d1201c0bf501d9adf4484..74fa0b2531b9332c68a9c258eab849249b11cce9 100644 (file)
       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
index 7fc240f2d42537a0a3ce8d2a33af4fba94a337e4..750bdadf069c10b0bfb4d006462116e9f865f67d 100644 (file)
       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
index 723a1480de1b2e9e70b59a96b430cd376fbc3fcc..af482b2dbbe17fbb0d0db51cb4a69dd41a3de343 100644 (file)
         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.
index 35a8f0f704908ffed20d655458cf91db67bac27b..fb477c810c8d1b1180da68ec4f5d6b1454039487 100644 (file)
       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
index 92451918c7bf27a3779dbd12348772a626528af8..4a3b8e5d98ee2c5ef8d2d21dc0c44eaad567821a 100644 (file)
       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
index bd3dcc4b8cac6f5d1aa758187595de79a0156e83..fe6936d4113dad1da69329675eb71cac766242ab 100644 (file)
       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
diff --git a/Resources/Textures/Interface/NavMap/beveled_hexagon.png b/Resources/Textures/Interface/NavMap/beveled_hexagon.png
new file mode 100644 (file)
index 0000000..dc69a73
Binary files /dev/null and b/Resources/Textures/Interface/NavMap/beveled_hexagon.png differ
diff --git a/Resources/Textures/Interface/NavMap/beveled_square.png b/Resources/Textures/Interface/NavMap/beveled_square.png
new file mode 100644 (file)
index 0000000..5efa178
Binary files /dev/null and b/Resources/Textures/Interface/NavMap/beveled_square.png differ
diff --git a/Resources/Textures/Interface/NavMap/beveled_triangle.png b/Resources/Textures/Interface/NavMap/beveled_triangle.png
new file mode 100644 (file)
index 0000000..d0e9072
Binary files /dev/null and b/Resources/Textures/Interface/NavMap/beveled_triangle.png differ
diff --git a/Resources/Textures/Interface/PowerMonitoring/load_arrow.png b/Resources/Textures/Interface/PowerMonitoring/load_arrow.png
new file mode 100644 (file)
index 0000000..fb112fe
Binary files /dev/null and b/Resources/Textures/Interface/PowerMonitoring/load_arrow.png differ
diff --git a/Resources/Textures/Interface/PowerMonitoring/source_arrow.png b/Resources/Textures/Interface/PowerMonitoring/source_arrow.png
new file mode 100644 (file)
index 0000000..afe30cb
Binary files /dev/null and b/Resources/Textures/Interface/PowerMonitoring/source_arrow.png differ
index f111c8a64adb973bc47e30ac6157b775ae09c78c..1da3a0a73362a22f8decf4e555ad7e936e144340 100644 (file)
@@ -12,6 +12,9 @@
     },
     {
       "name": "ca_on"
+    },
+       {
+      "name": "static"
     },
     {
       "name": "ca_active",
diff --git a/Resources/Textures/Structures/Power/Generation/Singularity/collector.rsi/static.png b/Resources/Textures/Structures/Power/Generation/Singularity/collector.rsi/static.png
new file mode 100644 (file)
index 0000000..6097982
Binary files /dev/null and b/Resources/Textures/Structures/Power/Generation/Singularity/collector.rsi/static.png differ
index fd88680d9186150412e06441ee115870a8345b65..8f1a2b7a31114576eed62a67db93c343b241f6f9 100644 (file)
@@ -7,7 +7,7 @@
     "y": 32
   },
   "states": [
-    {
+       {
       "name": "shield_0"
     },
     {
@@ -86,6 +86,9 @@
     },
     {
       "name": "control"
+    },
+       {
+      "name": "static"
     },
     {
       "name": "control_critical",
diff --git a/Resources/Textures/Structures/Power/Generation/ame.rsi/static.png b/Resources/Textures/Structures/Power/Generation/ame.rsi/static.png
new file mode 100644 (file)
index 0000000..de0e6db
Binary files /dev/null and b/Resources/Textures/Structures/Power/Generation/ame.rsi/static.png differ
index 49c9b279ad77588661734686a2d0979a64b151ef..0046666656022fd42ade5a3afae675f4a71ac46e 100644 (file)
@@ -1 +1,30 @@
-{"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"
+               }
+       ]
+}
diff --git a/Resources/Textures/Structures/Power/Generation/solar_panel.rsi/static.png b/Resources/Textures/Structures/Power/Generation/solar_panel.rsi/static.png
new file mode 100644 (file)
index 0000000..455a061
Binary files /dev/null and b/Resources/Textures/Structures/Power/Generation/solar_panel.rsi/static.png differ
index f0c0b58f05e76915fab334db882b9b0481b00af6..b14f15b5791445e09b158d85fbac6ad4068535aa 100644 (file)
@@ -7,9 +7,12 @@
     "y": 32
   },
   "states": [
-    {
+       {
       "name": "teg",
       "directions": 4
+    },
+       {
+      "name": "static"
     },
     {
       "name": "teg-op1",
diff --git a/Resources/Textures/Structures/Power/Generation/teg.rsi/static.png b/Resources/Textures/Structures/Power/Generation/teg.rsi/static.png
new file mode 100644 (file)
index 0000000..f0c9303
Binary files /dev/null and b/Resources/Textures/Structures/Power/Generation/teg.rsi/static.png differ
index 75571ab84451063f3809c1631c5a50ee30190c9e..df1f633a18e5fe8b10f77c4b920541212547c623 100644 (file)
@@ -9,6 +9,9 @@
   "states": [
     {
       "name": "panel"
+    },
+       {
+      "name": "static"
     },
     {
       "name": "on",
diff --git a/Resources/Textures/Structures/Power/Generation/wallmount_generator.rsi/static.png b/Resources/Textures/Structures/Power/Generation/wallmount_generator.rsi/static.png
new file mode 100644 (file)
index 0000000..fa674a8
Binary files /dev/null and b/Resources/Textures/Structures/Power/Generation/wallmount_generator.rsi/static.png differ
index bcc12b2e5583ee33f56faae5ae91b018e8a79b1d..82f78ccb875d61dcec133761b34e3f4a4b8b8a1b 100644 (file)
@@ -9,6 +9,9 @@
   "states": [
     {
       "name": "base"
+    },
+       {
+      "name": "static"
     },
     {
       "name": "broken"
diff --git a/Resources/Textures/Structures/Power/apc.rsi/static.png b/Resources/Textures/Structures/Power/apc.rsi/static.png
new file mode 100644 (file)
index 0000000..43089d0
Binary files /dev/null and b/Resources/Textures/Structures/Power/apc.rsi/static.png differ
index e841421f7c4b1804de209d7cd74009c31052ca65..2ca8d1fb2262d808f0c18d58e084794b9096b70f 100644 (file)
@@ -9,6 +9,9 @@
   "states": [
     {
       "name": "smes"
+    },
+               {
+      "name": "static"
     },
     {
       "name": "smes-open"
diff --git a/Resources/Textures/Structures/Power/smes.rsi/static.png b/Resources/Textures/Structures/Power/smes.rsi/static.png
new file mode 100644 (file)
index 0000000..3e52b6c
Binary files /dev/null and b/Resources/Textures/Structures/Power/smes.rsi/static.png differ
index 83d05c9ff0323f0df55f15c648fed172575cb31c..a2ade57116e19e63c4ad493c3fcfb9df2aa4e1ed 100644 (file)
     {
       "name": "substation"
     },
-    {
+       {
+         "name": "substation_static"
+       },
+    {          
       "name": "substation_wall"
     },
+       {
+         "name": "substation_wall_static"
+       },
     {
       "name": "full"
     },
diff --git a/Resources/Textures/Structures/Power/substation.rsi/substation_static.png b/Resources/Textures/Structures/Power/substation.rsi/substation_static.png
new file mode 100644 (file)
index 0000000..2a1de1a
Binary files /dev/null and b/Resources/Textures/Structures/Power/substation.rsi/substation_static.png differ
diff --git a/Resources/Textures/Structures/Power/substation.rsi/substation_wall_static.png b/Resources/Textures/Structures/Power/substation.rsi/substation_wall_static.png
new file mode 100644 (file)
index 0000000..ee24ff4
Binary files /dev/null and b/Resources/Textures/Structures/Power/substation.rsi/substation_wall_static.png differ