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.
887 lines
34 KiB
C#
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()
|
|
{
|
|
}
|
|
}
|
|
}
|