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

436 lines
15 KiB
C#

using System;
using Barotrauma.Networking;
using Microsoft.Xna.Framework;
using System.Collections.Generic;
using System.Linq;
using System.Xml.Linq;
namespace Barotrauma.Items.Components
{
partial class ConnectionPanel : ItemComponent, IServerSerializable, IClientSerializable
{
const int MaxConnectionCount = 256;
public readonly List<Connection> Connections = new List<Connection>();
private Character user;
/// <summary>
/// Wires that have been disconnected from the panel, but not removed completely (visible at the bottom of the connection panel).
/// </summary>
public readonly HashSet<Wire> DisconnectedWires = new HashSet<Wire>();
private List<ushort> disconnectedWireIds;
/// <summary>
/// Allows rewiring the connection panel despite rewiring being disabled on a server
/// </summary>
public bool AlwaysAllowRewiring
{
get
{
if (item.Submarine == null) { return true; }
switch (item.Submarine.Info.Type)
{
case SubmarineType.Wreck:
case SubmarineType.BeaconStation:
case SubmarineType.EnemySubmarine:
case SubmarineType.Ruin:
return true;
}
return false;
}
}
[Editable, Serialize(false, IsPropertySaveable.Yes, description: "Locked connection panels cannot be rewired in-game.", alwaysUseInstanceValues: true)]
public bool Locked
{
get;
set;
}
public bool TemporarilyLocked
{
get { return Level.IsLoadedOutpost && (item.GetComponent<DockingPort>()?.Docked ?? false); }
}
//connection panels can't be deactivated externally (by signals or status effects)
public override bool IsActive
{
get { return base.IsActive; }
set { /*do nothing*/ }
}
public Character User
{
get { return user; }
}
public ConnectionPanel(Item item, ContentXElement element)
: base(item, element)
{
foreach (var subElement in element.Elements())
{
if (Connections.Count == MaxConnectionCount)
{
DebugConsole.ThrowError($"Too many connections in the item {item.Prefab.Identifier} (> {MaxConnectionCount}).");
break;
}
switch (subElement.Name.ToString())
{
case "input":
Connections.Add(new Connection(subElement, connectionIndex: Connections.Count, this, IdRemap.DiscardId, isItemSwap: false));
break;
case "output":
Connections.Add(new Connection(subElement, connectionIndex: Connections.Count, this, IdRemap.DiscardId, isItemSwap: false));
break;
}
}
base.IsActive = true;
InitProjSpecific();
}
partial void InitProjSpecific();
private bool linksInitialized;
public override void OnMapLoaded()
{
if (linksInitialized) { return; }
InitializeLinks();
}
public void InitializeLinks()
{
foreach (Connection c in Connections)
{
c.InitializeFromLoaded();
}
if (disconnectedWireIds != null)
{
foreach (ushort disconnectedWireId in disconnectedWireIds)
{
if (!(Entity.FindEntityByID(disconnectedWireId) is Item wireItem)) { continue; }
Wire wire = wireItem.GetComponent<Wire>();
if (wire != null)
{
if (Item.ItemList.Any(it => it != item && (it.GetComponent<ConnectionPanel>()?.DisconnectedWires.Contains(wire) ?? false)))
{
if (wire.Item.body != null) { wire.Item.body.Enabled = false; }
wire.IsActive = false;
wire.UpdateSections();
}
DisconnectedWires.Add(wire);
base.IsActive = true;
}
}
}
linksInitialized = true;
}
public override void OnItemLoaded()
{
if (item.body != null && item.body.BodyType == FarseerPhysics.BodyType.Dynamic)
{
var holdable = item.GetComponent<Holdable>();
if (holdable == null || !holdable.Attachable)
{
DebugConsole.ThrowError("Item \"" + item.Name + "\" has a ConnectionPanel component," +
" but cannot be wired because it has an active physics body that cannot be attached to a wall." +
" Remove the physics body or add a Holdable component with the Attachable attribute set to true.");
}
}
}
public void MoveConnectedWires(Vector2 amount)
{
Vector2 wireNodeOffset = item.Submarine == null ? Vector2.Zero : item.Submarine.HiddenSubPosition + amount;
foreach (Connection c in Connections)
{
// Use ToArray() snapshot for thread-safe iteration
foreach (Wire wire in c.Wires.ToArray())
{
if (wire == null) { continue; }
TryMoveWire(wire);
}
}
// Use ToList() snapshot for thread-safe iteration
foreach (var wire in DisconnectedWires.ToList())
{
TryMoveWire(wire);
}
void TryMoveWire(Wire wire)
{
#if CLIENT
if (wire.Item.IsSelected) { return; }
#endif
var wireNodes = wire.GetNodes();
if (wireNodes.Count == 0) { return; }
if (Submarine.RectContains(item.Rect, wireNodes[0] + wireNodeOffset))
{
wire.MoveNode(0, amount);
}
else if (Submarine.RectContains(item.Rect, wireNodes[wireNodes.Count - 1] + wireNodeOffset))
{
wire.MoveNode(wireNodes.Count - 1, amount);
}
}
}
public override void Update(float deltaTime, Camera cam)
{
UpdateProjSpecific(deltaTime);
if (user == null || (user.SelectedItem != item && user.SelectedSecondaryItem != item))
{
#if SERVER
if (user != null) { item.CreateServerEvent(this); }
#endif
user = null;
if (DisconnectedWires.Count == 0) { base.IsActive = false; }
return;
}
if (!user.Enabled || !HasRequiredItems(user, addMessage: false))
{
user = null;
base.IsActive = false;
return;
}
user.AnimController.UpdateUseItem(!user.IsClimbing, item.WorldPosition + new Vector2(0.0f, 100.0f) * (((float)Timing.TotalTime / 10.0f) % 0.1f));
}
public override void UpdateBroken(float deltaTime, Camera cam)
{
Update(deltaTime, cam);
}
partial void UpdateProjSpecific(float deltaTime);
public bool CanRewire()
{
if (item.Container?.GetComponent<CircuitBox>() != null)
{
return true;
}
//attaching wires to items with a body is not allowed
//(signal items remove their bodies when attached to a wall)
if (item.body != null && item.body.BodyType == FarseerPhysics.BodyType.Dynamic)
{
return false;
}
return true;
}
public override bool Select(Character picker)
{
if (!CanRewire())
{
return false;
}
user = picker;
#if SERVER
if (user != null) { item.CreateServerEvent(this); }
#endif
base.IsActive = true;
return true;
}
public override bool Use(float deltaTime, Character character = null)
{
if (character == null || character != user) { return false; }
return true;
}
/// <summary>
/// Check if the character manages to succesfully rewire the panel, and if not, apply OnFailure effects
/// </summary>
public bool CheckCharacterSuccess(Character character)
{
if (character == null) { return false; }
//no electrocution in sub editor
if (Screen.Selected == GameMain.SubEditorScreen) { return true; }
var reactor = item.GetComponent<Reactor>();
if (reactor != null)
{
//reactors that arent generating power atm can be rewired without the risk of electrical shock
if (MathUtils.NearlyEqual(reactor.CurrPowerConsumption, 0.0f)) { return true; }
}
var powerContainer = item.GetComponent<PowerContainer>();
if (powerContainer != null)
{
//empty batteries/supercapacitors can be rewired without the risk of electrical shock
//non-empty ones always have a chance of zapping the user
if (powerContainer.Charge <= 0.0f) { return true; }
}
var powered = item.GetComponent<Powered>();
if (powered != null && powerContainer == null)
{
//unpowered panels can be rewired without the risk of electrical shock
if (powered.Voltage < 0.1f) { return true; }
}
float degreeOfSuccess = DegreeOfSuccess(character);
if (Rand.Range(0.0f, 0.5f) < degreeOfSuccess) { return true; }
ApplyStatusEffects(ActionType.OnFailure, 1.0f, character);
return false;
}
public override void Load(ContentXElement element, bool usePrefabValues, IdRemap idRemap, bool isItemSwap)
{
base.Load(element, usePrefabValues, idRemap, isItemSwap);
List<Connection> loadedConnections = new List<Connection>();
foreach (var subElement in element.Elements())
{
switch (subElement.Name.ToString())
{
case "input":
loadedConnections.Add(new Connection(subElement, connectionIndex: loadedConnections.Count, this, idRemap, isItemSwap));
break;
case "output":
loadedConnections.Add(new Connection(subElement, connectionIndex: loadedConnections.Count, this, idRemap, isItemSwap));
break;
}
}
for (int i = 0; i < loadedConnections.Count && i < Connections.Count; i++)
{
Connections[i].LoadedWires.Clear();
Connections[i].LoadedWires.AddRange(loadedConnections[i].LoadedWires);
}
disconnectedWireIds = element.GetAttributeUshortArray("disconnectedwires", Array.Empty<ushort>()).ToList();
for (int i = 0; i < disconnectedWireIds.Count; i++)
{
disconnectedWireIds[i] = idRemap.GetOffsetId(disconnectedWireIds[i]);
}
}
public override XElement Save(XElement parentElement)
{
XElement componentElement = base.Save(parentElement);
foreach (Connection c in Connections)
{
c.Save(componentElement);
}
if (DisconnectedWires.Count > 0)
{
componentElement.Add(new XAttribute("disconnectedwires", string.Join(",", DisconnectedWires.Select(w => w.Item.ID))));
}
return componentElement;
}
protected override void ShallowRemoveComponentSpecific()
{
//do nothing
}
protected override void RemoveComponentSpecific()
{
base.RemoveComponentSpecific();
foreach (Wire wire in DisconnectedWires.ToList())
{
if (wire.OtherConnection(null) == null) //wire not connected to anything else
{
#if CLIENT
if (SubEditorScreen.IsSubEditor())
{
wire.Item.Remove();
}
else
{
wire.Item.Drop(null);
}
#else
wire.Item.Drop(null);
#endif
}
}
DisconnectedWires.Clear();
foreach (Connection c in Connections)
{
foreach (Wire wire in c.Wires.ToArray())
{
if (wire.OtherConnection(c) == null) //wire not connected to anything else
{
#if CLIENT
if (SubEditorScreen.IsSubEditor())
{
wire.Item.Remove();
}
else
{
wire.Item.Drop(null);
}
#else
wire.Item.Drop(null);
#endif
}
else
{
wire.RemoveConnection(item);
}
}
c.Grid = null;
}
foreach (var connection in Connections)
{
Powered.UnmarkConnectionChanged(connection);
connection.Recipients.Clear();
}
Connections.Clear();
#if CLIENT
rewireSoundChannel?.FadeOutAndDispose();
rewireSoundChannel = null;
#endif
}
public override void ReceiveSignal(Signal signal, Connection connection)
{
//do nothing
}
public void ClientEventWrite(IWriteMessage msg, NetEntityEvent.IData extraData = null)
{
#if CLIENT
TriggerRewiringSound();
#endif
msg.WriteByte((byte)Connections.Count);
foreach (Connection connection in Connections)
{
// Use ToArray() snapshot for thread-safe iteration
var wiresSnapshot = connection.Wires.ToArray();
msg.WriteVariableUInt32((uint)wiresSnapshot.Length);
foreach (Wire wire in wiresSnapshot)
{
msg.WriteUInt16(wire?.Item == null ? (ushort)0 : wire.Item.ID);
}
}
// Use ToList() snapshot for thread-safe iteration
var disconnectedSnapshot = DisconnectedWires.ToList();
msg.WriteUInt16((ushort)disconnectedSnapshot.Count);
foreach (Wire disconnectedWire in disconnectedSnapshot)
{
msg.WriteUInt16(disconnectedWire.Item.ID);
}
}
}
}