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.
This commit is contained in:
Eero
2025-12-28 13:10:17 +08:00
parent 31812d524d
commit c5fa49405f
8 changed files with 111 additions and 51 deletions
@@ -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<Either<IEntitySpawnInfo, Entity>> 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<Item> 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<Item> 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<Character> 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<Character> 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<Character> 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
/// </summary>
public bool IsInSpawnQueue(Predicate<IEntitySpawnInfo> 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;
}
/// <summary>
@@ -412,29 +441,40 @@ namespace Barotrauma
/// </summary>
public int CountSpawnQueue(Predicate<IEntitySpawnInfo> 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<IEntitySpawnInfo, Entity> 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