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'
This commit is contained in:
chromiumboy
2023-12-24 00:07:41 -06:00
committed by GitHub
parent e6b52cc294
commit 1de682e23f
55 changed files with 2719 additions and 220 deletions

View File

@@ -2,15 +2,15 @@
xmlns:ui="clr-namespace:Content.Client.Medical.CrewMonitoring" xmlns:ui="clr-namespace:Content.Client.Medical.CrewMonitoring"
xmlns:controls="clr-namespace:Content.Client.UserInterface.Controls" xmlns:controls="clr-namespace:Content.Client.UserInterface.Controls"
Title="{Loc 'crew-monitoring-user-interface-title'}" Title="{Loc 'crew-monitoring-user-interface-title'}"
SetSize="1200 700" SetSize="1210 700"
MinSize="1200 700"> MinSize="1210 700">
<BoxContainer Orientation="Vertical"> <BoxContainer Orientation="Vertical">
<BoxContainer Orientation="Horizontal" VerticalExpand="True" HorizontalExpand="True"> <BoxContainer Orientation="Horizontal" VerticalExpand="True" HorizontalExpand="True">
<ui:CrewMonitoringNavMapControl Name="NavMap" HorizontalExpand="True" VerticalExpand="True" Margin="5 20"/> <ui:CrewMonitoringNavMapControl Name="NavMap" HorizontalExpand="True" VerticalExpand="True" Margin="5 20"/>
<BoxContainer Orientation="Vertical"> <BoxContainer Orientation="Vertical" Margin="0 0 10 0">
<controls:StripeBack> <controls:StripeBack>
<PanelContainer> <PanelContainer>
<Label Name="StationName" Text="Unknown station" Align="Center" /> <Label Name="StationName" Text="Unknown station" Align="Center" Margin="0 5 0 3"/>
</PanelContainer> </PanelContainer>
</controls:StripeBack> </controls:StripeBack>

View File

