Files
LuaCsForBarotraumaEP/Barotrauma/BarotraumaShared/SharedSource/Items/Components/Power/Powered.cs
2026-04-30 21:59:54 +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()
{
}
}
}