From 16131e0accc4bef7d8b912a26796d9b8ab9a7e9c Mon Sep 17 00:00:00 2001 From: NotAlwaysTrue <77662224+NotAlwaysTrue@users.noreply.github.com> Date: Sat, 27 Dec 2025 18:45:51 +0800 Subject: [PATCH 01/14] Fixed a typo --- README.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/README.md b/README.md index eba4ae19a..0e6cfed9a 100644 --- a/README.md +++ b/README.md @@ -1,4 +1,4 @@ -# LuaCsForBarotrauma Enhanced Performence Project +# LuaCsForBarotrauma Enhanced Performance Project > ⚠ **Warning:** This release is only available for server-side use and is not recommended to run on the client. Make sure that compatibility is adequately tested before deployment. From edd50ef181fa57c267eda40c6b1811f97382fa01 Mon Sep 17 00:00:00 2001 From: NotAlwaysTrue <77662224+NotAlwaysTrue@users.noreply.github.com> Date: Sat, 27 Dec 2025 20:54:06 +0800 Subject: [PATCH 02/14] Update publish-release.yml --- .github/workflows/publish-release.yml | 13 ++++++++----- 1 file changed, 8 insertions(+), 5 deletions(-) diff --git a/.github/workflows/publish-release.yml b/.github/workflows/publish-release.yml index 5d63b200d..d4283cdd3 100644 --- a/.github/workflows/publish-release.yml +++ b/.github/workflows/publish-release.yml @@ -23,13 +23,16 @@ on: env: CI_DIR: 2049ef39-42a2-46d2-b513-ee6d2e3a7b15 RELEASES: | - windows:client:Windows/Client windows:server:Windows/Server - linux:client:Linux/Client linux:server:Linux/Server - mac:client:Mac/Client/Barotrauma.app/Contents/MacOS mac:server:Mac/Server - ARCHIVE_BASE_NAME: luacsforbarotrauma + ARCHIVE_BASE_NAME: luacsforbarotraumaEP + + # windows:client:Windows/Client + # linux:client:Linux/Client + # mac:client:Mac/Client/Barotrauma.app/Contents/MacOS + # we do not currently provide a CL + # XXX: these file names are subject to shell expansion. # Be careful when using special characters. ARCHIVE_FILES_SERVER: | @@ -211,4 +214,4 @@ jobs: files: | ${{ env.CI_DIR }}/archives/${{ env.ARCHIVE_BASE_NAME }}_{build,patch}_{windows,linux,mac}_{client,server}.zip ${{ env.CI_DIR }}/archives/${{ env.ARCHIVE_BASE_NAME }}_{build,patch}_linux_{client,server}.tar.gz - ${{ env.CI_DIR }}/archives/${{ env.ARCHIVE_BASE_NAME }}_refs.zip \ No newline at end of file + ${{ env.CI_DIR }}/archives/${{ env.ARCHIVE_BASE_NAME }}_refs.zip From 90962b232880c1f5978c49757baa5c0e20c71f39 Mon Sep 17 00:00:00 2001 From: Eero Date: Sun, 28 Dec 2025 03:57:04 +0800 Subject: [PATCH 03/14] Refactor Item collections for thread safety and performance Replaces static Item.ItemList and related collections with thread-safe data structures using ConcurrentDictionary and ImmutableHashSet. Adds thread-safe helpers for marking items for deconstruction and managing item lists. Updates all usages of Item.ItemList and DeconstructItems to use new APIs, improving performance and safety in multi-threaded contexts. Also refactors MeleeWeapon and Projectile impact queues to use ConcurrentQueue, and updates related logic throughout the codebase. --- .gitignore | 2 + .../ClientSource/GameSession/CrewManager.cs | 4 +- .../ClientSource/Items/Inventory.cs | 2 +- .../ClientSource/Map/MapEntity.cs | 4 +- .../ServerSource/Networking/RespawnManager.cs | 2 +- .../AI/Objectives/AIObjectiveCleanupItem.cs | 2 +- .../AI/Objectives/AIObjectiveCleanupItems.cs | 2 +- .../SharedSource/Characters/AI/Order.cs | 4 +- .../SharedSource/Characters/Character.cs | 9 +- .../SharedSource/DebugConsole.cs | 4 +- .../SharedSource/Events/MalfunctionEvent.cs | 2 +- .../Missions/AbandonedOutpostMission.cs | 2 +- .../Events/Missions/EndMission.cs | 2 +- .../SharedSource/GameSession/CrewManager.cs | 4 +- .../Items/Components/Holdable/MeleeWeapon.cs | 12 +- .../Items/Components/Projectile.cs | 6 +- .../Items/Components/Signal/Wire.cs | 4 +- .../SharedSource/Items/Item.cs | 239 ++++++++++++++---- .../SharedSource/Map/Entity.cs | 2 +- .../SharedSource/Map/Levels/Level.cs | 8 +- .../SharedSource/Map/Map/Location.cs | 2 +- .../SharedSource/Map/Submarine.cs | 6 +- 22 files changed, 232 insertions(+), 92 deletions(-) diff --git a/.gitignore b/.gitignore index 287132dc0..56e643245 100644 --- a/.gitignore +++ b/.gitignore @@ -60,3 +60,5 @@ Deploy/DeployAll/PrivateKey.* #Rider *.DotSettings.user .vscode/settings.json +.vscode/launch.json +.vscode/tasks.json diff --git a/Barotrauma/BarotraumaClient/ClientSource/GameSession/CrewManager.cs b/Barotrauma/BarotraumaClient/ClientSource/GameSession/CrewManager.cs index f6bce310a..91600e24c 100644 --- a/Barotrauma/BarotraumaClient/ClientSource/GameSession/CrewManager.cs +++ b/Barotrauma/BarotraumaClient/ClientSource/GameSession/CrewManager.cs @@ -867,7 +867,7 @@ namespace Barotrauma { foreach (var stackedItem in item.GetStackedItems()) { - Item.DeconstructItems.Add(stackedItem); + Item.MarkForDeconstruction(stackedItem); } HintManager.OnItemMarkedForDeconstruction(order.OrderGiver); } @@ -875,7 +875,7 @@ namespace Barotrauma { foreach (var stackedItem in item.GetStackedItems()) { - Item.DeconstructItems.Remove(stackedItem); + Item.UnmarkForDeconstruction(stackedItem); } } } diff --git a/Barotrauma/BarotraumaClient/ClientSource/Items/Inventory.cs b/Barotrauma/BarotraumaClient/ClientSource/Items/Inventory.cs index 21f681e5e..88168faec 100644 --- a/Barotrauma/BarotraumaClient/ClientSource/Items/Inventory.cs +++ b/Barotrauma/BarotraumaClient/ClientSource/Items/Inventory.cs @@ -1891,7 +1891,7 @@ namespace Barotrauma } } - else if (Item.DeconstructItems.Contains(item) && + else if (Item.IsMarkedForDeconstruction(item) && OrderPrefab.Prefabs.TryGet(Tags.DeconstructThis, out OrderPrefab deconstructOrder)) { DrawSideIcon(deconstructOrder.SymbolSprite, Direction.Right, TextManager.Get("tooltip.markedfordeconstruction"), GUIStyle.Red, out bool mouseOn); diff --git a/Barotrauma/BarotraumaClient/ClientSource/Map/MapEntity.cs b/Barotrauma/BarotraumaClient/ClientSource/Map/MapEntity.cs index 65363372a..2d9eea200 100644 --- a/Barotrauma/BarotraumaClient/ClientSource/Map/MapEntity.cs +++ b/Barotrauma/BarotraumaClient/ClientSource/Map/MapEntity.cs @@ -471,11 +471,11 @@ namespace Barotrauma if (item0 == null && item1 != null) { - item0 = Item.ItemList.Find(it => it.GetComponent()?.DisconnectedWires.Contains(wire) ?? false); + item0 = Item.ItemList.FirstOrDefault(it => it.GetComponent()?.DisconnectedWires.Contains(wire) ?? false); } else if (item0 != null && item1 == null) { - item1 = Item.ItemList.Find(it => it.GetComponent()?.DisconnectedWires.Contains(wire) ?? false); + item1 = Item.ItemList.FirstOrDefault(it => it.GetComponent()?.DisconnectedWires.Contains(wire) ?? false); } if (item0 != null && item1 != null && SelectedList.Contains(item0) && SelectedList.Contains(item1)) { diff --git a/Barotrauma/BarotraumaServer/ServerSource/Networking/RespawnManager.cs b/Barotrauma/BarotraumaServer/ServerSource/Networking/RespawnManager.cs index b83376d66..2b54023dd 100644 --- a/Barotrauma/BarotraumaServer/ServerSource/Networking/RespawnManager.cs +++ b/Barotrauma/BarotraumaServer/ServerSource/Networking/RespawnManager.cs @@ -279,7 +279,7 @@ namespace Barotrauma.Networking var shuttleGaps = Gap.GapList.FindAll(g => RespawnShuttles.Contains(g.Submarine) && g.ConnectedWall != null); shuttleGaps.ForEach(g => Spawner.AddEntityToRemoveQueue(g)); - var dockingPorts = Item.ItemList.FindAll(i => RespawnShuttles.Contains(i.Submarine) && i.GetComponent() != null); + var dockingPorts = Item.ItemList.Where(i => RespawnShuttles.Contains(i.Submarine) && i.GetComponent() != null).ToList(); dockingPorts.ForEach(d => d.GetComponent().Undock()); if (!IsShuttleInsideLevel || DateTime.Now > teamSpecificState.DespawnTime) diff --git a/Barotrauma/BarotraumaShared/SharedSource/Characters/AI/Objectives/AIObjectiveCleanupItem.cs b/Barotrauma/BarotraumaShared/SharedSource/Characters/AI/Objectives/AIObjectiveCleanupItem.cs index e110e20c1..6b90cd48d 100644 --- a/Barotrauma/BarotraumaShared/SharedSource/Characters/AI/Objectives/AIObjectiveCleanupItem.cs +++ b/Barotrauma/BarotraumaShared/SharedSource/Characters/AI/Objectives/AIObjectiveCleanupItem.cs @@ -119,7 +119,7 @@ namespace Barotrauma protected override bool CheckObjectiveState() { - if (item.IgnoreByAI(character) || Item.DeconstructItems.Contains(item)) + if (item.IgnoreByAI(character) || Item.IsMarkedForDeconstruction(item)) { Abandon = true; } diff --git a/Barotrauma/BarotraumaShared/SharedSource/Characters/AI/Objectives/AIObjectiveCleanupItems.cs b/Barotrauma/BarotraumaShared/SharedSource/Characters/AI/Objectives/AIObjectiveCleanupItems.cs index b5304e13c..89fea879b 100644 --- a/Barotrauma/BarotraumaShared/SharedSource/Characters/AI/Objectives/AIObjectiveCleanupItems.cs +++ b/Barotrauma/BarotraumaShared/SharedSource/Characters/AI/Objectives/AIObjectiveCleanupItems.cs @@ -114,7 +114,7 @@ namespace Barotrauma if (!allowUnloading) { return false; } if (requireValidContainer && !IsValidContainer(item.Container, character)) { return false; } } - if (ignoreItemsMarkedForDeconstruction && Item.DeconstructItems.Contains(item)) { return false; } + if (ignoreItemsMarkedForDeconstruction && Item.IsMarkedForDeconstruction(item)) { return false; } if (!item.HasAccess(character)) { return false; } if (character != null && !IsItemInsideValidSubmarine(item, character)) { return false; } if (item.HasBallastFloraInHull) { return false; } diff --git a/Barotrauma/BarotraumaShared/SharedSource/Characters/AI/Order.cs b/Barotrauma/BarotraumaShared/SharedSource/Characters/AI/Order.cs index 8ca9bedf5..4a343171a 100644 --- a/Barotrauma/BarotraumaShared/SharedSource/Characters/AI/Order.cs +++ b/Barotrauma/BarotraumaShared/SharedSource/Characters/AI/Order.cs @@ -440,7 +440,7 @@ namespace Barotrauma if (Identifier == Tags.DeconstructThis && item.AllowDeconstruct) { - if (item.AllowDeconstruct && !Item.DeconstructItems.Contains(item) && + if (item.AllowDeconstruct && !Item.IsMarkedForDeconstruction(item) && //only allow deconstructing if there are no deconstruction recipes (= deconstructing yields nothing), or deconstruction recipes that (item.Prefab.DeconstructItems.None() || item.Prefab.DeconstructItems.Any(deconstructItem => @@ -454,7 +454,7 @@ namespace Barotrauma } else if (Identifier == Tags.DontDeconstructThis) { - if (Item.DeconstructItems.Contains(item)) { return true; } + if (Item.IsMarkedForDeconstruction(item)) { return true; } } ImmutableArray targetItems = GetTargetItems(option); diff --git a/Barotrauma/BarotraumaShared/SharedSource/Characters/Character.cs b/Barotrauma/BarotraumaShared/SharedSource/Characters/Character.cs index af94426d0..2424f966f 100644 --- a/Barotrauma/BarotraumaShared/SharedSource/Characters/Character.cs +++ b/Barotrauma/BarotraumaShared/SharedSource/Characters/Character.cs @@ -2761,10 +2761,11 @@ namespace Barotrauma } int itemsPerFrame = IsOnPlayerTeam ? 100 : 10; int checkedItemCount = 0; - for (int i = 0; i < itemsPerFrame && itemIndex < Item.ItemList.Count; i++, itemIndex++) + var cachedItems = Item.GetCachedItemList(); + for (int i = 0; i < itemsPerFrame && itemIndex < cachedItems.Count; i++, itemIndex++) { checkedItemCount++; - var item = Item.ItemList[itemIndex]; + var item = cachedItems[itemIndex]; if (!item.IsInteractable(this)) { continue; } if (ignoredItems != null && ignoredItems.Contains(item)) { continue; } if (item.Submarine == null) { continue; } @@ -2800,10 +2801,10 @@ namespace Barotrauma } } targetItem = _foundItem; - bool completed = itemIndex >= Item.ItemList.Count - 1; + bool completed = itemIndex >= cachedItems.Count - 1; if (HumanAIController.DebugAI && checkedItemCount > 0 && targetItem != null && StopWatch.ElapsedMilliseconds > 1) { - var msg = $"Went through {checkedItemCount} of total {Item.ItemList.Count} items. Found item {targetItem.Name} in {StopWatch.ElapsedMilliseconds} ms. Completed: {completed}"; + var msg = $"Went through {checkedItemCount} of total {cachedItems.Count} items. Found item {targetItem.Name} in {StopWatch.ElapsedMilliseconds} ms. Completed: {completed}"; if (StopWatch.ElapsedMilliseconds > 5) { DebugConsole.ThrowError(msg); diff --git a/Barotrauma/BarotraumaShared/SharedSource/DebugConsole.cs b/Barotrauma/BarotraumaShared/SharedSource/DebugConsole.cs index 05444468b..5cdfefe6a 100644 --- a/Barotrauma/BarotraumaShared/SharedSource/DebugConsole.cs +++ b/Barotrauma/BarotraumaShared/SharedSource/DebugConsole.cs @@ -1478,7 +1478,7 @@ namespace Barotrauma newItemName = args[2]; } - var oldItem = Item.ItemList.FindAll(it => it.Name == args[0]).ElementAtOrDefault(itemIndex); + var oldItem = Item.ItemList.Where(it => it.Name == args[0]).ElementAtOrDefault(itemIndex); if (oldItem == null) { ThrowError($"Could not find an item with the name {args[0]} (index {itemIndex})."); @@ -1852,7 +1852,7 @@ namespace Barotrauma commands.Add(new Command("power", "power: Immediately powers up the submarine's nuclear reactor.", (string[] args) => { - Item reactorItem = Item.ItemList.Find(i => i.GetComponent() != null); + Item reactorItem = Item.ItemList.FirstOrDefault(i => i.GetComponent() != null); if (reactorItem == null) { return; } var reactor = reactorItem.GetComponent(); diff --git a/Barotrauma/BarotraumaShared/SharedSource/Events/MalfunctionEvent.cs b/Barotrauma/BarotraumaShared/SharedSource/Events/MalfunctionEvent.cs index 306a0f8a2..ab50f481b 100644 --- a/Barotrauma/BarotraumaShared/SharedSource/Events/MalfunctionEvent.cs +++ b/Barotrauma/BarotraumaShared/SharedSource/Events/MalfunctionEvent.cs @@ -41,7 +41,7 @@ namespace Barotrauma protected override void InitEventSpecific(EventSet parentSet) { - var matchingItems = Item.ItemList.FindAll(i => i.Condition > 0.0f && targetItemIdentifiers.Contains(i.Prefab.Identifier)); + var matchingItems = Item.ItemList.Where(i => i.Condition > 0.0f && targetItemIdentifiers.Contains(i.Prefab.Identifier)).ToList(); int itemAmount = Rand.Range(minItemAmount, maxItemAmount, Rand.RandSync.ServerAndClient); for (int i = 0; i < itemAmount; i++) { diff --git a/Barotrauma/BarotraumaShared/SharedSource/Events/Missions/AbandonedOutpostMission.cs b/Barotrauma/BarotraumaShared/SharedSource/Events/Missions/AbandonedOutpostMission.cs index 09703cf05..730d77e44 100644 --- a/Barotrauma/BarotraumaShared/SharedSource/Events/Missions/AbandonedOutpostMission.cs +++ b/Barotrauma/BarotraumaShared/SharedSource/Events/Missions/AbandonedOutpostMission.cs @@ -111,7 +111,7 @@ namespace Barotrauma { if (!itemTag.IsEmpty) { - var itemsToDestroy = Item.ItemList.FindAll(it => it.Submarine?.Info.Type != SubmarineType.Player && it.HasTag(itemTag)); + var itemsToDestroy = Item.ItemList.Where(it => it.Submarine?.Info.Type != SubmarineType.Player && it.HasTag(itemTag)).ToList(); if (!itemsToDestroy.Any()) { DebugConsole.ThrowError($"Error in mission \"{Prefab.Identifier}\". Could not find an item with the tag \"{itemTag}\".", diff --git a/Barotrauma/BarotraumaShared/SharedSource/Events/Missions/EndMission.cs b/Barotrauma/BarotraumaShared/SharedSource/Events/Missions/EndMission.cs index 1ffc09b93..513cdd8c5 100644 --- a/Barotrauma/BarotraumaShared/SharedSource/Events/Missions/EndMission.cs +++ b/Barotrauma/BarotraumaShared/SharedSource/Events/Missions/EndMission.cs @@ -181,7 +181,7 @@ namespace Barotrauma return; } destructibleItems.Clear(); - destructibleItems.AddRange(Item.ItemList.FindAll(it => it.HasTag(destructibleItemTag))); + destructibleItems.AddRange(Item.ItemList.Where(it => it.HasTag(destructibleItemTag))); if (destructibleItems.None()) { DebugConsole.ThrowError($"Error in end mission \"{Prefab.Identifier}\". Could not find any destructible items with the tag \"{spawnPointTag}\".", diff --git a/Barotrauma/BarotraumaShared/SharedSource/GameSession/CrewManager.cs b/Barotrauma/BarotraumaShared/SharedSource/GameSession/CrewManager.cs index cf243e4cc..b6d62e81f 100644 --- a/Barotrauma/BarotraumaShared/SharedSource/GameSession/CrewManager.cs +++ b/Barotrauma/BarotraumaShared/SharedSource/GameSession/CrewManager.cs @@ -128,7 +128,7 @@ namespace Barotrauma { foreach (var stackedItem in item.GetStackedItems()) { - Item.DeconstructItems.Add(stackedItem); + Item.MarkForDeconstruction(stackedItem); } #if CLIENT HintManager.OnItemMarkedForDeconstruction(order.OrderGiver); @@ -138,7 +138,7 @@ namespace Barotrauma { foreach (var stackedItem in item.GetStackedItems()) { - Item.DeconstructItems.Remove(stackedItem); + Item.UnmarkForDeconstruction(stackedItem); } } } diff --git a/Barotrauma/BarotraumaShared/SharedSource/Items/Components/Holdable/MeleeWeapon.cs b/Barotrauma/BarotraumaShared/SharedSource/Items/Components/Holdable/MeleeWeapon.cs index d4d45f120..15fe42183 100644 --- a/Barotrauma/BarotraumaShared/SharedSource/Items/Components/Holdable/MeleeWeapon.cs +++ b/Barotrauma/BarotraumaShared/SharedSource/Items/Components/Holdable/MeleeWeapon.cs @@ -3,6 +3,7 @@ using FarseerPhysics.Dynamics; using FarseerPhysics.Dynamics.Contacts; using Microsoft.Xna.Framework; using System; +using System.Collections.Concurrent; using System.Collections.Generic; using System.Collections.Immutable; using System.Linq; @@ -24,7 +25,7 @@ namespace Barotrauma.Items.Components private readonly HashSet hitTargets = new HashSet(); - private readonly Queue impactQueue = new Queue(); + private readonly ConcurrentQueue impactQueue = new ConcurrentQueue(); public Character User { get; private set; } @@ -190,17 +191,16 @@ namespace Barotrauma.Items.Components { if (!item.body.Enabled) { - impactQueue.Clear(); + while (impactQueue.TryDequeue(out _)) { } // Clear queue return; } if (picker == null || !picker.HeldItems.Contains(item)) { - impactQueue.Clear(); + while (impactQueue.TryDequeue(out _)) { } // Clear queue IsActive = false; } - while (impactQueue.Count > 0) + while (impactQueue.TryDequeue(out var impact)) { - var impact = impactQueue.Dequeue(); HandleImpact(impact); } //in case handling the impact does something to the picker @@ -300,7 +300,7 @@ namespace Barotrauma.Items.Components private void RestoreCollision() { - impactQueue.Clear(); + while (impactQueue.TryDequeue(out _)) { } // Clear queue item.body.FarseerBody.OnCollision -= OnCollision; item.body.CollisionCategories = Physics.CollisionItem; item.body.CollidesWith = Physics.DefaultItemCollidesWith; diff --git a/Barotrauma/BarotraumaShared/SharedSource/Items/Components/Projectile.cs b/Barotrauma/BarotraumaShared/SharedSource/Items/Components/Projectile.cs index 52df29696..9c2423f0b 100644 --- a/Barotrauma/BarotraumaShared/SharedSource/Items/Components/Projectile.cs +++ b/Barotrauma/BarotraumaShared/SharedSource/Items/Components/Projectile.cs @@ -5,6 +5,7 @@ using FarseerPhysics.Dynamics.Contacts; using FarseerPhysics.Dynamics.Joints; using Microsoft.Xna.Framework; using System; +using System.Collections.Concurrent; using System.Collections.Generic; using System.Collections.Immutable; using System.Linq; @@ -72,7 +73,7 @@ namespace Barotrauma.Items.Components public const float WaterDragCoefficient = 0.1f; - private readonly Queue impactQueue = new Queue(); + private readonly ConcurrentQueue impactQueue = new ConcurrentQueue(); private bool removePending; @@ -840,9 +841,8 @@ namespace Barotrauma.Items.Components DisableProjectileCollisions(); } } - while (impactQueue.Count > 0) + while (impactQueue.TryDequeue(out var impact)) { - var impact = impactQueue.Dequeue(); HandleProjectileCollision(impact.Fixture, impact.Normal, impact.LinearVelocity); } diff --git a/Barotrauma/BarotraumaShared/SharedSource/Items/Components/Signal/Wire.cs b/Barotrauma/BarotraumaShared/SharedSource/Items/Components/Signal/Wire.cs index 2dd1fbd7f..a7d007824 100644 --- a/Barotrauma/BarotraumaShared/SharedSource/Items/Components/Signal/Wire.cs +++ b/Barotrauma/BarotraumaShared/SharedSource/Items/Components/Signal/Wire.cs @@ -723,11 +723,11 @@ namespace Barotrauma.Items.Components if (item0 == null && item1 != null) { - item0 = Item.ItemList.Find(it => it.GetComponent()?.DisconnectedWires.Contains(this) ?? false); + item0 = Item.ItemList.FirstOrDefault(it => it.GetComponent()?.DisconnectedWires.Contains(this) ?? false); } else if (item0 != null && item1 == null) { - item1 = Item.ItemList.Find(it => it.GetComponent()?.DisconnectedWires.Contains(this) ?? false); + item1 = Item.ItemList.FirstOrDefault(it => it.GetComponent()?.DisconnectedWires.Contains(this) ?? false); } if (item0 == null || item1 == null || nodes.Count == 0) { return; } diff --git a/Barotrauma/BarotraumaShared/SharedSource/Items/Item.cs b/Barotrauma/BarotraumaShared/SharedSource/Items/Item.cs index 83b869ba4..ed5916b09 100644 --- a/Barotrauma/BarotraumaShared/SharedSource/Items/Item.cs +++ b/Barotrauma/BarotraumaShared/SharedSource/Items/Item.cs @@ -14,7 +14,9 @@ using Barotrauma.Extensions; using Barotrauma.MapCreatures.Behavior; using MoonSharp.Interpreter; using System.Collections.Immutable; +using System.Threading; using Barotrauma.Abilities; +using HarmonyLib; #if CLIENT using Microsoft.Xna.Framework.Graphics; @@ -27,57 +29,172 @@ namespace Barotrauma #region Lists /// - /// A list of every item that exists somewhere in the world. Note that there can be a huge number of items in the list, - /// and you probably shouldn't be enumerating it to find some that match some specific criteria (unless that's done very, very sparsely or during initialization). + /// Thread-safe dictionary of all items by ID. /// - public static readonly List ItemList = new List(); + private static readonly ConcurrentDictionary _itemDictionary = new ConcurrentDictionary(); - private static readonly HashSet _dangerousItems = new HashSet(); + /// + /// Provides thread-safe enumeration over all items. + /// + public static ICollection ItemList => _itemDictionary.Values; + + /// + /// Thread-safe item lookup by ID. + /// + public static Item GetItemById(ushort id) + { + _itemDictionary.TryGetValue(id, out var item); + return item; + } + + // Thread-safe optimized item collections using Immutable + atomic swap pattern + private static volatile ImmutableHashSet _dangerousItems = ImmutableHashSet.Empty; + private static volatile ImmutableHashSet _repairableItems = ImmutableHashSet.Empty; + private static volatile ImmutableHashSet _cleanableItems = ImmutableHashSet.Empty; + private static volatile ImmutableHashSet _sonarVisibleItems = ImmutableHashSet.Empty; + private static volatile ImmutableHashSet _turretTargetItems = ImmutableHashSet.Empty; + private static volatile ImmutableHashSet _chairItems = ImmutableHashSet.Empty; + + // DeconstructItems uses ConcurrentDictionary to simulate a thread-safe HashSet + private static readonly ConcurrentDictionary _deconstructItems = new ConcurrentDictionary(); public static IReadOnlyCollection DangerousItems => _dangerousItems; - private static readonly List _repairableItems = new List(); - /// /// Items that have one more more Repairable component /// public static IReadOnlyCollection RepairableItems => _repairableItems; - private static readonly List _cleanableItems = new List(); - /// /// Items that may potentially need to be cleaned up (pickable, not attached to a wall, and not inside a valid container) /// public static IReadOnlyCollection CleanableItems => _cleanableItems; - private static readonly HashSet _deconstructItems = new HashSet(); - /// - /// Items that have been marked for deconstruction + /// Items that have been marked for deconstruction. Thread-safe collection. /// - public static HashSet DeconstructItems => _deconstructItems; - - private static readonly List _sonarVisibleItems = new List(); + public static ICollection DeconstructItems => _deconstructItems.Keys; /// /// Items whose is larger than 0 /// public static IReadOnlyCollection SonarVisibleItems => _sonarVisibleItems; - private static readonly List _turretTargetItems = new List(); - /// /// Items whose is true. /// public static IReadOnlyCollection TurretTargetItems => _turretTargetItems; - private static readonly List _chairItems = new List(); - /// /// Items that have the tag . Which is an oddly specific thing, but useful as an optimization for NPC AI. /// public static IReadOnlyCollection ChairItems => _chairItems; + #region Thread-safe collection helpers + + /// + /// Atomically adds an item to an immutable set using compare-and-swap. + /// + private static void AddToImmutableSet(ref ImmutableHashSet location, Item item) + { + ImmutableHashSet original, updated; + do + { + original = location; + updated = original.Add(item); + if (ReferenceEquals(original, updated)) return; // Already exists + } + while (Interlocked.CompareExchange(ref location, updated, original) != original); + } + + /// + /// Atomically removes an item from an immutable set using compare-and-swap. + /// + private static void RemoveFromImmutableSet(ref ImmutableHashSet location, Item item) + { + ImmutableHashSet original, updated; + do + { + original = location; + updated = original.Remove(item); + if (ReferenceEquals(original, updated)) return; // Doesn't exist + } + while (Interlocked.CompareExchange(ref location, updated, original) != original); + } + + /// + /// Marks an item for deconstruction (thread-safe). + /// + public static void MarkForDeconstruction(Item item) + { + _deconstructItems.TryAdd(item, 0); + } + + /// + /// Unmarks an item for deconstruction (thread-safe). + /// + public static void UnmarkForDeconstruction(Item item) + { + _deconstructItems.TryRemove(item, out _); + } + + /// + /// Checks if an item is marked for deconstruction (thread-safe). + /// + public static bool IsMarkedForDeconstruction(Item item) + { + return _deconstructItems.ContainsKey(item); + } + + /// + /// Clears all item collections (thread-safe). Used during unloading. + /// + public static void ClearAllItemCollections() + { + _itemDictionary.Clear(); + _dangerousItems = ImmutableHashSet.Empty; + _repairableItems = ImmutableHashSet.Empty; + _cleanableItems = ImmutableHashSet.Empty; + _sonarVisibleItems = ImmutableHashSet.Empty; + _turretTargetItems = ImmutableHashSet.Empty; + _chairItems = ImmutableHashSet.Empty; + _deconstructItems.Clear(); + while (_pendingConditionUpdates.TryDequeue(out _)) { } + _cachedItemList = null; + _cachedItemListVersion = -1; + } + + // Cached item list for indexed access (used by AI systems) + private static volatile List _cachedItemList; + private static volatile int _cachedItemListVersion = -1; + private static volatile int _itemListVersion; + + /// + /// Gets a cached list snapshot of all items for indexed access. + /// The list is refreshed when items are added or removed. + /// Thread-safe but may return slightly stale data. + /// + public static List GetCachedItemList() + { + int currentVersion = _itemListVersion; + if (_cachedItemList == null || _cachedItemListVersion != currentVersion) + { + _cachedItemList = _itemDictionary.Values.ToList(); + _cachedItemListVersion = currentVersion; + } + return _cachedItemList; + } + + /// + /// Called when items are added or removed to invalidate the cached list. + /// + private static void InvalidateCachedItemList() + { + Interlocked.Increment(ref _itemListVersion); + } + + #endregion + #endregion public new ItemPrefab Prefab => base.Prefab as ItemPrefab; @@ -179,7 +296,12 @@ namespace Barotrauma private bool transformDirty = true; - private static readonly List itemsWithPendingConditionUpdates = new List(); + private static readonly ConcurrentQueue _pendingConditionUpdates = new ConcurrentQueue(); + + /// + /// Flag to avoid duplicate enqueue for pending condition updates. + /// + private volatile bool _hasPendingConditionUpdate; private float lastSentCondition; private float sendConditionUpdateTimer; @@ -845,11 +967,11 @@ namespace Barotrauma isDangerous = value; if (!value) { - _dangerousItems.Remove(this); + RemoveFromImmutableSet(ref _dangerousItems, this); } else { - _dangerousItems.Add(this); + AddToImmutableSet(ref _dangerousItems, this); } } } @@ -1398,12 +1520,13 @@ namespace Barotrauma } InsertToList(); - ItemList.Add(this); - if (Prefab.IsDangerous) { _dangerousItems.Add(this); } - if (Repairables.Any()) { _repairableItems.Add(this); } - if (Prefab.SonarSize > 0.0f) { _sonarVisibleItems.Add(this); } - if (Prefab.IsAITurretTarget) { _turretTargetItems.Add(this); } - if (Prefab.Tags.Contains(Barotrauma.Tags.ChairItem)) { _chairItems.Add(this); } + _itemDictionary.TryAdd(ID, this); + InvalidateCachedItemList(); + if (Prefab.IsDangerous) { AddToImmutableSet(ref _dangerousItems, this); } + if (Repairables.Any()) { AddToImmutableSet(ref _repairableItems, this); } + if (Prefab.SonarSize > 0.0f) { AddToImmutableSet(ref _sonarVisibleItems, this); } + if (Prefab.IsAITurretTarget) { AddToImmutableSet(ref _turretTargetItems, this); } + if (Prefab.Tags.Contains(Barotrauma.Tags.ChairItem)) { AddToImmutableSet(ref _chairItems, this); } CheckCleanable(); DebugConsole.Log("Created " + Name + " (" + ID + ")"); @@ -1756,14 +1879,11 @@ namespace Barotrauma Prefab.PreferredContainers.Any() && (container == null || container.HasTag(Barotrauma.Tags.AllowCleanup))) { - if (!_cleanableItems.Contains(this)) - { - _cleanableItems.Add(this); - } + AddToImmutableSet(ref _cleanableItems, this); } else { - _cleanableItems.Remove(this); + RemoveFromImmutableSet(ref _cleanableItems, this); } } @@ -2294,9 +2414,10 @@ namespace Barotrauma { needsConditionUpdate = true; } - if (needsConditionUpdate && !itemsWithPendingConditionUpdates.Contains(this)) + if (needsConditionUpdate && !_hasPendingConditionUpdate) { - itemsWithPendingConditionUpdates.Add(this); + _hasPendingConditionUpdate = true; + _pendingConditionUpdates.Enqueue(this); } } @@ -2352,9 +2473,9 @@ namespace Barotrauma public void SendPendingNetworkUpdates() { if (!(GameMain.NetworkMember is { IsServer: true })) { return; } - if (!itemsWithPendingConditionUpdates.Contains(this)) { return; } + if (!_hasPendingConditionUpdate) { return; } SendPendingNetworkUpdatesInternal(); - itemsWithPendingConditionUpdates.Remove(this); + _hasPendingConditionUpdate = false; } private void SendPendingNetworkUpdatesInternal() @@ -2383,21 +2504,35 @@ namespace Barotrauma public static void UpdatePendingConditionUpdates(float deltaTime) { if (GameMain.NetworkMember is not { IsServer: true }) { return; } - for (int i = 0; i < itemsWithPendingConditionUpdates.Count; i++) + + int count = _pendingConditionUpdates.Count; + for (int i = 0; i < count; i++) { - var item = itemsWithPendingConditionUpdates[i]; + if (!_pendingConditionUpdates.TryDequeue(out var item)) { break; } + if (item == null || item.Removed) { - itemsWithPendingConditionUpdates.RemoveAt(i--); + item._hasPendingConditionUpdate = false; + continue; + } + + if (item.Submarine is { Loading: true }) + { + // Re-enqueue, still loading + _pendingConditionUpdates.Enqueue(item); continue; } - if (item.Submarine is { Loading: true }) { continue; } item.sendConditionUpdateTimer -= deltaTime; if (item.sendConditionUpdateTimer <= 0.0f) { item.SendPendingNetworkUpdatesInternal(); - itemsWithPendingConditionUpdates.RemoveAt(i--); + item._hasPendingConditionUpdate = false; + } + else + { + // Not ready yet, re-enqueue + _pendingConditionUpdates.Enqueue(item); } } } @@ -4217,7 +4352,7 @@ namespace Barotrauma } } - if (element.GetAttributeBool("markedfordeconstruction", false)) { _deconstructItems.Add(item); } + if (element.GetAttributeBool("markedfordeconstruction", false)) { _deconstructItems.TryAdd(item, 0); } float prevRotation = item.Rotation; if (element.GetAttributeBool("flippedx", false)) { item.FlipX(relativeToSub: false, force: true); } @@ -4510,7 +4645,7 @@ namespace Barotrauma new XAttribute("name", Prefab.OriginalName), new XAttribute("identifier", Prefab.Identifier), new XAttribute("ID", ID), - new XAttribute("markedfordeconstruction", _deconstructItems.Contains(this))); + new XAttribute("markedfordeconstruction", _deconstructItems.ContainsKey(this))); if (PendingItemSwap != null) { @@ -4713,14 +4848,16 @@ namespace Barotrauma private void RemoveFromLists() { - ItemList.Remove(this); - _dangerousItems.Remove(this); - _repairableItems.Remove(this); - _sonarVisibleItems.Remove(this); - _cleanableItems.Remove(this); - _deconstructItems.Remove(this); - _turretTargetItems.Remove(this); - _chairItems.Remove(this); + _itemDictionary.TryRemove(ID, out _); + InvalidateCachedItemList(); + RemoveFromImmutableSet(ref _dangerousItems, this); + RemoveFromImmutableSet(ref _repairableItems, this); + RemoveFromImmutableSet(ref _sonarVisibleItems, this); + RemoveFromImmutableSet(ref _cleanableItems, this); + _deconstructItems.TryRemove(this, out _); + RemoveFromImmutableSet(ref _turretTargetItems, this); + RemoveFromImmutableSet(ref _chairItems, this); + _hasPendingConditionUpdate = false; RemoveFromDroppedStack(allowClientExecute: true); } diff --git a/Barotrauma/BarotraumaShared/SharedSource/Map/Entity.cs b/Barotrauma/BarotraumaShared/SharedSource/Map/Entity.cs index 3798ce08b..d9665886e 100644 --- a/Barotrauma/BarotraumaShared/SharedSource/Map/Entity.cs +++ b/Barotrauma/BarotraumaShared/SharedSource/Map/Entity.cs @@ -261,7 +261,7 @@ namespace Barotrauma DebugConsole.ThrowError($"Error while removing item \"{item}\"", exception); } } - Item.ItemList.Clear(); + Item.ClearAllItemCollections(); } if (Character.CharacterList.Count > 0) { diff --git a/Barotrauma/BarotraumaShared/SharedSource/Map/Levels/Level.cs b/Barotrauma/BarotraumaShared/SharedSource/Map/Levels/Level.cs index c5f0d161f..c2e123bd1 100644 --- a/Barotrauma/BarotraumaShared/SharedSource/Map/Levels/Level.cs +++ b/Barotrauma/BarotraumaShared/SharedSource/Map/Levels/Level.cs @@ -4775,7 +4775,7 @@ namespace Barotrauma // BeaconStation.FlipX(); // } - Item sonarItem = Item.ItemList.Find(it => it.Submarine == BeaconStation && it.GetComponent() != null); + Item sonarItem = Item.ItemList.FirstOrDefault(it => it.Submarine == BeaconStation && it.GetComponent() != null); if (sonarItem == null) { DebugConsole.ThrowError($"No sonar found in the beacon station \"{beaconStationName}\"!"); @@ -4794,7 +4794,7 @@ namespace Barotrauma throw new InvalidOperationException("Failed to prepare beacon station (no beacon station in the level)."); } - List beaconItems = Item.ItemList.FindAll(it => it.Submarine == BeaconStation); + List beaconItems = Item.ItemList.Where(it => it.Submarine == BeaconStation).ToList(); Item reactorItem = beaconItems.Find(it => it.GetComponent() != null); Reactor reactorComponent = null; @@ -4840,7 +4840,7 @@ namespace Barotrauma if (BeaconStation?.Info?.BeaconStationInfo is { AllowDisconnectedWires: false }) { return; } if (disconnectWireProbability <= 0.0f) { return; } - List beaconItems = Item.ItemList.FindAll(it => it.Submarine == BeaconStation); + List beaconItems = Item.ItemList.Where(it => it.Submarine == BeaconStation).ToList(); foreach (Item item in beaconItems.Where(it => it.GetComponent() != null).ToList()) { if (item.NonInteractable || item.InvulnerableToDamage) { continue; } @@ -4878,7 +4878,7 @@ namespace Barotrauma if (breakDeviceProbability <= 0.0f) { return; } //break powered items - List beaconItems = Item.ItemList.FindAll(it => it.Submarine == BeaconStation); + List beaconItems = Item.ItemList.Where(it => it.Submarine == BeaconStation).ToList(); foreach (Item item in beaconItems.Where(it => it.Components.Any(c => c is Powered) && it.Components.Any(c => c is Repairable))) { if (item.NonInteractable || item.InvulnerableToDamage) { continue; } diff --git a/Barotrauma/BarotraumaShared/SharedSource/Map/Map/Location.cs b/Barotrauma/BarotraumaShared/SharedSource/Map/Map/Location.cs index 637d5d2a8..1c45364fd 100644 --- a/Barotrauma/BarotraumaShared/SharedSource/Map/Map/Location.cs +++ b/Barotrauma/BarotraumaShared/SharedSource/Map/Map/Location.cs @@ -1371,7 +1371,7 @@ namespace Barotrauma { foreach (TakenItem takenItem in takenItems) { - Item item = Item.ItemList.Find(it => takenItem.Matches(it)); + Item item = Item.ItemList.FirstOrDefault(it => takenItem.Matches(it)); item?.Remove(); } } diff --git a/Barotrauma/BarotraumaShared/SharedSource/Map/Submarine.cs b/Barotrauma/BarotraumaShared/SharedSource/Map/Submarine.cs index f9e7a706d..969eca15a 100644 --- a/Barotrauma/BarotraumaShared/SharedSource/Map/Submarine.cs +++ b/Barotrauma/BarotraumaShared/SharedSource/Map/Submarine.cs @@ -1077,7 +1077,7 @@ namespace Barotrauma Item.UpdateHulls(); - List bodyItems = Item.ItemList.FindAll(it => it.Submarine == this && it.body != null); + List bodyItems = Item.ItemList.Where(it => it.Submarine == this && it.body != null).ToList(); List subEntities = MapEntity.MapEntityList.FindAll(me => me.Submarine == this); foreach (MapEntity e in subEntities) @@ -1507,7 +1507,7 @@ 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); + public List GetItems(bool alsoFromConnectedSubs) => GetEntities(alsoFromConnectedSubs, Item.ItemList).ToList(); public List GetWaypoints(bool alsoFromConnectedSubs) => GetEntities(alsoFromConnectedSubs, WayPoint.WayPointList); public List GetWalls(bool alsoFromConnectedSubs) => GetEntities(alsoFromConnectedSubs, Structure.WallList); @@ -2148,7 +2148,7 @@ namespace Barotrauma DebugConsole.ThrowError("Error while removing \"" + item.Name + "\"!", e); } } - Item.ItemList.Clear(); + Item.ClearAllItemCollections(); } Ragdoll.RemoveAll(); From 46595b139932d43724b0fb1c7e31e1ffd86919fc Mon Sep 17 00:00:00 2001 From: Eero Date: Sun, 28 Dec 2025 04:59:56 +0800 Subject: [PATCH 04/14] 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. --- .../SharedSource/GameSession/GameSession.cs | 2 +- .../Items/Components/DockingPort.cs | 20 +- .../SharedSource/Items/Components/Door.cs | 9 +- .../Items/Components/ElectricalDischarger.cs | 12 +- .../Items/Components/Holdable/Holdable.cs | 12 +- .../Items/Components/Holdable/RepairTool.cs | 32 ++-- .../Items/Components/ItemComponent.cs | 3 +- .../Items/Components/ItemContainer.cs | 3 +- .../SharedSource/Items/Components/Ladder.cs | 10 +- .../Items/Components/Machines/Controller.cs | 11 +- .../Items/Components/Machines/Sonar.cs | 23 ++- .../Items/Components/Power/PowerTransfer.cs | 119 ++++++------ .../Items/Components/Power/Powered.cs | 108 +++++++++-- .../Items/Components/Repairable.cs | 9 +- .../Items/Components/Signal/Connection.cs | 43 +++-- .../Components/Signal/ConnectionPanel.cs | 20 +- .../Items/Components/Signal/RelayComponent.cs | 6 +- .../Items/Components/Signal/WifiComponent.cs | 15 +- .../Items/Components/Signal/Wire.cs | 3 +- .../SharedSource/Items/Inventory.cs | 3 +- .../SharedSource/Items/Item.cs | 21 ++- .../SharedSource/Map/MapEntity.cs | 4 +- .../SharedSource/Map/Submarine.cs | 4 +- .../StatusEffects/DelayedEffect.cs | 96 +++++++--- .../StatusEffects/StatusEffect.cs | 176 ++++++++++++++---- 25 files changed, 519 insertions(+), 245 deletions(-) diff --git a/Barotrauma/BarotraumaShared/SharedSource/GameSession/GameSession.cs b/Barotrauma/BarotraumaShared/SharedSource/GameSession/GameSession.cs index f3782f134..1af627a98 100644 --- a/Barotrauma/BarotraumaShared/SharedSource/GameSession/GameSession.cs +++ b/Barotrauma/BarotraumaShared/SharedSource/GameSession/GameSession.cs @@ -1094,7 +1094,7 @@ namespace Barotrauma #endif //Clear the grids to allow for garbage collection Powered.Grids.Clear(); - Powered.ChangedConnections.Clear(); + Powered.ClearChangedConnections(); try { diff --git a/Barotrauma/BarotraumaShared/SharedSource/Items/Components/DockingPort.cs b/Barotrauma/BarotraumaShared/SharedSource/Items/Components/DockingPort.cs index be801e1eb..0046999ff 100644 --- a/Barotrauma/BarotraumaShared/SharedSource/Items/Components/DockingPort.cs +++ b/Barotrauma/BarotraumaShared/SharedSource/Items/Components/DockingPort.cs @@ -5,6 +5,7 @@ using FarseerPhysics.Dynamics; using FarseerPhysics.Dynamics.Joints; using Microsoft.Xna.Framework; using System; +using System.Collections.Concurrent; using System.Collections.Generic; using System.Linq; #if CLIENT @@ -24,11 +25,8 @@ namespace Barotrauma.Items.Components Right } - private static readonly List list = new List(); - public static IEnumerable List - { - get { return list; } - } + private static readonly ConcurrentDictionary _dockingPortDict = new ConcurrentDictionary(); + public static IEnumerable List => _dockingPortDict.Keys; private Sprite overlaySprite; private float dockingState; @@ -168,7 +166,7 @@ namespace Barotrauma.Items.Components IsActive = true; - list.Add(this); + _dockingPortDict.TryAdd(this, 0); } public override void FlipX(bool relativeToSub) @@ -200,7 +198,7 @@ namespace Barotrauma.Items.Components { float closestDist = float.MaxValue; 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; } float xDist = Math.Abs(port.item.WorldPosition.X - item.WorldPosition.X); @@ -532,8 +530,8 @@ namespace Barotrauma.Items.Components wire.TryConnect(recipient, addNode: false); //Flag connections to be updated - Powered.ChangedConnections.Add(powerConnection); - Powered.ChangedConnections.Add(recipient); + Powered.MarkConnectionChanged(powerConnection); + Powered.MarkConnectionChanged(recipient); } private void CreateDoorBody() @@ -1007,7 +1005,7 @@ namespace Barotrauma.Items.Components Connection powerConnection = Item.Connections.Find(c => c.IsPower); if (powerConnection != null) { - Powered.ChangedConnections.Add(powerConnection); + Powered.MarkConnectionChanged(powerConnection); } if (doorBody != null) @@ -1151,7 +1149,7 @@ namespace Barotrauma.Items.Components protected override void RemoveComponentSpecific() { base.RemoveComponentSpecific(); - list.Remove(this); + _dockingPortDict.TryRemove(this, out _); hulls[0]?.Remove(); hulls[0] = null; hulls[1]?.Remove(); hulls[1] = null; gap?.Remove(); gap = null; diff --git a/Barotrauma/BarotraumaShared/SharedSource/Items/Components/Door.cs b/Barotrauma/BarotraumaShared/SharedSource/Items/Components/Door.cs index 65b00c67c..fad7c7fab 100644 --- a/Barotrauma/BarotraumaShared/SharedSource/Items/Components/Door.cs +++ b/Barotrauma/BarotraumaShared/SharedSource/Items/Components/Door.cs @@ -2,6 +2,7 @@ using FarseerPhysics; using Microsoft.Xna.Framework; using System; +using System.Collections.Concurrent; using System.Collections.Generic; using System.Linq; using FarseerPhysics.Dynamics; @@ -14,9 +15,9 @@ namespace Barotrauma.Items.Components { partial class Door : Pickable, IDrawableComponent, IServerSerializable { - private static readonly HashSet doorList = new HashSet(); + private static readonly ConcurrentDictionary _doorDict = new ConcurrentDictionary(); - public static IReadOnlyCollection DoorList { get { return doorList; } } + public static ICollection DoorList => _doorDict.Keys; private Gap linkedGap; private bool isOpen; @@ -277,7 +278,7 @@ namespace Barotrauma.Items.Components } IsActive = true; - doorList.Add(this); + _doorDict.TryAdd(this, 0); } public override void OnItemLoaded() @@ -669,7 +670,7 @@ namespace Barotrauma.Items.Components convexHull2?.Remove(); #endif - doorList.Remove(this); + _doorDict.TryRemove(this, out _); } private bool CheckSubmarinesInDoorWay() diff --git a/Barotrauma/BarotraumaShared/SharedSource/Items/Components/ElectricalDischarger.cs b/Barotrauma/BarotraumaShared/SharedSource/Items/Components/ElectricalDischarger.cs index 4bdbe2d0f..e78319bf1 100644 --- a/Barotrauma/BarotraumaShared/SharedSource/Items/Components/ElectricalDischarger.cs +++ b/Barotrauma/BarotraumaShared/SharedSource/Items/Components/ElectricalDischarger.cs @@ -3,6 +3,7 @@ using Barotrauma.Networking; using FarseerPhysics; using Microsoft.Xna.Framework; using System; +using System.Collections.Concurrent; using System.Collections.Generic; using System.Linq; @@ -10,11 +11,8 @@ namespace Barotrauma.Items.Components { partial class ElectricalDischarger : Powered, IServerSerializable { - private static readonly List list = new List(); - public static IEnumerable List - { - get { return list; } - } + private static readonly ConcurrentDictionary _dischargerDict = new ConcurrentDictionary(); + public static IEnumerable List => _dischargerDict.Keys; const int MaxNodes = 100; const float MaxNodeDistance = 150.0f; @@ -115,7 +113,7 @@ namespace Barotrauma.Items.Components public ElectricalDischarger(Item item, ContentXElement element) : base(item, element) { - list.Add(this); + _dischargerDict.TryAdd(this, 0); foreach (var subElement in element.Elements()) { @@ -604,7 +602,7 @@ namespace Barotrauma.Items.Components protected override void RemoveComponentSpecific() { base.RemoveComponentSpecific(); - list.Remove(this); + _dischargerDict.TryRemove(this, out _); } public void ServerEventWrite(IWriteMessage msg, Client c, NetEntityEvent.IData extraData = null) diff --git a/Barotrauma/BarotraumaShared/SharedSource/Items/Components/Holdable/Holdable.cs b/Barotrauma/BarotraumaShared/SharedSource/Items/Components/Holdable/Holdable.cs index dacf4103a..429554361 100644 --- a/Barotrauma/BarotraumaShared/SharedSource/Items/Components/Holdable/Holdable.cs +++ b/Barotrauma/BarotraumaShared/SharedSource/Items/Components/Holdable/Holdable.cs @@ -9,6 +9,7 @@ using System; using System.Collections.Generic; using System.Collections.Immutable; using System.Linq; +using System.Threading; using System.Xml.Linq; namespace Barotrauma.Items.Components @@ -616,12 +617,13 @@ namespace Barotrauma.Items.Components return CanBeAttached(user, out _); } - private static List tempOverlappingItems = new List(); + private static readonly ThreadLocal> tempOverlappingItems = new ThreadLocal>(() => new List()); private bool CanBeAttached(Character user, out IEnumerable overlappingItems) { - tempOverlappingItems.Clear(); - overlappingItems = tempOverlappingItems; + var overlapping = tempOverlappingItems.Value; + overlapping.Clear(); + overlappingItems = overlapping; if (!attachable || !Reattachable) { return false; } //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.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 diff --git a/Barotrauma/BarotraumaShared/SharedSource/Items/Components/Holdable/RepairTool.cs b/Barotrauma/BarotraumaShared/SharedSource/Items/Components/Holdable/RepairTool.cs index f5c73ab42..63aaf6726 100644 --- a/Barotrauma/BarotraumaShared/SharedSource/Items/Components/Holdable/RepairTool.cs +++ b/Barotrauma/BarotraumaShared/SharedSource/Items/Components/Holdable/RepairTool.cs @@ -4,6 +4,7 @@ using Microsoft.Xna.Framework; using System; using System.Collections.Generic; using System.Linq; +using System.Threading; using Barotrauma.Extensions; using Barotrauma.MapCreatures.Behavior; @@ -315,7 +316,7 @@ namespace Barotrauma.Items.Components partial void UseProjSpecific(float deltaTime, Vector2 raystart); - private static readonly List hitBodies = new List(); + private static readonly ThreadLocal> hitBodies = new ThreadLocal>(() => new List()); private readonly HashSet hitCharacters = new HashSet(); private readonly List fireSourcesInRange = new List(); private void Repair(Vector2 rayStart, Vector2 rayEnd, float deltaTime, Character user, float degreeOfSuccess, List ignoredBodies) @@ -373,13 +374,13 @@ namespace Barotrauma.Items.Components }, allowInsideFixture: true); - hitBodies.Clear(); - hitBodies.AddRange(bodies.Distinct()); + hitBodies.Value.Clear(); + hitBodies.Value.AddRange(bodies.Distinct()); lastPickedFraction = Submarine.LastPickedFraction; Type lastHitType = null; hitCharacters.Clear(); - foreach (Body body in hitBodies) + foreach (Body body in hitBodies.Value) { Type bodyType = body.UserData?.GetType(); if (!RepairThroughWalls && bodyType != null && bodyType != lastHitType) @@ -897,48 +898,49 @@ namespace Barotrauma.Items.Components } } - private static List currentTargets = new List(); + private static readonly ThreadLocal> currentTargets = new ThreadLocal>(() => new List()); 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.TryGetValue(actionType, out List statusEffects)) { return; } + var targets = currentTargets.Value; foreach (StatusEffect effect in statusEffects) { - currentTargets.Clear(); + targets.Clear(); effect.SetUser(user); if (effect.HasTargetType(StatusEffect.TargetType.UseTarget)) { if (targetItem != null) { - currentTargets.AddRange(targetItem.AllPropertyObjects); + targets.AddRange(targetItem.AllPropertyObjects); } if (structure != null) { - currentTargets.Add(structure); + targets.Add(structure); } 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)) { - currentTargets.Add(user); - effect.Apply(actionType, deltaTime, item, currentTargets); + targets.Add(user); + effect.Apply(actionType, deltaTime, item, targets); } else if (effect.HasTargetType(StatusEffect.TargetType.Limb)) { - currentTargets.Add(limb); - effect.Apply(actionType, deltaTime, item, currentTargets); + targets.Add(limb); + effect.Apply(actionType, deltaTime, item, targets); } #if CLIENT if (user == null) { return; } // 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. - foreach (ISerializableEntity target in currentTargets) + foreach (ISerializableEntity target in targets) { if (target is not Door door) { continue; } if (!door.CanBeWelded || !door.Item.IsInteractable(user)) { continue; } diff --git a/Barotrauma/BarotraumaShared/SharedSource/Items/Components/ItemComponent.cs b/Barotrauma/BarotraumaShared/SharedSource/Items/Components/ItemComponent.cs index 8e156bb54..0ae99cd9e 100644 --- a/Barotrauma/BarotraumaShared/SharedSource/Items/Components/ItemComponent.cs +++ b/Barotrauma/BarotraumaShared/SharedSource/Items/Components/ItemComponent.cs @@ -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 (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 brokenEffects)) { continue; } foreach (var brokenEffect in brokenEffects) diff --git a/Barotrauma/BarotraumaShared/SharedSource/Items/Components/ItemContainer.cs b/Barotrauma/BarotraumaShared/SharedSource/Items/Components/ItemContainer.cs index ae7016391..722b63588 100644 --- a/Barotrauma/BarotraumaShared/SharedSource/Items/Components/ItemContainer.cs +++ b/Barotrauma/BarotraumaShared/SharedSource/Items/Components/ItemContainer.cs @@ -884,7 +884,8 @@ namespace Barotrauma.Items.Components RelatedItem containableItem = FindContainableItem(containedItem); 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; } diff --git a/Barotrauma/BarotraumaShared/SharedSource/Items/Components/Ladder.cs b/Barotrauma/BarotraumaShared/SharedSource/Items/Components/Ladder.cs index 73674fbf8..584089e75 100644 --- a/Barotrauma/BarotraumaShared/SharedSource/Items/Components/Ladder.cs +++ b/Barotrauma/BarotraumaShared/SharedSource/Items/Components/Ladder.cs @@ -1,17 +1,19 @@ -using System.Collections.Generic; +using System.Collections.Concurrent; +using System.Collections.Generic; using System.Xml.Linq; namespace Barotrauma.Items.Components { partial class Ladder : ItemComponent { - public static List List { get; } = new List(); + private static readonly ConcurrentDictionary _ladderDict = new ConcurrentDictionary(); + public static IEnumerable List => _ladderDict.Keys; public Ladder(Item item, ContentXElement element) : base(item, element) { InitProjSpecific(element); - List.Add(this); + _ladderDict.TryAdd(this, 0); } partial void InitProjSpecific(ContentXElement element); @@ -28,7 +30,7 @@ namespace Barotrauma.Items.Components { base.RemoveComponentSpecific(); RemoveProjSpecific(); - List.Remove(this); + _ladderDict.TryRemove(this, out _); } partial void RemoveProjSpecific(); diff --git a/Barotrauma/BarotraumaShared/SharedSource/Items/Components/Machines/Controller.cs b/Barotrauma/BarotraumaShared/SharedSource/Items/Components/Machines/Controller.cs index 899258e4f..9d032f85d 100644 --- a/Barotrauma/BarotraumaShared/SharedSource/Items/Components/Machines/Controller.cs +++ b/Barotrauma/BarotraumaShared/SharedSource/Items/Components/Machines/Controller.cs @@ -5,6 +5,7 @@ using System; using System.Collections.Generic; using System.Globalization; using System.Xml.Linq; +using System.Linq; 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); - 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 (item.LastSentSignalRecipients[i].Item.Prefab.FocusOnSelected) + if (signalRecipients[i].Item.Condition <= 0.0f || signalRecipients[i].IsPower) { continue; } + if (signalRecipients[i].Item.Prefab.FocusOnSelected) { - return item.LastSentSignalRecipients[i].Item; + return signalRecipients[i].Item; } } diff --git a/Barotrauma/BarotraumaShared/SharedSource/Items/Components/Machines/Sonar.cs b/Barotrauma/BarotraumaShared/SharedSource/Items/Components/Machines/Sonar.cs index bfd4aaafe..44cdf4398 100644 --- a/Barotrauma/BarotraumaShared/SharedSource/Items/Components/Machines/Sonar.cs +++ b/Barotrauma/BarotraumaShared/SharedSource/Items/Components/Machines/Sonar.cs @@ -1,14 +1,17 @@ using Barotrauma.Networking; using Microsoft.Xna.Framework; using System; +using System.Collections.Concurrent; using System.Collections.Generic; using System.Linq; +using System.Threading; namespace Barotrauma.Items.Components { partial class Sonar : Powered, IServerSerializable, IClientSerializable { - public static List SonarList = new List(); + private static readonly ConcurrentDictionary _sonarDict = new ConcurrentDictionary(); + public static IEnumerable SonarList => _sonarDict.Keys; public enum Mode { @@ -169,7 +172,7 @@ namespace Barotrauma.Items.Components IsActive = true; InitProjSpecific(element); CurrentMode = Mode.Passive; - SonarList.Add(this); + _sonarDict.TryAdd(this, 0); } partial void InitProjSpecific(ContentXElement element); @@ -291,13 +294,15 @@ namespace Barotrauma.Items.Components return currentPingIndex != -1 && (character == null || characterUsable); } - private static readonly Dictionary> targetGroups = new Dictionary>(); + private static readonly ThreadLocal>> targetGroups = + new ThreadLocal>>(() => new Dictionary>()); public override bool CrewAIOperate(float deltaTime, Character character, AIObjectiveOperateItem objective) { if (currentMode == Mode.Passive || !aiPingCheckPending) { return false; } - foreach (List targetGroup in targetGroups.Values) + var groups = targetGroups.Value; + foreach (List targetGroup in groups.Values) { targetGroup.Clear(); } @@ -310,14 +315,14 @@ namespace Barotrauma.Items.Components #warning This is not the best key for a dictionary. string directionName = GetDirectionName(c.WorldPosition - item.WorldPosition).Value; - if (!targetGroups.ContainsKey(directionName)) + if (!groups.ContainsKey(directionName)) { - targetGroups.Add(directionName, new List()); + groups.Add(directionName, new List()); } - targetGroups[directionName].Add(c); + groups[directionName].Add(c); } - foreach (KeyValuePair> targetGroup in targetGroups) + foreach (KeyValuePair> targetGroup in groups) { if (!targetGroup.Value.Any()) { continue; } string dialogTag = "DialogSonarTarget"; @@ -401,7 +406,7 @@ namespace Barotrauma.Items.Components MineralClusters = null; #endif - SonarList.Remove(this); + _sonarDict.TryRemove(this, out _); } diff --git a/Barotrauma/BarotraumaShared/SharedSource/Items/Components/Power/PowerTransfer.cs b/Barotrauma/BarotraumaShared/SharedSource/Items/Components/Power/PowerTransfer.cs index c110e8b9e..f2a44513c 100644 --- a/Barotrauma/BarotraumaShared/SharedSource/Items/Components/Power/PowerTransfer.cs +++ b/Barotrauma/BarotraumaShared/SharedSource/Items/Components/Power/PowerTransfer.cs @@ -1,6 +1,7 @@ using Barotrauma.Extensions; using Microsoft.Xna.Framework; using System; +using System.Collections.Concurrent; using System.Collections.Generic; using System.Linq; using System.Xml.Linq; @@ -13,10 +14,12 @@ namespace Barotrauma.Items.Components private readonly HashSet signalConnections = new HashSet(); - private readonly Dictionary connectionDirty = new Dictionary(); + private readonly ConcurrentDictionary connectionDirty = new ConcurrentDictionary(); //a list of connections a given connection is connected to, either directly or via other power transfer components - private readonly Dictionary> connectedRecipients = new Dictionary>(); + //Uses ConcurrentDictionary as a thread-safe HashSet replacement + private readonly ConcurrentDictionary> connectedRecipients = + new ConcurrentDictionary>(); private float overloadCooldownTimer; private const float OverloadCooldown = 5.0f; @@ -132,7 +135,7 @@ namespace Barotrauma.Items.Components partial void InitProjectSpecific(XElement element); - private static readonly HashSet recipientsToRefresh = new HashSet(); + private static readonly System.Collections.Concurrent.ConcurrentDictionary _recipientsToRefresh = new System.Collections.Concurrent.ConcurrentDictionary(); public override void UpdateBroken(float deltaTime, Camera cam) { base.UpdateBroken(deltaTime, cam); @@ -144,20 +147,21 @@ namespace Barotrauma.Items.Components powerLoad = 0.0f; currPowerConsumption = 0.0f; SetAllConnectionsDirty(); - recipientsToRefresh.Clear(); - foreach (HashSet recipientList in connectedRecipients.Values) + _recipientsToRefresh.Clear(); + // 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; } var recipientPowerTransfer = c.Item.GetComponent(); if (recipientPowerTransfer != null) { - recipientsToRefresh.Add(recipientPowerTransfer); + _recipientsToRefresh.TryAdd(recipientPowerTransfer, 0); } } } - foreach (PowerTransfer recipientPowerTransfer in recipientsToRefresh) + foreach (PowerTransfer recipientPowerTransfer in _recipientsToRefresh.Keys) { recipientPowerTransfer.SetAllConnectionsDirty(); recipientPowerTransfer.RefreshConnections(); @@ -304,58 +308,56 @@ namespace Barotrauma.Items.Components protected void RefreshConnections() { 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; + isDirty = true; } - else if (!connectionDirty[c]) + + if (!isDirty) { continue; } //find all connections that are connected to this one (directly or via another PowerTransfer) - HashSet tempConnected; - if (!connectedRecipients.ContainsKey(c)) + var tempConnected = connectedRecipients.GetOrAdd(c, _ => new ConcurrentDictionary()); + + // 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(); - connectedRecipients.Add(c, tempConnected); - } - else - { - tempConnected = connectedRecipients[c]; - tempConnected.Clear(); - //mark all previous recipients as dirty - foreach (Connection recipient in tempConnected) - { - var pt = recipient.Item.GetComponent(); - if (pt != null) { pt.connectionDirty[recipient] = true; } - } + var pt = recipient.Item.GetComponent(); + if (pt != null) { pt.connectionDirty[recipient] = true; } } - tempConnected.Add(c); + tempConnected.TryAdd(c, 0); if (item.Condition > 0.0f) { GetConnected(c, tempConnected); //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) - 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; } var recipientPowerTransfer = recipient.Item.GetComponent(); if (recipientPowerTransfer == null) { continue; } - if (!recipientPowerTransfer.connectedRecipients.ContainsKey(recipient)) + + var recipientSet = recipientPowerTransfer.connectedRecipients.GetOrAdd(recipient, _ => new ConcurrentDictionary()); + recipientSet.Clear(); + foreach (var connection in tempConnectedSnapshot) { - recipientPowerTransfer.connectedRecipients.Add(recipient, new HashSet()); - } - else - { - recipientPowerTransfer.connectedRecipients[recipient].Clear(); - } - foreach (var connection in tempConnected) - { - recipientPowerTransfer.connectedRecipients[recipient].Add(connection); + recipientSet.TryAdd(connection, 0); } 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. - private void GetConnected(Connection c, HashSet connected) + //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, ConcurrentDictionary connected) { - var recipients = c.Recipients; + // Take snapshot for thread-safe iteration + var recipients = c.Recipients.ToList(); foreach (Connection recipient in recipients) { - if (recipient == null || connected.Contains(recipient)) { continue; } + if (recipient == null || connected.ContainsKey(recipient)) { continue; } Item it = recipient.Item; if (it == null || it.Condition <= 0.0f) { continue; } - connected.Add(recipient); + connected.TryAdd(recipient, 0); var powerTransfer = it.GetComponent(); if (powerTransfer != null && powerTransfer.CanTransfer && powerTransfer.IsActive) @@ -394,10 +397,14 @@ namespace Barotrauma.Items.Components connectionDirty[c] = true; if (c.IsPower) { - ChangedConnections.Add(c); + MarkConnectionChanged(c); 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; if (connection.IsPower) { - ChangedConnections.Add(connection); + MarkConnectionChanged(connection); 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) { 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; } - 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; } 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 //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)) { - 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); } @@ -484,7 +499,7 @@ namespace Barotrauma.Items.Components base.RemoveComponentSpecific(); connectedRecipients?.Clear(); connectionDirty?.Clear(); - recipientsToRefresh.Clear(); + _recipientsToRefresh.Clear(); } } } diff --git a/Barotrauma/BarotraumaShared/SharedSource/Items/Components/Power/Powered.cs b/Barotrauma/BarotraumaShared/SharedSource/Items/Components/Power/Powered.cs index 962b6a132..8d14850c0 100644 --- a/Barotrauma/BarotraumaShared/SharedSource/Items/Components/Power/Powered.cs +++ b/Barotrauma/BarotraumaShared/SharedSource/Items/Components/Power/Powered.cs @@ -1,4 +1,6 @@ using System; +using System.Collections.Concurrent; +using System.Threading; using Microsoft.Xna.Framework; using System.Collections.Generic; using System.Linq; @@ -62,17 +64,77 @@ namespace Barotrauma.Items.Components protected const float UpdateInterval = (float)Timing.Step; /// - /// List of all powered ItemComponents + /// List of all powered ItemComponents (thread-safe) /// - private static readonly List poweredList = new List(); + private static readonly ConcurrentDictionary _poweredDict = new ConcurrentDictionary(); + + /// + /// Cached list for iteration - updated when collection changes + /// + private static volatile List _cachedPoweredList; + private static int _poweredListVersion; + public static IEnumerable PoweredList { - get { return poweredList; } + get + { + var cached = _cachedPoweredList; + if (cached != null) return cached; + return GetCachedPoweredList(); + } + } + + private static List GetCachedPoweredList() + { + var newList = _poweredDict.Keys.ToList(); + _cachedPoweredList = newList; + return newList; + } + + private static void InvalidatePoweredListCache() + { + _cachedPoweredList = null; + Interlocked.Increment(ref _poweredListVersion); } - public static readonly HashSet ChangedConnections = new HashSet(); + /// + /// Thread-safe set of changed connections + /// + private static readonly ConcurrentDictionary _changedConnections = new ConcurrentDictionary(); + + /// + /// Gets all changed connections (snapshot) + /// + public static ICollection ChangedConnections => _changedConnections.Keys; + + /// + /// Add a connection to the changed set + /// + public static void MarkConnectionChanged(Connection c) + { + _changedConnections.TryAdd(c, 0); + } + + /// + /// Clear all changed connections + /// + public static void ClearChangedConnections() + { + _changedConnections.Clear(); + } + + /// + /// Remove a connection from the changed set + /// + public static void UnmarkConnectionChanged(Connection c) + { + _changedConnections.TryRemove(c, out _); + } - public readonly static Dictionary Grids = new Dictionary(); + /// + /// Thread-safe grid dictionary + /// + public readonly static ConcurrentDictionary Grids = new ConcurrentDictionary(); /// /// 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) : base(item, element) { - poweredList.Add(this); + _poweredDict.TryAdd(this, 0); + InvalidatePoweredListCache(); InitProjectSpecific(element); } @@ -322,17 +385,20 @@ namespace Barotrauma.Items.Components //don't use cache if there are no existing grids if (Grids.Count > 0 && useCache) { + // Take a snapshot of changed connections for iteration + var changedSnapshot = ChangedConnections.ToList(); + //delete all grids that were affected - foreach (Connection c in ChangedConnections) + foreach (Connection c in changedSnapshot) { if (c.Grid != null) { - Grids.Remove(c.Grid.ID); + Grids.TryRemove(c.Grid.ID, out _); 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 //Ensure the connection has other connections @@ -346,7 +412,7 @@ namespace Barotrauma.Items.Components else { //Clear all grid IDs from connections - foreach (Powered powered in poweredList) + foreach (Powered powered in PoweredList) { //Only check devices with connectors if (powered.powerIn != null) @@ -361,7 +427,7 @@ namespace Barotrauma.Items.Components Grids.Clear(); - foreach (Powered powered in poweredList) + foreach (Powered powered in PoweredList) { if (powered.Item.Condition <= 0f) { continue; } @@ -392,7 +458,7 @@ namespace Barotrauma.Items.Components } //Clear changed connections after each update - ChangedConnections.Clear(); + ClearChangedConnections(); } private static GridInfo PropagateGrid(Connection conn) @@ -422,8 +488,8 @@ namespace Barotrauma.Items.Components c.Grid = grid; grid.AddConnection(c); - //Add on recipients - foreach (Connection otherC in c.Recipients) + //Add on recipients - use ToList() snapshot for thread-safe iteration + foreach (Connection otherC in c.Recipients.ToList()) { //Only add valid connections 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 - foreach (Powered powered in poweredList) + foreach (Powered powered in PoweredList) { //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) @@ -730,7 +796,8 @@ namespace Barotrauma.Items.Components { 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.Item?.GetComponent() is PowerContainer battery) @@ -750,13 +817,14 @@ namespace Barotrauma.Items.Components { if (c.IsPower && c.Grid != null) { - ChangedConnections.Add(c); + MarkConnectionChanged(c); } } } base.RemoveComponentSpecific(); - poweredList.Remove(this); + _poweredDict.TryRemove(this, out _); + InvalidatePoweredListCache(); } } @@ -780,9 +848,9 @@ namespace Barotrauma.Items.Components Connections.Remove(c); //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 _); } } diff --git a/Barotrauma/BarotraumaShared/SharedSource/Items/Components/Repairable.cs b/Barotrauma/BarotraumaShared/SharedSource/Items/Components/Repairable.cs index 1aebf8187..d2c7d4525 100644 --- a/Barotrauma/BarotraumaShared/SharedSource/Items/Components/Repairable.cs +++ b/Barotrauma/BarotraumaShared/SharedSource/Items/Components/Repairable.cs @@ -456,7 +456,8 @@ namespace Barotrauma.Items.Components 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) { @@ -713,7 +714,8 @@ namespace Barotrauma.Items.Components #endif 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) { @@ -761,7 +763,8 @@ namespace Barotrauma.Items.Components 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) { diff --git a/Barotrauma/BarotraumaShared/SharedSource/Items/Components/Signal/Connection.cs b/Barotrauma/BarotraumaShared/SharedSource/Items/Components/Signal/Connection.cs index e14929804..d55150237 100644 --- a/Barotrauma/BarotraumaShared/SharedSource/Items/Components/Signal/Connection.cs +++ b/Barotrauma/BarotraumaShared/SharedSource/Items/Components/Signal/Connection.cs @@ -222,13 +222,14 @@ namespace Barotrauma.Items.Components public void SetRecipientsDirty() { recipientsDirty = true; - if (IsPower) { Powered.ChangedConnections.Add(this); } + if (IsPower) { Powered.MarkConnectionChanged(this); } } private void RefreshRecipients() { 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); if (recipient != null) { recipients.Add(recipient); } @@ -267,8 +268,8 @@ namespace Barotrauma.Items.Components //Check if both connections belong to a larger grid if (prevOtherConnection.recipients.Count > 1 && recipients.Count > 1) { - Powered.ChangedConnections.Add(prevOtherConnection); - Powered.ChangedConnections.Add(this); + Powered.MarkConnectionChanged(prevOtherConnection); + Powered.MarkConnectionChanged(this); } else if (recipients.Count > 1) { @@ -284,7 +285,7 @@ namespace Barotrauma.Items.Components else if (Grid.Connections.Count == 2) { //Delete the grid as these were the only 2 devices - Powered.Grids.Remove(Grid.ID); + Powered.Grids.TryRemove(Grid.ID, out _); Grid = null; prevOtherConnection.Grid = null; } @@ -325,8 +326,8 @@ namespace Barotrauma.Items.Components else { //Flag change so that proper grids can be formed - Powered.ChangedConnections.Add(this); - Powered.ChangedConnections.Add(otherConnection); + Powered.MarkConnectionChanged(this); + Powered.MarkConnectionChanged(otherConnection); } } @@ -339,7 +340,8 @@ namespace Barotrauma.Items.Components { LastSentSignal = signal; 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); if (recipient == null) { continue; } @@ -354,14 +356,14 @@ namespace Barotrauma.Items.Components 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); GameMain.LuaCs.Hook.Call("signalReceived", signal, connection.Connection); GameMain.LuaCs.Hook.Call("signalReceived." + connection.Connection.Item.Prefab.Identifier, signal, connection); } enumeratingWires = false; - foreach (var removedWire in removedWires) + foreach (var removedWire in removedWires.ToArray()) { wires.Remove(removedWire); } @@ -372,14 +374,16 @@ namespace Barotrauma.Items.Components { 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); } 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); } @@ -389,13 +393,15 @@ namespace Barotrauma.Items.Components { if (IsPower && Grid != null) { - Powered.ChangedConnections.Add(this); - foreach (Connection c in recipients) + Powered.MarkConnectionChanged(this); + // 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); recipientsDirty = true; @@ -403,7 +409,7 @@ namespace Barotrauma.Items.Components if (enumeratingWires) { - foreach (var wire in wires) + foreach (var wire in wires.ToArray()) { removedWires.Add(wire); } @@ -446,7 +452,8 @@ namespace Barotrauma.Items.Components { 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", new XAttribute("w", wire.Item.ID.ToString()), diff --git a/Barotrauma/BarotraumaShared/SharedSource/Items/Components/Signal/ConnectionPanel.cs b/Barotrauma/BarotraumaShared/SharedSource/Items/Components/Signal/ConnectionPanel.cs index 5fddc96c6..e25edc896 100644 --- a/Barotrauma/BarotraumaShared/SharedSource/Items/Components/Signal/ConnectionPanel.cs +++ b/Barotrauma/BarotraumaShared/SharedSource/Items/Components/Signal/ConnectionPanel.cs @@ -148,14 +148,16 @@ namespace Barotrauma.Items.Components Vector2 wireNodeOffset = item.Submarine == null ? Vector2.Zero : item.Submarine.HiddenSubPosition + amount; 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; } TryMoveWire(wire); } } - foreach (var wire in DisconnectedWires) + // Use ToList() snapshot for thread-safe iteration + foreach (var wire in DisconnectedWires.ToList()) { TryMoveWire(wire); } @@ -387,7 +389,7 @@ namespace Barotrauma.Items.Components } foreach (var connection in Connections) { - Powered.ChangedConnections.Remove(connection); + Powered.UnmarkConnectionChanged(connection); connection.Recipients.Clear(); } Connections.Clear(); @@ -412,15 +414,19 @@ namespace Barotrauma.Items.Components msg.WriteByte((byte)Connections.Count); foreach (Connection connection in Connections) { - msg.WriteVariableUInt32((uint)connection.Wires.Count); - foreach (Wire wire in connection.Wires) + // Use ToArray() snapshot for thread-safe iteration + var wiresSnapshot = connection.Wires.ToArray(); + msg.WriteVariableUInt32((uint)wiresSnapshot.Length); + foreach (Wire wire in wiresSnapshot) { msg.WriteUInt16(wire?.Item == null ? (ushort)0 : wire.Item.ID); } } - msg.WriteUInt16((ushort)DisconnectedWires.Count); - foreach (Wire disconnectedWire in DisconnectedWires) + // Use ToList() snapshot for thread-safe iteration + var disconnectedSnapshot = DisconnectedWires.ToList(); + msg.WriteUInt16((ushort)disconnectedSnapshot.Count); + foreach (Wire disconnectedWire in disconnectedSnapshot) { msg.WriteUInt16(disconnectedWire.Item.ID); } diff --git a/Barotrauma/BarotraumaShared/SharedSource/Items/Components/Signal/RelayComponent.cs b/Barotrauma/BarotraumaShared/SharedSource/Items/Components/Signal/RelayComponent.cs index ea7c81e90..adb51a1ff 100644 --- a/Barotrauma/BarotraumaShared/SharedSource/Items/Components/Signal/RelayComponent.cs +++ b/Barotrauma/BarotraumaShared/SharedSource/Items/Components/Signal/RelayComponent.cs @@ -2,6 +2,7 @@ using Microsoft.Xna.Framework; using System; using System.Collections.Generic; +using System.Collections.Immutable; using System.Linq; using System.Xml.Linq; @@ -31,7 +32,8 @@ namespace Barotrauma.Items.Components private float thirdInverseMax = 0, loadEqnConstant = 0; - private static readonly Dictionary connectionPairs = new Dictionary + // Thread-safe immutable dictionary for connection pairs (read-only after initialization) + private static readonly ImmutableDictionary connectionPairs = new Dictionary { { "power_in", "power_out"}, { "signal_in", "signal_out" }, @@ -40,7 +42,7 @@ namespace Barotrauma.Items.Components { "signal_in3", "signal_out3" }, { "signal_in4", "signal_out4" }, { "signal_in5", "signal_out5" } - }; + }.ToImmutableDictionary(); protected override PowerPriority Priority { get { return PowerPriority.Relay; } } diff --git a/Barotrauma/BarotraumaShared/SharedSource/Items/Components/Signal/WifiComponent.cs b/Barotrauma/BarotraumaShared/SharedSource/Items/Components/Signal/WifiComponent.cs index e59ab6599..68a978626 100644 --- a/Barotrauma/BarotraumaShared/SharedSource/Items/Components/Signal/WifiComponent.cs +++ b/Barotrauma/BarotraumaShared/SharedSource/Items/Components/Signal/WifiComponent.cs @@ -1,6 +1,7 @@ using Barotrauma.Networking; using Microsoft.Xna.Framework; using System; +using System.Collections.Concurrent; using System.Collections.Generic; using System.Globalization; using System.Linq; @@ -10,7 +11,8 @@ namespace Barotrauma.Items.Components { partial class WifiComponent : ItemComponent, IServerSerializable, IClientSerializable { - private static readonly List list = new List(); + private static readonly ConcurrentDictionary _wifiDict = new ConcurrentDictionary(); + private static IEnumerable AllWifiComponents => _wifiDict.Keys; const int ChannelMemorySize = 10; @@ -111,7 +113,7 @@ namespace Barotrauma.Items.Components public WifiComponent(Item item, ContentXElement element) : base (item, element) { - list.Add(this); + _wifiDict.TryAdd(this, 0); IsActive = true; } @@ -156,7 +158,7 @@ namespace Barotrauma.Items.Components /// public IEnumerable GetReceiversInRange() { - return list.Where(w => w != this && w.CanReceive(this)); + return AllWifiComponents.Where(w => w != this && w.CanReceive(this)); } public bool CanReceive(WifiComponent sender) @@ -185,7 +187,7 @@ namespace Barotrauma.Items.Components /// public IEnumerable GetTransmittersInRange() { - return list.Where(w => w != this && w.CanTransmit(this)); + return AllWifiComponents.Where(w => w != this && w.CanTransmit(this)); } public bool CanTransmit(WifiComponent sender) @@ -275,7 +277,8 @@ namespace Barotrauma.Items.Components 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)) { @@ -366,7 +369,7 @@ namespace Barotrauma.Items.Components protected override void RemoveComponentSpecific() { base.RemoveComponentSpecific(); - list.Remove(this); + _wifiDict.TryRemove(this, out _); } public override XElement Save(XElement parentElement) diff --git a/Barotrauma/BarotraumaShared/SharedSource/Items/Components/Signal/Wire.cs b/Barotrauma/BarotraumaShared/SharedSource/Items/Components/Signal/Wire.cs index a7d007824..2db146e99 100644 --- a/Barotrauma/BarotraumaShared/SharedSource/Items/Components/Signal/Wire.cs +++ b/Barotrauma/BarotraumaShared/SharedSource/Items/Components/Signal/Wire.cs @@ -250,7 +250,8 @@ namespace Barotrauma.Items.Components { 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; } diff --git a/Barotrauma/BarotraumaShared/SharedSource/Items/Inventory.cs b/Barotrauma/BarotraumaShared/SharedSource/Items/Inventory.cs index d1be5145e..68c35aab3 100644 --- a/Barotrauma/BarotraumaShared/SharedSource/Items/Inventory.cs +++ b/Barotrauma/BarotraumaShared/SharedSource/Items/Inventory.cs @@ -381,7 +381,8 @@ namespace Barotrauma 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(); } diff --git a/Barotrauma/BarotraumaShared/SharedSource/Items/Item.cs b/Barotrauma/BarotraumaShared/SharedSource/Items/Item.cs index ed5916b09..c2576bb48 100644 --- a/Barotrauma/BarotraumaShared/SharedSource/Items/Item.cs +++ b/Barotrauma/BarotraumaShared/SharedSource/Items/Item.cs @@ -2441,10 +2441,11 @@ namespace Barotrauma { if (c.IsPower) { - Powered.ChangedConnections.Add(c); - foreach (Connection conn in c.Recipients) + Powered.MarkConnectionChanged(c); + // 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) { 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(); if (component != null) @@ -3013,7 +3015,8 @@ namespace Barotrauma foreach (Connection c in connectionPanel.Connections) { 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(); if (component != null && !connectedComponents.Contains(component)) @@ -3067,12 +3070,13 @@ namespace Barotrauma alreadySearched.Add(c); static IEnumerable 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; } //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; } @@ -3212,7 +3216,8 @@ namespace Barotrauma if (signal.stepsTaken > 5 && signal.source != null) { 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) { diff --git a/Barotrauma/BarotraumaShared/SharedSource/Map/MapEntity.cs b/Barotrauma/BarotraumaShared/SharedSource/Map/MapEntity.cs index 2634f9560..8c839934b 100644 --- a/Barotrauma/BarotraumaShared/SharedSource/Map/MapEntity.cs +++ b/Barotrauma/BarotraumaShared/SharedSource/Map/MapEntity.cs @@ -712,11 +712,11 @@ namespace Barotrauma try { - foreach (Item item in itemList) + Parallel.ForEach(itemList, parallelOptions, item => { lastUpdatedItem = item; item.Update(scaledDeltaTime, cam); - } + }); } catch (InvalidOperationException e) { diff --git a/Barotrauma/BarotraumaShared/SharedSource/Map/Submarine.cs b/Barotrauma/BarotraumaShared/SharedSource/Map/Submarine.cs index 969eca15a..dfe6eceb1 100644 --- a/Barotrauma/BarotraumaShared/SharedSource/Map/Submarine.cs +++ b/Barotrauma/BarotraumaShared/SharedSource/Map/Submarine.cs @@ -2157,7 +2157,7 @@ namespace Barotrauma GameMain.World = null; Powered.Grids.Clear(); - Powered.ChangedConnections.Clear(); + Powered.ClearChangedConnections(); GC.Collect(); @@ -2197,7 +2197,7 @@ namespace Barotrauma ConnectedDockingPorts?.Clear(); - Powered.ChangedConnections.Clear(); + Powered.ClearChangedConnections(); Powered.Grids.Clear(); loaded.Remove(this); diff --git a/Barotrauma/BarotraumaShared/SharedSource/StatusEffects/DelayedEffect.cs b/Barotrauma/BarotraumaShared/SharedSource/StatusEffects/DelayedEffect.cs index 9c9be6e95..b589b8d83 100644 --- a/Barotrauma/BarotraumaShared/SharedSource/StatusEffects/DelayedEffect.cs +++ b/Barotrauma/BarotraumaShared/SharedSource/StatusEffects/DelayedEffect.cs @@ -1,26 +1,56 @@ using Barotrauma.Items.Components; using Microsoft.Xna.Framework; using System; +using System.Collections.Concurrent; using System.Collections.Generic; using System.Linq; +using System.Threading; namespace Barotrauma { class DelayedListElement { + public readonly long Id; public readonly DelayedEffect Parent; 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; + } + } + } + /// /// 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. /// public bool GetPositionBasedOnTargets; public readonly Vector2? StartPosition; public readonly List Targets; - public float Delay; + + private volatile float _delay; + public float Delay + { + get => _delay; + set => _delay = value; + } public DelayedListElement(DelayedEffect parentEffect, Entity parentEntity, IEnumerable targets, float delay, Vector2? worldPosition, Vector2? startPosition) { + Id = Interlocked.Increment(ref DelayedEffect._delayElementIdCounter); Parent = parentEffect; Entity = parentEntity; Targets = new List(targets); @@ -29,9 +59,19 @@ namespace Barotrauma StartPosition = startPosition; } } + class DelayedEffect : StatusEffect { - public static readonly List DelayList = new List(); + // Thread-safe counter for generating unique IDs for DelayedListElement + internal static long _delayElementIdCounter; + + // Thread-safe dictionary for delayed effects + public static readonly ConcurrentDictionary DelayListDict = new ConcurrentDictionary(); + + /// + /// Provides a thread-safe enumerable view of the delay list for iteration. + /// + public static IEnumerable DelayList => DelayListDict.Values; private enum DelayTypes { @@ -62,9 +102,10 @@ namespace Barotrauma if (this.type != type || !HasRequiredItems(entity)) { return; } 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; } @@ -72,18 +113,19 @@ namespace Barotrauma } if (!IsValidTarget(target)) { return; } - currentTargets.Clear(); - currentTargets.Add(target); - if (!HasRequiredConditions(currentTargets)) { return; } + var targets = CurrentTargets; + targets.Clear(); + targets.Add(target); + if (!HasRequiredConditions(targets)) { return; } switch (delayType) { 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 }; - DelayList.Add(newDelayListElement); + DelayListDict.TryAdd(newDelayListElement.Id, newDelayListElement); break; case DelayTypes.ReachCursor: Projectile projectile = (entity as Item)?.GetComponent(); @@ -105,7 +147,8 @@ namespace Barotrauma 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; } } @@ -119,25 +162,28 @@ namespace Barotrauma if (delayType == DelayTypes.ReachCursor && Character.Controlled == null) { return; } 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) { if (!IsValidTarget(target)) { continue; } - currentTargets.Add(target); + localTargets.Add(target); } - if (!HasRequiredConditions(currentTargets)) { return; } + if (!HasRequiredConditions(localTargets)) { return; } switch (delayType) { 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; case DelayTypes.ReachCursor: Projectile projectile = (entity as Item)?.GetComponent(); @@ -161,19 +207,21 @@ namespace Barotrauma 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; } } 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)) { - DelayList.Remove(element); + DelayListDict.TryRemove(element.Id, out _); 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) 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; } @@ -198,8 +246,8 @@ namespace Barotrauma } element.Parent.Apply(deltaTime, element.Entity, element.Targets, element.WorldPosition); - DelayList.Remove(element); + DelayListDict.TryRemove(element.Id, out _); } } } -} \ No newline at end of file +} diff --git a/Barotrauma/BarotraumaShared/SharedSource/StatusEffects/StatusEffect.cs b/Barotrauma/BarotraumaShared/SharedSource/StatusEffects/StatusEffect.cs index a0cd1be2a..7aaf375bc 100644 --- a/Barotrauma/BarotraumaShared/SharedSource/StatusEffects/StatusEffect.cs +++ b/Barotrauma/BarotraumaShared/SharedSource/StatusEffects/StatusEffect.cs @@ -7,15 +7,18 @@ using FarseerPhysics.Dynamics; using Microsoft.Xna.Framework; using Steamworks; using System; +using System.Collections.Concurrent; using System.Collections.Generic; using System.Collections.Immutable; using System.Linq; +using System.Threading; using System.Xml.Linq; namespace Barotrauma { class DurationListElement { + public readonly long Id; public readonly StatusEffect Parent; public readonly Entity Entity; public float Duration @@ -24,12 +27,24 @@ namespace Barotrauma private set; } public readonly List 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 targets, float duration, Character user) { + Id = Interlocked.Increment(ref StatusEffect._durationElementIdCounter); Parent = parentEffect; Entity = parentEntity; Targets = new List(targets); @@ -39,8 +54,9 @@ namespace Barotrauma public void Reset(float duration, Character newUser) { - Timer = Duration = duration; - User = newUser; + Duration = duration; + Volatile.Write(ref _timer, duration); + _user = newUser; } } @@ -601,14 +617,23 @@ namespace Barotrauma private readonly float lifeTime; private float lifeTimer; - private Dictionary intervalTimers; + private ConcurrentDictionary intervalTimers; /// /// Makes the effect only execute once. After it has executed, it'll never execute again (during the same round). /// private readonly bool oneShot; - public static readonly List DurationList = new List(); + // Thread-safe counter for generating unique IDs for DurationListElement + internal static long _durationElementIdCounter; + + // Thread-safe dictionary for duration effects + public static readonly ConcurrentDictionary DurationListDict = new ConcurrentDictionary(); + + /// + /// Provides a thread-safe enumerable view of the duration list for iteration. + /// + public static IEnumerable DurationList => DurationListDict.Values; /// /// 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 intervalsToRemove = new List(); + // Thread-local list to avoid contention when cleaning up removed entities + [ThreadStatic] + private static List _threadLocalIntervalsToRemove; + + private static List IntervalsToRemove + { + get + { + _threadLocalIntervalsToRemove ??= new List(); + return _threadLocalIntervalsToRemove; + } + } 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; - if (intervalTimers[entity] > 0.0f) { return true; } + Interlocked.CompareExchange(ref intervalTimers, new ConcurrentDictionary(), null); } - intervalsToRemove.Clear(); - intervalsToRemove.AddRange(intervalTimers.Keys.Where(e => e.Removed)); - foreach (var toRemove in intervalsToRemove) + + if (intervalTimers.TryGetValue(entity, out float currentTimer)) { - 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; @@ -1655,7 +1710,7 @@ namespace Barotrauma if (Duration > 0.0f && !Stackable) { //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 (ResetDurationWhenReapplied) @@ -1666,30 +1721,74 @@ namespace Barotrauma } } - currentTargets.Clear(); - currentTargets.Add(target); - if (!HasRequiredConditions(currentTargets)) { return; } - Apply(deltaTime, entity, currentTargets, worldPosition); + var targets = CurrentTargets; + targets.Clear(); + targets.Add(target); + if (!HasRequiredConditions(targets)) { return; } + Apply(deltaTime, entity, targets, worldPosition); } - protected readonly List currentTargets = new List(); + // Thread-local list to avoid contention when collecting targets + [ThreadStatic] + private static List _threadLocalCurrentTargets; + + protected List CurrentTargets + { + get + { + _threadLocalCurrentTargets ??= new List(); + return _threadLocalCurrentTargets; + } + } + + /// + /// Thread-safe method to find an existing duration effect for a single target. + /// + private DurationListElement FindExistingDurationEffect(ISerializableEntity target) + { + foreach (var element in DurationListDict.Values) + { + if (element.Parent == this && element.Targets.FirstOrDefault() == target) + { + return element; + } + } + return null; + } + + /// + /// Thread-safe method to find an existing duration effect for multiple targets. + /// + private DurationListElement FindExistingDurationEffect(IReadOnlyList 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 targets, Vector2? worldPosition = null) { if (Disabled) { return; } if (this.type != type) { return; } if (ShouldWaitForInterval(entity, deltaTime)) { return; } - currentTargets.Clear(); + var localTargets = CurrentTargets; + localTargets.Clear(); foreach (ISerializableEntity target in targets) { 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); - if (!hasRequiredItems || !HasRequiredConditions(currentTargets)) + if (!hasRequiredItems || !HasRequiredConditions(localTargets)) { #if CLIENT if (!hasRequiredItems && playSoundOnRequiredItemFailure) @@ -1703,15 +1802,15 @@ namespace Barotrauma if (Duration > 0.0f && !Stackable) { //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) { - existingEffect?.Reset(Math.Max(existingEffect.Timer, Duration), user); + existingEffect.Reset(Math.Max(existingEffect.Timer, Duration), user); return; } } - Apply(deltaTime, entity, currentTargets, worldPosition); + Apply(deltaTime, entity, localTargets, worldPosition); } private Hull GetHull(Entity entity) @@ -1924,7 +2023,8 @@ namespace Barotrauma 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 { @@ -2452,7 +2552,7 @@ namespace Barotrauma } if (Interval > 0.0f && entity != null) { - intervalTimers ??= new Dictionary(); + intervalTimers ??= new ConcurrentDictionary(); intervalTimers[entity] = Interval; } } @@ -2849,13 +2949,15 @@ namespace Barotrauma UpdateAllProjSpecific(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)) { - DurationList.RemoveAt(i); + DurationListDict.TryRemove(element.Id, out _); continue; } @@ -2864,7 +2966,7 @@ namespace Barotrauma (t is Limb limb && (limb.character == null || limb.character.Removed))); if (element.Targets.Count == 0) { - DurationList.RemoveAt(i); + DurationListDict.TryRemove(element.Id, out _); continue; } @@ -2959,7 +3061,7 @@ namespace Barotrauma element.Timer -= deltaTime; if (element.Timer > 0.0f) { continue; } - DurationList.Remove(element); + DurationListDict.TryRemove(element.Id, out _); } } @@ -3059,8 +3161,8 @@ namespace Barotrauma public static void StopAll() { CoroutineManager.StopCoroutines("statuseffect"); - DelayedEffect.DelayList.Clear(); - DurationList.Clear(); + DelayedEffect.DelayListDict.Clear(); + DurationListDict.Clear(); } public void AddTag(Identifier tag) From 31812d524dcc1d5fbd20bef58a531bd5d555fb48 Mon Sep 17 00:00:00 2001 From: Eero Date: Sun, 28 Dec 2025 12:53:10 +0800 Subject: [PATCH 05/14] Make collections thread-safe for AI and character systems Refactored various collections (lists, dictionaries, queues, and caches) in AI, character, animation, and item component systems to use thread-safe patterns and concurrent data structures. This includes introducing copy-on-write wrappers, ConcurrentDictionary, ConcurrentQueue, and ThreadLocal where appropriate to ensure safe concurrent access and mutation, improving stability in multi-threaded scenarios. --- .../SharedSource/Characters/AI/AITarget.cs | 56 ++++++++++++- .../Characters/AI/HumanAIController.cs | 18 +++-- .../Characters/AI/NPCConversation.cs | 26 +++++- .../AI/Objectives/AIObjectiveFindThieves.cs | 4 +- .../Characters/Animation/Ragdoll.cs | 50 ++++++++---- .../SharedSource/Characters/Character.cs | 64 ++++++++++++++- .../Characters/Health/CharacterHealth.cs | 80 +++++++++---------- .../Params/Animation/AnimationParams.cs | 34 ++++---- .../Params/Ragdoll/RagdollParams.cs | 29 +++---- .../Characters/Talents/CharacterTalent.cs | 6 +- .../ContentFile/NPCConversationsFile.cs | 6 +- .../Items/Components/Signal/DelayComponent.cs | 30 ++++--- 12 files changed, 281 insertions(+), 122 deletions(-) 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; } From c5fa49405fafe20f1d31f07a568dd987eda91a05 Mon Sep 17 00:00:00 2001 From: Eero Date: Sun, 28 Dec 2025 13:10:17 +0800 Subject: [PATCH 06/14] WIP Make networking code thread-safe and refactor update ID Replaces direct increments of LastClientListUpdateID with a thread-safe IncrementLastClientListUpdateID method and uses Interlocked for atomic operations. Refactors EntitySpawner to lock access to the spawn/remove queue for thread safety. Updates INetSerializableStruct to use concurrent collections for cached variables and type behaviors, improving thread safety in networking code. --- .../ServerSource/Networking/Client.cs | 2 +- .../Networking/FileTransfer/FileSender.cs | 4 +- .../ServerSource/Networking/GameServer.cs | 14 +-- .../ServerSource/Networking/KarmaManager.cs | 2 +- .../SharedSource/Networking/Client.cs | 8 +- .../SharedSource/Networking/EntitySpawner.cs | 95 ++++++++++++++----- .../Networking/INetSerializableStruct.cs | 23 +++-- .../SharedSource/Networking/NetworkMember.cs | 14 ++- 8 files changed, 111 insertions(+), 51 deletions(-) diff --git a/Barotrauma/BarotraumaServer/ServerSource/Networking/Client.cs b/Barotrauma/BarotraumaServer/ServerSource/Networking/Client.cs index a89bd2c3b..6b1a3bd86 100644 --- a/Barotrauma/BarotraumaServer/ServerSource/Networking/Client.cs +++ b/Barotrauma/BarotraumaServer/ServerSource/Networking/Client.cs @@ -126,7 +126,7 @@ namespace Barotrauma.Networking if (!MathUtils.NearlyEqual(karma, syncedKarma, 10.0f)) { syncedKarma = karma; - GameMain.NetworkMember.LastClientListUpdateID++; + GameMain.NetworkMember.IncrementLastClientListUpdateID(); } } } diff --git a/Barotrauma/BarotraumaServer/ServerSource/Networking/FileTransfer/FileSender.cs b/Barotrauma/BarotraumaServer/ServerSource/Networking/FileTransfer/FileSender.cs index 6d5fa3619..5632b80e0 100644 --- a/Barotrauma/BarotraumaServer/ServerSource/Networking/FileTransfer/FileSender.cs +++ b/Barotrauma/BarotraumaServer/ServerSource/Networking/FileTransfer/FileSender.cs @@ -174,7 +174,7 @@ namespace Barotrauma.Networking StartTime = DateTime.Now; OnStarted(transfer); - GameMain.Server.LastClientListUpdateID++; + GameMain.Server.IncrementLastClientListUpdateID(); return transfer; } @@ -204,7 +204,7 @@ namespace Barotrauma.Networking if (numRemoved > 0 || endedTransfers.Count > 0) { - GameMain.Server.LastClientListUpdateID++; + GameMain.Server.IncrementLastClientListUpdateID(); } } diff --git a/Barotrauma/BarotraumaServer/ServerSource/Networking/GameServer.cs b/Barotrauma/BarotraumaServer/ServerSource/Networking/GameServer.cs index d4538fe6a..ad967ce8f 100644 --- a/Barotrauma/BarotraumaServer/ServerSource/Networking/GameServer.cs +++ b/Barotrauma/BarotraumaServer/ServerSource/Networking/GameServer.cs @@ -327,7 +327,7 @@ namespace Barotrauma.Networking } } - LastClientListUpdateID++; + IncrementLastClientListUpdateID(); if (newClient.Connection == OwnerConnection && OwnerConnection != null) { @@ -3222,7 +3222,7 @@ namespace Barotrauma.Networking initiatedStartGame = false; GameMain.ResetFrameTime(); - LastClientListUpdateID++; + IncrementLastClientListUpdateID(); roundStartTime = DateTime.Now; @@ -3532,7 +3532,7 @@ namespace Barotrauma.Networking { var coolDownRemaining = Client.NameChangeCoolDown - timeSinceNameChange; SendDirectChatMessage($"ServerMessage.NameChangeFailedCooldownActive~[seconds]={(int)coolDownRemaining.TotalSeconds}", c); - LastClientListUpdateID++; + IncrementLastClientListUpdateID(); //increment the ID to make sure the current server-side name is treated as the "latest", //and the client correctly reverts back to the old name c.NameId++; @@ -3545,7 +3545,7 @@ namespace Barotrauma.Networking if (result != null) { - LastClientListUpdateID++; + IncrementLastClientListUpdateID(); return result.Value; } @@ -3562,14 +3562,14 @@ namespace Barotrauma.Networking c.Name = newName; c.RejectedName = string.Empty; SendChatMessage($"ServerMessage.NameChangeSuccessful~[oldname]={oldName}~[newname]={newName}", ChatMessageType.Server); - LastClientListUpdateID++; + IncrementLastClientListUpdateID(); return true; } else { //update client list even if the name cannot be changed to the one sent by the client, //so the client will be informed what their actual name is - LastClientListUpdateID++; + IncrementLastClientListUpdateID(); return false; } } @@ -4857,7 +4857,7 @@ namespace Barotrauma.Networking private void UpdateClientLobbies() { // Triggers a call to WriteClientList(), which causes clients to call GameClient.ReadClientList() - LastClientListUpdateID++; + IncrementLastClientListUpdateID(); } private List GetPlayingClients() diff --git a/Barotrauma/BarotraumaServer/ServerSource/Networking/KarmaManager.cs b/Barotrauma/BarotraumaServer/ServerSource/Networking/KarmaManager.cs index 6bab9e4ec..9a325a12b 100644 --- a/Barotrauma/BarotraumaServer/ServerSource/Networking/KarmaManager.cs +++ b/Barotrauma/BarotraumaServer/ServerSource/Networking/KarmaManager.cs @@ -163,7 +163,7 @@ namespace Barotrauma { client.Character.CharacterHealth.ApplyAffliction(null, new Affliction(herpesAffliction, herpesStrength)); GameServer.Log($"{GameServer.ClientLogName(client)} has contracted space herpes due to low karma.", ServerLog.MessageType.Karma); - GameMain.NetworkMember.LastClientListUpdateID++; + GameMain.NetworkMember.IncrementLastClientListUpdateID(); } else if (existingAffliction != null) { diff --git a/Barotrauma/BarotraumaShared/SharedSource/Networking/Client.cs b/Barotrauma/BarotraumaShared/SharedSource/Networking/Client.cs index c59fd4ec9..92442e806 100644 --- a/Barotrauma/BarotraumaShared/SharedSource/Networking/Client.cs +++ b/Barotrauma/BarotraumaShared/SharedSource/Networking/Client.cs @@ -62,7 +62,7 @@ namespace Barotrauma.Networking DebugConsole.Log($"Changed client {Name}'s team to {teamID}."); if (GameMain.NetworkMember != null && GameMain.NetworkMember.IsServer) { - GameMain.NetworkMember.LastClientListUpdateID++; + GameMain.NetworkMember.IncrementLastClientListUpdateID(); } teamID = value; } @@ -86,7 +86,7 @@ namespace Barotrauma.Networking { if (GameMain.NetworkMember != null && GameMain.NetworkMember.IsServer) { - GameMain.NetworkMember.LastClientListUpdateID++; + GameMain.NetworkMember.IncrementLastClientListUpdateID(); if (value != null) { CharacterID = value.ID; @@ -154,7 +154,7 @@ namespace Barotrauma.Networking #endif if (GameMain.NetworkMember != null && GameMain.NetworkMember.IsServer) { - GameMain.NetworkMember.LastClientListUpdateID++; + GameMain.NetworkMember.IncrementLastClientListUpdateID(); } } } @@ -178,7 +178,7 @@ namespace Barotrauma.Networking { if (GameMain.NetworkMember != null && GameMain.NetworkMember.IsServer) { - GameMain.NetworkMember.LastClientListUpdateID++; + GameMain.NetworkMember.IncrementLastClientListUpdateID(); } inGame = value; } diff --git a/Barotrauma/BarotraumaShared/SharedSource/Networking/EntitySpawner.cs b/Barotrauma/BarotraumaShared/SharedSource/Networking/EntitySpawner.cs index 41fe185a9..625bc734c 100644 --- a/Barotrauma/BarotraumaShared/SharedSource/Networking/EntitySpawner.cs +++ b/Barotrauma/BarotraumaShared/SharedSource/Networking/EntitySpawner.cs @@ -5,6 +5,7 @@ using Microsoft.Xna.Framework; using System; using System.Collections.Generic; using System.Linq; +using System.Threading; namespace Barotrauma { @@ -205,6 +206,7 @@ namespace Barotrauma } private readonly Queue> spawnOrRemoveQueue; + private readonly object spawnOrRemoveQueueLock = new object(); public abstract class SpawnOrRemove : NetEntityEvent.IData { @@ -282,7 +284,10 @@ namespace Barotrauma GameAnalyticsManager.AddErrorEventOnce("EntitySpawner.AddToSpawnQueue1:ItemPrefabNull", GameAnalyticsManager.ErrorSeverity.Error, errorMsg); return; } - spawnOrRemoveQueue.Enqueue(new ItemSpawnInfo(itemPrefab, worldPosition, onSpawned, condition, quality)); + lock (spawnOrRemoveQueueLock) + { + spawnOrRemoveQueue.Enqueue(new ItemSpawnInfo(itemPrefab, worldPosition, onSpawned, condition, quality)); + } } public void AddItemToSpawnQueue(ItemPrefab itemPrefab, Vector2 position, Submarine sub, float? condition = null, int? quality = null, Action onSpawned = null) @@ -295,7 +300,10 @@ namespace Barotrauma GameAnalyticsManager.AddErrorEventOnce("EntitySpawner.AddToSpawnQueue2:ItemPrefabNull", GameAnalyticsManager.ErrorSeverity.Error, errorMsg); return; } - spawnOrRemoveQueue.Enqueue(new ItemSpawnInfo(itemPrefab, position, sub, onSpawned, condition, quality)); + lock (spawnOrRemoveQueueLock) + { + spawnOrRemoveQueue.Enqueue(new ItemSpawnInfo(itemPrefab, position, sub, onSpawned, condition, quality)); + } } public void AddItemToSpawnQueue(ItemPrefab itemPrefab, Inventory inventory, float? condition = null, int? quality = null, Action onSpawned = null, bool spawnIfInventoryFull = true, bool ignoreLimbSlots = false, InvSlotType slot = InvSlotType.None) @@ -308,12 +316,15 @@ namespace Barotrauma GameAnalyticsManager.AddErrorEventOnce("EntitySpawner.AddToSpawnQueue3:ItemPrefabNull", GameAnalyticsManager.ErrorSeverity.Error, errorMsg); return; } - spawnOrRemoveQueue.Enqueue(new ItemSpawnInfo(itemPrefab, inventory, onSpawned, condition, quality) - { - SpawnIfInventoryFull = spawnIfInventoryFull, - IgnoreLimbSlots = ignoreLimbSlots, - Slot = slot - }); + lock (spawnOrRemoveQueueLock) + { + spawnOrRemoveQueue.Enqueue(new ItemSpawnInfo(itemPrefab, inventory, onSpawned, condition, quality) + { + SpawnIfInventoryFull = spawnIfInventoryFull, + IgnoreLimbSlots = ignoreLimbSlots, + Slot = slot + }); + } } public void AddCharacterToSpawnQueue(Identifier speciesName, Vector2 worldPosition, Action onSpawn = null) @@ -326,7 +337,10 @@ namespace Barotrauma GameAnalyticsManager.AddErrorEventOnce("EntitySpawner.AddToSpawnQueue4:SpeciesNameNullOrEmpty", GameAnalyticsManager.ErrorSeverity.Error, errorMsg); return; } - spawnOrRemoveQueue.Enqueue(new CharacterSpawnInfo(speciesName, worldPosition, onSpawn)); + lock (spawnOrRemoveQueueLock) + { + spawnOrRemoveQueue.Enqueue(new CharacterSpawnInfo(speciesName, worldPosition, onSpawn)); + } } public void AddCharacterToSpawnQueue(Identifier speciesName, Vector2 position, Submarine sub, Action onSpawn = null) @@ -339,7 +353,10 @@ namespace Barotrauma GameAnalyticsManager.AddErrorEventOnce("EntitySpawner.AddToSpawnQueue5:SpeciesNameNullOrEmpty", GameAnalyticsManager.ErrorSeverity.Error, errorMsg); return; } - spawnOrRemoveQueue.Enqueue(new CharacterSpawnInfo(speciesName, position, sub, onSpawn)); + lock (spawnOrRemoveQueueLock) + { + spawnOrRemoveQueue.Enqueue(new CharacterSpawnInfo(speciesName, position, sub, onSpawn)); + } } public void AddCharacterToSpawnQueue(Identifier speciesName, Vector2 worldPosition, CharacterInfo characterInfo, Action onSpawn = null) @@ -352,7 +369,10 @@ namespace Barotrauma GameAnalyticsManager.AddErrorEventOnce("EntitySpawner.AddToSpawnQueue4:SpeciesNameNullOrEmpty", GameAnalyticsManager.ErrorSeverity.Error, errorMsg); return; } - spawnOrRemoveQueue.Enqueue(new CharacterSpawnInfo(speciesName, worldPosition, characterInfo, onSpawn)); + lock (spawnOrRemoveQueueLock) + { + spawnOrRemoveQueue.Enqueue(new CharacterSpawnInfo(speciesName, worldPosition, characterInfo, onSpawn)); + } } public void AddEntityToRemoveQueue(Entity entity) @@ -375,7 +395,10 @@ namespace Barotrauma #endif } - spawnOrRemoveQueue.Enqueue(entity); + lock (spawnOrRemoveQueueLock) + { + spawnOrRemoveQueue.Enqueue(entity); + } } public void AddItemToRemoveQueue(Item item) @@ -383,7 +406,10 @@ namespace Barotrauma if (GameMain.NetworkMember != null && GameMain.NetworkMember.IsClient) { return; } if (IsInRemoveQueue(item) || item.Removed) { return; } - spawnOrRemoveQueue.Enqueue(item); + lock (spawnOrRemoveQueueLock) + { + spawnOrRemoveQueue.Enqueue(item); + } item.IsInRemoveQueue = true; foreach (var containedItem in item.ContainedItems) @@ -400,11 +426,14 @@ namespace Barotrauma /// public bool IsInSpawnQueue(Predicate predicate) { - foreach (var spawnOrRemove in spawnOrRemoveQueue) + lock (spawnOrRemoveQueueLock) { - if (spawnOrRemove.TryGet(out IEntitySpawnInfo spawnInfo) && predicate(spawnInfo)) { return true; } + foreach (var spawnOrRemove in spawnOrRemoveQueue) + { + if (spawnOrRemove.TryGet(out IEntitySpawnInfo spawnInfo) && predicate(spawnInfo)) { return true; } + } + return false; } - return false; } /// @@ -412,29 +441,40 @@ namespace Barotrauma /// public int CountSpawnQueue(Predicate predicate) { - int count = 0; - foreach (var spawnOrRemove in spawnOrRemoveQueue) + lock (spawnOrRemoveQueueLock) { - if (spawnOrRemove.TryGet(out IEntitySpawnInfo spawnInfo) && predicate(spawnInfo)) { count++; } + int count = 0; + foreach (var spawnOrRemove in spawnOrRemoveQueue) + { + if (spawnOrRemove.TryGet(out IEntitySpawnInfo spawnInfo) && predicate(spawnInfo)) { count++; } + } + return count; } - return count; } public bool IsInRemoveQueue(Entity entity) { - foreach (var spawnOrRemove in spawnOrRemoveQueue) + lock (spawnOrRemoveQueueLock) { - if (spawnOrRemove.TryGet(out Entity entityToRemove) && entityToRemove == entity) { return true; } + foreach (var spawnOrRemove in spawnOrRemoveQueue) + { + if (spawnOrRemove.TryGet(out Entity entityToRemove) && entityToRemove == entity) { return true; } + } + return false; } - return false; } public void Update(bool createNetworkEvents = true) { if (GameMain.NetworkMember is { IsClient: true }) { return; } - while (spawnOrRemoveQueue.Count > 0) + while (true) { - var spawnOrRemove = spawnOrRemoveQueue.Dequeue(); + Either spawnOrRemove; + lock (spawnOrRemoveQueueLock) + { + if (spawnOrRemoveQueue.Count == 0) { break; } + spawnOrRemove = spawnOrRemoveQueue.Dequeue(); + } if (spawnOrRemove.TryGet(out Entity entityToRemove)) { if (entityToRemove is Item item) @@ -465,7 +505,10 @@ namespace Barotrauma public void Reset() { - spawnOrRemoveQueue.Clear(); + lock (spawnOrRemoveQueueLock) + { + spawnOrRemoveQueue.Clear(); + } #if CLIENT receivedEvents.Clear(); #endif diff --git a/Barotrauma/BarotraumaShared/SharedSource/Networking/INetSerializableStruct.cs b/Barotrauma/BarotraumaShared/SharedSource/Networking/INetSerializableStruct.cs index ebb9572ab..b3ab41c9e 100644 --- a/Barotrauma/BarotraumaShared/SharedSource/Networking/INetSerializableStruct.cs +++ b/Barotrauma/BarotraumaShared/SharedSource/Networking/INetSerializableStruct.cs @@ -1,5 +1,6 @@ #nullable enable using System; +using System.Collections.Concurrent; using System.Collections.Generic; using System.Collections.Immutable; using System.Diagnostics.CodeAnalysis; @@ -146,10 +147,10 @@ namespace Barotrauma } } - private static readonly Dictionary> CachedVariables = new Dictionary>(); + private static readonly ConcurrentDictionary> CachedVariables = new ConcurrentDictionary>(); - private static readonly Dictionary TypeBehaviors - = new Dictionary + private static readonly ConcurrentDictionary TypeBehaviors + = new ConcurrentDictionary(new Dictionary { { typeof(Boolean), new ReadWriteBehavior(ReadBoolean, WriteBoolean) }, { typeof(Byte), new ReadWriteBehavior(ReadByte, WriteByte) }, @@ -168,7 +169,7 @@ namespace Barotrauma { typeof(Vector2), new ReadWriteBehavior(ReadVector2, WriteVector2) }, { typeof(SerializableDateTime), new ReadWriteBehavior(ReadSerializableDateTime, WriteSerializableDateTime) }, { typeof(NetLimitedString), new ReadWriteBehavior(ReadNetLString, WriteNetLString) } - }; + }); private static readonly ImmutableDictionary, Func> BehaviorFactories = new Dictionary, Func> { @@ -584,7 +585,11 @@ namespace Barotrauma if (!predicate(type)) { continue; } behavior = factory(type); - TypeBehaviors.Add(type, behavior); + // Use TryAdd for thread-safety; if another thread already added, use that value + if (!TypeBehaviors.TryAdd(type, behavior)) + { + behavior = TypeBehaviors[type]; + } return true; } @@ -594,8 +599,11 @@ namespace Barotrauma public static ImmutableArray GetPropertiesAndFields(Type type) { - if (CachedVariables.TryGetValue(type, out var cached)) { return cached; } + return CachedVariables.GetOrAdd(type, static t => CreateCachedVariables(t)); + } + private static ImmutableArray CreateCachedVariables(Type type) + { List variables = new List(); IEnumerable propertyInfos = type.GetProperties().Where(HasAttribute).Where(NotStatic); @@ -633,7 +641,6 @@ namespace Barotrauma } ImmutableArray array = variables.All(v => v.HasOwnAttribute) ? variables.OrderBy(v => v.Attribute.OrderKey).ToImmutableArray() : variables.ToImmutableArray(); - CachedVariables.Add(type, array); return array; bool HasAttribute(MemberInfo info) => (info.GetCustomAttribute() ?? type.GetCustomAttribute()) != null; @@ -874,4 +881,4 @@ namespace Barotrauma } } } -} \ No newline at end of file +} diff --git a/Barotrauma/BarotraumaShared/SharedSource/Networking/NetworkMember.cs b/Barotrauma/BarotraumaShared/SharedSource/Networking/NetworkMember.cs index c33e87c57..cbea90c64 100644 --- a/Barotrauma/BarotraumaShared/SharedSource/Networking/NetworkMember.cs +++ b/Barotrauma/BarotraumaShared/SharedSource/Networking/NetworkMember.cs @@ -1,6 +1,7 @@ using Microsoft.Xna.Framework; using System; using System.Collections.Generic; +using System.Threading; namespace Barotrauma.Networking { @@ -186,10 +187,19 @@ namespace Barotrauma.Networking { protected const int MaxSubNameLengthInErrorMessages = 16; + private int lastClientListUpdateID; public UInt16 LastClientListUpdateID { - get; - set; + get => (UInt16)Interlocked.CompareExchange(ref lastClientListUpdateID, 0, 0); + set => Interlocked.Exchange(ref lastClientListUpdateID, value); + } + + /// + /// Thread-safe increment of LastClientListUpdateID + /// + public void IncrementLastClientListUpdateID() + { + Interlocked.Increment(ref lastClientListUpdateID); } public abstract bool IsServer { get; } From 45312af297678ce4500c682ef13a7459e4f56139 Mon Sep 17 00:00:00 2001 From: Eero Date: Sun, 28 Dec 2025 14:14:53 +0800 Subject: [PATCH 07/14] WIP Make static collections thread-safe using ThreadStatic and ThreadLocal Refactored various static and instance collections to use [ThreadStatic], ThreadLocal, or local variables to prevent concurrent modification issues during parallel updates. This affects status effect targets, affliction lists, damage modifiers, and cached data in Character, CharacterHealth, Limb, Explosion, Hull, Submarine, and ToolBox classes. Also replaced Dictionary caches with ConcurrentDictionary where appropriate for thread safety. --- .../SharedSource/Characters/Character.cs | 7 +- .../Characters/Health/CharacterHealth.cs | 59 ++++++++++++---- .../SharedSource/Characters/Limb.cs | 24 ++++--- .../SharedSource/Map/Explosion.cs | 7 +- .../BarotraumaShared/SharedSource/Map/Hull.cs | 14 ++-- .../SharedSource/Map/Submarine.cs | 70 +++++++++++-------- .../SharedSource/Utils/ToolBox.cs | 29 ++++---- 7 files changed, 135 insertions(+), 75 deletions(-) diff --git a/Barotrauma/BarotraumaShared/SharedSource/Characters/Character.cs b/Barotrauma/BarotraumaShared/SharedSource/Characters/Character.cs index b0e7aff11..2ab6d6a68 100644 --- a/Barotrauma/BarotraumaShared/SharedSource/Characters/Character.cs +++ b/Barotrauma/BarotraumaShared/SharedSource/Characters/Character.cs @@ -4880,7 +4880,11 @@ namespace Barotrauma HealthUpdateInterval = 0.0f; } - private readonly List targets = new List(); + // Thread-static to avoid concurrent modification in parallel item updates + [ThreadStatic] + private static List t_statusEffectTargets; + private static List StatusEffectTargets => t_statusEffectTargets ??= new List(); + public void ApplyStatusEffects(ActionType actionType, float deltaTime) { if (actionType == ActionType.OnEating) @@ -4909,6 +4913,7 @@ namespace Barotrauma if (statusEffect.HasTargetType(StatusEffect.TargetType.NearbyItems) || statusEffect.HasTargetType(StatusEffect.TargetType.NearbyCharacters)) { + var targets = StatusEffectTargets; targets.Clear(); statusEffect.AddNearbyTargets(WorldPosition, targets); statusEffect.Apply(actionType, deltaTime, this, targets); diff --git a/Barotrauma/BarotraumaShared/SharedSource/Characters/Health/CharacterHealth.cs b/Barotrauma/BarotraumaShared/SharedSource/Characters/Health/CharacterHealth.cs index a4c538451..03fecbc34 100644 --- a/Barotrauma/BarotraumaShared/SharedSource/Characters/Health/CharacterHealth.cs +++ b/Barotrauma/BarotraumaShared/SharedSource/Characters/Health/CharacterHealth.cs @@ -537,20 +537,25 @@ namespace Barotrauma return false; } - private readonly List matchingAfflictions = new List(); + // Thread-static to avoid concurrent modification in parallel item updates + [ThreadStatic] + private static List t_matchingAfflictions; + private static List MatchingAfflictions => t_matchingAfflictions ??= new List(); public void ReduceAllAfflictionsOnAllLimbs(float amount, ActionType? treatmentAction = null) { + var matchingAfflictions = MatchingAfflictions; matchingAfflictions.Clear(); matchingAfflictions.AddRange(afflictions.Keys); - ReduceMatchingAfflictions(amount, treatmentAction); + ReduceMatchingAfflictions(matchingAfflictions, amount, treatmentAction); } public void ReduceAfflictionOnAllLimbs(Identifier afflictionIdOrType, float amount, ActionType? treatmentAction = null, Character attacker = null) { if (afflictionIdOrType.IsEmpty) { throw new ArgumentException($"{nameof(afflictionIdOrType)} is empty"); } + var matchingAfflictions = MatchingAfflictions; matchingAfflictions.Clear(); foreach (var affliction in afflictions) { @@ -560,7 +565,7 @@ namespace Barotrauma } } - ReduceMatchingAfflictions(amount, treatmentAction, attacker); + ReduceMatchingAfflictions(matchingAfflictions, amount, treatmentAction, attacker); } private IEnumerable GetAfflictionsForLimb(Limb targetLimb) @@ -570,10 +575,11 @@ namespace Barotrauma { if (targetLimb is null) { throw new ArgumentNullException(nameof(targetLimb)); } + var matchingAfflictions = MatchingAfflictions; matchingAfflictions.Clear(); matchingAfflictions.AddRange(GetAfflictionsForLimb(targetLimb)); - ReduceMatchingAfflictions(amount, treatmentAction); + ReduceMatchingAfflictions(matchingAfflictions, amount, treatmentAction); } public void ReduceAfflictionOnLimb(Limb targetLimb, Identifier afflictionIdOrType, float amount, ActionType? treatmentAction = null, Character attacker = null) @@ -581,6 +587,7 @@ namespace Barotrauma if (afflictionIdOrType.IsEmpty) { throw new ArgumentException($"{nameof(afflictionIdOrType)} is empty"); } if (targetLimb is null) { throw new ArgumentNullException(nameof(targetLimb)); } + var matchingAfflictions = MatchingAfflictions; matchingAfflictions.Clear(); var targetLimbHealth = limbHealths[targetLimb.HealthIndex]; foreach (var affliction in afflictions) @@ -591,10 +598,10 @@ namespace Barotrauma matchingAfflictions.Add(affliction.Key); } } - ReduceMatchingAfflictions(amount, treatmentAction, attacker); + ReduceMatchingAfflictions(matchingAfflictions, amount, treatmentAction, attacker); } - private void ReduceMatchingAfflictions(float amount, ActionType? treatmentAction, Character attacker = null) + private void ReduceMatchingAfflictions(List matchingAfflictions, float amount, ActionType? treatmentAction, Character attacker = null) { if (matchingAfflictions.Count == 0) { return; } @@ -681,12 +688,19 @@ namespace Barotrauma } } - private readonly static List afflictionsToRemove = new List(); - private readonly static List> afflictionsToUpdate = new List>(); + // Thread-static to avoid concurrent modification when multiple characters are updated in parallel + [ThreadStatic] + private static List t_afflictionsToRemove; + [ThreadStatic] + private static List> t_afflictionsToUpdate; + private static List AfflictionsToRemove => t_afflictionsToRemove ??= new List(); + private static List> AfflictionsToUpdate => t_afflictionsToUpdate ??= new List>(); + public void SetAllDamage(float damageAmount, float bleedingDamageAmount, float burnDamageAmount) { if (Unkillable || Character.GodMode) { return; } + var afflictionsToRemove = AfflictionsToRemove; afflictionsToRemove.Clear(); afflictionsToRemove.AddRange(afflictions.Keys.Where(a => a.Prefab.AfflictionType == AfflictionPrefab.InternalDamage.AfflictionType || @@ -737,6 +751,7 @@ namespace Barotrauma public void RemoveAfflictions(Func predicate) { + var afflictionsToRemove = AfflictionsToRemove; afflictionsToRemove.Clear(); afflictionsToRemove.AddRange(afflictions.Keys.Where(affliction => predicate(affliction))); foreach (var affliction in afflictionsToRemove) @@ -748,6 +763,7 @@ namespace Barotrauma public void RemoveAllAfflictions() { + var afflictionsToRemove = AfflictionsToRemove; afflictionsToRemove.Clear(); afflictionsToRemove.AddRange(afflictions.Keys.Where(a => !irremovableAfflictions.ContainsKey(a))); foreach (var affliction in afflictionsToRemove) @@ -765,6 +781,7 @@ namespace Barotrauma public void RemoveNegativeAfflictions() { + var afflictionsToRemove = AfflictionsToRemove; afflictionsToRemove.Clear(); afflictionsToRemove.AddRange(afflictions.Keys.Where(a => !irremovableAfflictions.ContainsKey(a) && @@ -904,6 +921,8 @@ namespace Barotrauma if (!Character.GodMode) { + var afflictionsToRemove = AfflictionsToRemove; + var afflictionsToUpdate = AfflictionsToUpdate; afflictionsToRemove.Clear(); afflictionsToUpdate.Clear(); foreach (KeyValuePair kvp in afflictions) @@ -1198,9 +1217,14 @@ namespace Barotrauma return (causeOfDeath, strongestAffliction); } - private readonly List allAfflictions = new List(); + // Thread-static to avoid concurrent modification in parallel item updates + [ThreadStatic] + private static List t_allAfflictions; + private static List AllAfflictionsList => t_allAfflictions ??= new List(); + private IEnumerable GetAllAfflictions(bool mergeSameAfflictions, Func predicate = null) { + var allAfflictions = AllAfflictionsList; allAfflictions.Clear(); if (!mergeSameAfflictions) { @@ -1383,10 +1407,17 @@ namespace Barotrauma return MathHelper.Clamp(strength, 0.0f, affliction.Prefab.MaxStrength); } - private readonly List activeAfflictions = new List(); - private readonly List<(LimbHealth limbHealth, Affliction affliction)> limbAfflictions = new List<(LimbHealth limbHealth, Affliction affliction)>(); + // Thread-static to avoid concurrent modification in parallel updates + [ThreadStatic] + private static List t_activeAfflictions; + [ThreadStatic] + private static List<(LimbHealth limbHealth, Affliction affliction)> t_limbAfflictions; + private static List ActiveAfflictionsList => t_activeAfflictions ??= new List(); + private static List<(LimbHealth limbHealth, Affliction affliction)> LimbAfflictionsList => t_limbAfflictions ??= new List<(LimbHealth limbHealth, Affliction affliction)>(); + public void ServerWrite(IWriteMessage msg) { + var activeAfflictions = ActiveAfflictionsList; activeAfflictions.Clear(); foreach (KeyValuePair kvp in afflictions) { @@ -1412,6 +1443,7 @@ namespace Barotrauma } } + var limbAfflictions = LimbAfflictionsList; limbAfflictions.Clear(); foreach (KeyValuePair kvp in afflictions) { @@ -1441,8 +1473,9 @@ namespace Barotrauma public void Remove() { RemoveProjSpecific(); - afflictionsToRemove.Clear(); - afflictionsToUpdate.Clear(); + // Clear thread-static lists to help with garbage collection + AfflictionsToRemove.Clear(); + AfflictionsToUpdate.Clear(); } partial void RemoveProjSpecific(); diff --git a/Barotrauma/BarotraumaShared/SharedSource/Characters/Limb.cs b/Barotrauma/BarotraumaShared/SharedSource/Characters/Limb.cs index d9226ce75..79c09a34c 100644 --- a/Barotrauma/BarotraumaShared/SharedSource/Characters/Limb.cs +++ b/Barotrauma/BarotraumaShared/SharedSource/Characters/Limb.cs @@ -797,16 +797,14 @@ namespace Barotrauma return AddDamage(simPosition, afflictions, playSound); } - private readonly List appliedDamageModifiers = new List(); - private readonly List tempModifiers = new List(); - private readonly List afflictionsCopy = new List(); + // Thread-safe: using local variables instead of instance fields to avoid concurrent modification public AttackResult AddDamage(Vector2 simPosition, IEnumerable afflictions, bool playSound, float damageMultiplier = 1, float penetration = 0f, Character attacker = null) { - appliedDamageModifiers.Clear(); - afflictionsCopy.Clear(); + var appliedDamageModifiers = new List(); + var afflictionsCopy = new List(); foreach (var affliction in afflictions) { - tempModifiers.Clear(); + var tempModifiers = new List(); var newAffliction = affliction; float random = Rand.Value(Rand.RandSync.Unsynced); bool foundMatchingModifier = false; @@ -1022,13 +1020,18 @@ namespace Barotrauma partial void UpdateProjSpecific(float deltaTime); - private readonly List contactBodies = new List(); + // Thread-static to avoid concurrent modification in parallel item updates + [ThreadStatic] + private static List t_contactBodies; + private static List ContactBodies => t_contactBodies ??= new List(); + /// /// Returns true if the attack successfully hit something. If the distance is not given, it will be calculated. /// public bool UpdateAttack(float deltaTime, Vector2 attackSimPos, IDamageable damageTarget, out AttackResult attackResult, float distance = -1, Limb targetLimb = null) { attackResult = default; + var contactBodies = ContactBodies; Vector2 simPos = ragdoll.SimplePhysicsEnabled ? character.SimPosition : SimPosition; float dist = distance > -1 ? distance : ConvertUnits.ToDisplayUnits(Vector2.Distance(simPos, attackSimPos)); bool wasRunning = attack.IsRunning; @@ -1287,7 +1290,11 @@ namespace Barotrauma } } - private readonly List targets = new List(); + // Thread-static to avoid concurrent modification in parallel item updates + [ThreadStatic] + private static List t_statusEffectTargets; + private static List StatusEffectTargets => t_statusEffectTargets ??= new List(); + public void ApplyStatusEffects(ActionType actionType, float deltaTime) { if (!statusEffects.TryGetValue(actionType, out var statusEffectList)) { return; } @@ -1310,6 +1317,7 @@ namespace Barotrauma if (statusEffect.HasTargetType(StatusEffect.TargetType.NearbyItems) || statusEffect.HasTargetType(StatusEffect.TargetType.NearbyCharacters)) { + var targets = StatusEffectTargets; targets.Clear(); statusEffect.AddNearbyTargets(WorldPosition, targets); statusEffect.Apply(actionType, deltaTime, character, targets); diff --git a/Barotrauma/BarotraumaShared/SharedSource/Map/Explosion.cs b/Barotrauma/BarotraumaShared/SharedSource/Map/Explosion.cs index 5c11eebb7..f5c63b773 100644 --- a/Barotrauma/BarotraumaShared/SharedSource/Map/Explosion.cs +++ b/Barotrauma/BarotraumaShared/SharedSource/Map/Explosion.cs @@ -7,6 +7,7 @@ using Microsoft.Xna.Framework; using System; using System.Collections.Generic; using System.Linq; +using System.Threading; namespace Barotrauma { @@ -648,7 +649,11 @@ namespace Barotrauma } } - private static readonly Dictionary damagedStructures = new Dictionary(); + // ThreadLocal for thread-safe structure damage tracking + private static readonly ThreadLocal> damagedStructuresLocal = + new ThreadLocal>(() => new Dictionary()); + private static Dictionary damagedStructures => damagedStructuresLocal.Value; + /// /// Returns a dictionary where the keys are the structures that took damage and the values are the amount of damage taken /// diff --git a/Barotrauma/BarotraumaShared/SharedSource/Map/Hull.cs b/Barotrauma/BarotraumaShared/SharedSource/Map/Hull.cs index 1c193d983..1d4b733ba 100644 --- a/Barotrauma/BarotraumaShared/SharedSource/Map/Hull.cs +++ b/Barotrauma/BarotraumaShared/SharedSource/Map/Hull.cs @@ -6,6 +6,7 @@ using System; using System.Collections.Generic; using System.Globalization; using System.Linq; +using System.Threading; using System.Xml.Linq; using Barotrauma.MapCreatures.Behavior; using Barotrauma.Items.Components; @@ -1133,13 +1134,18 @@ namespace Barotrauma } /// - /// Used in + /// Used in - ThreadLocal for thread safety /// - private static readonly Dictionary cachedDistances = []; + private static readonly ThreadLocal> cachedDistancesLocal = + new ThreadLocal>(() => new Dictionary()); /// - /// Used in + /// Used in - ThreadLocal for thread safety /// - private static readonly PriorityQueue<(Hull hull, Vector2 pos), float> priorityQueue = new PriorityQueue<(Hull hull, Vector2 pos), float>(); + private static readonly ThreadLocal> priorityQueueLocal = + new ThreadLocal>(() => new PriorityQueue<(Hull hull, Vector2 pos), float>()); + + private static Dictionary cachedDistances => cachedDistancesLocal.Value; + private static PriorityQueue<(Hull hull, Vector2 pos), float> priorityQueue => priorityQueueLocal.Value; /// /// Approximate distance from this hull to the target hull, moving through open gaps without passing through walls. diff --git a/Barotrauma/BarotraumaShared/SharedSource/Map/Submarine.cs b/Barotrauma/BarotraumaShared/SharedSource/Map/Submarine.cs index dfe6eceb1..4a8e2a3c9 100644 --- a/Barotrauma/BarotraumaShared/SharedSource/Map/Submarine.cs +++ b/Barotrauma/BarotraumaShared/SharedSource/Map/Submarine.cs @@ -12,6 +12,7 @@ using System.Collections.Generic; using System.Collections.Immutable; using System.Diagnostics; using System.Linq; +using System.Threading; using System.Xml.Linq; using Voronoi2; @@ -97,10 +98,11 @@ namespace Barotrauma } } - private static Vector2 lastPickedPosition; - private static float lastPickedFraction; - private static Fixture lastPickedFixture; - private static Vector2 lastPickedNormal; + // ThreadLocal for thread-safe ray casting results + private static readonly ThreadLocal lastPickedPositionLocal = new ThreadLocal(); + private static readonly ThreadLocal lastPickedFractionLocal = new ThreadLocal(); + private static readonly ThreadLocal lastPickedFixtureLocal = new ThreadLocal(); + private static readonly ThreadLocal lastPickedNormalLocal = new ThreadLocal(); private Vector2 prevPosition; @@ -114,22 +116,22 @@ namespace Barotrauma public static Vector2 LastPickedPosition { - get { return lastPickedPosition; } + get { return lastPickedPositionLocal.Value; } } public static float LastPickedFraction { - get { return lastPickedFraction; } + get { return lastPickedFractionLocal.Value; } } public static Fixture LastPickedFixture { - get { return lastPickedFixture; } + get { return lastPickedFixtureLocal.Value; } } public static Vector2 LastPickedNormal { - get { return lastPickedNormal; } + get { return lastPickedNormalLocal.Value; } } public bool Loading @@ -854,10 +856,10 @@ namespace Barotrauma }, ref aabb); if (closestFraction <= 0.0f) { - lastPickedPosition = rayStart; - lastPickedFraction = closestFraction; - lastPickedFixture = closestFixture; - lastPickedNormal = closestNormal; + lastPickedPositionLocal.Value = rayStart; + lastPickedFractionLocal.Value = closestFraction; + lastPickedFixtureLocal.Value = closestFixture; + lastPickedNormalLocal.Value = closestNormal; return closestBody; } } @@ -876,16 +878,22 @@ namespace Barotrauma return fraction; }, rayStart, rayEnd, collisionCategory ?? Category.All); - lastPickedPosition = rayStart + (rayEnd - rayStart) * closestFraction; - lastPickedFraction = closestFraction; - lastPickedFixture = closestFixture; - lastPickedNormal = closestNormal; + lastPickedPositionLocal.Value = rayStart + (rayEnd - rayStart) * closestFraction; + lastPickedFractionLocal.Value = closestFraction; + lastPickedFixtureLocal.Value = closestFixture; + lastPickedNormalLocal.Value = closestNormal; return closestBody; } - private static readonly Dictionary bodyDist = new Dictionary(); - private static readonly List bodies = new List(); + // ThreadLocal for thread-safe body picking + private static readonly ThreadLocal> bodyDistLocal = + new ThreadLocal>(() => new Dictionary()); + private static readonly ThreadLocal> bodiesLocal = + new ThreadLocal>(() => new List()); + + private static Dictionary bodyDist => bodyDistLocal.Value; + private static List bodies => bodiesLocal.Value; public static float LastPickedBodyDist(Body body) { @@ -919,10 +927,10 @@ namespace Barotrauma } if (fraction < closestFraction) { - lastPickedPosition = rayStart + (rayEnd - rayStart) * fraction; - lastPickedFraction = fraction; - lastPickedNormal = normal; - lastPickedFixture = fixture; + lastPickedPositionLocal.Value = rayStart + (rayEnd - rayStart) * fraction; + lastPickedFractionLocal.Value = fraction; + lastPickedNormalLocal.Value = normal; + lastPickedFixtureLocal.Value = fixture; } //continue return -1; @@ -940,10 +948,10 @@ namespace Barotrauma if (!fixture.Shape.TestPoint(ref transform, ref rayStart)) { return true; } closestFraction = 0.0f; - lastPickedPosition = rayStart; - lastPickedFraction = 0.0f; - lastPickedNormal = Vector2.Normalize(rayEnd - rayStart); - lastPickedFixture = fixture; + lastPickedPositionLocal.Value = rayStart; + lastPickedFractionLocal.Value = 0.0f; + lastPickedNormalLocal.Value = Vector2.Normalize(rayEnd - rayStart); + lastPickedFixtureLocal.Value = fixture; bodies.Add(fixture.Body); bodyDist[fixture.Body] = 0.0f; return false; @@ -1011,7 +1019,7 @@ namespace Barotrauma if (Vector2.DistanceSquared(rayStart, rayEnd) < 0.01f) { - lastPickedPosition = rayEnd; + lastPickedPositionLocal.Value = rayEnd; return null; } @@ -1053,10 +1061,10 @@ namespace Barotrauma , rayStart, rayEnd); - lastPickedPosition = rayStart + (rayEnd - rayStart) * closestFraction; - lastPickedFraction = closestFraction; - lastPickedFixture = closestFixture; - lastPickedNormal = closestNormal; + lastPickedPositionLocal.Value = rayStart + (rayEnd - rayStart) * closestFraction; + lastPickedFractionLocal.Value = closestFraction; + lastPickedFixtureLocal.Value = closestFixture; + lastPickedNormalLocal.Value = closestNormal; return closestBody; } diff --git a/Barotrauma/BarotraumaShared/SharedSource/Utils/ToolBox.cs b/Barotrauma/BarotraumaShared/SharedSource/Utils/ToolBox.cs index a96376047..2ab344c36 100644 --- a/Barotrauma/BarotraumaShared/SharedSource/Utils/ToolBox.cs +++ b/Barotrauma/BarotraumaShared/SharedSource/Utils/ToolBox.cs @@ -2,6 +2,7 @@ using Barotrauma.Networking; using Microsoft.Xna.Framework; using System; +using System.Collections.Concurrent; using System.Collections.Generic; using System.Collections.Immutable; using System.Diagnostics.CodeAnalysis; @@ -75,7 +76,7 @@ namespace Barotrauma return !corrected; } - private static readonly Dictionary cachedFileNames = new Dictionary(); + private static readonly ConcurrentDictionary cachedFileNames = new ConcurrentDictionary(); public static string CorrectFilenameCase(string filename, out bool corrected, string directory = "") { @@ -153,7 +154,7 @@ namespace Barotrauma if (i < subDirs.Length - 1) { filename += "/"; } } - cachedFileNames.Add(originalFilename, filename); + cachedFileNames.TryAdd(originalFilename, filename); return filename; } @@ -355,32 +356,26 @@ namespace Barotrauma return text; } - private static Dictionary> cachedLines = new Dictionary>(); + private static readonly ConcurrentDictionary> cachedLines = new ConcurrentDictionary>(); public static string GetRandomLine(string filePath, Rand.RandSync randSync = Rand.RandSync.ServerAndClient) { - List lines; - if (cachedLines.ContainsKey(filePath)) - { - lines = cachedLines[filePath]; - } - else + List lines = cachedLines.GetOrAdd(filePath, path => { try { - lines = File.ReadAllLines(filePath, catchUnauthorizedAccessExceptions: false).ToList(); - cachedLines.Add(filePath, lines); - if (lines.Count == 0) + var fileLines = File.ReadAllLines(path, catchUnauthorizedAccessExceptions: false).ToList(); + if (fileLines.Count == 0) { - DebugConsole.ThrowError("File \"" + filePath + "\" is empty!"); - return ""; + DebugConsole.ThrowError("File \"" + path + "\" is empty!"); } + return fileLines; } catch (Exception e) { - DebugConsole.ThrowError("Couldn't open file \"" + filePath + "\"!", e); - return ""; + DebugConsole.ThrowError("Couldn't open file \"" + path + "\"!", e); + return new List(); } - } + }); if (lines.Count == 0) return ""; return lines[Rand.Range(0, lines.Count, randSync)]; From 49355fe32b2104e353b245e8ceb1d7cef76608dd Mon Sep 17 00:00:00 2001 From: Eero Date: Sun, 28 Dec 2025 14:42:17 +0800 Subject: [PATCH 08/14] Unstable Add thread-safe queue for deferred physics body creation Introduces PhysicsBodyQueue to safely defer physics body creation to the main thread, addressing thread-safety issues with Farseer Physics during parallel updates. Updates LevelResource, TriggerComponent, BallastFloraBehavior, and MapEntity to use the queue for all physics body creation and refresh operations, ensuring they are processed outside of parallel loops. Also adds cleanup of the queue at round end. --- .../SharedSource/GameSession/GameSession.cs | 1 + .../Components/Holdable/LevelResource.cs | 23 +++++- .../Items/Components/TriggerComponent.cs | 29 ++++++- .../Map/Creatures/BallastFloraBehavior.cs | 57 ++++++++++++- .../SharedSource/Map/MapEntity.cs | 8 ++ .../SharedSource/Physics/PhysicsBodyQueue.cs | 82 +++++++++++++++++++ 6 files changed, 191 insertions(+), 9 deletions(-) create mode 100644 Barotrauma/BarotraumaShared/SharedSource/Physics/PhysicsBodyQueue.cs diff --git a/Barotrauma/BarotraumaShared/SharedSource/GameSession/GameSession.cs b/Barotrauma/BarotraumaShared/SharedSource/GameSession/GameSession.cs index 1af627a98..c23a81644 100644 --- a/Barotrauma/BarotraumaShared/SharedSource/GameSession/GameSession.cs +++ b/Barotrauma/BarotraumaShared/SharedSource/GameSession/GameSession.cs @@ -1153,6 +1153,7 @@ namespace Barotrauma EventManager?.EndRound(); StatusEffect.StopAll(); AfflictionPrefab.ClearAllEffects(); + PhysicsBodyQueue.Clear(); IsRunning = false; #if CLIENT diff --git a/Barotrauma/BarotraumaShared/SharedSource/Items/Components/Holdable/LevelResource.cs b/Barotrauma/BarotraumaShared/SharedSource/Items/Components/Holdable/LevelResource.cs index 5f03a6163..797ec14e7 100644 --- a/Barotrauma/BarotraumaShared/SharedSource/Items/Components/Holdable/LevelResource.cs +++ b/Barotrauma/BarotraumaShared/SharedSource/Items/Components/Holdable/LevelResource.cs @@ -13,6 +13,12 @@ namespace Barotrauma.Items.Components private Holdable holdable; private float deattachTimer; + + /// + /// Flag to prevent multiple queued creation requests. + /// Uses volatile to ensure visibility across threads. + /// + private volatile bool triggerBodyCreationQueued; [Serialize(1.0f, IsPropertySaveable.No, description: "How long it takes to deattach the item from the level walls (in seconds).")] public float DeattachDuration @@ -109,9 +115,22 @@ namespace Barotrauma.Items.Components } else { - if (trigger == null) + if (trigger == null && !triggerBodyCreationQueued) { - CreateTriggerBody(); + // Queue the physics body creation to be processed on the main thread. + // This is necessary because physics body creation is not thread-safe + // and Update() may be called from a parallel loop. + triggerBodyCreationQueued = true; + PhysicsBodyQueue.EnqueueCreation(() => + { + // Double-check that trigger hasn't been created yet + // (in case this was called multiple times before queue processing) + if (trigger == null && !item.Removed) + { + CreateTriggerBody(); + } + triggerBodyCreationQueued = false; + }); } if (trigger != null && Vector2.DistanceSquared(item.SimPosition, trigger.SimPosition) > 0.01f) { diff --git a/Barotrauma/BarotraumaShared/SharedSource/Items/Components/TriggerComponent.cs b/Barotrauma/BarotraumaShared/SharedSource/Items/Components/TriggerComponent.cs index b2ff80ee6..c839d488c 100644 --- a/Barotrauma/BarotraumaShared/SharedSource/Items/Components/TriggerComponent.cs +++ b/Barotrauma/BarotraumaShared/SharedSource/Items/Components/TriggerComponent.cs @@ -73,6 +73,11 @@ namespace Barotrauma.Items.Components public PhysicsBody PhysicsBody { get; private set; } + /// + /// Flag to prevent multiple queued refresh requests. + /// + private volatile bool physicsBodyRefreshQueued; + private float radius; [Editable, Serialize(0.0f, IsPropertySaveable.Yes)] public float Radius @@ -83,7 +88,7 @@ namespace Barotrauma.Items.Components { if (radius == value) { return; } radius = value; - if (PhysicsBody != null) { RefreshPhysicsBodySize(); } + if (PhysicsBody != null) { QueuePhysicsBodyRefresh(); } } } @@ -97,7 +102,7 @@ namespace Barotrauma.Items.Components { if (width == value) { return; } width = value; - if (PhysicsBody != null) { RefreshPhysicsBodySize(); } + if (PhysicsBody != null) { QueuePhysicsBodyRefresh(); } } } @@ -111,10 +116,28 @@ namespace Barotrauma.Items.Components { if (height == value) { return; } height = value; - if (PhysicsBody != null) { RefreshPhysicsBodySize(); } + if (PhysicsBody != null) { QueuePhysicsBodyRefresh(); } } } + /// + /// Queue the physics body refresh to be executed on the main thread. + /// This is necessary because physics body operations are not thread-safe. + /// + private void QueuePhysicsBodyRefresh() + { + if (physicsBodyRefreshQueued) { return; } + physicsBodyRefreshQueued = true; + PhysicsBodyQueue.EnqueueCreation(() => + { + if (!item.Removed) + { + RefreshPhysicsBodySize(); + } + physicsBodyRefreshQueued = false; + }); + } + private float currentRadius, currentWidth, currentHeight; private Vector2 bodyOffset; diff --git a/Barotrauma/BarotraumaShared/SharedSource/Map/Creatures/BallastFloraBehavior.cs b/Barotrauma/BarotraumaShared/SharedSource/Map/Creatures/BallastFloraBehavior.cs index a06585116..351083203 100644 --- a/Barotrauma/BarotraumaShared/SharedSource/Map/Creatures/BallastFloraBehavior.cs +++ b/Barotrauma/BarotraumaShared/SharedSource/Map/Creatures/BallastFloraBehavior.cs @@ -307,6 +307,12 @@ namespace Barotrauma.MapCreatures.Behavior public readonly List Branches = new List(); private BallastFloraBranch? root; private readonly List bodies = new List(); + + /// + /// Branches that need physics bodies created on the main thread. + /// + private readonly List pendingBodyCreations = new List(); + private readonly object pendingBodyCreationsLock = new object(); private bool isDead; @@ -347,7 +353,8 @@ namespace Barotrauma.MapCreatures.Behavior } } UpdateConnections(branch); - CreateBody(branch); + // OnMapLoaded runs on the main thread, so we can create bodies immediately + CreateBody(branch, immediate: true); } } @@ -998,10 +1005,52 @@ namespace Barotrauma.MapCreatures.Behavior } /// - /// Create a body for a branch which works as the hitbox for flamer + /// Queue a physics body creation for a branch. + /// The actual body will be created on the main thread to ensure thread safety. /// - /// - private void CreateBody(BallastFloraBranch branch) + /// The branch to create a body for + /// If true, create the body immediately (only safe when called from main thread) + private void CreateBody(BallastFloraBranch branch, bool immediate = false) + { + if (immediate) + { + CreateBodyImmediate(branch); + return; + } + + lock (pendingBodyCreationsLock) + { + pendingBodyCreations.Add(branch); + } + PhysicsBodyQueue.EnqueueCreation(() => ProcessPendingBodyCreations()); + } + + /// + /// Process all pending body creations on the main thread. + /// This ensures Farseer Physics operations are thread-safe. + /// + private void ProcessPendingBodyCreations() + { + List branchesToProcess; + lock (pendingBodyCreationsLock) + { + if (pendingBodyCreations.Count == 0) { return; } + branchesToProcess = new List(pendingBodyCreations); + pendingBodyCreations.Clear(); + } + + foreach (var branch in branchesToProcess) + { + if (branch.Removed) { continue; } + CreateBodyImmediate(branch); + } + } + + /// + /// Actually create the physics body for a branch. + /// Must be called on the main thread. + /// + private void CreateBodyImmediate(BallastFloraBranch branch) { Rectangle rect = branch.Rect; Vector2 pos = Parent.Position + Offset + branch.Position; diff --git a/Barotrauma/BarotraumaShared/SharedSource/Map/MapEntity.cs b/Barotrauma/BarotraumaShared/SharedSource/Map/MapEntity.cs index 8c839934b..572990b3b 100644 --- a/Barotrauma/BarotraumaShared/SharedSource/Map/MapEntity.cs +++ b/Barotrauma/BarotraumaShared/SharedSource/Map/MapEntity.cs @@ -686,6 +686,10 @@ namespace Barotrauma } ); + // Process any physics body creation operations queued during Hull/Structure updates. + // BallastFlora growth (from Hull.Update) may queue physics body creations. + PhysicsBodyQueue.ProcessPendingCreations(); + #if CLIENT // Hull Cheats need to be executed after Hull update Hull.UpdateCheats(deltaTime, cam); @@ -727,6 +731,10 @@ namespace Barotrauma throw new InvalidOperationException($"Error while updating item {lastUpdatedItem?.Name ?? "null"}", innerException: e); } + // Process any physics body creation operations that were queued during the parallel update. + // This must be done on the main thread because Farseer Physics is not thread-safe. + PhysicsBodyQueue.ProcessPendingCreations(); + UpdateAllProjSpecific(scaledDeltaTime); Spawner?.Update(); diff --git a/Barotrauma/BarotraumaShared/SharedSource/Physics/PhysicsBodyQueue.cs b/Barotrauma/BarotraumaShared/SharedSource/Physics/PhysicsBodyQueue.cs new file mode 100644 index 000000000..64e20e51f --- /dev/null +++ b/Barotrauma/BarotraumaShared/SharedSource/Physics/PhysicsBodyQueue.cs @@ -0,0 +1,82 @@ +using System; +using System.Collections.Generic; + +namespace Barotrauma +{ + /// + /// Thread-safe queue for deferring physics body creation operations to the main thread. + /// This is necessary because Farseer Physics' DynamicTree is not thread-safe, + /// and physics bodies cannot be safely created during parallel item updates. + /// + static class PhysicsBodyQueue + { + private static readonly object _lock = new object(); + private static readonly Queue _pendingCreations = new Queue(); + + /// + /// Enqueues a physics body creation action to be executed on the main thread. + /// This method is thread-safe and can be called from parallel update loops. + /// + /// The action that creates the physics body + public static void EnqueueCreation(Action createAction) + { + if (createAction == null) { return; } + lock (_lock) + { + _pendingCreations.Enqueue(createAction); + } + } + + /// + /// Gets the number of pending physics body creation operations. + /// + public static int PendingCount + { + get + { + lock (_lock) + { + return _pendingCreations.Count; + } + } + } + + /// + /// Processes all pending physics body creation operations. + /// Must be called on the main thread, outside of any parallel loops. + /// + public static void ProcessPendingCreations() + { + while (true) + { + Action action; + lock (_lock) + { + if (_pendingCreations.Count == 0) { break; } + action = _pendingCreations.Dequeue(); + } + try + { + action?.Invoke(); + } + catch (Exception e) + { + DebugConsole.ThrowError($"Error processing deferred physics body creation: {e.Message}", e); + } + } + } + + /// + /// Clears all pending physics body creation operations. + /// Should be called when ending a round or cleaning up. + /// + public static void Clear() + { + lock (_lock) + { + _pendingCreations.Clear(); + } + } + } +} + From f4855836210e330e99a02978e5114e9c81bedc2c Mon Sep 17 00:00:00 2001 From: Eero Date: Sun, 28 Dec 2025 15:10:06 +0800 Subject: [PATCH 09/14] Unstable 0.2 Defer physics operations during parallel updates Introduces a thread-safe queue for deferring physics operations (such as body creation and transforms) to the main thread, ensuring Farseer Physics is not accessed from parallel contexts. Updates Holdable, Item, MapEntity, and GameScreen to use the new PhysicsBodyQueue for safe physics operations during parallel updates, and refactors PhysicsBodyQueue to support general deferred physics actions. --- .../Items/Components/Holdable/Holdable.cs | 40 ++++++- .../SharedSource/Items/Item.cs | 64 +++++++++-- .../SharedSource/Map/MapEntity.cs | 55 ++++++++-- .../SharedSource/Physics/PhysicsBodyQueue.cs | 103 +++++++++++++++--- .../SharedSource/Screens/GameScreen.cs | 45 +++++++- 5 files changed, 261 insertions(+), 46 deletions(-) diff --git a/Barotrauma/BarotraumaShared/SharedSource/Items/Components/Holdable/Holdable.cs b/Barotrauma/BarotraumaShared/SharedSource/Items/Components/Holdable/Holdable.cs index 429554361..405b0c9b8 100644 --- a/Barotrauma/BarotraumaShared/SharedSource/Items/Components/Holdable/Holdable.cs +++ b/Barotrauma/BarotraumaShared/SharedSource/Items/Components/Holdable/Holdable.cs @@ -486,7 +486,10 @@ namespace Barotrauma.Items.Components } else { - item.body.ResetDynamics(); + // Calculate target position + Vector2 targetPos; + Submarine forceSubmarine = picker.Submarine; + Limb heldHand, arm; if (picker.Inventory.IsInLimbSlot(item, InvSlotType.LeftHand)) { @@ -504,17 +507,42 @@ namespace Barotrauma.Items.Components Vector2 diff = new Vector2( (heldHand.SimPosition.X - arm.SimPosition.X) / 2f, (heldHand.SimPosition.Y - arm.SimPosition.Y) / 2.5f); + targetPos = heldHand.SimPosition + diff; + } + else + { + targetPos = picker.SimPosition; + } + // Defer physics operations if in parallel context + if (PhysicsBodyQueue.IsInParallelContext) + { + var capturedBody = item.body; + var capturedItem = item; + var capturedTargetPos = targetPos; + var capturedForceSubmarine = forceSubmarine; + + PhysicsBodyQueue.Enqueue(() => + { + if (capturedBody.Removed || capturedItem.Removed) { return; } + capturedBody.ResetDynamics(); + //we have forced the item to be in the same sub as the dropper above, + //and are placing it to the position of the hands in "local" coordinates + //which may be outside the sub if the character is e.g. standing half-way through the airlock + // -> let's use the forceSubmarine argument ensure the item is still considered to be in the sub's coordinate space, + // or it will end up in a weird state and seemingly disappear + capturedItem.SetTransform(capturedTargetPos, 0.0f, forceSubmarine: capturedForceSubmarine); + }); + } + else + { + item.body.ResetDynamics(); //we have forced the item to be in the same sub as the dropper above, //and are placing it to the position of the hands in "local" coordinates //which may be outside the sub if the character is e.g. standing half-way through the airlock // -> let's use the forceSubmarine argument ensure the item is still considered to be in the sub's coordinate space, // or it will end up in a weird state and seemingly disappear - item.SetTransform(heldHand.SimPosition + diff, 0.0f, forceSubmarine: picker.Submarine); - } - else - { - item.SetTransform(picker.SimPosition, 0.0f, forceSubmarine: picker.Submarine); + item.SetTransform(targetPos, 0.0f, forceSubmarine: forceSubmarine); } } } diff --git a/Barotrauma/BarotraumaShared/SharedSource/Items/Item.cs b/Barotrauma/BarotraumaShared/SharedSource/Items/Item.cs index c2576bb48..2ce5e3d2d 100644 --- a/Barotrauma/BarotraumaShared/SharedSource/Items/Item.cs +++ b/Barotrauma/BarotraumaShared/SharedSource/Items/Item.cs @@ -3669,20 +3669,45 @@ namespace Barotrauma if (body != null) { IsActive = true; - body.Enabled = true; - body.PhysEnabled = true; - body.ResetDynamics(); - if (dropper != null) + + // Physics body operations must be deferred if we're in a parallel update context, + // because Farseer Physics is not thread-safe. + if (PhysicsBodyQueue.IsInParallelContext) { - if (body.Removed) + // Capture the values we need for the deferred operation + var capturedBody = body; + var capturedDropperSimPos = dropper?.SimPosition ?? Microsoft.Xna.Framework.Vector2.Zero; + var capturedSetTransform = setTransform && dropper != null; + + PhysicsBodyQueue.Enqueue(() => { - DebugConsole.ThrowError( - "Failed to drop the item \"" + Name + "\" (body has been removed" - + (Removed ? ", item has been removed)" : ")")); - } - else if (setTransform) + if (capturedBody.Removed || Removed) { return; } + capturedBody.Enabled = true; + capturedBody.PhysEnabled = true; + capturedBody.ResetDynamics(); + if (capturedSetTransform) + { + capturedBody.SetTransformIgnoreContacts(capturedDropperSimPos, 0.0f); + } + }); + } + else + { + body.Enabled = true; + body.PhysEnabled = true; + body.ResetDynamics(); + if (dropper != null) { - body.SetTransformIgnoreContacts(dropper.SimPosition, 0.0f); + if (body.Removed) + { + DebugConsole.ThrowError( + "Failed to drop the item \"" + Name + "\" (body has been removed" + + (Removed ? ", item has been removed)" : ")")); + } + else if (setTransform) + { + body.SetTransformIgnoreContacts(dropper.SimPosition, 0.0f); + } } } } @@ -3693,7 +3718,22 @@ namespace Barotrauma { if (setTransform) { - SetTransform(Container.SimPosition, 0.0f); + // Defer SetTransform if in parallel context + if (PhysicsBodyQueue.IsInParallelContext) + { + var capturedContainerSimPos = Container.SimPosition; + PhysicsBodyQueue.Enqueue(() => + { + if (!Removed) + { + SetTransform(capturedContainerSimPos, 0.0f); + } + }); + } + else + { + SetTransform(Container.SimPosition, 0.0f); + } } Container.RemoveContained(this); Container = null; diff --git a/Barotrauma/BarotraumaShared/SharedSource/Map/MapEntity.cs b/Barotrauma/BarotraumaShared/SharedSource/Map/MapEntity.cs index 572990b3b..39c77c116 100644 --- a/Barotrauma/BarotraumaShared/SharedSource/Map/MapEntity.cs +++ b/Barotrauma/BarotraumaShared/SharedSource/Map/MapEntity.cs @@ -660,7 +660,15 @@ namespace Barotrauma { Parallel.ForEach(hullList, parallelOptions, hull => { - hull.Update(deltaTime, cam); + PhysicsBodyQueue.IsInParallelContext = true; + try + { + hull.Update(deltaTime, cam); + } + finally + { + PhysicsBodyQueue.IsInParallelContext = false; + } }); }, // Structure parallel update @@ -668,7 +676,15 @@ namespace Barotrauma { Parallel.ForEach(structureList, parallelOptions, structure => { - structure.Update(deltaTime, cam); + PhysicsBodyQueue.IsInParallelContext = true; + try + { + structure.Update(deltaTime, cam); + } + finally + { + PhysicsBodyQueue.IsInParallelContext = false; + } }); }, // Gap reset (must be done before update) @@ -686,9 +702,9 @@ namespace Barotrauma } ); - // Process any physics body creation operations queued during Hull/Structure updates. - // BallastFlora growth (from Hull.Update) may queue physics body creations. - PhysicsBodyQueue.ProcessPendingCreations(); + // Process any physics operations queued during Hull/Structure updates. + // BallastFlora growth (from Hull.Update) may queue physics body creations/transforms. + PhysicsBodyQueue.ProcessPendingOperations(); #if CLIENT // Hull Cheats need to be executed after Hull update @@ -699,8 +715,19 @@ namespace Barotrauma var shuffledGaps = gapList.OrderBy(g => Rand.Int(int.MaxValue)).ToList(); Parallel.ForEach(gapList, parallelOptions, gap => { - gap.Update(deltaTime, cam); + PhysicsBodyQueue.IsInParallelContext = true; + try + { + gap.Update(deltaTime, cam); + } + finally + { + PhysicsBodyQueue.IsInParallelContext = false; + } }); + + // Process any physics operations queued during Gap updates. + PhysicsBodyQueue.ProcessPendingOperations(); #if CLIENT sw.Stop(); @@ -718,8 +745,16 @@ namespace Barotrauma { Parallel.ForEach(itemList, parallelOptions, item => { - lastUpdatedItem = item; - item.Update(scaledDeltaTime, cam); + PhysicsBodyQueue.IsInParallelContext = true; + try + { + lastUpdatedItem = item; + item.Update(scaledDeltaTime, cam); + } + finally + { + PhysicsBodyQueue.IsInParallelContext = false; + } }); } catch (InvalidOperationException e) @@ -731,9 +766,9 @@ namespace Barotrauma throw new InvalidOperationException($"Error while updating item {lastUpdatedItem?.Name ?? "null"}", innerException: e); } - // Process any physics body creation operations that were queued during the parallel update. + // Process any physics operations that were queued during the parallel update. // This must be done on the main thread because Farseer Physics is not thread-safe. - PhysicsBodyQueue.ProcessPendingCreations(); + PhysicsBodyQueue.ProcessPendingOperations(); UpdateAllProjSpecific(scaledDeltaTime); Spawner?.Update(); diff --git a/Barotrauma/BarotraumaShared/SharedSource/Physics/PhysicsBodyQueue.cs b/Barotrauma/BarotraumaShared/SharedSource/Physics/PhysicsBodyQueue.cs index 64e20e51f..a8aec47ef 100644 --- a/Barotrauma/BarotraumaShared/SharedSource/Physics/PhysicsBodyQueue.cs +++ b/Barotrauma/BarotraumaShared/SharedSource/Physics/PhysicsBodyQueue.cs @@ -1,17 +1,65 @@ using System; using System.Collections.Generic; +using System.Threading; namespace Barotrauma { /// - /// Thread-safe queue for deferring physics body creation operations to the main thread. + /// Thread-safe queue for deferring physics operations to the main thread. /// This is necessary because Farseer Physics' DynamicTree is not thread-safe, - /// and physics bodies cannot be safely created during parallel item updates. + /// and physics operations cannot be safely performed during parallel updates. + /// + /// Supported operations include: + /// - Physics body creation + /// - Physics body transform updates (SetTransform, SetTransformIgnoreContacts) + /// - Any other operation that modifies the Farseer physics world /// +/// +/// ├─> PhysicsBodyQueue.IsInParallelContext = true (ThreadStatic) +/// ├─> Item.Update() +/// │ └─> StatusEffect.Apply() +/// │ └─> Character.Kill() +/// │ └─> Item.Drop() +/// │ └─> Check if IsInParallelContext == true +/// │ └─> PhysicsBodyQueue.Enqueue(Physics operation) +/// ├──> PhysicsBodyQueue.IsInParallelContext = false +/// └──> PhysicsBodyQueue.ProcessPendingOperations() ← Main thread executes +/// └─> body.SetTransformIgnoreContacts() static class PhysicsBodyQueue { private static readonly object _lock = new object(); - private static readonly Queue _pendingCreations = new Queue(); + private static readonly Queue _pendingOperations = new Queue(); + + /// + /// Thread-local flag indicating whether the current thread is in a parallel physics update context. + /// When true, physics operations should be deferred using this queue instead of executing immediately. + /// + [ThreadStatic] + private static bool _isInParallelContext; + + /// + /// Gets or sets whether the current thread is in a parallel update context. + /// When true, physics operations should be queued instead of executed immediately. + /// + public static bool IsInParallelContext + { + get => _isInParallelContext; + set => _isInParallelContext = value; + } + + /// + /// Enqueues a physics operation to be executed on the main thread. + /// This method is thread-safe and can be called from parallel update loops. + /// + /// The physics operation to defer + public static void Enqueue(Action operation) + { + if (operation == null) { return; } + lock (_lock) + { + _pendingOperations.Enqueue(operation); + } + } /// /// Enqueues a physics body creation action to be executed on the main thread. @@ -20,15 +68,31 @@ namespace Barotrauma /// The action that creates the physics body public static void EnqueueCreation(Action createAction) { - if (createAction == null) { return; } - lock (_lock) + Enqueue(createAction); + } + + /// + /// Executes a physics operation, either immediately or deferred depending on context. + /// If called from a parallel context, the operation will be queued for later execution. + /// If called from the main thread (outside parallel loops), the operation executes immediately. + /// + /// The physics operation to execute + public static void ExecuteOrDefer(Action operation) + { + if (operation == null) { return; } + + if (_isInParallelContext) { - _pendingCreations.Enqueue(createAction); + Enqueue(operation); + } + else + { + operation(); } } /// - /// Gets the number of pending physics body creation operations. + /// Gets the number of pending physics operations. /// public static int PendingCount { @@ -36,24 +100,24 @@ namespace Barotrauma { lock (_lock) { - return _pendingCreations.Count; + return _pendingOperations.Count; } } } /// - /// Processes all pending physics body creation operations. + /// Processes all pending physics operations. /// Must be called on the main thread, outside of any parallel loops. /// - public static void ProcessPendingCreations() + public static void ProcessPendingOperations() { while (true) { Action action; lock (_lock) { - if (_pendingCreations.Count == 0) { break; } - action = _pendingCreations.Dequeue(); + if (_pendingOperations.Count == 0) { break; } + action = _pendingOperations.Dequeue(); } try { @@ -61,20 +125,29 @@ namespace Barotrauma } catch (Exception e) { - DebugConsole.ThrowError($"Error processing deferred physics body creation: {e.Message}", e); + DebugConsole.ThrowError($"Error processing deferred physics operation: {e.Message}", e); } } } /// - /// Clears all pending physics body creation operations. + /// Legacy method for backwards compatibility. + /// Calls ProcessPendingOperations(). + /// + public static void ProcessPendingCreations() + { + ProcessPendingOperations(); + } + + /// + /// Clears all pending physics operations. /// Should be called when ending a round or cleaning up. /// public static void Clear() { lock (_lock) { - _pendingCreations.Clear(); + _pendingOperations.Clear(); } } } diff --git a/Barotrauma/BarotraumaShared/SharedSource/Screens/GameScreen.cs b/Barotrauma/BarotraumaShared/SharedSource/Screens/GameScreen.cs index 11b3ce9ad..577324da2 100644 --- a/Barotrauma/BarotraumaShared/SharedSource/Screens/GameScreen.cs +++ b/Barotrauma/BarotraumaShared/SharedSource/Screens/GameScreen.cs @@ -178,9 +178,23 @@ namespace Barotrauma Parallel.Invoke(parallelOptions, () => GameMain.ParticleManager.Update((float)deltaTime), - () => { if (Level.Loaded != null) Level.Loaded.Update((float)deltaTime, cam); } + () => + { + PhysicsBodyQueue.IsInParallelContext = true; + try + { + if (Level.Loaded != null) Level.Loaded.Update((float)deltaTime, cam); + } + finally + { + PhysicsBodyQueue.IsInParallelContext = false; + } + } ); + // Process any physics operations queued during Level update + PhysicsBodyQueue.ProcessPendingOperations(); + sw.Stop(); GameMain.PerformanceCounter.AddElapsedTicks("Update:Particles+Level", sw.ElapsedTicks); @@ -246,9 +260,34 @@ namespace Barotrauma #elif SERVER Parallel.Invoke(parallelOptions, - () => { if (Level.Loaded != null) Level.Loaded.Update((float)deltaTime, Camera.Instance); }, - () => Character.UpdateAll((float)deltaTime, Camera.Instance) + () => + { + PhysicsBodyQueue.IsInParallelContext = true; + try + { + if (Level.Loaded != null) Level.Loaded.Update((float)deltaTime, Camera.Instance); + } + finally + { + PhysicsBodyQueue.IsInParallelContext = false; + } + }, + () => + { + PhysicsBodyQueue.IsInParallelContext = true; + try + { + Character.UpdateAll((float)deltaTime, Camera.Instance); + } + finally + { + PhysicsBodyQueue.IsInParallelContext = false; + } + } ); + + // Process any physics operations queued during parallel updates + PhysicsBodyQueue.ProcessPendingOperations(); #endif var submarines = Submarine.Loaded.ToList(); From ad0bcddaa49fb3108bfea35c3a624a9bd2318e22 Mon Sep 17 00:00:00 2001 From: Eero Date: Sun, 28 Dec 2025 15:11:56 +0800 Subject: [PATCH 10/14] CL_Fix Use TryAdd when adding afflictions to dictionary Replaces afflictions.Add with afflictions.TryAdd to prevent exceptions if the affliction already exists in the dictionary. --- .../ClientSource/Characters/Health/CharacterHealth.cs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Barotrauma/BarotraumaClient/ClientSource/Characters/Health/CharacterHealth.cs b/Barotrauma/BarotraumaClient/ClientSource/Characters/Health/CharacterHealth.cs index 9e38c0b43..0c126392c 100644 --- a/Barotrauma/BarotraumaClient/ClientSource/Characters/Health/CharacterHealth.cs +++ b/Barotrauma/BarotraumaClient/ClientSource/Characters/Health/CharacterHealth.cs @@ -2143,7 +2143,7 @@ namespace Barotrauma if (existingAffliction == null) { existingAffliction = afflictionPrefab.Instantiate(strength); - afflictions.Add(existingAffliction, limb); + afflictions.TryAdd(existingAffliction, limb); newAdded = true; } existingAffliction.SetStrength(strength); From 59bf2749dd9260d250757494b861dfef75baf036 Mon Sep 17 00:00:00 2001 From: Eero Date: Sun, 28 Dec 2025 16:18:49 +0800 Subject: [PATCH 11/14] Improve thread safety in sound and physics systems Refactored SoundChannel and SoundManager to use explicit locking for OpenAL operations and channel assignment, preventing race conditions during parallel sound playback. Added thread-local stacks in DynamicTree to ensure thread safety during parallel physics queries and raycasts. These changes address concurrency issues when sounds or physics queries are triggered from multiple threads. --- .../ClientSource/Sounds/SoundChannel.cs | 152 ++++++++++-------- .../ClientSource/Sounds/SoundManager.cs | 58 +++++-- .../Collision/DynamicTree.cs | 48 +++--- 3 files changed, 154 insertions(+), 104 deletions(-) diff --git a/Barotrauma/BarotraumaClient/ClientSource/Sounds/SoundChannel.cs b/Barotrauma/BarotraumaClient/ClientSource/Sounds/SoundChannel.cs index df9d68998..ade827da5 100644 --- a/Barotrauma/BarotraumaClient/ClientSource/Sounds/SoundChannel.cs +++ b/Barotrauma/BarotraumaClient/ClientSource/Sounds/SoundChannel.cs @@ -503,86 +503,102 @@ namespace Barotrauma.Sounds mutex = new object(); } + // Use the playingChannels lock to protect both channel assignment AND OpenAL operations. + // This prevents race conditions when multiple threads try to play sounds simultaneously + // (e.g., during Parallel.ForEach in MapEntity.UpdateAll). + int poolIndex = (int)sound.SourcePoolIndex; + object channelsLock = sound.Owner.GetPlayingChannelsLock(sound.SourcePoolIndex); + #if !DEBUG try { #endif - if (mutex != null) { Monitor.Enter(mutex); } - if (sound.Owner.CountPlayingInstances(sound) < sound.MaxSimultaneousInstances) + lock (channelsLock) { - ALSourceIndex = sound.Owner.AssignFreeSourceToChannel(this); - } - - if (ALSourceIndex >= 0) - { - if (!IsStream) + if (mutex != null) { Monitor.Enter(mutex); } + try { - Al.Sourcei(sound.Owner.GetSourceFromIndex(Sound.SourcePoolIndex, ALSourceIndex), Al.Buffer, 0); - int alError = Al.GetError(); - if (alError != Al.NoError) + if (sound.Owner.CountPlayingInstancesUnsafe(sound, poolIndex) < sound.MaxSimultaneousInstances) { - throw new Exception("Failed to reset source buffer: " + debugName + ", " + Al.GetErrorString(alError)); + ALSourceIndex = sound.Owner.AssignFreeSourceToChannelUnsafe(this, poolIndex); } - Sound.FillAlBuffers(); - if (Sound.Buffers is not { AlBuffer: not 0, AlMuffledBuffer: not 0 }) { return; } - - uint alBuffer = sound.Owner.GetCategoryMuffle(category) || muffled ? Sound.Buffers.AlMuffledBuffer : Sound.Buffers.AlBuffer; - Al.Sourcei(sound.Owner.GetSourceFromIndex(Sound.SourcePoolIndex, ALSourceIndex), Al.Buffer, (int)alBuffer); - alError = Al.GetError(); - if (alError != Al.NoError) + if (ALSourceIndex >= 0) { - throw new Exception("Failed to bind buffer to source (" + ALSourceIndex.ToString() + ":" + sound.Owner.GetSourceFromIndex(Sound.SourcePoolIndex, ALSourceIndex) + "," + alBuffer.ToString() + "): " + debugName + ", " + Al.GetErrorString(alError)); - } + if (!IsStream) + { + Al.Sourcei(sound.Owner.GetSourceFromIndex(Sound.SourcePoolIndex, ALSourceIndex), Al.Buffer, 0); + int alError = Al.GetError(); + if (alError != Al.NoError) + { + throw new Exception("Failed to reset source buffer: " + debugName + ", " + Al.GetErrorString(alError)); + } - SetProperties(); + Sound.FillAlBuffers(); + if (Sound.Buffers is not { AlBuffer: not 0, AlMuffledBuffer: not 0 }) { return; } - Al.SourcePlay(sound.Owner.GetSourceFromIndex(Sound.SourcePoolIndex, ALSourceIndex)); - alError = Al.GetError(); - if (alError != Al.NoError) - { - throw new Exception("Failed to play source: " + debugName + ", " + Al.GetErrorString(alError)); + uint alBuffer = sound.Owner.GetCategoryMuffle(category) || muffled ? Sound.Buffers.AlMuffledBuffer : Sound.Buffers.AlBuffer; + Al.Sourcei(sound.Owner.GetSourceFromIndex(Sound.SourcePoolIndex, ALSourceIndex), Al.Buffer, (int)alBuffer); + alError = Al.GetError(); + if (alError != Al.NoError) + { + throw new Exception("Failed to bind buffer to source (" + ALSourceIndex.ToString() + ":" + sound.Owner.GetSourceFromIndex(Sound.SourcePoolIndex, ALSourceIndex) + "," + alBuffer.ToString() + "): " + debugName + ", " + Al.GetErrorString(alError)); + } + + SetProperties(); + + Al.SourcePlay(sound.Owner.GetSourceFromIndex(Sound.SourcePoolIndex, ALSourceIndex)); + alError = Al.GetError(); + if (alError != Al.NoError) + { + throw new Exception("Failed to play source: " + debugName + ", " + Al.GetErrorString(alError)); + } + } + else + { + uint alBuffer = 0; + Al.Sourcei(sound.Owner.GetSourceFromIndex(Sound.SourcePoolIndex, ALSourceIndex), Al.Buffer, (int)alBuffer); + int alError = Al.GetError(); + if (alError != Al.NoError) + { + throw new Exception("Failed to reset source buffer: " + debugName + ", " + Al.GetErrorString(alError)); + } + + Al.Sourcei(sound.Owner.GetSourceFromIndex(Sound.SourcePoolIndex, ALSourceIndex), Al.Looping, Al.False); + alError = Al.GetError(); + if (alError != Al.NoError) + { + throw new Exception("Failed to set stream looping state: " + debugName + ", " + Al.GetErrorString(alError)); + } + + streamShortBuffer = new short[STREAM_BUFFER_SIZE]; + + streamBuffers = new uint[4]; + unqueuedBuffers = new uint[4]; + streamBufferAmplitudes = new float[4]; + for (int i = 0; i < 4; i++) + { + Al.GenBuffer(out streamBuffers[i]); + + alError = Al.GetError(); + if (alError != Al.NoError) + { + throw new Exception("Failed to generate stream buffers: " + debugName + ", " + Al.GetErrorString(alError)); + } + + if (!Al.IsBuffer(streamBuffers[i])) + { + throw new Exception("Generated streamBuffer[" + i.ToString() + "] is invalid! " + debugName); + } + } + Sound.Owner.InitUpdateChannelThread(); + SetProperties(); + } } } - else + finally { - uint alBuffer = 0; - Al.Sourcei(sound.Owner.GetSourceFromIndex(Sound.SourcePoolIndex, ALSourceIndex), Al.Buffer, (int)alBuffer); - int alError = Al.GetError(); - if (alError != Al.NoError) - { - throw new Exception("Failed to reset source buffer: " + debugName + ", " + Al.GetErrorString(alError)); - } - - Al.Sourcei(sound.Owner.GetSourceFromIndex(Sound.SourcePoolIndex, ALSourceIndex), Al.Looping, Al.False); - alError = Al.GetError(); - if (alError != Al.NoError) - { - throw new Exception("Failed to set stream looping state: " + debugName + ", " + Al.GetErrorString(alError)); - } - - streamShortBuffer = new short[STREAM_BUFFER_SIZE]; - - streamBuffers = new uint[4]; - unqueuedBuffers = new uint[4]; - streamBufferAmplitudes = new float[4]; - for (int i = 0; i < 4; i++) - { - Al.GenBuffer(out streamBuffers[i]); - - alError = Al.GetError(); - if (alError != Al.NoError) - { - throw new Exception("Failed to generate stream buffers: " + debugName + ", " + Al.GetErrorString(alError)); - } - - if (!Al.IsBuffer(streamBuffers[i])) - { - throw new Exception("Generated streamBuffer[" + i.ToString() + "] is invalid! " + debugName); - } - } - Sound.Owner.InitUpdateChannelThread(); - SetProperties(); + if (mutex != null) { Monitor.Exit(mutex); } } } #if !DEBUG @@ -591,12 +607,6 @@ namespace Barotrauma.Sounds { throw; } - finally - { -#endif - if (mutex != null) { Monitor.Exit(mutex); } -#if !DEBUG - } #endif void SetProperties() diff --git a/Barotrauma/BarotraumaClient/ClientSource/Sounds/SoundManager.cs b/Barotrauma/BarotraumaClient/ClientSource/Sounds/SoundManager.cs index 0826aa2d3..e320bf3d9 100644 --- a/Barotrauma/BarotraumaClient/ClientSource/Sounds/SoundManager.cs +++ b/Barotrauma/BarotraumaClient/ClientSource/Sounds/SoundManager.cs @@ -417,6 +417,15 @@ namespace Barotrauma.Sounds return sourcePools[(int)poolIndex].ALSources[srcInd]; } + /// + /// Gets the lock object for the playing channels array for a specific pool. + /// Used to protect OpenAL operations that need to be atomic with channel assignment. + /// + public object GetPlayingChannelsLock(SourcePoolIndex poolIndex) + { + return playingChannels[(int)poolIndex]; + } + public int AssignFreeSourceToChannel(SoundChannel newChannel) { if (Disabled) { return -1; } @@ -427,14 +436,25 @@ namespace Barotrauma.Sounds lock (playingChannels[poolIndex]) { - for (int i = 0; i < playingChannels[poolIndex].Length; i++) + return AssignFreeSourceToChannelUnsafe(newChannel, poolIndex); + } + } + + /// + /// Assigns a free source to a channel without locking. + /// Caller MUST hold the playingChannels[poolIndex] lock before calling this method. + /// + public int AssignFreeSourceToChannelUnsafe(SoundChannel newChannel, int poolIndex) + { + if (Disabled) { return -1; } + + for (int i = 0; i < playingChannels[poolIndex].Length; i++) + { + if (playingChannels[poolIndex][i] == null || !playingChannels[poolIndex][i].IsPlaying) { - if (playingChannels[poolIndex][i] == null || !playingChannels[poolIndex][i].IsPlaying) - { - if (playingChannels[poolIndex][i] != null) { playingChannels[poolIndex][i].Dispose(); } - playingChannels[poolIndex][i] = newChannel; - return i; - } + if (playingChannels[poolIndex][i] != null) { playingChannels[poolIndex][i].Dispose(); } + playingChannels[poolIndex][i] = newChannel; + return i; } } @@ -476,13 +496,25 @@ namespace Barotrauma.Sounds int count = 0; lock (playingChannels[(int)sound.SourcePoolIndex]) { - for (int i = 0; i < playingChannels[(int)sound.SourcePoolIndex].Length; i++) + count = CountPlayingInstancesUnsafe(sound, (int)sound.SourcePoolIndex); + } + return count; + } + + /// + /// Counts playing instances without locking. + /// Caller MUST hold the playingChannels[poolIndex] lock before calling this method. + /// + public int CountPlayingInstancesUnsafe(Sound sound, int poolIndex) + { + if (Disabled) { return 0; } + int count = 0; + for (int i = 0; i < playingChannels[poolIndex].Length; i++) + { + if (playingChannels[poolIndex][i] != null && + playingChannels[poolIndex][i].Sound.Filename == sound.Filename) { - if (playingChannels[(int)sound.SourcePoolIndex][i] != null && - playingChannels[(int)sound.SourcePoolIndex][i].Sound.Filename == sound.Filename) - { - if (playingChannels[(int)sound.SourcePoolIndex][i].IsPlaying) { count++; }; - } + if (playingChannels[poolIndex][i].IsPlaying) { count++; }; } } return count; diff --git a/Libraries/Farseer Physics Engine 3.5/Collision/DynamicTree.cs b/Libraries/Farseer Physics Engine 3.5/Collision/DynamicTree.cs index fac9982bd..6bc690f09 100644 --- a/Libraries/Farseer Physics Engine 3.5/Collision/DynamicTree.cs +++ b/Libraries/Farseer Physics Engine 3.5/Collision/DynamicTree.cs @@ -30,6 +30,7 @@ using System; using System.Collections.Generic; using System.Diagnostics; +using System.Threading; using FarseerPhysics.Common; using FarseerPhysics.Dynamics; using Microsoft.Xna.Framework; @@ -74,8 +75,15 @@ namespace FarseerPhysics.Collision /// public class DynamicTree { - private Stack _raycastStack = new Stack(256); - private Stack _queryStack = new Stack(256); + // Thread-local stacks to ensure thread safety during parallel queries/raycasts + [ThreadStatic] + private static Stack _raycastStack; + [ThreadStatic] + private static Stack _queryStack; + + private static Stack RaycastStack => _raycastStack ??= new Stack(256); + private static Stack QueryStack => _queryStack ??= new Stack(256); + private int _freeList; private int _nodeCapacity; private int _nodeCount; @@ -346,12 +354,12 @@ namespace FarseerPhysics.Collision /// The aabb. public void Query(Func callback, ref AABB aabb, ref Body body) { - _queryStack.Clear(); - _queryStack.Push(_root); + QueryStack.Clear(); + QueryStack.Push(_root); - while (_queryStack.Count > 0) + while (QueryStack.Count > 0) { - int nodeId = _queryStack.Pop(); + int nodeId = QueryStack.Pop(); if (nodeId == NullNode) { continue; @@ -386,8 +394,8 @@ namespace FarseerPhysics.Collision } else { - _queryStack.Push(node.Child1); - _queryStack.Push(node.Child2); + QueryStack.Push(node.Child1); + QueryStack.Push(node.Child2); } } } @@ -395,12 +403,12 @@ namespace FarseerPhysics.Collision public void Query(Func callback, ref AABB aabb) { - _queryStack.Clear(); - _queryStack.Push(_root); + QueryStack.Clear(); + QueryStack.Push(_root); - while (_queryStack.Count > 0) + while (QueryStack.Count > 0) { - int nodeId = _queryStack.Pop(); + int nodeId = QueryStack.Pop(); if (nodeId == NullNode) { continue; @@ -419,8 +427,8 @@ namespace FarseerPhysics.Collision } else { - _queryStack.Push(_nodes[nodeId].Child1); - _queryStack.Push(_nodes[nodeId].Child2); + QueryStack.Push(_nodes[nodeId].Child1); + QueryStack.Push(_nodes[nodeId].Child2); } } } @@ -460,12 +468,12 @@ namespace FarseerPhysics.Collision Vector2.Max(ref p1, ref t, out segmentAABB.UpperBound); } - _raycastStack.Clear(); - _raycastStack.Push(_root); + RaycastStack.Clear(); + RaycastStack.Push(_root); - while (_raycastStack.Count > 0) + while (RaycastStack.Count > 0) { - int nodeId = _raycastStack.Pop(); + int nodeId = RaycastStack.Pop(); if (nodeId == NullNode) { continue; @@ -522,8 +530,8 @@ namespace FarseerPhysics.Collision } else { - _raycastStack.Push(_nodes[nodeId].Child1); - _raycastStack.Push(_nodes[nodeId].Child2); + RaycastStack.Push(_nodes[nodeId].Child1); + RaycastStack.Push(_nodes[nodeId].Child2); } } } From 1db14631dfa1a22a8954dcbd6cf2f0ad653ae60f Mon Sep 17 00:00:00 2001 From: Eero Date: Sun, 28 Dec 2025 17:14:16 +0800 Subject: [PATCH 12/14] Defer physics transforms to ensure thread safety Refactored multiple components to defer Farseer physics transform operations using PhysicsBodyQueue, preventing unsafe calls from parallel contexts. This change addresses thread safety issues with Farseer's DynamicTree and ensures transforms are executed in a safe context. Also increased the spawn amount limit in DebugConsole from 100 to 100000. --- .../SharedSource/DebugConsole.cs | 2 +- .../SharedSource/Items/Components/Door.cs | 30 ++++++++++---- .../Components/Holdable/LevelResource.cs | 12 ++++-- .../Items/Components/ItemContainer.cs | 6 ++- .../Items/Components/TriggerComponent.cs | 9 ++++- .../SharedSource/Items/Item.cs | 39 +++++++++++++++---- 6 files changed, 76 insertions(+), 22 deletions(-) diff --git a/Barotrauma/BarotraumaShared/SharedSource/DebugConsole.cs b/Barotrauma/BarotraumaShared/SharedSource/DebugConsole.cs index 5cdfefe6a..fb2cacc66 100644 --- a/Barotrauma/BarotraumaShared/SharedSource/DebugConsole.cs +++ b/Barotrauma/BarotraumaShared/SharedSource/DebugConsole.cs @@ -3225,7 +3225,7 @@ namespace Barotrauma if (args.Length > spawnLocationIndex + 1) { if (!int.TryParse(args[spawnLocationIndex + 1], NumberStyles.Any, CultureInfo.InvariantCulture, out amount)) { amount = 1; } - amount = Math.Min(amount, 100); + amount = Math.Min(amount, 100000); } if (args.Length > spawnLocationIndex + 2) diff --git a/Barotrauma/BarotraumaShared/SharedSource/Items/Components/Door.cs b/Barotrauma/BarotraumaShared/SharedSource/Items/Components/Door.cs index fad7c7fab..b3de4b02b 100644 --- a/Barotrauma/BarotraumaShared/SharedSource/Items/Components/Door.cs +++ b/Barotrauma/BarotraumaShared/SharedSource/Items/Components/Door.cs @@ -314,13 +314,21 @@ namespace Barotrauma.Items.Components public override void Move(Vector2 amount, bool ignoreContacts = false) { - if (ignoreContacts) + // Defer physics operation if in parallel context (Farseer is not thread-safe) + if (Body != null) { - Body?.SetTransformIgnoreContacts(Body.SimPosition + ConvertUnits.ToSimUnits(amount), 0.0f); - } - else - { - Body?.SetTransform(Body.SimPosition + ConvertUnits.ToSimUnits(amount), 0.0f); + var capturedBody = Body; + var capturedNewPos = Body.SimPosition + ConvertUnits.ToSimUnits(amount); + if (ignoreContacts) + { + PhysicsBodyQueue.ExecuteOrDefer(() => + capturedBody.SetTransformIgnoreContacts(capturedNewPos, 0.0f)); + } + else + { + PhysicsBodyQueue.ExecuteOrDefer(() => + capturedBody.SetTransform(capturedNewPos, 0.0f)); + } } #if CLIENT UpdateConvexHulls(); @@ -786,13 +794,19 @@ namespace Barotrauma.Items.Components //immediately teleport it to the correct side if (Math.Sign(diff) != dir) { + // Defer physics operation if in parallel context (Farseer is not thread-safe) + var capturedBody = body; if (IsHorizontal) { - body.SetTransformIgnoreContacts(new Vector2(body.SimPosition.X, item.SimPosition.Y + dir * doorRectSimSize.Y * 2.0f), body.Rotation); + Vector2 newPos = new Vector2(body.SimPosition.X, item.SimPosition.Y + dir * doorRectSimSize.Y * 2.0f); + float rotation = body.Rotation; + PhysicsBodyQueue.ExecuteOrDefer(() => capturedBody.SetTransformIgnoreContacts(newPos, rotation)); } else { - body.SetTransformIgnoreContacts(new Vector2(item.SimPosition.X + dir * doorRectSimSize.X * 1.2f, body.SimPosition.Y), body.Rotation); + Vector2 newPos = new Vector2(item.SimPosition.X + dir * doorRectSimSize.X * 1.2f, body.SimPosition.Y); + float rotation = body.Rotation; + PhysicsBodyQueue.ExecuteOrDefer(() => capturedBody.SetTransformIgnoreContacts(newPos, rotation)); } } diff --git a/Barotrauma/BarotraumaShared/SharedSource/Items/Components/Holdable/LevelResource.cs b/Barotrauma/BarotraumaShared/SharedSource/Items/Components/Holdable/LevelResource.cs index 797ec14e7..5c672cda5 100644 --- a/Barotrauma/BarotraumaShared/SharedSource/Items/Components/Holdable/LevelResource.cs +++ b/Barotrauma/BarotraumaShared/SharedSource/Items/Components/Holdable/LevelResource.cs @@ -92,13 +92,16 @@ namespace Barotrauma.Items.Components { if (trigger != null && amount.LengthSquared() > 0.00001f) { + // Defer physics operation if in parallel context (Farseer is not thread-safe) + var capturedTrigger = trigger; + var capturedPos = item.SimPosition; if (ignoreContacts) { - trigger.SetTransformIgnoreContacts(item.SimPosition, 0.0f); + PhysicsBodyQueue.ExecuteOrDefer(() => capturedTrigger.SetTransformIgnoreContacts(capturedPos, 0.0f)); } else { - trigger.SetTransform(item.SimPosition, 0.0f); + PhysicsBodyQueue.ExecuteOrDefer(() => capturedTrigger.SetTransform(capturedPos, 0.0f)); } } } @@ -134,7 +137,10 @@ namespace Barotrauma.Items.Components } if (trigger != null && Vector2.DistanceSquared(item.SimPosition, trigger.SimPosition) > 0.01f) { - trigger.SetTransform(item.SimPosition, 0.0f); + // Defer physics operation if in parallel context (Farseer is not thread-safe) + var capturedTrigger = trigger; + var capturedPos = item.SimPosition; + PhysicsBodyQueue.ExecuteOrDefer(() => capturedTrigger.SetTransform(capturedPos, 0.0f)); } IsActive = false; } diff --git a/Barotrauma/BarotraumaShared/SharedSource/Items/Components/ItemContainer.cs b/Barotrauma/BarotraumaShared/SharedSource/Items/Components/ItemContainer.cs index 722b63588..992b4bfc6 100644 --- a/Barotrauma/BarotraumaShared/SharedSource/Items/Components/ItemContainer.cs +++ b/Barotrauma/BarotraumaShared/SharedSource/Items/Components/ItemContainer.cs @@ -1011,7 +1011,11 @@ namespace Barotrauma.Items.Components if (flippedX ^ flippedY) { rotation = -rotation; } rotation += -item.RotationRad; } - contained.Item.body.FarseerBody.SetTransformIgnoreContacts(ref simPos, rotation); + // Defer physics operation if in parallel context (Farseer is not thread-safe) + var capturedBody = contained.Item.body.FarseerBody; + var capturedSimPos = simPos; + var capturedRotation = rotation; + PhysicsBodyQueue.ExecuteOrDefer(() => capturedBody.SetTransformIgnoreContacts(ref capturedSimPos, capturedRotation)); contained.Item.body.UpdateDrawPosition(interpolate: false); } catch (Exception e) diff --git a/Barotrauma/BarotraumaShared/SharedSource/Items/Components/TriggerComponent.cs b/Barotrauma/BarotraumaShared/SharedSource/Items/Components/TriggerComponent.cs index c839d488c..228df8dc9 100644 --- a/Barotrauma/BarotraumaShared/SharedSource/Items/Components/TriggerComponent.cs +++ b/Barotrauma/BarotraumaShared/SharedSource/Items/Components/TriggerComponent.cs @@ -312,13 +312,18 @@ namespace Barotrauma.Items.Components Matrix transform = Matrix.CreateRotationZ(-item.RotationRad); offset = Vector2.Transform(offset, transform); } + + // Defer physics operations if in parallel context (Farseer is not thread-safe) + var capturedBody = PhysicsBody; + var capturedPos = item.SimPosition + offset; + var capturedRot = -item.RotationRad; if (ignoreContacts) { - PhysicsBody.SetTransformIgnoreContacts(item.SimPosition + offset, -item.RotationRad); + PhysicsBodyQueue.ExecuteOrDefer(() => capturedBody.SetTransformIgnoreContacts(capturedPos, capturedRot)); } else { - PhysicsBody.SetTransform(item.SimPosition + offset, -item.RotationRad); + PhysicsBodyQueue.ExecuteOrDefer(() => capturedBody.SetTransform(capturedPos, capturedRot)); } PhysicsBody.UpdateDrawPosition(); } diff --git a/Barotrauma/BarotraumaShared/SharedSource/Items/Item.cs b/Barotrauma/BarotraumaShared/SharedSource/Items/Item.cs index 2ce5e3d2d..8ad7bfea1 100644 --- a/Barotrauma/BarotraumaShared/SharedSource/Items/Item.cs +++ b/Barotrauma/BarotraumaShared/SharedSource/Items/Item.cs @@ -1822,7 +1822,13 @@ namespace Barotrauma try { #endif - body.SetTransformIgnoreContacts(simPosition, rotation, setPrevTransform); + // Defer physics operation if in parallel context (Farseer is not thread-safe) + var capturedBody = body; + var capturedSimPos = simPosition; + var capturedRotation = rotation; + var capturedSetPrevTransform = setPrevTransform; + PhysicsBodyQueue.ExecuteOrDefer(() => + capturedBody.SetTransformIgnoreContacts(capturedSimPos, capturedRotation, capturedSetPrevTransform)); #if DEBUG } catch (Exception e) @@ -1899,13 +1905,19 @@ namespace Barotrauma if (ItemList != null && body != null) { + // Defer physics operation if in parallel context (Farseer is not thread-safe) + var capturedBody = body; + var capturedNewPos = body.SimPosition + ConvertUnits.ToSimUnits(amount); + var capturedRotation = body.Rotation; if (ignoreContacts) { - body.SetTransformIgnoreContacts(body.SimPosition + ConvertUnits.ToSimUnits(amount), body.Rotation); + PhysicsBodyQueue.ExecuteOrDefer(() => + capturedBody.SetTransformIgnoreContacts(capturedNewPos, capturedRotation)); } else { - body.SetTransform(body.SimPosition + ConvertUnits.ToSimUnits(amount), body.Rotation); + PhysicsBodyQueue.ExecuteOrDefer(() => + capturedBody.SetTransform(capturedNewPos, capturedRotation)); } } foreach (ItemComponent ic in components) @@ -2563,7 +2575,12 @@ namespace Barotrauma if (item != this) { item.body.Enabled = false; - item.body.SetTransformIgnoreContacts(this.SimPosition, body.Rotation); + // Defer physics operation if in parallel context (Farseer is not thread-safe) + var capturedItemBody = item.body; + var capturedSimPos = this.SimPosition; + var capturedRotation = body.Rotation; + PhysicsBodyQueue.ExecuteOrDefer(() => + capturedItemBody.SetTransformIgnoreContacts(capturedSimPos, capturedRotation)); } } } @@ -2755,17 +2772,25 @@ namespace Barotrauma FindHull(); } + // Defer physics transform operations if in parallel context. + // Farseer's DynamicTree is not thread-safe. if (Submarine == null && prevSub != null) { - body.SetTransformIgnoreContacts(body.SimPosition + prevSub.SimPosition, body.Rotation); + Vector2 newPos = body.SimPosition + prevSub.SimPosition; + float rotation = body.Rotation; + PhysicsBodyQueue.ExecuteOrDefer(() => body.SetTransformIgnoreContacts(newPos, rotation)); } else if (Submarine != null && prevSub == null) { - body.SetTransformIgnoreContacts(body.SimPosition - Submarine.SimPosition, body.Rotation); + Vector2 newPos = body.SimPosition - Submarine.SimPosition; + float rotation = body.Rotation; + PhysicsBodyQueue.ExecuteOrDefer(() => body.SetTransformIgnoreContacts(newPos, rotation)); } else if (Submarine != null && prevSub != null && Submarine != prevSub) { - body.SetTransformIgnoreContacts(body.SimPosition + prevSub.SimPosition - Submarine.SimPosition, body.Rotation); + Vector2 newPos = body.SimPosition + prevSub.SimPosition - Submarine.SimPosition; + float rotation = body.Rotation; + PhysicsBodyQueue.ExecuteOrDefer(() => body.SetTransformIgnoreContacts(newPos, rotation)); } if (Submarine != prevSub) From bd1e624eb1fbcc7e157d18005f49893cda1a0d52 Mon Sep 17 00:00:00 2001 From: Eero Date: Sun, 28 Dec 2025 17:45:51 +0800 Subject: [PATCH 13/14] Remove unnecessary thread-safety code from entity spawning Eliminated redundant locks and related comments in EntitySpawner and Entity classes, simplifying the spawn and remove queue handling. Also removed outdated comments in GameScreen regarding thread safety. These changes assume entity spawning and removal are no longer performed from multiple threads, improving code clarity and maintainability. --- .../SharedSource/Map/Entity.cs | 1 - .../SharedSource/Networking/EntitySpawner.cs | 95 +++++-------------- .../SharedSource/Screens/GameScreen.cs | 2 - 3 files changed, 26 insertions(+), 72 deletions(-) diff --git a/Barotrauma/BarotraumaShared/SharedSource/Map/Entity.cs b/Barotrauma/BarotraumaShared/SharedSource/Map/Entity.cs index d9665886e..54b5014d0 100644 --- a/Barotrauma/BarotraumaShared/SharedSource/Map/Entity.cs +++ b/Barotrauma/BarotraumaShared/SharedSource/Map/Entity.cs @@ -147,7 +147,6 @@ namespace Barotrauma CreationStackTrace += $"{fileName}@{fileLineNumber}; "; } #endif - #warning TODO: consider removing this mutex, entity creation probably shouldn't be multithreaded lock (creationCounterMutex) { CreationIndex = creationCounter; diff --git a/Barotrauma/BarotraumaShared/SharedSource/Networking/EntitySpawner.cs b/Barotrauma/BarotraumaShared/SharedSource/Networking/EntitySpawner.cs index 625bc734c..d95b5bced 100644 --- a/Barotrauma/BarotraumaShared/SharedSource/Networking/EntitySpawner.cs +++ b/Barotrauma/BarotraumaShared/SharedSource/Networking/EntitySpawner.cs @@ -5,7 +5,6 @@ using Microsoft.Xna.Framework; using System; using System.Collections.Generic; using System.Linq; -using System.Threading; namespace Barotrauma { @@ -206,7 +205,6 @@ namespace Barotrauma } private readonly Queue> spawnOrRemoveQueue; - private readonly object spawnOrRemoveQueueLock = new object(); public abstract class SpawnOrRemove : NetEntityEvent.IData { @@ -284,10 +282,7 @@ namespace Barotrauma GameAnalyticsManager.AddErrorEventOnce("EntitySpawner.AddToSpawnQueue1:ItemPrefabNull", GameAnalyticsManager.ErrorSeverity.Error, errorMsg); return; } - lock (spawnOrRemoveQueueLock) - { - spawnOrRemoveQueue.Enqueue(new ItemSpawnInfo(itemPrefab, worldPosition, onSpawned, condition, quality)); - } + spawnOrRemoveQueue.Enqueue(new ItemSpawnInfo(itemPrefab, worldPosition, onSpawned, condition, quality)); } public void AddItemToSpawnQueue(ItemPrefab itemPrefab, Vector2 position, Submarine sub, float? condition = null, int? quality = null, Action onSpawned = null) @@ -300,10 +295,7 @@ namespace Barotrauma GameAnalyticsManager.AddErrorEventOnce("EntitySpawner.AddToSpawnQueue2:ItemPrefabNull", GameAnalyticsManager.ErrorSeverity.Error, errorMsg); return; } - lock (spawnOrRemoveQueueLock) - { - spawnOrRemoveQueue.Enqueue(new ItemSpawnInfo(itemPrefab, position, sub, onSpawned, condition, quality)); - } + spawnOrRemoveQueue.Enqueue(new ItemSpawnInfo(itemPrefab, position, sub, onSpawned, condition, quality)); } public void AddItemToSpawnQueue(ItemPrefab itemPrefab, Inventory inventory, float? condition = null, int? quality = null, Action onSpawned = null, bool spawnIfInventoryFull = true, bool ignoreLimbSlots = false, InvSlotType slot = InvSlotType.None) @@ -316,15 +308,12 @@ namespace Barotrauma GameAnalyticsManager.AddErrorEventOnce("EntitySpawner.AddToSpawnQueue3:ItemPrefabNull", GameAnalyticsManager.ErrorSeverity.Error, errorMsg); return; } - lock (spawnOrRemoveQueueLock) - { - spawnOrRemoveQueue.Enqueue(new ItemSpawnInfo(itemPrefab, inventory, onSpawned, condition, quality) - { - SpawnIfInventoryFull = spawnIfInventoryFull, - IgnoreLimbSlots = ignoreLimbSlots, - Slot = slot - }); - } + spawnOrRemoveQueue.Enqueue(new ItemSpawnInfo(itemPrefab, inventory, onSpawned, condition, quality) + { + SpawnIfInventoryFull = spawnIfInventoryFull, + IgnoreLimbSlots = ignoreLimbSlots, + Slot = slot + }); } public void AddCharacterToSpawnQueue(Identifier speciesName, Vector2 worldPosition, Action onSpawn = null) @@ -337,10 +326,7 @@ namespace Barotrauma GameAnalyticsManager.AddErrorEventOnce("EntitySpawner.AddToSpawnQueue4:SpeciesNameNullOrEmpty", GameAnalyticsManager.ErrorSeverity.Error, errorMsg); return; } - lock (spawnOrRemoveQueueLock) - { - spawnOrRemoveQueue.Enqueue(new CharacterSpawnInfo(speciesName, worldPosition, onSpawn)); - } + spawnOrRemoveQueue.Enqueue(new CharacterSpawnInfo(speciesName, worldPosition, onSpawn)); } public void AddCharacterToSpawnQueue(Identifier speciesName, Vector2 position, Submarine sub, Action onSpawn = null) @@ -353,10 +339,7 @@ namespace Barotrauma GameAnalyticsManager.AddErrorEventOnce("EntitySpawner.AddToSpawnQueue5:SpeciesNameNullOrEmpty", GameAnalyticsManager.ErrorSeverity.Error, errorMsg); return; } - lock (spawnOrRemoveQueueLock) - { - spawnOrRemoveQueue.Enqueue(new CharacterSpawnInfo(speciesName, position, sub, onSpawn)); - } + spawnOrRemoveQueue.Enqueue(new CharacterSpawnInfo(speciesName, position, sub, onSpawn)); } public void AddCharacterToSpawnQueue(Identifier speciesName, Vector2 worldPosition, CharacterInfo characterInfo, Action onSpawn = null) @@ -369,10 +352,7 @@ namespace Barotrauma GameAnalyticsManager.AddErrorEventOnce("EntitySpawner.AddToSpawnQueue4:SpeciesNameNullOrEmpty", GameAnalyticsManager.ErrorSeverity.Error, errorMsg); return; } - lock (spawnOrRemoveQueueLock) - { - spawnOrRemoveQueue.Enqueue(new CharacterSpawnInfo(speciesName, worldPosition, characterInfo, onSpawn)); - } + spawnOrRemoveQueue.Enqueue(new CharacterSpawnInfo(speciesName, worldPosition, characterInfo, onSpawn)); } public void AddEntityToRemoveQueue(Entity entity) @@ -395,10 +375,7 @@ namespace Barotrauma #endif } - lock (spawnOrRemoveQueueLock) - { - spawnOrRemoveQueue.Enqueue(entity); - } + spawnOrRemoveQueue.Enqueue(entity); } public void AddItemToRemoveQueue(Item item) @@ -406,10 +383,7 @@ namespace Barotrauma if (GameMain.NetworkMember != null && GameMain.NetworkMember.IsClient) { return; } if (IsInRemoveQueue(item) || item.Removed) { return; } - lock (spawnOrRemoveQueueLock) - { - spawnOrRemoveQueue.Enqueue(item); - } + spawnOrRemoveQueue.Enqueue(item); item.IsInRemoveQueue = true; foreach (var containedItem in item.ContainedItems) @@ -426,14 +400,11 @@ namespace Barotrauma /// public bool IsInSpawnQueue(Predicate predicate) { - lock (spawnOrRemoveQueueLock) + foreach (var spawnOrRemove in spawnOrRemoveQueue) { - foreach (var spawnOrRemove in spawnOrRemoveQueue) - { - if (spawnOrRemove.TryGet(out IEntitySpawnInfo spawnInfo) && predicate(spawnInfo)) { return true; } - } - return false; + if (spawnOrRemove.TryGet(out IEntitySpawnInfo spawnInfo) && predicate(spawnInfo)) { return true; } } + return false; } /// @@ -441,40 +412,29 @@ namespace Barotrauma /// public int CountSpawnQueue(Predicate predicate) { - lock (spawnOrRemoveQueueLock) + int count = 0; + foreach (var spawnOrRemove in spawnOrRemoveQueue) { - int count = 0; - foreach (var spawnOrRemove in spawnOrRemoveQueue) - { - if (spawnOrRemove.TryGet(out IEntitySpawnInfo spawnInfo) && predicate(spawnInfo)) { count++; } - } - return count; + if (spawnOrRemove.TryGet(out IEntitySpawnInfo spawnInfo) && predicate(spawnInfo)) { count++; } } + return count; } public bool IsInRemoveQueue(Entity entity) { - lock (spawnOrRemoveQueueLock) + foreach (var spawnOrRemove in spawnOrRemoveQueue) { - foreach (var spawnOrRemove in spawnOrRemoveQueue) - { - if (spawnOrRemove.TryGet(out Entity entityToRemove) && entityToRemove == entity) { return true; } - } - return false; + if (spawnOrRemove.TryGet(out Entity entityToRemove) && entityToRemove == entity) { return true; } } + return false; } public void Update(bool createNetworkEvents = true) { if (GameMain.NetworkMember is { IsClient: true }) { return; } - while (true) + while (spawnOrRemoveQueue.Count > 0) { - Either spawnOrRemove; - lock (spawnOrRemoveQueueLock) - { - if (spawnOrRemoveQueue.Count == 0) { break; } - spawnOrRemove = spawnOrRemoveQueue.Dequeue(); - } + if (!spawnOrRemoveQueue.TryDequeue(out var spawnOrRemove)) { break; } if (spawnOrRemove.TryGet(out Entity entityToRemove)) { if (entityToRemove is Item item) @@ -505,10 +465,7 @@ namespace Barotrauma public void Reset() { - lock (spawnOrRemoveQueueLock) - { - spawnOrRemoveQueue.Clear(); - } + spawnOrRemoveQueue.Clear(); #if CLIENT receivedEvents.Clear(); #endif diff --git a/Barotrauma/BarotraumaShared/SharedSource/Screens/GameScreen.cs b/Barotrauma/BarotraumaShared/SharedSource/Screens/GameScreen.cs index 577324da2..e2fddef7c 100644 --- a/Barotrauma/BarotraumaShared/SharedSource/Screens/GameScreen.cs +++ b/Barotrauma/BarotraumaShared/SharedSource/Screens/GameScreen.cs @@ -286,7 +286,6 @@ namespace Barotrauma } ); - // Process any physics operations queued during parallel updates PhysicsBodyQueue.ProcessPendingOperations(); #endif @@ -311,7 +310,6 @@ namespace Barotrauma MapEntity.UpdateAll((float)deltaTime, Camera.Instance, parallelOptions); - //StatusEffect.UpdateAll is not thread-safe and must be executed on the main thread StatusEffect.UpdateAll((float)deltaTime); #endif From e167a34f327e7bcdcd29da2fd5596f5b1dc0671b Mon Sep 17 00:00:00 2001 From: Eero Date: Sun, 28 Dec 2025 21:59:03 +0800 Subject: [PATCH 14/14] 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; } }