Files
LuaCsForBarotraumaEP/Barotrauma/BarotraumaShared/SharedSource/Items/Components/Power/Powered.cs
Eero 46595b1399 WIP Make collections thread-safe and add safe iteration
Replaced static lists and dictionaries with thread-safe ConcurrentDictionary or ThreadLocal collections for various item components and systems. Updated all relevant code to use snapshots (ToArray, ToList) for safe iteration, and added helper methods for marking and clearing changed connections. These changes improve thread safety and prevent potential concurrency issues in multi-threaded scenarios.
2025-12-28 04:59:56 +08:00

887 lines
34 KiB
C#

using System;
using System.Collections.Concurrent;
using System.Threading;
using Microsoft.Xna.Framework;
using System.Collections.Generic;
using System.Linq;
using Barotrauma.Extensions;
#if CLIENT
using Barotrauma.Sounds;
#endif
namespace Barotrauma.Items.Components
{
/// <summary>
/// Order in which power sources will provide to a grid, lower number is higher priority
/// </summary>
public enum PowerPriority
{
Default = 0, // Use for status effects and/or extraload
Reactor = 1,
Relay = 3,
Battery = 5
}
readonly struct PowerRange
{
public readonly static PowerRange Zero = default;
public readonly float Min;
public readonly float Max;
/// <summary>
/// Used by reactors to communicate their maximum output to each other so they can divide the grid load between each other in a sensible way
/// </summary>
public readonly float ReactorMaxOutput;
public PowerRange(float min, float max) : this(min, max, 0.0f)
{
}
public PowerRange(float min, float max, float reactorMaxOutput)
{
System.Diagnostics.Debug.Assert(max >= min);
System.Diagnostics.Debug.Assert(min >= 0);
System.Diagnostics.Debug.Assert(max >= 0);
Min = min;
Max = max;
ReactorMaxOutput = reactorMaxOutput;
}
public static PowerRange operator +(PowerRange a, PowerRange b)
{
return new PowerRange(a.Min + b.Min, a.Max + b.Max, a.ReactorMaxOutput + b.ReactorMaxOutput);
}
public static PowerRange operator -(PowerRange a, PowerRange b)
{
return new PowerRange(a.Min - b.Min, a.Max - b.Max, a.ReactorMaxOutput - b.ReactorMaxOutput);
}
}
partial class Powered : ItemComponent
{
//TODO: test sparser update intervals?
protected const float UpdateInterval = (float)Timing.Step;
/// <summary>
/// List of all powered ItemComponents (thread-safe)
/// </summary>
private static readonly ConcurrentDictionary<Powered, byte> _poweredDict = new ConcurrentDictionary<Powered, byte>();
/// <summary>
/// Cached list for iteration - updated when collection changes
/// </summary>
private static volatile List<Powered> _cachedPoweredList;
private static int _poweredListVersion;
public static IEnumerable<Powered> PoweredList
{
get
{
var cached = _cachedPoweredList;
if (cached != null) return cached;
return GetCachedPoweredList();
}
}
private static List<Powered> GetCachedPoweredList()
{
var newList = _poweredDict.Keys.ToList();
_cachedPoweredList = newList;
return newList;
}
private static void InvalidatePoweredListCache()
{
_cachedPoweredList = null;
Interlocked.Increment(ref _poweredListVersion);
}
/// <summary>
/// Thread-safe set of changed connections
/// </summary>
private static readonly ConcurrentDictionary<Connection, byte> _changedConnections = new ConcurrentDictionary<Connection, byte>();
/// <summary>
/// Gets all changed connections (snapshot)
/// </summary>
public static ICollection<Connection> ChangedConnections => _changedConnections.Keys;
/// <summary>
/// Add a connection to the changed set
/// </summary>
public static void MarkConnectionChanged(Connection c)
{
_changedConnections.TryAdd(c, 0);
}
/// <summary>
/// Clear all changed connections
/// </summary>
public static void ClearChangedConnections()
{
_changedConnections.Clear();
}
/// <summary>
/// Remove a connection from the changed set
/// </summary>
public static void UnmarkConnectionChanged(Connection c)
{
_changedConnections.TryRemove(c, out _);
}
/// <summary>
/// Thread-safe grid dictionary
/// </summary>
public readonly static ConcurrentDictionary<int, GridInfo> Grids = new ConcurrentDictionary<int, GridInfo>();
/// <summary>
/// The amount of power currently consumed by the item. Negative values mean that the item is providing power to connected items
/// </summary>
protected float currPowerConsumption;
/// <summary>
/// Current voltage of the item (load / power)
/// </summary>
private float voltage;
/// <summary>
/// The minimum voltage required for the item to work
/// </summary>
private float minVoltage;
/// <summary>
/// The maximum amount of power the item can draw from connected items
/// </summary>
protected float powerConsumption;
protected Connection powerIn;
protected List<Connection> powerOuts = new List<Connection>();
/// <summary>
/// Throws an error if there is more than one power out connection.<br/>
/// Use <see cref="powerOuts"/> if a component should handle multiple outputs.
/// </summary>
protected Connection powerOut
{
get
{
if (powerOuts.Count > 1) { DebugConsole.ThrowErrorOnce($"{item.ID}.multiplePowerOut", $"Item {item.Name} ({item.Prefab.Identifier}) has multiple power outputs, but only supports one!"); }
return powerOuts.FirstOrDefault();
}
}
protected bool powerInIsPowerOut => powerOuts.Contains(powerIn);
/// <summary>
/// Maximum voltage factor when the device is being overvolted. I.e. how many times more effectively the device can function when it's being overvolted
/// </summary>
protected const float MaxOverVoltageFactor = 2.0f;
protected virtual PowerPriority Priority { get { return PowerPriority.Default; } }
[Header(localizedTextTag: "sp.powered.propertyheader")]
[Editable, Serialize(0.5f, IsPropertySaveable.Yes, description: "The minimum voltage required for the device to function. " +
"The voltage is calculated as power / powerconsumption, meaning that a device " +
"with a power consumption of 1000 kW would need at least 500 kW of power to work if the minimum voltage is set to 0.5.")]
public float MinVoltage
{
get { return powerConsumption <= 0.0f ? 0.0f : minVoltage; }
set { minVoltage = value; }
}
[Editable, Serialize(0.0f, IsPropertySaveable.Yes, description: "How much power the device draws (or attempts to draw) from the electrical grid when active.")]
public float PowerConsumption
{
get { return powerConsumption; }
set { powerConsumption = value; }
}
[Serialize(false, IsPropertySaveable.Yes, description: "Is the device currently active. Inactive devices don't consume power.")]
public override bool IsActive
{
get { return base.IsActive; }
set
{
base.IsActive = value;
if (!value)
{
currPowerConsumption = 0.0f;
}
}
}
[Serialize(0.0f, IsPropertySaveable.Yes, description: "The current power consumption of the device. Intended to be used by StatusEffect conditionals (setting the value from XML is not recommended).")]
public float CurrPowerConsumption
{
get {return currPowerConsumption; }
set { currPowerConsumption = value; }
}
[Serialize(0.0f, IsPropertySaveable.Yes, description: "The current voltage of the item (calculated as power consumption / available power). Intended to be used by StatusEffect conditionals (setting the value from XML is not recommended).")]
public float Voltage
{
get
{
if (PoweredByTinkering)
{
return 1.0f;
}
else if (powerIn != null)
{
if (powerIn?.Grid != null) { return powerIn.Grid.Voltage; }
}
else if (powerOuts.Any())
{
IEnumerable<Connection> gridConnections = powerOuts.Where(static conn => conn.Grid != null);
if (gridConnections.Any()) { return gridConnections.Average(static conn => conn.Grid.Voltage); }
}
if (this is PowerTransfer && item.Condition <= 0.0f)
{
//if the junction box or other power transfer device is broken,
//it cannot be supplying any power (voltage = 0)
return 0.0f;
}
return PowerConsumption <= 0.0f ? 1.0f : voltage;
}
set
{
voltage = Math.Max(0.0f, value);
}
}
/// <summary>
/// Essentially Voltage / MinVoltage (= how much of the minimum required voltage has been satisfied), clamped between 0 and 1.
/// Can be used by status effects or sounds to check if the item has enough power to run
/// </summary>
public float RelativeVoltage => minVoltage <= 0.0f ? 1.0f : MathHelper.Clamp(Voltage / minVoltage, 0.0f, 1.0f);
public virtual bool HasPower => Voltage >= MinVoltage;
public bool PoweredByTinkering { get; set; }
[Editable, Serialize(true, IsPropertySaveable.Yes, description: "Can the item be damaged by electomagnetic pulses.")]
public bool VulnerableToEMP
{
get;
set;
}
public Powered(Item item, ContentXElement element)
: base(item, element)
{
_poweredDict.TryAdd(this, 0);
InvalidatePoweredListCache();
InitProjectSpecific(element);
}
partial void InitProjectSpecific(ContentXElement element);
protected void UpdateOnActiveEffects(float deltaTime)
{
if (currPowerConsumption <= 0.0f && PowerConsumption <= 0.0f)
{
//if the item consumes no power, ignore the voltage requirement and
//apply OnActive statuseffects as long as this component is active
ApplyStatusEffects(ActionType.OnActive, deltaTime);
return;
}
if (Voltage > minVoltage)
{
ApplyStatusEffects(ActionType.OnActive, deltaTime);
}
#if CLIENT
if (Voltage > minVoltage)
{
if (!powerOnSoundPlayed && powerOnSound != null)
{
SoundPlayer.PlaySound(powerOnSound, item.WorldPosition, hullGuess: item.CurrentHull);
powerOnSoundPlayed = true;
}
}
else if (Voltage < 0.1f)
{
powerOnSoundPlayed = false;
}
#endif
}
public override void Update(float deltaTime, Camera cam)
{
UpdateOnActiveEffects(deltaTime);
}
public override void OnItemLoaded()
{
if (item.Connections == null) { return; }
foreach (Connection c in item.Connections)
{
if (!c.IsPower) { continue; }
if (this is PowerTransfer pt)
{
if (c.Name == "power_in")
{
powerIn = c;
}
else if (c.Name == "power")
{
powerIn = c;
powerOuts.Add(c);
}
else if (c.IsOutput)
{
powerOuts.Add(c);
// Connection takes the lowest priority
if (Priority > c.Priority)
{
c.Priority = Priority;
}
}
}
else
{
if (c.IsOutput)
{
if (c.Name == "power_in")
{
#if DEBUG
DebugConsole.ThrowError($"Item \"{item.Name}\" has a power output connection called power_in. If the item is supposed to receive power through the connection, change it to an input connection.");
#else
DebugConsole.NewMessage($"Item \"{item.Name}\" has a power output connection called power_in. If the item is supposed to receive power through the connection, change it to an input connection.", Color.Orange);
#endif
}
powerOuts.Add(c);
// Connection takes the lowest priority
if (Priority > c.Priority)
{
c.Priority = Priority;
}
}
else
{
if (c.Name == "power_out")
{
#if DEBUG
DebugConsole.ThrowError($"Item \"{item.Name}\" has a power input connection called power_out. If the item is supposed to output power through the connection, change it to an output connection.");
#else
DebugConsole.NewMessage($"Item \"{item.Name}\" has a power input connection called power_out. If the item is supposed to output power through the connection, change it to an output connection.", Color.Orange);
#endif
}
powerIn = c;
}
}
}
}
/// <summary>
/// Allocate electrical devices into their grids based on connections
/// </summary>
/// <param name="useCache">Use previous grids and change in connections</param>
public static void UpdateGrids(bool useCache = true)
{
//don't use cache if there are no existing grids
if (Grids.Count > 0 && useCache)
{
// Take a snapshot of changed connections for iteration
var changedSnapshot = ChangedConnections.ToList();
//delete all grids that were affected
foreach (Connection c in changedSnapshot)
{
if (c.Grid != null)
{
Grids.TryRemove(c.Grid.ID, out _);
c.Grid = null;
}
}
foreach (Connection c in changedSnapshot)
{
//Make sure the connection grid hasn't been resolved by another connection update
//Ensure the connection has other connections
if (c.Grid == null && c.Recipients.Count > 0 && c.Item.Condition > 0.0f)
{
GridInfo grid = PropagateGrid(c);
Grids[grid.ID] = grid;
}
}
}
else
{
//Clear all grid IDs from connections
foreach (Powered powered in PoweredList)
{
//Only check devices with connectors
if (powered.powerIn != null)
{
powered.powerIn.Grid = null;
}
foreach (Connection powerOut in powered.powerOuts)
{
powerOut.Grid = null;
}
}
Grids.Clear();
foreach (Powered powered in PoweredList)
{
if (powered.Item.Condition <= 0f) { continue; }
//Probe through all connections that don't have a gridID
if (powered.powerIn != null && powered.powerIn.Grid == null && !powered.powerInIsPowerOut)
{
// Only create grids for networks with more than 1 device
if (powered.powerIn.Recipients.Count > 0)
{
GridInfo grid = PropagateGrid(powered.powerIn);
Grids[grid.ID] = grid;
}
}
foreach (Connection powerOut in powered.powerOuts)
{
if (powerOut != null && powerOut.Grid == null)
{
//Only create grids for networks with more than 1 device
if (powerOut.Recipients.Count > 0)
{
GridInfo grid = PropagateGrid(powerOut);
Grids[grid.ID] = grid;
}
}
}
}
}
//Clear changed connections after each update
ClearChangedConnections();
}
private static GridInfo PropagateGrid(Connection conn)
{
//Generate unique Key
int id = Rand.Int(int.MaxValue, Rand.RandSync.Unsynced);
while (Grids.ContainsKey(id))
{
id = Rand.Int(int.MaxValue, Rand.RandSync.Unsynced);
}
return PropagateGrid(conn, id);
}
private static GridInfo PropagateGrid(Connection conn, int gridID)
{
Stack<Connection> probeStack = new Stack<Connection>();
GridInfo grid = new GridInfo(gridID);
probeStack.Push(conn);
//Non recursive approach to traversing connection tree
while (probeStack.Count > 0)
{
Connection c = probeStack.Pop();
c.Grid = grid;
grid.AddConnection(c);
//Add on recipients - use ToList() snapshot for thread-safe iteration
foreach (Connection otherC in c.Recipients.ToList())
{
//Only add valid connections
if (otherC.Grid != grid && (otherC.Grid == null || !Grids.ContainsKey(otherC.Grid.ID)) && ValidPowerConnection(c, otherC))
{
otherC.Grid = grid; //Assigning ID early prevents unncessary adding to stack
probeStack.Push(otherC);
}
}
}
return grid;
}
/// <summary>
/// Update the power calculations of all devices and grids
/// Updates grids in the order of
/// ConnCurrConsumption - Get load of device/ flag it as an outputting connection
/// -- If outputting power --
/// MinMaxPower - Minimum and Maximum power output of the connection for devices to coordinate
/// ConnPowerOut - Final power output based on the sum of the MinMaxPower
/// -- Finally --
/// GridResolved - Indicate that a connection's grid has been finished being calculated
///
/// Power outputting devices are calculated in stages based on their priority
/// Reactors will output first, followed by relays then batteries.
///
/// </summary>
/// <param name="deltaTime"></param>
public static void UpdatePower(float deltaTime)
{
//Don't update the power if the round is ending
if (GameMain.GameSession != null && GameMain.GameSession.RoundEnding)
{
return;
}
//Only update the power at the given update interval
/*
//Not use currently as update interval of 1/60
if (updateTimer > 0.0f)
{
updateTimer -= deltaTime;
return;
}
updateTimer = UpdateInterval;
*/
#if CLIENT
System.Diagnostics.Stopwatch sw = new System.Diagnostics.Stopwatch();
sw.Start();
#endif
//Ensure all grids are updated correctly and have the correct connections
UpdateGrids();
#if CLIENT
sw.Stop();
GameMain.PerformanceCounter.AddElapsedTicks("Update:Power", sw.ElapsedTicks);
sw.Restart();
#endif
//Reset all grids
foreach (GridInfo grid in Grids.Values)
{
//Wipe priority groups as connections can change to not be outputting -- Can be improved caching wise --
grid.PowerSourceGroups.Clear();
grid.Power = 0;
grid.Load = 0;
}
//Determine if devices are adding a load or providing power, also resolve solo nodes
foreach (Powered powered in PoweredList)
{
//Make voltage decay to ensure the device powers down.
//This only effects devices with no power input (whose voltage is set by other means, e.g. status effects from a contained battery)
//or devices that have been disconnected from the power grid - other devices use the voltage of the grid instead.
powered.Voltage -= deltaTime;
//Handle the device if it's got a power connection
if (powered.powerIn != null && !powered.powerInIsPowerOut)
{
//Get the new load for the connection
float currLoad = powered.GetCurrentPowerConsumption(powered.powerIn);
//If its a load update its grid load
if (currLoad >= 0)
{
if (powered.PoweredByTinkering) { currLoad = 0.0f; }
powered.CurrPowerConsumption = currLoad;
if (powered.powerIn.Grid != null)
{
powered.powerIn.Grid.Load += currLoad;
}
}
else if (powered.powerIn.Grid != null)
{
//If connected to a grid add as a source to be processed
powered.powerIn.Grid.AddSrc(powered.powerIn);
}
else
{
powered.CurrPowerConsumption = -powered.GetConnectionPowerOut(powered.powerIn, 0, powered.MinMaxPowerOut(powered.powerIn, 0), 0);
powered.GridResolved(powered.powerIn);
}
}
//Handle the device power depending on if its powerout
foreach (Connection powerOut in powered.powerOuts)
{
//Get the connection's load
float currLoad = powered.GetCurrentPowerConsumption(powerOut);
//Update the device's output load to the correct variable
if (powered is PowerTransfer pt)
{
pt.PowerLoad = currLoad;
}
else if (powered is PowerContainer pc)
{
// PowerContainer handle its own output value
}
else
{
powered.CurrPowerConsumption = currLoad;
}
if (currLoad >= 0)
{
//Add to the grid load if possible
if (powerOut.Grid != null)
{
powerOut.Grid.Load += currLoad;
}
}
else if (powerOut.Grid != null)
{
//Add connection as a source to be processed
powerOut.Grid.AddSrc(powerOut);
}
else
{
//Perform power calculations for the singular connection
float loadOut = -powered.GetConnectionPowerOut(powerOut, 0, powered.MinMaxPowerOut(powerOut, 0), 0);
if (powered is PowerTransfer pt2)
{
pt2.PowerLoad = loadOut;
}
else if (powered is PowerContainer pc)
{
//PowerContainer handles its own output value
}
else
{
powered.CurrPowerConsumption = loadOut;
}
//Indicate grid is resolved as it was the only device
powered.GridResolved(powerOut);
}
}
}
//Iterate through all grids to determine the power on the grid
foreach (GridInfo grid in Grids.Values)
{
//Iterate through the priority src groups lowest first
foreach (PowerSourceGroup scrGroup in grid.PowerSourceGroups.Values)
{
scrGroup.MinMaxPower = PowerRange.Zero;
//Iterate through all connections in the group to get their minmax power and sum them
foreach (Connection c in scrGroup.Connections)
{
foreach (var device in c.Item.GetComponents<Powered>())
{
scrGroup.MinMaxPower += device.MinMaxPowerOut(c, grid.Load);
}
}
//Iterate through all connections to get their final power out provided the min max information
float addedPower = 0;
foreach (Connection c in scrGroup.Connections)
{
foreach (var device in c.Item.GetComponents<Powered>())
{
addedPower += device.GetConnectionPowerOut(c, grid.Power, scrGroup.MinMaxPower, grid.Load);
}
}
//Add the power to the grid
grid.Power += addedPower;
}
//Calculate Grid voltage, limit between 0 - 1000
float newVoltage = MathHelper.Min(grid.Power / MathHelper.Max(grid.Load, 1E-10f), 1000);
if (float.IsNegative(newVoltage))
{
newVoltage = 0.0f;
}
grid.Voltage = newVoltage;
//Iterate through all connections on that grid and run their gridResolved function
foreach (Connection c in grid.Connections)
{
foreach (var device in c.Item.GetComponents<Powered>())
{
device?.GridResolved(c);
}
}
}
#if CLIENT
sw.Stop();
GameMain.PerformanceCounter.AddElapsedTicks("Update:Power", sw.ElapsedTicks);
#endif
}
/// <summary>
/// Current power consumption of the device (or amount of generated power if negative)
/// </summary>
/// <param name="connection">Connection to calculate power consumption for.</param>
public virtual float GetCurrentPowerConsumption(Connection connection = null)
{
// If a handheld device there is no consumption
if (powerIn == null && powerOuts.None())
{
return 0;
}
// Add extraload for PowerTransfer devices
if (this is PowerTransfer pt)
{
return PowerConsumption + pt.ExtraLoad;
}
else if (connection != this.powerIn || !IsActive)
{
//If not the power in connection or is inactive there is no draw
return 0;
}
//Otherwise return the max powerconsumption of the device
return PowerConsumption;
}
/// <summary>
/// Minimum and maximum power the connection can provide
/// </summary>
/// <param name="conn">Connection being queried about its power capabilities</param>
/// <param name="load">Load of the connected grid</param>
public virtual PowerRange MinMaxPowerOut(Connection conn, float load = 0)
{
return PowerRange.Zero;
}
/// <summary>
/// Finalize how much power the device will be outputting to the connection
/// </summary>
/// <param name="conn">Connection being queried</param>
/// <param name="power">Current grid power</param>
/// <param name="load">Current load on the grid</param>
/// <returns>Power pushed to the grid</returns>
public virtual float GetConnectionPowerOut(Connection conn, float power, PowerRange minMaxPower, float load)
{
return powerOuts.Contains(conn) ? MathHelper.Max(-CurrPowerConsumption, 0) : 0;
}
/// <summary>
/// Can be overridden to perform updates for the device after the connected grid has resolved its power calculations, i.e. storing voltage for later updates
/// </summary>
public virtual void GridResolved(Connection conn) { }
public static bool ValidPowerConnection(Connection conn1, Connection conn2)
{
return
conn1.IsPower && conn2.IsPower &&
conn1.Item.Condition > 0.0f && conn2.Item.Condition > 0.0f &&
conn1.Item.GetComponent<PowerTransfer>() is not { CanTransfer: false } &&
conn2.Item.GetComponent<PowerTransfer>() is not { CanTransfer: false } &&
(conn1.Item.HasTag(Tags.JunctionBox) || conn2.Item.HasTag(Tags.JunctionBox) || conn1.Item.HasTag(Tags.DockingPort) || conn2.Item.HasTag(Tags.DockingPort) || conn1.IsOutput != conn2.IsOutput);
}
/// <summary>
/// Returns the amount of power that can be supplied by batteries directly connected to the item
/// </summary>
protected float GetAvailableInstantaneousBatteryPower()
{
if (item.Connections == null || powerIn == null) { return 0.0f; }
float availablePower = 0.0f;
var recipients = powerIn.Recipients;
foreach (Connection recipient in recipients)
{
if (!recipient.IsPower || !recipient.IsOutput) { continue; }
var battery = recipient.Item?.GetComponent<PowerContainer>();
if (battery == null || battery.Item.Condition <= 0.0f) { continue; }
if (battery.OutputDisabled) { continue; }
float maxOutputPerFrame = battery.MaxOutPut / 60.0f;
float framesPerMinute = 3600.0f;
availablePower += Math.Min(battery.Charge * framesPerMinute, maxOutputPerFrame);
}
return availablePower;
}
protected IEnumerable<PowerContainer> GetDirectlyConnectedBatteries()
{
if (item.Connections != null && powerIn != null)
{
// Use ToList() snapshot for thread-safe iteration
foreach (Connection recipient in powerIn.Recipients.ToList())
{
if (!recipient.IsPower || !recipient.IsOutput) { continue; }
if (recipient.Item?.GetComponent<PowerContainer>() is PowerContainer battery)
{
yield return battery;
}
}
}
}
protected override void RemoveComponentSpecific()
{
//Flag power connections to be updated
if (item.Connections != null)
{
foreach (Connection c in item.Connections)
{
if (c.IsPower && c.Grid != null)
{
MarkConnectionChanged(c);
}
}
}
base.RemoveComponentSpecific();
_poweredDict.TryRemove(this, out _);
InvalidatePoweredListCache();
}
}
partial class GridInfo
{
public readonly int ID;
public float Voltage = 0;
public float Load = 0;
public float Power = 0;
public readonly List<Connection> Connections = new List<Connection>();
public readonly SortedList<PowerPriority, PowerSourceGroup> PowerSourceGroups = new SortedList<PowerPriority, PowerSourceGroup>();
public GridInfo(int id)
{
ID = id;
}
public void RemoveConnection(Connection c)
{
Connections.Remove(c);
//Remove the grid if it has no devices
if (Connections.Count == 0)
{
Powered.Grids.TryRemove(ID, out _);
}
}
public void AddConnection(Connection c)
{
Connections.Add(c);
}
public void AddSrc(Connection c)
{
if (PowerSourceGroups.ContainsKey(c.Priority))
{
PowerSourceGroups[c.Priority].Connections.Add(c);
}
else
{
PowerSourceGroup group = new PowerSourceGroup();
group.Connections.Add(c);
PowerSourceGroups[c.Priority] = group;
}
}
}
partial class PowerSourceGroup
{
public PowerRange MinMaxPower;
public readonly List<Connection> Connections = new List<Connection>();
public PowerSourceGroup()
{
}
}
}