From 6a06fed29569e74f5a97e82cc7251ed1538816be Mon Sep 17 00:00:00 2001 From: TrylobyteWasTaken Date: Tue, 17 Mar 2026 03:58:00 -0400 Subject: [PATCH 1/3] Project Menlo Port (#1017) * Project Menlo Port Ports the stuff we want from Frontier's Project Menlo, without the stuff we don't! * Battery (SMES/substation) interface (#36386) * Add ENERGYWATTHOURS() loc function Takes in joules (energy), displays as watt-hours. * Add simple OnOffButton control * Re-add Inset style class This was sloppily removed at some point?? Whatever, I need it. * Add helper functions for setting title/guidebook IDs on FancyWindow Reagent dispenser uses these, more in the next commits. * Add BuiPredictionState helper This enables me to implement coarse prediction manually in the battery UI. Basically it's a local buffer of predicted inputs that can easily be replayed against future BUI states from the server. * Add input coalescing infrastructure I ran into the following problem: Robust's Slider control absolutely *spams* input events, to such a degree that it actually causes issues for the networking layer if directly passed through. For something like a slider, we just need to send the most recent value. There is no good way for us to handle this in the control itself, as it *really* needs to happen in PreEngine. For simplicity reasons (for BUIs) I came to the conclusion it's best if it's there, as it's *before* any new states from the server can be applied. We can't just do this in Update() or something on the control as the timing just doesn't line up. I made a content system, BuiPreTickUpdateSystem, that runs in the ModRunLevel.PreEngine phase to achieve this. It runs a method on a new IBuiPreTickUpdate interface on all open BUIs. They can then implement their own coalescing logic. In the simplest case, this coalescing logic can just be "save the last value, and if we have any new value since the last update, send an input event." This is what the new InputCoalescer type is for. Adding new coalescing logic should be possible in the future, of course. It's all just small helpers. * Battery interface This adds a proper interface to batteries (SMES/substation). Players can turn IO on and off, and they can change charge and discharge rate. There's also a ton of numbers and stuff. It looks great. This actually enables charge and discharge rates to be changed for these devices. The settings for both have been set between 5kW and 150kW. * Oops, forgot to remove these style class defs. # Conflicts: # Content.Client/Entry/EntryPoint.cs # Content.Client/Stylesheets/StyleNano.cs # Content.Client/UserInterface/BuiPreTickUpdateSystem.cs # Content.Client/UserInterface/BuiPredictionState.cs # Content.Client/UserInterface/Controls/FancyWindow.xaml.cs # Content.Client/UserInterface/InputCoalescer.cs # Content.Shared/Localizations/ContentLocalizationManager.cs * Update StyleNano.cs --------- Co-authored-by: Pieter-Jan Briers --- .../UI/ReagentDispenserBoundUserInterface.cs | 4 +- Content.Client/Entry/EntryPoint.cs | 11 + .../Battery/BatteryBoundUserInterface.cs | 85 ++++++ Content.Client/Power/Battery/BatteryMenu.xaml | 146 +++++++++ .../Power/Battery/BatteryMenu.xaml.cs | 280 ++++++++++++++++++ Content.Client/Stylesheets/StyleNano.cs | 5 + .../UserInterface/BuiPreTickUpdateSystem.cs | 2 +- .../UserInterface/BuiPredictionState.cs | 2 +- .../UserInterface/Controls/OnOffButton.xaml | 6 + .../Controls/OnOffButton.xaml.cs | 48 +++ .../UserInterface/InputCoalescer.cs | 2 +- .../EntitySpawnPowerConsumerSystem.cs | 4 + .../AdjustablePowerDrawBoundUserInterface.cs | 54 ++++ .../_NF/Power/AdjustablePowerDrawMenu.xaml | 28 ++ .../_NF/Power/AdjustablePowerDrawMenu.xaml.cs | 130 ++++++++ .../Components/BatteryInterfaceComponent.cs | 37 +++ .../EntitySystems/BatteryInterfaceSystem.cs | 120 ++++++++ .../GasSpawnPowerConsumerComponent.cs | 116 ++++++++ .../EntitySpawnPowerConsumerSystem.cs | 233 +++++++++++++++ .../GasSpawnPowerConsumerSystem.cs | 225 ++++++++++++++ .../Components/PowerTransmissionComponent.cs | 108 +++++++ .../EntitySystems/PowerTransmissionSystem.cs | 191 ++++++++++++ .../ContentLocalizationManager.cs | 17 +- Content.Shared/Power/SharedBattery.cs | 82 +++++ Content.Shared/_NF/Bank/BUI/NFLedgerState.cs | 1 + .../EntitySpawnPowerConsumerComponent.cs | 130 ++++++++ .../SharedEntitySpawnPowerConsumerSystem.cs | 24 ++ .../_NF/Manufacturing/SharedManufacturing.cs | 12 + .../_NF/Power/SharedAdjustablePowerDraw.cs | 42 +++ Resources/Audio/_NF/Ambience/attributions.yml | 9 +- Resources/Audio/_NF/Ambience/power-hum.ogg | Bin 0 -> 26518 bytes .../en-US/_NF/cartridge-loader/cartridges.ftl | 1 + .../en-US/_NF/devices/device-network.ftl | 1 + .../entity-spawn-power-consumer-component.ftl | 2 + .../gas-spawn-power-consumer-component.ftl | 2 + .../adjustable-power-draw-component.ftl | 7 + .../power-transmission-component.ftl | 2 + Resources/Locale/en-US/_lib.ftl | 12 +- Resources/Locale/en-US/power/battery.ftl | 22 ++ Resources/Locale/en-US/ui/controls.ftl | 3 + .../Entities/Structures/Power/smes.yml | 13 + .../Entities/Structures/Power/substation.yml | 13 + .../Entities/Objects/Power/antimatter_jar.yml | 16 + .../Entities/Structures/Machines/ame_fab.yml | 138 +++++++++ .../Structures/Piping/Atmospherics/unary.yml | 116 ++++++++ .../_NF/Entities/Structures/Power/smes.yml | 68 +++++ .../Structures/Power/transmission_point.yml | 100 +++++++ .../Power/big_ame_jar.rsi/inhand-left.png | Bin 0 -> 957 bytes .../Power/big_ame_jar.rsi/inhand-right.png | Bin 0 -> 749 bytes .../_NF/Objects/Power/big_ame_jar.rsi/jar.png | Bin 0 -> 491 bytes .../Objects/Power/big_ame_jar.rsi/meta.json | 22 ++ .../AMEFab/32x32.rsi/dial-animated.png | Bin 0 -> 430 bytes .../Machines/AMEFab/32x32.rsi/dial.png | Bin 0 -> 183 bytes .../Machines/AMEFab/32x32.rsi/inserting.png | Bin 0 -> 243 bytes .../AMEFab/32x32.rsi/material-good.png | Bin 0 -> 131 bytes .../AMEFab/32x32.rsi/material-low.png | Bin 0 -> 131 bytes .../Machines/AMEFab/32x32.rsi/meta.json | 59 ++++ .../Machines/AMEFab/32x32.rsi/panel-unlit.png | Bin 0 -> 179 bytes .../Machines/AMEFab/32x32.rsi/slot-full.png | Bin 0 -> 432 bytes .../Machines/AMEFab/32x32.rsi/slot-unlit.png | Bin 0 -> 128 bytes .../AMEFab/64x32.rsi/flywheel-animated.png | Bin 0 -> 934 bytes .../Machines/AMEFab/64x32.rsi/flywheel.png | Bin 0 -> 469 bytes .../Machines/AMEFab/64x32.rsi/meta.json | 47 +++ .../AMEFab/64x32.rsi/press-animated.png | Bin 0 -> 493 bytes .../Machines/AMEFab/64x32.rsi/press.png | Bin 0 -> 177 bytes .../Machines/AMEFab/base.rsi/base.png | Bin 0 -> 2610 bytes .../Machines/AMEFab/base.rsi/meta.json | 14 + .../GasConverter/32x32.rsi/fan-animated.png | Bin 0 -> 375 bytes .../Machines/GasConverter/32x32.rsi/fan.png | Bin 0 -> 250 bytes .../32x32.rsi/front-panel-unlit.png | Bin 0 -> 207 bytes .../Machines/GasConverter/32x32.rsi/gas.png | Bin 0 -> 140 bytes .../Machines/GasConverter/32x32.rsi/meta.json | 57 ++++ .../GasConverter/32x32.rsi/panel-unlit.png | Bin 0 -> 454 bytes .../GasConverter/32x32.rsi/window-unlit.png | Bin 0 -> 891 bytes .../Machines/GasConverter/base.rsi/base.png | Bin 0 -> 2786 bytes .../Machines/GasConverter/base.rsi/meta.json | 14 + .../TransmissionPoint/32x32.rsi/icon.png | Bin 0 -> 757 bytes .../TransmissionPoint/32x32.rsi/meta.json | 24 ++ .../32x32.rsi/panel-unlit.png | Bin 0 -> 252 bytes .../96x32.rsi/base-foreground.png | Bin 0 -> 173 bytes .../96x32.rsi/base-unlit.png | Bin 0 -> 1274 bytes .../TransmissionPoint/96x32.rsi/meta.json | 25 ++ .../Power/TransmissionPoint/base.rsi/base.png | Bin 0 -> 4473 bytes .../TransmissionPoint/base.rsi/meta.json | 14 + .../Structures/Power/bles.rsi/bles-glass.png | Bin 0 -> 134 bytes .../Structures/Power/bles.rsi/bles-oc0.png | Bin 0 -> 126 bytes .../Structures/Power/bles.rsi/bles-oc1.png | Bin 0 -> 173 bytes .../Structures/Power/bles.rsi/bles-oc2.png | Bin 0 -> 173 bytes .../Structures/Power/bles.rsi/bles-og1.png | Bin 0 -> 121 bytes .../Structures/Power/bles.rsi/bles-og2.png | Bin 0 -> 131 bytes .../Structures/Power/bles.rsi/bles-og3.png | Bin 0 -> 136 bytes .../Structures/Power/bles.rsi/bles-og4.png | Bin 0 -> 154 bytes .../Structures/Power/bles.rsi/bles-og5.png | Bin 0 -> 162 bytes .../Structures/Power/bles.rsi/bles-op0.png | Bin 0 -> 120 bytes .../Structures/Power/bles.rsi/bles-op1.png | Bin 0 -> 166 bytes .../Structures/Power/bles.rsi/bles-op2.png | Bin 0 -> 129 bytes .../Structures/Power/bles.rsi/bles-open.png | Bin 0 -> 350 bytes .../Structures/Power/bles.rsi/bles-unlit.png | Bin 0 -> 676 bytes .../_NF/Structures/Power/bles.rsi/bles.png | Bin 0 -> 2179 bytes .../_NF/Structures/Power/bles.rsi/meta.json | 90 ++++++ 100 files changed, 3027 insertions(+), 9 deletions(-) create mode 100644 Content.Client/Power/Battery/BatteryBoundUserInterface.cs create mode 100644 Content.Client/Power/Battery/BatteryMenu.xaml create mode 100644 Content.Client/Power/Battery/BatteryMenu.xaml.cs create mode 100644 Content.Client/UserInterface/Controls/OnOffButton.xaml create mode 100644 Content.Client/UserInterface/Controls/OnOffButton.xaml.cs create mode 100644 Content.Client/_NF/Manufacturing/EntitySystems/EntitySpawnPowerConsumerSystem.cs create mode 100644 Content.Client/_NF/Power/AdjustablePowerDrawBoundUserInterface.cs create mode 100644 Content.Client/_NF/Power/AdjustablePowerDrawMenu.xaml create mode 100644 Content.Client/_NF/Power/AdjustablePowerDrawMenu.xaml.cs create mode 100644 Content.Server/Power/Components/BatteryInterfaceComponent.cs create mode 100644 Content.Server/Power/EntitySystems/BatteryInterfaceSystem.cs create mode 100644 Content.Server/_NF/Manufacturing/Components/GasSpawnPowerConsumerComponent.cs create mode 100644 Content.Server/_NF/Manufacturing/EntitySystems/EntitySpawnPowerConsumerSystem.cs create mode 100644 Content.Server/_NF/Manufacturing/EntitySystems/GasSpawnPowerConsumerSystem.cs create mode 100644 Content.Server/_NF/Power/Components/PowerTransmissionComponent.cs create mode 100644 Content.Server/_NF/Power/EntitySystems/PowerTransmissionSystem.cs create mode 100644 Content.Shared/Power/SharedBattery.cs create mode 100644 Content.Shared/_NF/Manufacturing/Components/EntitySpawnPowerConsumerComponent.cs create mode 100644 Content.Shared/_NF/Manufacturing/EntitySystems/SharedEntitySpawnPowerConsumerSystem.cs create mode 100644 Content.Shared/_NF/Manufacturing/SharedManufacturing.cs create mode 100644 Content.Shared/_NF/Power/SharedAdjustablePowerDraw.cs create mode 100644 Resources/Audio/_NF/Ambience/power-hum.ogg create mode 100644 Resources/Locale/en-US/_NF/manufacturing/entity-spawn-power-consumer-component.ftl create mode 100644 Resources/Locale/en-US/_NF/manufacturing/gas-spawn-power-consumer-component.ftl create mode 100644 Resources/Locale/en-US/_NF/power/components/adjustable-power-draw-component.ftl create mode 100644 Resources/Locale/en-US/_NF/power/components/power-transmission-component.ftl create mode 100644 Resources/Locale/en-US/power/battery.ftl create mode 100644 Resources/Locale/en-US/ui/controls.ftl create mode 100644 Resources/Prototypes/_NF/Entities/Objects/Power/antimatter_jar.yml create mode 100644 Resources/Prototypes/_NF/Entities/Structures/Machines/ame_fab.yml create mode 100644 Resources/Prototypes/_NF/Entities/Structures/Power/smes.yml create mode 100644 Resources/Prototypes/_NF/Entities/Structures/Power/transmission_point.yml create mode 100644 Resources/Textures/_NF/Objects/Power/big_ame_jar.rsi/inhand-left.png create mode 100644 Resources/Textures/_NF/Objects/Power/big_ame_jar.rsi/inhand-right.png create mode 100644 Resources/Textures/_NF/Objects/Power/big_ame_jar.rsi/jar.png create mode 100644 Resources/Textures/_NF/Objects/Power/big_ame_jar.rsi/meta.json create mode 100644 Resources/Textures/_NF/Structures/Machines/AMEFab/32x32.rsi/dial-animated.png create mode 100644 Resources/Textures/_NF/Structures/Machines/AMEFab/32x32.rsi/dial.png create mode 100644 Resources/Textures/_NF/Structures/Machines/AMEFab/32x32.rsi/inserting.png create mode 100644 Resources/Textures/_NF/Structures/Machines/AMEFab/32x32.rsi/material-good.png create mode 100644 Resources/Textures/_NF/Structures/Machines/AMEFab/32x32.rsi/material-low.png create mode 100644 Resources/Textures/_NF/Structures/Machines/AMEFab/32x32.rsi/meta.json create mode 100644 Resources/Textures/_NF/Structures/Machines/AMEFab/32x32.rsi/panel-unlit.png create mode 100644 Resources/Textures/_NF/Structures/Machines/AMEFab/32x32.rsi/slot-full.png create mode 100644 Resources/Textures/_NF/Structures/Machines/AMEFab/32x32.rsi/slot-unlit.png create mode 100644 Resources/Textures/_NF/Structures/Machines/AMEFab/64x32.rsi/flywheel-animated.png create mode 100644 Resources/Textures/_NF/Structures/Machines/AMEFab/64x32.rsi/flywheel.png create mode 100644 Resources/Textures/_NF/Structures/Machines/AMEFab/64x32.rsi/meta.json create mode 100644 Resources/Textures/_NF/Structures/Machines/AMEFab/64x32.rsi/press-animated.png create mode 100644 Resources/Textures/_NF/Structures/Machines/AMEFab/64x32.rsi/press.png create mode 100644 Resources/Textures/_NF/Structures/Machines/AMEFab/base.rsi/base.png create mode 100644 Resources/Textures/_NF/Structures/Machines/AMEFab/base.rsi/meta.json create mode 100644 Resources/Textures/_NF/Structures/Machines/GasConverter/32x32.rsi/fan-animated.png create mode 100644 Resources/Textures/_NF/Structures/Machines/GasConverter/32x32.rsi/fan.png create mode 100644 Resources/Textures/_NF/Structures/Machines/GasConverter/32x32.rsi/front-panel-unlit.png create mode 100644 Resources/Textures/_NF/Structures/Machines/GasConverter/32x32.rsi/gas.png create mode 100644 Resources/Textures/_NF/Structures/Machines/GasConverter/32x32.rsi/meta.json create mode 100644 Resources/Textures/_NF/Structures/Machines/GasConverter/32x32.rsi/panel-unlit.png create mode 100644 Resources/Textures/_NF/Structures/Machines/GasConverter/32x32.rsi/window-unlit.png create mode 100644 Resources/Textures/_NF/Structures/Machines/GasConverter/base.rsi/base.png create mode 100644 Resources/Textures/_NF/Structures/Machines/GasConverter/base.rsi/meta.json create mode 100644 Resources/Textures/_NF/Structures/Power/TransmissionPoint/32x32.rsi/icon.png create mode 100644 Resources/Textures/_NF/Structures/Power/TransmissionPoint/32x32.rsi/meta.json create mode 100644 Resources/Textures/_NF/Structures/Power/TransmissionPoint/32x32.rsi/panel-unlit.png create mode 100644 Resources/Textures/_NF/Structures/Power/TransmissionPoint/96x32.rsi/base-foreground.png create mode 100644 Resources/Textures/_NF/Structures/Power/TransmissionPoint/96x32.rsi/base-unlit.png create mode 100644 Resources/Textures/_NF/Structures/Power/TransmissionPoint/96x32.rsi/meta.json create mode 100644 Resources/Textures/_NF/Structures/Power/TransmissionPoint/base.rsi/base.png create mode 100644 Resources/Textures/_NF/Structures/Power/TransmissionPoint/base.rsi/meta.json create mode 100644 Resources/Textures/_NF/Structures/Power/bles.rsi/bles-glass.png create mode 100644 Resources/Textures/_NF/Structures/Power/bles.rsi/bles-oc0.png create mode 100644 Resources/Textures/_NF/Structures/Power/bles.rsi/bles-oc1.png create mode 100644 Resources/Textures/_NF/Structures/Power/bles.rsi/bles-oc2.png create mode 100644 Resources/Textures/_NF/Structures/Power/bles.rsi/bles-og1.png create mode 100644 Resources/Textures/_NF/Structures/Power/bles.rsi/bles-og2.png create mode 100644 Resources/Textures/_NF/Structures/Power/bles.rsi/bles-og3.png create mode 100644 Resources/Textures/_NF/Structures/Power/bles.rsi/bles-og4.png create mode 100644 Resources/Textures/_NF/Structures/Power/bles.rsi/bles-og5.png create mode 100644 Resources/Textures/_NF/Structures/Power/bles.rsi/bles-op0.png create mode 100644 Resources/Textures/_NF/Structures/Power/bles.rsi/bles-op1.png create mode 100644 Resources/Textures/_NF/Structures/Power/bles.rsi/bles-op2.png create mode 100644 Resources/Textures/_NF/Structures/Power/bles.rsi/bles-open.png create mode 100644 Resources/Textures/_NF/Structures/Power/bles.rsi/bles-unlit.png create mode 100644 Resources/Textures/_NF/Structures/Power/bles.rsi/bles.png create mode 100644 Resources/Textures/_NF/Structures/Power/bles.rsi/meta.json diff --git a/Content.Client/Chemistry/UI/ReagentDispenserBoundUserInterface.cs b/Content.Client/Chemistry/UI/ReagentDispenserBoundUserInterface.cs index 2ad1b718887..b0f2a77eed6 100644 --- a/Content.Client/Chemistry/UI/ReagentDispenserBoundUserInterface.cs +++ b/Content.Client/Chemistry/UI/ReagentDispenserBoundUserInterface.cs @@ -1,4 +1,5 @@ using Content.Client.Guidebook.Components; +using Content.Client.UserInterface.Controls; using Content.Shared.Chemistry; using Content.Shared.Containers.ItemSlots; using JetBrains.Annotations; @@ -31,8 +32,7 @@ protected override void Open() // Setup window layout/elements _window = this.CreateWindow(); - _window.Title = EntMan.GetComponent(Owner).EntityName; - _window.HelpGuidebookIds = EntMan.GetComponent(Owner).Guides; + _window.SetInfoFromEntity(EntMan, Owner); // Setup static button actions. _window.EjectButton.OnPressed += _ => SendMessage(new ItemSlotButtonPressedEvent(SharedReagentDispenser.OutputSlotName)); diff --git a/Content.Client/Entry/EntryPoint.cs b/Content.Client/Entry/EntryPoint.cs index acf96e6d771..dec48d69ef9 100644 --- a/Content.Client/Entry/EntryPoint.cs +++ b/Content.Client/Entry/EntryPoint.cs @@ -20,6 +20,7 @@ using Content.Client.Screenshot; using Content.Client.Singularity; using Content.Client.Stylesheets; +using Content.Client.UserInterface; using Content.Client.Viewport; using Content.Client.Voting; using Content.Shared.Ame.Components; @@ -77,6 +78,7 @@ public sealed class EntryPoint : GameClient [Dependency] private readonly DebugMonitorManager _debugMonitorManager = default!; [Dependency] private readonly TitleWindowManager _titleWindowManager = default!; [Dependency] private readonly DiscordAuthManager _discordAuth = default!; // Floofstation + [Dependency] private readonly IEntitySystemManager _entitySystemManager = default!; public override void Init() { @@ -237,6 +239,15 @@ public override void Update(ModUpdateLevel level, FrameEventArgs frameEventArgs) { _debugMonitorManager.FrameUpdate(); } + + if (level == ModUpdateLevel.PreEngine) + { + if (_baseClient.RunLevel is ClientRunLevel.InGame or ClientRunLevel.SinglePlayerGame) + { + var updateSystem = _entitySystemManager.GetEntitySystem(); + updateSystem.RunUpdates(); + } + } } } } diff --git a/Content.Client/Power/Battery/BatteryBoundUserInterface.cs b/Content.Client/Power/Battery/BatteryBoundUserInterface.cs new file mode 100644 index 00000000000..561fe90e408 --- /dev/null +++ b/Content.Client/Power/Battery/BatteryBoundUserInterface.cs @@ -0,0 +1,85 @@ +using Content.Client.UserInterface; +using Content.Shared.Power; +using JetBrains.Annotations; +using Robust.Client.Timing; +using Robust.Client.UserInterface; + +namespace Content.Client.Power.Battery; + +/// +/// BUI for . +/// +/// +/// +[UsedImplicitly] +public sealed class BatteryBoundUserInterface : BoundUserInterface, IBuiPreTickUpdate +{ + [Dependency] private readonly IClientGameTiming _gameTiming = null!; + + [ViewVariables] + private BatteryMenu? _menu; + + private BuiPredictionState? _pred; + private InputCoalescer _chargeRateCoalescer; + private InputCoalescer _dischargeRateCoalescer; + + public BatteryBoundUserInterface(EntityUid owner, Enum uiKey) : base(owner, uiKey) + { + IoCManager.InjectDependencies(this); + } + + protected override void Open() + { + base.Open(); + + _pred = new BuiPredictionState(this, _gameTiming); + + _menu = this.CreateWindow(); + _menu.SetEntity(Owner); + + _menu.OnInBreaker += val => _pred!.SendMessage(new BatterySetInputBreakerMessage(val)); + _menu.OnOutBreaker += val => _pred!.SendMessage(new BatterySetOutputBreakerMessage(val)); + + _menu.OnChargeRate += val => _chargeRateCoalescer.Set(val); + _menu.OnDischargeRate += val => _dischargeRateCoalescer.Set(val); + } + + void IBuiPreTickUpdate.PreTickUpdate() + { + if (_chargeRateCoalescer.CheckIsModified(out var chargeRateValue)) + _pred!.SendMessage(new BatterySetChargeRateMessage(chargeRateValue)); + + if (_dischargeRateCoalescer.CheckIsModified(out var dischargeRateValue)) + _pred!.SendMessage(new BatterySetDischargeRateMessage(dischargeRateValue)); + } + + protected override void UpdateState(BoundUserInterfaceState state) + { + if (state is not BatteryBuiState batteryState) + return; + + foreach (var replayMsg in _pred!.MessagesToReplay()) + { + switch (replayMsg) + { + case BatterySetInputBreakerMessage setInputBreaker: + batteryState.CanCharge = setInputBreaker.On; + break; + + case BatterySetOutputBreakerMessage setOutputBreaker: + batteryState.CanDischarge = setOutputBreaker.On; + break; + + case BatterySetChargeRateMessage setChargeRate: + batteryState.MaxChargeRate = setChargeRate.Rate; + break; + + case BatterySetDischargeRateMessage setDischargeRate: + batteryState.MaxSupply = setDischargeRate.Rate; + break; + } + } + + _menu?.Update(batteryState); + } +} diff --git a/Content.Client/Power/Battery/BatteryMenu.xaml b/Content.Client/Power/Battery/BatteryMenu.xaml new file mode 100644 index 00000000000..83483a517e3 --- /dev/null +++ b/Content.Client/Power/Battery/BatteryMenu.xaml @@ -0,0 +1,146 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/Content.Client/Power/Battery/BatteryMenu.xaml.cs b/Content.Client/Power/Battery/BatteryMenu.xaml.cs new file mode 100644 index 00000000000..78cc669fd04 --- /dev/null +++ b/Content.Client/Power/Battery/BatteryMenu.xaml.cs @@ -0,0 +1,280 @@ +using System.Diagnostics.CodeAnalysis; +using Content.Client.Stylesheets; +using Content.Client.UserInterface.Controls; +using Content.Shared.Power; +using Content.Shared.Rounding; +using Robust.Client.AutoGenerated; +using Robust.Client.Graphics; +using Robust.Client.UserInterface.Controls; +using Robust.Client.UserInterface.XAML; +using Robust.Shared.Timing; + +namespace Content.Client.Power.Battery; + +/// +/// Interface control for batteries. +/// +/// +[GenerateTypedNameReferences] +public sealed partial class BatteryMenu : FancyWindow +{ + // Cutoff for the ETA time to switch from "~" to ">" and cap out. + private const float MaxEtaValueMinutes = 60; + // Cutoff where ETA times likely don't make sense and it's better to just say "N/A". + private const float NotApplicableEtaHighCutoffMinutes = 1000; + private const float NotApplicableEtaLowCutoffMinutes = 0.01f; + // Fudge factor to ignore small charge/discharge values, that are likely caused by floating point rounding errors. + private const float PrecisionRoundFactor = 100_000; + + // Colors used for the storage cell bar graphic. + private static readonly Color[] StorageColors = + [ + StyleNano.DangerousRedFore, + Color.FromHex("#C49438"), + Color.FromHex("#B3BF28"), + StyleNano.GoodGreenFore, + ]; + + // StorageColors but dimmed for "off" bars. + private static readonly Color[] DimStorageColors = + [ + DimStorageColor(StorageColors[0]), + DimStorageColor(StorageColors[1]), + DimStorageColor(StorageColors[2]), + DimStorageColor(StorageColors[3]), + ]; + + // Parameters for the sine wave pulsing animations for active power lines in the UI. + private static readonly Color ActivePowerLineHighColor = Color.FromHex("#CCC"); + private static readonly Color ActivePowerLineLowColor = Color.FromHex("#888"); + private const float PowerPulseFactor = 4; + + // Dependencies + [Dependency] private readonly IEntityManager _entityManager = null!; + [Dependency] private readonly ILocalizationManager _loc = null!; + + // Active and inactive style boxes for power lines. + // We modify _activePowerLineStyleBox's properties programmatically to implement the pulsing animation. + private readonly StyleBoxFlat _activePowerLineStyleBox = new(); + private readonly StyleBoxFlat _inactivePowerLineStyleBox = new() { BackgroundColor = Color.FromHex("#555") }; + + // Style boxes for the storage cell bar graphic. + // We modify the properties of these to change the bars' colors. + private StyleBoxFlat[] _chargeMeterBoxes; + + // State for the powerline pulsing animation. + private float _powerPulseValue; + + // State for the storage cell bar graphic and its blinking effect. + private float _blinkPulseValue; + private bool _blinkPulse; + private int _storageLevel; + private bool _hasStorageDelta; + + // The entity that this UI is for. + private EntityUid _entity; + + // Used to avoid sending input events when updating slider values. + private bool _suppressSliderEvents; + + // Events for the BUI to subscribe to. + public event Action? OnInBreaker; + public event Action? OnOutBreaker; + + public event Action? OnChargeRate; + public event Action? OnDischargeRate; + + public BatteryMenu() + { + RobustXamlLoader.Load(this); + IoCManager.InjectDependencies(this); + + InitChargeMeter(); + + InBreaker.StateChanged += val => OnInBreaker?.Invoke(val); + OutBreaker.StateChanged += val => OnOutBreaker?.Invoke(val); + + ChargeRateSlider.OnValueChanged += _ => + { + if (!_suppressSliderEvents) + OnChargeRate?.Invoke(ChargeRateSlider.Value); + }; + DischargeRateSlider.OnValueChanged += _ => + { + if (!_suppressSliderEvents) + OnDischargeRate?.Invoke(DischargeRateSlider.Value); + }; + } + + public void SetEntity(EntityUid entity) + { + _entity = entity; + + this.SetInfoFromEntity(_entityManager, _entity); + + EntityView.SetEntity(entity); + } + + [MemberNotNull(nameof(_chargeMeterBoxes))] + public void InitChargeMeter() + { + _chargeMeterBoxes = new StyleBoxFlat[StorageColors.Length]; + + for (var i = StorageColors.Length - 1; i >= 0; i--) + { + var styleBox = new StyleBoxFlat(); + _chargeMeterBoxes[i] = styleBox; + + for (var j = 0; j < ChargeMeter.Columns; j++) + { + var control = new PanelContainer + { + Margin = new Thickness(2), + PanelOverride = styleBox, + HorizontalExpand = true, + VerticalExpand = true, + }; + ChargeMeter.AddChild(control); + } + } + } + + public void Update(BatteryBuiState msg) + { + var inValue = msg.CurrentReceiving; + var outValue = msg.CurrentSupply; + + var storageDelta = inValue - outValue; + // Mask rounding errors in power code. + if (Math.Abs(storageDelta) < msg.Capacity / PrecisionRoundFactor) + storageDelta = 0; + + // Update power lines based on a ton of parameters. + SetPowerLineState(InPowerLine, msg.SupplyingNetworkHasPower); + SetPowerLineState(OutPowerLine, msg.LoadingNetworkHasPower); + SetPowerLineState(InSecondPowerLine, msg.SupplyingNetworkHasPower && msg.CanCharge); + SetPowerLineState(ChargePowerLine, msg.SupplyingNetworkHasPower && msg.CanCharge && storageDelta > 0); + SetPowerLineState(PassthroughPowerLine, msg.SupplyingNetworkHasPower && msg.CanCharge && msg.CanDischarge); + SetPowerLineState(OutSecondPowerLine, + msg.CanDischarge && (msg.Charge > 0 || msg.SupplyingNetworkHasPower && msg.CanCharge)); + SetPowerLineState(DischargePowerLine, storageDelta < 0); + + // Update breakers. + InBreaker.IsOn = msg.CanCharge; + OutBreaker.IsOn = msg.CanDischarge; + + // Update various power values. + InValue.Text = FormatPower(inValue); + OutValue.Text = FormatPower(outValue); + PassthroughValue.Text = FormatPower(Math.Min(msg.CurrentReceiving, msg.CurrentSupply)); + ChargeMaxValue.Text = FormatPower(msg.MaxChargeRate); + DischargeMaxValue.Text = FormatPower(msg.MaxSupply); + ChargeCurrentValue.Text = FormatPower(Math.Max(0, storageDelta)); + DischargeCurrentValue.Text = FormatPower(Math.Max(0, -storageDelta)); + + // Update charge/discharge rate sliders. + _suppressSliderEvents = true; + ChargeRateSlider.MaxValue = msg.MaxMaxChargeRate; + ChargeRateSlider.MinValue = msg.MinMaxChargeRate; + ChargeRateSlider.Value = msg.MaxChargeRate; + + DischargeRateSlider.MaxValue = msg.MaxMaxSupply; + DischargeRateSlider.MinValue = msg.MinMaxSupply; + DischargeRateSlider.Value = msg.MaxSupply; + _suppressSliderEvents = false; + + // Update ETA display. + var storageEtaDiff = storageDelta > 0 ? (msg.Capacity - msg.Charge) * (1 / msg.Efficiency) : -msg.Charge; + var etaTimeSeconds = storageEtaDiff / storageDelta; + var etaTimeMinutes = etaTimeSeconds / 60.0; + + EtaLabel.Text = _loc.GetString( + storageDelta > 0 ? "battery-menu-eta-full" : "battery-menu-eta-empty"); + if (!double.IsFinite(etaTimeMinutes) + || Math.Abs(etaTimeMinutes) > NotApplicableEtaHighCutoffMinutes + || Math.Abs(etaTimeMinutes) < NotApplicableEtaLowCutoffMinutes) + { + EtaValue.Text = _loc.GetString("battery-menu-eta-value-na"); + } + else + { + EtaValue.Text = _loc.GetString( + etaTimeMinutes > MaxEtaValueMinutes ? "battery-menu-eta-value-max" : "battery-menu-eta-value", + ("minutes", Math.Min(Math.Ceiling(etaTimeMinutes), MaxEtaValueMinutes))); + } + + // Update storage display. + StoredPercentageValue.Text = _loc.GetString( + "battery-menu-stored-percent-value", + ("value", msg.Charge / msg.Capacity)); + StoredEnergyValue.Text = _loc.GetString( + "battery-menu-stored-energy-value", + ("value", msg.Charge)); + + // Update charge meter. + _storageLevel = ContentHelpers.RoundToNearestLevels(msg.Charge, msg.Capacity, _chargeMeterBoxes.Length); + _hasStorageDelta = Math.Abs(storageDelta) > 0; + } + + private static Color DimStorageColor(Color color) + { + var hsv = Color.ToHsv(color); + hsv.Z /= 5; + return Color.FromHsv(hsv); + } + + private void SetPowerLineState(PanelContainer control, bool value) + { + control.PanelOverride = value ? _activePowerLineStyleBox : _inactivePowerLineStyleBox; + } + + private string FormatPower(float value) + { + return _loc.GetString("battery-menu-power-value", ("value", value)); + } + + protected override void FrameUpdate(FrameEventArgs args) + { + base.FrameUpdate(args); + + // Pulse power lines. + _powerPulseValue += args.DeltaSeconds * PowerPulseFactor; + + var color = Color.InterpolateBetween( + ActivePowerLineLowColor, + ActivePowerLineHighColor, + MathF.Sin(_powerPulseValue) / 2 + 1); + _activePowerLineStyleBox.BackgroundColor = color; + + // Update storage indicator and blink it. + for (var i = 0; i < _chargeMeterBoxes.Length; i++) + { + var box = _chargeMeterBoxes[i]; + if (_storageLevel > i) + { + // On + box.BackgroundColor = StorageColors[i]; + } + else + { + box.BackgroundColor = DimStorageColors[i]; + } + } + + _blinkPulseValue += args.DeltaSeconds; + if (_blinkPulseValue > 1) + { + _blinkPulseValue -= 1; + _blinkPulse ^= true; + } + + // If there is a storage delta (charging or discharging), we want to blink the highest bar. + if (_hasStorageDelta) + { + // If there is no highest bar (UI completely at 0), then blink bar 0. + var toBlink = Math.Max(0, _storageLevel - 1); + _chargeMeterBoxes[toBlink].BackgroundColor = + _blinkPulse ? StorageColors[toBlink] : DimStorageColors[toBlink]; + } + } +} diff --git a/Content.Client/Stylesheets/StyleNano.cs b/Content.Client/Stylesheets/StyleNano.cs index cde97bc9ecb..be66c4add64 100644 --- a/Content.Client/Stylesheets/StyleNano.cs +++ b/Content.Client/Stylesheets/StyleNano.cs @@ -66,6 +66,7 @@ public sealed class StyleNano : StyleBase public const string StyleClassChatChannelSelectorButton = "chatSelectorOptionButton"; public const string StyleClassChatFilterOptionButton = "chatFilterOptionButton"; public const string StyleClassStorageButton = "storageButton"; + public const string StyleClassInset = "Inset"; public const string StyleClassConsoleHeading = "ConsoleHeading"; public const string StyleClassConsoleSubHeading = "ConsoleSubHeading"; @@ -2028,6 +2029,10 @@ public StyleNano(IResourceCache resCache) : base(resCache) { Modulate = ButtonColorGoodDefault }), + + Element() + .Class(StyleClassInset) + .Prop(PanelContainer.StylePropertyPanel, insetBack), }).ToList()); } } diff --git a/Content.Client/UserInterface/BuiPreTickUpdateSystem.cs b/Content.Client/UserInterface/BuiPreTickUpdateSystem.cs index 632894c2aaa..330cb51dcc8 100644 --- a/Content.Client/UserInterface/BuiPreTickUpdateSystem.cs +++ b/Content.Client/UserInterface/BuiPreTickUpdateSystem.cs @@ -1,4 +1,4 @@ -using Robust.Client.GameObjects; +using Robust.Client.GameObjects; using Robust.Client.Player; using Robust.Shared.ContentPack; using Robust.Shared.Timing; diff --git a/Content.Client/UserInterface/BuiPredictionState.cs b/Content.Client/UserInterface/BuiPredictionState.cs index bd1342e1e0d..e3299e03f3e 100644 --- a/Content.Client/UserInterface/BuiPredictionState.cs +++ b/Content.Client/UserInterface/BuiPredictionState.cs @@ -1,4 +1,4 @@ -using System.Linq; +using System.Linq; using Robust.Client.Timing; using Robust.Shared.Timing; diff --git a/Content.Client/UserInterface/Controls/OnOffButton.xaml b/Content.Client/UserInterface/Controls/OnOffButton.xaml new file mode 100644 index 00000000000..f642e709ec2 --- /dev/null +++ b/Content.Client/UserInterface/Controls/OnOffButton.xaml @@ -0,0 +1,6 @@ + + +