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<T> 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.
This commit is contained in:
committed by
GitHub
parent
791f7af5d4
commit
ffe130b38d
@@ -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 @@ namespace Content.Client.Chemistry.UI
|
||||
|
||||
// Setup window layout/elements
|
||||
_window = this.CreateWindow<ReagentDispenserWindow>();
|
||||
_window.Title = EntMan.GetComponent<MetaDataComponent>(Owner).EntityName;
|
||||
_window.HelpGuidebookIds = EntMan.GetComponent<GuideHelpComponent>(Owner).Guides;
|
||||
_window.SetInfoFromEntity(EntMan, Owner);
|
||||
|
||||
// Setup static button actions.
|
||||
_window.EjectButton.OnPressed += _ => SendMessage(new ItemSlotButtonPressedEvent(SharedReagentDispenser.OutputSlotName));
|
||||
|
||||
@@ -19,6 +19,7 @@ using Content.Client.Replay;
|
||||
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;
|
||||
@@ -72,6 +73,7 @@ namespace Content.Client.Entry
|
||||
[Dependency] private readonly ILogManager _logManager = default!;
|
||||
[Dependency] private readonly DebugMonitorManager _debugMonitorManager = default!;
|
||||
[Dependency] private readonly TitleWindowManager _titleWindowManager = default!;
|
||||
[Dependency] private readonly IEntitySystemManager _entitySystemManager = default!;
|
||||
|
||||
public override void Init()
|
||||
{
|
||||
@@ -224,6 +226,15 @@ namespace Content.Client.Entry
|
||||
{
|
||||
_debugMonitorManager.FrameUpdate();
|
||||
}
|
||||
|
||||
if (level == ModUpdateLevel.PreEngine)
|
||||
{
|
||||
if (_baseClient.RunLevel is ClientRunLevel.InGame or ClientRunLevel.SinglePlayerGame)
|
||||
{
|
||||
var updateSystem = _entitySystemManager.GetEntitySystem<BuiPreTickUpdateSystem>();
|
||||
updateSystem.RunUpdates();
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
85
Content.Client/Power/Battery/BatteryBoundUserInterface.cs
Normal file
85
Content.Client/Power/Battery/BatteryBoundUserInterface.cs
Normal file
@@ -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;
|
||||
|
||||
/// <summary>
|
||||
/// BUI for <see cref="BatteryUiKey.Key"/>.
|
||||
/// </summary>
|
||||
/// <seealso cref="BoundUserInterfaceState"/>
|
||||
/// <seealso cref="BatteryMenu"/>
|
||||
[UsedImplicitly]
|
||||
public sealed class BatteryBoundUserInterface : BoundUserInterface, IBuiPreTickUpdate
|
||||
{
|
||||
[Dependency] private readonly IClientGameTiming _gameTiming = null!;
|
||||
|
||||
[ViewVariables]
|
||||
private BatteryMenu? _menu;
|
||||
|
||||
private BuiPredictionState? _pred;
|
||||
private InputCoalescer<float> _chargeRateCoalescer;
|
||||
private InputCoalescer<float> _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<BatteryMenu>();
|
||||
_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);
|
||||
}
|
||||
}
|
||||
146
Content.Client/Power/Battery/BatteryMenu.xaml
Normal file
146
Content.Client/Power/Battery/BatteryMenu.xaml
Normal file
@@ -0,0 +1,146 @@
|
||||
<controls:FancyWindow xmlns="https://spacestation14.io"
|
||||
xmlns:controls="clr-namespace:Content.Client.UserInterface.Controls"
|
||||
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
|
||||
xmlns:gfx="clr-namespace:Robust.Client.Graphics;assembly=Robust.Client"
|
||||
SetSize="650 330"
|
||||
Resizable="False">
|
||||
<BoxContainer Orientation="Vertical">
|
||||
<!-- Top row: main content -->
|
||||
<BoxContainer Name="MainContent" Orientation="Horizontal" VerticalExpand="True" Margin="4">
|
||||
<!-- Left pane: I/O, passthrough, sprite view -->
|
||||
<BoxContainer Name="IOPane" Orientation="Vertical" HorizontalExpand="True">
|
||||
<!-- Top row: input -->
|
||||
<BoxContainer Name="InputRow" Orientation="Horizontal">
|
||||
<!-- Input power line -->
|
||||
<PanelContainer Name="InPowerLine" SetHeight="2" VerticalAlignment="Top" SetWidth="32"
|
||||
Margin="2 16" />
|
||||
|
||||
<!-- Box with breaker, label, values -->
|
||||
<PanelContainer HorizontalExpand="True" StyleClasses="Inset">
|
||||
<BoxContainer Orientation="Vertical">
|
||||
<BoxContainer Orientation="Horizontal">
|
||||
<Label Text="{Loc 'battery-menu-in'}" HorizontalExpand="True" VerticalAlignment="Top"
|
||||
StyleClasses="LabelKeyText" />
|
||||
<controls:OnOffButton Name="InBreaker" />
|
||||
</BoxContainer>
|
||||
<Label Name="InValue" />
|
||||
</BoxContainer>
|
||||
</PanelContainer>
|
||||
</BoxContainer>
|
||||
|
||||
<!-- Middle row: Entity view & passthrough -->
|
||||
<BoxContainer Name="MiddleRow" Orientation="Horizontal" VerticalExpand="True">
|
||||
<SpriteView Name="EntityView" SetSize="64 64" Scale="2 2" OverrideDirection="South" Margin="15" />
|
||||
|
||||
<BoxContainer Orientation="Vertical" VerticalAlignment="Center" HorizontalExpand="True"
|
||||
HorizontalAlignment="Right">
|
||||
<Label HorizontalAlignment="Right" Text="{Loc 'battery-menu-passthrough'}" StyleClasses="StatusFieldTitle" />
|
||||
<Label HorizontalAlignment="Right" Name="PassthroughValue" />
|
||||
</BoxContainer>
|
||||
</BoxContainer>
|
||||
|
||||
<!-- Bottom row: output -->
|
||||
<BoxContainer Name="OutputRow" Orientation="Horizontal">
|
||||
<!-- Output power line -->
|
||||
<PanelContainer Name="OutPowerLine" SetHeight="2" VerticalAlignment="Bottom" SetWidth="32"
|
||||
Margin="2 16" />
|
||||
|
||||
<!-- Box with breaker, label, values -->
|
||||
<PanelContainer HorizontalExpand="True" StyleClasses="Inset">
|
||||
<BoxContainer Orientation="Vertical">
|
||||
<BoxContainer Orientation="Horizontal">
|
||||
<Label Text="{Loc 'battery-menu-out'}" HorizontalExpand="True" VerticalAlignment="Top"
|
||||
StyleClasses="LabelKeyText" />
|
||||
<controls:OnOffButton Name="OutBreaker" />
|
||||
</BoxContainer>
|
||||
<Label Name="OutValue" />
|
||||
</BoxContainer>
|
||||
</PanelContainer>
|
||||
</BoxContainer>
|
||||
</BoxContainer>
|
||||
|
||||
<!-- Separator connecting panes with some wires -->
|
||||
<BoxContainer Orientation="Vertical" SetWidth="22" Margin="2 16">
|
||||
<PanelContainer Name="InSecondPowerLine" SetHeight="2" />
|
||||
<PanelContainer Name="PassthroughPowerLine" SetWidth="2" HorizontalAlignment="Center" VerticalExpand="True" />
|
||||
<PanelContainer Name="OutSecondPowerLine" SetHeight="2" />
|
||||
</BoxContainer>
|
||||
|
||||
<!-- Middle pane: charge/discharge -->
|
||||
<BoxContainer Name="ChargeDischarge" Orientation="Vertical" HorizontalExpand="True">
|
||||
<!-- Charge -->
|
||||
<PanelContainer VerticalExpand="True" StyleClasses="Inset" Margin="0 0 0 8">
|
||||
<BoxContainer Orientation="Vertical">
|
||||
<Label Text="{Loc 'battery-menu-charge-header'}" StyleClasses="LabelKeyText" />
|
||||
<BoxContainer Orientation="Vertical" VerticalExpand="True" VerticalAlignment="Center">
|
||||
<Slider Name="ChargeRateSlider" />
|
||||
<BoxContainer Orientation="Horizontal">
|
||||
<Label Text="{Loc 'battery-menu-max'}" StyleClasses="StatusFieldTitle" HorizontalExpand="True" />
|
||||
<Label Name="ChargeMaxValue" />
|
||||
</BoxContainer>
|
||||
</BoxContainer>
|
||||
<BoxContainer Orientation="Horizontal">
|
||||
<Label Text="{Loc 'battery-menu-current'}" StyleClasses="StatusFieldTitle" HorizontalExpand="True" />
|
||||
<Label Name="ChargeCurrentValue" />
|
||||
</BoxContainer>
|
||||
</BoxContainer>
|
||||
</PanelContainer>
|
||||
<!-- Discharge -->
|
||||
<PanelContainer VerticalExpand="True" StyleClasses="Inset">
|
||||
<BoxContainer Orientation="Vertical">
|
||||
<Label Text="{Loc 'battery-menu-discharge-header'}" StyleClasses="LabelKeyText" />
|
||||
<BoxContainer Orientation="Vertical" VerticalExpand="True" VerticalAlignment="Center">
|
||||
<Slider Name="DischargeRateSlider" />
|
||||
<BoxContainer Orientation="Horizontal">
|
||||
<Label Text="{Loc 'battery-menu-max'}" StyleClasses="StatusFieldTitle" HorizontalExpand="True" />
|
||||
<Label Name="DischargeMaxValue" />
|
||||
</BoxContainer>
|
||||
</BoxContainer>
|
||||
<BoxContainer Orientation="Horizontal">
|
||||
<Label Text="{Loc 'battery-menu-current'}" StyleClasses="StatusFieldTitle" HorizontalExpand="True" />
|
||||
<Label Name="DischargeCurrentValue" />
|
||||
</BoxContainer>
|
||||
</BoxContainer>
|
||||
</PanelContainer>
|
||||
</BoxContainer>
|
||||
|
||||
<!-- Separator connecting panes with some wires -->
|
||||
<BoxContainer Orientation="Vertical" SetWidth="22" Margin="2 16">
|
||||
<PanelContainer Name="ChargePowerLine" SetHeight="2" VerticalAlignment="Top" VerticalExpand="True" />
|
||||
<PanelContainer Name="DischargePowerLine" SetHeight="2" VerticalAlignment="Bottom" VerticalExpand="True" />
|
||||
</BoxContainer>
|
||||
|
||||
<!-- Right pane: storage -->
|
||||
<PanelContainer Name="Storage" StyleClasses="Inset" HorizontalExpand="True">
|
||||
<BoxContainer Orientation="Vertical">
|
||||
<Label Text="{Loc 'battery-menu-storage-header'}" StyleClasses="LabelKeyText" />
|
||||
<GridContainer Columns="2">
|
||||
<Label Text="{Loc 'battery-menu-stored'}" StyleClasses="StatusFieldTitle" />
|
||||
<Label Name="StoredPercentageValue" HorizontalAlignment="Right" HorizontalExpand="True" />
|
||||
<Label Text="{Loc 'battery-menu-energy'}" StyleClasses="StatusFieldTitle" />
|
||||
<Label Name="StoredEnergyValue" HorizontalAlignment="Right" />
|
||||
<Label Name="EtaLabel" StyleClasses="StatusFieldTitle" />
|
||||
<Label Name="EtaValue" HorizontalAlignment="Right" />
|
||||
</GridContainer>
|
||||
|
||||
<!-- Charge meter -->
|
||||
<GridContainer Name="ChargeMeter" Columns="3" VerticalExpand="True" Margin="0 24 0 0">
|
||||
|
||||
</GridContainer>
|
||||
</BoxContainer>
|
||||
</PanelContainer>
|
||||
</BoxContainer>
|
||||
|
||||
<!-- Footer -->
|
||||
<BoxContainer Name="Footer" Orientation="Vertical">
|
||||
<PanelContainer StyleClasses="LowDivider" />
|
||||
<BoxContainer Orientation="Horizontal" Margin="10 2 5 0" VerticalAlignment="Bottom">
|
||||
<Label Text="{Loc 'battery-menu-footer-left'}" StyleClasses="WindowFooterText" />
|
||||
<Label Text="{Loc 'battery-menu-footer-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>
|
||||
</controls:FancyWindow>
|
||||
280
Content.Client/Power/Battery/BatteryMenu.xaml.cs
Normal file
280
Content.Client/Power/Battery/BatteryMenu.xaml.cs
Normal file
@@ -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;
|
||||
|
||||
/// <summary>
|
||||
/// Interface control for batteries.
|
||||
/// </summary>
|
||||
/// <seealso cref="BatteryBoundUserInterface"/>
|
||||
[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<bool>? OnInBreaker;
|
||||
public event Action<bool>? OnOutBreaker;
|
||||
|
||||
public event Action<float>? OnChargeRate;
|
||||
public event Action<float>? 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];
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -66,6 +66,7 @@ namespace Content.Client.Stylesheets
|
||||
public const string StyleClassChatChannelSelectorButton = "chatSelectorOptionButton";
|
||||
public const string StyleClassChatFilterOptionButton = "chatFilterOptionButton";
|
||||
public const string StyleClassStorageButton = "storageButton";
|
||||
public const string StyleClassInset = "Inset";
|
||||
|
||||
public const string StyleClassSliderRed = "Red";
|
||||
public const string StyleClassSliderGreen = "Green";
|
||||
@@ -1675,7 +1676,11 @@ namespace Content.Client.Stylesheets
|
||||
new[]
|
||||
{
|
||||
new StyleProperty(TextureButton.StylePropertyTexture, resCache.GetTexture("/Textures/Interface/Bwoink/un_pinned.png"))
|
||||
})
|
||||
}),
|
||||
|
||||
Element<PanelContainer>()
|
||||
.Class(StyleClassInset)
|
||||
.Prop(PanelContainer.StylePropertyPanel, insetBack),
|
||||
}).ToList());
|
||||
}
|
||||
}
|
||||
|
||||
75
Content.Client/UserInterface/BuiPreTickUpdateSystem.cs
Normal file
75
Content.Client/UserInterface/BuiPreTickUpdateSystem.cs
Normal file
@@ -0,0 +1,75 @@
|
||||
using Robust.Client.GameObjects;
|
||||
using Robust.Client.Player;
|
||||
using Robust.Shared.ContentPack;
|
||||
using Robust.Shared.Timing;
|
||||
using Robust.Shared.Utility;
|
||||
|
||||
namespace Content.Client.UserInterface;
|
||||
|
||||
/// <summary>
|
||||
/// Interface for <see cref="BoundUserInterface"/>s that need some updating logic
|
||||
/// ran in the <see cref="ModUpdateLevel.PreEngine"/> stage.
|
||||
/// </summary>
|
||||
/// <remarks>
|
||||
/// <para>
|
||||
/// This is called on all open <see cref="BoundUserInterface"/>s that implement this interface.
|
||||
/// </para>
|
||||
/// <para>
|
||||
/// One intended use case is coalescing input events (e.g. via <see cref="InputCoalescer{T}"/>) to send them to the
|
||||
/// server only once per tick.
|
||||
/// </para>
|
||||
/// </remarks>
|
||||
/// <seealso cref="BuiPreTickUpdateSystem"/>
|
||||
public interface IBuiPreTickUpdate
|
||||
{
|
||||
void PreTickUpdate();
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Implements <see cref="BuiPreTickUpdateSystem"/>.
|
||||
/// </summary>
|
||||
public sealed class BuiPreTickUpdateSystem : EntitySystem
|
||||
{
|
||||
[Dependency] private readonly IPlayerManager _playerManager = null!;
|
||||
[Dependency] private readonly UserInterfaceSystem _uiSystem = null!;
|
||||
[Dependency] private readonly IGameTiming _gameTiming = null!;
|
||||
|
||||
private EntityQuery<UserInterfaceUserComponent> _userQuery;
|
||||
|
||||
public override void Initialize()
|
||||
{
|
||||
base.Initialize();
|
||||
|
||||
_userQuery = GetEntityQuery<UserInterfaceUserComponent>();
|
||||
}
|
||||
|
||||
public void RunUpdates()
|
||||
{
|
||||
if (!_gameTiming.IsFirstTimePredicted)
|
||||
return;
|
||||
|
||||
var localSession = _playerManager.LocalSession;
|
||||
if (localSession?.AttachedEntity is not { } localEntity)
|
||||
return;
|
||||
|
||||
if (!_userQuery.TryGetComponent(localEntity, out var userUIComp))
|
||||
return;
|
||||
|
||||
foreach (var (entity, uis) in userUIComp.OpenInterfaces)
|
||||
{
|
||||
foreach (var key in uis)
|
||||
{
|
||||
if (!_uiSystem.TryGetOpenUi(entity, key, out var ui))
|
||||
{
|
||||
DebugTools.Assert("Unable to find UI that was in the open UIs list??");
|
||||
continue;
|
||||
}
|
||||
|
||||
if (ui is IBuiPreTickUpdate tickUpdate)
|
||||
{
|
||||
tickUpdate.PreTickUpdate();
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
80
Content.Client/UserInterface/BuiPredictionState.cs
Normal file
80
Content.Client/UserInterface/BuiPredictionState.cs
Normal file
@@ -0,0 +1,80 @@
|
||||
using System.Linq;
|
||||
using Robust.Client.Timing;
|
||||
using Robust.Shared.Timing;
|
||||
|
||||
namespace Content.Client.UserInterface;
|
||||
|
||||
/// <summary>
|
||||
/// A local buffer for <see cref="BoundUserInterface"/>s to manually implement prediction.
|
||||
/// </summary>
|
||||
/// <remarks>
|
||||
/// <para>
|
||||
/// In many current (and future) cases, it is not practically possible to implement prediction for UIs
|
||||
/// by implementing the logic in shared. At the same time, we want to implement prediction for the best user experience
|
||||
/// (and it is sometimes the easiest way to make even a middling user experience).
|
||||
/// </para>
|
||||
/// <para>
|
||||
/// You can queue predicted messages into this class with <see cref="SendMessage"/>,
|
||||
/// and then call <see cref="MessagesToReplay"/> later from <see cref="BoundUserInterface.UpdateState"/>
|
||||
/// to get all messages that are still "ahead" of the latest server state.
|
||||
/// These messages can then manually be "applied" to the latest state received from the server.
|
||||
/// </para>
|
||||
/// <para>
|
||||
/// Note that this system only works if the server is guaranteed to send some kind of update in response to UI messages,
|
||||
/// or at a regular schedule. If it does not, there is no opportunity to error correct the prediction.
|
||||
/// </para>
|
||||
/// </remarks>
|
||||
public sealed class BuiPredictionState
|
||||
{
|
||||
private readonly BoundUserInterface _parent;
|
||||
private readonly IClientGameTiming _gameTiming;
|
||||
|
||||
private readonly Queue<MessageData> _queuedMessages = new();
|
||||
|
||||
public BuiPredictionState(BoundUserInterface parent, IClientGameTiming gameTiming)
|
||||
{
|
||||
_parent = parent;
|
||||
_gameTiming = gameTiming;
|
||||
}
|
||||
|
||||
public void SendMessage(BoundUserInterfaceMessage message)
|
||||
{
|
||||
if (_gameTiming.IsFirstTimePredicted)
|
||||
{
|
||||
var messageData = new MessageData
|
||||
{
|
||||
TickSent = _gameTiming.CurTick,
|
||||
Message = message,
|
||||
};
|
||||
|
||||
_queuedMessages.Enqueue(messageData);
|
||||
}
|
||||
|
||||
_parent.SendPredictedMessage(message);
|
||||
}
|
||||
|
||||
public IEnumerable<BoundUserInterfaceMessage> MessagesToReplay()
|
||||
{
|
||||
var curTick = _gameTiming.LastRealTick;
|
||||
while (_queuedMessages.TryPeek(out var data) && data.TickSent <= curTick)
|
||||
{
|
||||
_queuedMessages.Dequeue();
|
||||
}
|
||||
|
||||
if (_queuedMessages.Count == 0)
|
||||
return [];
|
||||
|
||||
return _queuedMessages.Select(c => c.Message);
|
||||
}
|
||||
|
||||
private struct MessageData
|
||||
{
|
||||
public GameTick TickSent;
|
||||
public required BoundUserInterfaceMessage Message;
|
||||
|
||||
public override string ToString()
|
||||
{
|
||||
return $"{Message} @ {TickSent}";
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -81,4 +81,54 @@ namespace Content.Client.UserInterface.Controls
|
||||
return mode;
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Helper functions for working with <see cref="FancyWindow"/>.
|
||||
/// </summary>
|
||||
public static class FancyWindowExt
|
||||
{
|
||||
/// <summary>
|
||||
/// Sets information for a window (title and guidebooks) based on an entity.
|
||||
/// </summary>
|
||||
/// <param name="window">The window to modify.</param>
|
||||
/// <param name="entityManager">Entity manager used to retrieve the information.</param>
|
||||
/// <param name="entity">The entity that this window represents.</param>
|
||||
/// <seealso cref="SetTitleFromEntity"/>
|
||||
/// <seealso cref="SetGuidebookFromEntity"/>
|
||||
public static void SetInfoFromEntity(this FancyWindow window, IEntityManager entityManager, EntityUid entity)
|
||||
{
|
||||
window.SetTitleFromEntity(entityManager, entity);
|
||||
window.SetGuidebookFromEntity(entityManager, entity);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Set a window's title to the name of an entity.
|
||||
/// </summary>
|
||||
/// <param name="window">The window to modify.</param>
|
||||
/// <param name="entityManager">Entity manager used to retrieve the information.</param>
|
||||
/// <param name="entity">The entity that this window represents.</param>
|
||||
/// <seealso cref="SetInfoFromEntity"/>
|
||||
public static void SetTitleFromEntity(
|
||||
this FancyWindow window,
|
||||
IEntityManager entityManager,
|
||||
EntityUid entity)
|
||||
{
|
||||
window.Title = entityManager.GetComponent<MetaDataComponent>(entity).EntityName;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Set a window's guidebook IDs to those of an entity.
|
||||
/// </summary>
|
||||
/// <param name="window">The window to modify.</param>
|
||||
/// <param name="entityManager">Entity manager used to retrieve the information.</param>
|
||||
/// <param name="entity">The entity that this window represents.</param>
|
||||
/// <seealso cref="SetInfoFromEntity"/>
|
||||
public static void SetGuidebookFromEntity(
|
||||
this FancyWindow window,
|
||||
IEntityManager entityManager,
|
||||
EntityUid entity)
|
||||
{
|
||||
window.HelpGuidebookIds = entityManager.GetComponentOrNull<GuideHelpComponent>(entity)?.Guides;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
6
Content.Client/UserInterface/Controls/OnOffButton.xaml
Normal file
6
Content.Client/UserInterface/Controls/OnOffButton.xaml
Normal file
@@ -0,0 +1,6 @@
|
||||
<Control xmlns="https://spacestation14.io">
|
||||
<BoxContainer Orientation="Horizontal">
|
||||
<Button Name="OffButton" StyleClasses="OpenRight" Text="{Loc 'ui-button-off'}" />
|
||||
<Button Name="OnButton" StyleClasses="OpenLeft" Text="{Loc 'ui-button-on'}" />
|
||||
</BoxContainer>
|
||||
</Control>
|
||||
48
Content.Client/UserInterface/Controls/OnOffButton.xaml.cs
Normal file
48
Content.Client/UserInterface/Controls/OnOffButton.xaml.cs
Normal file
@@ -0,0 +1,48 @@
|
||||
using Robust.Client.AutoGenerated;
|
||||
using Robust.Client.UserInterface;
|
||||
using Robust.Client.UserInterface.Controls;
|
||||
using Robust.Client.UserInterface.XAML;
|
||||
|
||||
namespace Content.Client.UserInterface.Controls;
|
||||
|
||||
/// <summary>
|
||||
/// A simple control that displays a toggleable on/off button.
|
||||
/// </summary>
|
||||
[GenerateTypedNameReferences]
|
||||
public sealed partial class OnOffButton : Control
|
||||
{
|
||||
/// <summary>
|
||||
/// Whether the control is currently in the "on" state.
|
||||
/// </summary>
|
||||
public bool IsOn
|
||||
{
|
||||
get => OnButton.Pressed;
|
||||
set
|
||||
{
|
||||
if (value)
|
||||
OnButton.Pressed = true;
|
||||
else
|
||||
OffButton.Pressed = true;
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Raised when the user changes the state of the control.
|
||||
/// </summary>
|
||||
/// <remarks>
|
||||
/// This does not get raised if state is changed with <see cref="set_IsOn"/>.
|
||||
/// </remarks>
|
||||
public event Action<bool>? StateChanged;
|
||||
|
||||
public OnOffButton()
|
||||
{
|
||||
RobustXamlLoader.Load(this);
|
||||
|
||||
var group = new ButtonGroup(isNoneSetAllowed: false);
|
||||
OffButton.Group = group;
|
||||
OnButton.Group = group;
|
||||
|
||||
OffButton.OnPressed += _ => StateChanged?.Invoke(false);
|
||||
OnButton.OnPressed += _ => StateChanged?.Invoke(true);
|
||||
}
|
||||
}
|
||||
40
Content.Client/UserInterface/InputCoalescer.cs
Normal file
40
Content.Client/UserInterface/InputCoalescer.cs
Normal file
@@ -0,0 +1,40 @@
|
||||
using System.Diagnostics.CodeAnalysis;
|
||||
|
||||
namespace Content.Client.UserInterface;
|
||||
|
||||
/// <summary>
|
||||
/// A simple utility class to "coalesce" multiple input events into a single one, fired later.
|
||||
/// </summary>
|
||||
/// <typeparam name="T"></typeparam>
|
||||
public struct InputCoalescer<T>
|
||||
{
|
||||
public bool IsModified;
|
||||
public T LastValue;
|
||||
|
||||
/// <summary>
|
||||
/// Replace the value in the <see cref="InputCoalescer{T}"/>. This sets <see cref="IsModified"/> to true.
|
||||
/// </summary>
|
||||
public void Set(T value)
|
||||
{
|
||||
LastValue = value;
|
||||
IsModified = true;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Check if the <see cref="InputCoalescer{T}"/> has been modified.
|
||||
/// If it was, return the value and clear <see cref="IsModified"/>.
|
||||
/// </summary>
|
||||
/// <returns>True if the value was modified since the last check.</returns>
|
||||
public bool CheckIsModified([MaybeNullWhen(false)] out T value)
|
||||
{
|
||||
if (IsModified)
|
||||
{
|
||||
value = LastValue;
|
||||
IsModified = false;
|
||||
return true;
|
||||
}
|
||||
|
||||
value = default;
|
||||
return IsModified;
|
||||
}
|
||||
}
|
||||
37
Content.Server/Power/Components/BatteryInterfaceComponent.cs
Normal file
37
Content.Server/Power/Components/BatteryInterfaceComponent.cs
Normal file
@@ -0,0 +1,37 @@
|
||||
using Content.Server.Power.EntitySystems;
|
||||
using Content.Shared.Power;
|
||||
|
||||
namespace Content.Server.Power.Components;
|
||||
|
||||
/// <summary>
|
||||
/// Necessary component for battery management UI for SMES/substations.
|
||||
/// </summary>
|
||||
/// <seealso cref="BatteryUiKey.Key"/>
|
||||
/// <seealso cref="BatteryInterfaceSystem"/>
|
||||
[RegisterComponent]
|
||||
public sealed partial class BatteryInterfaceComponent : Component
|
||||
{
|
||||
/// <summary>
|
||||
/// The maximum charge rate users can configure through the UI.
|
||||
/// </summary>
|
||||
[DataField]
|
||||
public float MaxChargeRate;
|
||||
|
||||
/// <summary>
|
||||
/// The minimum charge rate users can configure through the UI.
|
||||
/// </summary>
|
||||
[DataField]
|
||||
public float MinChargeRate;
|
||||
|
||||
/// <summary>
|
||||
/// The maximum discharge rate users can configure through the UI.
|
||||
/// </summary>
|
||||
[DataField]
|
||||
public float MaxSupply;
|
||||
|
||||
/// <summary>
|
||||
/// The minimum discharge rate users can configure through the UI.
|
||||
/// </summary>
|
||||
[DataField]
|
||||
public float MinSupply;
|
||||
}
|
||||
120
Content.Server/Power/EntitySystems/BatteryInterfaceSystem.cs
Normal file
120
Content.Server/Power/EntitySystems/BatteryInterfaceSystem.cs
Normal file
@@ -0,0 +1,120 @@
|
||||
using Content.Server.Power.Components;
|
||||
using Content.Shared.Power;
|
||||
using Robust.Server.GameObjects;
|
||||
|
||||
namespace Content.Server.Power.EntitySystems;
|
||||
|
||||
/// <summary>
|
||||
/// Handles logic for the battery interface on SMES/substations.
|
||||
/// </summary>
|
||||
/// <remarks>
|
||||
/// <para>
|
||||
/// These devices have interfaces that allow user to toggle input and output,
|
||||
/// and configure charge/discharge power limits.
|
||||
/// </para>
|
||||
/// <para>
|
||||
/// This system is not responsible for any power logic on its own,
|
||||
/// it merely reconfigures parameters on <see cref="PowerNetworkBatteryComponent"/> from the UI.
|
||||
/// </para>
|
||||
/// </remarks>
|
||||
public sealed class BatteryInterfaceSystem : EntitySystem
|
||||
{
|
||||
[Dependency] private readonly UserInterfaceSystem _uiSystem = null!;
|
||||
|
||||
public override void Initialize()
|
||||
{
|
||||
base.Initialize();
|
||||
|
||||
UpdatesAfter.Add(typeof(PowerNetSystem));
|
||||
|
||||
Subs.BuiEvents<BatteryInterfaceComponent>(
|
||||
BatteryUiKey.Key,
|
||||
subs =>
|
||||
{
|
||||
subs.Event<BatterySetInputBreakerMessage>(HandleSetInputBreaker);
|
||||
subs.Event<BatterySetOutputBreakerMessage>(HandleSetOutputBreaker);
|
||||
|
||||
subs.Event<BatterySetChargeRateMessage>(HandleSetChargeRate);
|
||||
subs.Event<BatterySetDischargeRateMessage>(HandleSetDischargeRate);
|
||||
});
|
||||
}
|
||||
|
||||
private void HandleSetInputBreaker(Entity<BatteryInterfaceComponent> ent, ref BatterySetInputBreakerMessage args)
|
||||
{
|
||||
var netBattery = Comp<PowerNetworkBatteryComponent>(ent);
|
||||
netBattery.CanCharge = args.On;
|
||||
}
|
||||
|
||||
private void HandleSetOutputBreaker(Entity<BatteryInterfaceComponent> ent, ref BatterySetOutputBreakerMessage args)
|
||||
{
|
||||
var netBattery = Comp<PowerNetworkBatteryComponent>(ent);
|
||||
netBattery.CanDischarge = args.On;
|
||||
}
|
||||
|
||||
private void HandleSetChargeRate(Entity<BatteryInterfaceComponent> ent, ref BatterySetChargeRateMessage args)
|
||||
{
|
||||
var netBattery = Comp<PowerNetworkBatteryComponent>(ent);
|
||||
netBattery.MaxChargeRate = Math.Clamp(args.Rate, ent.Comp.MinChargeRate, ent.Comp.MaxChargeRate);
|
||||
}
|
||||
|
||||
private void HandleSetDischargeRate(Entity<BatteryInterfaceComponent> ent, ref BatterySetDischargeRateMessage args)
|
||||
{
|
||||
var netBattery = Comp<PowerNetworkBatteryComponent>(ent);
|
||||
netBattery.MaxSupply = Math.Clamp(args.Rate, ent.Comp.MinSupply, ent.Comp.MaxSupply);
|
||||
}
|
||||
|
||||
public override void Update(float frameTime)
|
||||
{
|
||||
var query = EntityQueryEnumerator<BatteryInterfaceComponent, BatteryComponent, PowerNetworkBatteryComponent>();
|
||||
|
||||
while (query.MoveNext(out var uid, out var batteryInterface, out var battery, out var netBattery))
|
||||
{
|
||||
UpdateUI(uid, batteryInterface, battery, netBattery);
|
||||
}
|
||||
}
|
||||
|
||||
private void UpdateUI(
|
||||
EntityUid uid,
|
||||
BatteryInterfaceComponent batteryInterface,
|
||||
BatteryComponent battery,
|
||||
PowerNetworkBatteryComponent netBattery)
|
||||
{
|
||||
if (!_uiSystem.IsUiOpen(uid, BatteryUiKey.Key))
|
||||
return;
|
||||
|
||||
_uiSystem.SetUiState(
|
||||
uid,
|
||||
BatteryUiKey.Key,
|
||||
new BatteryBuiState
|
||||
{
|
||||
Capacity = battery.MaxCharge,
|
||||
Charge = battery.CurrentCharge,
|
||||
CanCharge = netBattery.CanCharge,
|
||||
CanDischarge = netBattery.CanDischarge,
|
||||
CurrentReceiving = netBattery.CurrentReceiving,
|
||||
CurrentSupply = netBattery.CurrentSupply,
|
||||
MaxSupply = netBattery.MaxSupply,
|
||||
MaxChargeRate = netBattery.MaxChargeRate,
|
||||
Efficiency = netBattery.Efficiency,
|
||||
MaxMaxSupply = batteryInterface.MaxSupply,
|
||||
MinMaxSupply = batteryInterface.MinSupply,
|
||||
MaxMaxChargeRate = batteryInterface.MaxChargeRate,
|
||||
MinMaxChargeRate = batteryInterface.MinChargeRate,
|
||||
SupplyingNetworkHasPower = CheckHasPower<BatteryChargerComponent>(uid),
|
||||
LoadingNetworkHasPower = CheckHasPower<BatteryDischargerComponent>(uid),
|
||||
});
|
||||
|
||||
return;
|
||||
|
||||
bool CheckHasPower<TComp>(EntityUid entity) where TComp : BasePowerNetComponent
|
||||
{
|
||||
if (!TryComp(entity, out TComp? comp))
|
||||
return false;
|
||||
|
||||
if (comp.Net == null)
|
||||
return false;
|
||||
|
||||
return comp.Net.NetworkNode.LastCombinedMaxSupply > 0;
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -31,6 +31,8 @@ namespace Content.Shared.Localizations
|
||||
_loc.AddFunction(culture, "PRESSURE", FormatPressure);
|
||||
_loc.AddFunction(culture, "POWERWATTS", FormatPowerWatts);
|
||||
_loc.AddFunction(culture, "POWERJOULES", FormatPowerJoules);
|
||||
// NOTE: ENERGYWATTHOURS() still takes a value in joules, but formats as watt-hours.
|
||||
_loc.AddFunction(culture, "ENERGYWATTHOURS", FormatEnergyWattHours);
|
||||
_loc.AddFunction(culture, "UNITS", FormatUnits);
|
||||
_loc.AddFunction(culture, "TOSTRING", args => FormatToString(culture, args));
|
||||
_loc.AddFunction(culture, "LOC", FormatLoc);
|
||||
@@ -172,11 +174,17 @@ namespace Content.Shared.Localizations
|
||||
return new LocValueString(obj?.ToString() ?? "");
|
||||
}
|
||||
|
||||
private static ILocValue FormatUnitsGeneric(LocArgs args, string mode)
|
||||
private static ILocValue FormatUnitsGeneric(
|
||||
LocArgs args,
|
||||
string mode,
|
||||
Func<double, double>? transformValue = null)
|
||||
{
|
||||
const int maxPlaces = 5; // Matches amount in _lib.ftl
|
||||
var pressure = ((LocValueNumber) args.Args[0]).Value;
|
||||
|
||||
if (transformValue != null)
|
||||
pressure = transformValue(pressure);
|
||||
|
||||
var places = 0;
|
||||
while (pressure > 1000 && places < maxPlaces)
|
||||
{
|
||||
@@ -202,6 +210,13 @@ namespace Content.Shared.Localizations
|
||||
return FormatUnitsGeneric(args, "zzzz-fmt-power-joules");
|
||||
}
|
||||
|
||||
private static ILocValue FormatEnergyWattHours(LocArgs args)
|
||||
{
|
||||
const double joulesToWattHours = 1.0 / 3600;
|
||||
|
||||
return FormatUnitsGeneric(args, "zzzz-fmt-energy-watt-hours", joules => joules * joulesToWattHours);
|
||||
}
|
||||
|
||||
private static ILocValue FormatUnits(LocArgs args)
|
||||
{
|
||||
if (!Units.Types.TryGetValue(((LocValueString) args.Args[0]).Value, out var ut))
|
||||
|
||||
82
Content.Shared/Power/SharedBattery.cs
Normal file
82
Content.Shared/Power/SharedBattery.cs
Normal file
@@ -0,0 +1,82 @@
|
||||
using Robust.Shared.Serialization;
|
||||
|
||||
namespace Content.Shared.Power;
|
||||
|
||||
/// <summary>
|
||||
/// UI key for large battery (SMES/substation) UIs.
|
||||
/// </summary>
|
||||
[NetSerializable, Serializable]
|
||||
public enum BatteryUiKey : byte
|
||||
{
|
||||
Key,
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// UI state for large battery (SMES/substation) UIs.
|
||||
/// </summary>
|
||||
/// <seealso cref="BatteryUiKey"/>
|
||||
[Serializable, NetSerializable]
|
||||
public sealed class BatteryBuiState : BoundUserInterfaceState
|
||||
{
|
||||
// These are mostly just regular Pow3r parameters.
|
||||
|
||||
// I/O
|
||||
public bool CanCharge;
|
||||
public bool CanDischarge;
|
||||
public bool SupplyingNetworkHasPower;
|
||||
public bool LoadingNetworkHasPower;
|
||||
public float CurrentReceiving;
|
||||
public float CurrentSupply;
|
||||
|
||||
// Charge
|
||||
public float MaxChargeRate;
|
||||
public float MinMaxChargeRate;
|
||||
public float MaxMaxChargeRate;
|
||||
public float Efficiency;
|
||||
|
||||
// Discharge
|
||||
public float MaxSupply;
|
||||
public float MinMaxSupply;
|
||||
public float MaxMaxSupply;
|
||||
|
||||
// Storage
|
||||
public float Charge;
|
||||
public float Capacity;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Sent client to server to change the input breaker state on a large battery.
|
||||
/// </summary>
|
||||
[Serializable, NetSerializable]
|
||||
public sealed class BatterySetInputBreakerMessage(bool on) : BoundUserInterfaceMessage
|
||||
{
|
||||
public bool On = on;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Sent client to server to change the output breaker state on a large battery.
|
||||
/// </summary>
|
||||
[Serializable, NetSerializable]
|
||||
public sealed class BatterySetOutputBreakerMessage(bool on) : BoundUserInterfaceMessage
|
||||
{
|
||||
public bool On = on;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Sent client to server to change the charge rate on a large battery.
|
||||
/// </summary>
|
||||
[Serializable, NetSerializable]
|
||||
public sealed class BatterySetChargeRateMessage(float rate) : BoundUserInterfaceMessage
|
||||
{
|
||||
public float Rate = rate;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Sent client to server to change the discharge rate on a large battery.
|
||||
/// </summary>
|
||||
[Serializable, NetSerializable]
|
||||
public sealed class BatterySetDischargeRateMessage(float rate) : BoundUserInterfaceMessage
|
||||
{
|
||||
public float Rate = rate;
|
||||
}
|
||||
|
||||
@@ -32,5 +32,15 @@ zzzz-fmt-power-joules = { TOSTRING($divided, "F1") } { $places ->
|
||||
*[5] ???
|
||||
}
|
||||
|
||||
# Used internally by the ENERGYWATTHOURS() function.
|
||||
zzzz-fmt-energy-watt-hours = { TOSTRING($divided, "F1") } { $places ->
|
||||
[0] Wh
|
||||
[1] kWh
|
||||
[2] MWh
|
||||
[3] GWh
|
||||
[4] TWh
|
||||
*[5] ???
|
||||
}
|
||||
|
||||
# Used internally by the PLAYTIME() function.
|
||||
zzzz-fmt-playtime = {$hours}H {$minutes}M
|
||||
zzzz-fmt-playtime = {$hours}H {$minutes}M
|
||||
|
||||
22
Resources/Locale/en-US/power/battery.ftl
Normal file
22
Resources/Locale/en-US/power/battery.ftl
Normal file
@@ -0,0 +1,22 @@
|
||||
## Strings for the battery (SMES/substation) menu
|
||||
|
||||
battery-menu-footer-left = Danger: high voltage
|
||||
battery-menu-footer-right = 7.2 REV 6
|
||||
battery-menu-out = OUT
|
||||
battery-menu-in = IN
|
||||
battery-menu-charge-header = Charge Circuit
|
||||
battery-menu-discharge-header = Discharge Circuit
|
||||
battery-menu-storage-header = Storage Cells
|
||||
battery-menu-passthrough = Passthrough
|
||||
battery-menu-max = Max:
|
||||
battery-menu-current = Current:
|
||||
battery-menu-stored = Stored:
|
||||
battery-menu-energy = Energy:
|
||||
battery-menu-eta-full = ETA (full):
|
||||
battery-menu-eta-empty = ETA (empty):
|
||||
battery-menu-eta-value = ~{ $minutes } min
|
||||
battery-menu-eta-value-max = >{ $minutes } min
|
||||
battery-menu-eta-value-na = N/A
|
||||
battery-menu-power-value = { POWERWATTS($value) }
|
||||
battery-menu-stored-percent-value = { TOSTRING($value, "P1") }
|
||||
battery-menu-stored-energy-value = { ENERGYWATTHOURS($value) }
|
||||
3
Resources/Locale/en-US/ui/controls.ftl
Normal file
3
Resources/Locale/en-US/ui/controls.ftl
Normal file
@@ -0,0 +1,3 @@
|
||||
## Loc strings for generic "on/off button" control.
|
||||
ui-button-off = Off
|
||||
ui-button-on = On
|
||||
@@ -98,6 +98,19 @@
|
||||
- VoltageNetworks
|
||||
- Power
|
||||
|
||||
# Interface
|
||||
- type: BatteryInterface
|
||||
minChargeRate: 5000
|
||||
maxChargeRate: 150000
|
||||
minSupply: 5000
|
||||
maxSupply: 150000
|
||||
- type: UserInterface
|
||||
interfaces:
|
||||
enum.BatteryUiKey.Key:
|
||||
type: BatteryBoundUserInterface
|
||||
- type: ActivatableUI
|
||||
key: enum.BatteryUiKey.Key
|
||||
|
||||
# SMES' in use
|
||||
|
||||
- type: entity
|
||||
|
||||
@@ -113,6 +113,19 @@
|
||||
- VoltageNetworks
|
||||
- Power
|
||||
|
||||
# Interface
|
||||
- type: BatteryInterface
|
||||
minChargeRate: 5000
|
||||
maxChargeRate: 150000
|
||||
minSupply: 5000
|
||||
maxSupply: 150000
|
||||
- type: UserInterface
|
||||
interfaces:
|
||||
enum.BatteryUiKey.Key:
|
||||
type: BatteryBoundUserInterface
|
||||
- type: ActivatableUI
|
||||
key: enum.BatteryUiKey.Key
|
||||
|
||||
# Compact Wall Substation Base
|
||||
- type: entity
|
||||
id: BaseSubstationWall
|
||||
|
||||
Reference in New Issue
Block a user