]> git.smokeofanarchy.ru Git - space-station-14.git/commitdiff
Reapply "Trip APCs when they exceed a power limit (#41377)" (#41789)
authorslarticodefast <161409025+slarticodefast@users.noreply.github.com>
Mon, 8 Dec 2025 10:35:11 +0000 (11:35 +0100)
committerGitHub <noreply@github.com>
Mon, 8 Dec 2025 10:35:11 +0000 (10:35 +0000)
Revert "Merge stable into master"

Content.Client/Power/APC/UI/ApcMenu.xaml
Content.Client/Power/APC/UI/ApcMenu.xaml.cs
Content.IntegrationTests/Tests/Power/StationPowerTests.cs
Content.Server/Power/Components/ApcComponent.cs
Content.Server/Power/EntitySystems/ApcSystem.cs
Content.Shared/APC/SharedApc.cs
Resources/Locale/en-US/ui/power-apc.ftl
Resources/Prototypes/Entities/Objects/Devices/Syndicate_Gadgets/singularity_beacon.yml

index 4c14dcc0adef09e918fb8389ba653830c51628ca..682aea55d2ab821115ac4f900ea21b7ba940889b 100644 (file)
@@ -19,7 +19,7 @@
                         <!-- Power On/Off -->
                         <Label Text="{Loc 'apc-menu-breaker-label'}" HorizontalExpand="True"
                                 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"/>
                         </BoxContainer>
                         <!--Charging Status-->
index 14c00986ce339257b912f7d94768c9143144a325..097ee715819e263db504281d5fb088ebac217b7e 100644 (file)
@@ -40,7 +40,14 @@ namespace Content.Client.Power.APC.UI
 
             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)
index 59975443e1785714d70709b767c66ff192a03e13..542a4645c65af03041082421d155d71130c49b7e 100644 (file)
@@ -7,11 +7,11 @@ using Content.Server.Power.Pow3r;
 using Content.Shared.Maps;
 using Content.Shared.Power.Components;
 using Content.Shared.NodeContainer;
+using Robust.Server.GameObjects;
 using Robust.Shared.EntitySerialization;
 
 namespace Content.IntegrationTests.Tests.Power;
 
-[Explicit]
 public sealed class StationPowerTests
 {
     /// <summary>
@@ -21,27 +21,21 @@ public sealed class StationPowerTests
 
     private static readonly string[] GameMaps =
     [
-        "Fland",
-        "Meta",
-        "Packed",
-        "Omega",
         "Bagel",
         "Box",
-        "Core",
+        "Elkridge",
+        "Fland",
         "Marathon",
-        "Saltern",
-        "Reach",
-        "Train",
         "Oasis",
-        "Gate",
-        "Amber",
-        "Loop",
+        "Packed",
         "Plasma",
-        "Elkridge",
-        "Convex",
         "Relic",
+        "Snowball",
+        "Reach",
+        "Exo",
     ];
 
+    [Explicit]
     [Test, TestCaseSource(nameof(GameMaps))]
     public async Task TestStationStartingPowerWindow(string mapProtoId)
     {
@@ -100,6 +94,54 @@ public sealed class StationPowerTests
                 $"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();
     }
index 0bf9bc1872163ecb870adec9b149372b56910b5c..8309304d84862fdc49e5b8be37ac68402fc2df19 100644 (file)
@@ -5,7 +5,7 @@ using Robust.Shared.Serialization.TypeSerializers.Implementations.Custom;
 
 namespace Content.Server.Power.Components;
 
-[RegisterComponent]
+[RegisterComponent, AutoGenerateComponentPause]
 public sealed partial class ApcComponent : BaseApcNetComponent
 {
     [DataField("onReceiveMessageSound")]
@@ -34,6 +34,32 @@ public sealed partial class ApcComponent : BaseApcNetComponent
     public const float HighPowerThreshold = 0.9f;
     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!
     // End the suffering
     protected override void AddSelfToNet(IApcNet apcNet)
index ed7ec0e225a33b09dd099e453fe66bc59d0e745e..b3b2330a3c24fd33fce0c569772da3d5a4be7fce 100644 (file)
@@ -43,11 +43,12 @@ public sealed class ApcSystem : EntitySystem
     public override void Update(float deltaTime)
     {
         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))
         {
-            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);
             }
 
@@ -55,6 +56,28 @@ public sealed class ApcSystem : EntitySystem
             {
                 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;
         battery.CanDischarge = apc.MainBreakerEnabled;
 
+        if (apc.MainBreakerEnabled)
+            apc.TripFlag = false;
+
         UpdateUIState(uid, apc);
         _audio.PlayPvs(apc.OnReceiveMessageSound, uid, AudioParams.Default.WithVolume(-2f));
     }
@@ -169,7 +195,9 @@ public sealed class ApcSystem : EntitySystem
 
         var state = new ApcBoundInterfaceState(apc.MainBreakerEnabled,
             (int) MathF.Ceiling(battery.CurrentSupply), apc.LastExternalState,
-            charge);
+            charge,
+            apc.MaxLoad,
+            apc.TripFlag);
 
         _ui.SetUiState((uid, ui), ApcUiKey.Key, state);
     }
index 802c06a6ab6f83664d300220c0edae043be6e264..c8bf1cc3e8821eb1c93a19f9a4d1116ae98db01b 100644 (file)
@@ -181,13 +181,17 @@ namespace Content.Shared.APC
         public readonly int Power;
         public readonly ApcExternalPowerState ApcExternalPower;
         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;
             Power = power;
             ApcExternalPower = apcExternalPower;
             Charge = charge;
+            MaxLoad = maxLoad;
+            Tripped = tripped;
         }
 
         public bool Equals(ApcBoundInterfaceState? other)
@@ -197,7 +201,9 @@ namespace Content.Shared.APC
             return MainBreaker == other.MainBreaker &&
                    Power == other.Power &&
                    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)
@@ -207,7 +213,7 @@ namespace Content.Shared.APC
 
         public override int GetHashCode()
         {
-            return HashCode.Combine(MainBreaker, Power, (int) ApcExternalPower, Charge);
+            return HashCode.Combine(MainBreaker, Power, (int) ApcExternalPower, Charge, MaxLoad, Tripped);
         }
     }
 
index d30a906d1c0a534bd480d0d9b77a87a2d2ce4b7d..125d77fa7ce1996a1f302e6190f3d757b563b380 100644 (file)
@@ -10,7 +10,8 @@ apc-menu-charge-label = {$percent} Charged
 apc-menu-power-state-good = Good
 apc-menu-power-state-low = Low
 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
 
index 28da7331d222b2c32da92e30f1f82ee1b78aa96e..5c5dcd897949e815b914c9afc423a34a13c67346 100644 (file)
@@ -38,6 +38,6 @@
       - !type:DoActsBehavior
         acts: [ "Destruction" ]
   - type: ApcPowerReceiver
-    powerLoad: 15000
+    powerLoad: 5000
   - type: StaticPrice
     price: 7500