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.
This commit is contained in:
Eero
2025-12-28 03:57:04 +08:00
parent edd50ef181
commit 90962b2328
22 changed files with 232 additions and 92 deletions
@@ -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<Entity> hitTargets = new HashSet<Entity>();
private readonly Queue<Fixture> impactQueue = new Queue<Fixture>();
private readonly ConcurrentQueue<Fixture> impactQueue = new ConcurrentQueue<Fixture>();
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;
@@ -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<Impact> impactQueue = new Queue<Impact>();
private readonly ConcurrentQueue<Impact> impactQueue = new ConcurrentQueue<Impact>();
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);
}
@@ -723,11 +723,11 @@ namespace Barotrauma.Items.Components
if (item0 == null && item1 != null)
{
item0 = Item.ItemList.Find(it => it.GetComponent<ConnectionPanel>()?.DisconnectedWires.Contains(this) ?? false);
item0 = Item.ItemList.FirstOrDefault(it => it.GetComponent<ConnectionPanel>()?.DisconnectedWires.Contains(this) ?? false);
}
else if (item0 != null && item1 == null)
{
item1 = Item.ItemList.Find(it => it.GetComponent<ConnectionPanel>()?.DisconnectedWires.Contains(this) ?? false);
item1 = Item.ItemList.FirstOrDefault(it => it.GetComponent<ConnectionPanel>()?.DisconnectedWires.Contains(this) ?? false);
}
if (item0 == null || item1 == null || nodes.Count == 0) { return; }
@@ -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
/// <summary>
/// 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.
/// </summary>
public static readonly List<Item> ItemList = new List<Item>();
private static readonly ConcurrentDictionary<ushort, Item> _itemDictionary = new ConcurrentDictionary<ushort, Item>();
private static readonly HashSet<Item> _dangerousItems = new HashSet<Item>();
/// <summary>
/// Provides thread-safe enumeration over all items.
/// </summary>
public static ICollection<Item> ItemList => _itemDictionary.Values;
/// <summary>
/// Thread-safe item lookup by ID.
/// </summary>
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<Item> _dangerousItems = ImmutableHashSet<Item>.Empty;
private static volatile ImmutableHashSet<Item> _repairableItems = ImmutableHashSet<Item>.Empty;
private static volatile ImmutableHashSet<Item> _cleanableItems = ImmutableHashSet<Item>.Empty;
private static volatile ImmutableHashSet<Item> _sonarVisibleItems = ImmutableHashSet<Item>.Empty;
private static volatile ImmutableHashSet<Item> _turretTargetItems = ImmutableHashSet<Item>.Empty;
private static volatile ImmutableHashSet<Item> _chairItems = ImmutableHashSet<Item>.Empty;
// DeconstructItems uses ConcurrentDictionary to simulate a thread-safe HashSet
private static readonly ConcurrentDictionary<Item, byte> _deconstructItems = new ConcurrentDictionary<Item, byte>();
public static IReadOnlyCollection<Item> DangerousItems => _dangerousItems;
private static readonly List<Item> _repairableItems = new List<Item>();
/// <summary>
/// Items that have one more more Repairable component
/// </summary>
public static IReadOnlyCollection<Item> RepairableItems => _repairableItems;
private static readonly List<Item> _cleanableItems = new List<Item>();
/// <summary>
/// Items that may potentially need to be cleaned up (pickable, not attached to a wall, and not inside a valid container)
/// </summary>
public static IReadOnlyCollection<Item> CleanableItems => _cleanableItems;
private static readonly HashSet<Item> _deconstructItems = new HashSet<Item>();
/// <summary>
/// Items that have been marked for deconstruction
/// Items that have been marked for deconstruction. Thread-safe collection.
/// </summary>
public static HashSet<Item> DeconstructItems => _deconstructItems;
private static readonly List<Item> _sonarVisibleItems = new List<Item>();
public static ICollection<Item> DeconstructItems => _deconstructItems.Keys;
/// <summary>
/// Items whose <see cref="ItemPrefab.SonarSize"/> is larger than 0
/// </summary>
public static IReadOnlyCollection<Item> SonarVisibleItems => _sonarVisibleItems;
private static readonly List<Item> _turretTargetItems = new List<Item>();
/// <summary>
/// Items whose <see cref="ItemPrefab.IsAITurretTarget"/> is true.
/// </summary>
public static IReadOnlyCollection<Item> TurretTargetItems => _turretTargetItems;
private static readonly List<Item> _chairItems = new List<Item>();
/// <summary>
/// Items that have the tag <see cref="Tags.ChairItem"/>. Which is an oddly specific thing, but useful as an optimization for NPC AI.
/// </summary>
public static IReadOnlyCollection<Item> ChairItems => _chairItems;
#region Thread-safe collection helpers
/// <summary>
/// Atomically adds an item to an immutable set using compare-and-swap.
/// </summary>
private static void AddToImmutableSet(ref ImmutableHashSet<Item> location, Item item)
{
ImmutableHashSet<Item> original, updated;
do
{
original = location;
updated = original.Add(item);
if (ReferenceEquals(original, updated)) return; // Already exists
}
while (Interlocked.CompareExchange(ref location, updated, original) != original);
}
/// <summary>
/// Atomically removes an item from an immutable set using compare-and-swap.
/// </summary>
private static void RemoveFromImmutableSet(ref ImmutableHashSet<Item> location, Item item)
{
ImmutableHashSet<Item> 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);
}
/// <summary>
/// Marks an item for deconstruction (thread-safe).
/// </summary>
public static void MarkForDeconstruction(Item item)
{
_deconstructItems.TryAdd(item, 0);
}
/// <summary>
/// Unmarks an item for deconstruction (thread-safe).
/// </summary>
public static void UnmarkForDeconstruction(Item item)
{
_deconstructItems.TryRemove(item, out _);
}
/// <summary>
/// Checks if an item is marked for deconstruction (thread-safe).
/// </summary>
public static bool IsMarkedForDeconstruction(Item item)
{
return _deconstructItems.ContainsKey(item);
}
/// <summary>
/// Clears all item collections (thread-safe). Used during unloading.
/// </summary>
public static void ClearAllItemCollections()
{
_itemDictionary.Clear();
_dangerousItems = ImmutableHashSet<Item>.Empty;
_repairableItems = ImmutableHashSet<Item>.Empty;
_cleanableItems = ImmutableHashSet<Item>.Empty;
_sonarVisibleItems = ImmutableHashSet<Item>.Empty;
_turretTargetItems = ImmutableHashSet<Item>.Empty;
_chairItems = ImmutableHashSet<Item>.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<Item> _cachedItemList;
private static volatile int _cachedItemListVersion = -1;
private static volatile int _itemListVersion;
/// <summary>
/// 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.
/// </summary>
public static List<Item> GetCachedItemList()
{
int currentVersion = _itemListVersion;
if (_cachedItemList == null || _cachedItemListVersion != currentVersion)
{
_cachedItemList = _itemDictionary.Values.ToList();
_cachedItemListVersion = currentVersion;
}
return _cachedItemList;
}
/// <summary>
/// Called when items are added or removed to invalidate the cached list.
/// </summary>
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<Item> itemsWithPendingConditionUpdates = new List<Item>();
private static readonly ConcurrentQueue<Item> _pendingConditionUpdates = new ConcurrentQueue<Item>();
/// <summary>
/// Flag to avoid duplicate enqueue for pending condition updates.
/// </summary>
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);
}