#nullable enable using System.Threading.Tasks; using Content.Server.NodeContainer; using Content.Server.NodeContainer.Nodes; using Content.Server.Power.Components; using Content.Server.Power.EntitySystems; using Content.Server.Power.Nodes; using Content.Shared.Coordinates; using NUnit.Framework; using Robust.Shared.GameObjects; using Robust.Shared.Map; using Robust.Shared.Maths; using Robust.Shared.Timing; namespace Content.IntegrationTests.Tests.Power { [TestFixture] public sealed class PowerTest { private const string Prototypes = @" - type: entity id: GeneratorDummy components: - type: NodeContainer nodes: output: !type:CableDeviceNode nodeGroupID: HVPower - type: PowerSupplier - type: Transform anchored: true - type: entity id: ConsumerDummy components: - type: Transform anchored: true - type: NodeContainer nodes: input: !type:CableDeviceNode nodeGroupID: HVPower - type: PowerConsumer - type: entity id: ChargingBatteryDummy components: - type: Transform anchored: true - type: NodeContainer nodes: output: !type:CableDeviceNode nodeGroupID: HVPower - type: PowerNetworkBattery - type: Battery - type: BatteryCharger - type: entity id: DischargingBatteryDummy components: - type: Transform anchored: true - type: NodeContainer nodes: output: !type:CableDeviceNode nodeGroupID: HVPower - type: PowerNetworkBattery - type: Battery - type: BatteryDischarger - type: entity id: FullBatteryDummy components: - type: Transform anchored: true - type: NodeContainer nodes: output: !type:CableDeviceNode nodeGroupID: HVPower input: !type:CableTerminalPortNode nodeGroupID: HVPower - type: PowerNetworkBattery - type: Battery - type: BatteryDischarger node: output - type: BatteryCharger node: input - type: entity id: SubstationDummy components: - type: NodeContainer nodes: input: !type:CableDeviceNode nodeGroupID: HVPower output: !type:CableDeviceNode nodeGroupID: MVPower - type: BatteryCharger voltage: High - type: BatteryDischarger voltage: Medium - type: PowerNetworkBattery maxChargeRate: 1000 maxSupply: 1000 supplyRampTolerance: 1000 - type: Battery maxCharge: 1000 startingCharge: 1000 - type: Transform anchored: true - type: entity id: ApcDummy components: - type: Battery maxCharge: 10000 startingCharge: 10000 - type: PowerNetworkBattery maxChargeRate: 1000 maxSupply: 1000 supplyRampTolerance: 1000 - type: BatteryCharger voltage: Medium - type: BatteryDischarger voltage: Apc - type: Apc voltage: Apc - type: NodeContainer nodes: input: !type:CableDeviceNode nodeGroupID: MVPower output: !type:CableDeviceNode nodeGroupID: Apc - type: Transform anchored: true - type: UserInterface interfaces: - key: enum.ApcUiKey.Key type: ApcBoundUserInterface - type: AccessReader access: [['Engineering']] - type: entity id: ApcPowerReceiverDummy components: - type: ApcPowerReceiver - type: ExtensionCableReceiver - type: Transform anchored: true "; /// /// Test small power net with a simple surplus of power over the loads. /// [Test] public async Task TestSimpleSurplus() { await using var pairTracker = await PoolManager.GetServerClient(new PoolSettings{NoClient = true, ExtraPrototypes = Prototypes}); var server = pairTracker.Pair.Server; var mapManager = server.ResolveDependency(); var entityManager = server.ResolveDependency(); const float loadPower = 200; PowerSupplierComponent supplier = default!; PowerConsumerComponent consumer1 = default!; PowerConsumerComponent consumer2 = default!; await server.WaitAssertion(() => { var map = mapManager.CreateMap(); var grid = mapManager.CreateGrid(map); // Power only works when anchored for (var i = 0; i < 3; i++) { grid.SetTile(new Vector2i(0, i), new Tile(1)); entityManager.SpawnEntity("CableHV", grid.ToCoordinates(0, i)); } var generatorEnt = entityManager.SpawnEntity("GeneratorDummy", grid.ToCoordinates()); var consumerEnt1 = entityManager.SpawnEntity("ConsumerDummy", grid.ToCoordinates(0, 1)); var consumerEnt2 = entityManager.SpawnEntity("ConsumerDummy", grid.ToCoordinates(0, 2)); supplier = entityManager.GetComponent(generatorEnt); consumer1 = entityManager.GetComponent(consumerEnt1); consumer2 = entityManager.GetComponent(consumerEnt2); // Plenty of surplus and tolerance supplier.MaxSupply = loadPower * 4; supplier.SupplyRampTolerance = loadPower * 4; consumer1.DrawRate = loadPower; consumer2.DrawRate = loadPower; }); server.RunTicks(1); //let run a tick for PowerNet to process power await server.WaitAssertion(() => { // Assert both consumers fully powered Assert.That(consumer1.ReceivedPower, Is.EqualTo(consumer1.DrawRate).Within(0.1)); Assert.That(consumer2.ReceivedPower, Is.EqualTo(consumer2.DrawRate).Within(0.1)); // Assert that load adds up on supply. Assert.That(supplier.CurrentSupply, Is.EqualTo(loadPower * 2).Within(0.1)); }); await pairTracker.CleanReturnAsync(); } /// /// Test small power net with a simple deficit of power over the loads. /// [Test] public async Task TestSimpleDeficit() { await using var pairTracker = await PoolManager.GetServerClient(new PoolSettings{NoClient = true, ExtraPrototypes = Prototypes}); var server = pairTracker.Pair.Server; var mapManager = server.ResolveDependency(); var entityManager = server.ResolveDependency(); const float loadPower = 200; PowerSupplierComponent supplier = default!; PowerConsumerComponent consumer1 = default!; PowerConsumerComponent consumer2 = default!; await server.WaitAssertion(() => { var map = mapManager.CreateMap(); var grid = mapManager.CreateGrid(map); // Power only works when anchored for (var i = 0; i < 3; i++) { grid.SetTile(new Vector2i(0, i), new Tile(1)); entityManager.SpawnEntity("CableHV", grid.ToCoordinates(0, i)); } var generatorEnt = entityManager.SpawnEntity("GeneratorDummy", grid.ToCoordinates()); var consumerEnt1 = entityManager.SpawnEntity("ConsumerDummy", grid.ToCoordinates(0, 1)); var consumerEnt2 = entityManager.SpawnEntity("ConsumerDummy", grid.ToCoordinates(0, 2)); supplier = entityManager.GetComponent(generatorEnt); consumer1 = entityManager.GetComponent(consumerEnt1); consumer2 = entityManager.GetComponent(consumerEnt2); // Too little supply, both consumers should get 33% power. supplier.MaxSupply = loadPower; supplier.SupplyRampTolerance = loadPower; consumer1.DrawRate = loadPower; consumer2.DrawRate = loadPower * 2; }); server.RunTicks(1); //let run a tick for PowerNet to process power await server.WaitAssertion(() => { // Assert both consumers get 33% power. Assert.That(consumer1.ReceivedPower, Is.EqualTo(consumer1.DrawRate / 3).Within(0.1)); Assert.That(consumer2.ReceivedPower, Is.EqualTo(consumer2.DrawRate / 3).Within(0.1)); // Supply should be maxed out Assert.That(supplier.CurrentSupply, Is.EqualTo(supplier.MaxSupply).Within(0.1)); }); await pairTracker.CleanReturnAsync(); } [Test] public async Task TestSupplyRamp() { await using var pairTracker = await PoolManager.GetServerClient(new PoolSettings{NoClient = true, ExtraPrototypes = Prototypes}); var server = pairTracker.Pair.Server; var mapManager = server.ResolveDependency(); var entityManager = server.ResolveDependency(); var gameTiming = server.ResolveDependency(); PowerSupplierComponent supplier = default!; PowerConsumerComponent consumer = default!; await server.WaitAssertion(() => { var map = mapManager.CreateMap(); var grid = mapManager.CreateGrid(map); // Power only works when anchored for (var i = 0; i < 3; i++) { grid.SetTile(new Vector2i(0, i), new Tile(1)); entityManager.SpawnEntity("CableHV", grid.ToCoordinates(0, i)); } var generatorEnt = entityManager.SpawnEntity("GeneratorDummy", grid.ToCoordinates()); var consumerEnt = entityManager.SpawnEntity("ConsumerDummy", grid.ToCoordinates(0, 2)); supplier = entityManager.GetComponent(generatorEnt); consumer = entityManager.GetComponent(consumerEnt); // Supply has enough total power but needs to ramp up to match. supplier.MaxSupply = 400; supplier.SupplyRampRate = 400; supplier.SupplyRampTolerance = 100; consumer.DrawRate = 400; }); // Exact values can/will be off by a tick, add tolerance for that. var tickRate = (float) gameTiming.TickPeriod.TotalSeconds; var tickDev = 400 * tickRate * 1.1f; server.RunTicks(1); await server.WaitAssertion(() => { // First tick, supply should be delivering 100 W (max tolerance) and start ramping up. Assert.That(supplier.CurrentSupply, Is.EqualTo(100).Within(0.1)); Assert.That(consumer.ReceivedPower, Is.EqualTo(100).Within(0.1)); }); server.RunTicks(14); await server.WaitAssertion(() => { // After 15 ticks (0.25 seconds), supply ramp pos should be at 100 W and supply at 100, approx. Assert.That(supplier.CurrentSupply, Is.EqualTo(200).Within(tickDev)); Assert.That(supplier.SupplyRampPosition, Is.EqualTo(100).Within(tickDev)); Assert.That(consumer.ReceivedPower, Is.EqualTo(200).Within(tickDev)); }); server.RunTicks(45); await server.WaitAssertion(() => { // After 1 second total, ramp should be at 400 and supply should be at 400, everybody happy. Assert.That(supplier.CurrentSupply, Is.EqualTo(400).Within(tickDev)); Assert.That(supplier.SupplyRampPosition, Is.EqualTo(400).Within(tickDev)); Assert.That(consumer.ReceivedPower, Is.EqualTo(400).Within(tickDev)); }); await pairTracker.CleanReturnAsync(); } [Test] public async Task TestBatteryRamp() { await using var pairTracker = await PoolManager.GetServerClient(new PoolSettings{NoClient = true, ExtraPrototypes = Prototypes}); var server = pairTracker.Pair.Server; var mapManager = server.ResolveDependency(); var entityManager = server.ResolveDependency(); var gameTiming = server.ResolveDependency(); const float startingCharge = 100_000; PowerNetworkBatteryComponent netBattery = default!; BatteryComponent battery = default!; PowerConsumerComponent consumer = default!; await server.WaitAssertion(() => { var map = mapManager.CreateMap(); var grid = mapManager.CreateGrid(map); // Power only works when anchored for (var i = 0; i < 3; i++) { grid.SetTile(new Vector2i(0, i), new Tile(1)); entityManager.SpawnEntity("CableHV", grid.ToCoordinates(0, i)); } var generatorEnt = entityManager.SpawnEntity("DischargingBatteryDummy", grid.ToCoordinates()); var consumerEnt = entityManager.SpawnEntity("ConsumerDummy", grid.ToCoordinates(0, 2)); netBattery = entityManager.GetComponent(generatorEnt); battery = entityManager.GetComponent(generatorEnt); consumer = entityManager.GetComponent(consumerEnt); battery.MaxCharge = startingCharge; battery.CurrentCharge = startingCharge; netBattery.MaxSupply = 400; netBattery.SupplyRampRate = 400; netBattery.SupplyRampTolerance = 100; consumer.DrawRate = 400; }); // Exact values can/will be off by a tick, add tolerance for that. var tickRate = (float) gameTiming.TickPeriod.TotalSeconds; var tickDev = 400 * tickRate * 1.1f; server.RunTicks(1); await server.WaitAssertion(() => { // First tick, supply should be delivering 100 W (max tolerance) and start ramping up. Assert.That(netBattery.CurrentSupply, Is.EqualTo(100).Within(0.1)); Assert.That(consumer.ReceivedPower, Is.EqualTo(100).Within(0.1)); }); server.RunTicks(14); await server.WaitAssertion(() => { // After 15 ticks (0.25 seconds), supply ramp pos should be at 100 W and supply at 100, approx. Assert.That(netBattery.CurrentSupply, Is.EqualTo(200).Within(tickDev)); Assert.That(netBattery.SupplyRampPosition, Is.EqualTo(100).Within(tickDev)); Assert.That(consumer.ReceivedPower, Is.EqualTo(200).Within(tickDev)); // Trivial integral to calculate expected power spent. const double spentExpected = (200 + 100) / 2.0 * 0.25; Assert.That(battery.CurrentCharge, Is.EqualTo(startingCharge - spentExpected).Within(tickDev)); }); server.RunTicks(45); await server.WaitAssertion(() => { // After 1 second total, ramp should be at 400 and supply should be at 400, everybody happy. Assert.That(netBattery.CurrentSupply, Is.EqualTo(400).Within(tickDev)); Assert.That(netBattery.SupplyRampPosition, Is.EqualTo(400).Within(tickDev)); Assert.That(consumer.ReceivedPower, Is.EqualTo(400).Within(tickDev)); // Trivial integral to calculate expected power spent. const double spentExpected = (400 + 100) / 2.0 * 0.75 + 400 * 0.25; Assert.That(battery.CurrentCharge, Is.EqualTo(startingCharge - spentExpected).Within(tickDev)); }); await pairTracker.CleanReturnAsync(); } [Test] public async Task TestSimpleBatteryChargeDeficit() { await using var pairTracker = await PoolManager.GetServerClient(new PoolSettings{NoClient = true, ExtraPrototypes = Prototypes}); var server = pairTracker.Pair.Server; var mapManager = server.ResolveDependency(); var entityManager = server.ResolveDependency(); PowerSupplierComponent supplier = default!; BatteryComponent battery = default!; await server.WaitAssertion(() => { var map = mapManager.CreateMap(); var grid = mapManager.CreateGrid(map); // Power only works when anchored for (var i = 0; i < 3; i++) { grid.SetTile(new Vector2i(0, i), new Tile(1)); entityManager.SpawnEntity("CableHV", grid.ToCoordinates(0, i)); } var generatorEnt = entityManager.SpawnEntity("GeneratorDummy", grid.ToCoordinates()); var batteryEnt = entityManager.SpawnEntity("ChargingBatteryDummy", grid.ToCoordinates(0, 2)); supplier = entityManager.GetComponent(generatorEnt); var netBattery = entityManager.GetComponent(batteryEnt); battery = entityManager.GetComponent(batteryEnt); supplier.MaxSupply = 500; supplier.SupplyRampTolerance = 500; battery.MaxCharge = 100000; netBattery.MaxChargeRate = 1000; netBattery.Efficiency = 0.5f; }); server.RunTicks(30); // 60 TPS, 0.5 seconds await server.WaitAssertion(() => { // half a second @ 500 W = 250 // 50% efficiency, so 125 J stored total. Assert.That(battery.CurrentCharge, Is.EqualTo(125).Within(0.1)); Assert.That(supplier.CurrentSupply, Is.EqualTo(500).Within(0.1)); }); await pairTracker.CleanReturnAsync(); } [Test] public async Task TestFullBattery() { await using var pairTracker = await PoolManager.GetServerClient(new PoolSettings{NoClient = true, ExtraPrototypes = Prototypes}); var server = pairTracker.Pair.Server; var mapManager = server.ResolveDependency(); var entityManager = server.ResolveDependency(); var gameTiming = server.ResolveDependency(); PowerConsumerComponent consumer = default!; PowerSupplierComponent supplier = default!; PowerNetworkBatteryComponent netBattery = default!; BatteryComponent battery = default!; await server.WaitAssertion(() => { var map = mapManager.CreateMap(); var grid = mapManager.CreateGrid(map); // Power only works when anchored for (var i = 0; i < 4; i++) { grid.SetTile(new Vector2i(0, i), new Tile(1)); entityManager.SpawnEntity("CableHV", grid.ToCoordinates(0, i)); } var terminal = entityManager.SpawnEntity("CableTerminal", grid.ToCoordinates(0, 1)); entityManager.GetComponent(terminal).LocalRotation = Angle.FromDegrees(180); var batteryEnt = entityManager.SpawnEntity("FullBatteryDummy", grid.ToCoordinates(0, 2)); var supplyEnt = entityManager.SpawnEntity("GeneratorDummy", grid.ToCoordinates(0, 0)); var consumerEnt = entityManager.SpawnEntity("ConsumerDummy", grid.ToCoordinates(0, 3)); consumer = entityManager.GetComponent(consumerEnt); supplier = entityManager.GetComponent(supplyEnt); netBattery = entityManager.GetComponent(batteryEnt); battery = entityManager.GetComponent(batteryEnt); // Consumer needs 1000 W, supplier can only provide 800, battery fills in the remaining 200. consumer.DrawRate = 1000; supplier.MaxSupply = 800; supplier.SupplyRampTolerance = 800; netBattery.MaxSupply = 400; netBattery.SupplyRampTolerance = 400; netBattery.SupplyRampRate = 100_000; battery.MaxCharge = 100_000; battery.CurrentCharge = 100_000; }); // Run some ticks so everything is stable. server.RunTicks(60); // Exact values can/will be off by a tick, add tolerance for that. var tickRate = (float) gameTiming.TickPeriod.TotalSeconds; var tickDev = 400 * tickRate * 1.1f; await server.WaitAssertion(() => { Assert.That(consumer.ReceivedPower, Is.EqualTo(consumer.DrawRate).Within(0.1)); Assert.That(supplier.CurrentSupply, Is.EqualTo(supplier.MaxSupply).Within(0.1)); // Battery's current supply includes passed-through power from the supply. // Assert ramp position is correct to make sure it's only supplying 200 W for real. Assert.That(netBattery.CurrentSupply, Is.EqualTo(1000).Within(0.1)); Assert.That(netBattery.SupplyRampPosition, Is.EqualTo(200).Within(0.1)); const int expectedSpent = 200; Assert.That(battery.CurrentCharge, Is.EqualTo(battery.MaxCharge - expectedSpent).Within(tickDev)); }); await pairTracker.CleanReturnAsync(); } [Test] public async Task TestFullBatteryEfficiencyPassThrough() { await using var pairTracker = await PoolManager.GetServerClient(new PoolSettings{NoClient = true, ExtraPrototypes = Prototypes}); var server = pairTracker.Pair.Server; var mapManager = server.ResolveDependency(); var entityManager = server.ResolveDependency(); var gameTiming = server.ResolveDependency(); PowerConsumerComponent consumer = default!; PowerSupplierComponent supplier = default!; PowerNetworkBatteryComponent netBattery = default!; BatteryComponent battery = default!; await server.WaitAssertion(() => { var map = mapManager.CreateMap(); var grid = mapManager.CreateGrid(map); // Power only works when anchored for (var i = 0; i < 4; i++) { grid.SetTile(new Vector2i(0, i), new Tile(1)); entityManager.SpawnEntity("CableHV", grid.ToCoordinates(0, i)); } var terminal = entityManager.SpawnEntity("CableTerminal", grid.ToCoordinates(0, 1)); entityManager.GetComponent(terminal).LocalRotation = Angle.FromDegrees(180); var batteryEnt = entityManager.SpawnEntity("FullBatteryDummy", grid.ToCoordinates(0, 2)); var supplyEnt = entityManager.SpawnEntity("GeneratorDummy", grid.ToCoordinates(0, 0)); var consumerEnt = entityManager.SpawnEntity("ConsumerDummy", grid.ToCoordinates(0, 3)); consumer = entityManager.GetComponent(consumerEnt); supplier = entityManager.GetComponent(supplyEnt); netBattery = entityManager.GetComponent(batteryEnt); battery = entityManager.GetComponent(batteryEnt); // Consumer needs 1000 W, supply and battery can only provide 400 each. // BUT the battery has 50% input efficiency, so 50% of the power of the supply gets lost. consumer.DrawRate = 1000; supplier.MaxSupply = 400; supplier.SupplyRampTolerance = 400; netBattery.MaxSupply = 400; netBattery.SupplyRampTolerance = 400; netBattery.SupplyRampRate = 100_000; netBattery.Efficiency = 0.5f; battery.MaxCharge = 1_000_000; battery.CurrentCharge = 1_000_000; }); // Run some ticks so everything is stable. server.RunTicks(60); // Exact values can/will be off by a tick, add tolerance for that. var tickRate = (float) gameTiming.TickPeriod.TotalSeconds; var tickDev = 400 * tickRate * 1.1f; await server.WaitAssertion(() => { Assert.That(consumer.ReceivedPower, Is.EqualTo(600).Within(0.1)); Assert.That(supplier.CurrentSupply, Is.EqualTo(supplier.MaxSupply).Within(0.1)); Assert.That(netBattery.CurrentSupply, Is.EqualTo(600).Within(0.1)); Assert.That(netBattery.SupplyRampPosition, Is.EqualTo(400).Within(0.1)); const int expectedSpent = 400; Assert.That(battery.CurrentCharge, Is.EqualTo(battery.MaxCharge - expectedSpent).Within(tickDev)); }); await pairTracker.CleanReturnAsync(); } [Test] public async Task TestFullBatteryEfficiencyDemandPassThrough() { await using var pairTracker = await PoolManager.GetServerClient(new PoolSettings{NoClient = true, ExtraPrototypes = Prototypes}); var server = pairTracker.Pair.Server; var mapManager = server.ResolveDependency(); var entityManager = server.ResolveDependency(); PowerConsumerComponent consumer1 = default!; PowerConsumerComponent consumer2 = default!; PowerSupplierComponent supplier = default!; await server.WaitAssertion(() => { var map = mapManager.CreateMap(); var grid = mapManager.CreateGrid(map); // Map layout here is // C - consumer // B - battery // G - generator // B - battery // C - consumer // Connected in the only way that makes sense. // Power only works when anchored for (var i = 0; i < 5; i++) { grid.SetTile(new Vector2i(0, i), new Tile(1)); entityManager.SpawnEntity("CableHV", grid.ToCoordinates(0, i)); } entityManager.SpawnEntity("CableTerminal", grid.ToCoordinates(0, 2)); var terminal = entityManager.SpawnEntity("CableTerminal", grid.ToCoordinates(0, 2)); entityManager.GetComponent(terminal).LocalRotation = Angle.FromDegrees(180); var batteryEnt1 = entityManager.SpawnEntity("FullBatteryDummy", grid.ToCoordinates(0, 1)); var batteryEnt2 = entityManager.SpawnEntity("FullBatteryDummy", grid.ToCoordinates(0, 3)); var supplyEnt = entityManager.SpawnEntity("GeneratorDummy", grid.ToCoordinates(0, 2)); var consumerEnt1 = entityManager.SpawnEntity("ConsumerDummy", grid.ToCoordinates(0, 0)); var consumerEnt2 = entityManager.SpawnEntity("ConsumerDummy", grid.ToCoordinates(0, 4)); consumer1 = entityManager.GetComponent(consumerEnt1); consumer2 = entityManager.GetComponent(consumerEnt2); supplier = entityManager.GetComponent(supplyEnt); var netBattery1 = entityManager.GetComponent(batteryEnt1); var netBattery2 = entityManager.GetComponent(batteryEnt2); var battery1 = entityManager.GetComponent(batteryEnt1); var battery2 = entityManager.GetComponent(batteryEnt2); // There are two loads, 500 W and 1000 W respectively. // The 500 W load is behind a 50% efficient battery, // so *effectively* it needs 2x as much power from the supply to run. // Assert that both are getting 50% power. // Batteries are empty and only a bridge. consumer1.DrawRate = 500; consumer2.DrawRate = 1000; supplier.MaxSupply = 1000; supplier.SupplyRampTolerance = 1000; battery1.MaxCharge = 1_000_000; battery2.MaxCharge = 1_000_000; netBattery1.MaxChargeRate = 1_000; netBattery2.MaxChargeRate = 1_000; netBattery1.Efficiency = 0.5f; netBattery1.MaxSupply = 1_000_000; netBattery2.MaxSupply = 1_000_000; netBattery1.SupplyRampTolerance = 1_000_000; netBattery2.SupplyRampTolerance = 1_000_000; }); // Run some ticks so everything is stable. server.RunTicks(10); await server.WaitAssertion(() => { Assert.That(consumer1.ReceivedPower, Is.EqualTo(250).Within(0.1)); Assert.That(consumer2.ReceivedPower, Is.EqualTo(500).Within(0.1)); Assert.That(supplier.CurrentSupply, Is.EqualTo(supplier.MaxSupply).Within(0.1)); }); await pairTracker.CleanReturnAsync(); } /// /// Test that power is distributed proportionally, even through batteries. /// [Test] public async Task TestBatteriesProportional() { await using var pairTracker = await PoolManager.GetServerClient(new PoolSettings{NoClient = true, ExtraPrototypes = Prototypes}); var server = pairTracker.Pair.Server; var mapManager = server.ResolveDependency(); var entityManager = server.ResolveDependency(); PowerConsumerComponent consumer1 = default!; PowerConsumerComponent consumer2 = default!; PowerSupplierComponent supplier = default!; await server.WaitAssertion(() => { var map = mapManager.CreateMap(); var grid = mapManager.CreateGrid(map); // Map layout here is // C - consumer // B - battery // G - generator // B - battery // C - consumer // Connected in the only way that makes sense. // Power only works when anchored for (var i = 0; i < 5; i++) { grid.SetTile(new Vector2i(0, i), new Tile(1)); entityManager.SpawnEntity("CableHV", grid.ToCoordinates(0, i)); } entityManager.SpawnEntity("CableTerminal", grid.ToCoordinates(0, 2)); var terminal = entityManager.SpawnEntity("CableTerminal", grid.ToCoordinates(0, 2)); entityManager.GetComponent(terminal).LocalRotation = Angle.FromDegrees(180); var batteryEnt1 = entityManager.SpawnEntity("FullBatteryDummy", grid.ToCoordinates(0, 1)); var batteryEnt2 = entityManager.SpawnEntity("FullBatteryDummy", grid.ToCoordinates(0, 3)); var supplyEnt = entityManager.SpawnEntity("GeneratorDummy", grid.ToCoordinates(0, 2)); var consumerEnt1 = entityManager.SpawnEntity("ConsumerDummy", grid.ToCoordinates(0, 0)); var consumerEnt2 = entityManager.SpawnEntity("ConsumerDummy", grid.ToCoordinates(0, 4)); consumer1 = entityManager.GetComponent(consumerEnt1); consumer2 = entityManager.GetComponent(consumerEnt2); supplier = entityManager.GetComponent(supplyEnt); var netBattery1 = entityManager.GetComponent(batteryEnt1); var netBattery2 = entityManager.GetComponent(batteryEnt2); var battery1 = entityManager.GetComponent(batteryEnt1); var battery2 = entityManager.GetComponent(batteryEnt2); consumer1.DrawRate = 500; consumer2.DrawRate = 1000; supplier.MaxSupply = 1000; supplier.SupplyRampTolerance = 1000; battery1.MaxCharge = 1_000_000; battery2.MaxCharge = 1_000_000; netBattery1.MaxChargeRate = 20; netBattery2.MaxChargeRate = 20; netBattery1.MaxSupply = 1_000_000; netBattery2.MaxSupply = 1_000_000; netBattery1.SupplyRampTolerance = 1_000_000; netBattery2.SupplyRampTolerance = 1_000_000; }); // Run some ticks so everything is stable. server.RunTicks(60); await server.WaitAssertion(() => { // NOTE: MaxChargeRate on batteries actually skews the demand. // So that's why the tolerance is so high, the charge rate is so *low*, // and we run so many ticks to stabilize. Assert.That(consumer1.ReceivedPower, Is.EqualTo(333.333).Within(10)); Assert.That(consumer2.ReceivedPower, Is.EqualTo(666.666).Within(10)); Assert.That(supplier.CurrentSupply, Is.EqualTo(supplier.MaxSupply).Within(0.1)); }); await pairTracker.CleanReturnAsync(); } [Test] public async Task TestBatteryEngineCut() { await using var pairTracker = await PoolManager.GetServerClient(new PoolSettings{NoClient = true, ExtraPrototypes = Prototypes}); var server = pairTracker.Pair.Server; var mapManager = server.ResolveDependency(); var entityManager = server.ResolveDependency(); PowerConsumerComponent consumer = default!; PowerSupplierComponent supplier = default!; PowerNetworkBatteryComponent netBattery = default!; await server.WaitPost(() => { var map = mapManager.CreateMap(); var grid = mapManager.CreateGrid(map); // Power only works when anchored for (var i = 0; i < 4; i++) { grid.SetTile(new Vector2i(0, i), new Tile(1)); entityManager.SpawnEntity("CableHV", grid.ToCoordinates(0, i)); } var terminal = entityManager.SpawnEntity("CableTerminal", grid.ToCoordinates(0, 1)); entityManager.GetComponent(terminal).LocalRotation = Angle.FromDegrees(180); var batteryEnt = entityManager.SpawnEntity("FullBatteryDummy", grid.ToCoordinates(0, 2)); var supplyEnt = entityManager.SpawnEntity("GeneratorDummy", grid.ToCoordinates(0, 0)); var consumerEnt = entityManager.SpawnEntity("ConsumerDummy", grid.ToCoordinates(0, 3)); consumer = entityManager.GetComponent(consumerEnt); supplier = entityManager.GetComponent(supplyEnt); netBattery = entityManager.GetComponent(batteryEnt); var battery = entityManager.GetComponent(batteryEnt); // Consumer needs 1000 W, supplier can only provide 800, battery fills in the remaining 200. consumer.DrawRate = 1000; supplier.MaxSupply = 1000; supplier.SupplyRampTolerance = 1000; netBattery.MaxSupply = 1000; netBattery.SupplyRampTolerance = 200; netBattery.SupplyRampRate = 10; battery.MaxCharge = 100_000; battery.CurrentCharge = 100_000; }); // Run some ticks so everything is stable. server.RunTicks(5); await server.WaitAssertion(() => { // Supply and consumer are fully loaded/supplied. Assert.That(consumer.ReceivedPower, Is.EqualTo(consumer.DrawRate).Within(0.5)); Assert.That(supplier.CurrentSupply, Is.EqualTo(supplier.MaxSupply).Within(0.5)); // Cut off the supplier supplier.Enabled = false; // Remove tolerance on battery too. netBattery.SupplyRampTolerance = 5; }); server.RunTicks(3); await server.WaitAssertion(() => { // Assert that network drops to 0 power and starts ramping up Assert.That(consumer.ReceivedPower, Is.LessThan(50).And.GreaterThan(0)); Assert.That(netBattery.CurrentReceiving, Is.EqualTo(0)); Assert.That(netBattery.CurrentSupply, Is.GreaterThan(0)); }); await pairTracker.CleanReturnAsync(); } /// /// Test that correctly isolates two networks. /// [Test] public async Task TestTerminalNodeGroups() { await using var pairTracker = await PoolManager.GetServerClient(new PoolSettings{NoClient = true, ExtraPrototypes = Prototypes}); var server = pairTracker.Pair.Server; var mapManager = server.ResolveDependency(); var entityManager = server.ResolveDependency(); CableNode leftNode = default!; CableNode rightNode = default!; Node batteryInput = default!; Node batteryOutput = default!; await server.WaitAssertion(() => { var map = mapManager.CreateMap(); var grid = mapManager.CreateGrid(map); // Power only works when anchored for (var i = 0; i < 4; i++) { grid.SetTile(new Vector2i(0, i), new Tile(1)); } var leftEnt = entityManager.SpawnEntity("CableHV", grid.ToCoordinates(0, 0)); entityManager.SpawnEntity("CableHV", grid.ToCoordinates(0, 1)); entityManager.SpawnEntity("CableHV", grid.ToCoordinates(0, 2)); var rightEnt = entityManager.SpawnEntity("CableHV", grid.ToCoordinates(0, 3)); var terminal = entityManager.SpawnEntity("CableTerminal", grid.ToCoordinates(0, 1)); entityManager.GetComponent(terminal).LocalRotation = Angle.FromDegrees(180); var battery = entityManager.SpawnEntity("FullBatteryDummy", grid.ToCoordinates(0, 2)); var batteryNodeContainer = entityManager.GetComponent(battery); leftNode = entityManager.GetComponent(leftEnt).GetNode("power"); rightNode = entityManager.GetComponent(rightEnt).GetNode("power"); batteryInput = batteryNodeContainer.GetNode("input"); batteryOutput = batteryNodeContainer.GetNode("output"); }); // Run ticks to allow node groups to update. server.RunTicks(1); await server.WaitAssertion(() => { Assert.That(batteryInput.NodeGroup, Is.EqualTo(leftNode.NodeGroup)); Assert.That(batteryOutput.NodeGroup, Is.EqualTo(rightNode.NodeGroup)); Assert.That(leftNode.NodeGroup, Is.Not.EqualTo(rightNode.NodeGroup)); }); await pairTracker.CleanReturnAsync(); } [Test] public async Task ApcChargingTest() { await using var pairTracker = await PoolManager.GetServerClient(new PoolSettings{NoClient = true, ExtraPrototypes = Prototypes}); var server = pairTracker.Pair.Server; var mapManager = server.ResolveDependency(); var entityManager = server.ResolveDependency(); PowerNetworkBatteryComponent substationNetBattery = default!; BatteryComponent apcBattery = default!; await server.WaitAssertion(() => { var map = mapManager.CreateMap(); var grid = mapManager.CreateGrid(map); // Power only works when anchored for (var i = 0; i < 3; i++) { grid.SetTile(new Vector2i(0, i), new Tile(1)); } entityManager.SpawnEntity("CableHV", grid.ToCoordinates(0, 0)); entityManager.SpawnEntity("CableHV", grid.ToCoordinates(0, 1)); entityManager.SpawnEntity("CableMV", grid.ToCoordinates(0, 1)); entityManager.SpawnEntity("CableMV", grid.ToCoordinates(0, 2)); var generatorEnt = entityManager.SpawnEntity("GeneratorDummy", grid.ToCoordinates(0, 0)); var substationEnt = entityManager.SpawnEntity("SubstationDummy", grid.ToCoordinates(0, 1)); var apcEnt = entityManager.SpawnEntity("ApcDummy", grid.ToCoordinates(0, 2)); var generatorSupplier = entityManager.GetComponent(generatorEnt); substationNetBattery = entityManager.GetComponent(substationEnt); apcBattery = entityManager.GetComponent(apcEnt); generatorSupplier.MaxSupply = 1000; generatorSupplier.SupplyRampTolerance = 1000; apcBattery.CurrentCharge = 0; }); server.RunTicks(5); //let run a few ticks for PowerNets to reevaluate and start charging apc await server.WaitAssertion(() => { Assert.That(substationNetBattery.CurrentSupply, Is.GreaterThan(0)); //substation should be providing power Assert.That(apcBattery.CurrentCharge, Is.GreaterThan(0)); //apc battery should have gained charge }); await pairTracker.CleanReturnAsync(); } [Test] public async Task ApcNetTest() { await using var pairTracker = await PoolManager.GetServerClient(new PoolSettings{NoClient = true, ExtraPrototypes = Prototypes}); var server = pairTracker.Pair.Server; var mapManager = server.ResolveDependency(); var entityManager = server.ResolveDependency(); var extensionCableSystem = entityManager.EntitySysManager.GetEntitySystem(); PowerNetworkBatteryComponent apcNetBattery = default!; ApcPowerReceiverComponent receiver = default!; await server.WaitAssertion(() => { var map = mapManager.CreateMap(); var grid = mapManager.CreateGrid(map); // Power only works when anchored for (var i = 0; i < 3; i++) { grid.SetTile(new Vector2i(0, i), new Tile(1)); } var apcEnt = entityManager.SpawnEntity("ApcDummy", grid.ToCoordinates(0, 0)); var apcExtensionEnt = entityManager.SpawnEntity("CableApcExtension", grid.ToCoordinates(0, 0)); var powerReceiverEnt = entityManager.SpawnEntity("ApcPowerReceiverDummy", grid.ToCoordinates(0, 2)); receiver = entityManager.GetComponent(powerReceiverEnt); var battery = entityManager.GetComponent(apcEnt); apcNetBattery = entityManager.GetComponent(apcEnt); extensionCableSystem.SetProviderTransferRange(apcExtensionEnt, 5); extensionCableSystem.SetReceiverReceptionRange(powerReceiverEnt, 5); battery.MaxCharge = 10000; //arbitrary nonzero amount of charge battery.CurrentCharge = battery.MaxCharge; //fill battery receiver.Load = 1; //arbitrary small amount of power }); server.RunTicks(1); //let run a tick for ApcNet to process power await server.WaitAssertion(() => { Assert.That(receiver.Powered); Assert.That(apcNetBattery.CurrentSupply, Is.EqualTo(1).Within(0.1)); }); await pairTracker.CleanReturnAsync(); } } }