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.
436 lines
15 KiB
C#
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);
|
|
}
|
|
}
|
|
}
|
|
}
|