From 90962b232880c1f5978c49757baa5c0e20c71f39 Mon Sep 17 00:00:00 2001 From: Eero Date: Sun, 28 Dec 2025 03:57:04 +0800 Subject: [PATCH] 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();