From e167a34f327e7bcdcd29da2fd5596f5b1dc0671b Mon Sep 17 00:00:00 2001 From: Eero Date: Sun, 28 Dec 2025 21:59:03 +0800 Subject: [PATCH] Make entity lists thread-safe with copy-on-write wrappers Replaced static entity lists (e.g., HullList, GapList, MapEntityList, etc.) with thread-safe copy-on-write wrappers to improve concurrency and prevent race conditions. Updated usages and related methods to support the new thread-safe collections, ensuring atomic operations and lock-free reads throughout the codebase. --- .../SharedSource/AchievementManager.cs | 5 +- .../SharedSource/Events/EventManager.cs | 2 +- .../Events/Missions/PirateMission.cs | 2 +- .../Items/Components/Machines/Steering.cs | 2 +- .../Map/Creatures/BallastFloraBehavior.cs | 52 +++++- .../SharedSource/Map/Entity.cs | 11 +- .../BarotraumaShared/SharedSource/Map/Gap.cs | 60 ++++++- .../BarotraumaShared/SharedSource/Map/Hull.cs | 114 +++++++++++- .../SharedSource/Map/MapEntity.cs | 169 ++++++++++++++---- .../SharedSource/Map/Structure.cs | 60 ++++++- .../SharedSource/Map/Submarine.cs | 81 +++++++-- .../SharedSource/Map/WayPoint.cs | 62 ++++++- .../SharedSource/Physics/PhysicsBody.cs | 61 ++++++- 13 files changed, 613 insertions(+), 68 deletions(-) diff --git a/Barotrauma/BarotraumaShared/SharedSource/AchievementManager.cs b/Barotrauma/BarotraumaShared/SharedSource/AchievementManager.cs index a4feacedd..6e06d8b33 100644 --- a/Barotrauma/BarotraumaShared/SharedSource/AchievementManager.cs +++ b/Barotrauma/BarotraumaShared/SharedSource/AchievementManager.cs @@ -156,7 +156,8 @@ namespace Barotrauma Reactor reactor = item.GetComponent(); if (reactor != null && reactor.Item.Condition > 0.0f) { roundData.Reactors.Add(reactor); } } - pathFinder = new PathFinder(WayPoint.WayPointList, false); + + pathFinder = new PathFinder(WayPoint.WayPointList.ToList(), false); cachedDistances.Clear(); #if CLIENT @@ -323,7 +324,7 @@ namespace Barotrauma static CachedDistance CalculateNewCachedDistance(Character c) { - pathFinder ??= new PathFinder(WayPoint.WayPointList, false); + pathFinder ??= new PathFinder(WayPoint.WayPointList.ToList(), false); var path = pathFinder.FindPath(ConvertUnits.ToSimUnits(c.WorldPosition), ConvertUnits.ToSimUnits(Submarine.MainSub.WorldPosition)); if (path.Unreachable) { return null; } return new CachedDistance(c.WorldPosition, Submarine.MainSub.WorldPosition, path.TotalLength, Timing.TotalTime + Rand.Range(1.0f, 5.0f)); diff --git a/Barotrauma/BarotraumaShared/SharedSource/Events/EventManager.cs b/Barotrauma/BarotraumaShared/SharedSource/Events/EventManager.cs index 01009f1db..351d92b75 100644 --- a/Barotrauma/BarotraumaShared/SharedSource/Events/EventManager.cs +++ b/Barotrauma/BarotraumaShared/SharedSource/Events/EventManager.cs @@ -160,7 +160,7 @@ namespace Barotrauma MissionAction.ResetMissionsUnlockedThisRound(); UnlockPathAction.ResetPathsUnlockedThisRound(); #endif - pathFinder = new PathFinder(WayPoint.WayPointList, false); + pathFinder = new PathFinder(WayPoint.WayPointList.ToList(), false); totalPathLength = 0.0f; if (level != null) { diff --git a/Barotrauma/BarotraumaShared/SharedSource/Events/Missions/PirateMission.cs b/Barotrauma/BarotraumaShared/SharedSource/Events/Missions/PirateMission.cs index 6331311af..5a31acbdf 100644 --- a/Barotrauma/BarotraumaShared/SharedSource/Events/Missions/PirateMission.cs +++ b/Barotrauma/BarotraumaShared/SharedSource/Events/Missions/PirateMission.cs @@ -283,7 +283,7 @@ namespace Barotrauma if (!IsClient) { - PathFinder pathFinder = new PathFinder(WayPoint.WayPointList, false); + PathFinder pathFinder = new PathFinder(WayPoint.WayPointList.ToList(), false); var path = pathFinder.FindPath(ConvertUnits.ToSimUnits(patrolPos), ConvertUnits.ToSimUnits(preferredSpawnPos)); if (!path.Unreachable) { diff --git a/Barotrauma/BarotraumaShared/SharedSource/Items/Components/Machines/Steering.cs b/Barotrauma/BarotraumaShared/SharedSource/Items/Components/Machines/Steering.cs index d7130f534..9c1f83b30 100644 --- a/Barotrauma/BarotraumaShared/SharedSource/Items/Components/Machines/Steering.cs +++ b/Barotrauma/BarotraumaShared/SharedSource/Items/Components/Machines/Steering.cs @@ -637,7 +637,7 @@ namespace Barotrauma.Items.Components if (pathFinder == null) { - pathFinder = new PathFinder(WayPoint.WayPointList, false) + pathFinder = new PathFinder(WayPoint.WayPointList.ToList(), false) { GetNodePenalty = GetNodePenalty }; diff --git a/Barotrauma/BarotraumaShared/SharedSource/Map/Creatures/BallastFloraBehavior.cs b/Barotrauma/BarotraumaShared/SharedSource/Map/Creatures/BallastFloraBehavior.cs index 351083203..d6356c63d 100644 --- a/Barotrauma/BarotraumaShared/SharedSource/Map/Creatures/BallastFloraBehavior.cs +++ b/Barotrauma/BarotraumaShared/SharedSource/Map/Creatures/BallastFloraBehavior.cs @@ -4,6 +4,7 @@ using System.Collections.Generic; using System.Diagnostics; using System.Globalization; using System.Linq; +using System.Threading; using System.Xml.Linq; using Barotrauma.Extensions; using Barotrauma.Items.Components; @@ -14,6 +15,55 @@ using Microsoft.Xna.Framework; namespace Barotrauma.MapCreatures.Behavior { + /// + /// Thread-safe wrapper for BallastFloraBehavior list operations. + /// Uses copy-on-write pattern for lock-free reads. + /// + internal class ThreadSafeBallastFloraList : IEnumerable + { + private volatile List _list = new List(); + private readonly object _writeLock = new object(); + + public int Count => _list.Count; + + public void Add(BallastFloraBehavior entity) + { + lock (_writeLock) + { + var newList = new List(_list) { entity }; + Interlocked.Exchange(ref _list, newList); + } + } + + public bool Remove(BallastFloraBehavior entity) + { + lock (_writeLock) + { + var newList = new List(_list); + bool removed = newList.Remove(entity); + if (removed) + { + Interlocked.Exchange(ref _list, newList); + } + return removed; + } + } + + public void Clear() + { + Interlocked.Exchange(ref _list, new List()); + } + + public IEnumerator GetEnumerator() => _list.GetEnumerator(); + System.Collections.IEnumerator System.Collections.IEnumerable.GetEnumerator() => GetEnumerator(); + + // LINQ-friendly methods + public List ToList() => new List(_list); + public bool Any() => _list.Any(); + public bool Any(Func predicate) => _list.Any(predicate); + public IEnumerable Where(Func predicate) => _list.Where(predicate); + } + class BallastFloraBranch : VineTile { public readonly BallastFloraBehavior? ParentBallastFlora; @@ -132,7 +182,7 @@ namespace Barotrauma.MapCreatures.Behavior public List> debugSearchLines = new List>(); #endif - private readonly static List _entityList = new List(); + private readonly static ThreadSafeBallastFloraList _entityList = new ThreadSafeBallastFloraList(); public static IEnumerable EntityList => _entityList; public enum NetworkHeader diff --git a/Barotrauma/BarotraumaShared/SharedSource/Map/Entity.cs b/Barotrauma/BarotraumaShared/SharedSource/Map/Entity.cs index 54b5014d0..85d5d8cef 100644 --- a/Barotrauma/BarotraumaShared/SharedSource/Map/Entity.cs +++ b/Barotrauma/BarotraumaShared/SharedSource/Map/Entity.cs @@ -1,5 +1,6 @@ using Microsoft.Xna.Framework; using System; +using System.Collections.Concurrent; using System.Collections.Generic; using System.Diagnostics; using Barotrauma.IO; @@ -20,10 +21,10 @@ namespace Barotrauma public const ushort MaxEntityCount = ushort.MaxValue - 4; //ushort.MaxValue - 4 because the 4 values above are reserved values - private static readonly Dictionary dictionary = new Dictionary(); + private static readonly ConcurrentDictionary dictionary = new ConcurrentDictionary(); public static IReadOnlyCollection GetEntities() { - return dictionary.Values; + return (IReadOnlyCollection)dictionary.Values; } public static int EntityCount => dictionary.Count; @@ -122,13 +123,11 @@ namespace Barotrauma //give a unique ID ID = DetermineID(id, submarine); - if (dictionary.ContainsKey(ID)) + if (!dictionary.TryAdd(ID, this)) { throw new Exception($"ID {ID} is taken by {dictionary[ID]}"); } - dictionary.Add(ID, this); - CreationStackTrace = ""; #if DEBUG var st = new StackTrace(skipFrames: 2, fNeedFileInfo: true); @@ -324,7 +323,7 @@ namespace Barotrauma } else { - dictionary.Remove(ID); + dictionary.TryRemove(ID, out _); } IdFreed = true; } diff --git a/Barotrauma/BarotraumaShared/SharedSource/Map/Gap.cs b/Barotrauma/BarotraumaShared/SharedSource/Map/Gap.cs index 3fe156958..f617c584a 100644 --- a/Barotrauma/BarotraumaShared/SharedSource/Map/Gap.cs +++ b/Barotrauma/BarotraumaShared/SharedSource/Map/Gap.cs @@ -8,13 +8,71 @@ using System; using System.Collections.Concurrent; using System.Collections.Generic; using System.Linq; +using System.Threading; using System.Xml.Linq; namespace Barotrauma { + /// + /// Thread-safe wrapper for Gap list operations. + /// Uses copy-on-write pattern for lock-free reads. + /// + internal class ThreadSafeGapList : IEnumerable + { + private volatile List _list = new List(); + private readonly object _writeLock = new object(); + + public int Count => _list.Count; + + public void Add(Gap gap) + { + lock (_writeLock) + { + var newList = new List(_list) { gap }; + Interlocked.Exchange(ref _list, newList); + } + } + + public bool Remove(Gap gap) + { + lock (_writeLock) + { + var newList = new List(_list); + bool removed = newList.Remove(gap); + if (removed) + { + Interlocked.Exchange(ref _list, newList); + } + return removed; + } + } + + public void Clear() + { + Interlocked.Exchange(ref _list, new List()); + } + + public bool Contains(Gap gap) => _list.Contains(gap); + + public Gap this[int index] => _list[index]; + + public IEnumerator GetEnumerator() => _list.GetEnumerator(); + System.Collections.IEnumerator System.Collections.IEnumerable.GetEnumerator() => GetEnumerator(); + + // LINQ-friendly methods + public List ToList() => new List(_list); + public Gap FirstOrDefault(Func predicate) => _list.FirstOrDefault(predicate); + public Gap 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() => _list.Any(); + public bool Any(Func predicate) => _list.Any(predicate); + public IOrderedEnumerable OrderBy(Func keySelector) => _list.OrderBy(keySelector); + } + partial class Gap : MapEntity, ISerializableEntity { - public static List GapList = new List(); + public static ThreadSafeGapList GapList = new ThreadSafeGapList(); const float MaxFlowForce = 500.0f; diff --git a/Barotrauma/BarotraumaShared/SharedSource/Map/Hull.cs b/Barotrauma/BarotraumaShared/SharedSource/Map/Hull.cs index 1d4b733ba..50a1b3b39 100644 --- a/Barotrauma/BarotraumaShared/SharedSource/Map/Hull.cs +++ b/Barotrauma/BarotraumaShared/SharedSource/Map/Hull.cs @@ -14,6 +14,116 @@ using Barotrauma.Extensions; namespace Barotrauma { + /// + /// Thread-safe wrapper for Hull list operations. + /// Uses copy-on-write pattern for lock-free reads. + /// + internal class ThreadSafeHullList : IEnumerable + { + private volatile List _list = new List(); + private readonly object _writeLock = new object(); + + public int Count => _list.Count; + + public void Add(Hull hull) + { + lock (_writeLock) + { + var newList = new List(_list) { hull }; + Interlocked.Exchange(ref _list, newList); + } + } + + public bool Remove(Hull hull) + { + lock (_writeLock) + { + var newList = new List(_list); + bool removed = newList.Remove(hull); + if (removed) + { + Interlocked.Exchange(ref _list, newList); + } + return removed; + } + } + + public void Clear() + { + Interlocked.Exchange(ref _list, new List()); + } + + public bool Contains(Hull hull) => _list.Contains(hull); + + public Hull this[int index] => _list[index]; + + public IEnumerator GetEnumerator() => _list.GetEnumerator(); + System.Collections.IEnumerator System.Collections.IEnumerable.GetEnumerator() => GetEnumerator(); + + // LINQ-friendly methods + public List ToList() => new List(_list); + public Hull FirstOrDefault(Func predicate) => _list.FirstOrDefault(predicate); + public Hull 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() => _list.Any(); + public bool Any(Func predicate) => _list.Any(predicate); + public bool Exists(Predicate predicate) => _list.Exists(predicate); + public void ForEach(Action action) => _list.ForEach(action); + } + + /// + /// Thread-safe wrapper for EntityGrid list operations. + /// Uses copy-on-write pattern for lock-free reads. + /// + internal class ThreadSafeEntityGridList : IEnumerable + { + private volatile List _list = new List(); + private readonly object _writeLock = new object(); + + public int Count => _list.Count; + + public void Add(EntityGrid grid) + { + lock (_writeLock) + { + var newList = new List(_list) { grid }; + Interlocked.Exchange(ref _list, newList); + } + } + + public bool Remove(EntityGrid grid) + { + lock (_writeLock) + { + var newList = new List(_list); + bool removed = newList.Remove(grid); + if (removed) + { + Interlocked.Exchange(ref _list, newList); + } + return removed; + } + } + + public void Clear() + { + Interlocked.Exchange(ref _list, new List()); + } + + public EntityGrid this[int index] => _list[index]; + + public IEnumerator GetEnumerator() => _list.GetEnumerator(); + System.Collections.IEnumerator System.Collections.IEnumerable.GetEnumerator() => GetEnumerator(); + + // LINQ-friendly methods + public List ToList() => new List(_list); + public EntityGrid FirstOrDefault(Func predicate) => _list.FirstOrDefault(predicate); + public EntityGrid Find(Predicate predicate) => _list.Find(predicate); + public IEnumerable Where(Func predicate) => _list.Where(predicate); + public bool Any() => _list.Any(); + } + partial class BackgroundSection { public Rectangle Rect; @@ -114,8 +224,8 @@ namespace Barotrauma partial class Hull : MapEntity, ISerializableEntity, IServerSerializable { - public readonly static List HullList = new List(); - public readonly static List EntityGrids = new List(); + public readonly static ThreadSafeHullList HullList = new ThreadSafeHullList(); + public readonly static ThreadSafeEntityGridList EntityGrids = new ThreadSafeEntityGridList(); public static bool ShowHulls = true; diff --git a/Barotrauma/BarotraumaShared/SharedSource/Map/MapEntity.cs b/Barotrauma/BarotraumaShared/SharedSource/Map/MapEntity.cs index 39c77c116..b61d2e68e 100644 --- a/Barotrauma/BarotraumaShared/SharedSource/Map/MapEntity.cs +++ b/Barotrauma/BarotraumaShared/SharedSource/Map/MapEntity.cs @@ -6,14 +6,111 @@ using System.Collections.Generic; using System.Diagnostics; using System.Linq; using System.Reflection; +using System.Threading; using System.Threading.Tasks; using System.Xml.Linq; namespace Barotrauma { + /// + /// Thread-safe wrapper for MapEntity list operations. + /// Uses copy-on-write pattern for lock-free reads. + /// + internal class ThreadSafeMapEntityList : IEnumerable + { + private volatile List _list = new List(); + private readonly object _writeLock = new object(); + + public int Count => _list.Count; + + public void Add(MapEntity entity) + { + lock (_writeLock) + { + var newList = new List(_list) { entity }; + Interlocked.Exchange(ref _list, newList); + } + } + + public void Insert(int index, MapEntity entity) + { + lock (_writeLock) + { + var newList = new List(_list); + newList.Insert(index, entity); + Interlocked.Exchange(ref _list, newList); + } + } + + /// + /// Atomically inserts an entity at a position determined by the insertAction. + /// The insertAction is executed within the lock to ensure thread-safety. + /// + public void InsertWithAction(MapEntity entity, Action, MapEntity> insertAction) + { + lock (_writeLock) + { + var newList = new List(_list); + insertAction(newList, entity); + Interlocked.Exchange(ref _list, newList); + } + } + + public bool Remove(MapEntity entity) + { + lock (_writeLock) + { + var newList = new List(_list); + bool removed = newList.Remove(entity); + if (removed) + { + Interlocked.Exchange(ref _list, newList); + } + return removed; + } + } + + public int RemoveAll(Predicate match) + { + lock (_writeLock) + { + var newList = new List(_list); + int count = newList.RemoveAll(match); + if (count > 0) + { + Interlocked.Exchange(ref _list, newList); + } + return count; + } + } + + public void Clear() + { + Interlocked.Exchange(ref _list, new List()); + } + + public bool Contains(MapEntity entity) => _list.Contains(entity); + + public MapEntity this[int index] => _list[index]; + + public IEnumerator GetEnumerator() => _list.GetEnumerator(); + System.Collections.IEnumerator System.Collections.IEnumerable.GetEnumerator() => GetEnumerator(); + + // LINQ-friendly methods that work on a snapshot + public List ToList() => new List(_list); + public MapEntity FirstOrDefault(Func predicate) => _list.FirstOrDefault(predicate); + public MapEntity 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 Exists(Predicate predicate) => _list.Exists(predicate); + public IOrderedEnumerable OrderBy(Func keySelector) => _list.OrderBy(keySelector); + public void ForEach(Action action) => _list.ForEach(action); + } + abstract partial class MapEntity : Entity, ISpatialEntity { - public readonly static List MapEntityList = new List(); + public readonly static ThreadSafeMapEntityList MapEntityList = new ThreadSafeMapEntityList(); public readonly MapEntityPrefab Prefab; @@ -559,45 +656,51 @@ namespace Barotrauma return; } - //sort damageable walls by sprite depth: - //necessary because rendering the damage effect starts a new sprite batch and breaks the order otherwise - int i = 0; - if (this is Structure { DrawDamageEffect: true } structure) + // Use atomic insertion to ensure thread-safety + MapEntityList.InsertWithAction(this, (list, entity) => { - //insertion sort according to draw depth - float drawDepth = structure.SpriteDepth; - while (i < MapEntityList.Count) + int i = 0; + + //sort damageable walls by sprite depth: + //necessary because rendering the damage effect starts a new sprite batch and breaks the order otherwise + if (entity is Structure { DrawDamageEffect: true } structure) { - float otherDrawDepth = (MapEntityList[i] as Structure)?.SpriteDepth ?? 1.0f; - if (otherDrawDepth < drawDepth) { break; } - i++; - } - MapEntityList.Insert(i, this); - return; - } - - i = 0; - while (i < MapEntityList.Count) - { - i++; - if (MapEntityList[i - 1]?.Prefab == Prefab) - { - MapEntityList.Insert(i, this); + //insertion sort according to draw depth + float drawDepth = structure.SpriteDepth; + while (i < list.Count) + { + float otherDrawDepth = (list[i] as Structure)?.SpriteDepth ?? 1.0f; + if (otherDrawDepth < drawDepth) { break; } + i++; + } + list.Insert(i, entity); return; } - } + + i = 0; + var mapEntity = (MapEntity)entity; + while (i < list.Count) + { + i++; + if (list[i - 1]?.Prefab == mapEntity.Prefab) + { + list.Insert(i, entity); + return; + } + } #if CLIENT - i = 0; - while (i < MapEntityList.Count) - { - i++; - Sprite existingSprite = MapEntityList[i - 1].Sprite; - if (existingSprite == null) { continue; } - if (existingSprite.Texture == this.Sprite.Texture) { break; } - } + i = 0; + while (i < list.Count) + { + i++; + Sprite existingSprite = list[i - 1].Sprite; + if (existingSprite == null) { continue; } + if (existingSprite.Texture == mapEntity.Sprite?.Texture) { break; } + } #endif - MapEntityList.Insert(i, this); + list.Insert(i, entity); + }); } /// diff --git a/Barotrauma/BarotraumaShared/SharedSource/Map/Structure.cs b/Barotrauma/BarotraumaShared/SharedSource/Map/Structure.cs index 8339e6c65..9874a3dcb 100644 --- a/Barotrauma/BarotraumaShared/SharedSource/Map/Structure.cs +++ b/Barotrauma/BarotraumaShared/SharedSource/Map/Structure.cs @@ -7,6 +7,7 @@ using Microsoft.Xna.Framework; using System; using System.Collections.Generic; using System.Linq; +using System.Threading; using System.Xml.Linq; using System.Collections.Immutable; using Barotrauma.Abilities; @@ -18,6 +19,63 @@ using Barotrauma.Lights; namespace Barotrauma { + /// + /// Thread-safe wrapper for Structure list operations. + /// Uses copy-on-write pattern for lock-free reads. + /// + internal class ThreadSafeStructureList : IEnumerable + { + private volatile List _list = new List(); + private readonly object _writeLock = new object(); + + public int Count => _list.Count; + + public void Add(Structure structure) + { + lock (_writeLock) + { + var newList = new List(_list) { structure }; + Interlocked.Exchange(ref _list, newList); + } + } + + public bool Remove(Structure structure) + { + lock (_writeLock) + { + var newList = new List(_list); + bool removed = newList.Remove(structure); + if (removed) + { + Interlocked.Exchange(ref _list, newList); + } + return removed; + } + } + + public void Clear() + { + Interlocked.Exchange(ref _list, new List()); + } + + public bool Contains(Structure structure) => _list.Contains(structure); + + public Structure this[int index] => _list[index]; + + public IEnumerator GetEnumerator() => _list.GetEnumerator(); + System.Collections.IEnumerator System.Collections.IEnumerable.GetEnumerator() => GetEnumerator(); + + // LINQ-friendly methods + public List ToList() => new List(_list); + public Structure FirstOrDefault(Func predicate) => _list.FirstOrDefault(predicate); + public Structure 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() => _list.Any(); + public bool Any(Func predicate) => _list.Any(predicate); + public void ForEach(Action action) => _list.ForEach(action); + } + partial class WallSection : IIgnorable { public Rectangle rect; @@ -48,7 +106,7 @@ namespace Barotrauma partial class Structure : MapEntity, IDamageable, IServerSerializable, ISerializableEntity { public const int WallSectionSize = 96; - public static List WallList = new List(); + public static ThreadSafeStructureList WallList = new ThreadSafeStructureList(); const float LeakThreshold = 0.1f; const float BigGapThreshold = 0.7f; diff --git a/Barotrauma/BarotraumaShared/SharedSource/Map/Submarine.cs b/Barotrauma/BarotraumaShared/SharedSource/Map/Submarine.cs index 4a8e2a3c9..2bba31f2e 100644 --- a/Barotrauma/BarotraumaShared/SharedSource/Map/Submarine.cs +++ b/Barotrauma/BarotraumaShared/SharedSource/Map/Submarine.cs @@ -18,6 +18,64 @@ using Voronoi2; namespace Barotrauma { + /// + /// Thread-safe wrapper for Submarine list operations. + /// Uses copy-on-write pattern for lock-free reads. + /// + internal class ThreadSafeSubmarineList : IEnumerable + { + private volatile List _list = new List(); + private readonly object _writeLock = new object(); + + public int Count => _list.Count; + + public void Add(Submarine submarine) + { + lock (_writeLock) + { + var newList = new List(_list) { submarine }; + Interlocked.Exchange(ref _list, newList); + } + } + + public bool Remove(Submarine submarine) + { + lock (_writeLock) + { + var newList = new List(_list); + bool removed = newList.Remove(submarine); + if (removed) + { + Interlocked.Exchange(ref _list, newList); + } + return removed; + } + } + + public void Clear() + { + Interlocked.Exchange(ref _list, new List()); + } + + public bool Contains(Submarine submarine) => _list.Contains(submarine); + + public Submarine this[int index] => _list[index]; + + public IEnumerator GetEnumerator() => _list.GetEnumerator(); + System.Collections.IEnumerator System.Collections.IEnumerable.GetEnumerator() => GetEnumerator(); + + // LINQ-friendly methods + public List ToList() => new List(_list); + public Submarine FirstOrDefault(Func predicate) => _list.FirstOrDefault(predicate); + public Submarine 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() => _list.Any(); + public bool Any(Func predicate) => _list.Any(predicate); + public float Sum(Func selector) => _list.Sum(selector); + public IEnumerable Select(Func selector) => _list.Select(selector); + } + public enum Direction : byte { None = 0, Left = 1, Right = 2 @@ -73,7 +131,7 @@ namespace Barotrauma get { return MainSubs[0]; } set { MainSubs[0] = value; } } - private static readonly List loaded = new List(); + private static readonly ThreadSafeSubmarineList loaded = new ThreadSafeSubmarineList(); private readonly Identifier upgradeEventIdentifier; @@ -148,7 +206,7 @@ namespace Barotrauma public List ForcedOutpostModuleWayPoints = new List(); - public static List Loaded + public static ThreadSafeSubmarineList Loaded { get { return loaded; } } @@ -1515,13 +1573,13 @@ namespace Barotrauma public List GetHulls(bool alsoFromConnectedSubs) => GetEntities(alsoFromConnectedSubs, Hull.HullList); public List GetGaps(bool alsoFromConnectedSubs) => GetEntities(alsoFromConnectedSubs, Gap.GapList); - public List GetItems(bool alsoFromConnectedSubs) => GetEntities(alsoFromConnectedSubs, Item.ItemList).ToList(); + public List GetItems(bool alsoFromConnectedSubs) => GetEntities(alsoFromConnectedSubs, Item.ItemList); public List GetWaypoints(bool alsoFromConnectedSubs) => GetEntities(alsoFromConnectedSubs, WayPoint.WayPointList); public List GetWalls(bool alsoFromConnectedSubs) => GetEntities(alsoFromConnectedSubs, Structure.WallList); - public List GetEntities(bool includingConnectedSubs, List list) where T : MapEntity + public List GetEntities(bool includingConnectedSubs, IEnumerable list) where T : MapEntity { - return list.FindAll(e => IsEntityFoundOnThisSub(e, includingConnectedSubs)); + return list.Where(e => IsEntityFoundOnThisSub(e, includingConnectedSubs)).ToList(); } public List<(ItemContainer container, int freeSlots)> GetCargoContainers() @@ -1546,11 +1604,6 @@ namespace Barotrauma return containers; } - public IEnumerable GetEntities(bool includingConnectedSubs, IEnumerable list) where T : MapEntity - { - return list.Where(e => IsEntityFoundOnThisSub(e, includingConnectedSubs)); - } - public bool IsEntityFoundOnThisSub(MapEntity entity, bool includingConnectedSubs, bool allowDifferentTeam = false, bool allowDifferentType = false) { if (entity == null) { return false; } @@ -1674,9 +1727,8 @@ namespace Barotrauma HiddenSubPosition += Vector2.UnitY * GameMain.GameSession.LevelData.Size.Y; } - for (int i = 0; i < loaded.Count; i++) + foreach (Submarine sub in loaded) { - Submarine sub = loaded[i]; HiddenSubPosition = new Vector2( //1st sub on the left side, 2nd on the right, etc @@ -1808,10 +1860,9 @@ namespace Barotrauma } entityGrid = Hull.GenerateEntityGrid(this); - for (int i = 0; i < MapEntity.MapEntityList.Count; i++) + foreach (MapEntity me in MapEntity.MapEntityList.Where(e => e.Submarine == this)) { - if (MapEntity.MapEntityList[i].Submarine != this) { continue; } - MapEntity.MapEntityList[i].Move(HiddenSubPosition, ignoreContacts: true); + me.Move(HiddenSubPosition, ignoreContacts: true); } Loading = false; diff --git a/Barotrauma/BarotraumaShared/SharedSource/Map/WayPoint.cs b/Barotrauma/BarotraumaShared/SharedSource/Map/WayPoint.cs index 1dc9d991c..b2f5a366f 100644 --- a/Barotrauma/BarotraumaShared/SharedSource/Map/WayPoint.cs +++ b/Barotrauma/BarotraumaShared/SharedSource/Map/WayPoint.cs @@ -5,17 +5,75 @@ using Microsoft.Xna.Framework; using System; using System.Collections.Generic; using System.Linq; +using System.Threading; using System.Xml.Linq; using Barotrauma.Extensions; namespace Barotrauma { + /// + /// Thread-safe wrapper for WayPoint list operations. + /// Uses copy-on-write pattern for lock-free reads. + /// + internal class ThreadSafeWayPointList : IEnumerable + { + private volatile List _list = new List(); + private readonly object _writeLock = new object(); + + public int Count => _list.Count; + + public void Add(WayPoint waypoint) + { + lock (_writeLock) + { + var newList = new List(_list) { waypoint }; + Interlocked.Exchange(ref _list, newList); + } + } + + public bool Remove(WayPoint waypoint) + { + lock (_writeLock) + { + var newList = new List(_list); + bool removed = newList.Remove(waypoint); + if (removed) + { + Interlocked.Exchange(ref _list, newList); + } + return removed; + } + } + + public void Clear() + { + Interlocked.Exchange(ref _list, new List()); + } + + public bool Contains(WayPoint waypoint) => _list.Contains(waypoint); + + public WayPoint this[int index] => _list[index]; + + public IEnumerator GetEnumerator() => _list.GetEnumerator(); + System.Collections.IEnumerator System.Collections.IEnumerable.GetEnumerator() => GetEnumerator(); + + // LINQ-friendly methods + public List ToList() => new List(_list); + public WayPoint FirstOrDefault(Func predicate) => _list.FirstOrDefault(predicate); + public WayPoint 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() => _list.Any(); + public bool Any(Func predicate) => _list.Any(predicate); + public bool Exists(Predicate predicate) => _list.Exists(predicate); + } + [Flags] public enum SpawnType { Path = 0, Human = 1, Enemy = 2, Cargo = 4, Corpse = 8, Submarine = 16, ExitPoint = 32, Disabled = 64 }; partial class WayPoint : MapEntity { - public static List WayPointList = new List(); + public static ThreadSafeWayPointList WayPointList = new ThreadSafeWayPointList(); public static bool ShowWayPoints = true, ShowSpawnPoints = true; @@ -932,7 +990,7 @@ namespace Barotrauma public static WayPoint GetRandom(SpawnType spawnType = SpawnType.Human, JobPrefab assignedJob = null, Submarine sub = null, bool useSyncedRand = false, string spawnPointTag = null, bool ignoreSubmarine = false) { - return WayPointList.GetRandom(wp => + return WayPointList.ToList().GetRandom(wp => (ignoreSubmarine || wp.Submarine == sub) && //checking for the disabled flag is not strictly necessary because we check for equality of the spawn type, //but lets do that anyway in case we change the handling of the spawn type at some point diff --git a/Barotrauma/BarotraumaShared/SharedSource/Physics/PhysicsBody.cs b/Barotrauma/BarotraumaShared/SharedSource/Physics/PhysicsBody.cs index 58f0f5280..cfeac247b 100644 --- a/Barotrauma/BarotraumaShared/SharedSource/Physics/PhysicsBody.cs +++ b/Barotrauma/BarotraumaShared/SharedSource/Physics/PhysicsBody.cs @@ -4,12 +4,69 @@ using FarseerPhysics.Dynamics; using Microsoft.Xna.Framework; using System; using System.Collections.Generic; +using System.Linq; +using System.Threading; using System.Xml.Linq; using LimbParams = Barotrauma.RagdollParams.LimbParams; using ColliderParams = Barotrauma.RagdollParams.ColliderParams; namespace Barotrauma { + /// + /// Thread-safe wrapper for PhysicsBody list operations. + /// Uses copy-on-write pattern for lock-free reads. + /// + internal class ThreadSafePhysicsBodyList : IEnumerable + { + private volatile List _list = new List(); + private readonly object _writeLock = new object(); + + public int Count => _list.Count; + + public void Add(PhysicsBody body) + { + lock (_writeLock) + { + var newList = new List(_list) { body }; + Interlocked.Exchange(ref _list, newList); + } + } + + public bool Remove(PhysicsBody body) + { + lock (_writeLock) + { + var newList = new List(_list); + bool removed = newList.Remove(body); + if (removed) + { + Interlocked.Exchange(ref _list, newList); + } + return removed; + } + } + + public void Clear() + { + Interlocked.Exchange(ref _list, new List()); + } + + public bool Contains(PhysicsBody body) => _list.Contains(body); + + public PhysicsBody this[int index] => _list[index]; + + public IEnumerator GetEnumerator() => _list.GetEnumerator(); + System.Collections.IEnumerator System.Collections.IEnumerable.GetEnumerator() => GetEnumerator(); + + // LINQ-friendly methods + public List ToList() => new List(_list); + public PhysicsBody FirstOrDefault(Func predicate) => _list.FirstOrDefault(predicate); + public PhysicsBody Find(Predicate predicate) => _list.Find(predicate); + public IEnumerable Where(Func predicate) => _list.Where(predicate); + public bool Any() => _list.Any(); + public bool Any(Func predicate) => _list.Any(predicate); + } + class PosInfo { public Vector2 Position @@ -92,8 +149,8 @@ namespace Barotrauma public const float MinDensity = 0.01f; public const float DefaultAngularDamping = 5.0f; - private static readonly List list = new List(); - public static List List + private static readonly ThreadSafePhysicsBodyList list = new ThreadSafePhysicsBodyList(); + public static ThreadSafePhysicsBodyList List { get { return list; } }