@@ -27,10 +27,12 @@ public partial class NavMapControl : MapGridControl
[Dependency] private readonly IEntityManager _entManager = default!; [Dependency] private readonly IEntityManager _entManager = default!;
private readonly SharedTransformSystem _transformSystem = default!; private readonly SharedTransformSystem _transformSystem = default!;
public EntityUid? Owner;
public EntityUid? MapUid; public EntityUid? MapUid;
// Actions // Actions
public event Action<NetEntity?>? TrackedEntitySelectedAction; public event Action<NetEntity?>? TrackedEntitySelectedAction;
public event Action<DrawingHandleScreen>? PostWallDrawingAction;
// Tracked data // Tracked data
public Dictionary<EntityCoordinates, (bool Visible, Color Color)> TrackedCoordinates = new(); public Dictionary<EntityCoordinates, (bool Visible, Color Color)> TrackedCoordinates = new();
@@ -358,6 +360,9 @@ public partial class NavMapControl : MapGridControl
} }
} }
if (PostWallDrawingAction != null)
PostWallDrawingAction.Invoke(handle);
// Beacons // Beacons
if (_beacons.Pressed) if (_beacons.Pressed)
{ {
@@ -455,7 +460,7 @@ public partial class NavMapControl : MapGridControl
} }
} }
private void UpdateNavMap() protected virtual void UpdateNavMap()
{ {
if (_navMap == null || _grid == null) if (_navMap == null || _grid == null)
return; return;

View File

@@ -0,0 +1,52 @@
using Content.Shared.Power;
namespace Content.Client.Power;
public sealed class PowerMonitoringConsoleBoundUserInterface : BoundUserInterface
{
[ViewVariables]
private PowerMonitoringWindow? _menu;
public PowerMonitoringConsoleBoundUserInterface(EntityUid owner, Enum uiKey) : base(owner, uiKey) { }
protected override void Open()
{
_menu = new PowerMonitoringWindow(this, Owner);
_menu.OpenCentered();
_menu.OnClose += Close;
}
protected override void UpdateState(BoundUserInterfaceState state)
{
base.UpdateState(state);
var castState = (PowerMonitoringConsoleBoundInterfaceState) state;
if (castState == null)
return;
EntMan.TryGetComponent<TransformComponent>(Owner, out var xform);
_menu?.ShowEntites
(castState.TotalSources,
castState.TotalBatteryUsage,
castState.TotalLoads,
castState.AllEntries,
castState.FocusSources,
castState.FocusLoads,
xform?.Coordinates);
}
public void SendPowerMonitoringConsoleMessage(NetEntity? netEntity, PowerMonitoringConsoleGroup group)
{
SendMessage(new PowerMonitoringConsoleMessage(netEntity, group));
}
protected override void Dispose(bool disposing)
{
base.Dispose(disposing);
if (!disposing)
return;
_menu?.Dispose();
}
}

View File

@@ -0,0 +1,300 @@
using Content.Client.Pinpointer.UI;
using Content.Shared.Pinpointer;
using Content.Shared.Power;
using Robust.Client.Graphics;
using Robust.Shared.Collections;
using Robust.Shared.Map.Components;
using System.Numerics;
namespace Content.Client.Power;
public sealed partial class PowerMonitoringConsoleNavMapControl : NavMapControl
{
[Dependency] private readonly IEntityManager _entManager = default!;
// Cable indexing
// 0: CableType.HighVoltage
// 1: CableType.MediumVoltage
// 2: CableType.Apc
private readonly Color[] _powerCableColors = { Color.OrangeRed, Color.Yellow, Color.LimeGreen };
private readonly Vector2[] _powerCableOffsets = { new Vector2(-0.2f, -0.2f), Vector2.Zero, new Vector2(0.2f, 0.2f) };
private Dictionary<Color, Color> _sRGBLookUp = new Dictionary<Color, Color>();
public PowerMonitoringCableNetworksComponent? PowerMonitoringCableNetworks;
public List<PowerMonitoringConsoleLineGroup> HiddenLineGroups = new();
public Dictionary<Vector2i, List<PowerMonitoringConsoleLine>>? PowerCableNetwork;
public Dictionary<Vector2i, List<PowerMonitoringConsoleLine>>? FocusCableNetwork;
private MapGridComponent? _grid;
public PowerMonitoringConsoleNavMapControl() : base()
{
// Set colors
TileColor = new Color(30, 57, 67);
WallColor = new Color(102, 164, 217);
PostWallDrawingAction += DrawAllCableNetworks;
}
protected override void UpdateNavMap()
{
base.UpdateNavMap();
if (Owner == null)
return;
if (!_entManager.TryGetComponent<PowerMonitoringCableNetworksComponent>(Owner, out var cableNetworks))
return;
if (!_entManager.TryGetComponent(MapUid, out _grid))
return;
PowerCableNetwork = GetDecodedPowerCableChunks(cableNetworks.AllChunks, _grid);
FocusCableNetwork = GetDecodedPowerCableChunks(cableNetworks.FocusChunks, _grid);
}
public void DrawAllCableNetworks(DrawingHandleScreen handle)
{
// Draw full cable network
if (PowerCableNetwork != null && PowerCableNetwork.Count > 0)
{
var modulator = (FocusCableNetwork != null && FocusCableNetwork.Count > 0) ? Color.DimGray : Color.White;
DrawCableNetwork(handle, PowerCableNetwork, modulator);
}
// Draw focus network
if (FocusCableNetwork != null && FocusCableNetwork.Count > 0)
DrawCableNetwork(handle, FocusCableNetwork, Color.White);
}
public void DrawCableNetwork(DrawingHandleScreen handle, Dictionary<Vector2i, List<PowerMonitoringConsoleLine>> fullCableNetwork, Color modulator)
{
var offset = GetOffset();
var area = new Box2(-WorldRange, -WorldRange, WorldRange + 1f, WorldRange + 1f).Translated(offset);
if (WorldRange / WorldMaxRange > 0.5f)
{
var cableNetworks = new ValueList<Vector2>[3];
foreach ((var chunk, var chunkedLines) in fullCableNetwork)
{
var offsetChunk = new Vector2(chunk.X, chunk.Y) * SharedNavMapSystem.ChunkSize;
if (offsetChunk.X < area.Left - SharedNavMapSystem.ChunkSize || offsetChunk.X > area.Right)
continue;
if (offsetChunk.Y < area.Bottom - SharedNavMapSystem.ChunkSize || offsetChunk.Y > area.Top)
continue;
foreach (var chunkedLine in chunkedLines)
{
if (HiddenLineGroups.Contains(chunkedLine.Group))
continue;
var start = Scale(chunkedLine.Origin - new Vector2(offset.X, -offset.Y));
var end = Scale(chunkedLine.Terminus - new Vector2(offset.X, -offset.Y));
cableNetworks[(int) chunkedLine.Group].Add(start);
cableNetworks[(int) chunkedLine.Group].Add(end);
}
}
for (int cableNetworkIdx = 0; cableNetworkIdx < cableNetworks.Length; cableNetworkIdx++)
{
var cableNetwork = cableNetworks[cableNetworkIdx];
if (cableNetwork.Count > 0)
{
var color = _powerCableColors[cableNetworkIdx] * modulator;
if (!_sRGBLookUp.TryGetValue(color, out var sRGB))
{
sRGB = Color.ToSrgb(color);
_sRGBLookUp[color] = sRGB;
}
handle.DrawPrimitives(DrawPrimitiveTopology.LineList, cableNetwork.Span, sRGB);
}
}
}
else
{
var cableVertexUVs = new ValueList<Vector2>[3];
foreach ((var chunk, var chunkedLines) in fullCableNetwork)
{
var offsetChunk = new Vector2(chunk.X, chunk.Y) * SharedNavMapSystem.ChunkSize;
if (offsetChunk.X < area.Left - SharedNavMapSystem.ChunkSize || offsetChunk.X > area.Right)
continue;
if (offsetChunk.Y < area.Bottom - SharedNavMapSystem.ChunkSize || offsetChunk.Y > area.Top)
continue;
foreach (var chunkedLine in chunkedLines)
{
if (HiddenLineGroups.Contains(chunkedLine.Group))
continue;
var leftTop = Scale(new Vector2
(Math.Min(chunkedLine.Origin.X, chunkedLine.Terminus.X) - 0.1f,
Math.Min(chunkedLine.Origin.Y, chunkedLine.Terminus.Y) - 0.1f)
- new Vector2(offset.X, -offset.Y));
var rightTop = Scale(new Vector2
(Math.Max(chunkedLine.Origin.X, chunkedLine.Terminus.X) + 0.1f,
Math.Min(chunkedLine.Origin.Y, chunkedLine.Terminus.Y) - 0.1f)
- new Vector2(offset.X, -offset.Y));
var leftBottom = Scale(new Vector2
(Math.Min(chunkedLine.Origin.X, chunkedLine.Terminus.X) - 0.1f,
Math.Max(chunkedLine.Origin.Y, chunkedLine.Terminus.Y) + 0.1f)
- new Vector2(offset.X, -offset.Y));
var rightBottom = Scale(new Vector2
(Math.Max(chunkedLine.Origin.X, chunkedLine.Terminus.X) + 0.1f,
Math.Max(chunkedLine.Origin.Y, chunkedLine.Terminus.Y) + 0.1f)
- new Vector2(offset.X, -offset.Y));
cableVertexUVs[(int) chunkedLine.Group].Add(leftBottom);
cableVertexUVs[(int) chunkedLine.Group].Add(leftTop);
cableVertexUVs[(int) chunkedLine.Group].Add(rightBottom);
cableVertexUVs[(int) chunkedLine.Group].Add(leftTop);
cableVertexUVs[(int) chunkedLine.Group].Add(rightBottom);
cableVertexUVs[(int) chunkedLine.Group].Add(rightTop);
}
}
for (int cableNetworkIdx = 0; cableNetworkIdx < cableVertexUVs.Length; cableNetworkIdx++)
{
var cableVertexUV = cableVertexUVs[cableNetworkIdx];
if (cableVertexUV.Count > 0)
{
var color = _powerCableColors[cableNetworkIdx] * modulator;
if (!_sRGBLookUp.TryGetValue(color, out var sRGB))
{
sRGB = Color.ToSrgb(color);
_sRGBLookUp[color] = sRGB;
}
handle.DrawPrimitives(DrawPrimitiveTopology.TriangleList, cableVertexUV.Span, sRGB);
}
}
}
}
public Dictionary<Vector2i, List<PowerMonitoringConsoleLine>>? GetDecodedPowerCableChunks(Dictionary<Vector2i, PowerCableChunk>? chunks, MapGridComponent? grid)
{
if (chunks == null || grid == null)
return null;
var decodedOutput = new Dictionary<Vector2i, List<PowerMonitoringConsoleLine>>();
foreach ((var chunkOrigin, var chunk) in chunks)
{
var list = new List<PowerMonitoringConsoleLine>();
for (int cableIdx = 0; cableIdx < chunk.PowerCableData.Length; cableIdx++)
{
var chunkMask = chunk.PowerCableData[cableIdx];
Vector2 offset = _powerCableOffsets[cableIdx];
for (var chunkIdx = 0; chunkIdx < SharedNavMapSystem.ChunkSize * SharedNavMapSystem.ChunkSize; chunkIdx++)
{
var value = (int) Math.Pow(2, chunkIdx);
var mask = chunkMask & value;
if (mask == 0x0)
continue;
var relativeTile = SharedNavMapSystem.GetTile(mask);
var tile = (chunk.Origin * SharedNavMapSystem.ChunkSize + relativeTile) * grid.TileSize;
var position = new Vector2(tile.X, -tile.Y);
PowerCableChunk neighborChunk;
bool neighbor;
// Note: we only check the north and east neighbors
// East
if (relativeTile.X == SharedNavMapSystem.ChunkSize - 1)
{
neighbor = chunks.TryGetValue(chunkOrigin + new Vector2i(1, 0), out neighborChunk) &&
(neighborChunk.PowerCableData[cableIdx] & SharedNavMapSystem.GetFlag(new Vector2i(0, relativeTile.Y))) != 0x0;
}
else
{
var flag = SharedNavMapSystem.GetFlag(relativeTile + new Vector2i(1, 0));
neighbor = (chunkMask & flag) != 0x0;
}
if (neighbor)
{
// Add points
var line = new PowerMonitoringConsoleLine
(position + offset + new Vector2(grid.TileSize * 0.5f, -grid.TileSize * 0.5f),
position + new Vector2(1f, 0f) + offset + new Vector2(grid.TileSize * 0.5f, -grid.TileSize * 0.5f),
(PowerMonitoringConsoleLineGroup) cableIdx);
list.Add(line);
}
// North
if (relativeTile.Y == SharedNavMapSystem.ChunkSize - 1)
{
neighbor = chunks.TryGetValue(chunkOrigin + new Vector2i(0, 1), out neighborChunk) &&
(neighborChunk.PowerCableData[cableIdx] & SharedNavMapSystem.GetFlag(new Vector2i(relativeTile.X, 0))) != 0x0;
}
else
{
var flag = SharedNavMapSystem.GetFlag(relativeTile + new Vector2i(0, 1));
neighbor = (chunkMask & flag) != 0x0;
}
if (neighbor)
{
// Add points
var line = new PowerMonitoringConsoleLine
(position + offset + new Vector2(grid.TileSize * 0.5f, -grid.TileSize * 0.5f),
position + new Vector2(0f, -1f) + offset + new Vector2(grid.TileSize * 0.5f, -grid.TileSize * 0.5f),
(PowerMonitoringConsoleLineGroup) cableIdx);
list.Add(line);
}
}
}
if (list.Count > 0)
decodedOutput.Add(chunkOrigin, list);
}
return decodedOutput;
}
}
public struct PowerMonitoringConsoleLine
{
public readonly Vector2 Origin;
public readonly Vector2 Terminus;
public readonly PowerMonitoringConsoleLineGroup Group;
public PowerMonitoringConsoleLine(Vector2 origin, Vector2 terminus, PowerMonitoringConsoleLineGroup group)
{
Origin = origin;
Terminus = terminus;
Group = group;
}
}
public enum PowerMonitoringConsoleLineGroup : byte
{
HighVoltage,
MediumVoltage,
Apc,
}

View File

@@ -1,20 +1,117 @@
<DefaultWindow <controls:FancyWindow xmlns="https://spacestation14.io"
xmlns="https://spacestation14.io" xmlns:ui="clr-namespace:Content.Client.Power"
Title="{Loc 'power-monitoring-window-title'}"> xmlns:controls="clr-namespace:Content.Client.UserInterface.Controls"
<BoxContainer Orientation="Vertical" VerticalExpand="True"> xmlns:gfx="clr-namespace:Robust.Client.Graphics;assembly=Robust.Client"
<GridContainer Columns="2"> Title="{Loc 'power-monitoring-window-title'}"
<!-- Grid is used here to align things. --> SetSize="1120 750"
<Label Text="{Loc 'power-monitoring-window-total-sources'}"/><Label Name="TotalSourcesNum" Text="?"/> MinSize="1120 750">
<Label Text="{Loc 'power-monitoring-window-total-loads'}"/><Label Name="TotalLoadsNum" Text="?"/> <BoxContainer Orientation="Vertical">
</GridContainer> <!-- Main display -->
<TabContainer Name="MasterTabContainer" VerticalExpand="True"> <BoxContainer Orientation="Horizontal" VerticalExpand="True" HorizontalExpand="True">
<ItemList Name="SourcesList" VerticalExpand="True"> <!-- Nav map -->
</ItemList> <BoxContainer Orientation="Vertical" VerticalExpand="True" HorizontalExpand="True">
<BoxContainer Orientation="Vertical" VerticalExpand="True"> <ui:PowerMonitoringConsoleNavMapControl Name="NavMap" Margin="5 5" VerticalExpand="True" HorizontalExpand="True">
<CheckBox Margin="8 8" Name="ShowInactiveConsumersCheckBox" Text="{Loc 'power-monitoring-window-show-inactive-consumers'}" />
<ItemList Name="LoadsList" VerticalExpand="True"> <!-- System warning -->
</ItemList> <PanelContainer Name="SystemWarningPanel"
HorizontalAlignment="Center"
VerticalAlignment="Top"
HorizontalExpand="True"
Margin="0 48 0 0"
Visible="False">
<RichTextLabel Name="SystemWarningLabel" Margin="12 8 12 8"/>
</PanelContainer>
</ui:PowerMonitoringConsoleNavMapControl>
<!-- Nav map legend -->
<BoxContainer Orientation="Horizontal" Margin="0 10 0 10">
<TextureRect Stretch="KeepAspectCentered"
TexturePath="/Textures/Interface/NavMap/beveled_circle.png"
Modulate="#800080"
SetSize="16 16"
Margin="20 0 5 0"/>
<Label Text="{Loc 'power-monitoring-window-label-sources'}"/>
<TextureRect Stretch="KeepAspectCentered"
TexturePath="/Textures/Interface/NavMap/beveled_hexagon.png"
SetSize="16 16"
Modulate="#ff4500"
Margin="20 0 5 0"/>
<Label Text="{Loc 'power-monitoring-window-label-smes'}"/>
<TextureRect Stretch="KeepAspectCentered"
TexturePath="/Textures/Interface/NavMap/beveled_square.png"
SetSize="16 16"
Modulate="#ffff00"
Margin="20 0 5 0"/>
<Label Text="{Loc 'power-monitoring-window-label-substation'}"/>
<TextureRect Stretch="KeepAspectCentered"
TexturePath="/Textures/Interface/NavMap/beveled_triangle.png"
SetSize="16 16"
Modulate="#32cd32"
Margin="20 0 5 0"/>
<Label Text="{Loc 'power-monitoring-window-label-apc'}"/>
</BoxContainer>
</BoxContainer> </BoxContainer>
</TabContainer>
<!-- Power status -->
<BoxContainer Orientation="Vertical" VerticalExpand="True" SetWidth="440" Margin="0 0 10 10">
<!-- Station name -->
<controls:StripeBack>
<PanelContainer>
<RichTextLabel Name="StationName" HorizontalAlignment="Center" VerticalAlignment="Center" Margin="0 5 0 3"/>
</PanelContainer>
</controls:StripeBack>
<!-- Power overview -->
<GridContainer Columns="2">
<Label StyleClasses="StatusFieldTitle" Text="{Loc 'power-monitoring-window-total-sources'}"/>
<Label Name="TotalSources" Text="?" Margin="10 0 0 0"/>
<Label StyleClasses="StatusFieldTitle" Text="{Loc 'power-monitoring-window-total-battery-usage'}"/>
<Label Name="TotalBatteryUsage" Text="?" Margin="10 0 0 0"/>
<Label StyleClasses="StatusFieldTitle" Text="{Loc 'power-monitoring-window-total-loads'}"/>
<Label Name="TotalLoads" Text="?" Margin="10 0 0 0"/>
</GridContainer>
<!-- Loads / Sources (entries added by C# code) -->
<TabContainer Name="MasterTabContainer" VerticalExpand="True" HorizontalExpand="True" Margin="0 10 0 0">
<ScrollContainer HorizontalExpand="True" Margin="8, 8, 8, 8">
<BoxContainer Name="SourcesList" Orientation="Vertical" VerticalExpand="True" HorizontalExpand="True" Margin="0 0 10 0"/>
</ScrollContainer>
<ScrollContainer HorizontalExpand="True" Margin="8, 8, 8, 8">
<BoxContainer Name="SMESList" Orientation="Vertical" VerticalExpand="True" HorizontalExpand="True" Margin="0 0 10 0"/>
</ScrollContainer>
<ScrollContainer HorizontalExpand="True" Margin="8, 8, 8, 8">
<BoxContainer Name="SubstationList" Orientation="Vertical" VerticalExpand="True" HorizontalExpand="True" Margin="0 0 10 0"/>
</ScrollContainer>
<ScrollContainer HorizontalExpand="True" Margin="8, 8, 8, 8">
<BoxContainer Name="ApcList" Orientation="Vertical" VerticalExpand="True" HorizontalExpand="True" Margin="0 0 10 0"/>
</ScrollContainer>
</TabContainer>
<!-- Cable network toggles -->
<BoxContainer Orientation="Vertical" Margin="0 10 0 0">
<Label Text="{Loc 'power-monitoring-window-show-cable-networks'}" Margin="0 0 0 5"/>
<BoxContainer Orientation="Horizontal" HorizontalExpand="True">
<CheckBox Name="ShowHVCable" Text="{Loc 'power-monitoring-window-show-hv-cable'}" Pressed="True" Margin="0 0 0 0"/>
<CheckBox Name="ShowMVCable" Text="{Loc 'power-monitoring-window-show-mv-cable'}" Pressed="True" Margin="10 0 0 0"/>
<CheckBox Name="ShowLVCable" Text="{Loc 'power-monitoring-window-show-lv-cable'}" Pressed="True" Margin="10 0 0 0"/>
</BoxContainer>
</BoxContainer>
</BoxContainer>
</BoxContainer>
<!-- Footer -->
<BoxContainer Orientation="Vertical">
<PanelContainer StyleClasses="LowDivider" />
<BoxContainer Orientation="Horizontal" Margin="10 2 5 0" VerticalAlignment="Bottom">
<Label Text="{Loc 'power-monitoring-window-flavor-left'}" StyleClasses="WindowFooterText" />
<Label Text="{Loc 'power-monitoring-window-flavor-right'}" StyleClasses="WindowFooterText"
HorizontalAlignment="Right" HorizontalExpand="True" Margin="0 0 5 0" />
<TextureRect StyleClasses="NTLogoDark" Stretch="KeepAspectCentered"
VerticalAlignment="Center" HorizontalAlignment="Right" SetSize="19 19"/>
</BoxContainer>
</BoxContainer>
</BoxContainer> </BoxContainer>
</DefaultWindow> </controls:FancyWindow>

View File

@@ -0,0 +1,490 @@
using Content.Client.Stylesheets;
using Content.Shared.Power;
using Robust.Client.Graphics;
using Robust.Client.UserInterface.Controls;
using Robust.Shared.Utility;
using System.Diagnostics.CodeAnalysis;
using System.Linq;
using System.Numerics;
namespace Content.Client.Power;
public sealed partial class PowerMonitoringWindow
{
private SpriteSpecifier.Texture _sourceIcon = new SpriteSpecifier.Texture(new ResPath("/Textures/Interface/PowerMonitoring/source_arrow.png"));
private SpriteSpecifier.Texture _loadIconPath = new SpriteSpecifier.Texture(new ResPath("/Textures/Interface/PowerMonitoring/load_arrow.png"));
private bool _autoScrollActive = false;
private bool _autoScrollAwaitsUpdate = false;
private void UpdateWindowConsoleEntry
(BoxContainer masterContainer,
int index,
PowerMonitoringConsoleEntry entry,
PowerMonitoringConsoleEntry[] focusSources,
PowerMonitoringConsoleEntry[] focusLoads)
{
UpdateWindowConsoleEntry(masterContainer, index, entry);
var windowEntry = masterContainer.GetChild(index) as PowerMonitoringWindowEntry;
// If we exit here, something was added to the container that shouldn't have been added
if (windowEntry == null)
return;
// Update sources and loads
UpdateEntrySourcesOrLoads(masterContainer, windowEntry.SourcesContainer, focusSources, _sourceIcon);
UpdateEntrySourcesOrLoads(masterContainer, windowEntry.LoadsContainer, focusLoads, _loadIconPath);
windowEntry.MainContainer.Visible = true;
}
private void UpdateWindowConsoleEntry(BoxContainer masterContainer, int index, PowerMonitoringConsoleEntry entry)
{
PowerMonitoringWindowEntry? windowEntry;
// Add missing children
if (index >= masterContainer.ChildCount)
{
// Add basic entry
windowEntry = new PowerMonitoringWindowEntry(entry);
masterContainer.AddChild(windowEntry);
// Selection action
windowEntry.Button.OnButtonUp += args =>
{
windowEntry.SourcesContainer.DisposeAllChildren();
windowEntry.LoadsContainer.DisposeAllChildren();
ButtonAction(windowEntry, masterContainer);
};
}
else
{
windowEntry = masterContainer.GetChild(index) as PowerMonitoringWindowEntry;
}
// If we exit here, something was added to the container that shouldn't have been added
if (windowEntry == null)
return;
windowEntry.NetEntity = entry.NetEntity;
windowEntry.Entry = entry;
windowEntry.MainContainer.Visible = false;
UpdateWindowEntryButton(entry.NetEntity, windowEntry.Button, entry);
}
public void UpdateWindowEntryButton(NetEntity netEntity, PowerMonitoringButton button, PowerMonitoringConsoleEntry entry)
{
if (!netEntity.IsValid())
return;
if (entry.MetaData == null)
return;
// Update button style
if (netEntity == _focusEntity)
button.AddStyleClass(StyleNano.StyleClassButtonColorGreen);
else
button.RemoveStyleClass(StyleNano.StyleClassButtonColorGreen);
// Update sprite
if (entry.MetaData.Value.SpritePath != string.Empty && entry.MetaData.Value.SpriteState != string.Empty)
button.TextureRect.Texture = _spriteSystem.Frame0(new SpriteSpecifier.Rsi(new ResPath(entry.MetaData.Value.SpritePath), entry.MetaData.Value.SpriteState));
// Update name
var name = Loc.GetString(entry.MetaData.Value.EntityName);
button.NameLocalized.Text = name;
// Update tool tip
button.ToolTip = Loc.GetString(name);
// Update power value
button.PowerValue.Text = Loc.GetString("power-monitoring-window-value", ("value", entry.PowerValue));
}
private void UpdateEntrySourcesOrLoads(BoxContainer masterContainer, BoxContainer currentContainer, PowerMonitoringConsoleEntry[]? entries, SpriteSpecifier.Texture icon)
{
if (currentContainer == null)
return;
if (entries == null || entries.Length == 0)
{
currentContainer.RemoveAllChildren();
return;
}
// Remove excess children
while (currentContainer.ChildCount > entries.Length)
{
currentContainer.RemoveChild(currentContainer.GetChild(currentContainer.ChildCount - 1));
}
// Add missing children
while (currentContainer.ChildCount < entries.Length)
{
var entry = entries[currentContainer.ChildCount];
var subEntry = new PowerMonitoringWindowSubEntry(entry);
currentContainer.AddChild(subEntry);
// Selection action
subEntry.Button.OnButtonUp += args => { ButtonAction(subEntry, masterContainer); };
}
if (!_entManager.TryGetComponent<PowerMonitoringConsoleComponent>(_owner, out var console))
return;
// Update all children
foreach (var child in currentContainer.Children)
{
if (child is not PowerMonitoringWindowSubEntry)
continue;
var castChild = (PowerMonitoringWindowSubEntry) child;
if (castChild == null)
continue;
if (castChild.Icon != null)
castChild.Icon.Texture = _spriteSystem.Frame0(icon);
var entry = entries[child.GetPositionInParent()];
castChild.NetEntity = entry.NetEntity;
castChild.Entry = entry;
UpdateWindowEntryButton(entry.NetEntity, castChild.Button, entries.ElementAt(child.GetPositionInParent()));
}
}
private void ButtonAction(PowerMonitoringWindowBaseEntry entry, BoxContainer masterContainer)
{
// Toggle off button?
if (entry.NetEntity == _focusEntity)
{
entry.Button.RemoveStyleClass(StyleNano.StyleClassButtonColorGreen);
_focusEntity = null;
// Request an update from the power monitoring system
SendPowerMonitoringConsoleMessageAction?.Invoke(null, entry.Entry.Group);
return;
}
// Otherwise, toggle on
entry.Button.AddStyleClass(StyleNano.StyleClassButtonColorGreen);
ActivateAutoScrollToFocus();
// Toggle off the old button (if applicable)
if (_focusEntity != null)
{
foreach (PowerMonitoringWindowEntry sibling in masterContainer.Children)
{
if (sibling.NetEntity == _focusEntity)
{
sibling.Button.RemoveStyleClass(StyleNano.StyleClassButtonColorGreen);
break;
}
}
}
// Center the nav map on selected entity
_focusEntity = entry.NetEntity;
if (!NavMap.TrackedEntities.TryGetValue(entry.NetEntity, out var blip))
return;
NavMap.CenterToCoordinates(blip.Coordinates);
// Switch tabs
SwitchTabsBasedOnPowerMonitoringConsoleGroup(entry.Entry.Group);
// Send an update from the power monitoring system
SendPowerMonitoringConsoleMessageAction?.Invoke(_focusEntity, entry.Entry.Group);
}
private void ActivateAutoScrollToFocus()
{
_autoScrollActive = false;
_autoScrollAwaitsUpdate = true;
}
private bool TryGetNextScrollPosition([NotNullWhen(true)] out float? nextScrollPosition)
{
nextScrollPosition = null;
var scroll = MasterTabContainer.Children.ElementAt(MasterTabContainer.CurrentTab) as ScrollContainer;
if (scroll == null)
return false;
var container = scroll.Children.ElementAt(0) as BoxContainer;
if (container == null || container.Children.Count() == 0)
return false;
// Exit if the heights of the children haven't been initialized yet
if (!container.Children.Any(x => x.Height > 0))
return false;
nextScrollPosition = 0;
foreach (var control in container.Children)
{
if (control == null || control is not PowerMonitoringWindowEntry)
continue;
if (((PowerMonitoringWindowEntry) control).NetEntity == _focusEntity)
return true;
nextScrollPosition += control.Height;
}
// Failed to find control
nextScrollPosition = null;
return false;
}
private bool TryGetVerticalScrollbar(ScrollContainer scroll, [NotNullWhen(true)] out VScrollBar? vScrollBar)
{
vScrollBar = null;
foreach (var child in scroll.Children)
{
if (child is not VScrollBar)
continue;
var castChild = child as VScrollBar;
if (castChild != null)
{
vScrollBar = castChild;
return true;
}
}
return false;
}
private void AutoScrollToFocus()
{
if (!_autoScrollActive)
return;
var scroll = MasterTabContainer.Children.ElementAt(MasterTabContainer.CurrentTab) as ScrollContainer;
if (scroll == null)
return;
if (!TryGetVerticalScrollbar(scroll, out var vScrollbar))
return;
if (!TryGetNextScrollPosition(out float? nextScrollPosition))
return;
vScrollbar.ValueTarget = nextScrollPosition.Value;
if (MathHelper.CloseToPercent(vScrollbar.Value, vScrollbar.ValueTarget))
_autoScrollActive = false;
}
private void UpdateWarningLabel(PowerMonitoringFlags flags)
{
if (flags == PowerMonitoringFlags.None)
{
SystemWarningPanel.Visible = false;
return;
}
var msg = new FormattedMessage();
if ((flags & PowerMonitoringFlags.RoguePowerConsumer) != 0)
{
SystemWarningPanel.PanelOverride = new StyleBoxFlat
{
BackgroundColor = Color.Red,
BorderColor = Color.DarkRed,
BorderThickness = new Thickness(2),
};
msg.AddMarkup(Loc.GetString("power-monitoring-window-rogue-power-consumer"));
SystemWarningPanel.Visible = true;
}
else if ((flags & PowerMonitoringFlags.PowerNetAbnormalities) != 0)
{
SystemWarningPanel.PanelOverride = new StyleBoxFlat
{
BackgroundColor = Color.Orange,
BorderColor = Color.DarkOrange,
BorderThickness = new Thickness(2),
};
msg.AddMarkup(Loc.GetString("power-monitoring-window-power-net-abnormalities"));
SystemWarningPanel.Visible = true;
}
SystemWarningLabel.SetMessage(msg);
}
private void SwitchTabsBasedOnPowerMonitoringConsoleGroup(PowerMonitoringConsoleGroup group)
{
switch (group)
{
case PowerMonitoringConsoleGroup.Generator:
MasterTabContainer.CurrentTab = 0; break;
case PowerMonitoringConsoleGroup.SMES:
MasterTabContainer.CurrentTab = 1; break;
case PowerMonitoringConsoleGroup.Substation:
MasterTabContainer.CurrentTab = 2; break;
case PowerMonitoringConsoleGroup.APC:
MasterTabContainer.CurrentTab = 3; break;
}
}
private PowerMonitoringConsoleGroup GetCurrentPowerMonitoringConsoleGroup()
{
return (PowerMonitoringConsoleGroup) MasterTabContainer.CurrentTab;
}
}
public sealed class PowerMonitoringWindowEntry : PowerMonitoringWindowBaseEntry
{
public BoxContainer MainContainer;
public BoxContainer SourcesContainer;
public BoxContainer LoadsContainer;
public PowerMonitoringWindowEntry(PowerMonitoringConsoleEntry entry) : base(entry)
{
Entry = entry;
// Alignment
Orientation = LayoutOrientation.Vertical;
HorizontalExpand = true;
// Update selection button
Button.StyleClasses.Add("OpenLeft");
AddChild(Button);
// Grid container to hold sub containers
MainContainer = new BoxContainer()
{
Orientation = LayoutOrientation.Vertical,
HorizontalExpand = true,
Margin = new Thickness(8, 0, 0, 0),
Visible = false,
};
AddChild(MainContainer);
// Grid container to hold the list of sources when selected
SourcesContainer = new BoxContainer()
{
Orientation = LayoutOrientation.Vertical,
HorizontalExpand = true,
};
MainContainer.AddChild(SourcesContainer);
// Grid container to hold the list of loads when selected
LoadsContainer = new BoxContainer()
{
Orientation = LayoutOrientation.Vertical,
HorizontalExpand = true,
};
MainContainer.AddChild(LoadsContainer);
}
}
public sealed class PowerMonitoringWindowSubEntry : PowerMonitoringWindowBaseEntry
{
public TextureRect? Icon;
public PowerMonitoringWindowSubEntry(PowerMonitoringConsoleEntry entry) : base(entry)
{
Orientation = LayoutOrientation.Horizontal;
HorizontalExpand = true;
// Source/load icon
Icon = new TextureRect()
{
VerticalAlignment = VAlignment.Center,
Margin = new Thickness(0, 0, 2, 0),
};
AddChild(Icon);
// Selection button
Button.StyleClasses.Add("OpenBoth");
AddChild(Button);
}
}
public abstract class PowerMonitoringWindowBaseEntry : BoxContainer
{
public NetEntity NetEntity;
public PowerMonitoringConsoleEntry Entry;
public PowerMonitoringButton Button;
public PowerMonitoringWindowBaseEntry(PowerMonitoringConsoleEntry entry)
{
Entry = entry;
// Add selection button (properties set by derivative classes)
Button = new PowerMonitoringButton();
}
}
public sealed class PowerMonitoringButton : Button
{
public BoxContainer MainContainer;
public TextureRect TextureRect;
public Label NameLocalized;
public Label PowerValue;
public PowerMonitoringButton()
{
HorizontalExpand = true;
VerticalExpand = true;
Margin = new Thickness(0f, 1f, 0f, 1f);
MainContainer = new BoxContainer()
{
Orientation = BoxContainer.LayoutOrientation.Horizontal,
HorizontalExpand = true,
SetHeight = 32f,
};
AddChild(MainContainer);
TextureRect = new TextureRect()
{
HorizontalAlignment = HAlignment.Center,
VerticalAlignment = VAlignment.Center,
SetSize = new Vector2(32f, 32f),
Margin = new Thickness(0f, 0f, 5f, 0f),
};
MainContainer.AddChild(TextureRect);
NameLocalized = new Label()
{
HorizontalExpand = true,
ClipText = true,
};
MainContainer.AddChild(NameLocalized);
PowerValue = new Label()
{
HorizontalAlignment = HAlignment.Right,
SetWidth = 72f,
Margin = new Thickness(10, 0, 0, 0),
ClipText = true,
};
MainContainer.AddChild(PowerValue);
}
}

View File

@@ -1,84 +1,317 @@
using System.Linq; using Content.Client.Pinpointer.UI;
using System.Numerics; using Content.Client.UserInterface.Controls;
using Content.Client.Computer;
using Content.Shared.Power; using Content.Shared.Power;
using JetBrains.Annotations;
using Robust.Client.AutoGenerated; using Robust.Client.AutoGenerated;
using Robust.Client.GameObjects; using Robust.Client.GameObjects;
using Robust.Client.Graphics;
using Robust.Client.UserInterface.Controls; using Robust.Client.UserInterface.Controls;
using Robust.Client.UserInterface.CustomControls;
using Robust.Client.UserInterface.XAML; using Robust.Client.UserInterface.XAML;
using Robust.Shared.Graphics; using Robust.Shared.Map;
using Robust.Shared.Graphics.RSI; using Robust.Shared.Timing;
using Robust.Shared.Prototypes; using Robust.Shared.Utility;
using System.Linq;
namespace Content.Client.Power; namespace Content.Client.Power;
[GenerateTypedNameReferences] [GenerateTypedNameReferences]
public sealed partial class PowerMonitoringWindow : DefaultWindow, IComputerWindow<PowerMonitoringConsoleBoundInterfaceState> public sealed partial class PowerMonitoringWindow : FancyWindow
{ {
[Dependency] private readonly IPrototypeManager _prototypeManager = default!; private readonly IEntityManager _entManager;
private readonly SpriteSystem _spriteSystem = default!; private readonly SpriteSystem _spriteSystem;
private readonly IGameTiming _gameTiming;
public PowerMonitoringWindow() private const float BlinkFrequency = 1f;
private EntityUid? _owner;
private NetEntity? _focusEntity;
public event Action<NetEntity?, PowerMonitoringConsoleGroup>? SendPowerMonitoringConsoleMessageAction;
private Dictionary<PowerMonitoringConsoleGroup, (SpriteSpecifier.Texture, Color)> _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); RobustXamlLoader.Load(this);
SetSize = MinSize = new Vector2(300, 450); _entManager = IoCManager.Resolve<IEntityManager>();
IoCManager.InjectDependencies(this); _gameTiming = IoCManager.Resolve<IGameTiming>();
_spriteSystem = IoCManager.Resolve<IEntityManager>().System<SpriteSystem>();
MasterTabContainer.SetTabTitle(0, Loc.GetString("power-monitoring-window-tab-sources")); _spriteSystem = _entManager.System<SpriteSystem>();
MasterTabContainer.SetTabTitle(1, Loc.GetString("power-monitoring-window-tab-loads")); _owner = owner;
// Pass owner to nav map
NavMap.Owner = _owner;
// Set nav map grid uid
var stationName = Loc.GetString("power-monitoring-window-unknown-location");
if (_entManager.TryGetComponent<TransformComponent>(owner, out var xform))
{
NavMap.MapUid = xform.GridUid;
// Assign station name
if (_entManager.TryGetComponent<MetaDataComponent>(xform.GridUid, out var stationMetaData))
stationName = stationMetaData.EntityName;
var msg = new FormattedMessage();
msg.AddMarkup(Loc.GetString("power-monitoring-window-station-name", ("stationName", stationName)));
StationName.SetMessage(msg);
}
else
{
StationName.SetMessage(stationName);
NavMap.Visible = false;
}
// 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); SendPowerMonitoringConsoleMessageAction?.Invoke(_focusEntity, (PowerMonitoringConsoleGroup) tab);
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);
} }
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)); if (!NavMap.HiddenLineGroups.Remove(lineGroup))
// This magic is important to prevent scrolling issues. NavMap.HiddenLineGroups.Add(lineGroup);
while (list.Count > listVal.Length) }
public void ShowEntites
(double totalSources,
double totalBatteryUsage,
double totalLoads,
PowerMonitoringConsoleEntry[] allEntries,
PowerMonitoringConsoleEntry[] focusSources,
PowerMonitoringConsoleEntry[] focusLoads,
EntityCoordinates? monitorCoords)
{
if (_owner == null)
return;
if (!_entManager.TryGetComponent<PowerMonitoringConsoleComponent>(_owner.Value, out var console))
return;
// Update power status text
TotalSources.Text = Loc.GetString("power-monitoring-window-value", ("value", totalSources));
TotalBatteryUsage.Text = Loc.GetString("power-monitoring-window-value", ("value", totalBatteryUsage));
TotalLoads.Text = Loc.GetString("power-monitoring-window-value", ("value", totalLoads));
// 10+% of station power is being drawn from batteries
TotalBatteryUsage.FontColorOverride = (totalSources * 0.1111f) < totalBatteryUsage ? new Color(180, 0, 0) : Color.White;
// Station generator and battery output is less than the current demand
TotalLoads.FontColorOverride = (totalSources + totalBatteryUsage) < totalLoads &&
!MathHelper.CloseToPercent(totalSources + totalBatteryUsage, totalLoads, 0.1f) ? new Color(180, 0, 0) : Color.White;
// Update system warnings
UpdateWarningLabel(console.Flags);
// Reset nav map values
NavMap.TrackedCoordinates.Clear();
NavMap.TrackedEntities.Clear();
// Draw entities on the nav map
var entitiesOfInterest = new List<NetEntity>();
if (_focusEntity != null)
{ {
list.RemoveAt(list.Count - 1); entitiesOfInterest.Add(_focusEntity.Value);
foreach (var entry in focusSources)
entitiesOfInterest.Add(entry.NetEntity);
foreach (var entry in focusLoads)
entitiesOfInterest.Add(entry.NetEntity);
} }
while (list.Count < listVal.Length)
focusSources.Concat(focusLoads);
foreach ((var netEntity, var metaData) in console.PowerMonitoringDeviceMetaData)
{ {
list.AddItem("YOU SHOULD NEVER SEE THIS (REALLY!)", null, false); if (NavMap.Visible)
AddTrackedEntityToNavMap(netEntity, metaData, entitiesOfInterest);
} }
// Now overwrite the items properly...
for (var i = 0; i < listVal.Length; i++) // Show monitor location
var mon = _entManager.GetNetEntity(_owner);
if (monitorCoords != null && mon != null)
{ {
var ent = listVal[i]; var texture = _spriteSystem.Frame0(new SpriteSpecifier.Texture(new ResPath("/Textures/Interface/NavMap/beveled_circle.png")));
_prototypeManager.TryIndex(ent.IconEntityPrototypeId, out EntityPrototype? entityPrototype); var blip = new NavMapBlip(monitorCoords.Value, texture, Color.Cyan, true, false);
IRsiStateLike? iconState = null; NavMap.TrackedEntities[mon.Value] = blip;
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;
} }
// Update nav map
NavMap.ForceNavMapUpdate();
// If the entry group doesn't match the current tab, the data is out dated, do not use it
if (allEntries.Length > 0 && allEntries[0].Group != GetCurrentPowerMonitoringConsoleGroup())
return;
// Assign meta data to the console entries and sort them
allEntries = GetUpdatedPowerMonitoringConsoleEntries(allEntries, console);
focusSources = GetUpdatedPowerMonitoringConsoleEntries(focusSources, console);
focusLoads = GetUpdatedPowerMonitoringConsoleEntries(focusLoads, console);
// Get current console entry container
BoxContainer currentContainer = SourcesList;
switch (GetCurrentPowerMonitoringConsoleGroup())
{
case PowerMonitoringConsoleGroup.SMES:
currentContainer = SMESList; break;
case PowerMonitoringConsoleGroup.Substation:
currentContainer = SubstationList; break;
case PowerMonitoringConsoleGroup.APC:
currentContainer = ApcList; break;
}
// Clear excess children from the container
while (currentContainer.ChildCount > allEntries.Length)
currentContainer.RemoveChild(currentContainer.GetChild(currentContainer.ChildCount - 1));
// Update the remaining children
for (var index = 0; index < allEntries.Length; index++)
{
var entry = allEntries[index];
if (entry.NetEntity == _focusEntity)
UpdateWindowConsoleEntry(currentContainer, index, entry, focusSources, focusLoads);
else
UpdateWindowConsoleEntry(currentContainer, index, entry);
}
// Auto-scroll renable
if (_autoScrollAwaitsUpdate)
{
_autoScrollActive = true;
_autoScrollAwaitsUpdate = false;
}
}
private void AddTrackedEntityToNavMap(NetEntity netEntity, PowerMonitoringDeviceMetaData metaData, List<NetEntity> entitiesOfInterest)
{
if (!_groupBlips.TryGetValue(metaData.Group, out var data))
return;
var usedEntity = (metaData.CollectionMaster != null) ? metaData.CollectionMaster : netEntity;
var coords = _entManager.GetCoordinates(metaData.Coordinates);
var texture = data.Item1;
var color = data.Item2;
var blink = usedEntity == _focusEntity;
var modulator = Color.White;
if (_focusEntity != null && usedEntity != _focusEntity && !entitiesOfInterest.Contains(usedEntity.Value))
modulator = Color.DimGray;
var blip = new NavMapBlip(coords, _spriteSystem.Frame0(texture), color * modulator, blink);
NavMap.TrackedEntities[netEntity] = blip;
}
private void SetTrackedEntityFromNavMap(NetEntity? netEntity)
{
if (netEntity == null)
return;
if (!_entManager.TryGetComponent<PowerMonitoringConsoleComponent>(_owner, out var console))
return;
if (!console.PowerMonitoringDeviceMetaData.TryGetValue(netEntity.Value, out var metaData))
return;
// Switch entity for master, if applicable
// The master will always be in the same group as the entity
if (metaData.CollectionMaster != null)
netEntity = metaData.CollectionMaster;
_focusEntity = netEntity;
// Switch tabs
SwitchTabsBasedOnPowerMonitoringConsoleGroup(metaData.Group);
// Get the scroll position of the selected entity on the selected button the UI
ActivateAutoScrollToFocus();
// Send message to console that the focus has changed
SendPowerMonitoringConsoleMessageAction?.Invoke(_focusEntity, metaData.Group);
}
protected override void FrameUpdate(FrameEventArgs args)
{
AutoScrollToFocus();
// Warning sign pulse
var lit = _gameTiming.RealTime.TotalSeconds % BlinkFrequency > BlinkFrequency / 2f;
SystemWarningPanel.Modulate = lit ? Color.White : new Color(178, 178, 178);
}
private PowerMonitoringConsoleEntry[] GetUpdatedPowerMonitoringConsoleEntries(PowerMonitoringConsoleEntry[] entries, PowerMonitoringConsoleComponent console)
{
for (int i = 0; i < entries.Length; i++)
{
var entry = entries[i];
if (!console.PowerMonitoringDeviceMetaData.TryGetValue(entry.NetEntity, out var metaData))
continue;
entries[i].MetaData = metaData;
}
// Sort all devices alphabetically by their entity name (not by power usage; otherwise their position on the UI will shift)
Array.Sort(entries, AlphabeticalSort);
return entries;
}
private int AlphabeticalSort(PowerMonitoringConsoleEntry x, PowerMonitoringConsoleEntry y)
{
if (x.MetaData?.EntityName == null)
return -1;
if (y.MetaData?.EntityName == null)
return 1;
return x.MetaData.Value.EntityName.CompareTo(y.MetaData.Value.EntityName);
} }
} }
[UsedImplicitly] public struct PowerMonitoringConsoleTrackable
public sealed class PowerMonitoringConsoleBoundUserInterface : ComputerBoundUserInterface<PowerMonitoringWindow, PowerMonitoringConsoleBoundInterfaceState>
{ {
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;
} }
} }

