Files
LuaCsForBarotraumaEP/Barotrauma/BarotraumaShared/SharedSource/Items/Components/Signal/Connection.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

467 lines
18 KiB
C#

using System;
using System.Collections.Generic;
using System.Collections.Immutable;
using System.Linq;
using System.Xml.Linq;
namespace Barotrauma.Items.Components
{
partial class Connection
{
//how many wires can be linked to connectors by default
private const int DefaultMaxWires = 5;
//how many wires a player can link to this connection
public readonly int MaxPlayerConnectableWires = 5;
//how many wires can be linked to this connection in total
public readonly int MaxWires = 5;
public readonly int DisplayOrder;
public readonly string Name;
private readonly LocalizedString _displayName;
public LocalizedString DisplayName
{
get => DisplayNameOverride ?? _displayName;
private init => _displayName = value;
}
public LocalizedString DisplayNameOverride;
private readonly HashSet<Wire> wires;
public IReadOnlyCollection<Wire> Wires => wires;
/// <summary>
/// Circuit box input and output connections that are linked to this connection.
/// </summary>
/// <remarks>
/// We don't want to create a wire between the circuit boxes connection panel and the
/// connection panel of the item inside the circuit box so we use this to bridge the gap.
/// </remarks>
public List<CircuitBoxConnection> CircuitBoxConnections = new();
private bool enumeratingWires;
private readonly HashSet<Wire> removedWires = new HashSet<Wire>();
private readonly Item item;
public readonly bool IsOutput;
public readonly List<StatusEffect> Effects;
public readonly List<(ushort wireId, int? connectionIndex)> LoadedWires;
//The grid the connection is a part of
public GridInfo Grid;
//Priority in which power output will be handled - load is unaffected
public PowerPriority Priority = PowerPriority.Default;
public Signal LastSentSignal { get; private set; }
public Signal LastReceivedSignal {get; private set;}
public bool IsPower
{
get;
private set;
}
private bool recipientsDirty = true;
private readonly List<Connection> recipients = new List<Connection>();
public List<Connection> Recipients
{
get
{
if (recipientsDirty) { RefreshRecipients(); }
return recipients;
}
}
public Item Item
{
get { return item; }
}
public ConnectionPanel ConnectionPanel
{
get;
private set;
}
public override string ToString()
{
return "Connection (" + item.Name + ", " + Name + ")";
}
public Connection(ContentXElement element, int connectionIndex, ConnectionPanel connectionPanel, IdRemap idRemap, bool isItemSwap)
{
#if CLIENT
if (connector == null)
{
connector = GUIStyle.GetComponentStyle("ConnectionPanelConnector").GetDefaultSprite();
wireVertical = GUIStyle.GetComponentStyle("ConnectionPanelWire").GetDefaultSprite();
connectionSprite = GUIStyle.GetComponentStyle("ConnectionPanelConnection").GetDefaultSprite();
connectionSpriteHighlight = GUIStyle.GetComponentStyle("ConnectionPanelConnection").GetSprite(GUIComponent.ComponentState.Hover);
screwSprites = GUIStyle.GetComponentStyle("ConnectionPanelScrew").Sprites[GUIComponent.ComponentState.None].Select(s => s.Sprite).ToList();
}
#endif
ConnectionPanel = connectionPanel;
item = connectionPanel.Item;
MaxWires = element.GetAttributeInt("maxwires", DefaultMaxWires);
MaxWires = Math.Max(element.Elements().Count(e => e.Name.ToString().Equals("link", StringComparison.OrdinalIgnoreCase)), MaxWires);
MaxPlayerConnectableWires = element.GetAttributeInt("maxplayerconnectablewires", MaxWires);
wires = new HashSet<Wire>();
IsOutput = element.Name.ToString() == "output";
Name = element.GetAttributeString("name", IsOutput ? "output" : "input");
int displayOrder;
if (element.GetAttribute("displayorderoverride") is not { } displayOrderAttr)
{
var sameElements = connectionPanel.Connections.Where(c => c.IsOutput == IsOutput);
displayOrder = !sameElements.Any() ? 0 : sameElements.Max(static c => c.DisplayOrder) + 1;
}
else
{
displayOrder = displayOrderAttr.GetAttributeInt(0);
}
DisplayOrder = displayOrder;
string displayNameTag = "", fallbackTag = "";
//if displayname is not present, attempt to find it from the prefab
if (element.GetAttribute("displayname") == null)
{
foreach (var subElement in item.Prefab.ConfigElement.Elements())
{
if (!subElement.Name.ToString().Equals("connectionpanel", StringComparison.OrdinalIgnoreCase)) { continue; }
int prefabConnectionIndex = 0;
foreach (XElement connectionElement in subElement.Elements())
{
string prefabConnectionName = connectionElement.GetAttributeString("name", null);
if (prefabConnectionName.IsNullOrEmpty()) { continue; }
string[] aliases = connectionElement.GetAttributeStringArray("aliases", Array.Empty<string>());
if (prefabConnectionName == Name || aliases.Contains(Name) ||
//when swapping items, we move wires based on the order of the connections, not the names
//= we should find a connection based on the index if the name doesn't match
(isItemSwap && connectionIndex == prefabConnectionIndex))
{
displayNameTag = connectionElement.GetAttributeString("displayname", "");
fallbackTag = connectionElement.GetAttributeString("fallbackdisplayname", "");
}
prefabConnectionIndex++;
}
}
}
else
{
displayNameTag = element.GetAttributeString("displayname", "");
fallbackTag = element.GetAttributeString("fallbackdisplayname", null);
}
if (!string.IsNullOrEmpty(displayNameTag))
{
//extract the tag parts in case the tags contains variables
string tagWithoutVariables = displayNameTag?.Split('~')?.FirstOrDefault();
string fallbackTagWithoutVariables = fallbackTag?.Split('~')?.FirstOrDefault();
//use displayNameTag if found, otherwise fallBack
if (TextManager.ContainsTag(tagWithoutVariables))
{
DisplayName = TextManager.GetServerMessage(displayNameTag);
}
else if (TextManager.ContainsTag(fallbackTagWithoutVariables))
{
DisplayName = TextManager.GetServerMessage(fallbackTag);
}
}
if (DisplayName.IsNullOrEmpty())
{
#if DEBUG
DebugConsole.ThrowError($"Could not find a display name for the connection {Name} in the item {item.Name} (submarine: {item.Submarine?.Info?.Name ?? "none"})");
#endif
DisplayName = Name;
}
IsPower = element.GetAttributeBool("ispower", Name is "power_in" or "power" or "power_out");
LoadedWires = new List<(ushort wireId, int? connectionIndex)>();
foreach (var subElement in element.Elements())
{
switch (subElement.Name.ToString().ToLowerInvariant())
{
case "link":
int id = subElement.GetAttributeInt("w", 0);
int? i = null;
if (subElement.GetAttribute("i") != null)
{
i = subElement.GetAttributeInt("i", 0);
}
if (id < 0) { id = 0; }
if (LoadedWires.Count < MaxWires) { LoadedWires.Add((idRemap.GetOffsetId(id), i)); }
break;
case "statuseffect":
Effects ??= new List<StatusEffect>();
Effects.Add(StatusEffect.Load(subElement, item.Name + ", connection " + Name));
break;
}
}
}
/// <summary>
/// Checks if the the connection is connected to a wire or a circuit box connection
/// </summary>
public bool IsConnectedToSomething()
=> wires.Count > 0 || CircuitBoxConnections.Count > 0;
public void SetRecipientsDirty()
{
recipientsDirty = true;
if (IsPower) { Powered.MarkConnectionChanged(this); }
}
private void RefreshRecipients()
{
recipients.Clear();
// Use ToArray() snapshot for thread-safe iteration
foreach (var wire in wires.ToArray())
{
Connection recipient = wire.OtherConnection(this);
if (recipient != null) { recipients.Add(recipient); }
}
recipientsDirty = false;
}
public Wire FindWireByItem(Item it)
=> Wires.FirstOrDefault(w => w.Item == it);
public bool WireSlotsAvailable()
=> wires.Count < MaxWires;
public bool TryAddLink(Wire wire)
{
if (wire is null
|| wires.Contains(wire)
|| !WireSlotsAvailable())
{
return false;
}
wires.Add(wire);
return true;
}
public void DisconnectWire(Wire wire)
{
if (wire == null || !wires.Contains(wire)) { return; }
var prevOtherConnection = wire.OtherConnection(this);
if (prevOtherConnection != null)
{
//Change the connection grids or flag them for updating
if (IsPower && prevOtherConnection.IsPower && Grid != null)
{
//Check if both connections belong to a larger grid
if (prevOtherConnection.recipients.Count > 1 && recipients.Count > 1)
{
Powered.MarkConnectionChanged(prevOtherConnection);
Powered.MarkConnectionChanged(this);
}
else if (recipients.Count > 1)
{
//This wire was the only one at the other grid
prevOtherConnection.Grid?.RemoveConnection(prevOtherConnection);
prevOtherConnection.Grid = null;
}
else if (prevOtherConnection.recipients.Count > 1)
{
Grid?.RemoveConnection(this);
Grid = null;
}
else if (Grid.Connections.Count == 2)
{
//Delete the grid as these were the only 2 devices
Powered.Grids.TryRemove(Grid.ID, out _);
Grid = null;
prevOtherConnection.Grid = null;
}
}
prevOtherConnection.recipientsDirty = true;
}
if (enumeratingWires)
{
removedWires.Add(wire);
}
else
{
wires.Remove(wire);
}
recipientsDirty = true;
}
public void ConnectWire(Wire wire)
{
if (wire == null || !TryAddLink(wire)) { return; }
ConnectionPanel.DisconnectedWires.Remove(wire);
var otherConnection = wire.OtherConnection(this);
if (otherConnection != null)
{
//Set the other connection grid if a grid exists already
if (Powered.ValidPowerConnection(this, otherConnection))
{
if (Grid == null && otherConnection.Grid != null)
{
otherConnection.Grid.AddConnection(this);
Grid = otherConnection.Grid;
}
else if (Grid != null && otherConnection.Grid == null)
{
Grid.AddConnection(otherConnection);
otherConnection.Grid = Grid;
}
else
{
//Flag change so that proper grids can be formed
Powered.MarkConnectionChanged(this);
Powered.MarkConnectionChanged(otherConnection);
}
}
otherConnection.recipientsDirty = true;
}
recipientsDirty = true;
}
public void SendSignal(Signal signal)
{
LastSentSignal = signal;
enumeratingWires = true;
// Use ToArray() snapshot for thread-safe iteration
foreach (var wire in wires.ToArray())
{
Connection recipient = wire.OtherConnection(this);
if (recipient == null) { continue; }
if (recipient.item == this.item || signal.source?.LastSentSignalRecipients.LastOrDefault() == recipient) { continue; }
signal.source?.LastSentSignalRecipients.Add(recipient);
#if CLIENT
wire.RegisterSignal(signal, source: this);
#endif
SendSignalIntoConnection(signal, recipient);
GameMain.LuaCs.Hook.Call("signalReceived", signal, recipient);
GameMain.LuaCs.Hook.Call("signalReceived." + recipient.item.Prefab.Identifier, signal, recipient);
}
foreach (CircuitBoxConnection connection in CircuitBoxConnections.ToArray())
{
connection.ReceiveSignal(signal);
GameMain.LuaCs.Hook.Call("signalReceived", signal, connection.Connection);
GameMain.LuaCs.Hook.Call("signalReceived." + connection.Connection.Item.Prefab.Identifier, signal, connection);
}
enumeratingWires = false;
foreach (var removedWire in removedWires.ToArray())
{
wires.Remove(removedWire);
}
removedWires.Clear();
}
public static void SendSignalIntoConnection(Signal signal, Connection conn)
{
conn.LastReceivedSignal = signal;
// Use ToArray() snapshot for thread-safe iteration
foreach (ItemComponent ic in conn.item.Components.ToArray())
{
ic.ReceiveSignal(signal, conn);
}
if (conn.Effects == null || signal.value == "0") { return; }
// Use ToArray() snapshot for thread-safe iteration
foreach (StatusEffect effect in conn.Effects.ToArray())
{
conn.Item.ApplyStatusEffect(effect, ActionType.OnUse, (float)Timing.Step);
}
}
public void ClearConnections()
{
if (IsPower && Grid != null)
{
Powered.MarkConnectionChanged(this);
// Use ToArray() snapshot for thread-safe iteration
foreach (Connection c in recipients.ToArray())
{
Powered.MarkConnectionChanged(c);
}
}
// Use ToArray() snapshot for thread-safe iteration
foreach (var wire in wires.ToArray())
{
wire.RemoveConnection(this);
recipientsDirty = true;
}
if (enumeratingWires)
{
foreach (var wire in wires.ToArray())
{
removedWires.Add(wire);
}
}
else
{
wires.Clear();
}
}
public void InitializeFromLoaded()
{
if (LoadedWires.Count == 0) { return; }
foreach ((ushort wireId, int? connectionIndex) in LoadedWires)
{
if (Entity.FindEntityByID(wireId) is not Item wireItem) { continue; }
var wire = wireItem.GetComponent<Wire>();
if (wire != null && TryAddLink(wire))
{
if (wire.Item.body != null) { wire.Item.body.Enabled = false; }
if (connectionIndex.HasValue)
{
wire.Connect(this, connectionIndex.Value, addNode: false, sendNetworkEvent: false);
}
else
{
wire.TryConnect(this, addNode: false, sendNetworkEvent: false);
}
wire.FixNodeEnds();
recipientsDirty = true;
}
}
LoadedWires.Clear();
}
public void Save(XElement parentElement)
{
XElement newElement = new XElement(IsOutput ? "output" : "input", new XAttribute("name", Name));
// Use ToArray() snapshot before OrderBy for thread-safe iteration
foreach (var wire in wires.ToArray().OrderBy(w => w.Item.ID))
{
newElement.Add(new XElement("link",
new XAttribute("w", wire.Item.ID.ToString()),
new XAttribute("i", wire.Connections[0] == this ? 0 : 1)));
}
parentElement.Add(newElement);
}
}
}