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.
This commit is contained in:
Eero
2025-12-28 04:59:56 +08:00
parent 90962b2328
commit 46595b1399
25 changed files with 519 additions and 245 deletions
@@ -1094,7 +1094,7 @@ namespace Barotrauma
#endif #endif
//Clear the grids to allow for garbage collection //Clear the grids to allow for garbage collection
Powered.Grids.Clear(); Powered.Grids.Clear();
Powered.ChangedConnections.Clear(); Powered.ClearChangedConnections();
try try
{ {
@@ -5,6 +5,7 @@ using FarseerPhysics.Dynamics;
using FarseerPhysics.Dynamics.Joints; using FarseerPhysics.Dynamics.Joints;
using Microsoft.Xna.Framework; using Microsoft.Xna.Framework;
using System; using System;
using System.Collections.Concurrent;
using System.Collections.Generic; using System.Collections.Generic;
using System.Linq; using System.Linq;
#if CLIENT #if CLIENT
@@ -24,11 +25,8 @@ namespace Barotrauma.Items.Components
Right Right
} }
private static readonly List<DockingPort> list = new List<DockingPort>(); private static readonly ConcurrentDictionary<DockingPort, byte> _dockingPortDict = new ConcurrentDictionary<DockingPort, byte>();
public static IEnumerable<DockingPort> List public static IEnumerable<DockingPort> List => _dockingPortDict.Keys;
{
get { return list; }
}
private Sprite overlaySprite; private Sprite overlaySprite;
private float dockingState; private float dockingState;
@@ -168,7 +166,7 @@ namespace Barotrauma.Items.Components
IsActive = true; IsActive = true;
list.Add(this); _dockingPortDict.TryAdd(this, 0);
} }
public override void FlipX(bool relativeToSub) public override void FlipX(bool relativeToSub)
@@ -200,7 +198,7 @@ namespace Barotrauma.Items.Components
{ {
float closestDist = float.MaxValue; float closestDist = float.MaxValue;
DockingPort closestPort = null; DockingPort closestPort = null;
foreach (DockingPort port in list) foreach (DockingPort port in List)
{ {
if (port == this || port.item.Submarine == item.Submarine || port.IsHorizontal != IsHorizontal) { continue; } if (port == this || port.item.Submarine == item.Submarine || port.IsHorizontal != IsHorizontal) { continue; }
float xDist = Math.Abs(port.item.WorldPosition.X - item.WorldPosition.X); float xDist = Math.Abs(port.item.WorldPosition.X - item.WorldPosition.X);
@@ -532,8 +530,8 @@ namespace Barotrauma.Items.Components
wire.TryConnect(recipient, addNode: false); wire.TryConnect(recipient, addNode: false);
//Flag connections to be updated //Flag connections to be updated
Powered.ChangedConnections.Add(powerConnection); Powered.MarkConnectionChanged(powerConnection);
Powered.ChangedConnections.Add(recipient); Powered.MarkConnectionChanged(recipient);
} }
private void CreateDoorBody() private void CreateDoorBody()
@@ -1007,7 +1005,7 @@ namespace Barotrauma.Items.Components
Connection powerConnection = Item.Connections.Find(c => c.IsPower); Connection powerConnection = Item.Connections.Find(c => c.IsPower);
if (powerConnection != null) if (powerConnection != null)
{ {
Powered.ChangedConnections.Add(powerConnection); Powered.MarkConnectionChanged(powerConnection);
} }
if (doorBody != null) if (doorBody != null)
@@ -1151,7 +1149,7 @@ namespace Barotrauma.Items.Components
protected override void RemoveComponentSpecific() protected override void RemoveComponentSpecific()
{ {
base.RemoveComponentSpecific(); base.RemoveComponentSpecific();
list.Remove(this); _dockingPortDict.TryRemove(this, out _);
hulls[0]?.Remove(); hulls[0] = null; hulls[0]?.Remove(); hulls[0] = null;
hulls[1]?.Remove(); hulls[1] = null; hulls[1]?.Remove(); hulls[1] = null;
gap?.Remove(); gap = null; gap?.Remove(); gap = null;
@@ -2,6 +2,7 @@
using FarseerPhysics; using FarseerPhysics;
using Microsoft.Xna.Framework; using Microsoft.Xna.Framework;
using System; using System;
using System.Collections.Concurrent;
using System.Collections.Generic; using System.Collections.Generic;
using System.Linq; using System.Linq;
using FarseerPhysics.Dynamics; using FarseerPhysics.Dynamics;
@@ -14,9 +15,9 @@ namespace Barotrauma.Items.Components
{ {
partial class Door : Pickable, IDrawableComponent, IServerSerializable partial class Door : Pickable, IDrawableComponent, IServerSerializable
{ {
private static readonly HashSet<Door> doorList = new HashSet<Door>(); private static readonly ConcurrentDictionary<Door, byte> _doorDict = new ConcurrentDictionary<Door, byte>();
public static IReadOnlyCollection<Door> DoorList { get { return doorList; } } public static ICollection<Door> DoorList => _doorDict.Keys;
private Gap linkedGap; private Gap linkedGap;
private bool isOpen; private bool isOpen;
@@ -277,7 +278,7 @@ namespace Barotrauma.Items.Components
} }
IsActive = true; IsActive = true;
doorList.Add(this); _doorDict.TryAdd(this, 0);
} }
public override void OnItemLoaded() public override void OnItemLoaded()
@@ -669,7 +670,7 @@ namespace Barotrauma.Items.Components
convexHull2?.Remove(); convexHull2?.Remove();
#endif #endif
doorList.Remove(this); _doorDict.TryRemove(this, out _);
} }
private bool CheckSubmarinesInDoorWay() private bool CheckSubmarinesInDoorWay()
@@ -3,6 +3,7 @@ using Barotrauma.Networking;
using FarseerPhysics; using FarseerPhysics;
using Microsoft.Xna.Framework; using Microsoft.Xna.Framework;
using System; using System;
using System.Collections.Concurrent;
using System.Collections.Generic; using System.Collections.Generic;
using System.Linq; using System.Linq;
@@ -10,11 +11,8 @@ namespace Barotrauma.Items.Components
{ {
partial class ElectricalDischarger : Powered, IServerSerializable partial class ElectricalDischarger : Powered, IServerSerializable
{ {
private static readonly List<ElectricalDischarger> list = new List<ElectricalDischarger>(); private static readonly ConcurrentDictionary<ElectricalDischarger, byte> _dischargerDict = new ConcurrentDictionary<ElectricalDischarger, byte>();
public static IEnumerable<ElectricalDischarger> List public static IEnumerable<ElectricalDischarger> List => _dischargerDict.Keys;
{
get { return list; }
}
const int MaxNodes = 100; const int MaxNodes = 100;
const float MaxNodeDistance = 150.0f; const float MaxNodeDistance = 150.0f;
@@ -115,7 +113,7 @@ namespace Barotrauma.Items.Components
public ElectricalDischarger(Item item, ContentXElement element) : public ElectricalDischarger(Item item, ContentXElement element) :
base(item, element) base(item, element)
{ {
list.Add(this); _dischargerDict.TryAdd(this, 0);
foreach (var subElement in element.Elements()) foreach (var subElement in element.Elements())
{ {
@@ -604,7 +602,7 @@ namespace Barotrauma.Items.Components
protected override void RemoveComponentSpecific() protected override void RemoveComponentSpecific()
{ {
base.RemoveComponentSpecific(); base.RemoveComponentSpecific();
list.Remove(this); _dischargerDict.TryRemove(this, out _);
} }
public void ServerEventWrite(IWriteMessage msg, Client c, NetEntityEvent.IData extraData = null) public void ServerEventWrite(IWriteMessage msg, Client c, NetEntityEvent.IData extraData = null)
@@ -9,6 +9,7 @@ using System;
using System.Collections.Generic; using System.Collections.Generic;
using System.Collections.Immutable; using System.Collections.Immutable;
using System.Linq; using System.Linq;
using System.Threading;
using System.Xml.Linq; using System.Xml.Linq;
namespace Barotrauma.Items.Components namespace Barotrauma.Items.Components
@@ -616,12 +617,13 @@ namespace Barotrauma.Items.Components
return CanBeAttached(user, out _); return CanBeAttached(user, out _);
} }
private static List<Item> tempOverlappingItems = new List<Item>(); private static readonly ThreadLocal<List<Item>> tempOverlappingItems = new ThreadLocal<List<Item>>(() => new List<Item>());
private bool CanBeAttached(Character user, out IEnumerable<Item> overlappingItems) private bool CanBeAttached(Character user, out IEnumerable<Item> overlappingItems)
{ {
tempOverlappingItems.Clear(); var overlapping = tempOverlappingItems.Value;
overlappingItems = tempOverlappingItems; overlapping.Clear();
overlappingItems = overlapping;
if (!attachable || !Reattachable) { return false; } if (!attachable || !Reattachable) { return false; }
//can be attached anywhere in sub editor //can be attached anywhere in sub editor
@@ -664,9 +666,9 @@ namespace Barotrauma.Items.Components
} }
if (attachPos.X + size.X < worldRect.X || attachPos.X - size.X > worldRect.Right) { continue; } if (attachPos.X + size.X < worldRect.X || attachPos.X - size.X > worldRect.Right) { continue; }
if (attachPos.Y - size.Y > worldRect.Y || attachPos.Y + size.Y < worldRect.Y - worldRect.Height) { continue; } if (attachPos.Y - size.Y > worldRect.Y || attachPos.Y + size.Y < worldRect.Y - worldRect.Height) { continue; }
tempOverlappingItems.Add(otherItem); overlapping.Add(otherItem);
} }
if (tempOverlappingItems.Any()) { return false; } if (overlapping.Any()) { return false; }
} }
//can be attached anywhere inside hulls //can be attached anywhere inside hulls
@@ -4,6 +4,7 @@ using Microsoft.Xna.Framework;
using System; using System;
using System.Collections.Generic; using System.Collections.Generic;
using System.Linq; using System.Linq;
using System.Threading;
using Barotrauma.Extensions; using Barotrauma.Extensions;
using Barotrauma.MapCreatures.Behavior; using Barotrauma.MapCreatures.Behavior;
@@ -315,7 +316,7 @@ namespace Barotrauma.Items.Components
partial void UseProjSpecific(float deltaTime, Vector2 raystart); partial void UseProjSpecific(float deltaTime, Vector2 raystart);
private static readonly List<Body> hitBodies = new List<Body>(); private static readonly ThreadLocal<List<Body>> hitBodies = new ThreadLocal<List<Body>>(() => new List<Body>());
private readonly HashSet<Character> hitCharacters = new HashSet<Character>(); private readonly HashSet<Character> hitCharacters = new HashSet<Character>();
private readonly List<FireSource> fireSourcesInRange = new List<FireSource>(); private readonly List<FireSource> fireSourcesInRange = new List<FireSource>();
private void Repair(Vector2 rayStart, Vector2 rayEnd, float deltaTime, Character user, float degreeOfSuccess, List<Body> ignoredBodies) private void Repair(Vector2 rayStart, Vector2 rayEnd, float deltaTime, Character user, float degreeOfSuccess, List<Body> ignoredBodies)
@@ -373,13 +374,13 @@ namespace Barotrauma.Items.Components
}, },
allowInsideFixture: true); allowInsideFixture: true);
hitBodies.Clear(); hitBodies.Value.Clear();
hitBodies.AddRange(bodies.Distinct()); hitBodies.Value.AddRange(bodies.Distinct());
lastPickedFraction = Submarine.LastPickedFraction; lastPickedFraction = Submarine.LastPickedFraction;
Type lastHitType = null; Type lastHitType = null;
hitCharacters.Clear(); hitCharacters.Clear();
foreach (Body body in hitBodies) foreach (Body body in hitBodies.Value)
{ {
Type bodyType = body.UserData?.GetType(); Type bodyType = body.UserData?.GetType();
if (!RepairThroughWalls && bodyType != null && bodyType != lastHitType) if (!RepairThroughWalls && bodyType != null && bodyType != lastHitType)
@@ -897,48 +898,49 @@ namespace Barotrauma.Items.Components
} }
} }
private static List<ISerializableEntity> currentTargets = new List<ISerializableEntity>(); private static readonly ThreadLocal<List<ISerializableEntity>> currentTargets = new ThreadLocal<List<ISerializableEntity>>(() => new List<ISerializableEntity>());
private void ApplyStatusEffectsOnTarget(Character user, float deltaTime, ActionType actionType, Item targetItem = null, Character character = null, Limb limb = null, Structure structure = null) private void ApplyStatusEffectsOnTarget(Character user, float deltaTime, ActionType actionType, Item targetItem = null, Character character = null, Limb limb = null, Structure structure = null)
{ {
if (statusEffectLists == null) { return; } if (statusEffectLists == null) { return; }
if (!statusEffectLists.TryGetValue(actionType, out List<StatusEffect> statusEffects)) { return; } if (!statusEffectLists.TryGetValue(actionType, out List<StatusEffect> statusEffects)) { return; }
var targets = currentTargets.Value;
foreach (StatusEffect effect in statusEffects) foreach (StatusEffect effect in statusEffects)
{ {
currentTargets.Clear(); targets.Clear();
effect.SetUser(user); effect.SetUser(user);
if (effect.HasTargetType(StatusEffect.TargetType.UseTarget)) if (effect.HasTargetType(StatusEffect.TargetType.UseTarget))
{ {
if (targetItem != null) if (targetItem != null)
{ {
currentTargets.AddRange(targetItem.AllPropertyObjects); targets.AddRange(targetItem.AllPropertyObjects);
} }
if (structure != null) if (structure != null)
{ {
currentTargets.Add(structure); targets.Add(structure);
} }
if (character != null) if (character != null)
{ {
currentTargets.Add(character); targets.Add(character);
} }
effect.Apply(actionType, deltaTime, item, currentTargets); effect.Apply(actionType, deltaTime, item, targets);
} }
else if (effect.HasTargetType(StatusEffect.TargetType.Character)) else if (effect.HasTargetType(StatusEffect.TargetType.Character))
{ {
currentTargets.Add(user); targets.Add(user);
effect.Apply(actionType, deltaTime, item, currentTargets); effect.Apply(actionType, deltaTime, item, targets);
} }
else if (effect.HasTargetType(StatusEffect.TargetType.Limb)) else if (effect.HasTargetType(StatusEffect.TargetType.Limb))
{ {
currentTargets.Add(limb); targets.Add(limb);
effect.Apply(actionType, deltaTime, item, currentTargets); effect.Apply(actionType, deltaTime, item, targets);
} }
#if CLIENT #if CLIENT
if (user == null) { return; } if (user == null) { return; }
// Hard-coded progress bars for welding doors stuck. // Hard-coded progress bars for welding doors stuck.
// A general purpose system could be better, but it would most likely require changes in the way we define the status effects in xml. // A general purpose system could be better, but it would most likely require changes in the way we define the status effects in xml.
foreach (ISerializableEntity target in currentTargets) foreach (ISerializableEntity target in targets)
{ {
if (target is not Door door) { continue; } if (target is not Door door) { continue; }
if (!door.CanBeWelded || !door.Item.IsInteractable(user)) { continue; } if (!door.CanBeWelded || !door.Item.IsInteractable(user)) { continue; }
@@ -949,7 +949,8 @@ namespace Barotrauma.Items.Components
//if any of the effects reduce the item's condition, set the user for OnBroken effects as well //if any of the effects reduce the item's condition, set the user for OnBroken effects as well
if (reducesCondition && user != null && type != ActionType.OnBroken) if (reducesCondition && user != null && type != ActionType.OnBroken)
{ {
foreach (ItemComponent ic in item.Components) // Use ToArray() snapshot for thread-safe iteration
foreach (ItemComponent ic in item.Components.ToArray())
{ {
if (ic.statusEffectLists == null || !ic.statusEffectLists.TryGetValue(ActionType.OnBroken, out List<StatusEffect> brokenEffects)) { continue; } if (ic.statusEffectLists == null || !ic.statusEffectLists.TryGetValue(ActionType.OnBroken, out List<StatusEffect> brokenEffects)) { continue; }
foreach (var brokenEffect in brokenEffects) foreach (var brokenEffect in brokenEffects)
@@ -884,7 +884,8 @@ namespace Barotrauma.Items.Components
RelatedItem containableItem = FindContainableItem(containedItem); RelatedItem containableItem = FindContainableItem(containedItem);
if (containableItem != null && containableItem.SetActive) if (containableItem != null && containableItem.SetActive)
{ {
foreach (var ic in containedItem.Components) // Use ToArray() snapshot for thread-safe iteration
foreach (var ic in containedItem.Components.ToArray())
{ {
ic.IsActive = active; ic.IsActive = active;
} }
@@ -1,17 +1,19 @@
using System.Collections.Generic; using System.Collections.Concurrent;
using System.Collections.Generic;
using System.Xml.Linq; using System.Xml.Linq;
namespace Barotrauma.Items.Components namespace Barotrauma.Items.Components
{ {
partial class Ladder : ItemComponent partial class Ladder : ItemComponent
{ {
public static List<Ladder> List { get; } = new List<Ladder>(); private static readonly ConcurrentDictionary<Ladder, byte> _ladderDict = new ConcurrentDictionary<Ladder, byte>();
public static IEnumerable<Ladder> List => _ladderDict.Keys;
public Ladder(Item item, ContentXElement element) public Ladder(Item item, ContentXElement element)
: base(item, element) : base(item, element)
{ {
InitProjSpecific(element); InitProjSpecific(element);
List.Add(this); _ladderDict.TryAdd(this, 0);
} }
partial void InitProjSpecific(ContentXElement element); partial void InitProjSpecific(ContentXElement element);
@@ -28,7 +30,7 @@ namespace Barotrauma.Items.Components
{ {
base.RemoveComponentSpecific(); base.RemoveComponentSpecific();
RemoveProjSpecific(); RemoveProjSpecific();
List.Remove(this); _ladderDict.TryRemove(this, out _);
} }
partial void RemoveProjSpecific(); partial void RemoveProjSpecific();
@@ -5,6 +5,7 @@ using System;
using System.Collections.Generic; using System.Collections.Generic;
using System.Globalization; using System.Globalization;
using System.Xml.Linq; using System.Xml.Linq;
using System.Linq;
namespace Barotrauma.Items.Components namespace Barotrauma.Items.Components
{ {
@@ -497,12 +498,14 @@ namespace Barotrauma.Items.Components
item.SendSignal(new Signal(MathHelper.ToDegrees(targetRotation).ToString("G", CultureInfo.InvariantCulture), sender: user), positionOut); item.SendSignal(new Signal(MathHelper.ToDegrees(targetRotation).ToString("G", CultureInfo.InvariantCulture), sender: user), positionOut);
for (int i = item.LastSentSignalRecipients.Count - 1; i >= 0; i--) // Use ToList() snapshot for thread-safe iteration
var signalRecipients = item.LastSentSignalRecipients.ToList();
for (int i = signalRecipients.Count - 1; i >= 0; i--)
{ {
if (item.LastSentSignalRecipients[i].Item.Condition <= 0.0f || item.LastSentSignalRecipients[i].IsPower) { continue; } if (signalRecipients[i].Item.Condition <= 0.0f || signalRecipients[i].IsPower) { continue; }
if (item.LastSentSignalRecipients[i].Item.Prefab.FocusOnSelected) if (signalRecipients[i].Item.Prefab.FocusOnSelected)
{ {
return item.LastSentSignalRecipients[i].Item; return signalRecipients[i].Item;
} }
} }
@@ -1,14 +1,17 @@
using Barotrauma.Networking; using Barotrauma.Networking;
using Microsoft.Xna.Framework; using Microsoft.Xna.Framework;
using System; using System;
using System.Collections.Concurrent;
using System.Collections.Generic; using System.Collections.Generic;
using System.Linq; using System.Linq;
using System.Threading;
namespace Barotrauma.Items.Components namespace Barotrauma.Items.Components
{ {
partial class Sonar : Powered, IServerSerializable, IClientSerializable partial class Sonar : Powered, IServerSerializable, IClientSerializable
{ {
public static List<Sonar> SonarList = new List<Sonar>(); private static readonly ConcurrentDictionary<Sonar, byte> _sonarDict = new ConcurrentDictionary<Sonar, byte>();
public static IEnumerable<Sonar> SonarList => _sonarDict.Keys;
public enum Mode public enum Mode
{ {
@@ -169,7 +172,7 @@ namespace Barotrauma.Items.Components
IsActive = true; IsActive = true;
InitProjSpecific(element); InitProjSpecific(element);
CurrentMode = Mode.Passive; CurrentMode = Mode.Passive;
SonarList.Add(this); _sonarDict.TryAdd(this, 0);
} }
partial void InitProjSpecific(ContentXElement element); partial void InitProjSpecific(ContentXElement element);
@@ -291,13 +294,15 @@ namespace Barotrauma.Items.Components
return currentPingIndex != -1 && (character == null || characterUsable); return currentPingIndex != -1 && (character == null || characterUsable);
} }
private static readonly Dictionary<string, List<Character>> targetGroups = new Dictionary<string, List<Character>>(); private static readonly ThreadLocal<Dictionary<string, List<Character>>> targetGroups =
new ThreadLocal<Dictionary<string, List<Character>>>(() => new Dictionary<string, List<Character>>());
public override bool CrewAIOperate(float deltaTime, Character character, AIObjectiveOperateItem objective) public override bool CrewAIOperate(float deltaTime, Character character, AIObjectiveOperateItem objective)
{ {
if (currentMode == Mode.Passive || !aiPingCheckPending) { return false; } if (currentMode == Mode.Passive || !aiPingCheckPending) { return false; }
foreach (List<Character> targetGroup in targetGroups.Values) var groups = targetGroups.Value;
foreach (List<Character> targetGroup in groups.Values)
{ {
targetGroup.Clear(); targetGroup.Clear();
} }
@@ -310,14 +315,14 @@ namespace Barotrauma.Items.Components
#warning This is not the best key for a dictionary. #warning This is not the best key for a dictionary.
string directionName = GetDirectionName(c.WorldPosition - item.WorldPosition).Value; string directionName = GetDirectionName(c.WorldPosition - item.WorldPosition).Value;
if (!targetGroups.ContainsKey(directionName)) if (!groups.ContainsKey(directionName))
{ {
targetGroups.Add(directionName, new List<Character>()); groups.Add(directionName, new List<Character>());
} }
targetGroups[directionName].Add(c); groups[directionName].Add(c);
} }
foreach (KeyValuePair<string, List<Character>> targetGroup in targetGroups) foreach (KeyValuePair<string, List<Character>> targetGroup in groups)
{ {
if (!targetGroup.Value.Any()) { continue; } if (!targetGroup.Value.Any()) { continue; }
string dialogTag = "DialogSonarTarget"; string dialogTag = "DialogSonarTarget";
@@ -401,7 +406,7 @@ namespace Barotrauma.Items.Components
MineralClusters = null; MineralClusters = null;
#endif #endif
SonarList.Remove(this); _sonarDict.TryRemove(this, out _);
} }
@@ -1,6 +1,7 @@
using Barotrauma.Extensions; using Barotrauma.Extensions;
using Microsoft.Xna.Framework; using Microsoft.Xna.Framework;
using System; using System;
using System.Collections.Concurrent;
using System.Collections.Generic; using System.Collections.Generic;
using System.Linq; using System.Linq;
using System.Xml.Linq; using System.Xml.Linq;
@@ -13,10 +14,12 @@ namespace Barotrauma.Items.Components
private readonly HashSet<Connection> signalConnections = new HashSet<Connection>(); private readonly HashSet<Connection> signalConnections = new HashSet<Connection>();
private readonly Dictionary<Connection, bool> connectionDirty = new Dictionary<Connection, bool>(); private readonly ConcurrentDictionary<Connection, bool> connectionDirty = new ConcurrentDictionary<Connection, bool>();
//a list of connections a given connection is connected to, either directly or via other power transfer components //a list of connections a given connection is connected to, either directly or via other power transfer components
private readonly Dictionary<Connection, HashSet<Connection>> connectedRecipients = new Dictionary<Connection, HashSet<Connection>>(); //Uses ConcurrentDictionary<Connection, byte> as a thread-safe HashSet replacement
private readonly ConcurrentDictionary<Connection, ConcurrentDictionary<Connection, byte>> connectedRecipients =
new ConcurrentDictionary<Connection, ConcurrentDictionary<Connection, byte>>();
private float overloadCooldownTimer; private float overloadCooldownTimer;
private const float OverloadCooldown = 5.0f; private const float OverloadCooldown = 5.0f;
@@ -132,7 +135,7 @@ namespace Barotrauma.Items.Components
partial void InitProjectSpecific(XElement element); partial void InitProjectSpecific(XElement element);
private static readonly HashSet<PowerTransfer> recipientsToRefresh = new HashSet<PowerTransfer>(); private static readonly System.Collections.Concurrent.ConcurrentDictionary<PowerTransfer, byte> _recipientsToRefresh = new System.Collections.Concurrent.ConcurrentDictionary<PowerTransfer, byte>();
public override void UpdateBroken(float deltaTime, Camera cam) public override void UpdateBroken(float deltaTime, Camera cam)
{ {
base.UpdateBroken(deltaTime, cam); base.UpdateBroken(deltaTime, cam);
@@ -144,20 +147,21 @@ namespace Barotrauma.Items.Components
powerLoad = 0.0f; powerLoad = 0.0f;
currPowerConsumption = 0.0f; currPowerConsumption = 0.0f;
SetAllConnectionsDirty(); SetAllConnectionsDirty();
recipientsToRefresh.Clear(); _recipientsToRefresh.Clear();
foreach (HashSet<Connection> recipientList in connectedRecipients.Values) // Take snapshot for thread-safe iteration (no locks needed with ConcurrentDictionary)
foreach (var recipientDict in connectedRecipients.Values)
{ {
foreach (Connection c in recipientList) foreach (Connection c in recipientDict.Keys)
{ {
if (c.Item == item) { continue; } if (c.Item == item) { continue; }
var recipientPowerTransfer = c.Item.GetComponent<PowerTransfer>(); var recipientPowerTransfer = c.Item.GetComponent<PowerTransfer>();
if (recipientPowerTransfer != null) if (recipientPowerTransfer != null)
{ {
recipientsToRefresh.Add(recipientPowerTransfer); _recipientsToRefresh.TryAdd(recipientPowerTransfer, 0);
} }
} }
} }
foreach (PowerTransfer recipientPowerTransfer in recipientsToRefresh) foreach (PowerTransfer recipientPowerTransfer in _recipientsToRefresh.Keys)
{ {
recipientPowerTransfer.SetAllConnectionsDirty(); recipientPowerTransfer.SetAllConnectionsDirty();
recipientPowerTransfer.RefreshConnections(); recipientPowerTransfer.RefreshConnections();
@@ -304,58 +308,56 @@ namespace Barotrauma.Items.Components
protected void RefreshConnections() protected void RefreshConnections()
{ {
var connections = item.Connections; var connections = item.Connections;
foreach (Connection c in connections) if (connections == null) { return; }
// Take a snapshot of connections for thread-safe iteration
var connectionSnapshot = connections.ToList();
foreach (Connection c in connectionSnapshot)
{ {
if (!connectionDirty.ContainsKey(c)) if (!connectionDirty.TryGetValue(c, out bool isDirty))
{ {
connectionDirty[c] = true; connectionDirty[c] = true;
isDirty = true;
} }
else if (!connectionDirty[c])
if (!isDirty)
{ {
continue; continue;
} }
//find all connections that are connected to this one (directly or via another PowerTransfer) //find all connections that are connected to this one (directly or via another PowerTransfer)
HashSet<Connection> tempConnected; var tempConnected = connectedRecipients.GetOrAdd(c, _ => new ConcurrentDictionary<Connection, byte>());
if (!connectedRecipients.ContainsKey(c))
// Get previous recipients and clear
var previousRecipients = tempConnected.Keys.ToList();
tempConnected.Clear();
//mark all previous recipients as dirty
foreach (Connection recipient in previousRecipients)
{ {
tempConnected = new HashSet<Connection>(); var pt = recipient.Item.GetComponent<PowerTransfer>();
connectedRecipients.Add(c, tempConnected); if (pt != null) { pt.connectionDirty[recipient] = true; }
}
else
{
tempConnected = connectedRecipients[c];
tempConnected.Clear();
//mark all previous recipients as dirty
foreach (Connection recipient in tempConnected)
{
var pt = recipient.Item.GetComponent<PowerTransfer>();
if (pt != null) { pt.connectionDirty[recipient] = true; }
}
} }
tempConnected.Add(c); tempConnected.TryAdd(c, 0);
if (item.Condition > 0.0f) if (item.Condition > 0.0f)
{ {
GetConnected(c, tempConnected); GetConnected(c, tempConnected);
//go through all the PowerTransfers that we're connected to and set their connections to match the ones we just calculated //go through all the PowerTransfers that we're connected to and set their connections to match the ones we just calculated
//(no need to go through the recursive GetConnected method again) //(no need to go through the recursive GetConnected method again)
foreach (Connection recipient in tempConnected) // Take snapshot for thread-safe iteration (no locks needed)
var tempConnectedSnapshot = tempConnected.Keys.ToList();
foreach (Connection recipient in tempConnectedSnapshot)
{ {
if (recipient == c) { continue; } if (recipient == c) { continue; }
var recipientPowerTransfer = recipient.Item.GetComponent<PowerTransfer>(); var recipientPowerTransfer = recipient.Item.GetComponent<PowerTransfer>();
if (recipientPowerTransfer == null) { continue; } if (recipientPowerTransfer == null) { continue; }
if (!recipientPowerTransfer.connectedRecipients.ContainsKey(recipient))
var recipientSet = recipientPowerTransfer.connectedRecipients.GetOrAdd(recipient, _ => new ConcurrentDictionary<Connection, byte>());
recipientSet.Clear();
foreach (var connection in tempConnectedSnapshot)
{ {
recipientPowerTransfer.connectedRecipients.Add(recipient, new HashSet<Connection>()); recipientSet.TryAdd(connection, 0);
}
else
{
recipientPowerTransfer.connectedRecipients[recipient].Clear();
}
foreach (var connection in tempConnected)
{
recipientPowerTransfer.connectedRecipients[recipient].Add(connection);
} }
recipientPowerTransfer.connectionDirty[recipient] = false; recipientPowerTransfer.connectionDirty[recipient] = false;
} }
@@ -364,19 +366,20 @@ namespace Barotrauma.Items.Components
} }
} }
//Finds all the connections that can receive a signal sent into the given connection and stores them in the hashset. //Finds all the connections that can receive a signal sent into the given connection and stores them in the concurrent dictionary.
private void GetConnected(Connection c, HashSet<Connection> connected) private void GetConnected(Connection c, ConcurrentDictionary<Connection, byte> connected)
{ {
var recipients = c.Recipients; // Take snapshot for thread-safe iteration
var recipients = c.Recipients.ToList();
foreach (Connection recipient in recipients) foreach (Connection recipient in recipients)
{ {
if (recipient == null || connected.Contains(recipient)) { continue; } if (recipient == null || connected.ContainsKey(recipient)) { continue; }
Item it = recipient.Item; Item it = recipient.Item;
if (it == null || it.Condition <= 0.0f) { continue; } if (it == null || it.Condition <= 0.0f) { continue; }
connected.Add(recipient); connected.TryAdd(recipient, 0);
var powerTransfer = it.GetComponent<PowerTransfer>(); var powerTransfer = it.GetComponent<PowerTransfer>();
if (powerTransfer != null && powerTransfer.CanTransfer && powerTransfer.IsActive) if (powerTransfer != null && powerTransfer.CanTransfer && powerTransfer.IsActive)
@@ -394,10 +397,14 @@ namespace Barotrauma.Items.Components
connectionDirty[c] = true; connectionDirty[c] = true;
if (c.IsPower) if (c.IsPower)
{ {
ChangedConnections.Add(c); MarkConnectionChanged(c);
if (connectedRecipients.TryGetValue(c, out var recipients)) if (connectedRecipients.TryGetValue(c, out var recipients))
{ {
recipients.Where(c => c.IsPower).ForEach(c => ChangedConnections.Add(c)); // No lock needed - ConcurrentDictionary.Keys is thread-safe
foreach (var conn in recipients.Keys.Where(conn => conn.IsPower))
{
MarkConnectionChanged(conn);
}
} }
} }
} }
@@ -410,10 +417,14 @@ namespace Barotrauma.Items.Components
connectionDirty[connection] = true; connectionDirty[connection] = true;
if (connection.IsPower) if (connection.IsPower)
{ {
ChangedConnections.Add(connection); MarkConnectionChanged(connection);
if (connectedRecipients.TryGetValue(connection, out var recipients)) if (connectedRecipients.TryGetValue(connection, out var recipients))
{ {
recipients.Where(c => c.IsPower).ForEach(c => ChangedConnections.Add(c)); // No lock needed - ConcurrentDictionary.Keys is thread-safe
foreach (var conn in recipients.Keys.Where(conn => conn.IsPower))
{
MarkConnectionChanged(conn);
}
} }
} }
} }
@@ -452,16 +463,19 @@ namespace Barotrauma.Items.Components
public override void ReceiveSignal(Signal signal, Connection connection) public override void ReceiveSignal(Signal signal, Connection connection)
{ {
if (item.Condition <= 0.0f || connection.IsPower) { return; } if (item.Condition <= 0.0f || connection.IsPower) { return; }
if (!connectedRecipients.ContainsKey(connection)) { return; } if (!connectedRecipients.TryGetValue(connection, out var recipients)) { return; }
if (!signalConnections.Contains(connection)) { return; } if (!signalConnections.Contains(connection)) { return; }
foreach (Connection recipient in connectedRecipients[connection]) // No lock needed - ConcurrentDictionary.Keys is thread-safe
// Use ToList() snapshot for thread-safe iteration
foreach (Connection recipient in recipients.Keys.ToList())
{ {
if (recipient.Item == item || recipient.Item == signal.source) { continue; } if (recipient.Item == item || recipient.Item == signal.source) { continue; }
signal.source?.LastSentSignalRecipients.Add(recipient); signal.source?.LastSentSignalRecipients.Add(recipient);
foreach (ItemComponent ic in recipient.Item.Components) // Use ToArray() snapshot for thread-safe iteration
foreach (ItemComponent ic in recipient.Item.Components.ToArray())
{ {
//other junction boxes don't need to receive the signal in the pass-through signal connections //other junction boxes don't need to receive the signal in the pass-through signal connections
//because we relay it straight to the connected items without going through the whole chain of junction boxes //because we relay it straight to the connected items without going through the whole chain of junction boxes
@@ -471,7 +485,8 @@ namespace Barotrauma.Items.Components
if (recipient.Effects != null && signal.value != "0" && !string.IsNullOrEmpty(signal.value)) if (recipient.Effects != null && signal.value != "0" && !string.IsNullOrEmpty(signal.value))
{ {
foreach (StatusEffect effect in recipient.Effects) // Use ToArray() snapshot for thread-safe iteration
foreach (StatusEffect effect in recipient.Effects.ToArray())
{ {
recipient.Item.ApplyStatusEffect(effect, ActionType.OnUse, 1.0f); recipient.Item.ApplyStatusEffect(effect, ActionType.OnUse, 1.0f);
} }
@@ -484,7 +499,7 @@ namespace Barotrauma.Items.Components
base.RemoveComponentSpecific(); base.RemoveComponentSpecific();
connectedRecipients?.Clear(); connectedRecipients?.Clear();
connectionDirty?.Clear(); connectionDirty?.Clear();
recipientsToRefresh.Clear(); _recipientsToRefresh.Clear();
} }
} }
} }
@@ -1,4 +1,6 @@
using System; using System;
using System.Collections.Concurrent;
using System.Threading;
using Microsoft.Xna.Framework; using Microsoft.Xna.Framework;
using System.Collections.Generic; using System.Collections.Generic;
using System.Linq; using System.Linq;
@@ -62,17 +64,77 @@ namespace Barotrauma.Items.Components
protected const float UpdateInterval = (float)Timing.Step; protected const float UpdateInterval = (float)Timing.Step;
/// <summary> /// <summary>
/// List of all powered ItemComponents /// List of all powered ItemComponents (thread-safe)
/// </summary> /// </summary>
private static readonly List<Powered> poweredList = new List<Powered>(); 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 public static IEnumerable<Powered> PoweredList
{ {
get { return 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);
} }
public static readonly HashSet<Connection> ChangedConnections = new HashSet<Connection>(); /// <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 _);
}
public readonly static Dictionary<int, GridInfo> Grids = new Dictionary<int, GridInfo>(); /// <summary>
/// Thread-safe grid dictionary
/// </summary>
public readonly static ConcurrentDictionary<int, GridInfo> Grids = new ConcurrentDictionary<int, GridInfo>();
/// <summary> /// <summary>
/// The amount of power currently consumed by the item. Negative values mean that the item is providing power to connected items /// The amount of power currently consumed by the item. Negative values mean that the item is providing power to connected items
@@ -209,7 +271,8 @@ namespace Barotrauma.Items.Components
public Powered(Item item, ContentXElement element) public Powered(Item item, ContentXElement element)
: base(item, element) : base(item, element)
{ {
poweredList.Add(this); _poweredDict.TryAdd(this, 0);
InvalidatePoweredListCache();
InitProjectSpecific(element); InitProjectSpecific(element);
} }
@@ -322,17 +385,20 @@ namespace Barotrauma.Items.Components
//don't use cache if there are no existing grids //don't use cache if there are no existing grids
if (Grids.Count > 0 && useCache) if (Grids.Count > 0 && useCache)
{ {
// Take a snapshot of changed connections for iteration
var changedSnapshot = ChangedConnections.ToList();
//delete all grids that were affected //delete all grids that were affected
foreach (Connection c in ChangedConnections) foreach (Connection c in changedSnapshot)
{ {
if (c.Grid != null) if (c.Grid != null)
{ {
Grids.Remove(c.Grid.ID); Grids.TryRemove(c.Grid.ID, out _);
c.Grid = null; c.Grid = null;
} }
} }
foreach (Connection c in ChangedConnections) foreach (Connection c in changedSnapshot)
{ {
//Make sure the connection grid hasn't been resolved by another connection update //Make sure the connection grid hasn't been resolved by another connection update
//Ensure the connection has other connections //Ensure the connection has other connections
@@ -346,7 +412,7 @@ namespace Barotrauma.Items.Components
else else
{ {
//Clear all grid IDs from connections //Clear all grid IDs from connections
foreach (Powered powered in poweredList) foreach (Powered powered in PoweredList)
{ {
//Only check devices with connectors //Only check devices with connectors
if (powered.powerIn != null) if (powered.powerIn != null)
@@ -361,7 +427,7 @@ namespace Barotrauma.Items.Components
Grids.Clear(); Grids.Clear();
foreach (Powered powered in poweredList) foreach (Powered powered in PoweredList)
{ {
if (powered.Item.Condition <= 0f) { continue; } if (powered.Item.Condition <= 0f) { continue; }
@@ -392,7 +458,7 @@ namespace Barotrauma.Items.Components
} }
//Clear changed connections after each update //Clear changed connections after each update
ChangedConnections.Clear(); ClearChangedConnections();
} }
private static GridInfo PropagateGrid(Connection conn) private static GridInfo PropagateGrid(Connection conn)
@@ -422,8 +488,8 @@ namespace Barotrauma.Items.Components
c.Grid = grid; c.Grid = grid;
grid.AddConnection(c); grid.AddConnection(c);
//Add on recipients //Add on recipients - use ToList() snapshot for thread-safe iteration
foreach (Connection otherC in c.Recipients) foreach (Connection otherC in c.Recipients.ToList())
{ {
//Only add valid connections //Only add valid connections
if (otherC.Grid != grid && (otherC.Grid == null || !Grids.ContainsKey(otherC.Grid.ID)) && ValidPowerConnection(c, otherC)) if (otherC.Grid != grid && (otherC.Grid == null || !Grids.ContainsKey(otherC.Grid.ID)) && ValidPowerConnection(c, otherC))
@@ -494,7 +560,7 @@ namespace Barotrauma.Items.Components
} }
//Determine if devices are adding a load or providing power, also resolve solo nodes //Determine if devices are adding a load or providing power, also resolve solo nodes
foreach (Powered powered in poweredList) foreach (Powered powered in PoweredList)
{ {
//Make voltage decay to ensure the device powers down. //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) //This only effects devices with no power input (whose voltage is set by other means, e.g. status effects from a contained battery)
@@ -730,7 +796,8 @@ namespace Barotrauma.Items.Components
{ {
if (item.Connections != null && powerIn != null) if (item.Connections != null && powerIn != null)
{ {
foreach (Connection recipient in powerIn.Recipients) // Use ToList() snapshot for thread-safe iteration
foreach (Connection recipient in powerIn.Recipients.ToList())
{ {
if (!recipient.IsPower || !recipient.IsOutput) { continue; } if (!recipient.IsPower || !recipient.IsOutput) { continue; }
if (recipient.Item?.GetComponent<PowerContainer>() is PowerContainer battery) if (recipient.Item?.GetComponent<PowerContainer>() is PowerContainer battery)
@@ -750,13 +817,14 @@ namespace Barotrauma.Items.Components
{ {
if (c.IsPower && c.Grid != null) if (c.IsPower && c.Grid != null)
{ {
ChangedConnections.Add(c); MarkConnectionChanged(c);
} }
} }
} }
base.RemoveComponentSpecific(); base.RemoveComponentSpecific();
poweredList.Remove(this); _poweredDict.TryRemove(this, out _);
InvalidatePoweredListCache();
} }
} }
@@ -780,9 +848,9 @@ namespace Barotrauma.Items.Components
Connections.Remove(c); Connections.Remove(c);
//Remove the grid if it has no devices //Remove the grid if it has no devices
if (Connections.Count == 0 && Powered.Grids.ContainsKey(ID)) if (Connections.Count == 0)
{ {
Powered.Grids.Remove(ID); Powered.Grids.TryRemove(ID, out _);
} }
} }
@@ -456,7 +456,8 @@ namespace Barotrauma.Items.Components
item.SendSignal(conditionSignal, "condition_out"); item.SendSignal(conditionSignal, "condition_out");
foreach (var component in item.Components) // Use ToArray() snapshot for thread-safe iteration
foreach (var component in item.Components.ToArray())
{ {
if (component is IDeteriorateUnderStress deteriorateUnderStress) if (component is IDeteriorateUnderStress deteriorateUnderStress)
{ {
@@ -713,7 +714,8 @@ namespace Barotrauma.Items.Components
#endif #endif
if (LastActiveTime > Timing.TotalTime) { return true; } if (LastActiveTime > Timing.TotalTime) { return true; }
foreach (ItemComponent ic in item.Components) // Use ToArray() snapshot for thread-safe iteration
foreach (ItemComponent ic in item.Components.ToArray())
{ {
if (ic is Fabricator || ic is Deconstructor) if (ic is Fabricator || ic is Deconstructor)
{ {
@@ -761,7 +763,8 @@ namespace Barotrauma.Items.Components
private float GetDeteriorationDelayMultiplier() private float GetDeteriorationDelayMultiplier()
{ {
foreach (ItemComponent ic in item.Components) // Use ToArray() snapshot for thread-safe iteration
foreach (ItemComponent ic in item.Components.ToArray())
{ {
if (ic is Engine engine) if (ic is Engine engine)
{ {
@@ -222,13 +222,14 @@ namespace Barotrauma.Items.Components
public void SetRecipientsDirty() public void SetRecipientsDirty()
{ {
recipientsDirty = true; recipientsDirty = true;
if (IsPower) { Powered.ChangedConnections.Add(this); } if (IsPower) { Powered.MarkConnectionChanged(this); }
} }
private void RefreshRecipients() private void RefreshRecipients()
{ {
recipients.Clear(); recipients.Clear();
foreach (var wire in wires) // Use ToArray() snapshot for thread-safe iteration
foreach (var wire in wires.ToArray())
{ {
Connection recipient = wire.OtherConnection(this); Connection recipient = wire.OtherConnection(this);
if (recipient != null) { recipients.Add(recipient); } if (recipient != null) { recipients.Add(recipient); }
@@ -267,8 +268,8 @@ namespace Barotrauma.Items.Components
//Check if both connections belong to a larger grid //Check if both connections belong to a larger grid
if (prevOtherConnection.recipients.Count > 1 && recipients.Count > 1) if (prevOtherConnection.recipients.Count > 1 && recipients.Count > 1)
{ {
Powered.ChangedConnections.Add(prevOtherConnection); Powered.MarkConnectionChanged(prevOtherConnection);
Powered.ChangedConnections.Add(this); Powered.MarkConnectionChanged(this);
} }
else if (recipients.Count > 1) else if (recipients.Count > 1)
{ {
@@ -284,7 +285,7 @@ namespace Barotrauma.Items.Components
else if (Grid.Connections.Count == 2) else if (Grid.Connections.Count == 2)
{ {
//Delete the grid as these were the only 2 devices //Delete the grid as these were the only 2 devices
Powered.Grids.Remove(Grid.ID); Powered.Grids.TryRemove(Grid.ID, out _);
Grid = null; Grid = null;
prevOtherConnection.Grid = null; prevOtherConnection.Grid = null;
} }
@@ -325,8 +326,8 @@ namespace Barotrauma.Items.Components
else else
{ {
//Flag change so that proper grids can be formed //Flag change so that proper grids can be formed
Powered.ChangedConnections.Add(this); Powered.MarkConnectionChanged(this);
Powered.ChangedConnections.Add(otherConnection); Powered.MarkConnectionChanged(otherConnection);
} }
} }
@@ -339,7 +340,8 @@ namespace Barotrauma.Items.Components
{ {
LastSentSignal = signal; LastSentSignal = signal;
enumeratingWires = true; enumeratingWires = true;
foreach (var wire in wires) // Use ToArray() snapshot for thread-safe iteration
foreach (var wire in wires.ToArray())
{ {
Connection recipient = wire.OtherConnection(this); Connection recipient = wire.OtherConnection(this);
if (recipient == null) { continue; } if (recipient == null) { continue; }
@@ -354,14 +356,14 @@ namespace Barotrauma.Items.Components
GameMain.LuaCs.Hook.Call("signalReceived." + recipient.item.Prefab.Identifier, signal, recipient); GameMain.LuaCs.Hook.Call("signalReceived." + recipient.item.Prefab.Identifier, signal, recipient);
} }
foreach (CircuitBoxConnection connection in CircuitBoxConnections) foreach (CircuitBoxConnection connection in CircuitBoxConnections.ToArray())
{ {
connection.ReceiveSignal(signal); connection.ReceiveSignal(signal);
GameMain.LuaCs.Hook.Call("signalReceived", signal, connection.Connection); GameMain.LuaCs.Hook.Call("signalReceived", signal, connection.Connection);
GameMain.LuaCs.Hook.Call("signalReceived." + connection.Connection.Item.Prefab.Identifier, signal, connection); GameMain.LuaCs.Hook.Call("signalReceived." + connection.Connection.Item.Prefab.Identifier, signal, connection);
} }
enumeratingWires = false; enumeratingWires = false;
foreach (var removedWire in removedWires) foreach (var removedWire in removedWires.ToArray())
{ {
wires.Remove(removedWire); wires.Remove(removedWire);
} }
@@ -372,14 +374,16 @@ namespace Barotrauma.Items.Components
{ {
conn.LastReceivedSignal = signal; conn.LastReceivedSignal = signal;
foreach (ItemComponent ic in conn.item.Components) // Use ToArray() snapshot for thread-safe iteration
foreach (ItemComponent ic in conn.item.Components.ToArray())
{ {
ic.ReceiveSignal(signal, conn); ic.ReceiveSignal(signal, conn);
} }
if (conn.Effects == null || signal.value == "0") { return; } if (conn.Effects == null || signal.value == "0") { return; }
foreach (StatusEffect effect in conn.Effects) // Use ToArray() snapshot for thread-safe iteration
foreach (StatusEffect effect in conn.Effects.ToArray())
{ {
conn.Item.ApplyStatusEffect(effect, ActionType.OnUse, (float)Timing.Step); conn.Item.ApplyStatusEffect(effect, ActionType.OnUse, (float)Timing.Step);
} }
@@ -389,13 +393,15 @@ namespace Barotrauma.Items.Components
{ {
if (IsPower && Grid != null) if (IsPower && Grid != null)
{ {
Powered.ChangedConnections.Add(this); Powered.MarkConnectionChanged(this);
foreach (Connection c in recipients) // Use ToArray() snapshot for thread-safe iteration
foreach (Connection c in recipients.ToArray())
{ {
Powered.ChangedConnections.Add(c); Powered.MarkConnectionChanged(c);
} }
} }
foreach (var wire in wires) // Use ToArray() snapshot for thread-safe iteration
foreach (var wire in wires.ToArray())
{ {
wire.RemoveConnection(this); wire.RemoveConnection(this);
recipientsDirty = true; recipientsDirty = true;
@@ -403,7 +409,7 @@ namespace Barotrauma.Items.Components
if (enumeratingWires) if (enumeratingWires)
{ {
foreach (var wire in wires) foreach (var wire in wires.ToArray())
{ {
removedWires.Add(wire); removedWires.Add(wire);
} }
@@ -446,7 +452,8 @@ namespace Barotrauma.Items.Components
{ {
XElement newElement = new XElement(IsOutput ? "output" : "input", new XAttribute("name", Name)); XElement newElement = new XElement(IsOutput ? "output" : "input", new XAttribute("name", Name));
foreach (var wire in wires.OrderBy(w => w.Item.ID)) // 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", newElement.Add(new XElement("link",
new XAttribute("w", wire.Item.ID.ToString()), new XAttribute("w", wire.Item.ID.ToString()),
@@ -148,14 +148,16 @@ namespace Barotrauma.Items.Components
Vector2 wireNodeOffset = item.Submarine == null ? Vector2.Zero : item.Submarine.HiddenSubPosition + amount; Vector2 wireNodeOffset = item.Submarine == null ? Vector2.Zero : item.Submarine.HiddenSubPosition + amount;
foreach (Connection c in Connections) foreach (Connection c in Connections)
{ {
foreach (Wire wire in c.Wires) // Use ToArray() snapshot for thread-safe iteration
foreach (Wire wire in c.Wires.ToArray())
{ {
if (wire == null) { continue; } if (wire == null) { continue; }
TryMoveWire(wire); TryMoveWire(wire);
} }
} }
foreach (var wire in DisconnectedWires) // Use ToList() snapshot for thread-safe iteration
foreach (var wire in DisconnectedWires.ToList())
{ {
TryMoveWire(wire); TryMoveWire(wire);
} }
@@ -387,7 +389,7 @@ namespace Barotrauma.Items.Components
} }
foreach (var connection in Connections) foreach (var connection in Connections)
{ {
Powered.ChangedConnections.Remove(connection); Powered.UnmarkConnectionChanged(connection);
connection.Recipients.Clear(); connection.Recipients.Clear();
} }
Connections.Clear(); Connections.Clear();
@@ -412,15 +414,19 @@ namespace Barotrauma.Items.Components
msg.WriteByte((byte)Connections.Count); msg.WriteByte((byte)Connections.Count);
foreach (Connection connection in Connections) foreach (Connection connection in Connections)
{ {
msg.WriteVariableUInt32((uint)connection.Wires.Count); // Use ToArray() snapshot for thread-safe iteration
foreach (Wire wire in connection.Wires) 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); msg.WriteUInt16(wire?.Item == null ? (ushort)0 : wire.Item.ID);
} }
} }
msg.WriteUInt16((ushort)DisconnectedWires.Count); // Use ToList() snapshot for thread-safe iteration
foreach (Wire disconnectedWire in DisconnectedWires) var disconnectedSnapshot = DisconnectedWires.ToList();
msg.WriteUInt16((ushort)disconnectedSnapshot.Count);
foreach (Wire disconnectedWire in disconnectedSnapshot)
{ {
msg.WriteUInt16(disconnectedWire.Item.ID); msg.WriteUInt16(disconnectedWire.Item.ID);
} }
@@ -2,6 +2,7 @@
using Microsoft.Xna.Framework; using Microsoft.Xna.Framework;
using System; using System;
using System.Collections.Generic; using System.Collections.Generic;
using System.Collections.Immutable;
using System.Linq; using System.Linq;
using System.Xml.Linq; using System.Xml.Linq;
@@ -31,7 +32,8 @@ namespace Barotrauma.Items.Components
private float thirdInverseMax = 0, loadEqnConstant = 0; private float thirdInverseMax = 0, loadEqnConstant = 0;
private static readonly Dictionary<string, string> connectionPairs = new Dictionary<string, string> // Thread-safe immutable dictionary for connection pairs (read-only after initialization)
private static readonly ImmutableDictionary<string, string> connectionPairs = new Dictionary<string, string>
{ {
{ "power_in", "power_out"}, { "power_in", "power_out"},
{ "signal_in", "signal_out" }, { "signal_in", "signal_out" },
@@ -40,7 +42,7 @@ namespace Barotrauma.Items.Components
{ "signal_in3", "signal_out3" }, { "signal_in3", "signal_out3" },
{ "signal_in4", "signal_out4" }, { "signal_in4", "signal_out4" },
{ "signal_in5", "signal_out5" } { "signal_in5", "signal_out5" }
}; }.ToImmutableDictionary();
protected override PowerPriority Priority { get { return PowerPriority.Relay; } } protected override PowerPriority Priority { get { return PowerPriority.Relay; } }
@@ -1,6 +1,7 @@
using Barotrauma.Networking; using Barotrauma.Networking;
using Microsoft.Xna.Framework; using Microsoft.Xna.Framework;
using System; using System;
using System.Collections.Concurrent;
using System.Collections.Generic; using System.Collections.Generic;
using System.Globalization; using System.Globalization;
using System.Linq; using System.Linq;
@@ -10,7 +11,8 @@ namespace Barotrauma.Items.Components
{ {
partial class WifiComponent : ItemComponent, IServerSerializable, IClientSerializable partial class WifiComponent : ItemComponent, IServerSerializable, IClientSerializable
{ {
private static readonly List<WifiComponent> list = new List<WifiComponent>(); private static readonly ConcurrentDictionary<WifiComponent, byte> _wifiDict = new ConcurrentDictionary<WifiComponent, byte>();
private static IEnumerable<WifiComponent> AllWifiComponents => _wifiDict.Keys;
const int ChannelMemorySize = 10; const int ChannelMemorySize = 10;
@@ -111,7 +113,7 @@ namespace Barotrauma.Items.Components
public WifiComponent(Item item, ContentXElement element) public WifiComponent(Item item, ContentXElement element)
: base (item, element) : base (item, element)
{ {
list.Add(this); _wifiDict.TryAdd(this, 0);
IsActive = true; IsActive = true;
} }
@@ -156,7 +158,7 @@ namespace Barotrauma.Items.Components
/// </summary> /// </summary>
public IEnumerable<WifiComponent> GetReceiversInRange() public IEnumerable<WifiComponent> GetReceiversInRange()
{ {
return list.Where(w => w != this && w.CanReceive(this)); return AllWifiComponents.Where(w => w != this && w.CanReceive(this));
} }
public bool CanReceive(WifiComponent sender) public bool CanReceive(WifiComponent sender)
@@ -185,7 +187,7 @@ namespace Barotrauma.Items.Components
/// </summary> /// </summary>
public IEnumerable<WifiComponent> GetTransmittersInRange() public IEnumerable<WifiComponent> GetTransmittersInRange()
{ {
return list.Where(w => w != this && w.CanTransmit(this)); return AllWifiComponents.Where(w => w != this && w.CanTransmit(this));
} }
public bool CanTransmit(WifiComponent sender) public bool CanTransmit(WifiComponent sender)
@@ -275,7 +277,8 @@ namespace Barotrauma.Items.Components
if (signal.source != null) if (signal.source != null)
{ {
foreach (Connection receiver in wifiComp.item.LastSentSignalRecipients) // Use ToList() snapshot for thread-safe iteration
foreach (Connection receiver in wifiComp.item.LastSentSignalRecipients.ToList())
{ {
if (!signal.source.LastSentSignalRecipients.Contains(receiver)) if (!signal.source.LastSentSignalRecipients.Contains(receiver))
{ {
@@ -366,7 +369,7 @@ namespace Barotrauma.Items.Components
protected override void RemoveComponentSpecific() protected override void RemoveComponentSpecific()
{ {
base.RemoveComponentSpecific(); base.RemoveComponentSpecific();
list.Remove(this); _wifiDict.TryRemove(this, out _);
} }
public override XElement Save(XElement parentElement) public override XElement Save(XElement parentElement)
@@ -250,7 +250,8 @@ namespace Barotrauma.Items.Components
{ {
if (connections[0] != null && connections[1] != null) if (connections[0] != null && connections[1] != null)
{ {
foreach (ItemComponent ic in item.Components) // Use ToArray() snapshot for thread-safe iteration
foreach (ItemComponent ic in item.Components.ToArray())
{ {
if (ic == this) { continue; } if (ic == this) { continue; }
@@ -381,7 +381,8 @@ namespace Barotrauma
if (Owner is not Item it) { return; } if (Owner is not Item it) { return; }
foreach (var c in it.Components) // Use ToArray() snapshot for thread-safe iteration
foreach (var c in it.Components.ToArray())
{ {
c.OnInventoryChanged(); c.OnInventoryChanged();
} }
@@ -2441,10 +2441,11 @@ namespace Barotrauma
{ {
if (c.IsPower) if (c.IsPower)
{ {
Powered.ChangedConnections.Add(c); Powered.MarkConnectionChanged(c);
foreach (Connection conn in c.Recipients) // Use ToList() snapshot for thread-safe iteration
foreach (Connection conn in c.Recipients.ToList())
{ {
Powered.ChangedConnections.Add(conn); Powered.MarkConnectionChanged(conn);
} }
} }
} }
@@ -2980,7 +2981,8 @@ namespace Barotrauma
foreach (Connection c in connectionPanel.Connections) foreach (Connection c in connectionPanel.Connections)
{ {
if (connectionFilter != null && !connectionFilter(c)) { continue; } if (connectionFilter != null && !connectionFilter(c)) { continue; }
foreach (Connection recipient in c.Recipients) // Use ToList() snapshot for thread-safe iteration
foreach (Connection recipient in c.Recipients.ToList())
{ {
var component = recipient.Item.GetComponent<T>(); var component = recipient.Item.GetComponent<T>();
if (component != null) if (component != null)
@@ -3013,7 +3015,8 @@ namespace Barotrauma
foreach (Connection c in connectionPanel.Connections) foreach (Connection c in connectionPanel.Connections)
{ {
if (connectionFilter != null && !connectionFilter(c)) { continue; } if (connectionFilter != null && !connectionFilter(c)) { continue; }
foreach (Connection recipient in c.Recipients) // Use ToList() snapshot for thread-safe iteration
foreach (Connection recipient in c.Recipients.ToList())
{ {
var component = recipient.Item.GetComponent<T>(); var component = recipient.Item.GetComponent<T>();
if (component != null && !connectedComponents.Contains(component)) if (component != null && !connectedComponents.Contains(component))
@@ -3067,12 +3070,13 @@ namespace Barotrauma
alreadySearched.Add(c); alreadySearched.Add(c);
static IEnumerable<Connection> GetRecipients(Connection c) static IEnumerable<Connection> GetRecipients(Connection c)
{ {
foreach (Connection recipient in c.Recipients) // Use ToList() snapshot for thread-safe iteration
foreach (Connection recipient in c.Recipients.ToList())
{ {
yield return recipient; yield return recipient;
} }
//check circuit box inputs/outputs this connection is connected to //check circuit box inputs/outputs this connection is connected to
foreach (var circuitBoxConnection in c.CircuitBoxConnections) foreach (var circuitBoxConnection in c.CircuitBoxConnections.ToArray())
{ {
yield return circuitBoxConnection.Connection; yield return circuitBoxConnection.Connection;
} }
@@ -3212,7 +3216,8 @@ namespace Barotrauma
if (signal.stepsTaken > 5 && signal.source != null) if (signal.stepsTaken > 5 && signal.source != null)
{ {
int duplicateRecipients = 0; int duplicateRecipients = 0;
foreach (var recipient in signal.source.LastSentSignalRecipients) // Use ToList() snapshot for thread-safe iteration
foreach (var recipient in signal.source.LastSentSignalRecipients.ToList())
{ {
if (recipient == connection) if (recipient == connection)
{ {
@@ -712,11 +712,11 @@ namespace Barotrauma
try try
{ {
foreach (Item item in itemList) Parallel.ForEach(itemList, parallelOptions, item =>
{ {
lastUpdatedItem = item; lastUpdatedItem = item;
item.Update(scaledDeltaTime, cam); item.Update(scaledDeltaTime, cam);
} });
} }
catch (InvalidOperationException e) catch (InvalidOperationException e)
{ {
@@ -2157,7 +2157,7 @@ namespace Barotrauma
GameMain.World = null; GameMain.World = null;
Powered.Grids.Clear(); Powered.Grids.Clear();
Powered.ChangedConnections.Clear(); Powered.ClearChangedConnections();
GC.Collect(); GC.Collect();
@@ -2197,7 +2197,7 @@ namespace Barotrauma
ConnectedDockingPorts?.Clear(); ConnectedDockingPorts?.Clear();
Powered.ChangedConnections.Clear(); Powered.ClearChangedConnections();
Powered.Grids.Clear(); Powered.Grids.Clear();
loaded.Remove(this); loaded.Remove(this);
@@ -1,26 +1,56 @@
using Barotrauma.Items.Components; using Barotrauma.Items.Components;
using Microsoft.Xna.Framework; using Microsoft.Xna.Framework;
using System; using System;
using System.Collections.Concurrent;
using System.Collections.Generic; using System.Collections.Generic;
using System.Linq; using System.Linq;
using System.Threading;
namespace Barotrauma namespace Barotrauma
{ {
class DelayedListElement class DelayedListElement
{ {
public readonly long Id;
public readonly DelayedEffect Parent; public readonly DelayedEffect Parent;
public readonly Entity Entity; public readonly Entity Entity;
public Vector2? WorldPosition;
private Vector2? _worldPosition;
private readonly object _worldPositionLock = new object();
public Vector2? WorldPosition
{
get
{
lock (_worldPositionLock)
{
return _worldPosition;
}
}
set
{
lock (_worldPositionLock)
{
_worldPosition = value;
}
}
}
/// <summary> /// <summary>
/// Should the delayed effect attempt to determine the position of the effect based on the targets, or just use the position that was passed to the constructor. /// Should the delayed effect attempt to determine the position of the effect based on the targets, or just use the position that was passed to the constructor.
/// </summary> /// </summary>
public bool GetPositionBasedOnTargets; public bool GetPositionBasedOnTargets;
public readonly Vector2? StartPosition; public readonly Vector2? StartPosition;
public readonly List<ISerializableEntity> Targets; public readonly List<ISerializableEntity> Targets;
public float Delay;
private volatile float _delay;
public float Delay
{
get => _delay;
set => _delay = value;
}
public DelayedListElement(DelayedEffect parentEffect, Entity parentEntity, IEnumerable<ISerializableEntity> targets, float delay, Vector2? worldPosition, Vector2? startPosition) public DelayedListElement(DelayedEffect parentEffect, Entity parentEntity, IEnumerable<ISerializableEntity> targets, float delay, Vector2? worldPosition, Vector2? startPosition)
{ {
Id = Interlocked.Increment(ref DelayedEffect._delayElementIdCounter);
Parent = parentEffect; Parent = parentEffect;
Entity = parentEntity; Entity = parentEntity;
Targets = new List<ISerializableEntity>(targets); Targets = new List<ISerializableEntity>(targets);
@@ -29,9 +59,19 @@ namespace Barotrauma
StartPosition = startPosition; StartPosition = startPosition;
} }
} }
class DelayedEffect : StatusEffect class DelayedEffect : StatusEffect
{ {
public static readonly List<DelayedListElement> DelayList = new List<DelayedListElement>(); // Thread-safe counter for generating unique IDs for DelayedListElement
internal static long _delayElementIdCounter;
// Thread-safe dictionary for delayed effects
public static readonly ConcurrentDictionary<long, DelayedListElement> DelayListDict = new ConcurrentDictionary<long, DelayedListElement>();
/// <summary>
/// Provides a thread-safe enumerable view of the delay list for iteration.
/// </summary>
public static IEnumerable<DelayedListElement> DelayList => DelayListDict.Values;
private enum DelayTypes private enum DelayTypes
{ {
@@ -62,9 +102,10 @@ namespace Barotrauma
if (this.type != type || !HasRequiredItems(entity)) { return; } if (this.type != type || !HasRequiredItems(entity)) { return; }
if (!Stackable) if (!Stackable)
{ {
foreach (var existingEffect in DelayList) // Thread-safe iteration over ConcurrentDictionary
foreach (var kvp in DelayListDict)
{ {
if (existingEffect.Parent == this && existingEffect.Targets.FirstOrDefault() == target) if (kvp.Value.Parent == this && kvp.Value.Targets.FirstOrDefault() == target)
{ {
return; return;
} }
@@ -72,18 +113,19 @@ namespace Barotrauma
} }
if (!IsValidTarget(target)) { return; } if (!IsValidTarget(target)) { return; }
currentTargets.Clear(); var targets = CurrentTargets;
currentTargets.Add(target); targets.Clear();
if (!HasRequiredConditions(currentTargets)) { return; } targets.Add(target);
if (!HasRequiredConditions(targets)) { return; }
switch (delayType) switch (delayType)
{ {
case DelayTypes.Timer: case DelayTypes.Timer:
var newDelayListElement = new DelayedListElement(this, entity, currentTargets, delay, worldPosition ?? GetPosition(entity, currentTargets, worldPosition), startPosition: null) var newDelayListElement = new DelayedListElement(this, entity, targets, delay, worldPosition ?? GetPosition(entity, targets, worldPosition), startPosition: null)
{ {
GetPositionBasedOnTargets = worldPosition == null GetPositionBasedOnTargets = worldPosition == null
}; };
DelayList.Add(newDelayListElement); DelayListDict.TryAdd(newDelayListElement.Id, newDelayListElement);
break; break;
case DelayTypes.ReachCursor: case DelayTypes.ReachCursor:
Projectile projectile = (entity as Item)?.GetComponent<Projectile>(); Projectile projectile = (entity as Item)?.GetComponent<Projectile>();
@@ -105,7 +147,8 @@ namespace Barotrauma
return; return;
} }
DelayList.Add(new DelayedListElement(this, entity, currentTargets, Vector2.Distance(entity.WorldPosition, projectile.User.CursorWorldPosition), worldPosition, entity.WorldPosition)); var reachCursorElement = new DelayedListElement(this, entity, targets, Vector2.Distance(entity.WorldPosition, projectile.User.CursorWorldPosition), worldPosition, entity.WorldPosition);
DelayListDict.TryAdd(reachCursorElement.Id, reachCursorElement);
break; break;
} }
} }
@@ -119,25 +162,28 @@ namespace Barotrauma
if (delayType == DelayTypes.ReachCursor && Character.Controlled == null) { return; } if (delayType == DelayTypes.ReachCursor && Character.Controlled == null) { return; }
if (!Stackable) if (!Stackable)
{ {
foreach (var existingEffect in DelayList) // Thread-safe iteration over ConcurrentDictionary
foreach (var kvp in DelayListDict)
{ {
if (existingEffect.Parent == this && existingEffect.Targets.SequenceEqual(targets)) { return; } if (kvp.Value.Parent == this && kvp.Value.Targets.SequenceEqual(targets)) { return; }
} }
} }
currentTargets.Clear(); var localTargets = CurrentTargets;
localTargets.Clear();
foreach (ISerializableEntity target in targets) foreach (ISerializableEntity target in targets)
{ {
if (!IsValidTarget(target)) { continue; } if (!IsValidTarget(target)) { continue; }
currentTargets.Add(target); localTargets.Add(target);
} }
if (!HasRequiredConditions(currentTargets)) { return; } if (!HasRequiredConditions(localTargets)) { return; }
switch (delayType) switch (delayType)
{ {
case DelayTypes.Timer: case DelayTypes.Timer:
DelayList.Add(new DelayedListElement(this, entity, currentTargets, delay, worldPosition, null)); var timerElement = new DelayedListElement(this, entity, localTargets, delay, worldPosition, null);
DelayListDict.TryAdd(timerElement.Id, timerElement);
break; break;
case DelayTypes.ReachCursor: case DelayTypes.ReachCursor:
Projectile projectile = (entity as Item)?.GetComponent<Projectile>(); Projectile projectile = (entity as Item)?.GetComponent<Projectile>();
@@ -161,19 +207,21 @@ namespace Barotrauma
return; return;
} }
DelayList.Add(new DelayedListElement(this, entity, currentTargets, Vector2.Distance(entity.WorldPosition, user.CursorWorldPosition), worldPosition, entity.WorldPosition)); var reachCursorElement = new DelayedListElement(this, entity, localTargets, Vector2.Distance(entity.WorldPosition, user.CursorWorldPosition), worldPosition, entity.WorldPosition);
DelayListDict.TryAdd(reachCursorElement.Id, reachCursorElement);
break; break;
} }
} }
public static void Update(float deltaTime) public static void Update(float deltaTime)
{ {
for (int i = DelayList.Count - 1; i >= 0; i--) // Thread-safe iteration over ConcurrentDictionary
foreach (var kvp in DelayListDict)
{ {
DelayedListElement element = DelayList[i]; DelayedListElement element = kvp.Value;
if (element.Parent.CheckConditionalAlways && !element.Parent.HasRequiredConditions(element.Targets)) if (element.Parent.CheckConditionalAlways && !element.Parent.HasRequiredConditions(element.Targets))
{ {
DelayList.Remove(element); DelayListDict.TryRemove(element.Id, out _);
continue; continue;
} }
@@ -187,7 +235,7 @@ namespace Barotrauma
//keep refreshing the position until the effect runs (so e.g. a delayed effect runs at the last known position of a monster before it despawned) //keep refreshing the position until the effect runs (so e.g. a delayed effect runs at the last known position of a monster before it despawned)
if (element.GetPositionBasedOnTargets && element.Entity is { Removed: false }) if (element.GetPositionBasedOnTargets && element.Entity is { Removed: false })
{ {
element.WorldPosition = element.Parent.GetPosition(element.Entity, element.Parent.currentTargets); element.WorldPosition = element.Parent.GetPosition(element.Entity, element.Parent.CurrentTargets);
} }
continue; continue;
} }
@@ -198,8 +246,8 @@ namespace Barotrauma
} }
element.Parent.Apply(deltaTime, element.Entity, element.Targets, element.WorldPosition); element.Parent.Apply(deltaTime, element.Entity, element.Targets, element.WorldPosition);
DelayList.Remove(element); DelayListDict.TryRemove(element.Id, out _);
} }
} }
} }
} }
@@ -7,15 +7,18 @@ using FarseerPhysics.Dynamics;
using Microsoft.Xna.Framework; using Microsoft.Xna.Framework;
using Steamworks; using Steamworks;
using System; using System;
using System.Collections.Concurrent;
using System.Collections.Generic; using System.Collections.Generic;
using System.Collections.Immutable; using System.Collections.Immutable;
using System.Linq; using System.Linq;
using System.Threading;
using System.Xml.Linq; using System.Xml.Linq;
namespace Barotrauma namespace Barotrauma
{ {
class DurationListElement class DurationListElement
{ {
public readonly long Id;
public readonly StatusEffect Parent; public readonly StatusEffect Parent;
public readonly Entity Entity; public readonly Entity Entity;
public float Duration public float Duration
@@ -24,12 +27,24 @@ namespace Barotrauma
private set; private set;
} }
public readonly List<ISerializableEntity> Targets; public readonly List<ISerializableEntity> Targets;
public Character User { get; private set; }
private volatile Character _user;
public Character User
{
get => _user;
private set => _user = value;
}
public float Timer; private volatile float _timer;
public float Timer
{
get => _timer;
set => _timer = value;
}
public DurationListElement(StatusEffect parentEffect, Entity parentEntity, IEnumerable<ISerializableEntity> targets, float duration, Character user) public DurationListElement(StatusEffect parentEffect, Entity parentEntity, IEnumerable<ISerializableEntity> targets, float duration, Character user)
{ {
Id = Interlocked.Increment(ref StatusEffect._durationElementIdCounter);
Parent = parentEffect; Parent = parentEffect;
Entity = parentEntity; Entity = parentEntity;
Targets = new List<ISerializableEntity>(targets); Targets = new List<ISerializableEntity>(targets);
@@ -39,8 +54,9 @@ namespace Barotrauma
public void Reset(float duration, Character newUser) public void Reset(float duration, Character newUser)
{ {
Timer = Duration = duration; Duration = duration;
User = newUser; Volatile.Write(ref _timer, duration);
_user = newUser;
} }
} }
@@ -601,14 +617,23 @@ namespace Barotrauma
private readonly float lifeTime; private readonly float lifeTime;
private float lifeTimer; private float lifeTimer;
private Dictionary<Entity, float> intervalTimers; private ConcurrentDictionary<Entity, float> intervalTimers;
/// <summary> /// <summary>
/// Makes the effect only execute once. After it has executed, it'll never execute again (during the same round). /// Makes the effect only execute once. After it has executed, it'll never execute again (during the same round).
/// </summary> /// </summary>
private readonly bool oneShot; private readonly bool oneShot;
public static readonly List<DurationListElement> DurationList = new List<DurationListElement>(); // Thread-safe counter for generating unique IDs for DurationListElement
internal static long _durationElementIdCounter;
// Thread-safe dictionary for duration effects
public static readonly ConcurrentDictionary<long, DurationListElement> DurationListDict = new ConcurrentDictionary<long, DurationListElement>();
/// <summary>
/// Provides a thread-safe enumerable view of the duration list for iteration.
/// </summary>
public static IEnumerable<DurationListElement> DurationList => DurationListDict.Values;
/// <summary> /// <summary>
/// Only applicable for StatusEffects with a duration or delay. Should the conditional checks only be done when the effect triggers, /// Only applicable for StatusEffects with a duration or delay. Should the conditional checks only be done when the effect triggers,
@@ -1624,22 +1649,52 @@ namespace Barotrauma
} }
} }
private static readonly List<Entity> intervalsToRemove = new List<Entity>(); // Thread-local list to avoid contention when cleaning up removed entities
[ThreadStatic]
private static List<Entity> _threadLocalIntervalsToRemove;
private static List<Entity> IntervalsToRemove
{
get
{
_threadLocalIntervalsToRemove ??= new List<Entity>();
return _threadLocalIntervalsToRemove;
}
}
public bool ShouldWaitForInterval(Entity entity, float deltaTime) public bool ShouldWaitForInterval(Entity entity, float deltaTime)
{ {
if (Interval > 0.0f && entity != null && intervalTimers != null) if (Interval > 0.0f && entity != null)
{ {
if (intervalTimers.ContainsKey(entity)) // Thread-safe lazy initialization
if (intervalTimers == null)
{ {
intervalTimers[entity] -= deltaTime; Interlocked.CompareExchange(ref intervalTimers, new ConcurrentDictionary<Entity, float>(), null);
if (intervalTimers[entity] > 0.0f) { return true; }
} }
intervalsToRemove.Clear();
intervalsToRemove.AddRange(intervalTimers.Keys.Where(e => e.Removed)); if (intervalTimers.TryGetValue(entity, out float currentTimer))
foreach (var toRemove in intervalsToRemove)
{ {
intervalTimers.Remove(toRemove); float newTimer = currentTimer - deltaTime;
if (newTimer > 0.0f)
{
intervalTimers.AddOrUpdate(entity, newTimer, (_, __) => newTimer);
return true;
}
}
// Clean up removed entities using thread-local list
var toRemove = IntervalsToRemove;
toRemove.Clear();
foreach (var key in intervalTimers.Keys)
{
if (key.Removed)
{
toRemove.Add(key);
}
}
foreach (var key in toRemove)
{
intervalTimers.TryRemove(key, out _);
} }
} }
return false; return false;
@@ -1655,7 +1710,7 @@ namespace Barotrauma
if (Duration > 0.0f && !Stackable) if (Duration > 0.0f && !Stackable)
{ {
//ignore if not stackable and there's already an identical statuseffect //ignore if not stackable and there's already an identical statuseffect
DurationListElement existingEffect = DurationList.Find(d => d.Parent == this && d.Targets.FirstOrDefault() == target); DurationListElement existingEffect = FindExistingDurationEffect(target);
if (existingEffect != null) if (existingEffect != null)
{ {
if (ResetDurationWhenReapplied) if (ResetDurationWhenReapplied)
@@ -1666,30 +1721,74 @@ namespace Barotrauma
} }
} }
currentTargets.Clear(); var targets = CurrentTargets;
currentTargets.Add(target); targets.Clear();
if (!HasRequiredConditions(currentTargets)) { return; } targets.Add(target);
Apply(deltaTime, entity, currentTargets, worldPosition); if (!HasRequiredConditions(targets)) { return; }
Apply(deltaTime, entity, targets, worldPosition);
} }
protected readonly List<ISerializableEntity> currentTargets = new List<ISerializableEntity>(); // Thread-local list to avoid contention when collecting targets
[ThreadStatic]
private static List<ISerializableEntity> _threadLocalCurrentTargets;
protected List<ISerializableEntity> CurrentTargets
{
get
{
_threadLocalCurrentTargets ??= new List<ISerializableEntity>();
return _threadLocalCurrentTargets;
}
}
/// <summary>
/// Thread-safe method to find an existing duration effect for a single target.
/// </summary>
private DurationListElement FindExistingDurationEffect(ISerializableEntity target)
{
foreach (var element in DurationListDict.Values)
{
if (element.Parent == this && element.Targets.FirstOrDefault() == target)
{
return element;
}
}
return null;
}
/// <summary>
/// Thread-safe method to find an existing duration effect for multiple targets.
/// </summary>
private DurationListElement FindExistingDurationEffect(IReadOnlyList<ISerializableEntity> targets)
{
foreach (var element in DurationListDict.Values)
{
if (element.Parent == this && element.Targets.SequenceEqual(targets))
{
return element;
}
}
return null;
}
public virtual void Apply(ActionType type, float deltaTime, Entity entity, IReadOnlyList<ISerializableEntity> targets, Vector2? worldPosition = null) public virtual void Apply(ActionType type, float deltaTime, Entity entity, IReadOnlyList<ISerializableEntity> targets, Vector2? worldPosition = null)
{ {
if (Disabled) { return; } if (Disabled) { return; }
if (this.type != type) { return; } if (this.type != type) { return; }
if (ShouldWaitForInterval(entity, deltaTime)) { return; } if (ShouldWaitForInterval(entity, deltaTime)) { return; }
currentTargets.Clear(); var localTargets = CurrentTargets;
localTargets.Clear();
foreach (ISerializableEntity target in targets) foreach (ISerializableEntity target in targets)
{ {
if (!IsValidTarget(target)) { continue; } if (!IsValidTarget(target)) { continue; }
currentTargets.Add(target); localTargets.Add(target);
} }
if (TargetIdentifiers != null && currentTargets.Count == 0) { return; } if (TargetIdentifiers != null && localTargets.Count == 0) { return; }
bool hasRequiredItems = HasRequiredItems(entity); bool hasRequiredItems = HasRequiredItems(entity);
if (!hasRequiredItems || !HasRequiredConditions(currentTargets)) if (!hasRequiredItems || !HasRequiredConditions(localTargets))
{ {
#if CLIENT #if CLIENT
if (!hasRequiredItems && playSoundOnRequiredItemFailure) if (!hasRequiredItems && playSoundOnRequiredItemFailure)
@@ -1703,15 +1802,15 @@ namespace Barotrauma
if (Duration > 0.0f && !Stackable) if (Duration > 0.0f && !Stackable)
{ {
//ignore if not stackable and there's already an identical statuseffect //ignore if not stackable and there's already an identical statuseffect
DurationListElement existingEffect = DurationList.Find(d => d.Parent == this && d.Targets.SequenceEqual(currentTargets)); DurationListElement existingEffect = FindExistingDurationEffect(localTargets);
if (existingEffect != null) if (existingEffect != null)
{ {
existingEffect?.Reset(Math.Max(existingEffect.Timer, Duration), user); existingEffect.Reset(Math.Max(existingEffect.Timer, Duration), user);
return; return;
} }
} }
Apply(deltaTime, entity, currentTargets, worldPosition); Apply(deltaTime, entity, localTargets, worldPosition);
} }
private Hull GetHull(Entity entity) private Hull GetHull(Entity entity)
@@ -1924,7 +2023,8 @@ namespace Barotrauma
if (Duration > 0.0f) if (Duration > 0.0f)
{ {
DurationList.Add(new DurationListElement(this, entity, targets, Duration, user)); var element = new DurationListElement(this, entity, targets, Duration, user);
DurationListDict.TryAdd(element.Id, element);
} }
else else
{ {
@@ -2452,7 +2552,7 @@ namespace Barotrauma
} }
if (Interval > 0.0f && entity != null) if (Interval > 0.0f && entity != null)
{ {
intervalTimers ??= new Dictionary<Entity, float>(); intervalTimers ??= new ConcurrentDictionary<Entity, float>();
intervalTimers[entity] = Interval; intervalTimers[entity] = Interval;
} }
} }
@@ -2849,13 +2949,15 @@ namespace Barotrauma
UpdateAllProjSpecific(deltaTime); UpdateAllProjSpecific(deltaTime);
DelayedEffect.Update(deltaTime); DelayedEffect.Update(deltaTime);
for (int i = DurationList.Count - 1; i >= 0; i--)
// Thread-safe iteration over ConcurrentDictionary
foreach (var kvp in DurationListDict)
{ {
DurationListElement element = DurationList[i]; DurationListElement element = kvp.Value;
if (element.Parent.CheckConditionalAlways && !element.Parent.HasRequiredConditions(element.Targets)) if (element.Parent.CheckConditionalAlways && !element.Parent.HasRequiredConditions(element.Targets))
{ {
DurationList.RemoveAt(i); DurationListDict.TryRemove(element.Id, out _);
continue; continue;
} }
@@ -2864,7 +2966,7 @@ namespace Barotrauma
(t is Limb limb && (limb.character == null || limb.character.Removed))); (t is Limb limb && (limb.character == null || limb.character.Removed)));
if (element.Targets.Count == 0) if (element.Targets.Count == 0)
{ {
DurationList.RemoveAt(i); DurationListDict.TryRemove(element.Id, out _);
continue; continue;
} }
@@ -2959,7 +3061,7 @@ namespace Barotrauma
element.Timer -= deltaTime; element.Timer -= deltaTime;
if (element.Timer > 0.0f) { continue; } if (element.Timer > 0.0f) { continue; }
DurationList.Remove(element); DurationListDict.TryRemove(element.Id, out _);
} }
} }
@@ -3059,8 +3161,8 @@ namespace Barotrauma
public static void StopAll() public static void StopAll()
{ {
CoroutineManager.StopCoroutines("statuseffect"); CoroutineManager.StopCoroutines("statuseffect");
DelayedEffect.DelayList.Clear(); DelayedEffect.DelayListDict.Clear();
DurationList.Clear(); DurationListDict.Clear();
} }
public void AddTag(Identifier tag) public void AddTag(Identifier tag)