diff --git a/Barotrauma/BarotraumaShared/SharedSource/Characters/AI/AITarget.cs b/Barotrauma/BarotraumaShared/SharedSource/Characters/AI/AITarget.cs
index 6501c1333..8c246a8c5 100644
--- a/Barotrauma/BarotraumaShared/SharedSource/Characters/AI/AITarget.cs
+++ b/Barotrauma/BarotraumaShared/SharedSource/Characters/AI/AITarget.cs
@@ -1,13 +1,67 @@
using Microsoft.Xna.Framework;
using System;
using System.Collections.Generic;
+using System.Linq;
+using System.Threading;
using System.Xml.Linq;
namespace Barotrauma
{
+ ///
+ /// Thread-safe wrapper for AITarget list operations.
+ /// Uses copy-on-write pattern for lock-free reads.
+ ///
+ class ThreadSafeAITargetList : IEnumerable
+ {
+ private volatile List _list = new List();
+ private readonly object _writeLock = new object();
+
+ public int Count => _list.Count;
+
+ public void Add(AITarget target)
+ {
+ lock (_writeLock)
+ {
+ var newList = new List(_list) { target };
+ Interlocked.Exchange(ref _list, newList);
+ }
+ }
+
+ public bool Remove(AITarget target)
+ {
+ lock (_writeLock)
+ {
+ var newList = new List(_list);
+ bool removed = newList.Remove(target);
+ if (removed)
+ {
+ Interlocked.Exchange(ref _list, newList);
+ }
+ return removed;
+ }
+ }
+
+ public void Clear()
+ {
+ Interlocked.Exchange(ref _list, new List());
+ }
+
+ public bool Contains(AITarget target) => _list.Contains(target);
+
+ public AITarget this[int index] => _list[index];
+
+ public IEnumerator GetEnumerator() => _list.GetEnumerator();
+ System.Collections.IEnumerator System.Collections.IEnumerable.GetEnumerator() => GetEnumerator();
+
+ public List ToList() => new List(_list);
+ public AITarget FirstOrDefault(Func predicate) => _list.FirstOrDefault(predicate);
+ public IEnumerable Where(Func predicate) => _list.Where(predicate);
+ public bool Any(Func predicate) => _list.Any(predicate);
+ }
+
partial class AITarget
{
- public static List List = new List();
+ public static ThreadSafeAITargetList List = new ThreadSafeAITargetList();
private Entity entity;
public Entity Entity
diff --git a/Barotrauma/BarotraumaShared/SharedSource/Characters/AI/HumanAIController.cs b/Barotrauma/BarotraumaShared/SharedSource/Characters/AI/HumanAIController.cs
index e84df22f7..4479bdb81 100644
--- a/Barotrauma/BarotraumaShared/SharedSource/Characters/AI/HumanAIController.cs
+++ b/Barotrauma/BarotraumaShared/SharedSource/Characters/AI/HumanAIController.cs
@@ -5,6 +5,7 @@ using Microsoft.Xna.Framework;
using System;
using System.Collections.Generic;
using System.Linq;
+using System.Threading;
namespace Barotrauma
{
@@ -1817,7 +1818,9 @@ namespace Barotrauma
public static bool HasDivingMask(Character character, float conditionPercentage = 0, bool requireOxygenTank = true)
=> HasItem(character, Tags.LightDivingGear, out _, requireOxygenTank ? Tags.OxygenSource : Identifier.Empty, conditionPercentage, requireEquipped: true);
- private static List- matchingItems = new List
- ();
+ // ThreadLocal to ensure thread safety - each thread gets its own list instance
+ private static readonly ThreadLocal
> matchingItemsLocal = new ThreadLocal>(() => new List- ());
+ private static List
- matchingItems => matchingItemsLocal.Value;
///
/// Note: uses a single list for matching items. The item is reused each time when the method is called. So if you use the method twice, and then refer to the first items, you'll actually get the second.
@@ -1825,15 +1828,16 @@ namespace Barotrauma
///
public static bool HasItem(Character character, Identifier tagOrIdentifier, out IEnumerable
- items, Identifier containedTag = default, float conditionPercentage = 0, bool requireEquipped = false, bool recursive = true, Func
- predicate = null)
{
- matchingItems.Clear();
- items = matchingItems;
+ var localMatchingItems = matchingItems;
+ localMatchingItems.Clear();
+ items = localMatchingItems;
if (character?.Inventory == null) { return false; }
- matchingItems = character.Inventory.FindAllItems(i => (i.Prefab.Identifier == tagOrIdentifier || i.HasTag(tagOrIdentifier)) &&
+ character.Inventory.FindAllItems(i => (i.Prefab.Identifier == tagOrIdentifier || i.HasTag(tagOrIdentifier)) &&
i.ConditionPercentage >= conditionPercentage &&
(!requireEquipped || character.HasEquippedItem(i)) &&
- (predicate == null || predicate(i)), recursive, matchingItems);
- items = matchingItems;
- foreach (var item in matchingItems)
+ (predicate == null || predicate(i)), recursive, localMatchingItems);
+ items = localMatchingItems;
+ foreach (var item in localMatchingItems)
{
if (item == null) { continue; }
diff --git a/Barotrauma/BarotraumaShared/SharedSource/Characters/AI/NPCConversation.cs b/Barotrauma/BarotraumaShared/SharedSource/Characters/AI/NPCConversation.cs
index d1ca09d38..e1f2842fe 100644
--- a/Barotrauma/BarotraumaShared/SharedSource/Characters/AI/NPCConversation.cs
+++ b/Barotrauma/BarotraumaShared/SharedSource/Characters/AI/NPCConversation.cs
@@ -1,4 +1,5 @@
using System;
+using System.Collections.Concurrent;
using System.Collections.Generic;
using Barotrauma.IO;
using System.Linq;
@@ -9,7 +10,8 @@ namespace Barotrauma
{
class NPCConversationCollection : Prefab
{
- public static readonly Dictionary> Collections = new Dictionary>();
+ // Thread-safe dictionary for language-based collections
+ public static readonly ConcurrentDictionary> Collections = new ConcurrentDictionary>();
public readonly LanguageIdentifier Language;
@@ -160,7 +162,24 @@ namespace Barotrauma
return currentFlags;
}
- private static readonly List previousConversations = new List();
+ // Thread-safe previous conversations tracking using copy-on-write pattern
+ private static volatile List _previousConversations = new List();
+ private static readonly object _previousConversationsLock = new object();
+ private static List previousConversations => _previousConversations;
+
+ private static void AddToPreviousConversations(NPCConversation conversation)
+ {
+ lock (_previousConversationsLock)
+ {
+ var newList = new List(_previousConversations);
+ newList.Insert(0, conversation);
+ if (newList.Count > MaxPreviousConversations)
+ {
+ newList.RemoveAt(MaxPreviousConversations);
+ }
+ _previousConversations = newList;
+ }
+ }
public static List<(Character speaker, string line)> CreateRandom(List availableSpeakers)
{
@@ -281,8 +300,7 @@ namespace Barotrauma
if (baseConversation == null)
{
- previousConversations.Insert(0, selectedConversation);
- if (previousConversations.Count > MaxPreviousConversations) previousConversations.RemoveAt(MaxPreviousConversations);
+ AddToPreviousConversations(selectedConversation);
}
lineList.Add((speaker, selectedConversation.Line));
CreateConversation(availableSpeakers, assignedSpeakers, selectedConversation, lineList, availableConversations);
diff --git a/Barotrauma/BarotraumaShared/SharedSource/Characters/AI/Objectives/AIObjectiveFindThieves.cs b/Barotrauma/BarotraumaShared/SharedSource/Characters/AI/Objectives/AIObjectiveFindThieves.cs
index 59601f26e..b764a781e 100644
--- a/Barotrauma/BarotraumaShared/SharedSource/Characters/AI/Objectives/AIObjectiveFindThieves.cs
+++ b/Barotrauma/BarotraumaShared/SharedSource/Characters/AI/Objectives/AIObjectiveFindThieves.cs
@@ -1,5 +1,6 @@
#nullable enable
using Microsoft.Xna.Framework;
+using System.Collections.Concurrent;
using System.Collections.Generic;
using System.Linq;
using Barotrauma.Items.Components;
@@ -68,8 +69,9 @@ namespace Barotrauma
///
/// When did the character last inspect whether some other character has stolen items on them?
+ /// Thread-safe dictionary for concurrent access.
///
- private static readonly Dictionary lastInspectionTimes = new Dictionary();
+ private static readonly ConcurrentDictionary lastInspectionTimes = new ConcurrentDictionary();
private const float NormalInspectionInterval = 120.0f;
private const float CriminalInspectionInterval = 30.0f;
diff --git a/Barotrauma/BarotraumaShared/SharedSource/Characters/Animation/Ragdoll.cs b/Barotrauma/BarotraumaShared/SharedSource/Characters/Animation/Ragdoll.cs
index 5b7d36724..74ddd1820 100644
--- a/Barotrauma/BarotraumaShared/SharedSource/Characters/Animation/Ragdoll.cs
+++ b/Barotrauma/BarotraumaShared/SharedSource/Characters/Animation/Ragdoll.cs
@@ -5,8 +5,10 @@ using FarseerPhysics.Dynamics.Contacts;
using FarseerPhysics.Dynamics.Joints;
using Microsoft.Xna.Framework;
using System;
+using System.Collections.Concurrent;
using System.Collections.Generic;
using System.Linq;
+using System.Threading;
using System.Xml.Linq;
using Barotrauma.Extensions;
using LimbParams = Barotrauma.RagdollParams.LimbParams;
@@ -25,7 +27,33 @@ namespace Barotrauma
///
const float MaxImpactDamage = 0.1f;
- private static readonly List list = new List();
+ // Thread-safe list using copy-on-write pattern (ConcurrentBag doesn't support indexer/Remove)
+ private static volatile List _list = new List();
+ private static readonly object _listLock = new object();
+ private static List list => _list;
+
+ private static void ListAdd(Ragdoll ragdoll)
+ {
+ lock (_listLock)
+ {
+ var newList = new List(_list) { ragdoll };
+ Interlocked.Exchange(ref _list, newList);
+ }
+ }
+
+ private static bool ListRemove(Ragdoll ragdoll)
+ {
+ lock (_listLock)
+ {
+ var newList = new List(_list);
+ bool removed = newList.Remove(ragdoll);
+ if (removed)
+ {
+ Interlocked.Exchange(ref _list, newList);
+ }
+ return removed;
+ }
+ }
struct Impact
{
@@ -45,7 +73,8 @@ namespace Barotrauma
}
}
- private readonly Queue impactQueue = new Queue();
+ // Thread-safe queue for physics collision callbacks
+ private readonly ConcurrentQueue impactQueue = new ConcurrentQueue();
protected Hull currentHull;
@@ -467,7 +496,7 @@ namespace Barotrauma
public Ragdoll(Character character, string seed, RagdollParams ragdollParams = null)
{
- list.Add(this);
+ ListAdd(this);
this.character = character;
Recreate(ragdollParams ?? RagdollParams);
}
@@ -744,10 +773,7 @@ namespace Barotrauma
{
if (!f2.IsSensor)
{
- lock (impactQueue)
- {
- impactQueue.Enqueue(new Impact(f1, f2, contact, velocity));
- }
+ impactQueue.Enqueue(new Impact(f1, f2, contact, velocity));
}
return true;
}
@@ -819,10 +845,7 @@ namespace Barotrauma
}
}
- lock (impactQueue)
- {
- impactQueue.Enqueue(new Impact(f1, f2, contact, velocity));
- }
+ impactQueue.Enqueue(new Impact(f1, f2, contact, velocity));
return true;
}
@@ -1274,9 +1297,8 @@ namespace Barotrauma
{
if (!character.Enabled || character.Removed || Frozen || Invalid || Collider == null || Collider.Removed) { return; }
- while (impactQueue.Count > 0)
+ while (impactQueue.TryDequeue(out var impact))
{
- var impact = impactQueue.Dequeue();
ApplyImpact(impact.F1, impact.F2, impact.LocalNormal, impact.ImpactPos, impact.Velocity);
}
@@ -2325,7 +2347,7 @@ namespace Barotrauma
LimbJoints = null;
}
- list.Remove(this);
+ ListRemove(this);
}
public static void RemoveAll()
diff --git a/Barotrauma/BarotraumaShared/SharedSource/Characters/Character.cs b/Barotrauma/BarotraumaShared/SharedSource/Characters/Character.cs
index 2424f966f..b0e7aff11 100644
--- a/Barotrauma/BarotraumaShared/SharedSource/Characters/Character.cs
+++ b/Barotrauma/BarotraumaShared/SharedSource/Characters/Character.cs
@@ -7,10 +7,12 @@ using FarseerPhysics;
using FarseerPhysics.Dynamics;
using Microsoft.Xna.Framework;
using System;
+using System.Collections.Concurrent;
using System.Collections.Generic;
using System.Collections.Immutable;
using System.Diagnostics;
using System.Linq;
+using System.Threading;
using System.Xml.Linq;
#if SERVER
using System.Text;
@@ -28,12 +30,70 @@ namespace Barotrauma
public readonly record struct TalentResistanceIdentifier(Identifier ResistanceIdentifier, Identifier TalentIdentifier);
+ ///
+ /// Thread-safe wrapper for character list operations.
+ /// Provides lock-free read operations and synchronized write operations.
+ ///
+ class ThreadSafeCharacterList : IEnumerable
+ {
+ private volatile List _list = new List();
+ private readonly object _writeLock = new object();
+
+ public int Count => _list.Count;
+
+ public void Add(Character character)
+ {
+ lock (_writeLock)
+ {
+ var newList = new List(_list) { character };
+ Interlocked.Exchange(ref _list, newList);
+ }
+ }
+
+ public bool Remove(Character character)
+ {
+ lock (_writeLock)
+ {
+ var newList = new List(_list);
+ bool removed = newList.Remove(character);
+ if (removed)
+ {
+ Interlocked.Exchange(ref _list, newList);
+ }
+ return removed;
+ }
+ }
+
+ public void Clear()
+ {
+ Interlocked.Exchange(ref _list, new List());
+ }
+
+ public bool Contains(Character character) => _list.Contains(character);
+
+ public Character this[int index] => _list[index];
+
+ public IEnumerator GetEnumerator() => _list.GetEnumerator();
+ System.Collections.IEnumerator System.Collections.IEnumerable.GetEnumerator() => GetEnumerator();
+
+ // LINQ-friendly snapshot for complex queries
+ public List ToList() => new List(_list);
+
+ public Character FirstOrDefault(Func predicate) => _list.FirstOrDefault(predicate);
+ public Character Find(Predicate predicate) => _list.Find(predicate);
+ public List FindAll(Predicate predicate) => _list.FindAll(predicate);
+ public IEnumerable Where(Func predicate) => _list.Where(predicate);
+ public bool Any(Func predicate) => _list.Any(predicate);
+ public bool None(Func predicate) => !_list.Any(predicate);
+ public int CountWhere(Func predicate) => _list.Count(predicate);
+ }
+
partial class Character : Entity, IDamageable, ISerializableEntity, IClientSerializable, IServerPositionSync
{
- public static readonly List CharacterList = new List();
+ public static readonly ThreadSafeCharacterList CharacterList = new ThreadSafeCharacterList();
public static int CharacterUpdateInterval = 1;
- private static int characterUpdateTick = 1;
+ private static volatile int characterUpdateTick = 1;
public const float MaxHighlightDistance = 150.0f;
public const float MaxDragDistance = 200.0f;
diff --git a/Barotrauma/BarotraumaShared/SharedSource/Characters/Health/CharacterHealth.cs b/Barotrauma/BarotraumaShared/SharedSource/Characters/Health/CharacterHealth.cs
index 13f91414a..a4c538451 100644
--- a/Barotrauma/BarotraumaShared/SharedSource/Characters/Health/CharacterHealth.cs
+++ b/Barotrauma/BarotraumaShared/SharedSource/Characters/Health/CharacterHealth.cs
@@ -3,15 +3,13 @@ using Barotrauma.Extensions;
using Barotrauma.Networking;
using Microsoft.Xna.Framework;
using System;
+using System.Collections.Concurrent;
using System.Collections.Generic;
using System.Globalization;
using System.Linq;
+using System.Threading;
using System.Xml.Linq;
-using Barotrauma.Networking;
-using Barotrauma.Extensions;
-using System.Globalization;
using MoonSharp.Interpreter;
-using Barotrauma.Abilities;
namespace Barotrauma
{
@@ -132,8 +130,9 @@ namespace Barotrauma
private readonly List limbHealths = new List();
- private readonly Dictionary afflictions = new Dictionary();
- private readonly HashSet irremovableAfflictions = new HashSet();
+ // Thread-safe afflictions dictionary for concurrent access
+ private readonly ConcurrentDictionary afflictions = new ConcurrentDictionary();
+ private readonly ConcurrentDictionary irremovableAfflictions = new ConcurrentDictionary();
private Affliction bloodlossAffliction;
private Affliction oxygenLowAffliction;
private Affliction pressureAffliction;
@@ -324,13 +323,13 @@ namespace Barotrauma
private void InitIrremovableAfflictions()
{
- irremovableAfflictions.Add(bloodlossAffliction = new Affliction(AfflictionPrefab.Bloodloss, 0.0f));
- irremovableAfflictions.Add(stunAffliction = new Affliction(AfflictionPrefab.Stun, 0.0f));
- irremovableAfflictions.Add(pressureAffliction = new Affliction(AfflictionPrefab.Pressure, 0.0f));
- irremovableAfflictions.Add(oxygenLowAffliction = new Affliction(AfflictionPrefab.OxygenLow, 0.0f));
- foreach (Affliction affliction in irremovableAfflictions)
+ irremovableAfflictions.TryAdd(bloodlossAffliction = new Affliction(AfflictionPrefab.Bloodloss, 0.0f), 0);
+ irremovableAfflictions.TryAdd(stunAffliction = new Affliction(AfflictionPrefab.Stun, 0.0f), 0);
+ irremovableAfflictions.TryAdd(pressureAffliction = new Affliction(AfflictionPrefab.Pressure, 0.0f), 0);
+ irremovableAfflictions.TryAdd(oxygenLowAffliction = new Affliction(AfflictionPrefab.OxygenLow, 0.0f), 0);
+ foreach (Affliction affliction in irremovableAfflictions.Keys)
{
- afflictions.Add(affliction, null);
+ afflictions.TryAdd(affliction, null);
}
}
@@ -338,7 +337,7 @@ namespace Barotrauma
public IReadOnlyCollection GetAllAfflictions()
{
- return afflictions.Keys;
+ return afflictions.Keys.ToList();
}
public IEnumerable GetAllAfflictions(Func limbHealthFilter)
@@ -503,19 +502,18 @@ namespace Barotrauma
///
public float GetResistance(AfflictionPrefab afflictionPrefab, LimbType limbType)
{
- lock (afflictions) {
- // This is a % resistance (0 to 1.0)
- float resistance = 0.0f;
- foreach (KeyValuePair kvp in afflictions)
- {
- var affliction = kvp.Key;
- resistance += affliction.GetResistance(afflictionPrefab.Identifier, limbType);
- }
- // This is a multiplier, ie. 0.0 = 100% resistance and 1.0 = 0% resistance
- float abilityResistanceMultiplier = Character.GetAbilityResistance(afflictionPrefab);
- // The returned value is calculated to be a % resistance again
- return 1 - ((1 - resistance) * abilityResistanceMultiplier);
+ // ConcurrentDictionary is thread-safe, no lock needed
+ // This is a % resistance (0 to 1.0)
+ float resistance = 0.0f;
+ foreach (KeyValuePair kvp in afflictions)
+ {
+ var affliction = kvp.Key;
+ resistance += affliction.GetResistance(afflictionPrefab.Identifier, limbType);
}
+ // This is a multiplier, ie. 0.0 = 100% resistance and 1.0 = 0% resistance
+ float abilityResistanceMultiplier = Character.GetAbilityResistance(afflictionPrefab);
+ // The returned value is calculated to be a % resistance again
+ return 1 - ((1 - resistance) * abilityResistanceMultiplier);
}
public float GetStatValue(StatTypes statType)
@@ -696,14 +694,14 @@ namespace Barotrauma
a.Prefab.AfflictionType == AfflictionPrefab.Bleeding.AfflictionType));
foreach (var affliction in afflictionsToRemove)
{
- afflictions.Remove(affliction);
+ afflictions.TryRemove(affliction, out _);
}
foreach (LimbHealth limbHealth in limbHealths)
{
- if (damageAmount > 0.0f) { afflictions.Add(AfflictionPrefab.InternalDamage.Instantiate(damageAmount), limbHealth); }
- if (bleedingDamageAmount > 0.0f && DoesBleed) { afflictions.Add(AfflictionPrefab.Bleeding.Instantiate(bleedingDamageAmount), limbHealth); }
- if (burnDamageAmount > 0.0f) { afflictions.Add(AfflictionPrefab.Burn.Instantiate(burnDamageAmount), limbHealth); }
+ if (damageAmount > 0.0f) { afflictions.TryAdd(AfflictionPrefab.InternalDamage.Instantiate(damageAmount), limbHealth); }
+ if (bleedingDamageAmount > 0.0f && DoesBleed) { afflictions.TryAdd(AfflictionPrefab.Bleeding.Instantiate(bleedingDamageAmount), limbHealth); }
+ if (burnDamageAmount > 0.0f) { afflictions.TryAdd(AfflictionPrefab.Burn.Instantiate(burnDamageAmount), limbHealth); }
}
RecalculateVitality();
@@ -743,7 +741,7 @@ namespace Barotrauma
afflictionsToRemove.AddRange(afflictions.Keys.Where(affliction => predicate(affliction)));
foreach (var affliction in afflictionsToRemove)
{
- afflictions.Remove(affliction);
+ afflictions.TryRemove(affliction, out _);
}
CalculateVitality();
}
@@ -751,14 +749,14 @@ namespace Barotrauma
public void RemoveAllAfflictions()
{
afflictionsToRemove.Clear();
- afflictionsToRemove.AddRange(afflictions.Keys.Where(a => !irremovableAfflictions.Contains(a)));
+ afflictionsToRemove.AddRange(afflictions.Keys.Where(a => !irremovableAfflictions.ContainsKey(a)));
foreach (var affliction in afflictionsToRemove)
{
//set strength to 0 in case the affliction needs to react to becoming inactive
affliction.Strength = 0.0f;
- afflictions.Remove(affliction);
+ afflictions.TryRemove(affliction, out _);
}
- foreach (Affliction affliction in irremovableAfflictions)
+ foreach (Affliction affliction in irremovableAfflictions.Keys)
{
affliction.Strength = 0.0f;
}
@@ -769,15 +767,15 @@ namespace Barotrauma
{
afflictionsToRemove.Clear();
afflictionsToRemove.AddRange(afflictions.Keys.Where(a =>
- !irremovableAfflictions.Contains(a) &&
+ !irremovableAfflictions.ContainsKey(a) &&
!a.Prefab.IsBuff &&
a.Prefab.AfflictionType != "geneticmaterialbuff" &&
a.Prefab.AfflictionType != "geneticmaterialdebuff"));
foreach (var affliction in afflictionsToRemove)
{
- afflictions.Remove(affliction);
+ afflictions.TryRemove(affliction, out _);
}
- foreach (Affliction affliction in irremovableAfflictions)
+ foreach (Affliction affliction in irremovableAfflictions.Keys)
{
affliction.Strength = 0.0f;
}
@@ -869,7 +867,7 @@ namespace Barotrauma
var copyAffliction = newAffliction.Prefab.Instantiate(
Math.Min(newAffliction.Prefab.MaxStrength, newAffliction.Strength * (100.0f / MaxVitality) * (1f - GetResistance(newAffliction.Prefab, limbType))),
newAffliction.Source);
- afflictions.Add(copyAffliction, limbHealth);
+ afflictions.TryAdd(copyAffliction, limbHealth);
AchievementManager.OnAfflictionReceived(copyAffliction, Character);
MedicalClinic.OnAfflictionCountChanged(Character);
@@ -914,7 +912,7 @@ namespace Barotrauma
if (affliction.Strength <= 0.0f)
{
AchievementManager.OnAfflictionRemoved(affliction, Character);
- if (!irremovableAfflictions.Contains(affliction)) { afflictionsToRemove.Add(affliction); }
+ if (!irremovableAfflictions.ContainsKey(affliction)) { afflictionsToRemove.Add(affliction); }
continue;
}
if (affliction.Prefab.Duration > 0.0f)
@@ -952,7 +950,7 @@ namespace Barotrauma
foreach (var affliction in afflictionsToRemove)
{
- afflictions.Remove(affliction);
+ afflictions.TryRemove(affliction, out _);
}
if (afflictionsToRemove.Count is not 0)
@@ -1519,14 +1517,14 @@ namespace Barotrauma
}
if (afflictionPredicate != null && !afflictionPredicate.Invoke(afflictionPrefab)) { return; }
float strength = afflictionElement.GetAttributeFloat("strength", 0.0f);
- var irremovableAffliction = irremovableAfflictions.FirstOrDefault(a => a.Prefab == afflictionPrefab);
+ var irremovableAffliction = irremovableAfflictions.Keys.FirstOrDefault(a => a.Prefab == afflictionPrefab);
if (irremovableAffliction != null)
{
irremovableAffliction.Strength = strength;
}
else
{
- afflictions.Add(afflictionPrefab.Instantiate(strength), limbHealth);
+ afflictions.TryAdd(afflictionPrefab.Instantiate(strength), limbHealth);
}
}
}
diff --git a/Barotrauma/BarotraumaShared/SharedSource/Characters/Params/Animation/AnimationParams.cs b/Barotrauma/BarotraumaShared/SharedSource/Characters/Params/Animation/AnimationParams.cs
index 9954efb09..e6ec490d4 100644
--- a/Barotrauma/BarotraumaShared/SharedSource/Characters/Params/Animation/AnimationParams.cs
+++ b/Barotrauma/BarotraumaShared/SharedSource/Characters/Params/Animation/AnimationParams.cs
@@ -1,10 +1,12 @@
using Microsoft.Xna.Framework;
+using System.Collections.Concurrent;
using System.Collections.Generic;
using System.Globalization;
using Barotrauma.IO;
using System;
using System.Diagnostics;
using System.Linq;
+using System.Threading;
using System.Xml.Linq;
using Barotrauma.Extensions;
@@ -117,8 +119,9 @@ namespace Barotrauma
public virtual AnimationType AnimationType { get; protected set; }
///
/// The cached animations of all the characters that have been loaded.
+ /// Thread-safe cache using ConcurrentDictionary.
///
- private static readonly Dictionary> allAnimations = new Dictionary>();
+ private static readonly ConcurrentDictionary> allAnimations = new ConcurrentDictionary>();
[Header("Movement")]
[Serialize(1.0f, IsPropertySaveable.Yes), Editable(DecimalCount = 2, MinValueFloat = 0, MaxValueFloat = Ragdoll.MAX_SPEED, ValueStep = 0.1f)]
@@ -244,7 +247,9 @@ namespace Barotrauma
return GetAnimParams(speciesName, animSpecies, fallbackSpecies: character.Prefab.GetBaseCharacterSpeciesName(speciesName), animType, file, throwErrors);
}
- private static readonly List errorMessages = new List();
+ // ThreadLocal for thread-safe error message collection during animation loading
+ private static readonly ThreadLocal
> errorMessagesLocal = new ThreadLocal>(() => new List());
+ private static List errorMessages => errorMessagesLocal.Value;
private static T GetAnimParams(Identifier speciesName, Identifier animSpecies, Identifier fallbackSpecies, AnimationType animType, Either file, bool throwErrors = true) where T : AnimationParams, new()
{
@@ -262,11 +267,7 @@ namespace Barotrauma
}
ContentPackage contentPackage = contentPath?.ContentPackage ?? CharacterPrefab.FindBySpeciesName(speciesName)?.ContentPackage;
Debug.Assert(contentPackage != null);
- if (!allAnimations.TryGetValue(speciesName, out Dictionary animations))
- {
- animations = new Dictionary();
- allAnimations.Add(speciesName, animations);
- }
+ var animations = allAnimations.GetOrAdd(speciesName, _ => new ConcurrentDictionary());
string key = fileName ?? contentPath?.Value ?? GetDefaultFileName(animSpecies, animType);
if (animations.TryGetValue(key, out AnimationParams anim) && anim.AnimationType == animType)
{
@@ -418,16 +419,12 @@ namespace Barotrauma
{
throw new Exception("Cannot create an animation file of type " + animationType);
}
- if (!allAnimations.TryGetValue(speciesName, out Dictionary anims))
- {
- anims = new Dictionary();
- allAnimations.Add(speciesName, anims);
- }
+ var anims = allAnimations.GetOrAdd(speciesName, _ => new ConcurrentDictionary());
string fileName = IO.Path.GetFileNameWithoutExtension(fullPath);
if (anims.ContainsKey(fileName))
{
DebugConsole.NewMessage($"[AnimationParams] Removing the old animation of type {animationType}.", Color.Red);
- anims.Remove(fileName);
+ anims.TryRemove(fileName, out _);
}
var instance = new T();
XElement animationElement = new XElement(GetDefaultFileName(speciesName, animationType), new XAttribute("animationtype", animationType.ToString()));
@@ -439,7 +436,7 @@ namespace Barotrauma
instance.IsLoaded = instance.Deserialize(animationElement);
instance.Save();
instance.Load(contentPath, speciesName);
- anims.Add(fileName, instance);
+ anims.TryAdd(fileName, instance);
DebugConsole.NewMessage($"[AnimationParams] New animation file of type {animationType} created.", Color.GhostWhite);
return instance;
}
@@ -467,17 +464,14 @@ namespace Barotrauma
{
// Update the key by removing and re-adding the animation.
string fileName = FileNameWithoutExtension;
- if (allAnimations.TryGetValue(SpeciesName, out Dictionary animations))
+ if (allAnimations.TryGetValue(SpeciesName, out ConcurrentDictionary animations))
{
- animations.Remove(fileName);
+ animations.TryRemove(fileName, out _);
}
base.UpdatePath(newPath);
if (animations != null)
{
- if (!animations.ContainsKey(fileName))
- {
- animations.Add(fileName, this);
- }
+ animations.TryAdd(fileName, this);
}
}
}
diff --git a/Barotrauma/BarotraumaShared/SharedSource/Characters/Params/Ragdoll/RagdollParams.cs b/Barotrauma/BarotraumaShared/SharedSource/Characters/Params/Ragdoll/RagdollParams.cs
index f6094f8e0..f2ef4cc32 100644
--- a/Barotrauma/BarotraumaShared/SharedSource/Characters/Params/Ragdoll/RagdollParams.cs
+++ b/Barotrauma/BarotraumaShared/SharedSource/Characters/Params/Ragdoll/RagdollParams.cs
@@ -1,5 +1,6 @@
using Microsoft.Xna.Framework;
using System;
+using System.Collections.Concurrent;
using System.Collections.Generic;
using System.Diagnostics;
using System.Xml.Linq;
@@ -124,8 +125,9 @@ namespace Barotrauma
/// key1: Species name
/// key2: File path
/// value: Ragdoll parameters
+ /// Thread-safe cache using ConcurrentDictionary.
///
- private static readonly Dictionary> allRagdolls = new Dictionary>();
+ private static readonly ConcurrentDictionary> allRagdolls = new ConcurrentDictionary>();
public List Colliders { get; private set; } = new List();
public List Limbs { get; private set; } = new List();
@@ -222,11 +224,7 @@ namespace Barotrauma
Debug.Assert(!fileName.IsNullOrWhiteSpace() || !contentPath.IsNullOrWhiteSpace());
}
Debug.Assert(contentPackage != null);
- if (!allRagdolls.TryGetValue(speciesName, out Dictionary ragdolls))
- {
- ragdolls = new Dictionary();
- allRagdolls.Add(speciesName, ragdolls);
- }
+ var ragdolls = allRagdolls.GetOrAdd(speciesName, _ => new ConcurrentDictionary());
string key = fileName ?? contentPath?.Value ?? GetDefaultFileName(ragdollSpecies);
if (ragdolls.TryGetValue(key, out RagdollParams ragdoll))
{
@@ -331,10 +329,10 @@ namespace Barotrauma
if (allRagdolls.ContainsKey(speciesName))
{
DebugConsole.NewMessage($"[RagdollParams] Removing the old ragdolls from {speciesName}.", Color.Red);
- allRagdolls.Remove(speciesName);
+ allRagdolls.TryRemove(speciesName, out _);
}
- var ragdolls = new Dictionary();
- allRagdolls.Add(speciesName, ragdolls);
+ var ragdolls = new ConcurrentDictionary();
+ allRagdolls.TryAdd(speciesName, ragdolls);
var instance = new T
{
doc = new XDocument(mainElement)
@@ -345,7 +343,7 @@ namespace Barotrauma
instance.IsLoaded = instance.Deserialize(mainElement);
instance.Save();
instance.Load(contentPath, speciesName);
- ragdolls.Add(instance.FileNameWithoutExtension, instance);
+ ragdolls.TryAdd(instance.FileNameWithoutExtension, instance);
DebugConsole.NewMessage("[RagdollParams] New default ragdoll params successfully created at " + fullPath, Color.NavajoWhite);
return instance;
}
@@ -362,17 +360,14 @@ namespace Barotrauma
{
// Update the key by removing and re-adding the ragdoll.
string fileName = FileNameWithoutExtension;
- if (allRagdolls.TryGetValue(SpeciesName, out Dictionary ragdolls))
+ if (allRagdolls.TryGetValue(SpeciesName, out ConcurrentDictionary ragdolls))
{
- ragdolls.Remove(fileName);
+ ragdolls.TryRemove(fileName, out _);
}
base.UpdatePath(fullPath);
if (ragdolls != null)
{
- if (!ragdolls.ContainsKey(fileName))
- {
- ragdolls.Add(fileName, this);
- }
+ ragdolls.TryAdd(fileName, this);
}
}
}
@@ -1484,4 +1479,4 @@ namespace Barotrauma
}
#endregion
}
-}
\ No newline at end of file
+}
diff --git a/Barotrauma/BarotraumaShared/SharedSource/Characters/Talents/CharacterTalent.cs b/Barotrauma/BarotraumaShared/SharedSource/Characters/Talents/CharacterTalent.cs
index ae7f18849..d16a6fc60 100644
--- a/Barotrauma/BarotraumaShared/SharedSource/Characters/Talents/CharacterTalent.cs
+++ b/Barotrauma/BarotraumaShared/SharedSource/Characters/Talents/CharacterTalent.cs
@@ -1,6 +1,8 @@
using Barotrauma.Abilities;
using System;
+using System.Collections.Concurrent;
using System.Collections.Generic;
+using System.Threading;
namespace Barotrauma
{
@@ -72,7 +74,9 @@ namespace Barotrauma
}
}
- private static readonly HashSet checkedNonStackableTalents = new();
+ // ThreadLocal for thread-safe talent checking
+ private static readonly ThreadLocal> checkedNonStackableTalentsLocal = new ThreadLocal>(() => new HashSet());
+ private static HashSet checkedNonStackableTalents => checkedNonStackableTalentsLocal.Value;
///
/// Checks talents for a given AbilityObject taking into account non-stackable talents.
diff --git a/Barotrauma/BarotraumaShared/SharedSource/ContentManagement/ContentFile/NPCConversationsFile.cs b/Barotrauma/BarotraumaShared/SharedSource/ContentManagement/ContentFile/NPCConversationsFile.cs
index 95ee2d5c5..570b89364 100644
--- a/Barotrauma/BarotraumaShared/SharedSource/ContentManagement/ContentFile/NPCConversationsFile.cs
+++ b/Barotrauma/BarotraumaShared/SharedSource/ContentManagement/ContentFile/NPCConversationsFile.cs
@@ -1,4 +1,4 @@
-using System.Xml.Linq;
+using System.Xml.Linq;
namespace Barotrauma
{
@@ -21,7 +21,7 @@ namespace Barotrauma
var npcConversationCollection = new NPCConversationCollection(this, mainElement);
if (!NPCConversationCollection.Collections.ContainsKey(npcConversationCollection.Language))
{
- NPCConversationCollection.Collections.Add(npcConversationCollection.Language, new PrefabCollection());
+ NPCConversationCollection.Collections.TryAdd(npcConversationCollection.Language, new PrefabCollection());
}
NPCConversationCollection.Collections[npcConversationCollection.Language].Add(npcConversationCollection, allowOverriding);
}
@@ -42,4 +42,4 @@ namespace Barotrauma
}
}
}
-}
\ No newline at end of file
+}
diff --git a/Barotrauma/BarotraumaShared/SharedSource/Items/Components/Signal/DelayComponent.cs b/Barotrauma/BarotraumaShared/SharedSource/Items/Components/Signal/DelayComponent.cs
index 20ec92857..3c9e4e7f8 100644
--- a/Barotrauma/BarotraumaShared/SharedSource/Items/Components/Signal/DelayComponent.cs
+++ b/Barotrauma/BarotraumaShared/SharedSource/Items/Components/Signal/DelayComponent.cs
@@ -1,6 +1,8 @@
using System;
+using System.Collections.Concurrent;
using System.Collections.Generic;
using System.Globalization;
+using System.Linq;
using System.Xml.Linq;
using Microsoft.Xna.Framework;
namespace Barotrauma.Items.Components
@@ -25,7 +27,8 @@ namespace Barotrauma.Items.Components
private int signalQueueSize;
private int delayTicks;
- private readonly Queue signalQueue = new Queue();
+ // Thread-safe queue for concurrent access
+ private readonly ConcurrentQueue signalQueue = new ConcurrentQueue();
private DelayedSignal prevQueuedSignal;
@@ -40,7 +43,8 @@ namespace Barotrauma.Items.Components
delay = value;
delayTicks = (int)(delay / Timing.Step);
signalQueueSize = Math.Max(delayTicks, 1) * 2;
- signalQueue.Clear();
+ // ConcurrentQueue doesn't have Clear(), drain it instead
+ while (signalQueue.TryDequeue(out _)) { }
}
}
@@ -66,19 +70,19 @@ namespace Barotrauma.Items.Components
public override void Update(float deltaTime, Camera cam)
{
- if (signalQueue.Count == 0)
+ if (signalQueue.IsEmpty)
{
IsActive = false;
return;
}
- foreach (var val in signalQueue)
+ // Use ToArray() snapshot for thread-safe iteration
+ foreach (var val in signalQueue.ToArray())
{
val.SendTimer -= 1;
}
- while (signalQueue.Count > 0 && signalQueue.Peek().SendTimer <= 0)
+ while (signalQueue.TryPeek(out var signalOut) && signalOut.SendTimer <= 0)
{
- var signalOut = signalQueue.Peek();
signalOut.SendDuration -= 1;
item.SendSignal(new Signal(signalOut.Signal.value, sender: signalOut.Signal.sender, strength: signalOut.Signal.strength), "signal_out");
if (signalOut.SendDuration <= 0)
@@ -100,11 +104,15 @@ namespace Barotrauma.Items.Components
{
case "signal_in":
if (signalQueue.Count >= signalQueueSize) { return; }
- if (ResetWhenSignalReceived) { prevQueuedSignal = null; signalQueue.Clear(); }
- if (ResetWhenDifferentSignalReceived && signalQueue.Count > 0 && signalQueue.Peek().Signal.value != signal.value)
+ if (ResetWhenSignalReceived)
+ {
+ prevQueuedSignal = null;
+ while (signalQueue.TryDequeue(out _)) { }
+ }
+ if (ResetWhenDifferentSignalReceived && signalQueue.TryPeek(out var peekSignal) && peekSignal.Signal.value != signal.value)
{
prevQueuedSignal = null;
- signalQueue.Clear();
+ while (signalQueue.TryDequeue(out _)) { }
}
if (prevQueuedSignal != null &&
@@ -127,10 +135,10 @@ namespace Barotrauma.Items.Components
if (float.TryParse(signal.value, NumberStyles.Any, CultureInfo.InvariantCulture, out float newDelay))
{
newDelay = MathHelper.Clamp(newDelay, 0, 60);
- if (signalQueue.Count > 0 && newDelay != Delay)
+ if (!signalQueue.IsEmpty && newDelay != Delay)
{
prevQueuedSignal = null;
- signalQueue.Clear();
+ while (signalQueue.TryDequeue(out _)) { }
}
Delay = newDelay;
}