View File

@@ -1,4 +1,3 @@
using Content.Server.Station.Components;
using Content.Server.Station.Systems; using Content.Server.Station.Systems;
using Content.Server.Warps; using Content.Server.Warps;
using Content.Shared.Pinpointer; using Content.Shared.Pinpointer;
@@ -32,7 +31,7 @@ public sealed class NavMapSystem : SharedNavMapSystem
SubscribeLocalEvent<StationGridAddedEvent>(OnStationInit); SubscribeLocalEvent<StationGridAddedEvent>(OnStationInit);
SubscribeLocalEvent<NavMapComponent, ComponentStartup>(OnNavMapStartup); SubscribeLocalEvent<NavMapComponent, ComponentStartup>(OnNavMapStartup);
SubscribeLocalEvent<NavMapComponent, ComponentGetState>(OnGetState); SubscribeLocalEvent<NavMapComponent, ComponentGetState>(OnGetState);
SubscribeLocalEvent<NavMapComponent, GridSplitEvent>(OnNavMapSplit); SubscribeLocalEvent<GridSplitEvent>(OnNavMapSplit);
SubscribeLocalEvent<NavMapBeaconComponent, ComponentStartup>(OnNavMapBeaconStartup); SubscribeLocalEvent<NavMapBeaconComponent, ComponentStartup>(OnNavMapBeaconStartup);
SubscribeLocalEvent<NavMapBeaconComponent, AnchorStateChangedEvent>(OnNavMapBeaconAnchor); SubscribeLocalEvent<NavMapBeaconComponent, AnchorStateChangedEvent>(OnNavMapBeaconAnchor);
@@ -84,7 +83,7 @@ public sealed class NavMapSystem : SharedNavMapSystem
RefreshGrid(component, grid); RefreshGrid(component, grid);
} }
private void OnNavMapSplit(EntityUid uid, NavMapComponent component, ref GridSplitEvent args) private void OnNavMapSplit(ref GridSplitEvent args)
{ {
var gridQuery = GetEntityQuery<MapGridComponent>(); var gridQuery = GetEntityQuery<MapGridComponent>();
@@ -94,7 +93,7 @@ public sealed class NavMapSystem : SharedNavMapSystem
RefreshGrid(newComp, gridQuery.GetComponent(grid)); RefreshGrid(newComp, gridQuery.GetComponent(grid));
} }
RefreshGrid(component, gridQuery.GetComponent(uid)); RefreshGrid(Comp<NavMapComponent>(args.Grid), gridQuery.GetComponent(args.Grid));
} }
private void RefreshGrid(NavMapComponent component, MapGridComponent grid) private void 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) private void RefreshTile(MapGridComponent grid, NavMapComponent component, NavMapChunk chunk, Vector2i tile)
{ {
var relative = SharedMapSystem.GetChunkRelative(tile, ChunkSize); var relative = SharedMapSystem.GetChunkRelative(tile, ChunkSize);
var existing = chunk.TileData; var existing = chunk.TileData;
var flag = GetFlag(relative); var flag = GetFlag(relative);

View File

@@ -1,38 +1,54 @@
using Content.Server.Power.EntitySystems; using Content.Server.Power.EntitySystems;
using Content.Shared.Power;
using Content.Shared.Tools; using Content.Shared.Tools;
using Robust.Shared.Prototypes; using Robust.Shared.Prototypes;
using Robust.Shared.Serialization.TypeSerializers.Implementations.Custom.Prototype; using Robust.Shared.Serialization.TypeSerializers.Implementations.Custom.Prototype;
using System.Diagnostics.Tracing;
namespace Content.Server.Power.Components namespace Content.Server.Power.Components;
/// <summary>
/// Allows the attached entity to be destroyed by a cutting tool, dropping a piece of cable.
/// </summary>
[RegisterComponent]
[Access(typeof(CableSystem))]
public sealed partial class CableComponent : Component
{ {
[DataField("cableDroppedOnCutPrototype", customTypeSerializer: typeof(PrototypeIdSerializer<EntityPrototype>))]
public string CableDroppedOnCutPrototype = "CableHVStack1";
[DataField("cuttingQuality", customTypeSerializer:typeof(PrototypeIdSerializer<ToolQualityPrototype>))]
public string CuttingQuality = "Cutting";
/// <summary> /// <summary>
/// Allows the attached entity to be destroyed by a cutting tool, dropping a piece of cable. /// Checked by <see cref="CablePlacerComponent"/> to determine if there is
/// already a cable of a type on a tile.
/// </summary> /// </summary>
[RegisterComponent] [DataField("cableType")]
[Access(typeof(CableSystem))] public CableType CableType = CableType.HighVoltage;
public sealed partial class CableComponent : Component
[DataField("cuttingDelay")]
public float CuttingDelay = 1f;
}
/// <summary>
/// Event to be raised when a cable is anchored / unanchored
/// </summary>
[ByRefEvent]
public readonly struct CableAnchorStateChangedEvent
{
public readonly TransformComponent Transform;
public EntityUid Entity => Transform.Owner;
public bool Anchored => Transform.Anchored;
/// <summary>
/// If true, the entity is being detached to null-space
/// </summary>
public readonly bool Detaching;
public CableAnchorStateChangedEvent(TransformComponent transform, bool detaching = false)
{ {
[DataField("cableDroppedOnCutPrototype", customTypeSerializer: typeof(PrototypeIdSerializer<EntityPrototype>))] Detaching = detaching;
public string CableDroppedOnCutPrototype = "CableHVStack1"; Transform = transform;
[DataField("cuttingQuality", customTypeSerializer:typeof(PrototypeIdSerializer<ToolQualityPrototype>))]
public string CuttingQuality = "Cutting";
/// <summary>
/// Checked by <see cref="CablePlacerComponent"/> to determine if there is
/// already a cable of a type on a tile.
/// </summary>
[DataField("cableType")]
public CableType CableType = CableType.HighVoltage;
[DataField("cuttingDelay")]
public float CuttingDelay = 1f;
}
public enum CableType
{
HighVoltage,
MediumVoltage,
Apc,
} }
} }

View File

@@ -1,5 +1,6 @@
using Robust.Shared.Prototypes; using Robust.Shared.Prototypes;
using Robust.Shared.Serialization.TypeSerializers.Implementations.Custom.Prototype; using Robust.Shared.Serialization.TypeSerializers.Implementations.Custom.Prototype;
using Content.Shared.Power;
namespace Content.Server.Power.Components namespace Content.Server.Power.Components
{ {

View File

@@ -1,7 +0,0 @@
namespace Content.Server.Power.Components;
[RegisterComponent]
public sealed partial class PowerMonitoringConsoleComponent : Component
{
}

View File

@@ -0,0 +1,89 @@
using Content.Server.NodeContainer;
using Content.Server.NodeContainer.NodeGroups;
using Content.Server.Power.EntitySystems;
using Content.Shared.Power;
namespace Content.Server.Power.Components;
/// <summary>
/// Used to flag any entities that should appear on a power monitoring console
/// </summary>
[RegisterComponent, Access(typeof(PowerMonitoringConsoleSystem))]
public sealed partial class PowerMonitoringDeviceComponent : Component
{
/// <summary>
/// Name of the node that this device draws its power from (see <see cref="NodeContainerComponent"/>)
/// </summary>
[DataField("sourceNode"), ViewVariables]
public string SourceNode = string.Empty;
/// <summary>
/// Name of the node that this device distributes power to (see <see cref="NodeContainerComponent"/>)
/// </summary>
[DataField("loadNode"), ViewVariables]
public string LoadNode = string.Empty;
/// <summary>
/// Names of the nodes that this device can potentially distributes power to (see <see cref="NodeContainerComponent"/>)
/// </summary>
[DataField("loadNodes"), ViewVariables]
public List<string>? LoadNodes;
/// <summary>
/// This entity will be grouped with entities that have the same collection name
/// </summary>
[DataField("collectionName"), ViewVariables]
public string CollectionName = string.Empty;
[ViewVariables]
public BaseNodeGroup? NodeGroup = null;
/// <summary>
/// Indicates whether the entity is/should be part of a collection
/// </summary>
public bool IsCollectionMasterOrChild { get { return CollectionName != string.Empty; } }
/// <summary>
/// Specifies the uid of the master that represents this entity
/// </summary>
/// <remarks>
/// Used when grouping multiple entities into a single power monitoring console entry
/// </remarks>
[ViewVariables]
public EntityUid CollectionMaster;
/// <summary>
/// Indicates if this entity represents a group of entities
/// </summary>
/// <remarks>
/// Used when grouping multiple entities into a single power monitoring console entry
/// </remarks>
public bool IsCollectionMaster { get { return Owner == CollectionMaster; } }
/// <summary>
/// A list of other entities that are to be represented by this entity
/// </summary>
/// /// <remarks>
/// Used when grouping multiple entities into a single power monitoring console entry
/// </remarks>
[ViewVariables]
public Dictionary<EntityUid, PowerMonitoringDeviceComponent> ChildDevices = new();
/// <summary>
/// Path to the .rsi folder
/// </summary>
[DataField("sprite"), ViewVariables]
public string SpritePath = string.Empty;
/// <summary>
/// The .rsi state
/// </summary>
[DataField("state"), ViewVariables]
public string SpriteState = string.Empty;
/// <summary>
/// Determines what power monitoring group this entity should belong to
/// </summary>
[DataField("group", required: true), ViewVariables]
public PowerMonitoringConsoleGroup Group;
}

View File

@@ -8,6 +8,7 @@ using Content.Shared.Interaction;
using Content.Shared.Tools; using Content.Shared.Tools;
using Content.Shared.Tools.Components; using Content.Shared.Tools.Components;
using Robust.Shared.Map; using Robust.Shared.Map;
using System.Xml.Schema;
using CableCuttingFinishedEvent = Content.Shared.Tools.Systems.CableCuttingFinishedEvent; using CableCuttingFinishedEvent = Content.Shared.Tools.Systems.CableCuttingFinishedEvent;
using SharedToolSystem = Content.Shared.Tools.Systems.SharedToolSystem; 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 StackSystem _stack = default!;
[Dependency] private readonly ElectrocutionSystem _electrocutionSystem = default!; [Dependency] private readonly ElectrocutionSystem _electrocutionSystem = default!;
[Dependency] private readonly IAdminLogManager _adminLogs = default!; [Dependency] private readonly IAdminLogManager _adminLogs = default!;
[Dependency] private readonly PowerMonitoringConsoleSystem _powerMonitoringSystem = default!;
public override void Initialize() public override void Initialize()
{ {
@@ -47,17 +49,24 @@ public sealed partial class CableSystem : EntitySystem
if (args.Cancelled) if (args.Cancelled)
return; return;
var xform = Transform(uid);
var ev = new CableAnchorStateChangedEvent(xform);
RaiseLocalEvent(uid, ref ev);
if (_electrocutionSystem.TryDoElectrifiedAct(uid, args.User)) if (_electrocutionSystem.TryDoElectrifiedAct(uid, args.User))
return; 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); QueueDel(uid);
} }
private void OnAnchorChanged(EntityUid uid, CableComponent cable, ref AnchorStateChangedEvent args) 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) if (args.Anchored)
return; // huh? it wasn't anchored? return; // huh? it wasn't anchored?

View File

@@ -1,4 +1,4 @@
using System.Threading; using System.Threading;
using Content.Server.StationEvents.Events; using Content.Server.StationEvents.Events;
namespace Content.Server.StationEvents.Components; namespace Content.Server.StationEvents.Components;
@@ -8,6 +8,7 @@ public sealed partial class PowerGridCheckRuleComponent : Component
{ {
public CancellationTokenSource? AnnounceCancelToken; public CancellationTokenSource? AnnounceCancelToken;
public EntityUid AffectedStation;
public readonly List<EntityUid> Powered = new(); public readonly List<EntityUid> Powered = new();
public readonly List<EntityUid> Unpowered = new(); public readonly List<EntityUid> Unpowered = new();

View File

@@ -24,6 +24,8 @@ namespace Content.Server.StationEvents.Events
if (!TryGetRandomStation(out var chosenStation)) if (!TryGetRandomStation(out var chosenStation))
return; return;
component.AffectedStation = chosenStation.Value;
var query = AllEntityQuery<ApcComponent, TransformComponent>(); var query = AllEntityQuery<ApcComponent, TransformComponent>();
while (query.MoveNext(out var apcUid ,out var apc, out var transform)) while (query.MoveNext(out var apcUid ,out var apc, out var transform))
{ {

View File

@@ -1,5 +1,3 @@
using Robust.Shared.GameStates;
namespace Content.Shared.Pinpointer; namespace Content.Shared.Pinpointer;
/// <summary> /// <summary>

View File

@@ -1,5 +1,4 @@
using Robust.Shared.GameStates; using Robust.Shared.GameStates;
using Robust.Shared.Timing;
namespace Content.Shared.Pinpointer; namespace Content.Shared.Pinpointer;

View File

@@ -0,0 +1,39 @@
using Robust.Shared.GameStates;
using Robust.Shared.Serialization;
namespace Content.Shared.Power;
[RegisterComponent, NetworkedComponent, AutoGenerateComponentState]
[Access(typeof(SharedPowerMonitoringConsoleSystem))]
public sealed partial class PowerMonitoringCableNetworksComponent : Component
{
/// <summary>
/// A dictionary of the all the nav map chunks that contain anchored power cables
/// </summary>
[ViewVariables, AutoNetworkedField]
public Dictionary<Vector2i, PowerCableChunk> AllChunks = new();
/// <summary>
/// A dictionary of the all the nav map chunks that contain anchored power cables
/// that are directly connected to the console's current focus
/// </summary>
[ViewVariables, AutoNetworkedField]
public Dictionary<Vector2i, PowerCableChunk> FocusChunks = new();
}
[Serializable, NetSerializable]
public struct PowerCableChunk
{
public readonly Vector2i Origin;
/// <summary>
/// Bitmask dictionary for power cables, 1 for occupied and 0 for empty.
/// </summary>
public int[] PowerCableData;
public PowerCableChunk(Vector2i origin)
{
Origin = origin;
PowerCableData = new int[3];
}
}

View File

@@ -1,4 +1,4 @@
using Robust.Shared.Serialization; using Robust.Shared.Serialization;
namespace Content.Shared.Power namespace Content.Shared.Power
{ {
@@ -23,4 +23,12 @@ namespace Content.Shared.Power
WireCount, WireCount,
CutWires CutWires
} }
[Serializable, NetSerializable]
public enum CableType
{
HighVoltage,
MediumVoltage,
Apc,
}
} }

View File

@@ -1,43 +1,159 @@
#nullable enable using Robust.Shared.GameStates;
using Robust.Shared.Map;
using Robust.Shared.Serialization; using Robust.Shared.Serialization;
namespace Content.Shared.Power; namespace Content.Shared.Power;
/// <summary>
/// Flags an entity as being a power monitoring console
/// </summary>
[RegisterComponent, NetworkedComponent, AutoGenerateComponentState]
[Access(typeof(SharedPowerMonitoringConsoleSystem), Other = AccessPermissions.ReadExecute)]
public sealed partial class PowerMonitoringConsoleComponent : Component
{
/// <summary>
/// The EntityUid of the device that is the console's current focus
/// </summary>
/// <remarks>
/// Not-networked - set by the console UI
/// </remarks>
[ViewVariables]
public EntityUid? Focus;
/// <summary>
/// The group that the device that is the console's current focus belongs to
/// </summary>
/// /// <remarks>
/// Not-networked - set by the console UI
/// </remarks>
[ViewVariables]
public PowerMonitoringConsoleGroup FocusGroup = PowerMonitoringConsoleGroup.Generator;
/// <summary>
/// A list of flags relating to currently active events of interest to the console.
/// E.g., power sinks, power net anomalies
/// </summary>
[ViewVariables, AutoNetworkedField]
public PowerMonitoringFlags Flags = PowerMonitoringFlags.None;
/// <summary>
/// A dictionary containing all the meta data for tracked power monitoring devices
/// </summary>
[ViewVariables, AutoNetworkedField]
public Dictionary<NetEntity, PowerMonitoringDeviceMetaData> PowerMonitoringDeviceMetaData = new();
}
[Serializable, NetSerializable]
public struct PowerMonitoringDeviceMetaData
{
public string EntityName;
public NetCoordinates Coordinates;
public PowerMonitoringConsoleGroup Group;
public string SpritePath;
public string SpriteState;
public NetEntity? CollectionMaster;
public PowerMonitoringDeviceMetaData(string name, NetCoordinates coordinates, PowerMonitoringConsoleGroup group, string spritePath, string spriteState)
{
EntityName = name;
Coordinates = coordinates;
Group = group;
SpritePath = spritePath;
SpriteState = spriteState;
}
}
/// <summary>
/// Data from by the server to the client for the power monitoring console UI
/// </summary>
[Serializable, NetSerializable] [Serializable, NetSerializable]
public sealed class PowerMonitoringConsoleBoundInterfaceState : BoundUserInterfaceState public sealed class PowerMonitoringConsoleBoundInterfaceState : BoundUserInterfaceState
{ {
public double TotalSources; public double TotalSources;
public double TotalBatteryUsage;
public double TotalLoads; public double TotalLoads;
public PowerMonitoringConsoleEntry[] Sources; public PowerMonitoringConsoleEntry[] AllEntries;
public PowerMonitoringConsoleEntry[] Loads; public PowerMonitoringConsoleEntry[] FocusSources;
public PowerMonitoringConsoleBoundInterfaceState(double totalSources, double totalLoads, PowerMonitoringConsoleEntry[] sources, PowerMonitoringConsoleEntry[] loads) public PowerMonitoringConsoleEntry[] FocusLoads;
public PowerMonitoringConsoleBoundInterfaceState
(double totalSources,
double totalBatteryUsage,
double totalLoads,
PowerMonitoringConsoleEntry[] allEntries,
PowerMonitoringConsoleEntry[] focusSources,
PowerMonitoringConsoleEntry[] focusLoads)
{ {
TotalSources = totalSources; TotalSources = totalSources;
TotalBatteryUsage = totalBatteryUsage;
TotalLoads = totalLoads; TotalLoads = totalLoads;
Sources = sources; AllEntries = allEntries;
Loads = loads; FocusSources = focusSources;
FocusLoads = focusLoads;
} }
} }
/// <summary>
/// Contains all the data needed to update a single device on the power monitoring UI
/// </summary>
[Serializable, NetSerializable] [Serializable, NetSerializable]
public sealed class PowerMonitoringConsoleEntry public struct PowerMonitoringConsoleEntry
{ {
public string NameLocalized; public NetEntity NetEntity;
public string IconEntityPrototypeId; public PowerMonitoringConsoleGroup Group;
public double Size; public double PowerValue;
public bool IsBattery;
public PowerMonitoringConsoleEntry(string nl, string ipi, double size, bool isBattery) [NonSerialized] public PowerMonitoringDeviceMetaData? MetaData = null;
public PowerMonitoringConsoleEntry(NetEntity netEntity, PowerMonitoringConsoleGroup group, double powerValue = 0d)
{ {
NameLocalized = nl; NetEntity = netEntity;
IconEntityPrototypeId = ipi; Group = group;
Size = size; PowerValue = powerValue;
IsBattery = isBattery;
} }
} }
/// <summary>
/// Triggers the server to send updated power monitoring console data to the client for the single player session
/// </summary>
[Serializable, NetSerializable]
public sealed class PowerMonitoringConsoleMessage : BoundUserInterfaceMessage
{
public NetEntity? FocusDevice;
public PowerMonitoringConsoleGroup FocusGroup;
public PowerMonitoringConsoleMessage(NetEntity? focusDevice, PowerMonitoringConsoleGroup focusGroup)
{
FocusDevice = focusDevice;
FocusGroup = focusGroup;
}
}
/// <summary>
/// Determines how entities are grouped and color coded on the power monitor
/// </summary>
public enum PowerMonitoringConsoleGroup : byte
{
Generator,
SMES,
Substation,
APC,
Consumer,
}
[Flags]
public enum PowerMonitoringFlags : byte
{
None = 0,
RoguePowerConsumer = 1,
PowerNetAbnormalities = 2,
}
/// <summary>
/// UI key associated with the power monitoring console
/// </summary>
[Serializable, NetSerializable] [Serializable, NetSerializable]
public enum PowerMonitoringConsoleUiKey public enum PowerMonitoringConsoleUiKey
{ {
Key Key
} }

View File

@@ -0,0 +1,8 @@
using JetBrains.Annotations;
namespace Content.Shared.Power;
[UsedImplicitly]
public abstract class SharedPowerMonitoringConsoleSystem : EntitySystem
{
}

View File

@@ -1,8 +1,27 @@
power-monitoring-window-title = Power Monitoring Console power-monitoring-window-title = Power Monitoring Console
power-monitoring-window-tab-sources = Sources
power-monitoring-window-tab-loads = Loads power-monitoring-window-label-sources = Sources
power-monitoring-window-total-sources = Total Sources: power-monitoring-window-label-smes = SMES
power-monitoring-window-total-loads = Total Loads: 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-value = { POWERWATTS($value) }
power-monitoring-window-show-inactive-consumers = Show Inactive Consumers 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]

View File

@@ -239,6 +239,7 @@
- type: Computer - type: Computer
board: PowerComputerCircuitboard board: PowerComputerCircuitboard
- type: PowerMonitoringConsole - type: PowerMonitoringConsole
- type: PowerMonitoringCableNetworks
- type: NodeContainer - type: NodeContainer
examinable: true examinable: true
nodes: nodes:
@@ -246,6 +247,7 @@
!type:CableDeviceNode !type:CableDeviceNode
nodeGroupID: HVPower nodeGroupID: HVPower
- type: ActivatableUI - type: ActivatableUI
singleUser: true
key: enum.PowerMonitoringConsoleUiKey.Key key: enum.PowerMonitoringConsoleUiKey.Key
- type: UserInterface - type: UserInterface
interfaces: interfaces:

View File

@@ -50,6 +50,12 @@
input: input:
!type:CableDeviceNode !type:CableDeviceNode
nodeGroupID: HVPower nodeGroupID: HVPower
- type: PowerMonitoringDevice
group: Generator
loadNode: input
collectionName: radiationCollector
sprite: Structures/Power/Generation/Singularity/collector.rsi
state: static
- type: RadiationCollector - type: RadiationCollector
chargeModifier: 7500 chargeModifier: 7500
radiationReactiveGases: radiationReactiveGases:

View File

@@ -78,8 +78,11 @@
input: input:
!type:CableDeviceNode !type:CableDeviceNode
nodeGroupID: HVPower nodeGroupID: HVPower
# - type: ApcPowerReceiver - type: PowerMonitoringDevice
# - type: ExtensionCableReceiver group: Generator
loadNode: input
sprite: Structures/Power/Generation/ame.rsi
state: static
- type: PowerSupplier - type: PowerSupplier
supplyRate: 0 supplyRate: 0
- type: ContainerContainer - type: ContainerContainer

View File

@@ -40,6 +40,11 @@
output: output:
!type:CableDeviceNode !type:CableDeviceNode
nodeGroupID: HVPower nodeGroupID: HVPower
- type: PowerMonitoringDevice
group: Generator
loadNode: output
sprite: Structures/Power/power.rsi
state: generator
- type: PowerSupplier - type: PowerSupplier
supplyRate: 3000 supplyRate: 3000
supplyRampRate: 500 supplyRampRate: 500
@@ -124,6 +129,11 @@
output: output:
!type:CableDeviceNode !type:CableDeviceNode
nodeGroupID: HVPower nodeGroupID: HVPower
- type: PowerMonitoringDevice
group: Generator
loadNode: output
sprite: Structures/Power/Generation/wallmount_generator.rsi
state: static
- type: PowerSupplier - type: PowerSupplier
supplyRate: 3000 supplyRate: 3000
supplyRampRate: 500 supplyRampRate: 500
@@ -229,6 +239,9 @@
- type: Sprite - type: Sprite
sprite: Structures/Power/Generation/rtg.rsi sprite: Structures/Power/Generation/rtg.rsi
state: rtg state: rtg
- type: PowerMonitoringDevice
sprite: Structures/Power/Generation/rtg.rsi
state: rtg
- type: AmbientSound - type: AmbientSound
range: 5 range: 5
sound: sound:
@@ -272,6 +285,9 @@
layers: layers:
- state: rtg_damaged - state: rtg_damaged
- state: rtg_glow - state: rtg_glow
- type: PowerMonitoringDevice
sprite: Structures/Power/Generation/rtg.rsi
state: rtg_damaged
- type: RadiationSource # ideally only when opened. - type: RadiationSource # ideally only when opened.
intensity: 2 intensity: 2
- type: Destructible - type: Destructible

View File

@@ -170,6 +170,13 @@
gasType: CarbonDioxide gasType: CarbonDioxide
# 2 moles of gas for every sheet of plasma. # 2 moles of gas for every sheet of plasma.
moleRatio: 2 moleRatio: 2
- type: PowerMonitoringDevice
group: Generator
loadNodes:
- output_hv
- output_mv
sprite: Structures/Power/Generation/portable_generator.rsi
state: portgen0
- type: entity - type: entity
name: S.U.P.E.R.P.A.C.M.A.N.-type portable generator name: S.U.P.E.R.P.A.C.M.A.N.-type portable generator
@@ -219,6 +226,13 @@
- type: UpgradePowerSupplier - type: UpgradePowerSupplier
powerSupplyMultiplier: 1.25 powerSupplyMultiplier: 1.25
scaling: Exponential scaling: Exponential
- type: PowerMonitoringDevice
group: Generator
loadNodes:
- output_hv
- output_mv
sprite: Structures/Power/Generation/portable_generator.rsi
state: portgen1
- type: entity - type: entity
name: J.R.P.A.C.M.A.N.-type portable generator name: J.R.P.A.C.M.A.N.-type portable generator
@@ -294,6 +308,11 @@
output: output:
!type:CableDeviceNode !type:CableDeviceNode
nodeGroupID: Apc nodeGroupID: Apc
- type: PowerMonitoringDevice
group: Generator
loadNode: output
sprite: Structures/Power/Generation/portable_generator.rsi
state: portgen3
- type: PowerSupplier - type: PowerSupplier
# No ramping needed on this bugger. # No ramping needed on this bugger.
voltage: Apc voltage: Apc

View File

@@ -36,6 +36,12 @@
output: output:
!type:CableDeviceNode !type:CableDeviceNode
nodeGroupID: HVPower nodeGroupID: HVPower
- type: PowerMonitoringDevice
group: Generator
loadNode: output
sprite: Structures/Power/Generation/solar_panel.rsi
state: static
collectionName: SolarPanel
- type: Anchorable - type: Anchorable
- type: Pullable - type: Pullable
- type: Electrified - type: Electrified

View File

@@ -51,6 +51,11 @@
teg: teg:
!type:TegNodeGenerator !type:TegNodeGenerator
nodeGroupID: Teg nodeGroupID: Teg
- type: PowerMonitoringDevice
group: Generator
loadNode: output
sprite: Structures/Power/Generation/teg.rsi
state: static
- type: Rotatable - type: Rotatable
# Note that only the TEG center is an AtmosDevice. # Note that only the TEG center is an AtmosDevice.

View File

@@ -65,6 +65,13 @@
output: output:
!type:CableDeviceNode !type:CableDeviceNode
nodeGroupID: Apc nodeGroupID: Apc
- type: PowerMonitoringDevice
group: APC
sourceNode: input
loadNode: output
collectionName: apc
sprite: Structures/Power/apc.rsi
state: static
- type: BatteryCharger - type: BatteryCharger
voltage: Medium voltage: Medium
- type: PowerProvider - type: PowerProvider

View File

@@ -43,15 +43,22 @@
examinable: true examinable: true
nodes: nodes:
input: input:
!type:CableDeviceNode
nodeGroupID: HVPower
output:
!type:CableTerminalPortNode !type:CableTerminalPortNode
nodeGroupID: HVPower 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 voltage: High
node: output node: output
- type: BatteryDischarger - type: BatteryCharger
voltage: High voltage: High
node: input node: input
- type: PowerNetworkBattery - type: PowerNetworkBattery

View File

@@ -40,6 +40,13 @@
output: output:
!type:CableDeviceNode !type:CableDeviceNode
nodeGroupID: MVPower nodeGroupID: MVPower
- type: PowerMonitoringDevice
group: Substation
sourceNode: input
loadNode: output
collectionName: substation
sprite: Structures/Power/substation.rsi
state: substation_static
- type: BatteryCharger - type: BatteryCharger
voltage: High voltage: High
- type: BatteryDischarger - type: BatteryDischarger
@@ -166,6 +173,12 @@
output: output:
!type:CableDeviceNode !type:CableDeviceNode
nodeGroupID: MVPower nodeGroupID: MVPower
- type: PowerMonitoringDevice
group: Substation
sourceNode: input
loadNode: output
sprite: Structures/Power/substation.rsi
state: substation_wall_static
- type: BatteryCharger - type: BatteryCharger
voltage: High voltage: High
- type: BatteryDischarger - type: BatteryDischarger

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.4 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 904 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.2 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 826 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 774 B

View File

@@ -12,6 +12,9 @@
}, },
{ {
"name": "ca_on" "name": "ca_on"
},
{
"name": "static"
}, },
{ {
"name": "ca_active", "name": "ca_active",

Binary file not shown.

After

Width:  |  Height:  |  Size: 664 B

View File

@@ -7,7 +7,7 @@
"y": 32 "y": 32
}, },
"states": [ "states": [
{ {
"name": "shield_0" "name": "shield_0"
}, },
{ {
@@ -86,6 +86,9 @@
}, },
{ {
"name": "control" "name": "control"
},
{
"name": "static"
}, },
{ {
"name": "control_critical", "name": "control_critical",

Binary file not shown.

After

Width:  |  Height:  |  Size: 618 B

View File

@@ -1 +1,30 @@
{"version":1,"license":"CC-BY-SA-3.0","copyright":"Taken from https://github.com/discordia-space/CEV-Eris/blob/d1e0161af146835f4fb79d21a6200caa9cc842d0/icons/obj/power.dmi and modified.","size":{"x":32,"y":32},"states":[{"name":"normal","select":[],"flags":{},"directions":8},{"name":"broken","select":[],"flags":{},"directions":1},{"name": "solar_assembly"},{"name": "solar_tracker"}]} {
"version":1,
"license":"CC-BY-SA-3.0",
"copyright":"Taken from https://github.com/discordia-space/CEV-Eris/blob/d1e0161af146835f4fb79d21a6200caa9cc842d0/icons/obj/power.dmi and modified.",
"size":{"x":32,"y":32},
"states":
[
{
"name": "normal",
"select": [],
"flags": {},
"directions": 8
},
{
"name": "broken",
"select": [],
"flags": {},
"directions": 1
},
{
"name": "static"
},
{
"name": "solar_assembly"
},
{
"name": "solar_tracker"
}
]
}

Binary file not shown.

After

Width:  |  Height:  |  Size: 223 B

View File

@@ -7,9 +7,12 @@
"y": 32 "y": 32
}, },
"states": [ "states": [
{ {
"name": "teg", "name": "teg",
"directions": 4 "directions": 4
},
{
"name": "static"
}, },
{ {
"name": "teg-op1", "name": "teg-op1",

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.0 KiB

View File

@@ -9,6 +9,9 @@
"states": [ "states": [
{ {
"name": "panel" "name": "panel"
},
{
"name": "static"
}, },
{ {
"name": "on", "name": "on",

Binary file not shown.

After

Width:  |  Height:  |  Size: 335 B

View File

@@ -9,6 +9,9 @@
"states": [ "states": [
{ {
"name": "base" "name": "base"
},
{
"name": "static"
}, },
{ {
"name": "broken" "name": "broken"

Binary file not shown.

After

Width:  |  Height:  |  Size: 384 B

View File

@@ -9,6 +9,9 @@
"states": [ "states": [
{ {
"name": "smes" "name": "smes"
},
{
"name": "static"
}, },
{ {
"name": "smes-open" "name": "smes-open"

Binary file not shown.

After

Width:  |  Height:  |  Size: 884 B

View File

@@ -10,9 +10,15 @@
{ {
"name": "substation" "name": "substation"
}, },
{
"name": "substation_static"
},
{ {
"name": "substation_wall" "name": "substation_wall"
}, },
{
"name": "substation_wall_static"
},
{ {
"name": "full" "name": "full"
}, },

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.3 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 444 B