Trip APCs when they exceed a power limit (#41377)
* Implement APC overloading * Add power test * Review * Some more reviews * Show map coordinates for test failures * Widen column 2 * Reduce singularity beacon power consumption * Try to get grid coordinates
This commit is contained in:
@@ -19,7 +19,7 @@
|
|||||||
<!-- Power On/Off -->
|
<!-- Power On/Off -->
|
||||||
<Label Text="{Loc 'apc-menu-breaker-label'}" HorizontalExpand="True"
|
<Label Text="{Loc 'apc-menu-breaker-label'}" HorizontalExpand="True"
|
||||||
StyleClasses="highlight" MinWidth="120"/>
|
StyleClasses="highlight" MinWidth="120"/>
|
||||||
<BoxContainer Orientation="Horizontal" MinWidth="90">
|
<BoxContainer Orientation="Horizontal" MinWidth="150">
|
||||||
<Button Name="BreakerButton" Text="{Loc 'apc-menu-breaker-button'}" HorizontalExpand="True" ToggleMode="True"/>
|
<Button Name="BreakerButton" Text="{Loc 'apc-menu-breaker-button'}" HorizontalExpand="True" ToggleMode="True"/>
|
||||||
</BoxContainer>
|
</BoxContainer>
|
||||||
<!--Charging Status-->
|
<!--Charging Status-->
|
||||||
|
|||||||
@@ -40,7 +40,14 @@ namespace Content.Client.Power.APC.UI
|
|||||||
|
|
||||||
if (PowerLabel != null)
|
if (PowerLabel != null)
|
||||||
{
|
{
|
||||||
PowerLabel.Text = Loc.GetString("apc-menu-power-state-label-text", ("power", castState.Power));
|
if (castState.Tripped)
|
||||||
|
{
|
||||||
|
PowerLabel.Text = Loc.GetString("apc-menu-power-state-label-tripped");
|
||||||
|
}
|
||||||
|
else
|
||||||
|
{
|
||||||
|
PowerLabel.Text = Loc.GetString("apc-menu-power-state-label-text", ("power", castState.Power), ("maxLoad", castState.MaxLoad));
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
if (ExternalPowerStateLabel != null)
|
if (ExternalPowerStateLabel != null)
|
||||||
|
|||||||
@@ -7,11 +7,11 @@ using Content.Server.Power.NodeGroups;
|
|||||||
using Content.Server.Power.Pow3r;
|
using Content.Server.Power.Pow3r;
|
||||||
using Content.Shared.Power.Components;
|
using Content.Shared.Power.Components;
|
||||||
using Content.Shared.NodeContainer;
|
using Content.Shared.NodeContainer;
|
||||||
|
using Robust.Server.GameObjects;
|
||||||
using Robust.Shared.EntitySerialization;
|
using Robust.Shared.EntitySerialization;
|
||||||
|
|
||||||
namespace Content.IntegrationTests.Tests.Power;
|
namespace Content.IntegrationTests.Tests.Power;
|
||||||
|
|
||||||
[Explicit]
|
|
||||||
public sealed class StationPowerTests
|
public sealed class StationPowerTests
|
||||||
{
|
{
|
||||||
/// <summary>
|
/// <summary>
|
||||||
@@ -21,27 +21,20 @@ public sealed class StationPowerTests
|
|||||||
|
|
||||||
private static readonly string[] GameMaps =
|
private static readonly string[] GameMaps =
|
||||||
[
|
[
|
||||||
"Fland",
|
"Amber",
|
||||||
"Meta",
|
|
||||||
"Packed",
|
|
||||||
"Omega",
|
|
||||||
"Bagel",
|
"Bagel",
|
||||||
"Box",
|
"Box",
|
||||||
"Core",
|
|
||||||
"Marathon",
|
|
||||||
"Saltern",
|
|
||||||
"Reach",
|
|
||||||
"Train",
|
|
||||||
"Oasis",
|
|
||||||
"Gate",
|
|
||||||
"Amber",
|
|
||||||
"Loop",
|
|
||||||
"Plasma",
|
|
||||||
"Elkridge",
|
"Elkridge",
|
||||||
"Convex",
|
"Fland",
|
||||||
"Relic",
|
"Marathon",
|
||||||
|
"Oasis",
|
||||||
|
"Packed",
|
||||||
|
"Plasma",
|
||||||
|
"Reach",
|
||||||
|
"Exo",
|
||||||
];
|
];
|
||||||
|
|
||||||
|
[Explicit]
|
||||||
[Test, TestCaseSource(nameof(GameMaps))]
|
[Test, TestCaseSource(nameof(GameMaps))]
|
||||||
public async Task TestStationStartingPowerWindow(string mapProtoId)
|
public async Task TestStationStartingPowerWindow(string mapProtoId)
|
||||||
{
|
{
|
||||||
@@ -100,6 +93,54 @@ public sealed class StationPowerTests
|
|||||||
$"Needs at least {requiredStoredPower - totalStartingCharge} more stored power!");
|
$"Needs at least {requiredStoredPower - totalStartingCharge} more stored power!");
|
||||||
});
|
});
|
||||||
|
|
||||||
|
await pair.CleanReturnAsync();
|
||||||
|
}
|
||||||
|
|
||||||
|
[Test, TestCaseSource(nameof(GameMaps))]
|
||||||
|
public async Task TestApcLoad(string mapProtoId)
|
||||||
|
{
|
||||||
|
await using var pair = await PoolManager.GetServerClient(new PoolSettings
|
||||||
|
{
|
||||||
|
Dirty = true,
|
||||||
|
});
|
||||||
|
var server = pair.Server;
|
||||||
|
|
||||||
|
var entMan = server.EntMan;
|
||||||
|
var protoMan = server.ProtoMan;
|
||||||
|
var ticker = entMan.System<GameTicker>();
|
||||||
|
var xform = entMan.System<TransformSystem>();
|
||||||
|
|
||||||
|
// Load the map
|
||||||
|
await server.WaitAssertion(() =>
|
||||||
|
{
|
||||||
|
Assert.That(protoMan.TryIndex<GameMapPrototype>(mapProtoId, out var mapProto));
|
||||||
|
var opts = DeserializationOptions.Default with { InitializeMaps = true };
|
||||||
|
ticker.LoadGameMap(mapProto, out var mapId, opts);
|
||||||
|
});
|
||||||
|
|
||||||
|
// Wait long enough for power to ramp up, but before anything can trip
|
||||||
|
await pair.RunSeconds(2);
|
||||||
|
|
||||||
|
// Check that no APCs start overloaded
|
||||||
|
var apcQuery = entMan.EntityQueryEnumerator<ApcComponent, PowerNetworkBatteryComponent>();
|
||||||
|
Assert.Multiple(() =>
|
||||||
|
{
|
||||||
|
while (apcQuery.MoveNext(out var uid, out var apc, out var battery))
|
||||||
|
{
|
||||||
|
// Uncomment the following line to log starting APC load to the console
|
||||||
|
//Console.WriteLine($"ApcLoad:{mapProtoId}:{uid}:{battery.CurrentSupply}");
|
||||||
|
if (xform.TryGetMapOrGridCoordinates(uid, out var coord))
|
||||||
|
{
|
||||||
|
Assert.That(apc.MaxLoad, Is.GreaterThanOrEqualTo(battery.CurrentSupply),
|
||||||
|
$"APC {uid} on {mapProtoId} ({coord.Value.X}, {coord.Value.Y}) is overloaded {battery.CurrentSupply} / {apc.MaxLoad}");
|
||||||
|
}
|
||||||
|
else
|
||||||
|
{
|
||||||
|
Assert.That(apc.MaxLoad, Is.GreaterThanOrEqualTo(battery.CurrentSupply),
|
||||||
|
$"APC {uid} on {mapProtoId} is overloaded {battery.CurrentSupply} / {apc.MaxLoad}");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
await pair.CleanReturnAsync();
|
await pair.CleanReturnAsync();
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -5,7 +5,7 @@ using Robust.Shared.Serialization.TypeSerializers.Implementations.Custom;
|
|||||||
|
|
||||||
namespace Content.Server.Power.Components;
|
namespace Content.Server.Power.Components;
|
||||||
|
|
||||||
[RegisterComponent]
|
[RegisterComponent, AutoGenerateComponentPause]
|
||||||
public sealed partial class ApcComponent : BaseApcNetComponent
|
public sealed partial class ApcComponent : BaseApcNetComponent
|
||||||
{
|
{
|
||||||
[DataField("onReceiveMessageSound")]
|
[DataField("onReceiveMessageSound")]
|
||||||
@@ -34,6 +34,32 @@ public sealed partial class ApcComponent : BaseApcNetComponent
|
|||||||
public const float HighPowerThreshold = 0.9f;
|
public const float HighPowerThreshold = 0.9f;
|
||||||
public static TimeSpan VisualsChangeDelay = TimeSpan.FromSeconds(1);
|
public static TimeSpan VisualsChangeDelay = TimeSpan.FromSeconds(1);
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Maximum continuous load in Watts that this APC can supply to loads. Exceeding this starts a
|
||||||
|
/// timer, which after enough overloading causes the APC to "trip" off.
|
||||||
|
/// </summary>
|
||||||
|
[DataField]
|
||||||
|
public float MaxLoad = 20e3f;
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Time that the APC can be continuously overloaded before tripping off.
|
||||||
|
/// </summary>
|
||||||
|
[DataField]
|
||||||
|
public TimeSpan TripTime = TimeSpan.FromSeconds(3);
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Time that overloading began.
|
||||||
|
/// </summary>
|
||||||
|
[DataField(customTypeSerializer: typeof(TimeOffsetSerializer)), AutoPausedField]
|
||||||
|
public TimeSpan? TripStartTime;
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Set to true if the APC tripped off. Used to indicate problems in the UI. Reset by switching
|
||||||
|
/// APC on.
|
||||||
|
/// </summary>
|
||||||
|
[DataField]
|
||||||
|
public bool TripFlag;
|
||||||
|
|
||||||
// TODO ECS power a little better!
|
// TODO ECS power a little better!
|
||||||
// End the suffering
|
// End the suffering
|
||||||
protected override void AddSelfToNet(IApcNet apcNet)
|
protected override void AddSelfToNet(IApcNet apcNet)
|
||||||
|
|||||||
@@ -43,11 +43,12 @@ public sealed class ApcSystem : EntitySystem
|
|||||||
public override void Update(float deltaTime)
|
public override void Update(float deltaTime)
|
||||||
{
|
{
|
||||||
var query = EntityQueryEnumerator<ApcComponent, PowerNetworkBatteryComponent, UserInterfaceComponent>();
|
var query = EntityQueryEnumerator<ApcComponent, PowerNetworkBatteryComponent, UserInterfaceComponent>();
|
||||||
|
var curTime = _gameTiming.CurTime;
|
||||||
while (query.MoveNext(out var uid, out var apc, out var battery, out var ui))
|
while (query.MoveNext(out var uid, out var apc, out var battery, out var ui))
|
||||||
{
|
{
|
||||||
if (apc.LastUiUpdate + ApcComponent.VisualsChangeDelay < _gameTiming.CurTime && _ui.IsUiOpen((uid, ui), ApcUiKey.Key))
|
if (apc.LastUiUpdate + ApcComponent.VisualsChangeDelay < curTime && _ui.IsUiOpen((uid, ui), ApcUiKey.Key))
|
||||||
{
|
{
|
||||||
apc.LastUiUpdate = _gameTiming.CurTime;
|
apc.LastUiUpdate = curTime;
|
||||||
UpdateUIState(uid, apc, battery);
|
UpdateUIState(uid, apc, battery);
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -55,6 +56,28 @@ public sealed class ApcSystem : EntitySystem
|
|||||||
{
|
{
|
||||||
UpdateApcState(uid, apc, battery);
|
UpdateApcState(uid, apc, battery);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Overload
|
||||||
|
if (apc.MainBreakerEnabled && battery.CurrentSupply > apc.MaxLoad)
|
||||||
|
{
|
||||||
|
// Not already overloaded, start timer
|
||||||
|
if (apc.TripStartTime == null)
|
||||||
|
{
|
||||||
|
apc.TripStartTime = curTime;
|
||||||
|
}
|
||||||
|
else
|
||||||
|
{
|
||||||
|
if (curTime - apc.TripStartTime > apc.TripTime)
|
||||||
|
{
|
||||||
|
apc.TripFlag = true;
|
||||||
|
ApcToggleBreaker(uid, apc, battery); // off, we already checked MainBreakerEnabled above
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
else
|
||||||
|
{
|
||||||
|
apc.TripStartTime = null;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -106,6 +129,9 @@ public sealed class ApcSystem : EntitySystem
|
|||||||
apc.MainBreakerEnabled = !apc.MainBreakerEnabled;
|
apc.MainBreakerEnabled = !apc.MainBreakerEnabled;
|
||||||
battery.CanDischarge = apc.MainBreakerEnabled;
|
battery.CanDischarge = apc.MainBreakerEnabled;
|
||||||
|
|
||||||
|
if (apc.MainBreakerEnabled)
|
||||||
|
apc.TripFlag = false;
|
||||||
|
|
||||||
UpdateUIState(uid, apc);
|
UpdateUIState(uid, apc);
|
||||||
_audio.PlayPvs(apc.OnReceiveMessageSound, uid, AudioParams.Default.WithVolume(-2f));
|
_audio.PlayPvs(apc.OnReceiveMessageSound, uid, AudioParams.Default.WithVolume(-2f));
|
||||||
}
|
}
|
||||||
@@ -169,7 +195,9 @@ public sealed class ApcSystem : EntitySystem
|
|||||||
|
|
||||||
var state = new ApcBoundInterfaceState(apc.MainBreakerEnabled,
|
var state = new ApcBoundInterfaceState(apc.MainBreakerEnabled,
|
||||||
(int) MathF.Ceiling(battery.CurrentSupply), apc.LastExternalState,
|
(int) MathF.Ceiling(battery.CurrentSupply), apc.LastExternalState,
|
||||||
charge);
|
charge,
|
||||||
|
apc.MaxLoad,
|
||||||
|
apc.TripFlag);
|
||||||
|
|
||||||
_ui.SetUiState((uid, ui), ApcUiKey.Key, state);
|
_ui.SetUiState((uid, ui), ApcUiKey.Key, state);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -181,13 +181,17 @@ namespace Content.Shared.APC
|
|||||||
public readonly int Power;
|
public readonly int Power;
|
||||||
public readonly ApcExternalPowerState ApcExternalPower;
|
public readonly ApcExternalPowerState ApcExternalPower;
|
||||||
public readonly float Charge;
|
public readonly float Charge;
|
||||||
|
public readonly float MaxLoad;
|
||||||
|
public readonly bool Tripped;
|
||||||
|
|
||||||
public ApcBoundInterfaceState(bool mainBreaker, int power, ApcExternalPowerState apcExternalPower, float charge)
|
public ApcBoundInterfaceState(bool mainBreaker, int power, ApcExternalPowerState apcExternalPower, float charge, float maxLoad, bool tripped)
|
||||||
{
|
{
|
||||||
MainBreaker = mainBreaker;
|
MainBreaker = mainBreaker;
|
||||||
Power = power;
|
Power = power;
|
||||||
ApcExternalPower = apcExternalPower;
|
ApcExternalPower = apcExternalPower;
|
||||||
Charge = charge;
|
Charge = charge;
|
||||||
|
MaxLoad = maxLoad;
|
||||||
|
Tripped = tripped;
|
||||||
}
|
}
|
||||||
|
|
||||||
public bool Equals(ApcBoundInterfaceState? other)
|
public bool Equals(ApcBoundInterfaceState? other)
|
||||||
@@ -197,7 +201,9 @@ namespace Content.Shared.APC
|
|||||||
return MainBreaker == other.MainBreaker &&
|
return MainBreaker == other.MainBreaker &&
|
||||||
Power == other.Power &&
|
Power == other.Power &&
|
||||||
ApcExternalPower == other.ApcExternalPower &&
|
ApcExternalPower == other.ApcExternalPower &&
|
||||||
MathHelper.CloseTo(Charge, other.Charge);
|
MathHelper.CloseTo(Charge, other.Charge) &&
|
||||||
|
MathHelper.CloseTo(MaxLoad, other.MaxLoad) &&
|
||||||
|
Tripped == other.Tripped;
|
||||||
}
|
}
|
||||||
|
|
||||||
public override bool Equals(object? obj)
|
public override bool Equals(object? obj)
|
||||||
@@ -207,7 +213,7 @@ namespace Content.Shared.APC
|
|||||||
|
|
||||||
public override int GetHashCode()
|
public override int GetHashCode()
|
||||||
{
|
{
|
||||||
return HashCode.Combine(MainBreaker, Power, (int) ApcExternalPower, Charge);
|
return HashCode.Combine(MainBreaker, Power, (int) ApcExternalPower, Charge, MaxLoad, Tripped);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -10,7 +10,8 @@ apc-menu-charge-label = {$percent} Charged
|
|||||||
apc-menu-power-state-good = Good
|
apc-menu-power-state-good = Good
|
||||||
apc-menu-power-state-low = Low
|
apc-menu-power-state-low = Low
|
||||||
apc-menu-power-state-none = None
|
apc-menu-power-state-none = None
|
||||||
apc-menu-power-state-label-text = { POWERWATTS($power) }
|
apc-menu-power-state-label-text = { POWERWATTS($power) } / { POWERWATTS($maxLoad) }
|
||||||
|
apc-menu-power-state-label-tripped = OVERLOAD
|
||||||
|
|
||||||
# For the flavor text on the footer
|
# For the flavor text on the footer
|
||||||
|
|
||||||
|
|||||||
@@ -38,6 +38,6 @@
|
|||||||
- !type:DoActsBehavior
|
- !type:DoActsBehavior
|
||||||
acts: [ "Destruction" ]
|
acts: [ "Destruction" ]
|
||||||
- type: ApcPowerReceiver
|
- type: ApcPowerReceiver
|
||||||
powerLoad: 15000
|
powerLoad: 5000
|
||||||
- type: StaticPrice
|
- type: StaticPrice
|
||||||
price: 7500
|
price: 7500
|
||||||
|
|||||||
Reference in New Issue
Block a user