Files
tbd-station-14/Content.IntegrationTests/Tests/Power/StationPowerTests.cs
Partmedia a5c223c0c3 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
2025-11-21 15:01:23 +00:00

148 lines
5.6 KiB
C#

using System.Collections.Generic;
using System.Linq;
using Content.Server.GameTicking;
using Content.Server.Maps;
using Content.Server.Power.Components;
using Content.Server.Power.NodeGroups;
using Content.Server.Power.Pow3r;
using Content.Shared.Power.Components;
using Content.Shared.NodeContainer;
using Robust.Server.GameObjects;
using Robust.Shared.EntitySerialization;
namespace Content.IntegrationTests.Tests.Power;
public sealed class StationPowerTests
{
/// <summary>
/// How long the station should be able to survive on stored power if nothing is changed from round start.
/// </summary>
private const float MinimumPowerDurationSeconds = 10 * 60;
private static readonly string[] GameMaps =
[
"Amber",
"Bagel",
"Box",
"Elkridge",
"Fland",
"Marathon",
"Oasis",
"Packed",
"Plasma",
"Reach",
"Exo",
];
[Explicit]
[Test, TestCaseSource(nameof(GameMaps))]
public async Task TestStationStartingPowerWindow(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>();
// 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);
});
// Let powernet set up
await server.WaitRunTicks(1);
// Find the power network with the greatest stored charge in its batteries.
// This keeps backup SMESes out of the calculation.
var networks = new Dictionary<PowerState.Network, float>();
var batteryQuery = entMan.EntityQueryEnumerator<PowerNetworkBatteryComponent, BatteryComponent, NodeContainerComponent>();
while (batteryQuery.MoveNext(out var uid, out _, out var battery, out var nodeContainer))
{
if (!nodeContainer.Nodes.TryGetValue("output", out var node))
continue;
if (node.NodeGroup is not IBasePowerNet group)
continue;
networks.TryGetValue(group.NetworkNode, out var charge);
networks[group.NetworkNode] = charge + battery.CurrentCharge;
}
var totalStartingCharge = networks.MaxBy(n => n.Value).Value;
// Find how much charge all the APC-connected devices would like to use per second.
var totalAPCLoad = 0f;
var receiverQuery = entMan.EntityQueryEnumerator<ApcPowerReceiverComponent>();
while (receiverQuery.MoveNext(out _, out var receiver))
{
totalAPCLoad += receiver.Load;
}
var estimatedDuration = totalStartingCharge / totalAPCLoad;
var requiredStoredPower = totalAPCLoad * MinimumPowerDurationSeconds;
Assert.Multiple(() =>
{
Assert.That(estimatedDuration, Is.GreaterThanOrEqualTo(MinimumPowerDurationSeconds),
$"Initial power for {mapProtoId} does not last long enough! Needs at least {MinimumPowerDurationSeconds}s " +
$"but estimated to last only {estimatedDuration}s!");
Assert.That(totalStartingCharge, Is.GreaterThanOrEqualTo(requiredStoredPower),
$"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();
}
}