diff --git a/Content.Client/Entry/IgnoredComponents.cs b/Content.Client/Entry/IgnoredComponents.cs index 61c31b7a6e..3de1604e03 100644 --- a/Content.Client/Entry/IgnoredComponents.cs +++ b/Content.Client/Entry/IgnoredComponents.cs @@ -138,6 +138,7 @@ namespace Content.Client.Entry "Flash", "Docking", "Telecrystal", + "PowerMonitoringConsole", "RCD", "RCDAmmo", "CursedEntityStorage", diff --git a/Content.Client/Power/PowerMonitoringWindow.xaml b/Content.Client/Power/PowerMonitoringWindow.xaml new file mode 100644 index 0000000000..826da19d90 --- /dev/null +++ b/Content.Client/Power/PowerMonitoringWindow.xaml @@ -0,0 +1,20 @@ + + + + + + + + + + + + + + + + diff --git a/Content.Client/Power/PowerMonitoringWindow.xaml.cs b/Content.Client/Power/PowerMonitoringWindow.xaml.cs new file mode 100644 index 0000000000..f3343fce7b --- /dev/null +++ b/Content.Client/Power/PowerMonitoringWindow.xaml.cs @@ -0,0 +1,85 @@ +using System; +using System.Linq; +using Content.Client.Computer; +using Content.Client.IoC; +using Content.Shared.Power; +using JetBrains.Annotations; +using Robust.Client.AutoGenerated; +using Robust.Client.GameObjects; +using Robust.Client.Graphics; +using Robust.Client.UserInterface; +using Robust.Client.UserInterface.Controls; +using Robust.Client.UserInterface.CustomControls; +using Robust.Client.UserInterface.XAML; +using Robust.Shared.GameObjects; +using Robust.Shared.IoC; +using Robust.Shared.Localization; +using Robust.Shared.Maths; +using Robust.Shared.Timing; +using Robust.Shared.Prototypes; + +namespace Content.Client.Power; + +[GenerateTypedNameReferences] +public sealed partial class PowerMonitoringWindow : DefaultWindow, IComputerWindow +{ + [Dependency] private readonly IPrototypeManager _prototypeManager = default!; + + public PowerMonitoringWindow() + { + RobustXamlLoader.Load(this); + SetSize = MinSize = (300, 450); + IoCManager.InjectDependencies(this); + MasterTabContainer.SetTabTitle(0, Loc.GetString("power-monitoring-window-tab-sources")); + MasterTabContainer.SetTabTitle(1, Loc.GetString("power-monitoring-window-tab-loads")); + } + + public void UpdateState(PowerMonitoringConsoleBoundInterfaceState scc) + { + 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); + } + + public void UpdateList(Label number, double numberVal, ItemList list, PowerMonitoringConsoleEntry[] listVal) + { + number.Text = Loc.GetString("power-monitoring-window-value", ("value", numberVal)); + // This magic is important to prevent scrolling issues. + while (list.Count > listVal.Length) + { + list.RemoveAt(list.Count - 1); + } + while (list.Count < listVal.Length) + { + list.AddItem("YOU SHOULD NEVER SEE THIS (REALLY!)", null, false); + } + // Now overwrite the items properly... + for (var i = 0; i < listVal.Length; i++) + { + var ent = listVal[i]; + _prototypeManager.TryIndex(ent.IconEntityPrototypeId, out EntityPrototype? entityPrototype); + IRsiStateLike? iconState = null; + if (entityPrototype != null) + iconState = SpriteComponent.GetPrototypeIcon(entityPrototype, StaticIoC.ResC); + var icon = iconState?.GetFrame(RSI.State.Direction.South, 0); + var item = list[i]; + item.Text = $"{ent.NameLocalized} {Loc.GetString("power-monitoring-window-value", ("value", ent.Size))}"; + item.Icon = icon; + } + } +} + +[UsedImplicitly] +public sealed class PowerMonitoringConsoleBoundUserInterface : ComputerBoundUserInterface +{ + public PowerMonitoringConsoleBoundUserInterface(ClientUserInterfaceComponent owner, object uiKey) : base(owner, uiKey) {} +} + diff --git a/Content.Server/Power/Components/PowerMonitoringConsoleComponent.cs b/Content.Server/Power/Components/PowerMonitoringConsoleComponent.cs new file mode 100644 index 0000000000..72137293bf --- /dev/null +++ b/Content.Server/Power/Components/PowerMonitoringConsoleComponent.cs @@ -0,0 +1,16 @@ +using System; +using System.Collections.Generic; +using Content.Server.Power.NodeGroups; +using Content.Server.Utility; +using Robust.Server.GameObjects; +using Robust.Shared.GameObjects; +using Robust.Shared.IoC; +using Robust.Shared.ViewVariables; + +namespace Content.Server.Power.Components; + +[RegisterComponent] +public sealed class PowerMonitoringConsoleComponent : Component +{ +} + diff --git a/Content.Server/Power/EntitySystems/PowerMonitoringConsoleSystem.cs b/Content.Server/Power/EntitySystems/PowerMonitoringConsoleSystem.cs new file mode 100644 index 0000000000..772107d29d --- /dev/null +++ b/Content.Server/Power/EntitySystems/PowerMonitoringConsoleSystem.cs @@ -0,0 +1,102 @@ +using Content.Shared.Popups; +using Content.Shared.Power; +using Content.Server.NodeContainer; +using Content.Server.NodeContainer.Nodes; +using Content.Server.Power.Components; +using Content.Server.Power.NodeGroups; +using Content.Server.UserInterface; +using Content.Server.WireHacking; +using JetBrains.Annotations; +using Robust.Server.GameObjects; + +namespace Content.Server.Power.EntitySystems; + +[UsedImplicitly] +internal sealed class PowerMonitoringConsoleSystem : EntitySystem +{ + private float _updateTimer = 0.0f; + private const float UpdateTime = 1.0f; + + [Dependency] + private UserInterfaceSystem _userInterfaceSystem = default!; + + public override void Update(float frameTime) + { + _updateTimer += frameTime; + if (_updateTimer >= UpdateTime) + { + _updateTimer -= UpdateTime; + foreach (var component in EntityQuery()) + { + UpdateUIState(component.Owner, component); + } + } + } + + public void UpdateUIState(EntityUid target, PowerMonitoringConsoleComponent? pmcComp = null, NodeContainerComponent? ncComp = null) + { + if (!Resolve(target, ref pmcComp)) + return; + if (!Resolve(target, ref ncComp)) + 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 md = MetaData(comp.Owner); + var prototype = md.EntityPrototype?.ID ?? ""; + return new PowerMonitoringConsoleEntry(md.EntityName, prototype, rate, isBattery); + } + // Right, so, here's what needs to be considered here. + var netQ = ncComp.GetNode("hv").NodeGroup as PowerNet; + if (netQ != null) + { + var net = netQ!; + foreach (PowerConsumerComponent pcc in net.Consumers) + { + loads.Add(LoadOrSource(pcc, pcc.DrawRate, false)); + totalLoads += pcc.DrawRate; + } + foreach (BatteryChargerComponent pcc in net.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 net.Suppliers) + { + sources.Add(LoadOrSource(pcc, pcc.MaxSupply, false)); + totalSources += pcc.MaxSupply; + } + foreach (BatteryDischargerComponent pcc in net.Dischargers) + { + if (!TryComp(pcc.Owner, out PowerNetworkBatteryComponent? batteryComp)) + { + continue; + } + var rate = batteryComp.NetworkBattery.CurrentSupply; + sources.Add(LoadOrSource(pcc, rate, true)); + totalSources += rate; + } + } + // Sort + loads.Sort(CompareLoadOrSources); + sources.Sort(CompareLoadOrSources); + // Actually set state. + var state = new PowerMonitoringConsoleBoundInterfaceState(totalSources, totalLoads, sources.ToArray(), loads.ToArray()); + _userInterfaceSystem.GetUiOrNull(target, PowerMonitoringConsoleUiKey.Key)?.SetState(state); + } + + private int CompareLoadOrSources(PowerMonitoringConsoleEntry x, PowerMonitoringConsoleEntry y) + { + return -x.Size.CompareTo(y.Size); + } +} + diff --git a/Content.Shared/Power/SharedPowerMonitoringConsoleComponent.cs b/Content.Shared/Power/SharedPowerMonitoringConsoleComponent.cs new file mode 100644 index 0000000000..f2b3011b5a --- /dev/null +++ b/Content.Shared/Power/SharedPowerMonitoringConsoleComponent.cs @@ -0,0 +1,46 @@ +#nullable enable +using System; +using Robust.Shared.GameObjects; +using Robust.Shared.Maths; +using Robust.Shared.Serialization; + +namespace Content.Shared.Power; + +[Serializable, NetSerializable] +public sealed class PowerMonitoringConsoleBoundInterfaceState : BoundUserInterfaceState +{ + public double TotalSources; + public double TotalLoads; + public PowerMonitoringConsoleEntry[] Sources; + public PowerMonitoringConsoleEntry[] Loads; + public PowerMonitoringConsoleBoundInterfaceState(double totalSources, double totalLoads, PowerMonitoringConsoleEntry[] sources, PowerMonitoringConsoleEntry[] loads) + { + TotalSources = totalSources; + TotalLoads = totalLoads; + Sources = sources; + Loads = loads; + } +} + +[Serializable, NetSerializable] +public sealed class PowerMonitoringConsoleEntry +{ + public string NameLocalized; + public string IconEntityPrototypeId; + public double Size; + public bool IsBattery; + public PowerMonitoringConsoleEntry(string nl, string ipi, double size, bool isBattery) + { + NameLocalized = nl; + IconEntityPrototypeId = ipi; + Size = size; + IsBattery = isBattery; + } +} + +[Serializable, NetSerializable] +public enum PowerMonitoringConsoleUiKey +{ + Key +} + diff --git a/Resources/Locale/en-US/components/power-monitoring-component.ftl b/Resources/Locale/en-US/components/power-monitoring-component.ftl new file mode 100644 index 0000000000..ade9e2d933 --- /dev/null +++ b/Resources/Locale/en-US/components/power-monitoring-component.ftl @@ -0,0 +1,8 @@ +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-value = { POWERWATTS($value) } +power-monitoring-window-show-inactive-consumers = Show Inactive Consumers + diff --git a/Resources/Prototypes/Entities/Objects/Devices/Circuitboards/computer.yml b/Resources/Prototypes/Entities/Objects/Devices/Circuitboards/computer.yml index e27b4314f4..6fb9327302 100644 --- a/Resources/Prototypes/Entities/Objects/Devices/Circuitboards/computer.yml +++ b/Resources/Prototypes/Entities/Objects/Devices/Circuitboards/computer.yml @@ -66,6 +66,14 @@ - type: ComputerBoard prototype: ComputerSupplyRequest +- type: entity + parent: BaseComputerCircuitboard + id: PowerMonitoringComputerCircuitboard + name: power monitoring computer board + components: + - type: ComputerBoard + prototype: ComputerPowerMonitoring + - type: entity parent: BaseComputerCircuitboard id: ResearchComputerCircuitboard diff --git a/Resources/Prototypes/Entities/Structures/Machines/Computers/computers.yml b/Resources/Prototypes/Entities/Structures/Machines/Computers/computers.yml index 44108cf513..ad95457b1d 100644 --- a/Resources/Prototypes/Entities/Structures/Machines/Computers/computers.yml +++ b/Resources/Prototypes/Entities/Structures/Machines/Computers/computers.yml @@ -75,6 +75,19 @@ color: "#c9c042" - type: Computer board: PowerComputerCircuitboard + - type: PowerMonitoringConsole + - type: NodeContainer + examinable: true + nodes: + hv: + !type:CableDeviceNode + nodeGroupID: HVPower + - type: ActivatableUI + key: enum.PowerMonitoringConsoleUiKey.Key + - type: UserInterface + interfaces: + - key: enum.PowerMonitoringConsoleUiKey.Key + type: PowerMonitoringConsoleBoundUserInterface - type: entity parent: ComputerBase