From eebb31493cb34d810bc346b317f44be4d6be1c24 Mon Sep 17 00:00:00 2001 From: Leon Friedrich <60421075+ElectroJr@users.noreply.github.com> Date: Wed, 9 Nov 2022 14:43:45 +1300 Subject: [PATCH] Parallelize BatteryRampPegSolver (#12351) --- .../Tests/Power/PowerTest.cs | 185 +++++++- .../Electrocution/ElectrocutionSystem.cs | 2 +- .../Power/EntitySystems/ApcSystem.cs | 2 +- .../Power/EntitySystems/PowerNetSystem.cs | 32 +- Content.Server/Power/NodeGroups/PowerNet.cs | 19 +- .../Power/Pow3r/BatteryRampPegSolver.cs | 434 ++++++++++-------- Content.Server/Power/Pow3r/IPowerSolver.cs | 4 +- Content.Server/Power/Pow3r/NoOpSolver.cs | 4 +- Content.Server/Power/Pow3r/PowerState.cs | 80 +++- Pow3r/Program.Simulation.cs | 8 +- Pow3r/Program.UI.cs | 26 +- 11 files changed, 546 insertions(+), 250 deletions(-) diff --git a/Content.IntegrationTests/Tests/Power/PowerTest.cs b/Content.IntegrationTests/Tests/Power/PowerTest.cs index 0ce71302ce..ad883d954f 100644 --- a/Content.IntegrationTests/Tests/Power/PowerTest.cs +++ b/Content.IntegrationTests/Tests/Power/PowerTest.cs @@ -11,6 +11,8 @@ using Robust.Shared.GameObjects; using Robust.Shared.Map; using Robust.Shared.Maths; using Robust.Shared.Timing; +using TerraFX.Interop.Windows; +using static Content.Server.Power.Pow3r.PowerState; namespace Content.IntegrationTests.Tests.Power { @@ -428,6 +430,93 @@ namespace Content.IntegrationTests.Tests.Power await pairTracker.CleanReturnAsync(); } + [Test] + public async Task TestNoDemandRampdown() + { + // checks that batteries and supplies properly ramp down if the load is disconnected/disabled. + + 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!; + PowerNetworkBatteryComponent netBattery = default!; + BatteryComponent battery = default!; + PowerConsumerComponent consumer = default!; + + var rampRate = 500; + var rampTol = 100; + var draw = 1000; + + 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, 1)); + var batteryEnt = entityManager.SpawnEntity("DischargingBatteryDummy", grid.ToCoordinates(0,2)); + netBattery = entityManager.GetComponent(batteryEnt); + battery = entityManager.GetComponent(batteryEnt); + supplier = entityManager.GetComponent(generatorEnt); + consumer = entityManager.GetComponent(consumerEnt); + + consumer.DrawRate = draw; + + supplier.MaxSupply = draw/2; + supplier.SupplyRampRate = rampRate; + supplier.SupplyRampTolerance = rampTol; + + battery.MaxCharge = 100_000; + battery.CurrentCharge = 100_000; + netBattery.MaxSupply = draw/2; + netBattery.SupplyRampRate = rampRate; + netBattery.SupplyRampTolerance = rampTol; + }); + + server.RunTicks(1); + + await server.WaitAssertion(() => + { + Assert.That(supplier.CurrentSupply, Is.EqualTo(rampTol).Within(0.1)); + Assert.That(netBattery.CurrentSupply, Is.EqualTo(rampTol).Within(0.1)); + Assert.That(consumer.ReceivedPower, Is.EqualTo(rampTol*2).Within(0.1)); + }); + + server.RunTicks(60); + + await server.WaitAssertion(() => + { + Assert.That(supplier.CurrentSupply, Is.EqualTo(draw/2).Within(0.1)); + Assert.That(supplier.SupplyRampPosition, Is.EqualTo(draw/2).Within(0.1)); + Assert.That(netBattery.CurrentSupply, Is.EqualTo(draw / 2).Within(0.1)); + Assert.That(netBattery.SupplyRampPosition, Is.EqualTo(draw / 2).Within(0.1)); + Assert.That(consumer.ReceivedPower, Is.EqualTo(draw).Within(0.1)); + }); + + // now we disconnect the load; + consumer.NetworkLoad.Enabled = false; + + server.RunTicks(60); + + await server.WaitAssertion(() => + { + Assert.That(supplier.CurrentSupply, Is.EqualTo(0).Within(0.1)); + Assert.That(supplier.SupplyRampPosition, Is.EqualTo(0).Within(0.1)); + Assert.That(netBattery.CurrentSupply, Is.EqualTo(0).Within(0.1)); + Assert.That(netBattery.SupplyRampPosition, Is.EqualTo(0).Within(0.1)); + Assert.That(consumer.ReceivedPower, Is.EqualTo(0).Within(0.1)); + }); + + await pairTracker.CleanReturnAsync(); + } [Test] public async Task TestSimpleBatteryChargeDeficit() @@ -712,6 +801,101 @@ namespace Content.IntegrationTests.Tests.Power await pairTracker.CleanReturnAsync(); } + /// + /// Checks that if there is insufficient supply to meet demand, generators will run at full power instead of + /// having generators and batteries sharing the load. + /// + [Test] + public async Task TestSupplyPrioritized() + { + 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 supplier1 = default!; + PowerSupplierComponent supplier2 = default!; + PowerNetworkBatteryComponent netBattery1 = default!; + PowerNetworkBatteryComponent netBattery2 = default!; + BatteryComponent battery1 = default!; + BatteryComponent battery2 = default!; + + await server.WaitAssertion(() => + { + var map = mapManager.CreateMap(); + var grid = mapManager.CreateGrid(map); + + // Layout is two generators, two batteries, and one load. As to why two: because previously this test + // would fail ONLY if there were more than two batteries present, because each of them tries to supply + // the unmet load, leading to a double-battery supply attempt and ramping down of power generation from + // supplies. + + // Actual layout is Battery Supply, Load, Supply, Battery + + // Place cables + for (var i = -2; i <= 2; i++) + { + grid.SetTile(new Vector2i(0, i), new Tile(1)); + entityManager.SpawnEntity("CableHV", grid.ToCoordinates(0, i)); + } + + var batteryEnt1 = entityManager.SpawnEntity("FullBatteryDummy", grid.ToCoordinates(0, 2)); + var batteryEnt2 = entityManager.SpawnEntity("FullBatteryDummy", grid.ToCoordinates(0, -2)); + + var supplyEnt1 = entityManager.SpawnEntity("GeneratorDummy", grid.ToCoordinates(0, 1)); + var supplyEnt2 = entityManager.SpawnEntity("GeneratorDummy", grid.ToCoordinates(0, -1)); + + var consumerEnt = entityManager.SpawnEntity("ConsumerDummy", grid.ToCoordinates(0, 0)); + + consumer = entityManager.GetComponent(consumerEnt); + supplier1 = entityManager.GetComponent(supplyEnt1); + supplier2 = entityManager.GetComponent(supplyEnt2); + netBattery1 = entityManager.GetComponent(batteryEnt1); + netBattery2 = entityManager.GetComponent(batteryEnt2); + battery1 = entityManager.GetComponent(batteryEnt1); + battery2 = entityManager.GetComponent(batteryEnt2); + + // Consumer wants 2k, supplies can only provide 1k (500 each). Expectation is that batteries will only provide the necessary remaining 1k (500 each). + // Previously this failed with a 2x 333 w supplies and 2x 666 w batteries. + + consumer.DrawRate = 2000; + + supplier1.MaxSupply = 500; + supplier2.MaxSupply = 500; + supplier1.SupplyRampTolerance = 500; + supplier2.SupplyRampTolerance = 500; + + netBattery1.MaxSupply = 1000; + netBattery2.MaxSupply = 1000; + netBattery1.SupplyRampTolerance = 1000; + netBattery2.SupplyRampTolerance = 1000; + netBattery1.SupplyRampRate = 100_000; + netBattery2.SupplyRampRate = 100_000; + battery1.MaxCharge = 100_000; + battery2.MaxCharge = 100_000; + battery1.CurrentCharge = 100_000; + battery2.CurrentCharge = 100_000; + }); + + // Run some ticks so everything is stable. + server.RunTicks(60); + + await server.WaitAssertion(() => + { + Assert.That(consumer.ReceivedPower, Is.EqualTo(consumer.DrawRate).Within(0.1)); + Assert.That(supplier1.CurrentSupply, Is.EqualTo(supplier1.MaxSupply).Within(0.1)); + Assert.That(supplier2.CurrentSupply, Is.EqualTo(supplier2.MaxSupply).Within(0.1)); + + Assert.That(netBattery1.CurrentSupply, Is.EqualTo(500).Within(0.1)); + Assert.That(netBattery2.CurrentSupply, Is.EqualTo(500).Within(0.1)); + Assert.That(netBattery2.SupplyRampPosition, Is.EqualTo(500).Within(0.1)); + Assert.That(netBattery2.SupplyRampPosition, Is.EqualTo(500).Within(0.1)); + }); + + await pairTracker.CleanReturnAsync(); + } + /// /// Test that power is distributed proportionally, even through batteries. /// @@ -833,7 +1017,6 @@ namespace Content.IntegrationTests.Tests.Power 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; diff --git a/Content.Server/Electrocution/ElectrocutionSystem.cs b/Content.Server/Electrocution/ElectrocutionSystem.cs index 7f1c9d5961..81aa7b46c4 100644 --- a/Content.Server/Electrocution/ElectrocutionSystem.cs +++ b/Content.Server/Electrocution/ElectrocutionSystem.cs @@ -246,7 +246,7 @@ namespace Content.Server.Electrocution Node? TryNode(string? id) { if (id != null && nodeContainer.TryGetNode(id, out var tryNode) - && tryNode.NodeGroup is IBasePowerNet { NetworkNode: { LastAvailableSupplySum: >0 } }) + && tryNode.NodeGroup is IBasePowerNet { NetworkNode: { LastCombinedSupply: >0 } }) { return tryNode; } diff --git a/Content.Server/Power/EntitySystems/ApcSystem.cs b/Content.Server/Power/EntitySystems/ApcSystem.cs index bcac0e4132..35f7bc6bcb 100644 --- a/Content.Server/Power/EntitySystems/ApcSystem.cs +++ b/Content.Server/Power/EntitySystems/ApcSystem.cs @@ -178,7 +178,7 @@ namespace Content.Server.Power.EntitySystems return ApcExternalPowerState.None; } - var delta = netBat.CurrentReceiving - netBat.LoadingNetworkDemand; + var delta = netBat.CurrentReceiving - netBat.CurrentSupply; if (!MathHelper.CloseToPercent(delta, 0, 0.1f) && delta < 0) { return ApcExternalPowerState.Low; diff --git a/Content.Server/Power/EntitySystems/PowerNetSystem.cs b/Content.Server/Power/EntitySystems/PowerNetSystem.cs index f6aac9879a..94f736df9d 100644 --- a/Content.Server/Power/EntitySystems/PowerNetSystem.cs +++ b/Content.Server/Power/EntitySystems/PowerNetSystem.cs @@ -6,6 +6,7 @@ using Content.Server.Power.Pow3r; using JetBrains.Annotations; using Content.Shared.Power; using Robust.Server.GameObjects; +using Robust.Shared.Threading; namespace Content.Server.Power.EntitySystems { @@ -16,6 +17,7 @@ namespace Content.Server.Power.EntitySystems public sealed class PowerNetSystem : EntitySystem { [Dependency] private readonly AppearanceSystem _appearance = default!; + [Dependency] private readonly IParallelManager _parMan = default!; private readonly PowerState _powerState = new(); private readonly HashSet _powerNetReconnectQueue = new(); @@ -110,31 +112,37 @@ namespace Content.Server.Power.EntitySystems public void InitPowerNet(PowerNet powerNet) { AllocNetwork(powerNet.NetworkNode); + _powerState.GroupedNets = null; } public void DestroyPowerNet(PowerNet powerNet) { _powerState.Networks.Free(powerNet.NetworkNode.Id); + _powerState.GroupedNets = null; } public void QueueReconnectPowerNet(PowerNet powerNet) { _powerNetReconnectQueue.Add(powerNet); + _powerState.GroupedNets = null; } public void InitApcNet(ApcNet apcNet) { AllocNetwork(apcNet.NetworkNode); + _powerState.GroupedNets = null; } public void DestroyApcNet(ApcNet apcNet) { _powerState.Networks.Free(apcNet.NetworkNode.Id); + _powerState.GroupedNets = null; } public void QueueReconnectApcNet(ApcNet apcNet) { _apcNetReconnectQueue.Add(apcNet); + _powerState.GroupedNets = null; } public PowerStatistics GetStatistics() @@ -159,7 +167,7 @@ namespace Content.Server.Power.EntitySystems // A full battery will still have the same max draw rate, // but will likely have deliberately limited current draw rate. float consumptionW = network.Loads.Sum(s => _powerState.Loads[s].DesiredPower); - consumptionW += network.BatteriesCharging.Sum(s => _powerState.Batteries[s].CurrentReceiving); + consumptionW += network.BatteryLoads.Sum(s => _powerState.Batteries[s].CurrentReceiving); // This is interesting because LastMaxSupplySum seems to match LastAvailableSupplySum for some reason. // I suspect it's accounting for current supply rather than theoretical supply. @@ -172,7 +180,7 @@ namespace Content.Server.Power.EntitySystems float supplyBatteriesW = 0.0f; float storageCurrentJ = 0.0f; float storageMaxJ = 0.0f; - foreach (var discharger in network.BatteriesDischarging) + foreach (var discharger in network.BatterySupplies) { var nb = _powerState.Batteries[discharger]; supplyBatteriesW += nb.CurrentSupply; @@ -183,7 +191,7 @@ namespace Content.Server.Power.EntitySystems // And charging float outStorageCurrentJ = 0.0f; float outStorageMaxJ = 0.0f; - foreach (var charger in network.BatteriesCharging) + foreach (var charger in network.BatteryLoads) { var nb = _powerState.Batteries[charger]; outStorageCurrentJ += nb.CurrentStorage; @@ -191,7 +199,7 @@ namespace Content.Server.Power.EntitySystems } return new() { - SupplyCurrent = network.LastMaxSupplySum, + SupplyCurrent = network.LastCombinedMaxSupply, SupplyBatteries = supplyBatteriesW, SupplyTheoretical = maxSupplyW, Consumption = consumptionW, @@ -212,7 +220,7 @@ namespace Content.Server.Power.EntitySystems RaiseLocalEvent(new NetworkBatteryPreSync()); // Run power solver. - _solver.Tick(frameTime, _powerState); + _solver.Tick(frameTime, _powerState, _parMan.ParallelProcessCount); // Synchronize batteries, the other way around. RaiseLocalEvent(new NetworkBatteryPostSync()); @@ -332,8 +340,8 @@ namespace Content.Server.Power.EntitySystems var netNode = net.NetworkNode; netNode.Loads.Clear(); - netNode.BatteriesDischarging.Clear(); - netNode.BatteriesCharging.Clear(); + netNode.BatterySupplies.Clear(); + netNode.BatteryLoads.Clear(); netNode.Supplies.Clear(); foreach (var provider in net.Providers) @@ -356,7 +364,7 @@ namespace Content.Server.Power.EntitySystems foreach (var apc in net.Apcs) { var netBattery = batteryQuery.GetComponent(apc.Owner); - netNode.BatteriesDischarging.Add(netBattery.NetworkBattery.Id); + netNode.BatterySupplies.Add(netBattery.NetworkBattery.Id); netBattery.NetworkBattery.LinkedNetworkDischarging = netNode.Id; } } @@ -367,8 +375,8 @@ namespace Content.Server.Power.EntitySystems netNode.Loads.Clear(); netNode.Supplies.Clear(); - netNode.BatteriesCharging.Clear(); - netNode.BatteriesDischarging.Clear(); + netNode.BatteryLoads.Clear(); + netNode.BatterySupplies.Clear(); foreach (var consumer in net.Consumers) { @@ -387,14 +395,14 @@ namespace Content.Server.Power.EntitySystems foreach (var charger in net.Chargers) { var battery = batteryQuery.GetComponent(charger.Owner); - netNode.BatteriesCharging.Add(battery.NetworkBattery.Id); + netNode.BatteryLoads.Add(battery.NetworkBattery.Id); battery.NetworkBattery.LinkedNetworkCharging = netNode.Id; } foreach (var discharger in net.Dischargers) { var battery = batteryQuery.GetComponent(discharger.Owner); - netNode.BatteriesDischarging.Add(battery.NetworkBattery.Id); + netNode.BatterySupplies.Add(battery.NetworkBattery.Id); battery.NetworkBattery.LinkedNetworkDischarging = netNode.Id; } } diff --git a/Content.Server/Power/NodeGroups/PowerNet.cs b/Content.Server/Power/NodeGroups/PowerNet.cs index 3f90879711..cdf8b19c4f 100644 --- a/Content.Server/Power/NodeGroups/PowerNet.cs +++ b/Content.Server/Power/NodeGroups/PowerNet.cs @@ -1,10 +1,11 @@ -using System.Linq; using Content.Server.NodeContainer.NodeGroups; using Content.Server.NodeContainer.Nodes; using Content.Server.Power.Components; using Content.Server.Power.EntitySystems; using Content.Server.Power.Pow3r; using JetBrains.Annotations; +using Robust.Shared.Utility; +using System.Linq; namespace Content.Server.Power.NodeGroups { @@ -59,6 +60,7 @@ namespace Content.Server.Power.NodeGroups public void AddSupplier(PowerSupplierComponent supplier) { + DebugTools.Assert(supplier.NetworkSupply.LinkedNetwork == default); supplier.NetworkSupply.LinkedNetwork = default; Suppliers.Add(supplier); _powerNetSystem?.QueueReconnectPowerNet(this); @@ -66,6 +68,7 @@ namespace Content.Server.Power.NodeGroups public void RemoveSupplier(PowerSupplierComponent supplier) { + DebugTools.Assert(supplier.NetworkSupply.LinkedNetwork == NetworkNode.Id); supplier.NetworkSupply.LinkedNetwork = default; Suppliers.Remove(supplier); _powerNetSystem?.QueueReconnectPowerNet(this); @@ -73,6 +76,7 @@ namespace Content.Server.Power.NodeGroups public void AddConsumer(PowerConsumerComponent consumer) { + DebugTools.Assert(consumer.NetworkLoad.LinkedNetwork == default); consumer.NetworkLoad.LinkedNetwork = default; Consumers.Add(consumer); _powerNetSystem?.QueueReconnectPowerNet(this); @@ -80,6 +84,7 @@ namespace Content.Server.Power.NodeGroups public void RemoveConsumer(PowerConsumerComponent consumer) { + DebugTools.Assert(consumer.NetworkLoad.LinkedNetwork == NetworkNode.Id); consumer.NetworkLoad.LinkedNetwork = default; Consumers.Remove(consumer); _powerNetSystem?.QueueReconnectPowerNet(this); @@ -88,7 +93,8 @@ namespace Content.Server.Power.NodeGroups public void AddDischarger(BatteryDischargerComponent discharger) { var battery = IoCManager.Resolve().GetComponent(discharger.Owner); - battery.NetworkBattery.LinkedNetworkCharging = default; + DebugTools.Assert(battery.NetworkBattery.LinkedNetworkDischarging == default); + battery.NetworkBattery.LinkedNetworkDischarging = default; Dischargers.Add(discharger); _powerNetSystem?.QueueReconnectPowerNet(this); } @@ -97,7 +103,10 @@ namespace Content.Server.Power.NodeGroups { // Can be missing if the entity is being deleted, not a big deal. if (IoCManager.Resolve().TryGetComponent(discharger.Owner, out PowerNetworkBatteryComponent? battery)) - battery.NetworkBattery.LinkedNetworkCharging = default; + { + DebugTools.Assert(battery.NetworkBattery.LinkedNetworkDischarging == NetworkNode.Id); + battery.NetworkBattery.LinkedNetworkDischarging = default; + } Dischargers.Remove(discharger); _powerNetSystem?.QueueReconnectPowerNet(this); @@ -106,6 +115,7 @@ namespace Content.Server.Power.NodeGroups public void AddCharger(BatteryChargerComponent charger) { var battery = IoCManager.Resolve().GetComponent(charger.Owner); + DebugTools.Assert(battery.NetworkBattery.LinkedNetworkCharging == default); battery.NetworkBattery.LinkedNetworkCharging = default; Chargers.Add(charger); _powerNetSystem?.QueueReconnectPowerNet(this); @@ -115,7 +125,10 @@ namespace Content.Server.Power.NodeGroups { // Can be missing if the entity is being deleted, not a big deal. if (IoCManager.Resolve().TryGetComponent(charger.Owner, out PowerNetworkBatteryComponent? battery)) + { + DebugTools.Assert(battery.NetworkBattery.LinkedNetworkCharging == NetworkNode.Id); battery.NetworkBattery.LinkedNetworkCharging = default; + } Chargers.Remove(charger); _powerNetSystem?.QueueReconnectPowerNet(this); diff --git a/Content.Server/Power/Pow3r/BatteryRampPegSolver.cs b/Content.Server/Power/Pow3r/BatteryRampPegSolver.cs index 82c2fbb0ba..bec3af7686 100644 --- a/Content.Server/Power/Pow3r/BatteryRampPegSolver.cs +++ b/Content.Server/Power/Pow3r/BatteryRampPegSolver.cs @@ -1,4 +1,8 @@ -using Robust.Shared.Utility; +using Pidgin; +using Robust.Shared.Utility; +using System.Linq; +using System.Runtime.CompilerServices; +using System.Threading.Tasks; using static Content.Server.Power.Pow3r.PowerState; namespace Content.Server.Power.Pow3r @@ -17,11 +21,39 @@ namespace Content.Server.Power.Pow3r } } - private readonly PriorityQueue _sortBuffer = new(new HeightComparer()); - - public void Tick(float frameTime, PowerState state) + public void Tick(float frameTime, PowerState state, int parallel) + { + ClearLoadsAndSupplies(state); + + state.GroupedNets ??= GroupByNetworkDepth(state); + DebugTools.Assert(state.GroupedNets.Select(x => x.Count).Sum() == state.Networks.Count); + + // Each network height layer can be run in parallel without issues. + var opts = new ParallelOptions { MaxDegreeOfParallelism = parallel }; + foreach (var group in state.GroupedNets) + { + // Note that many net-layers only have a handful of networks. + // E.g., the number of nets from lowest to heights for box and saltern are: + // Saltern: 1477, 11, 2, 2, 3. + // Box: 3308, 20, 1, 5. + // + // I have NFI what the overhead for a Parallel.ForEach is, and how it compares to computing differently + // sized nets. Basic benchmarking shows that this is better, but maybe the highest-tier nets should just + // be run sequentially? But then again, maybe they are 2-3 very BIG networks at the top? So maybe: + // + // TODO make GroupByNetworkDepth evaluate the TOTAL size of each layer (i.e. loads + chargers + + // suppliers + discharger) Then decide based on total layer size whether its worth parallelizing that + // layer? + Parallel.ForEach(group, opts, net => UpdateNetwork(net, state, frameTime)); + } + + ClearBatteries(state); + + PowerSolverShared.UpdateRampPositions(frameTime, state); + } + + private void ClearLoadsAndSupplies(PowerState state) { - // Clear loads and supplies. foreach (var load in state.Loads.Values) { if (load.Paused) @@ -38,106 +70,87 @@ namespace Content.Server.Power.Pow3r supply.CurrentSupply = 0; supply.SupplyRampTarget = 0; } + } - // Run a pass to estimate network tree graph height. - // This is so that we can run networks before their children, - // to avoid draining batteries for a tick if their passing-supply gets cut off. - // It's not a big loss if this doesn't work (it won't, in some scenarios), but it's a nice-to-have. - foreach (var network in state.Networks.Values) - { - network.HeightTouched = false; - network.Height = -1; - } + private void UpdateNetwork(Network network, PowerState state, float frameTime) + { + // TODO Look at SIMD. + // a lot of this is performing very basic math on arrays of data objects like batteries + // this really shouldn't be hard to do. + // except for maybe the paused/enabled guff. If its mostly false, I guess they could just be 0 multipliers? - foreach (var network in state.Networks.Values) + // Add up demand from loads. + var demand = 0f; + foreach (var loadId in network.Loads) { - if (network.BatteriesDischarging.Count != 0) + var load = state.Loads[loadId]; + + if (!load.Enabled || load.Paused) continue; - EstimateNetworkDepth(state, network); + DebugTools.Assert(load.DesiredPower >= 0); + demand += load.DesiredPower; } - foreach (var network in state.Networks.Values) + // TODO: Consider having battery charge loads be processed "after" pass-through loads. + // This would mean that charge rate would have no impact on throughput rate like it does currently. + // Would require a second pass over the network, or something. Not sure. + + // Add demand from batteries + foreach (var batteryId in network.BatteryLoads) { - _sortBuffer.Enqueue(network.Height, network); + var battery = state.Batteries[batteryId]; + if (!battery.Enabled || !battery.CanCharge || battery.Paused) + continue; + + var batterySpace = (battery.Capacity - battery.CurrentStorage) * (1 / battery.Efficiency); + batterySpace = Math.Max(0, batterySpace); + var scaledSpace = batterySpace / frameTime; + + var chargeRate = battery.MaxChargeRate + battery.LoadingNetworkDemand / battery.Efficiency; + + battery.DesiredPower = Math.Min(chargeRate, scaledSpace); + DebugTools.Assert(battery.DesiredPower >= 0); + demand += battery.DesiredPower; } - // Go over every network. - while (_sortBuffer.TryDequeue(out _, out var network)) + DebugTools.Assert(demand >= 0); + + // Add up supply in network. + var totalSupply = 0f; + var totalMaxSupply = 0f; + foreach (var supplyId in network.Supplies) { - // Add up demand in network. - var demand = 0f; - foreach (var loadId in network.Loads) - { - var load = state.Loads[loadId]; + var supply = state.Supplies[supplyId]; + if (!supply.Enabled || supply.Paused) + continue; - if (!load.Enabled || load.Paused) - continue; + var rampMax = supply.SupplyRampPosition + supply.SupplyRampTolerance; + var effectiveSupply = Math.Min(rampMax, supply.MaxSupply); - DebugTools.Assert(load.DesiredPower >= 0); - demand += load.DesiredPower; - } + DebugTools.Assert(effectiveSupply >= 0); + DebugTools.Assert(supply.MaxSupply >= 0); - // TODO: Consider having battery charge loads be processed "after" pass-through loads. - // This would mean that charge rate would have no impact on throughput rate like it does currently. - // Would require a second pass over the network, or something. Not sure. + supply.AvailableSupply = effectiveSupply; + totalSupply += effectiveSupply; + totalMaxSupply += supply.MaxSupply; + } - // Loading batteries. - foreach (var batteryId in network.BatteriesCharging) - { - var battery = state.Batteries[batteryId]; - if (!battery.Enabled || !battery.CanCharge || battery.Paused) - continue; + var unmet = Math.Max(0, demand - totalSupply); + DebugTools.Assert(totalSupply >= 0); + DebugTools.Assert(totalMaxSupply >= 0); - var batterySpace = (battery.Capacity - battery.CurrentStorage) * (1 / battery.Efficiency); - batterySpace = Math.Max(0, batterySpace); - var scaledSpace = batterySpace / frameTime; + // Supplying batteries. Batteries need to go after local supplies so that local supplies are prioritized. + // Also, it makes demand-pulling of batteries. Because all batteries will desire the unmet demand of their + // loading network, there will be a "rush" of input current when a network powers on, before power + // stabilizes in the network. This is fine. - var chargeRate = battery.MaxChargeRate + battery.LoadingNetworkDemand / battery.Efficiency; - - var batDemand = Math.Min(chargeRate, scaledSpace); - - DebugTools.Assert(batDemand >= 0); - - battery.DesiredPower = batDemand; - demand += batDemand; - } - - DebugTools.Assert(demand >= 0); - - // Add up supply in network. - var availableSupplySum = 0f; - var maxSupplySum = 0f; - foreach (var supplyId in network.Supplies) - { - var supply = state.Supplies[supplyId]; - if (!supply.Enabled || supply.Paused) - continue; - - var rampMax = supply.SupplyRampPosition + supply.SupplyRampTolerance; - var effectiveSupply = Math.Min(rampMax, supply.MaxSupply); - - DebugTools.Assert(effectiveSupply >= 0); - DebugTools.Assert(supply.MaxSupply >= 0); - - supply.EffectiveMaxSupply = effectiveSupply; - availableSupplySum += effectiveSupply; - maxSupplySum += supply.MaxSupply; - } - - var unmet = Math.Max(0, demand - availableSupplySum); - - DebugTools.Assert(availableSupplySum >= 0); - DebugTools.Assert(maxSupplySum >= 0); - - // Supplying batteries. - // Batteries need to go after local supplies so that local supplies are prioritized. - // Also, it makes demand-pulling of batteries - // Because all batteries will will desire the unmet demand of their loading network, - // there will be a "rush" of input current when a network powers on, - // before power stabilizes in the network. - // This is fine. - foreach (var batteryId in network.BatteriesDischarging) + var totalBatterySupply = 0f; + var totalMaxBatterySupply = 0f; + if (unmet > 0) + { + // determine supply available from batteries + foreach (var batteryId in network.BatterySupplies) { var battery = state.Batteries[batteryId]; if (!battery.Enabled || !battery.CanDischarge || battery.Paused) @@ -147,103 +160,107 @@ namespace Content.Server.Power.Pow3r var supplyCap = Math.Min(battery.MaxSupply, battery.SupplyRampPosition + battery.SupplyRampTolerance); var supplyAndPassthrough = supplyCap + battery.CurrentReceiving * battery.Efficiency; - var tempSupply = Math.Min(scaledSpace, supplyAndPassthrough); - // Clamp final supply to the unmet demand, so that batteries refrain from taking power away from supplies. - var clampedSupply = Math.Min(unmet, tempSupply); - - DebugTools.Assert(clampedSupply >= 0); - - battery.TempMaxSupply = clampedSupply; - availableSupplySum += clampedSupply; - // TODO: Calculate this properly. - maxSupplySum += clampedSupply; + battery.AvailableSupply = Math.Min(scaledSpace, supplyAndPassthrough); battery.LoadingNetworkDemand = unmet; - battery.LoadingDemandMarked = true; - } - network.LastAvailableSupplySum = availableSupplySum; - network.LastMaxSupplySum = maxSupplySum; - - var met = Math.Min(demand, availableSupplySum); - - if (met != 0) - { - // Distribute supply to loads. - foreach (var loadId in network.Loads) - { - var load = state.Loads[loadId]; - if (!load.Enabled || load.DesiredPower == 0 || load.Paused) - continue; - - var ratio = load.DesiredPower / demand; - load.ReceivingPower = ratio * met; - } - - // Loading batteries - foreach (var batteryId in network.BatteriesCharging) - { - var battery = state.Batteries[batteryId]; - - if (!battery.Enabled || battery.DesiredPower == 0 || battery.Paused) - continue; - - var ratio = battery.DesiredPower / demand; - battery.CurrentReceiving = ratio * met; - var receivedPower = frameTime * battery.CurrentReceiving; - receivedPower *= battery.Efficiency; - battery.CurrentStorage = Math.Min( - battery.Capacity, - battery.CurrentStorage + receivedPower); - battery.LoadingMarked = true; - } - - // Load to supplies - foreach (var supplyId in network.Supplies) - { - var supply = state.Supplies[supplyId]; - if (!supply.Enabled || supply.EffectiveMaxSupply == 0 || supply.Paused) - continue; - - var ratio = supply.EffectiveMaxSupply / availableSupplySum; - supply.CurrentSupply = ratio * met; - - if (supply.MaxSupply != 0) - { - var maxSupplyRatio = supply.MaxSupply / maxSupplySum; - - supply.SupplyRampTarget = maxSupplyRatio * demand; - } - else - { - supply.SupplyRampTarget = 0; - } - } - - // Supplying batteries - foreach (var batteryId in network.BatteriesDischarging) - { - var battery = state.Batteries[batteryId]; - if (!battery.Enabled || battery.TempMaxSupply == 0 || battery.Paused) - continue; - - var ratio = battery.TempMaxSupply / availableSupplySum; - battery.CurrentSupply = ratio * met; - - battery.CurrentStorage = Math.Max( - 0, - battery.CurrentStorage - frameTime * battery.CurrentSupply); - - battery.SupplyRampTarget = battery.CurrentSupply - battery.CurrentReceiving * battery.Efficiency; - - /*var maxSupplyRatio = supply.MaxSupply / maxSupplySum; - - supply.SupplyRampTarget = maxSupplyRatio * demand;*/ - battery.SupplyingMarked = true; - } + battery.MaxEffectiveSupply = Math.Min(battery.CurrentStorage / frameTime, battery.MaxSupply + battery.CurrentReceiving * battery.Efficiency); + totalBatterySupply += battery.AvailableSupply; + totalMaxBatterySupply += battery.MaxEffectiveSupply; } } + network.LastCombinedSupply = totalSupply + totalBatterySupply; + network.LastCombinedMaxSupply = totalMaxSupply + totalMaxBatterySupply; + + var met = Math.Min(demand, network.LastCombinedSupply); + if (met == 0) + return; + + var supplyRatio = met / demand; + + // Distribute supply to loads. + foreach (var loadId in network.Loads) + { + var load = state.Loads[loadId]; + if (!load.Enabled || load.DesiredPower == 0 || load.Paused) + continue; + + load.ReceivingPower = load.DesiredPower * supplyRatio; + } + + // Distribute supply to batteries + foreach (var batteryId in network.BatteryLoads) + { + var battery = state.Batteries[batteryId]; + if (!battery.Enabled || battery.DesiredPower == 0 || battery.Paused) + continue; + + battery.LoadingMarked = true; + battery.CurrentReceiving = battery.DesiredPower * supplyRatio; + battery.CurrentStorage += frameTime * battery.CurrentReceiving * battery.Efficiency; + + DebugTools.Assert(battery.CurrentStorage <= battery.Capacity || MathHelper.CloseTo(battery.CurrentStorage, battery.Capacity)); + } + + // Target output capacity for supplies + var metSupply = Math.Min(demand, totalSupply); + if (metSupply > 0) + { + var relativeSupplyOutput = metSupply / totalSupply; + var targetRelativeSupplyOutput = Math.Min(demand, totalMaxSupply) / totalMaxSupply; + + // Apply load to supplies + foreach (var supplyId in network.Supplies) + { + var supply = state.Supplies[supplyId]; + if (!supply.Enabled || supply.Paused) + continue; + + supply.CurrentSupply = supply.AvailableSupply * relativeSupplyOutput; + + // Supply ramp assumes all supplies ramp at the same rate. If some generators spin up very slowly, in + // principle the fast supplies should try over-shoot until they can settle back down. E.g., all supplies + // need to reach 50% capacity, but it takes the nuclear reactor 1 hour to reach that, then our lil coal + // furnaces should run at 100% for a while. But I guess this is good enough for now. + supply.SupplyRampTarget = supply.MaxSupply * targetRelativeSupplyOutput; + } + } + + if (unmet <= 0 || totalBatterySupply <= 0) + return; + + // Target output capacity for batteries + var relativeBatteryOutput = Math.Min(unmet, totalBatterySupply) / totalBatterySupply; + var relativeTargetBatteryOutput = Math.Min(unmet, totalMaxBatterySupply) / totalMaxBatterySupply; + + // Apply load to supplying batteries + foreach (var batteryId in network.BatterySupplies) + { + var battery = state.Batteries[batteryId]; + if (!battery.Enabled || battery.Paused) + continue; + + battery.SupplyingMarked = true; + battery.CurrentSupply = battery.AvailableSupply * relativeBatteryOutput; + // Note that because available supply is always greater than or equal to the current ramp target, if you + // have multiple batteries running at less than 100% output, then batteries with greater ramp tolerances + // will contribute a larger relative fraction of output power. This is because while they will both ramp + // to the same relative maximum output, the larger tolerance will mean that one will have a larger + // available supply. IMO this is undesirable, but I can't think of an easy fix ATM. + + battery.CurrentStorage -= frameTime * battery.CurrentSupply; + DebugTools.Assert(battery.CurrentStorage >= 0 || MathHelper.CloseTo(battery.CurrentStorage, 0)); + + battery.SupplyRampTarget = battery.MaxEffectiveSupply * relativeTargetBatteryOutput - battery.CurrentReceiving * battery.Efficiency; + + DebugTools.Assert(battery.SupplyRampTarget + battery.CurrentReceiving * battery.Efficiency <= battery.LoadingNetworkDemand + || MathHelper.CloseTo(battery.SupplyRampTarget + battery.CurrentReceiving * battery.Efficiency, battery.LoadingNetworkDemand, 0.01)); + } + } + + private void ClearBatteries(PowerState state) + { // Clear supplying/loading on any batteries that haven't been marked by usage. // Because we need this data while processing ramp-pegging, we can't clear it at the start. foreach (var battery in state.Batteries.Values) @@ -252,48 +269,69 @@ namespace Content.Server.Power.Pow3r continue; if (!battery.SupplyingMarked) + { battery.CurrentSupply = 0; + battery.SupplyRampTarget = 0; + battery.LoadingNetworkDemand = 0; + } if (!battery.LoadingMarked) + { battery.CurrentReceiving = 0; - - if (!battery.LoadingDemandMarked) - battery.LoadingNetworkDemand = 0; + } battery.SupplyingMarked = false; battery.LoadingMarked = false; - battery.LoadingDemandMarked = false; } - - PowerSolverShared.UpdateRampPositions(frameTime, state); } - private static void EstimateNetworkDepth(PowerState state, Network network) + private List> GroupByNetworkDepth(PowerState state) { - network.HeightTouched = true; - - if (network.BatteriesCharging.Count == 0) + List> groupedNetworks = new(); + foreach (var network in state.Networks.Values) { - network.Height = 1; - return; + network.Height = -1; } - var max = 0; - foreach (var batteryId in network.BatteriesCharging) + foreach (var network in state.Networks.Values) + { + if (network.Height == -1) + RecursivelyEstimateNetworkDepth(state, network, groupedNetworks); + } + + return groupedNetworks; + } + + private static void RecursivelyEstimateNetworkDepth(PowerState state, Network network, List> groupedNetworks) + { + network.Height = -2; + var height = -1; + + foreach (var batteryId in network.BatteryLoads) { var battery = state.Batteries[batteryId]; - if (battery.LinkedNetworkDischarging == default) + if (battery.LinkedNetworkDischarging == default || battery.LinkedNetworkDischarging == network.Id) continue; var subNet = state.Networks[battery.LinkedNetworkDischarging]; - if (!subNet.HeightTouched) - EstimateNetworkDepth(state, subNet); + if (subNet.Height == -1) + RecursivelyEstimateNetworkDepth(state, subNet, groupedNetworks); + else if (subNet.Height == -2) + { + // this network is currently computing its own height (we encountered a loop). + continue; + } - max = Math.Max(subNet.Height, max); + height = Math.Max(subNet.Height, height); } - network.Height = 1 + max; + network.Height = 1 + height; + + if (network.Height >= groupedNetworks.Count) + groupedNetworks.Add(new() { network }); + else + groupedNetworks[network.Height].Add(network); } } } diff --git a/Content.Server/Power/Pow3r/IPowerSolver.cs b/Content.Server/Power/Pow3r/IPowerSolver.cs index af1fe2d035..d386888f0a 100644 --- a/Content.Server/Power/Pow3r/IPowerSolver.cs +++ b/Content.Server/Power/Pow3r/IPowerSolver.cs @@ -1,7 +1,7 @@ -namespace Content.Server.Power.Pow3r +namespace Content.Server.Power.Pow3r { public interface IPowerSolver { - void Tick(float frameTime, PowerState state); + void Tick(float frameTime, PowerState state, int parallel); } } diff --git a/Content.Server/Power/Pow3r/NoOpSolver.cs b/Content.Server/Power/Pow3r/NoOpSolver.cs index fa54a8b80d..2a714e49fd 100644 --- a/Content.Server/Power/Pow3r/NoOpSolver.cs +++ b/Content.Server/Power/Pow3r/NoOpSolver.cs @@ -1,8 +1,8 @@ -namespace Content.Server.Power.Pow3r +namespace Content.Server.Power.Pow3r { public sealed class NoOpSolver : IPowerSolver { - public void Tick(float frameTime, PowerState state) + public void Tick(float frameTime, PowerState state, int parallel) { // Literally nothing. } diff --git a/Content.Server/Power/Pow3r/PowerState.cs b/Content.Server/Power/Pow3r/PowerState.cs index a8f0420ce4..1d35af254b 100644 --- a/Content.Server/Power/Pow3r/PowerState.cs +++ b/Content.Server/Power/Pow3r/PowerState.cs @@ -1,10 +1,11 @@ -using System.Collections; +using System.Collections; using System.Diagnostics.CodeAnalysis; using System.Linq; using System.Runtime.CompilerServices; using System.Text.Json; using System.Text.Json.Serialization; using Robust.Shared.Utility; +using static Content.Server.Power.Pow3r.PowerState; namespace Content.Server.Power.Pow3r { @@ -20,6 +21,7 @@ namespace Content.Server.Power.Pow3r public GenIdStorage Networks = new(); public GenIdStorage Loads = new(); public GenIdStorage Batteries = new(); + public List>? GroupedNets; public readonly struct NodeId : IEquatable { @@ -168,6 +170,10 @@ namespace Content.Server.Power.Pow3r storage.Count = cache.Length; storage._nextFree = nextFree; + // I think there is some issue with Pow3er's Save & Load to json leading to it constructing invalid GenIdStorages from json? + // If you get this error, clear out your data.json + DebugTools.Assert(storage.Values.Count() == storage.Count); + return storage; } @@ -352,20 +358,29 @@ namespace Content.Server.Power.Pow3r // == Runtime parameters == - // Actual power supplied last network update. + /// + /// Actual power supplied last network update. + /// [ViewVariables(VVAccess.ReadWrite)] public float CurrentSupply; - // The amount of power we WANT to be supplying to match grid load. + /// + /// The amount of power we WANT to be supplying to match grid load. + /// [ViewVariables(VVAccess.ReadWrite)] [JsonIgnore] public float SupplyRampTarget; - // Position of the supply ramp. + /// + /// Position of the supply ramp. + /// [ViewVariables(VVAccess.ReadWrite)] public float SupplyRampPosition; [ViewVariables] [JsonIgnore] public NodeId LinkedNetwork; - // In-tick max supply thanks to ramp. Used during calculations. - [JsonIgnore] public float EffectiveMaxSupply; + /// + /// Supply available during a tick. The actual current supply will be less than or equal to this. Used + /// during calculations. + /// + [JsonIgnore] public float AvailableSupply; } public sealed class Load @@ -396,7 +411,15 @@ namespace Content.Server.Power.Pow3r [ViewVariables(VVAccess.ReadWrite)] public float MaxChargeRate; [ViewVariables(VVAccess.ReadWrite)] public float MaxThroughput; // 0 = infinite cuz imgui [ViewVariables(VVAccess.ReadWrite)] public float MaxSupply; + + /// + /// The batteries supply ramp tolerance. This is an always available supply added to the ramped supply. + /// + /// + /// Note that this MUST BE GREATER THAN ZERO, otherwise the current battery ramping calculation will not work. + /// [ViewVariables(VVAccess.ReadWrite)] public float SupplyRampTolerance = 5000; + [ViewVariables(VVAccess.ReadWrite)] public float SupplyRampRate = 5000; [ViewVariables(VVAccess.ReadWrite)] public float Efficiency = 1; @@ -413,11 +436,11 @@ namespace Content.Server.Power.Pow3r [ViewVariables(VVAccess.ReadWrite)] [JsonIgnore] public bool LoadingMarked; + /// + /// Amount of supply that the battery can provider this tick. + /// [ViewVariables(VVAccess.ReadWrite)] [JsonIgnore] - public bool LoadingDemandMarked; - - [ViewVariables(VVAccess.ReadWrite)] [JsonIgnore] - public float TempMaxSupply; + public float AvailableSupply; [ViewVariables(VVAccess.ReadWrite)] [JsonIgnore] public float DesiredPower; @@ -430,6 +453,13 @@ namespace Content.Server.Power.Pow3r [ViewVariables(VVAccess.ReadWrite)] [JsonIgnore] public NodeId LinkedNetworkDischarging; + + /// + /// Theoretical maximum effective supply, assuming the network providing power to this battery continues to supply it + /// at the same rate. + /// + [ViewVariables] + public float MaxEffectiveSupply; } // Readonly breaks json serialization. @@ -438,21 +468,37 @@ namespace Content.Server.Power.Pow3r { [ViewVariables] public NodeId Id; + /// + /// Power generators + /// [ViewVariables] public List Supplies = new(); + /// + /// Power consumers. + /// [ViewVariables] public List Loads = new(); - // "Loading" means the network is connected to the INPUT port of the battery. - [ViewVariables] public List BatteriesCharging = new(); + /// + /// Batteries that are draining power from this network (connected to the INPUT port of the battery). + /// + [ViewVariables] public List BatteryLoads = new(); - // "Supplying" means the network is connected to the OUTPUT port of the battery. - [ViewVariables] public List BatteriesDischarging = new(); + /// + /// Batteries that are supplying power to this network (connected to the OUTPUT port of the battery). + /// + [ViewVariables] public List BatterySupplies = new(); - [ViewVariables] public float LastAvailableSupplySum = 0f; - [ViewVariables] public float LastMaxSupplySum = 0f; + /// + /// Available supply, including both normal supplies and batteries. + /// + [ViewVariables] public float LastCombinedSupply = 0f; + + /// + /// Theoretical maximum supply, including both normal supplies and batteries. + /// + [ViewVariables] public float LastCombinedMaxSupply = 0f; [ViewVariables] [JsonIgnore] public int Height; - [JsonIgnore] public bool HeightTouched; } } } diff --git a/Pow3r/Program.Simulation.cs b/Pow3r/Program.Simulation.cs index fac2dbc415..683ee0eb3e 100644 --- a/Pow3r/Program.Simulation.cs +++ b/Pow3r/Program.Simulation.cs @@ -1,4 +1,4 @@ -using System.Collections.Generic; +using System.Collections.Generic; using System.Diagnostics; using Content.Server.Power.Pow3r; using static Content.Server.Power.Pow3r.PowerState; @@ -45,7 +45,7 @@ namespace Pow3r _simStopwatch.Restart(); _tickDataIdx = (_tickDataIdx + 1) % MaxTickData; - _solvers[_currentSolver].Tick(frameTime, _state); + _solvers[_currentSolver].Tick(frameTime, _state, 1); // Update tick history. foreach (var load in _state.Loads.Values) @@ -111,13 +111,13 @@ namespace Pow3r supply.LinkedNetwork = network.Id; } - foreach (var batteryId in network.BatteriesCharging) + foreach (var batteryId in network.BatteryLoads) { var battery = _state.Batteries[batteryId]; battery.LinkedNetworkCharging = network.Id; } - foreach (var batteryId in network.BatteriesDischarging) + foreach (var batteryId in network.BatterySupplies) { var battery = _state.Batteries[batteryId]; battery.LinkedNetworkDischarging = network.Id; diff --git a/Pow3r/Program.UI.cs b/Pow3r/Program.UI.cs index 9a99a52699..af14a36f90 100644 --- a/Pow3r/Program.UI.cs +++ b/Pow3r/Program.UI.cs @@ -1,4 +1,4 @@ -using System; +using System; using System.Collections.Generic; using ImGuiNET; using Robust.Shared.Maths; @@ -52,6 +52,7 @@ namespace Pow3r { var network = new Network(); _state.Networks.Allocate(out network.Id) = network; + _state.GroupedNets = null; _displayNetworks.Add(network.Id, new DisplayNetwork()); } @@ -60,6 +61,7 @@ namespace Pow3r var battery = new Battery(); _state.Batteries.Allocate(out battery.Id) = battery; _displayBatteries.Add(battery.Id, new DisplayBattery()); + _state.GroupedNets = null; } Checkbox("Paused", ref _paused); @@ -270,7 +272,8 @@ namespace Pow3r { if (battery.LinkedNetworkCharging == default && Button("Link as load")) { - _linking.BatteriesCharging.Add(battery.Id); + _linking.BatteryLoads.Add(battery.Id); + _state.GroupedNets = null; _linking = null; RefreshLinks(); } @@ -279,7 +282,8 @@ namespace Pow3r SameLine(); if (battery.LinkedNetworkDischarging == default && Button("Link as supply")) { - _linking.BatteriesDischarging.Add(battery.Id); + _linking.BatterySupplies.Add(battery.Id); + _state.GroupedNets = null; _linking = null; RefreshLinks(); } @@ -290,7 +294,8 @@ namespace Pow3r if (battery.LinkedNetworkCharging != default && Button("Unlink loading")) { var net = _state.Networks[battery.LinkedNetworkCharging]; - net.BatteriesCharging.Remove(battery.Id); + net.BatteryLoads.Remove(battery.Id); + _state.GroupedNets = null; battery.LinkedNetworkCharging = default; } else @@ -299,7 +304,8 @@ namespace Pow3r if (battery.LinkedNetworkDischarging != default && Button("Unlink supplying")) { var net = _state.Networks[battery.LinkedNetworkDischarging]; - net.BatteriesDischarging.Remove(battery.Id); + net.BatterySupplies.Remove(battery.Id); + _state.GroupedNets = null; battery.LinkedNetworkDischarging = default; } } @@ -331,13 +337,13 @@ namespace Pow3r DrawArrowLine(bgDrawList, load.CurrentWindowPos, displayNet.CurrentWindowPos, Color.Red); } - foreach (var batteryId in network.BatteriesCharging) + foreach (var batteryId in network.BatteryLoads) { var battery = _displayBatteries[batteryId]; DrawArrowLine(bgDrawList, battery.CurrentWindowPos, displayNet.CurrentWindowPos, Color.Purple); } - foreach (var batteryId in network.BatteriesDischarging) + foreach (var batteryId in network.BatterySupplies) { var battery = _displayBatteries[batteryId]; DrawArrowLine(bgDrawList, displayNet.CurrentWindowPos, battery.CurrentWindowPos, Color.Cyan); @@ -357,6 +363,7 @@ namespace Pow3r case Network n: _state.Networks.Free(n.Id); _displayNetworks.Remove(n.Id); + _state.GroupedNets = null; reLink = true; break; @@ -374,9 +381,10 @@ namespace Pow3r case Battery b: _state.Batteries.Free(b.Id); - _state.Networks.Values.ForEach(n => n.BatteriesCharging.Remove(b.Id)); - _state.Networks.Values.ForEach(n => n.BatteriesDischarging.Remove(b.Id)); + _state.Networks.Values.ForEach(n => n.BatteryLoads.Remove(b.Id)); + _state.Networks.Values.ForEach(n => n.BatterySupplies.Remove(b.Id)); _displayBatteries.Remove(b.Id); + _state.GroupedNets = null; break; } }