From 1de682e23fd3cb832fa3328a7e6c4bd4b1a858f7 Mon Sep 17 00:00:00 2001 From: chromiumboy <50505512+chromiumboy@users.noreply.github.com> Date: Sun, 24 Dec 2023 00:07:41 -0600 Subject: [PATCH] Power monitoring console overhaul (#20927) * 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' --- .../CrewMonitoring/CrewMonitoringWindow.xaml | 8 +- Content.Client/Pinpointer/UI/NavMapControl.cs | 7 +- ...owerMonitoringConsoleBoundUserInterface.cs | 52 + .../PowerMonitoringConsoleNavMapControl.cs | 300 +++++ .../Power/PowerMonitoringWindow.xaml | 133 ++- .../PowerMonitoringWindow.xaml.Widgets.cs | 490 ++++++++ .../Power/PowerMonitoringWindow.xaml.cs | 337 +++++- Content.Server/Pinpointer/NavMapSystem.cs | 8 +- .../Power/Components/CableComponent.cs | 70 +- .../Power/Components/CablePlacerComponent.cs | 1 + .../PowerMonitoringConsoleComponent.cs | 7 - .../PowerMonitoringDeviceComponent.cs | 89 ++ .../Power/EntitySystems/CableSystem.cs | 13 +- .../PowerMonitoringConsoleSystem.cs | 1017 +++++++++++++++-- .../Components/PowerGridCheckRuleComponent.cs | 3 +- .../Events/PowerGridCheckRule.cs | 2 + .../Pinpointer/NavMapBeaconComponent.cs | 2 - Content.Shared/Pinpointer/NavMapComponent.cs | 1 - .../PowerMonitoringCableNetworksComponent.cs | 39 + Content.Shared/Power/SharedPower.cs | 10 +- .../SharedPowerMonitoringConsoleComponent.cs | 150 ++- .../SharedPowerMonitoringConsoleSystem.cs | 8 + .../components/power-monitoring-component.ftl | 27 +- .../Machines/Computers/computers.yml | 2 + .../Generation/Singularity/collector.yml | 6 + .../Structures/Power/Generation/ame.yml | 7 +- .../Power/Generation/generators.yml | 16 + .../Power/Generation/portable_generator.yml | 25 +- .../Structures/Power/Generation/solar.yml | 6 + .../Structures/Power/Generation/teg.yml | 5 + .../Entities/Structures/Power/apc.yml | 7 + .../Entities/Structures/Power/smes.yml | 17 +- .../Entities/Structures/Power/substation.yml | 13 + .../Interface/NavMap/beveled_hexagon.png | Bin 0 -> 1406 bytes .../Interface/NavMap/beveled_square.png | Bin 0 -> 904 bytes .../Interface/NavMap/beveled_triangle.png | Bin 0 -> 1268 bytes .../Interface/PowerMonitoring/load_arrow.png | Bin 0 -> 826 bytes .../PowerMonitoring/source_arrow.png | Bin 0 -> 774 bytes .../Singularity/collector.rsi/meta.json | 3 + .../Singularity/collector.rsi/static.png | Bin 0 -> 664 bytes .../Power/Generation/ame.rsi/meta.json | 5 +- .../Power/Generation/ame.rsi/static.png | Bin 0 -> 618 bytes .../Generation/solar_panel.rsi/meta.json | 31 +- .../Generation/solar_panel.rsi/static.png | Bin 0 -> 223 bytes .../Power/Generation/teg.rsi/meta.json | 5 +- .../Power/Generation/teg.rsi/static.png | Bin 0 -> 1050 bytes .../wallmount_generator.rsi/meta.json | 3 + .../wallmount_generator.rsi/static.png | Bin 0 -> 335 bytes .../Structures/Power/apc.rsi/meta.json | 3 + .../Structures/Power/apc.rsi/static.png | Bin 0 -> 384 bytes .../Structures/Power/smes.rsi/meta.json | 3 + .../Structures/Power/smes.rsi/static.png | Bin 0 -> 884 bytes .../Structures/Power/substation.rsi/meta.json | 8 +- .../substation.rsi/substation_static.png | Bin 0 -> 1289 bytes .../substation.rsi/substation_wall_static.png | Bin 0 -> 444 bytes 55 files changed, 2719 insertions(+), 220 deletions(-) create mode 100644 Content.Client/Power/PowerMonitoringConsoleBoundUserInterface.cs create mode 100644 Content.Client/Power/PowerMonitoringConsoleNavMapControl.cs create mode 100644 Content.Client/Power/PowerMonitoringWindow.xaml.Widgets.cs delete mode 100644 Content.Server/Power/Components/PowerMonitoringConsoleComponent.cs create mode 100644 Content.Server/Power/Components/PowerMonitoringDeviceComponent.cs create mode 100644 Content.Shared/Power/PowerMonitoringCableNetworksComponent.cs create mode 100644 Content.Shared/Power/SharedPowerMonitoringConsoleSystem.cs create mode 100644 Resources/Textures/Interface/NavMap/beveled_hexagon.png create mode 100644 Resources/Textures/Interface/NavMap/beveled_square.png create mode 100644 Resources/Textures/Interface/NavMap/beveled_triangle.png create mode 100644 Resources/Textures/Interface/PowerMonitoring/load_arrow.png create mode 100644 Resources/Textures/Interface/PowerMonitoring/source_arrow.png create mode 100644 Resources/Textures/Structures/Power/Generation/Singularity/collector.rsi/static.png create mode 100644 Resources/Textures/Structures/Power/Generation/ame.rsi/static.png create mode 100644 Resources/Textures/Structures/Power/Generation/solar_panel.rsi/static.png create mode 100644 Resources/Textures/Structures/Power/Generation/teg.rsi/static.png create mode 100644 Resources/Textures/Structures/Power/Generation/wallmount_generator.rsi/static.png create mode 100644 Resources/Textures/Structures/Power/apc.rsi/static.png create mode 100644 Resources/Textures/Structures/Power/smes.rsi/static.png create mode 100644 Resources/Textures/Structures/Power/substation.rsi/substation_static.png create mode 100644 Resources/Textures/Structures/Power/substation.rsi/substation_wall_static.png diff --git a/Content.Client/Medical/CrewMonitoring/CrewMonitoringWindow.xaml b/Content.Client/Medical/CrewMonitoring/CrewMonitoringWindow.xaml index 80bf5a3f8b..b4bd76c328 100644 --- a/Content.Client/Medical/CrewMonitoring/CrewMonitoringWindow.xaml +++ b/Content.Client/Medical/CrewMonitoring/CrewMonitoringWindow.xaml @@ -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"> - + - diff --git a/Content.Client/Pinpointer/UI/NavMapControl.cs b/Content.Client/Pinpointer/UI/NavMapControl.cs index 438c06f7f2..a748dc4a6d 100644 --- a/Content.Client/Pinpointer/UI/NavMapControl.cs +++ b/Content.Client/Pinpointer/UI/NavMapControl.cs @@ -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? TrackedEntitySelectedAction; + public event Action? PostWallDrawingAction; // Tracked data public Dictionary 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 index 0000000000..dc1dcd03ef --- /dev/null +++ b/Content.Client/Power/PowerMonitoringConsoleBoundUserInterface.cs @@ -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(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 index 0000000000..ac2700564d --- /dev/null +++ b/Content.Client/Power/PowerMonitoringConsoleNavMapControl.cs @@ -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 _sRGBLookUp = new Dictionary(); + + public PowerMonitoringCableNetworksComponent? PowerMonitoringCableNetworks; + public List HiddenLineGroups = new(); + public Dictionary>? PowerCableNetwork; + public Dictionary>? 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(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> 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[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[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>? GetDecodedPowerCableChunks(Dictionary? chunks, MapGridComponent? grid) + { + if (chunks == null || grid == null) + return null; + + var decodedOutput = new Dictionary>(); + + foreach ((var chunkOrigin, var chunk) in chunks) + { + var list = new List(); + + 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, +} diff --git a/Content.Client/Power/PowerMonitoringWindow.xaml b/Content.Client/Power/PowerMonitoringWindow.xaml index 826da19d90..cdd5b8f13f 100644 --- a/Content.Client/Power/PowerMonitoringWindow.xaml +++ b/Content.Client/Power/PowerMonitoringWindow.xaml @@ -1,20 +1,117 @@ - - - - - - - - - - - - + + + + + + + + + + + + + + + + + + + + - + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + - + diff --git a/Content.Client/Power/PowerMonitoringWindow.xaml.Widgets.cs b/Content.Client/Power/PowerMonitoringWindow.xaml.Widgets.cs new file mode 100644 index 0000000000..25a586a75d --- /dev/null +++ b/Content.Client/Power/PowerMonitoringWindow.xaml.Widgets.cs @@ -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(_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); + } +} diff --git a/Content.Client/Power/PowerMonitoringWindow.xaml.cs b/Content.Client/Power/PowerMonitoringWindow.xaml.cs index 5cc48395a3..edc0eaa18a 100644 --- a/Content.Client/Power/PowerMonitoringWindow.xaml.cs +++ b/Content.Client/Power/PowerMonitoringWindow.xaml.cs @@ -1,84 +1,317 @@ -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 +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; - public PowerMonitoringWindow() + private const float BlinkFrequency = 1f; + + private EntityUid? _owner; + private NetEntity? _focusEntity; + + public event Action? SendPowerMonitoringConsoleMessageAction; + + private Dictionary _groupBlips = new() + { + { 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 PowerMonitoringWindow(PowerMonitoringConsoleBoundUserInterface userInterface, EntityUid? owner) { RobustXamlLoader.Load(this); - SetSize = MinSize = new Vector2(300, 450); - IoCManager.InjectDependencies(this); - _spriteSystem = IoCManager.Resolve().System(); - MasterTabContainer.SetTabTitle(0, Loc.GetString("power-monitoring-window-tab-sources")); - MasterTabContainer.SetTabTitle(1, Loc.GetString("power-monitoring-window-tab-loads")); + _entManager = IoCManager.Resolve(); + _gameTiming = IoCManager.Resolve(); + + _spriteSystem = _entManager.System(); + _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(owner, out var xform)) + { + NavMap.MapUid = xform.GridUid; + + // Assign station name + if (_entManager.TryGetComponent(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; + } + + // 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 UpdateState(PowerMonitoringConsoleBoundInterfaceState scc) + private void OnTabChanged(int tab) { - UpdateList(TotalSourcesNum, scc.TotalSources, SourcesList, scc.Sources); - var loads = scc.Loads; - if (!ShowInactiveConsumersCheckBox.Pressed) - { - // 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(); - } - UpdateList(TotalLoadsNum, scc.TotalLoads, LoadsList, loads); + SendPowerMonitoringConsoleMessageAction?.Invoke(_focusEntity, (PowerMonitoringConsoleGroup) tab); } - public void UpdateList(Label number, double numberVal, ItemList list, PowerMonitoringConsoleEntry[] listVal) + private void OnShowCableToggled(PowerMonitoringConsoleLineGroup lineGroup) { - number.Text = Loc.GetString("power-monitoring-window-value", ("value", numberVal)); - // This magic is important to prevent scrolling issues. - while (list.Count > listVal.Length) + 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(_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(); + + 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 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(_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 +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; } } - diff --git a/Content.Server/Pinpointer/NavMapSystem.cs b/Content.Server/Pinpointer/NavMapSystem.cs index a62c515520..a97e664d02 100644 --- a/Content.Server/Pinpointer/NavMapSystem.cs +++ b/Content.Server/Pinpointer/NavMapSystem.cs @@ -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(OnStationInit); SubscribeLocalEvent(OnNavMapStartup); SubscribeLocalEvent(OnGetState); - SubscribeLocalEvent(OnNavMapSplit); + SubscribeLocalEvent(OnNavMapSplit); SubscribeLocalEvent(OnNavMapBeaconStartup); SubscribeLocalEvent(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(); @@ -94,7 +93,7 @@ public sealed class NavMapSystem : SharedNavMapSystem RefreshGrid(newComp, gridQuery.GetComponent(grid)); } - RefreshGrid(component, gridQuery.GetComponent(uid)); + RefreshGrid(Comp(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); diff --git a/Content.Server/Power/Components/CableComponent.cs b/Content.Server/Power/Components/CableComponent.cs index 306c9f732e..a2a02a60f6 100644 --- a/Content.Server/Power/Components/CableComponent.cs +++ b/Content.Server/Power/Components/CableComponent.cs @@ -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; + +/// +/// Allows the attached entity to be destroyed by a cutting tool, dropping a piece of cable. +/// +[RegisterComponent] +[Access(typeof(CableSystem))] +public sealed partial class CableComponent : Component { + [DataField("cableDroppedOnCutPrototype", customTypeSerializer: typeof(PrototypeIdSerializer))] + public string CableDroppedOnCutPrototype = "CableHVStack1"; + + [DataField("cuttingQuality", customTypeSerializer:typeof(PrototypeIdSerializer))] + public string CuttingQuality = "Cutting"; + /// - /// Allows the attached entity to be destroyed by a cutting tool, dropping a piece of cable. + /// Checked by to determine if there is + /// already a cable of a type on a tile. /// - [RegisterComponent] - [Access(typeof(CableSystem))] - public sealed partial class CableComponent : Component + [DataField("cableType")] + public CableType CableType = CableType.HighVoltage; + + [DataField("cuttingDelay")] + public float CuttingDelay = 1f; +} + +/// +/// Event to be raised when a cable is anchored / unanchored +/// +[ByRefEvent] +public readonly struct CableAnchorStateChangedEvent +{ + public readonly TransformComponent Transform; + public EntityUid Entity => Transform.Owner; + public bool Anchored => Transform.Anchored; + + /// + /// If true, the entity is being detached to null-space + /// + public readonly bool Detaching; + + public CableAnchorStateChangedEvent(TransformComponent transform, bool detaching = false) { - [DataField("cableDroppedOnCutPrototype", customTypeSerializer: typeof(PrototypeIdSerializer))] - public string CableDroppedOnCutPrototype = "CableHVStack1"; - - [DataField("cuttingQuality", customTypeSerializer:typeof(PrototypeIdSerializer))] - public string CuttingQuality = "Cutting"; - - /// - /// Checked by to determine if there is - /// already a cable of a type on a tile. - /// - [DataField("cableType")] - public CableType CableType = CableType.HighVoltage; - - [DataField("cuttingDelay")] - public float CuttingDelay = 1f; - } - - public enum CableType - { - HighVoltage, - MediumVoltage, - Apc, + Detaching = detaching; + Transform = transform; } } diff --git a/Content.Server/Power/Components/CablePlacerComponent.cs b/Content.Server/Power/Components/CablePlacerComponent.cs index affe3c77a4..d52cfa118a 100644 --- a/Content.Server/Power/Components/CablePlacerComponent.cs +++ b/Content.Server/Power/Components/CablePlacerComponent.cs @@ -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 index 7bf8a3b414..0000000000 --- a/Content.Server/Power/Components/PowerMonitoringConsoleComponent.cs +++ /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 index 0000000000..c8f90d9ad5 --- /dev/null +++ b/Content.Server/Power/Components/PowerMonitoringDeviceComponent.cs @@ -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; + +/// +/// Used to flag any entities that should appear on a power monitoring console +/// +[RegisterComponent, Access(typeof(PowerMonitoringConsoleSystem))] +public sealed partial class PowerMonitoringDeviceComponent : Component +{ + /// + /// Name of the node that this device draws its power from (see ) + /// + [DataField("sourceNode"), ViewVariables] + public string SourceNode = string.Empty; + + /// + /// Name of the node that this device distributes power to (see ) + /// + [DataField("loadNode"), ViewVariables] + public string LoadNode = string.Empty; + + /// + /// Names of the nodes that this device can potentially distributes power to (see ) + /// + [DataField("loadNodes"), ViewVariables] + public List? LoadNodes; + + /// + /// This entity will be grouped with entities that have the same collection name + /// + [DataField("collectionName"), ViewVariables] + public string CollectionName = string.Empty; + + [ViewVariables] + public BaseNodeGroup? NodeGroup = null; + + /// + /// Indicates whether the entity is/should be part of a collection + /// + public bool IsCollectionMasterOrChild { get { return CollectionName != string.Empty; } } + + /// + /// Specifies the uid of the master that represents this entity + /// + /// + /// Used when grouping multiple entities into a single power monitoring console entry + /// + [ViewVariables] + public EntityUid CollectionMaster; + + /// + /// Indicates if this entity represents a group of entities + /// + /// + /// Used when grouping multiple entities into a single power monitoring console entry + /// + public bool IsCollectionMaster { get { return Owner == CollectionMaster; } } + + /// + /// A list of other entities that are to be represented by this entity + /// + /// /// + /// Used when grouping multiple entities into a single power monitoring console entry + /// + [ViewVariables] + public Dictionary ChildDevices = new(); + + /// + /// Path to the .rsi folder + /// + [DataField("sprite"), ViewVariables] + public string SpritePath = string.Empty; + + /// + /// The .rsi state + /// + [DataField("state"), ViewVariables] + public string SpriteState = string.Empty; + + /// + /// Determines what power monitoring group this entity should belong to + /// + [DataField("group", required: true), ViewVariables] + public PowerMonitoringConsoleGroup Group; +} diff --git a/Content.Server/Power/EntitySystems/CableSystem.cs b/Content.Server/Power/EntitySystems/CableSystem.cs index a5c9591d9a..dd478753be 100644 --- a/Content.Server/Power/EntitySystems/CableSystem.cs +++ b/Content.Server/Power/EntitySystems/CableSystem.cs @@ -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? diff --git a/Content.Server/Power/EntitySystems/PowerMonitoringConsoleSystem.cs b/Content.Server/Power/EntitySystems/PowerMonitoringConsoleSystem.cs index 6bf4e69334..107d09c898 100644 --- a/Content.Server/Power/EntitySystems/PowerMonitoringConsoleSystem.cs +++ b/Content.Server/Power/EntitySystems/PowerMonitoringConsoleSystem.cs @@ -1,110 +1,999 @@ -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; - private const float UpdateTime = 1.0f; - - [Dependency] private readonly NodeContainerSystem _nodeContainer = default!; [Dependency] private readonly UserInterfaceSystem _userInterfaceSystem = default!; + [Dependency] private readonly SharedMapSystem _sharedMapSystem = default!; + + // Note: this data does not need to be saved + private Dictionary> _gridPowerCableChunks = new(); + private float _updateTimer = 1.0f; + + private const float UpdateTime = 1.0f; + private const float RoguePowerConsumerThreshold = 100000; + + public override void Initialize() + { + base.Initialize(); + + // Console events + SubscribeLocalEvent(OnConsoleInit); + SubscribeLocalEvent(OnConsoleParentChanged); + SubscribeLocalEvent(OnCableNetworksInit); + SubscribeLocalEvent(OnCableNetworksParentChanged); + + // UI events + SubscribeLocalEvent(OnPowerMonitoringConsoleMessage); + SubscribeLocalEvent(OnBoundUIOpened); + + // Grid events + SubscribeLocalEvent(OnGridSplit); + SubscribeLocalEvent(OnCableAnchorStateChanged); + SubscribeLocalEvent(OnDeviceAnchoringChanged); + SubscribeLocalEvent(OnNodeGroupRebuilt); + + // Game rule events + SubscribeLocalEvent(OnPowerGridCheckStarted); + SubscribeLocalEvent(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(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(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(grid, out var map)) + continue; + + RefreshPowerCableGrid(grid, map); + } + + // Update power monitoring consoles that stand upon an updated grid + var query = AllEntityQuery(); + 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(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(); + 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(); + 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(); + 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(ev.RuleEntity, out var rule)) + return; + + var query = AllEntityQuery(); + while (query.MoveNext(out var uid, out var console, out var xform)) + { + if (CompOrNull(xform.GridUid)?.Station == rule.AffectedStation) + { + console.Flags |= PowerMonitoringFlags.PowerNetAbnormalities; + Dirty(uid, console); + } + } + } + + private void OnPowerGridCheckEnded(ref GameRuleEndedEvent ev) + { + if (!TryComp(ev.RuleEntity, out var rule)) + return; + + var query = AllEntityQuery(); + while (query.MoveNext(out var uid, out var console, out var xform)) + { + if (CompOrNull(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(); - while (query.MoveNext(out var uid, out var component)) + var query = AllEntityQuery(); + 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)) - return; - if (!Resolve(target, ref ncComp)) + if (!_userInterfaceSystem.TryGetUi(uid, PowerMonitoringConsoleUiKey.Key, out var bui)) return; - var totalSources = 0.0d; - var totalLoads = 0.0d; - var sources = new List(); - var loads = new List(); - PowerMonitoringConsoleEntry LoadOrSource(Component comp, double rate, bool isBattery) + var consoleXform = Transform(uid); + + if (consoleXform?.GridUid == null) + return; + + var gridUid = consoleXform.GridUid.Value; + + if (!TryComp(gridUid, out var mapGrid)) + return; + + // The grid must have a NavMapComponent to visualize the map in the UI + EnsureComp(gridUid); + + // Initializing data to be send to the client + var totalSources = 0d; + var totalBatteryUsage = 0d; + var totalLoads = 0d; + var allEntries = new List(); + var sourcesForFocus = new List(); + var loadsForFocus = new List(); + 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(); + while (powerConsumerQuery.MoveNext(out var ent, out var powerConsumer, out var xform)) { - var md = MetaData(comp.Owner); - var prototype = md.EntityPrototype?.ID ?? ""; - return new PowerMonitoringConsoleEntry(md.EntityName, prototype, rate, isBattery); + if (xform.Anchored == false || xform.GridUid != gridUid) + continue; + + if (TryComp(ent, out var device)) + continue; + + // Flag an alert if power consumption is ridiculous + if (powerConsumer.ReceivedPower >= RoguePowerConsumerThreshold) + component.Flags |= PowerMonitoringFlags.RoguePowerConsumer; + + totalLoads += powerConsumer.DrawRate; } - // Right, so, here's what needs to be considered here. - if (!_nodeContainer.TryGetNode(ncComp, "hv", out var node)) - return; - if (node.NodeGroup is PowerNet netQ) + if (component.Flags != flags) + Dirty(uid, component); + + // Loop over all tracked devices + var powerMonitoringDeviceQuery = AllEntityQuery(); + while (powerMonitoringDeviceQuery.MoveNext(out var ent, out var device, out var xform)) { - foreach (PowerConsumerComponent pcc in netQ.Consumers) - { - if (!pcc.ShowInMonitor) - continue; + // Ignore joint, non-master entities + if (device.IsCollectionMasterOrChild && !device.IsCollectionMaster) + continue; - loads.Add(LoadOrSource(pcc, pcc.DrawRate, false)); - totalLoads += pcc.DrawRate; - } - foreach (BatteryChargerComponent pcc in netQ.Chargers) - { - if (!TryComp(pcc.Owner, out PowerNetworkBatteryComponent? batteryComp)) - { - continue; - } - var rate = batteryComp.NetworkBattery.CurrentReceiving; - loads.Add(LoadOrSource(pcc, rate, true)); - totalLoads += rate; - } - foreach (PowerSupplierComponent pcc in netQ.Suppliers) - { - var supply = pcc.Enabled - ? pcc.MaxSupply - : 0f; + if (xform.Anchored == false || xform.GridUid != gridUid) + continue; - sources.Add(LoadOrSource(pcc, supply, false)); - totalSources += supply; - } - foreach (BatteryDischargerComponent pcc in netQ.Dischargers) + // 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(component.Focus, out var nodeContainer) && + TryComp(component.Focus, out var device)) { - if (!TryComp(pcc.Owner, out PowerNetworkBatteryComponent? batteryComp)) + // 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) { - continue; + 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(uid, out var cableNetworks) && + cableNetworks.FocusChunks.Count == 0) + { + var reachableEntities = new List(); + + 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); } - var rate = batteryComp.NetworkBattery.CurrentSupply; - sources.Add(LoadOrSource(pcc, rate, true)); - totalSources += rate; } } - // 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())); + // Set the UI state + _userInterfaceSystem.SetUiState(bui, + new PowerMonitoringConsoleBoundInterfaceState + (totalSources, + totalBatteryUsage, + totalLoads, + allEntries.ToArray(), + sourcesForFocus.ToArray(), + loadsForFocus.ToArray()), + session); } - private int CompareLoadOrSources(PowerMonitoringConsoleEntry x, PowerMonitoringConsoleEntry y) + private double GetPrimaryPowerValues(EntityUid uid, PowerMonitoringDeviceComponent device, out double powerSupplied, out double powerUsage, out double batteryUsage) { - return -x.Size.CompareTo(y.Size); + var powerValue = 0d; + powerSupplied = 0d; + powerUsage = 0d; + batteryUsage = 0d; + + if (device.Group == PowerMonitoringConsoleGroup.Generator) + { + // This covers most power sources + if (TryComp(uid, out var supplier)) + { + powerValue = supplier.CurrentSupply; + powerSupplied += powerValue; + } + + // Edge case: radiation collectors + else if (TryComp(uid, out var _) && + TryComp(uid, out var battery)) + { + powerValue = battery.NetworkBattery.CurrentSupply; + powerSupplied += powerValue; + } + } + + else if (device.Group == PowerMonitoringConsoleGroup.SMES || + device.Group == PowerMonitoringConsoleGroup.Substation || + device.Group == PowerMonitoringConsoleGroup.APC) + { + + if (TryComp(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 sources) + { + sources = new List(); + + if (node.NodeGroup is not PowerNet netQ) + return; + + var indexedSources = new Dictionary(); + var currentSupply = 0f; + var currentDemand = 0f; + + foreach (var powerSupplier in netQ.Suppliers) + { + var ent = powerSupplier.Owner; + + if (uid == ent) + continue; + + currentSupply += powerSupplier.CurrentSupply; + + if (TryComp(ent, out var entDevice)) + { + // Combine entities represented by an master into a single entry + if (entDevice.IsCollectionMasterOrChild && !entDevice.IsCollectionMaster) + ent = entDevice.CollectionMaster; + + if (indexedSources.TryGetValue(ent, out var entry)) + { + entry.PowerValue += powerSupplier.CurrentSupply; + indexedSources[ent] = entry; + + continue; + } + + indexedSources.Add(ent, new PowerMonitoringConsoleEntry(EntityManager.GetNetEntity(ent), entDevice.Group, powerSupplier.CurrentSupply)); + } + } + + foreach (var batteryDischarger in netQ.Dischargers) + { + var ent = batteryDischarger.Owner; + + if (uid == ent) + continue; + + if (!TryComp(ent, out var entBattery)) + continue; + + currentSupply += entBattery.CurrentSupply; + + if (TryComp(ent, out var entDevice)) + { + // Combine entities represented by an master into a single entry + if (entDevice.IsCollectionMasterOrChild && !entDevice.IsCollectionMaster) + ent = entDevice.CollectionMaster; + + if (indexedSources.TryGetValue(ent, out var entry)) + { + entry.PowerValue += entBattery.CurrentSupply; + indexedSources[ent] = entry; + + continue; + } + + indexedSources.Add(ent, new PowerMonitoringConsoleEntry(EntityManager.GetNetEntity(ent), entDevice.Group, entBattery.CurrentSupply)); + } + } + + 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(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(uid, out var battery)) + return; + + var powerUsage = battery.CurrentReceiving; + + if (TryComp(uid, out var device) && device.IsCollectionMaster) + { + foreach ((var child, var _) in device.ChildDevices) + { + if (TryComp(child, out var childBattery)) + powerUsage += childBattery.CurrentReceiving; + } + } + + // Update the power value for each source based on the fraction of power the entity is actually draining from each + var powerFraction = Math.Min(powerUsage / currentSupply, 1f) * Math.Min(currentSupply / currentDemand, 1f); + + for (int i = 0; i < sources.Count; i++) + { + var entry = sources[i]; + sources[i] = new PowerMonitoringConsoleEntry(entry.NetEntity, entry.Group, entry.PowerValue * powerFraction); + } + } + + private void GetLoadsForNode(EntityUid uid, Node node, out List loads, List? children = null) + { + loads = new List(); + + if (node.NodeGroup is not PowerNet netQ) + return; + + var indexedLoads = new Dictionary(); + var currentDemand = 0f; + + foreach (var powerConsumer in netQ.Consumers) + { + var ent = powerConsumer.Owner; + + if (uid == ent) + continue; + + currentDemand += powerConsumer.ReceivedPower; + + if (TryComp(ent, out var entDevice)) + { + // Combine entities represented by an master into a single entry + if (entDevice.IsCollectionMasterOrChild && !entDevice.IsCollectionMaster) + ent = entDevice.CollectionMaster; + + if (indexedLoads.TryGetValue(ent, out var entry)) + { + entry.PowerValue += powerConsumer.ReceivedPower; + indexedLoads[ent] = entry; + + continue; + } + + indexedLoads.Add(ent, new PowerMonitoringConsoleEntry(EntityManager.GetNetEntity(ent), entDevice.Group, powerConsumer.ReceivedPower)); + } + } + + foreach (var batteryCharger in netQ.Chargers) + { + var ent = batteryCharger.Owner; + + if (uid == ent) + continue; + + if (!TryComp(ent, out var battery)) + continue; + + currentDemand += battery.CurrentReceiving; + + if (TryComp(ent, out var entDevice)) + { + // Combine entities represented by an master into a single entry + if (entDevice.IsCollectionMasterOrChild && !entDevice.IsCollectionMaster) + ent = entDevice.CollectionMaster; + + if (indexedLoads.TryGetValue(ent, out var entry)) + { + entry.PowerValue += battery.CurrentReceiving; + indexedLoads[ent] = entry; + + continue; + } + + indexedLoads.Add(ent, new PowerMonitoringConsoleEntry(EntityManager.GetNetEntity(ent), entDevice.Group, battery.CurrentReceiving)); + } + } + + 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(uid, out var entBattery)) + supplying = entBattery.CurrentSupply; + + else if (TryComp(uid, out var entSupplier)) + supplying = entSupplier.CurrentSupply; + + if (TryComp(uid, out var device) && device.IsCollectionMaster) + { + foreach ((var child, var _) in device.ChildDevices) + { + if (TryComp(child, out var childBattery)) + supplying += childBattery.CurrentSupply; + + else if (TryComp(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); + } + } + + // Designates a supplied entity as a 'collection master'. Other entities which share this + // entities collection name and are attached on the same load network are assigned this entity + // as the master that represents them on the console UI. This way you can have one device + // represent multiple connected devices + private void AssignEntityAsCollectionMaster + (EntityUid uid, + PowerMonitoringDeviceComponent? device = null, + TransformComponent? xform = null, + NodeContainerComponent? nodeContainer = null) + { + if (!Resolve(uid, ref device, ref nodeContainer, ref xform, false)) + return; + + // If the device is not attached to a network, exit + var nodeName = device.SourceNode == string.Empty ? device.LoadNode : device.SourceNode; + + if (!nodeContainer.Nodes.TryGetValue(nodeName, out var node) || + node.ReachableNodes.Count == 0) + { + // Make a child the new master of the collection if necessary + if (device.ChildDevices.TryFirstOrNull(out var kvp)) + { + var newMaster = kvp.Value.Key; + var newMasterDevice = kvp.Value.Value; + + newMasterDevice.CollectionMaster = newMaster; + newMasterDevice.ChildDevices.Clear(); + + foreach ((var child, var childDevice) in device.ChildDevices) + { + newMasterDevice.ChildDevices.Add(child, childDevice); + + childDevice.CollectionMaster = newMaster; + UpdateCollectionChildMetaData(child, newMaster); + } + + UpdateCollectionMasterMetaData(newMaster, newMasterDevice.ChildDevices.Count); + } + + device.CollectionMaster = uid; + device.ChildDevices.Clear(); + UpdateCollectionMasterMetaData(uid, 0); + + return; + } + + // Check to see if the device has a valid existing master + if (!device.IsCollectionMaster && + device.CollectionMaster.IsValid() && + TryComp(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(); + 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(); + 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(); + 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 RefreshPowerCableGrid(EntityUid gridUid, MapGridComponent grid) + { + // Clears all chunks for the associated grid + var allChunks = new Dictionary(); + _gridPowerCableChunks[gridUid] = allChunks; + + // Adds all power cables to the grid + var query = AllEntityQuery(); + 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 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(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(); + 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(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); } } diff --git a/Content.Server/StationEvents/Components/PowerGridCheckRuleComponent.cs b/Content.Server/StationEvents/Components/PowerGridCheckRuleComponent.cs index 509c5a8ecf..98c60346c7 100644 --- a/Content.Server/StationEvents/Components/PowerGridCheckRuleComponent.cs +++ b/Content.Server/StationEvents/Components/PowerGridCheckRuleComponent.cs @@ -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 Powered = new(); public readonly List Unpowered = new(); diff --git a/Content.Server/StationEvents/Events/PowerGridCheckRule.cs b/Content.Server/StationEvents/Events/PowerGridCheckRule.cs index fdd3e30d43..5503438df8 100644 --- a/Content.Server/StationEvents/Events/PowerGridCheckRule.cs +++ b/Content.Server/StationEvents/Events/PowerGridCheckRule.cs @@ -24,6 +24,8 @@ namespace Content.Server.StationEvents.Events if (!TryGetRandomStation(out var chosenStation)) return; + component.AffectedStation = chosenStation.Value; + var query = AllEntityQuery(); while (query.MoveNext(out var apcUid ,out var apc, out var transform)) { diff --git a/Content.Shared/Pinpointer/NavMapBeaconComponent.cs b/Content.Shared/Pinpointer/NavMapBeaconComponent.cs index b9cb8d4488..2415f92f58 100644 --- a/Content.Shared/Pinpointer/NavMapBeaconComponent.cs +++ b/Content.Shared/Pinpointer/NavMapBeaconComponent.cs @@ -1,5 +1,3 @@ -using Robust.Shared.GameStates; - namespace Content.Shared.Pinpointer; /// diff --git a/Content.Shared/Pinpointer/NavMapComponent.cs b/Content.Shared/Pinpointer/NavMapComponent.cs index 86a0beef18..57f2d004d5 100644 --- a/Content.Shared/Pinpointer/NavMapComponent.cs +++ b/Content.Shared/Pinpointer/NavMapComponent.cs @@ -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 index 0000000000..75ac8869ed --- /dev/null +++ b/Content.Shared/Power/PowerMonitoringCableNetworksComponent.cs @@ -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 +{ + /// + /// A dictionary of the all the nav map chunks that contain anchored power cables + /// + [ViewVariables, AutoNetworkedField] + public Dictionary AllChunks = new(); + + /// + /// A dictionary of the all the nav map chunks that contain anchored power cables + /// that are directly connected to the console's current focus + /// + [ViewVariables, AutoNetworkedField] + public Dictionary FocusChunks = new(); +} + +[Serializable, NetSerializable] +public struct PowerCableChunk +{ + public readonly Vector2i Origin; + + /// + /// Bitmask dictionary for power cables, 1 for occupied and 0 for empty. + /// + public int[] PowerCableData; + + public PowerCableChunk(Vector2i origin) + { + Origin = origin; + PowerCableData = new int[3]; + } +} diff --git a/Content.Shared/Power/SharedPower.cs b/Content.Shared/Power/SharedPower.cs index 5dd366fd68..da88198825 100644 --- a/Content.Shared/Power/SharedPower.cs +++ b/Content.Shared/Power/SharedPower.cs @@ -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, + } } diff --git a/Content.Shared/Power/SharedPowerMonitoringConsoleComponent.cs b/Content.Shared/Power/SharedPowerMonitoringConsoleComponent.cs index 9fdedc5a38..4d404f209c 100644 --- a/Content.Shared/Power/SharedPowerMonitoringConsoleComponent.cs +++ b/Content.Shared/Power/SharedPowerMonitoringConsoleComponent.cs @@ -1,43 +1,159 @@ -#nullable enable +using Robust.Shared.GameStates; +using Robust.Shared.Map; using Robust.Shared.Serialization; namespace Content.Shared.Power; +/// +/// Flags an entity as being a power monitoring console +/// +[RegisterComponent, NetworkedComponent, AutoGenerateComponentState] +[Access(typeof(SharedPowerMonitoringConsoleSystem), Other = AccessPermissions.ReadExecute)] +public sealed partial class PowerMonitoringConsoleComponent : Component +{ + /// + /// The EntityUid of the device that is the console's current focus + /// + /// + /// Not-networked - set by the console UI + /// + [ViewVariables] + public EntityUid? Focus; + + /// + /// The group that the device that is the console's current focus belongs to + /// + /// /// + /// Not-networked - set by the console UI + /// + [ViewVariables] + public PowerMonitoringConsoleGroup FocusGroup = PowerMonitoringConsoleGroup.Generator; + + /// + /// A list of flags relating to currently active events of interest to the console. + /// E.g., power sinks, power net anomalies + /// + [ViewVariables, AutoNetworkedField] + public PowerMonitoringFlags Flags = PowerMonitoringFlags.None; + + /// + /// A dictionary containing all the meta data for tracked power monitoring devices + /// + [ViewVariables, AutoNetworkedField] + public Dictionary 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; + } +} + +/// +/// Data from by the server to the client for the power monitoring console UI +/// [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; } } +/// +/// Contains all the data needed to update a single device on the power monitoring UI +/// [Serializable, NetSerializable] -public sealed class PowerMonitoringConsoleEntry +public struct PowerMonitoringConsoleEntry { - public string NameLocalized; - public string IconEntityPrototypeId; - public double Size; - public bool IsBattery; - public PowerMonitoringConsoleEntry(string nl, string ipi, double size, bool isBattery) + public NetEntity NetEntity; + public PowerMonitoringConsoleGroup Group; + public double PowerValue; + + [NonSerialized] public PowerMonitoringDeviceMetaData? MetaData = null; + + public PowerMonitoringConsoleEntry(NetEntity netEntity, PowerMonitoringConsoleGroup group, double powerValue = 0d) { - NameLocalized = nl; - IconEntityPrototypeId = ipi; - Size = size; - IsBattery = isBattery; + NetEntity = netEntity; + Group = group; + PowerValue = powerValue; } } +/// +/// Triggers the server to send updated power monitoring console data to the client for the single player session +/// +[Serializable, NetSerializable] +public sealed class PowerMonitoringConsoleMessage : BoundUserInterfaceMessage +{ + public NetEntity? FocusDevice; + public PowerMonitoringConsoleGroup FocusGroup; + + public PowerMonitoringConsoleMessage(NetEntity? focusDevice, PowerMonitoringConsoleGroup focusGroup) + { + FocusDevice = focusDevice; + FocusGroup = focusGroup; + } +} + +/// +/// Determines how entities are grouped and color coded on the power monitor +/// +public enum PowerMonitoringConsoleGroup : byte +{ + Generator, + SMES, + Substation, + APC, + Consumer, +} + +[Flags] +public enum PowerMonitoringFlags : byte +{ + None = 0, + RoguePowerConsumer = 1, + PowerNetAbnormalities = 2, +} + +/// +/// UI key associated with the power monitoring console +/// [Serializable, NetSerializable] public enum PowerMonitoringConsoleUiKey { Key } - diff --git a/Content.Shared/Power/SharedPowerMonitoringConsoleSystem.cs b/Content.Shared/Power/SharedPowerMonitoringConsoleSystem.cs new file mode 100644 index 0000000000..dc4af23c23 --- /dev/null +++ b/Content.Shared/Power/SharedPowerMonitoringConsoleSystem.cs @@ -0,0 +1,8 @@ +using JetBrains.Annotations; + +namespace Content.Shared.Power; + +[UsedImplicitly] +public abstract class SharedPowerMonitoringConsoleSystem : EntitySystem +{ +} diff --git a/Resources/Locale/en-US/components/power-monitoring-component.ftl b/Resources/Locale/en-US/components/power-monitoring-component.ftl index ade9e2d933..e84c09e60d 100644 --- a/Resources/Locale/en-US/components/power-monitoring-component.ftl +++ b/Resources/Locale/en-US/components/power-monitoring-component.ftl @@ -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] diff --git a/Resources/Prototypes/Entities/Structures/Machines/Computers/computers.yml b/Resources/Prototypes/Entities/Structures/Machines/Computers/computers.yml index 24e828bf84..51bf12f8ef 100644 --- a/Resources/Prototypes/Entities/Structures/Machines/Computers/computers.yml +++ b/Resources/Prototypes/Entities/Structures/Machines/Computers/computers.yml @@ -239,6 +239,7 @@ - type: Computer board: PowerComputerCircuitboard - type: PowerMonitoringConsole + - type: PowerMonitoringCableNetworks - type: NodeContainer examinable: true nodes: @@ -246,6 +247,7 @@ !type:CableDeviceNode nodeGroupID: HVPower - type: ActivatableUI + singleUser: true key: enum.PowerMonitoringConsoleUiKey.Key - type: UserInterface interfaces: diff --git a/Resources/Prototypes/Entities/Structures/Power/Generation/Singularity/collector.yml b/Resources/Prototypes/Entities/Structures/Power/Generation/Singularity/collector.yml index acd1b80fb4..e43c80400e 100644 --- a/Resources/Prototypes/Entities/Structures/Power/Generation/Singularity/collector.yml +++ b/Resources/Prototypes/Entities/Structures/Power/Generation/Singularity/collector.yml @@ -50,6 +50,12 @@ 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: diff --git a/Resources/Prototypes/Entities/Structures/Power/Generation/ame.yml b/Resources/Prototypes/Entities/Structures/Power/Generation/ame.yml index a356bfe285..d12301b3f3 100644 --- a/Resources/Prototypes/Entities/Structures/Power/Generation/ame.yml +++ b/Resources/Prototypes/Entities/Structures/Power/Generation/ame.yml @@ -78,8 +78,11 @@ 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 diff --git a/Resources/Prototypes/Entities/Structures/Power/Generation/generators.yml b/Resources/Prototypes/Entities/Structures/Power/Generation/generators.yml index 65571f80ce..ad227956a7 100644 --- a/Resources/Prototypes/Entities/Structures/Power/Generation/generators.yml +++ b/Resources/Prototypes/Entities/Structures/Power/Generation/generators.yml @@ -40,6 +40,11 @@ output: !type:CableDeviceNode nodeGroupID: HVPower + - type: PowerMonitoringDevice + group: Generator + loadNode: output + sprite: Structures/Power/power.rsi + state: generator - type: PowerSupplier supplyRate: 3000 supplyRampRate: 500 @@ -124,6 +129,11 @@ 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 @@ -229,6 +239,9 @@ - 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: @@ -272,6 +285,9 @@ 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 diff --git a/Resources/Prototypes/Entities/Structures/Power/Generation/portable_generator.yml b/Resources/Prototypes/Entities/Structures/Power/Generation/portable_generator.yml index c49497b774..74fa0b2531 100644 --- a/Resources/Prototypes/Entities/Structures/Power/Generation/portable_generator.yml +++ b/Resources/Prototypes/Entities/Structures/Power/Generation/portable_generator.yml @@ -170,7 +170,14 @@ 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: |- @@ -219,7 +226,14 @@ - 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: |- @@ -293,7 +307,12 @@ 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 diff --git a/Resources/Prototypes/Entities/Structures/Power/Generation/solar.yml b/Resources/Prototypes/Entities/Structures/Power/Generation/solar.yml index 7fc240f2d4..750bdadf06 100644 --- a/Resources/Prototypes/Entities/Structures/Power/Generation/solar.yml +++ b/Resources/Prototypes/Entities/Structures/Power/Generation/solar.yml @@ -36,6 +36,12 @@ 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 diff --git a/Resources/Prototypes/Entities/Structures/Power/Generation/teg.yml b/Resources/Prototypes/Entities/Structures/Power/Generation/teg.yml index 723a1480de..af482b2dbb 100644 --- a/Resources/Prototypes/Entities/Structures/Power/Generation/teg.yml +++ b/Resources/Prototypes/Entities/Structures/Power/Generation/teg.yml @@ -51,6 +51,11 @@ 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. diff --git a/Resources/Prototypes/Entities/Structures/Power/apc.yml b/Resources/Prototypes/Entities/Structures/Power/apc.yml index 35a8f0f704..fb477c810c 100644 --- a/Resources/Prototypes/Entities/Structures/Power/apc.yml +++ b/Resources/Prototypes/Entities/Structures/Power/apc.yml @@ -65,6 +65,13 @@ 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 diff --git a/Resources/Prototypes/Entities/Structures/Power/smes.yml b/Resources/Prototypes/Entities/Structures/Power/smes.yml index 92451918c7..4a3b8e5d98 100644 --- a/Resources/Prototypes/Entities/Structures/Power/smes.yml +++ b/Resources/Prototypes/Entities/Structures/Power/smes.yml @@ -43,15 +43,22 @@ examinable: true nodes: input: - !type:CableDeviceNode - nodeGroupID: HVPower - output: !type:CableTerminalPortNode nodeGroupID: HVPower - - type: BatteryCharger + output: + !type:CableDeviceNode + nodeGroupID: HVPower + - 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 diff --git a/Resources/Prototypes/Entities/Structures/Power/substation.yml b/Resources/Prototypes/Entities/Structures/Power/substation.yml index bd3dcc4b8c..fe6936d411 100644 --- a/Resources/Prototypes/Entities/Structures/Power/substation.yml +++ b/Resources/Prototypes/Entities/Structures/Power/substation.yml @@ -40,6 +40,13 @@ 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 @@ -166,6 +173,12 @@ 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 index 0000000000000000000000000000000000000000..dc69a73480ef259111622b5a481390672d6772d4 GIT binary patch literal 1406 zcmV-^1%djBP)EX>4Tx04R}tkv&MmKpe$i(@Iq;3U(0bkfAzRC@SJ8RV;#q(pG5I!Q|2}Xws0R zxHt-~1qVMCs}3&Cx;nTDg5U>;tBaGOirkx9)H2Z_aE2g@DIN`^{2O&nHKjq-)8 z%L?Z$&T6H`TKD8H4CJ+yG}mc{5XTY{NJ4~+8p^1^LWEY06cZ`hk9F~nI{qZNWO9|k z$gzMbR7j2={11M2YZj&^-K0Pa=y|d2k1@c%3pDGt{e5iP%@e@;3|wh#f3*S3ev)2q zYvChca2vR|Zfo)$aJd7FJn51lIg*#AP$&TJXY@@uVE7j3UvqnF?c?+T$WT|yH^9Lm zFkYnW^)B!3?dXiU0t5;G000SaNLh0L z04^f{04^f|c%?sf00007bV*G`2j>YK1S}>ze@#OG000?uMObu0Z*6U5Zgc=ca%Ew3 zWn>_CX>@2HM@dakSAh-}000A3Nkl&NKR>_sf7!oEC4~@zPEJl}Yin!%S_%*md&s_w8iG;^rfH&HuYY~r6ub@K zc3A)zW2jcEux-1!y}kYJ9{t)Y}cQ4|nDAPhspMF;?*ITJ zq8fnT0o)p=tq&;3!%nAj+XC=0fEUL-R~Tb3O%s($1-5NNDRrw}udgs73d^kgt9(I9 zi9F9Cgn;8Xz5#%q)O784M4snJ(-dJC8pcE0l~00wQ4~m$1g`7i=;%lt930$dMD&c- z`T^(s1=p68{iptVMx=SY$SQ53=RJk{-XKm5-ynwy*3)mnd-rYR|X1GF&N+PFN4wodtJT_E zTwJ_6=={(HL@D)A6h&_shB0HBCh|N7=Nws8uYZOI+D2i})b{2Iyolk~N$x*r7 z-QB%IL|-YTUUpps<+ev41{W6vbf>1S@N6Yd=5z(H$0l0}|Lw(aUByV*mgE M07*qoM6N<$g4Bn6=l}o! literal 0 HcmV?d00001 diff --git a/Resources/Textures/Interface/NavMap/beveled_square.png b/Resources/Textures/Interface/NavMap/beveled_square.png new file mode 100644 index 0000000000000000000000000000000000000000..5efa1789324b412a5a89c494cc851afa2dcb77bb GIT binary patch literal 904 zcmV;319$w1P)EX>4Tx04R}tkv&MmKpe$i(@Iq;3U(0bkfAzRC@SJ8RV;#q(pG5I!Q|2}Xws0R zxHt-~1qVMCs}3&Cx;nTDg5U>;tBaGOirkx9)H2Z_aE2g@DIN`^{2O&nHKjq-)8 z%L?Z$&T6H`TKD8H4CJ+yG}mc{5XTY{NJ4~+8p^1^LWEY06cZ`hk9F~nI{qZNWO9|k z$gzMbR7j2={11M2YZj&^-K0Pa=y|d2k1@c%3pDGt{e5iP%@e@;3|wh#f3*S3ev)2q zYvChca2vR|Zfo)$aJd7FJn51lIg*#AP$&TJXY@@uVE7j3UvqnF?c?+T$WT|yH^9Lm zFkYnW^)B!3?dXiU0t5;G000SaNLh0L z04^f{04^f|c%?sf00007bV*G`2j>YK1SL2Zvdd`z000?uMObu0Z*6U5Zgc=ca%Ew3 zWn>_CX>@2HM@dakSAh-}0004BNklb;z6b9hmQ8(t?fUTG4zzuqz3L()e zbm&b`Z@}CMHYAoH6e~RP>^%E*3NnRCRViU0zmy?zr1#m5l4qb#5>?Gr^<7mL`u|y| z>YJ+OX-$(B2k;3Xzw$f_0FTC)0syjP;I)I|0FakuIcaZFz*ARS&CH{7fMFNK>$*l& zRl{?Dt0%??W`?yEHvxnYz|5$s3O8GUrfJ}uyA4295o1IM;WmI4k*0IN_w;}s&;xqF z{Qy+;)D$;k)+=JR<@M3XGbM$Wk*5rK%{y+3NB{EX>4Tx04R}tkv&MmKpe$i(@Iq;3U(0bkfAzRC@SJ8RV;#q(pG5I!Q|2}Xws0R zxHt-~1qVMCs}3&Cx;nTDg5U>;tBaGOirkx9)H2Z_aE2g@DIN`^{2O&nHKjq-)8 z%L?Z$&T6H`TKD8H4CJ+yG}mc{5XTY{NJ4~+8p^1^LWEY06cZ`hk9F~nI{qZNWO9|k z$gzMbR7j2={11M2YZj&^-K0Pa=y|d2k1@c%3pDGt{e5iP%@e@;3|wh#f3*S3ev)2q zYvChca2vR|Zfo)$aJd7FJn51lIg*#AP$&TJXY@@uVE7j3UvqnF?c?+T$WT|yH^9Lm zFkYnW^)B!3?dXiU0t5;G000SaNLh0L z04^f{04^f|c%?sf00007bV*G`2j>YK1TP(?S@)y>000?uMObu0Z*6U5Zgc=ca%Ew3 zWn>_CX>@2HM@dakSAh-}0008ZNkliYj0B?JpkK-7MqQEc= zHJi=8DHIAU%@nKwnCo>wQ52}E3ez-Un&zBs+iU$?tn?AR1>iG)sb1%V5QyU#K@gzX zY{GTja-~vvkk98E!xDIPn{xnQjDax*P19hSCOGF)*=+XpPy~qR9)Pb2Y6VdgAq+$K zz7N-RMWs^7&Ckz&8{UgQJzNhaU8d|r0034R;%a~ zun*wb)x#2T9770^ng}5T!Z5_e#RVFT2F}jT;5g2HTY%#@o1F9IAPDYA=TQ`))oLM% zqHAJ?A<%3#rGVP$>FMURNmweCW_;h@VvIePQ_J%_gkd|9p+TPy& e{{J8IpRiwKZoe!4E-tEX>4Tx04R}tkv&MmKpe$iQ?*j64t5Z6$WWauh>8ds^#*d7t}p^eB0g0X~st?f%Ws^E4huXpY-Cb%#9?Bw*v4`jvy!0_PZCEIRik_% z>$1Xmi?dp;vgSSc3qyHrIn8yNLx^J$2_zvxMim<>z{|sDdEq|pBOn;JI zYiW@qpl=(vxNd3k9&ot>3_R(QAvuztrcfvV?`QN)IiUX*2(G%lHTQA)0A#4Er5oVj z5Ev;^_L|4LyW4yF_e`_DA9Un$sH{n!4gdfE24YJ`L;x}X%>XiU0t5;G000SaNLh0L z04^f{04^f|c%?sf00007bV*G`2j>a~4=oQ9hsLA;000?uMObu0Z*6U5Zgc=ca%Ew3 zWn>_CX>@2HM@dakSAh-}0003INkltxiKh5QWc~G*_SzC@N76F95Ph1usC- zB9S}+9!G-3wr@~VYU>(Aap-Dv4R`0@nw9{)z1hoF>?E6PGW+u-=giJR5^9cHZAZJ# zIfi&~UT0HGfRm&El8_XDEX>4Tx04R}tkv&MmKpe$iQ?*j64t5Z6$WWauh>8ds^#*d7t}p^eB0g0X~st?f%Ws^E4huXpY-Cb%#9?Bw*v4`jvy!0_PZCEIRik_% z>$1Xmi?dp;vgSSc3qyHrIn8yNLx^J$2_zvxMim<>z{|sDdEq|pBOn;JI zYiW@qpl=(vxNd3k9&ot>3_R(QAvuztrcfvV?`QN)IiUX*2(G%lHTQA)0A#4Er5oVj z5Ev;^_L|4LyW4yF_e`_DA9Un$sH{n!4gdfE24YJ`L;x}X%>XiU0t5;G000SaNLh0L z04^f{04^f|c%?sf00007bV*G`2j>a~4=n>4F^Z1>000?uMObu0Z*6U5Zgc=ca%Ew3 zWn>_CX>@2HM@dakSAh-}0002pNklKFAX$309NZ!2~#bkoaOfr9hx!uc8{yp!%_b>46A~`J6d`wdAJ_v3usY&u8 zNhD8E6g5J3zfer>J`ZN@?k!2TH~`73q-Ln+U+aZPE<<$R(|gg~yG74_uTKQzwn+1d zQYg!}tY@74(adJgvJYSqJKg};0f3pELoDJn(fBmaJ+-o<{C5B(l?z~ID`il%1G#b_ zp-!Sc0Jx`F7XVg74vfkkLHqB&;h!gA1yBK00ACC60IjY$R)dTL)Bpeg07*qoM6N<$ Ef>$;`Qvd(} literal 0 HcmV?d00001 diff --git a/Resources/Textures/Structures/Power/Generation/Singularity/collector.rsi/meta.json b/Resources/Textures/Structures/Power/Generation/Singularity/collector.rsi/meta.json index f111c8a64a..1da3a0a733 100644 --- a/Resources/Textures/Structures/Power/Generation/Singularity/collector.rsi/meta.json +++ b/Resources/Textures/Structures/Power/Generation/Singularity/collector.rsi/meta.json @@ -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 index 0000000000000000000000000000000000000000..6097982ee260134d4413f66b77a391f69a499f97 GIT binary patch literal 664 zcmV;J0%!e+P)e&>y?jAUL&L5)( zEPa-k8UP&T5e$VIG-kR5b~3=E0XrFB0`Q*!wW8p6MQNC$`Ajh^3=9rjfF~ZarRxoj z-el{9V1^`O<5{nX9c=zV5J`UWz(;fiwUP&MCl}eVeKPBn(U&R2B zEy_U3Tm$#8plcwo?ZcDFaC<+?HA`hV?EdK@XJ(J=`7Z!~yN_Pr`OGvmnYMbC7aLDH zK4%;b2!ep&;j4!GTU+1tb{dVY!SBDNU$vuGt(Kc>M280lSdYh<=v(>t!Sven7Sz8a y0H7d>D2QT9DyQWTTb<8$h5DPmxw*}?D#E`ZG)$BvCsgPsQ73!(oVn_ zYb3%E6%WG@!{JaFIGs*07z`S(m?fI|$NdjDJHM1B@jMUj-|gWxz5`i=B0HE9L0zkGFVV8+P5Y!frq6oqDjqo?lIXLI} zcs`$3IGs)>-!=!Y2+lc@dGa*q0hCf49(+Q2|63>mKxS)apUr@)D`Qcj;zfuBlnWKd zvzas*r4*z`Kzam}QZ0eDa>lZPij|<0Y8O`vs41j62gO5ZWJLM(y11wf^p214@aM7i z5E=`Ze*Xc_^J)W&PS^yMEtrf?v;sxEO%9<`^k~Ld#v0DZLK4EXP*_EEYczT;E{1T%z4>zxbYv zzrcjA6JNcCJG;^(*x7{%UngGZ1F|GC=oFX5JF>pt1g3oT3jBTCo&gAQ+oFbKTM*^8 zOZ|X$L$V1biWR|bYzK&BxhkT7OoNMmPb zH)crF($ezq_|K5`|4bUg+{UHlKuN}uAirP+hi5m^fSepp7sn8d^T`PdL^*gJp6+gI zTl66^=2YO+4O?{dT>~FI>|A!_P%76dp)`Mf6IC@iz0;=yr!@Fzx&|7hawV!tEGt={ zUFc|@+QshJ9dlu7MAIDUhIiAL*i3>0nE0M#URhw8Y33lBV74Gkhl%0JI<4=q?XN*B$ literal 0 HcmV?d00001 diff --git a/Resources/Textures/Structures/Power/Generation/teg.rsi/meta.json b/Resources/Textures/Structures/Power/Generation/teg.rsi/meta.json index f0c0b58f05..b14f15b579 100644 --- a/Resources/Textures/Structures/Power/Generation/teg.rsi/meta.json +++ b/Resources/Textures/Structures/Power/Generation/teg.rsi/meta.json @@ -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 index 0000000000000000000000000000000000000000..f0c930394476460906b3900a070356073edcac6c GIT binary patch literal 1050 zcmV+#1m*jQP)b69#9kX8|q zz>=*65f(4Dm+Y-4S2|I4kYo`&tkB3LC=PK86|5BOA)RC<$)vh2BrM7W)cII*XXK#n6PeJBK*}JwA|b4>G(#B zSS(gmz+vll=4`!=R&HZgZF1yK}9 zBoeHzulq`s`yEotfV;7TOeTY>s-#jW78Vv-svzHynJrZ57T&y?0(WGYix%ulr4p%B3Q-iv zWHSC!u(`R3uIreFNmvOJ7OQfKpSYaP{S&wV49g-KjiPB9y{?AzjG?NkG^*fMAR3Kg z8ucc+&TvMfQO|QDdsS5tBuQ#|aeI54^Ye3x#UhdHWVju+lGnwgoYo$;;AIaijJ zmg>FWilHb9#bVJzNH7coMNw)hxS89wO;=Y}Qyn)pHhh^64i47V#bU8Gf{wY_8-{_P z>pGgIp(u(cQ%%#*b)B`fHBW%a$;r0TyHD4io*t6PWK(=ek}wPdUDv&1sH!TOrnLoV z$G%Yk$FkWh*=)9k;TgktJdRrh$Dv#;tW%fQpq(~OUg1JKvk*Os9JzO%E#^73*^ANQ?0aFwsxtNuaz{cpPMU$=nG UA+e97RsaA107*qoM6N<$g5mr6=l}o! literal 0 HcmV?d00001 diff --git a/Resources/Textures/Structures/Power/Generation/wallmount_generator.rsi/meta.json b/Resources/Textures/Structures/Power/Generation/wallmount_generator.rsi/meta.json index 75571ab844..df1f633a18 100644 --- a/Resources/Textures/Structures/Power/Generation/wallmount_generator.rsi/meta.json +++ b/Resources/Textures/Structures/Power/Generation/wallmount_generator.rsi/meta.json @@ -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 index 0000000000000000000000000000000000000000..fa674a8960ba7ccfd110644fbb782a7ca314da02 GIT binary patch literal 335 zcmV-V0kHmwP) zu}*|Q6h)6pv`~v_h%xrmv!S!GG}v0nulWhq)E1CTwbUd_YgU?9Y@#tCS*zWw3JEjF zYB8sSdAzx~Z-5IP&mVJOQM>M2&|3evSC-|V-hqwUG))0eN+BXBr8?`1q8Qxs(AKi5 zs+Z)ci*{NEHk@3Xb98kEz{BkwPxEKq z=dXZR_p&+z-|amPPXV}}-8jF7P7Zu5SFl|120UhqQQrY40WP!tH32~o5XW)nT3y%s zaR4C8vM<+3lI(8;V>TQuAc~^@>wt(5hM_+L-hpWaG)=S31iQJw_BZUD2l`Ub`lxVl$B-3vV7jkb$I0oI^gTuHw+96 z3=GXTW)rk5iq(@rSy_hR?%gLC`Y8&5N6%g|u41BOSJUot2w%P<^{ zfjHppt~OFz^e>z-bt`b^RL&b{Zp``!~|m|+-QMqs}0i!`6e z0zN%G9R)tKv$Ls0L>6$n-B!M}wY8x@YqgsCJw863NncaH*<^BZG8Hfgf=mMWd_I*> ztJPv|ZcZg?nkHjufs!(n;KgDwmWd4Lc^*L!#7M|ENtFS}4cpPhV)2iBX_m{uN5C+Q#N{H1B$0W{ zBxn+_TxGuR_Xso^4FI;cw-bpxBr}Lf&?s=VS|yjuv9YlMz{0{pZxu+ko12?4hEXEX zfP-2zV<7}vTU&kAQIjyA&$GI^8Y}nV;o(U3g5~Aq81Vl7K9`r5_`Wa3q7qcARdiiv zetuqkyuQBvB=DV`9R=KIG`POLW^Zp#O3;l-u)Dh(%RCIjzH&x4s3enpBcEXya(sLY zK&4XYlVHsEeNm}YViL%bNg_8lH`umKxm+G7YsYc0EDKH3&~^Q35&&qLM!8(3R4NTz zdQH>%mgJKp_){B+1W|kb?KugwvA zfMr<#)a&)sK_qv^K}PZP^mJ@1e<;D?;vxXHZQt*PrfI4(ojh8FVaUL1UGdnh*fdRC*Tpo=p$N!tBq+f7`MCnlydD3`>-jJ0P)kzs!)!wU0000< KMNUMnLSTZ_%A0in literal 0 HcmV?d00001 diff --git a/Resources/Textures/Structures/Power/substation.rsi/meta.json b/Resources/Textures/Structures/Power/substation.rsi/meta.json index 83d05c9ff0..a2ade57116 100644 --- a/Resources/Textures/Structures/Power/substation.rsi/meta.json +++ b/Resources/Textures/Structures/Power/substation.rsi/meta.json @@ -10,9 +10,15 @@ { "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 index 0000000000000000000000000000000000000000..2a1de1a06619f0c2231ee4ca131d0111629bb425 GIT binary patch literal 1289 zcmV+k1@`)hP)xQ6b9&gINfHp;S;dLKY_D zYX}p+aHjs4!7L6&f=osTlQ>Y;L8%P4F{{W*8Kbb#`h%U_+gomLdwUBpK7TGR=bZPP z_dW0DDeymsg#1x`xT&h;E2mQ|#>Tc^&J1+}>Wm#PCTiVjHr_dunY;IOiQ9RGdj~EW zJArLGHBYH`^Yh^Ydmkx#!sAcIZ{M^@BrRxJ{Iu@;fBV@qc>Ov}6Tw~P?WWgJ5{Xo@ z6G%wjlAsW9PQsrkg8HwSti$QN=ah=aFJKB_-?UK65{nNPcy;-0nc+y=H{xyhO3|Le zj8{F}aInMSm;)S6$6eIhMKS0e>6cx(wT~o!S+HwIwFFrphyMR{YaN81puPp$Giwg-6(>dm+ zd?g=Mdi16euQ2j42?pWj$hTY0h=n4X@kU9bc|0lr#{SPJPo+uL{X{0e3P zE&+U{#X@~ub@7*d{PR-NQ`0r#C}3>NZWmL8jWQvth$$zgXrQ)uH6IIND4?_bxo5v` zrRmw3U}=O52wOXU&o=G5Anek2wILOB;8hSHuPEPqpuzWbN6RWy_4~b00O_tssk#4D z))$32K*MW|`;OGmG)XICDwBQ$!s&wd^38A5Fbc0OwBrPSRM%`t)^1PZ7kpr}zk9F) z08lGRFCdoSg!UI77Cp8#XO{j3JPU{=IHNEz`=fyO%dU*ngat4LuuDe~Ono0j5PZdh zAb|%#_-Kz^rP+D4X=14v%JsB=PkT@MK*arTpO=S@;7^;J-5^(Y=r1}ppHuwU4W#|9 zyRpj$zRj&IXOHIz$^!5z3$U>vN%eH>Hek|Xo0|RG_l-~4PWFPfdf6{c2HOL|f1%lNf(GNS^=ieQ*UAgrakOm!gX#vC|DG*?{2n+{jyl>m>d+Yq^4PWv>&w_QN$3V%Ta_BR_yxgH?)D&J0gGYcpz zC@ndDj2@qxv`;NqY5Kr32lteg&=!&Uyw0G_0$RrFhCXcs03_)c{L4S8P&MZODoHFh zYO(5&ay}p^VE_Q@Pu3x;jnR!=s5BIpxHCHTK|_7Z=fDi?ckI*LzORzRW(pBJ;90=! zaq^a9#^C@K*DcDObKE!n;GxTCzj=oOqSKWrib&d$*ot7g#)Ha`r^ioB(U{bx6|jJp zUU>Pmt^k;{*uaebBnA+?n|UbrsGg1i!~#Y;fBo)qFLw}hvj8)i&<%`Wq`AjK1u6nu zT#?rTs7PupMFCMyL`fp$bSz-U0HO$D0b^3v8&+e-Y33`iuNELy8Wo%dLqlcZVL=Q! zMiw3xwammAJQ@ig8cKu0LlES%0Craz5HPw=P6e!A6aG+Wl<+;+H%BVKWw4Y~&o5t* ziVgPf5K{I5J51*;aU66-5b$$sh5`W4wM704ET-R;qt^rT`XV6{5!^sr(@v0Qc|z_(4r8rKE;b0PNpyZ@+mng8%g^J3AjA z0|Phr&!3+@k#6BYH+)DAfWhWqngsx?jSg$6rKhK+R#q}F%$zxUc6&R5uc)Z1s;0)k zaR2`8+qZ6!&#$N`D@#lqLbU+3JPzXzxoVJF9*6MZ>b^xUw z!PTq0d-uVEXU_(4Z;)EGz@9z3cON(~h)0Cf)YR0htf)64L@Ky->(;Gf$LK#IGzvz+ mC>RB!U=)mkQ7{TdDgXcrZx5U!7nCUg0000