From 7b8275100d6dc8ebd459781dfd140e4ea3535406 Mon Sep 17 00:00:00 2001 From: Eero Date: Mon, 29 Dec 2025 16:47:10 +0800 Subject: [PATCH 01/10] Improve thread safety and performance in core systems Refactors event, entity, and physics management to use thread-safe and lock-free data structures (Immutable collections, ConcurrentQueue, ConcurrentDictionary, Channel) for improved concurrency and performance. Replaces O(n) queue lookups with O(1) set/dictionary checks, ensures atomic updates for shared state, and optimizes queue draining and deferred action processing. Updates related code to use new APIs and patterns, and adds documentation for thread safety and workflow. --- .../ClientSource/DebugConsole.cs | 4 +- .../ClientSource/Events/EventManager.cs | 12 +- .../Items/Components/Machines/Sonar.cs | 2 +- .../ClientSource/Map/Submarine.cs | 10 +- .../ClientSource/Networking/EntitySpawner.cs | 28 ++- .../ClientSource/Networking/GameClient.cs | 2 +- .../ServerSource/Characters/Character.cs | 2 +- .../ServerSource/Events/EventManager.cs | 2 +- .../ServerSource/Items/Inventory.cs | 4 +- .../ServerSource/Networking/Client.cs | 27 +++ .../ServerSource/Networking/GameServer.cs | 15 +- .../Events/EventActions/UnlockPathAction.cs | 24 +- .../SharedSource/Events/EventManager.cs | 210 ++++++++++++------ .../SharedSource/Events/EventSet.cs | 24 +- .../SharedSource/Items/Item.cs | 15 +- .../SharedSource/Networking/EntitySpawner.cs | 67 ++++-- .../SharedSource/Physics/PhysicsBodyQueue.cs | 111 ++++----- 17 files changed, 368 insertions(+), 191 deletions(-) diff --git a/Barotrauma/BarotraumaClient/ClientSource/DebugConsole.cs b/Barotrauma/BarotraumaClient/ClientSource/DebugConsole.cs index 33f058b9f7..f2831f0eea 100644 --- a/Barotrauma/BarotraumaClient/ClientSource/DebugConsole.cs +++ b/Barotrauma/BarotraumaClient/ClientSource/DebugConsole.cs @@ -1397,12 +1397,12 @@ async Task gameOwnershipTokenTest() if (me.SimPosition.Length() > 2000.0f) { NewMessage("Removed " + me.Name + " (simposition " + me.SimPosition + ")", Color.Orange); - MapEntity.MapEntityList.RemoveAt(i); + MapEntity.MapEntityList.Remove(me); } else if (!me.ShouldBeSaved) { NewMessage("Removed " + me.Name + " (!ShouldBeSaved)", Color.Orange); - MapEntity.MapEntityList.RemoveAt(i); + MapEntity.MapEntityList.Remove(me); } else if (me is Item) { diff --git a/Barotrauma/BarotraumaClient/ClientSource/Events/EventManager.cs b/Barotrauma/BarotraumaClient/ClientSource/Events/EventManager.cs index e5540cfa8c..5c654bc8f2 100644 --- a/Barotrauma/BarotraumaClient/ClientSource/Events/EventManager.cs +++ b/Barotrauma/BarotraumaClient/ClientSource/Events/EventManager.cs @@ -28,7 +28,7 @@ partial class EventManager public void DebugDraw(SpriteBatch spriteBatch) { - foreach (Event ev in activeEvents) + foreach (Event ev in _activeEvents) { Vector2 drawPos = ev.DebugDrawPos; drawPos.Y = -drawPos.Y; @@ -41,7 +41,7 @@ public void DebugDraw(SpriteBatch spriteBatch) public void DebugDrawHUD(SpriteBatch spriteBatch, float y) { - foreach (ScriptedEvent scriptedEvent in activeEvents.Where(ev => !ev.IsFinished && ev is ScriptedEvent).Cast()) + foreach (ScriptedEvent scriptedEvent in _activeEvents.Where(ev => !ev.IsFinished && ev is ScriptedEvent).Cast()) { DrawEventTargetTags(spriteBatch, scriptedEvent); } @@ -156,7 +156,7 @@ void DrawTimeStamps(SpriteBatch sBatch, Color color, Vector2 pos, int order) { if (isGraphHovered || isGraphSelected) { - foreach (var timeStamp in timeStamps) + foreach (var timeStamp in _timeStamps) { int t = (int)Math.Abs(Math.Round((timeStamp.Time - lastIntensityUpdate) / intensityGraphUpdateInterval)); if (t == order) @@ -205,7 +205,7 @@ void DrawTimeStamps(SpriteBatch sBatch, Color color, Vector2 pos, int order) } adjustedYStep = GUI.AdjustForTextScale(12); - foreach (EventSet eventSet in pendingEventSets) + foreach (EventSet eventSet in _pendingEventSets) { if (Submarine.MainSub == null) { break; } @@ -263,7 +263,7 @@ void DrawTimeStamps(SpriteBatch sBatch, Color color, Vector2 pos, int order) y += yStep; adjustedYStep = GUI.AdjustForTextScale(18); - foreach (Event ev in activeEvents.Where(ev => !ev.IsFinished || PlayerInput.IsShiftDown())) + foreach (Event ev in _activeEvents.Where(ev => !ev.IsFinished || PlayerInput.IsShiftDown())) { GUI.DrawString(spriteBatch, new Vector2(x + 5, y), ev.ToString(), (!ev.IsFinished ? Color.White : Color.Red) * 0.8f, null, 0, GUIStyle.SmallFont); @@ -752,4 +752,4 @@ private static void ClientReadEventObjective(GameClient client, IReadMessage msg entry.CanBeCompleted); } } -} \ No newline at end of file +} diff --git a/Barotrauma/BarotraumaClient/ClientSource/Items/Components/Machines/Sonar.cs b/Barotrauma/BarotraumaClient/ClientSource/Items/Components/Machines/Sonar.cs index 872b9be0c4..b24371277d 100644 --- a/Barotrauma/BarotraumaClient/ClientSource/Items/Components/Machines/Sonar.cs +++ b/Barotrauma/BarotraumaClient/ClientSource/Items/Components/Machines/Sonar.cs @@ -1913,7 +1913,7 @@ private void DrawMarker(SpriteBatch spriteBatch, string label, Identifier iconId void CalculateDistance() { - pathFinder ??= new PathFinder(WayPoint.WayPointList, false); + pathFinder ??= new PathFinder(WayPoint.WayPointList.ToList(), false); var path = pathFinder.FindPath(ConvertUnits.ToSimUnits(transducerPosition), ConvertUnits.ToSimUnits(worldPosition)); if (!path.Unreachable) { diff --git a/Barotrauma/BarotraumaClient/ClientSource/Map/Submarine.cs b/Barotrauma/BarotraumaClient/ClientSource/Map/Submarine.cs index bbb921dcf4..24be7cfdb4 100644 --- a/Barotrauma/BarotraumaClient/ClientSource/Map/Submarine.cs +++ b/Barotrauma/BarotraumaClient/ClientSource/Map/Submarine.cs @@ -105,7 +105,7 @@ public static void ForceRemoveFromVisibleEntities(MapEntity entity) public static void Draw(SpriteBatch spriteBatch, bool editing = false) { - var entitiesToRender = !editing && visibleEntities != null ? visibleEntities : MapEntity.MapEntityList; + var entitiesToRender = !editing && visibleEntities != null ? visibleEntities : MapEntity.MapEntityList.ToList(); foreach (MapEntity e in entitiesToRender) { @@ -115,7 +115,7 @@ public static void Draw(SpriteBatch spriteBatch, bool editing = false) public static void DrawFront(SpriteBatch spriteBatch, bool editing = false, Predicate predicate = null) { - var entitiesToRender = !editing && visibleEntities != null ? visibleEntities : MapEntity.MapEntityList; + var entitiesToRender = !editing && visibleEntities != null ? visibleEntities : MapEntity.MapEntityList.ToList(); foreach (MapEntity e in entitiesToRender) { @@ -164,7 +164,7 @@ public static void DrawFront(SpriteBatch spriteBatch, bool editing = false, Pred public static void DrawDamageable(SpriteBatch spriteBatch, Effect damageEffect, bool editing = false, Predicate predicate = null) { - var entitiesToRender = !editing && visibleEntities != null ? visibleEntities : MapEntity.MapEntityList; + var entitiesToRender = !editing && visibleEntities != null ? visibleEntities : MapEntity.MapEntityList.ToList(); depthSortedDamageable.Clear(); @@ -197,7 +197,7 @@ public static void DrawDamageable(SpriteBatch spriteBatch, Effect damageEffect, public static void DrawPaintedColors(SpriteBatch spriteBatch, bool editing = false, Predicate predicate = null) { - var entitiesToRender = !editing && visibleEntities != null ? visibleEntities : MapEntity.MapEntityList; + var entitiesToRender = !editing && visibleEntities != null ? visibleEntities : MapEntity.MapEntityList.ToList(); foreach (MapEntity e in entitiesToRender) { @@ -217,7 +217,7 @@ public static void DrawPaintedColors(SpriteBatch spriteBatch, bool editing = fal public static void DrawBack(SpriteBatch spriteBatch, bool editing = false, Predicate predicate = null) { - var entitiesToRender = !editing && visibleEntities != null ? visibleEntities : MapEntity.MapEntityList; + var entitiesToRender = !editing && visibleEntities != null ? visibleEntities : MapEntity.MapEntityList.ToList(); foreach (MapEntity e in entitiesToRender) { diff --git a/Barotrauma/BarotraumaClient/ClientSource/Networking/EntitySpawner.cs b/Barotrauma/BarotraumaClient/ClientSource/Networking/EntitySpawner.cs index 6ba6e36a7b..f8c7e7d450 100644 --- a/Barotrauma/BarotraumaClient/ClientSource/Networking/EntitySpawner.cs +++ b/Barotrauma/BarotraumaClient/ClientSource/Networking/EntitySpawner.cs @@ -1,12 +1,32 @@ using Barotrauma.Items.Components; using Barotrauma.Networking; +using System.Collections.Concurrent; using System.Collections.Generic; namespace Barotrauma { partial class EntitySpawner : Entity, IServerSerializable { - public readonly List<(Entity entity, bool isRemoval)> receivedEvents = new List<(Entity entity, bool isRemoval)>(); + /// + /// Thread-safe queue for received entity spawn/remove events from the server. + /// + private readonly ConcurrentQueue<(Entity entity, bool isRemoval)> receivedEventsQueue = new ConcurrentQueue<(Entity entity, bool isRemoval)>(); + + /// + /// Gets a thread-safe snapshot of received events. + /// + public IEnumerable<(Entity entity, bool isRemoval)> GetReceivedEventsSnapshot() + { + return receivedEventsQueue.ToArray(); + } + + /// + /// Clears all received events from the queue. + /// + partial void ResetReceivedEvents() + { + while (receivedEventsQueue.TryDequeue(out _)) { } + } public void ClientEventRead(IReadMessage message, float sendingTime) { @@ -34,7 +54,7 @@ public void ClientEventRead(IReadMessage message, float sendingTime) { DebugConsole.Log("Received entity removal message for ID " + entityId + ". Entity with a matching ID not found."); } - receivedEvents.Add((entity, true)); + receivedEventsQueue.Enqueue((entity, true)); } else { @@ -57,7 +77,7 @@ public void ClientEventRead(IReadMessage message, float sendingTime) GameAnalyticsManager.AddDesignEvent("ItemFabricated:" + (GameMain.GameSession?.GameMode?.Preset.Identifier ?? "none".ToIdentifier()) + ":" + newItem.Prefab.Identifier); } } - receivedEvents.Add((newItem, false)); + receivedEventsQueue.Enqueue((newItem, false)); } break; case (byte)SpawnableType.Character: @@ -68,7 +88,7 @@ public void ClientEventRead(IReadMessage message, float sendingTime) } else { - receivedEvents.Add((character, false)); + receivedEventsQueue.Enqueue((character, false)); } break; default: diff --git a/Barotrauma/BarotraumaClient/ClientSource/Networking/GameClient.cs b/Barotrauma/BarotraumaClient/ClientSource/Networking/GameClient.cs index c416d7a108..a3fab020cd 100644 --- a/Barotrauma/BarotraumaClient/ClientSource/Networking/GameClient.cs +++ b/Barotrauma/BarotraumaClient/ClientSource/Networking/GameClient.cs @@ -3918,7 +3918,7 @@ private void WriteEventErrorData(ClientNetError error, UInt16 expectedID, UInt16 { errorLines.Add(""); errorLines.Add("EntitySpawner events:"); - foreach ((Entity entity, bool isRemoval) in Entity.Spawner.receivedEvents) + foreach ((Entity entity, bool isRemoval) in Entity.Spawner.GetReceivedEventsSnapshot()) { errorLines.Add( (isRemoval ? "Remove " : "Create ") + diff --git a/Barotrauma/BarotraumaServer/ServerSource/Characters/Character.cs b/Barotrauma/BarotraumaServer/ServerSource/Characters/Character.cs index e173c7a456..6559594ec7 100644 --- a/Barotrauma/BarotraumaServer/ServerSource/Characters/Character.cs +++ b/Barotrauma/BarotraumaServer/ServerSource/Characters/Character.cs @@ -75,7 +75,7 @@ partial void KillProjSpecific(CauseOfDeathType causeOfDeath, Affliction causeOfD { if (client.InGame) { - client.PendingPositionUpdates.Enqueue(this); + client.TryEnqueuePositionUpdate(this); } } } diff --git a/Barotrauma/BarotraumaServer/ServerSource/Events/EventManager.cs b/Barotrauma/BarotraumaServer/ServerSource/Events/EventManager.cs index 6850a51748..78d1288ccc 100644 --- a/Barotrauma/BarotraumaServer/ServerSource/Events/EventManager.cs +++ b/Barotrauma/BarotraumaServer/ServerSource/Events/EventManager.cs @@ -33,7 +33,7 @@ public void ServerRead(IReadMessage inc, Client sender) byte selectedOption = inc.ReadByte(); bool isIgnore = selectedOption == byte.MaxValue; - foreach (Event ev in activeEvents) + foreach (Event ev in _activeEvents) { if (ev is not ScriptedEvent scriptedEvent) { continue; } diff --git a/Barotrauma/BarotraumaServer/ServerSource/Items/Inventory.cs b/Barotrauma/BarotraumaServer/ServerSource/Items/Inventory.cs index 3d85e08adf..d2dfb2aad0 100644 --- a/Barotrauma/BarotraumaServer/ServerSource/Items/Inventory.cs +++ b/Barotrauma/BarotraumaServer/ServerSource/Items/Inventory.cs @@ -202,9 +202,9 @@ void HandleAddedItems() #if DEBUG || UNSTABLE DebugConsole.NewMessage($"Client {sender.Name} failed to put \"{item}\" in the inventory of {Owner} (parent inventory: {item.ParentInventory?.Owner.ToString() ?? "null"}). No access.", Color.Yellow); #endif - if (item.body != null && !sender.PendingPositionUpdates.Contains(item)) + if (item.body != null) { - sender.PendingPositionUpdates.Enqueue(item); + sender.TryEnqueuePositionUpdate(item); } item.PositionUpdateInterval = 0.0f; continue; diff --git a/Barotrauma/BarotraumaServer/ServerSource/Networking/Client.cs b/Barotrauma/BarotraumaServer/ServerSource/Networking/Client.cs index 6b1a3bd86a..134d86b402 100644 --- a/Barotrauma/BarotraumaServer/ServerSource/Networking/Client.cs +++ b/Barotrauma/BarotraumaServer/ServerSource/Networking/Client.cs @@ -64,6 +64,32 @@ public UInt16 LastRecvLobbyUpdate // key = entity, value = NetTime.Now when sending public readonly Dictionary PositionUpdateLastSent = new Dictionary(); public readonly Queue PendingPositionUpdates = new Queue(); + private readonly HashSet pendingPositionUpdatesSet = new HashSet(); + + /// + /// Attempts to enqueue a position update for the given entity. Returns true if the entity was added, false if it was already in the queue. + /// Uses HashSet for O(1) lookup instead of Queue.Contains() which is O(n). + /// + public bool TryEnqueuePositionUpdate(Entity entity) + { + if (pendingPositionUpdatesSet.Add(entity)) + { + PendingPositionUpdates.Enqueue(entity); + return true; + } + return false; + } + + /// + /// Dequeues a position update and removes it from the HashSet tracking. + /// + public Entity DequeuePositionUpdate() + { + if (PendingPositionUpdates.Count == 0) { return null; } + var entity = PendingPositionUpdates.Dequeue(); + pendingPositionUpdatesSet.Remove(entity); + return entity; + } public bool ReadyToStart; @@ -353,6 +379,7 @@ public void ResetSync() { NeedsMidRoundSync = false; PendingPositionUpdates.Clear(); + pendingPositionUpdatesSet.Clear(); EntityEventLastSent.Clear(); LastSentEntityEventID = 0; LastRecvEntityEventID = 0; diff --git a/Barotrauma/BarotraumaServer/ServerSource/Networking/GameServer.cs b/Barotrauma/BarotraumaServer/ServerSource/Networking/GameServer.cs index ad967ce8f1..26eb6b890f 100644 --- a/Barotrauma/BarotraumaServer/ServerSource/Networking/GameServer.cs +++ b/Barotrauma/BarotraumaServer/ServerSource/Networking/GameServer.cs @@ -744,11 +744,6 @@ public void Update(float deltaTime) { errorMsg += "\nInner exception: " + e.InnerException.Message + "\n" + e.InnerException.StackTrace.CleanupStackTrace(); } - - GameAnalyticsManager.AddErrorEventOnce( - "GameServer.Update:ClientWriteFailed" + e.StackTrace.CleanupStackTrace(), - GameAnalyticsManager.ErrorSeverity.Error, - errorMsg); } } @@ -2147,7 +2142,7 @@ static float GetShortestDistance(Vector2 viewPos, Character targetCharacter) { if (lastSent > NetTime.Now - updateInterval) { continue; } } - if (!c.PendingPositionUpdates.Contains(otherCharacter)) { c.PendingPositionUpdates.Enqueue(otherCharacter); } + c.TryEnqueuePositionUpdate(otherCharacter); } foreach (Submarine sub in Submarine.Loaded) @@ -2156,7 +2151,7 @@ static float GetShortestDistance(Vector2 viewPos, Character targetCharacter) // (= update is only sent for the docked sub that has the smallest ID, doesn't matter if it's the main sub or a shuttle) if (sub.Info.IsOutpost || sub.DockedTo.Any(s => s.ID < sub.ID)) { continue; } if (sub.PhysicsBody == null || sub.PhysicsBody.BodyType == FarseerPhysics.BodyType.Static) { continue; } - if (!c.PendingPositionUpdates.Contains(sub)) { c.PendingPositionUpdates.Enqueue(sub); } + c.TryEnqueuePositionUpdate(sub); } foreach (Item item in Item.ItemList) @@ -2173,7 +2168,7 @@ static float GetShortestDistance(Vector2 viewPos, Character targetCharacter) { if (lastSent > NetTime.Now - updateInterval) { continue; } } - if (!c.PendingPositionUpdates.Contains(item)) { c.PendingPositionUpdates.Enqueue(item); } + c.TryEnqueuePositionUpdate(item); } } @@ -2217,7 +2212,7 @@ static float GetShortestDistance(Vector2 viewPos, Character targetCharacter) entity.Removed || (entity is Item item && float.IsInfinity(item.PositionUpdateInterval))) { - c.PendingPositionUpdates.Dequeue(); + c.DequeuePositionUpdate(); continue; } @@ -2239,7 +2234,7 @@ static float GetShortestDistance(Vector2 viewPos, Character targetCharacter) outmsg.WritePadBits(); c.PositionUpdateLastSent[entity] = (float)NetTime.Now; - c.PendingPositionUpdates.Dequeue(); + c.DequeuePositionUpdate(); } positionUpdateBytes = outmsg.LengthBytes - positionUpdateBytes; diff --git a/Barotrauma/BarotraumaShared/SharedSource/Events/EventActions/UnlockPathAction.cs b/Barotrauma/BarotraumaShared/SharedSource/Events/EventActions/UnlockPathAction.cs index bdd9bb1911..977ad711bd 100644 --- a/Barotrauma/BarotraumaShared/SharedSource/Events/EventActions/UnlockPathAction.cs +++ b/Barotrauma/BarotraumaShared/SharedSource/Events/EventActions/UnlockPathAction.cs @@ -1,7 +1,8 @@ using Barotrauma.Networking; using Microsoft.Xna.Framework; using System; -using System.Collections.Generic; +using System.Collections.Immutable; +using System.Threading; namespace Barotrauma { @@ -10,11 +11,22 @@ namespace Barotrauma /// class UnlockPathAction : EventAction { - private static readonly HashSet pathsUnlockedThisRound = new HashSet(); + private static volatile ImmutableHashSet _pathsUnlockedThisRound = + ImmutableHashSet.Empty; public static void ResetPathsUnlockedThisRound() { - pathsUnlockedThisRound.Clear(); + _pathsUnlockedThisRound = ImmutableHashSet.Empty; + } + + private static void AddUnlockedPath(LocationConnection connection) + { + ImmutableHashSet original, updated; + do + { + original = _pathsUnlockedThisRound; + updated = original.Add(connection); + } while (Interlocked.CompareExchange(ref _pathsUnlockedThisRound, updated, original) != original); } public UnlockPathAction(ScriptedEvent parentEvent, ContentXElement element) : base(parentEvent, element) { } @@ -40,7 +52,7 @@ public override void Update(float deltaTime) { if (!connection.Locked) { continue; } connection.Locked = false; - pathsUnlockedThisRound.Add(connection); + AddUnlockedPath(connection); #if SERVER NotifyUnlock(connection); #else @@ -61,7 +73,7 @@ public override string ToDebugString() #if SERVER public static void NotifyPathsUnlockedThisRound(Client client) { - foreach (LocationConnection connection in pathsUnlockedThisRound) + foreach (LocationConnection connection in _pathsUnlockedThisRound) { NotifyUnlock(connection, client); } @@ -85,4 +97,4 @@ private static void NotifyUnlock(LocationConnection connection, Client client) } #endif } -} \ No newline at end of file +} diff --git a/Barotrauma/BarotraumaShared/SharedSource/Events/EventManager.cs b/Barotrauma/BarotraumaShared/SharedSource/Events/EventManager.cs index 351d92b756..517298a9ba 100644 --- a/Barotrauma/BarotraumaShared/SharedSource/Events/EventManager.cs +++ b/Barotrauma/BarotraumaShared/SharedSource/Events/EventManager.cs @@ -3,8 +3,11 @@ using FarseerPhysics; using Microsoft.Xna.Framework; 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 @@ -43,7 +46,7 @@ public readonly record struct NetEventObjective( private Level level; - private readonly List preloadedSprites = new List(); + private volatile ImmutableList _preloadedSprites = ImmutableList.Empty; //The "intensity" of the current situation (a value between 0.0 - 1.0). //High when a disaster has struck, low when nothing special is going on. @@ -83,14 +86,18 @@ public readonly record struct NetEventObjective( private float crewAwayResetTimer; private float crewAwayDuration; - private readonly List pendingEventSets = new List(); + // volatile + ImmutableCollections + private volatile ImmutableList _pendingEventSets = ImmutableList.Empty; - private readonly Dictionary> selectedEvents = new Dictionary>(); + private volatile ImmutableDictionary> _selectedEvents = + ImmutableDictionary>.Empty; - private readonly List activeEvents = new List(); + private volatile ImmutableList _activeEvents = ImmutableList.Empty; - private readonly HashSet finishedEvents = new HashSet(); - private readonly HashSet nonRepeatableEvents = new HashSet(); + private volatile ImmutableHashSet _finishedEvents = ImmutableHashSet.Empty; + private volatile ImmutableHashSet _nonRepeatableEvents = ImmutableHashSet.Empty; + + private volatile ImmutableQueue _deferredActions = ImmutableQueue.Empty; #if DEBUG && SERVER @@ -112,10 +119,10 @@ public float MusicIntensity public IEnumerable ActiveEvents { - get { return activeEvents; } + get { return _activeEvents; } } - public readonly Queue QueuedEvents = new Queue(); + public readonly ConcurrentQueue QueuedEvents = new ConcurrentQueue(); public readonly Queue QueuedEventsForNextRound = new Queue(); @@ -131,8 +138,8 @@ public TimeStamp(Event e) } } - private readonly List timeStamps = new List(); - public void AddTimeStamp(Event e) => timeStamps.Add(new TimeStamp(e)); + private volatile ImmutableList _timeStamps = ImmutableList.Empty; + public void AddTimeStamp(Event e) => AtomicUpdate(ref _timeStamps, list => list.Add(new TimeStamp(e))); public readonly EventLog EventLog = new EventLog(); @@ -143,6 +150,72 @@ public EventManager() public bool Enabled = true; + private static T AtomicUpdate(ref T location, Func updateFunc) where T : class + { + T original, updated; + do + { + original = Volatile.Read(ref location); + updated = updateFunc(original); + } while (Interlocked.CompareExchange(ref location, updated, original) != original); + return updated; + } + + // activeEvents + private void AddActiveEvent(Event ev) => AtomicUpdate(ref _activeEvents, list => list.Add(ev)); + private void ClearActiveEvents() => _activeEvents = ImmutableList.Empty; + + // pendingEventSets + private void AddPendingEventSet(EventSet eventSet) => + AtomicUpdate(ref _pendingEventSets, list => list.Contains(eventSet) ? list : list.Add(eventSet)); + private void RemovePendingEventSetAt(int index) => + AtomicUpdate(ref _pendingEventSets, list => index < list.Count ? list.RemoveAt(index) : list); + private void ClearPendingEventSets() => _pendingEventSets = ImmutableList.Empty; + + // selectedEvents + private void AddSelectedEvent(EventSet eventSet, Event ev) => + AtomicUpdate(ref _selectedEvents, dict => + { + var currentList = dict.GetValueOrDefault(eventSet, ImmutableList.Empty); + return dict.SetItem(eventSet, currentList.Add(ev)); + }); + private void RemoveSelectedEventSet(EventSet eventSet) => + AtomicUpdate(ref _selectedEvents, dict => dict.Remove(eventSet)); + private void ClearSelectedEvents() => + _selectedEvents = ImmutableDictionary>.Empty; + private ImmutableList GetSelectedEvents(EventSet eventSet) => + _selectedEvents.GetValueOrDefault(eventSet, ImmutableList.Empty); + private bool HasSelectedEvents(EventSet eventSet) => _selectedEvents.ContainsKey(eventSet); + + // finishedEvents + private void AddFinishedEvent(Event ev) => AtomicUpdate(ref _finishedEvents, set => set.Add(ev)); + private void ClearFinishedEvents() => _finishedEvents = ImmutableHashSet.Empty; + private bool IsEventFinished(Event ev) => _finishedEvents.Contains(ev); + + // nonRepeatableEvents + private void AddNonRepeatableEvent(Identifier id) => AtomicUpdate(ref _nonRepeatableEvents, set => set.Add(id)); + private void ClearNonRepeatableEvents() => _nonRepeatableEvents = ImmutableHashSet.Empty; + + // preloadedSprites + private void AddPreloadedSprite(Sprite sprite) => AtomicUpdate(ref _preloadedSprites, list => list.Add(sprite)); + private void ClearPreloadedSprites() + { + var sprites = Interlocked.Exchange(ref _preloadedSprites, ImmutableList.Empty); + foreach (var s in sprites) { s.Remove(); } + } + + // timeStamps + private void ClearTimeStamps() => _timeStamps = ImmutableList.Empty; + + private void EnqueueDeferredAction(Action action) => + AtomicUpdate(ref _deferredActions, queue => queue.Enqueue(action)); + private void ProcessDeferredActions() + { + var actions = Interlocked.Exchange(ref _deferredActions, ImmutableQueue.Empty); + foreach (var action in actions) { action(); } + } + + private MTRandom random; public int RandomSeed { get; private set; } @@ -152,10 +225,10 @@ public void StartRound(Level level) if (isClient) { return; } - timeStamps.Clear(); - pendingEventSets.Clear(); - selectedEvents.Clear(); - activeEvents.Clear(); + ClearTimeStamps(); + ClearPendingEventSets(); + ClearSelectedEvents(); + ClearActiveEvents(); #if SERVER MissionAction.ResetMissionsUnlockedThisRound(); UnlockPathAction.ResetPathsUnlockedThisRound(); @@ -235,8 +308,8 @@ public void StartRound(Level level) void AddSet(EventSet eventSet) { - if (pendingEventSets.Contains(eventSet)) { return; } - pendingEventSets.Add(eventSet); + if (_pendingEventSets.Contains(eventSet)) { return; } + AddPendingEventSet(eventSet); CreateEvents(eventSet); } @@ -251,7 +324,7 @@ void AddSet(EventSet eventSet) if (unlockPathEventPrefab != null) { var newEvent = unlockPathEventPrefab.CreateInstance(RandomSeed); - activeEvents.Add(newEvent); + AddActiveEvent(newEvent); } else { @@ -284,7 +357,7 @@ void RegisterNonRepeatableChildEvents(EventSet eventSet) { foreach (EventPrefab ep in eventSet.EventPrefabs.SelectMany(e => e.EventPrefabs)) { - nonRepeatableEvents.Add(ep.Identifier); + AddNonRepeatableEvent(ep.Identifier); } } foreach (EventSet childSet in eventSet.ChildSets) @@ -329,13 +402,13 @@ void RegisterNonRepeatableChildEvents(EventSet eventSet) public void ActivateEvent(Event newEvent) { - activeEvents.Add(newEvent); + AddActiveEvent(newEvent); newEvent.Init(); } public void ClearEvents() { - activeEvents.Clear(); + ClearActiveEvents(); } private void SelectSettings() @@ -388,7 +461,8 @@ private void SelectSettings() public IEnumerable GetFilesToPreload() { - foreach (List eventList in selectedEvents.Values) + var snapshot = _selectedEvents; + foreach (ImmutableList eventList in snapshot.Values) { foreach (Event ev in eventList) { @@ -441,13 +515,13 @@ public void PreloadContent(IEnumerable contentFiles) foreach (ContentFile file in filesToPreload) { - file.Preload(preloadedSprites.Add); + file.Preload(AddPreloadedSprite); } } public void TriggerOnEndRoundActions() { - foreach (var ev in activeEvents) + foreach (var ev in _activeEvents) { (ev as ScriptedEvent)?.OnRoundEndAction?.Update(1.0f); } @@ -455,17 +529,16 @@ public void TriggerOnEndRoundActions() public void EndRound() { - pendingEventSets.Clear(); - selectedEvents.Clear(); - activeEvents.Clear(); - QueuedEvents.Clear(); - finishedEvents.Clear(); - nonRepeatableEvents.Clear(); + ClearPendingEventSets(); + ClearSelectedEvents(); + ClearActiveEvents(); + while (QueuedEvents.TryDequeue(out _)) { } // 清空 ConcurrentQueue + ClearFinishedEvents(); + ClearNonRepeatableEvents(); - preloadedSprites.ForEach(s => s.Remove()); - preloadedSprites.Clear(); + ClearPreloadedSprites(); - timeStamps.Clear(); + ClearTimeStamps(); pathFinder = null; } @@ -481,7 +554,7 @@ public void StoreEventDataAtRoundEnd(bool registerFinishedOnly = false) { if (registerFinishedOnly) { - foreach (var finishedEvent in finishedEvents) + foreach (var finishedEvent in _finishedEvents) { EventSet parentSet = finishedEvent.ParentSet; if (parentSet == null) { continue; } @@ -496,7 +569,7 @@ public void StoreEventDataAtRoundEnd(bool registerFinishedOnly = false) } } - level.LevelData.EventHistory.AddRange(selectedEvents.Values + level.LevelData.EventHistory.AddRange(_selectedEvents.Values .SelectMany(v => v) .Select(e => e.Prefab.Identifier) .Where(eventId => Register(eventId) && !level.LevelData.EventHistory.Contains(eventId))); @@ -506,14 +579,14 @@ public void StoreEventDataAtRoundEnd(bool registerFinishedOnly = false) level.LevelData.EventHistory.RemoveRange(0, level.LevelData.EventHistory.Count - MaxEventHistory); } } - level.LevelData.NonRepeatableEvents.AddRange(nonRepeatableEvents.Where(eventId => Register(eventId) && !level.LevelData.NonRepeatableEvents.Contains(eventId))); + level.LevelData.NonRepeatableEvents.AddRange(_nonRepeatableEvents.Where(eventId => Register(eventId) && !level.LevelData.NonRepeatableEvents.Contains(eventId))); if (!registerFinishedOnly) { level.LevelData.FinishedEvents.Clear(); } - bool Register(Identifier eventId) => !registerFinishedOnly || finishedEvents.Any(fe => fe.Prefab.Identifier == eventId); + bool Register(Identifier eventId) => !registerFinishedOnly || _finishedEvents.Any(fe => fe.Prefab.Identifier == eventId); } public void SkipEventCooldown() @@ -531,7 +604,7 @@ private float CalculateCommonness(EventPrefab eventPrefab, float baseCommonness) private void CreateEvents(EventSet eventSet) { - selectedEvents.Remove(eventSet); + RemoveSelectedEventSet(eventSet); if (level == null) { return; } if (level.LevelData.HasHuntingGrounds && eventSet.DisableInHuntingGrounds) { return; } if (eventSet.Exhaustible && level.LevelData.IsEventSetExhausted(eventSet)) { return; } @@ -598,11 +671,7 @@ private void CreateEvents(EventSet eventSet) if (newEvent == null) { continue; } if (i < spawnPosFilter.Count) { newEvent.SpawnPosFilter = spawnPosFilter[i]; } DebugConsole.NewMessage($"Initialized event {newEvent}", debugOnly: true); - if (!selectedEvents.ContainsKey(eventSet)) - { - selectedEvents.Add(eventSet, new List()); - } - selectedEvents[eventSet].Add(newEvent); + AddSelectedEvent(eventSet, newEvent); unusedEvents.Remove(subEventPrefab); } } @@ -641,11 +710,7 @@ private void CreateEvents(EventSet eventSet) var newEvent = eventPrefab.CreateInstance(RandomSeed); if (newEvent == null) { continue; } if (i < spawnPosFilter.Count) { newEvent.SpawnPosFilter = spawnPosFilter[i]; } - if (!selectedEvents.ContainsKey(eventSet)) - { - selectedEvents.Add(eventSet, new List()); - } - selectedEvents[eventSet].Add(newEvent); + AddSelectedEvent(eventSet, newEvent); } var location = GetEventLocation(); @@ -837,9 +902,10 @@ public void Update(float deltaTime) if (!eventsInitialized) { - foreach (var eventSet in selectedEvents.Keys) + var selectedSnapshot = _selectedEvents; + foreach (var eventSet in selectedSnapshot.Keys) { - foreach (var ev in selectedEvents[eventSet]) + foreach (var ev in selectedSnapshot[eventSet]) { ev.Init(eventSet); } @@ -910,23 +976,25 @@ public void Update(float deltaTime) { recheck = false; //activate pending event sets that can be activated - for (int i = pendingEventSets.Count - 1; i >= 0; i--) + var pendingSnapshot = _pendingEventSets; + for (int i = pendingSnapshot.Count - 1; i >= 0; i--) { - var eventSet = pendingEventSets[i]; + var eventSet = pendingSnapshot[i]; if (eventCoolDown > 0.0f && !eventSet.IgnoreCoolDown) { continue; } if (currentIntensity > eventThreshold && !eventSet.IgnoreIntensity) { continue; } if (!CanStartEventSet(eventSet)) { continue; } - pendingEventSets.RemoveAt(i); + RemovePendingEventSetAt(i); - if (selectedEvents.ContainsKey(eventSet)) + var selectedEventsList = GetSelectedEvents(eventSet); + if (selectedEventsList.Count > 0) { //start events in this set - foreach (Event ev in selectedEvents[eventSet]) + foreach (Event ev in selectedEventsList) { - activeEvents.Add(ev); + AddActiveEvent(ev); eventThreshold = settings.DefaultEventThreshold; - if (eventSet.TriggerEventCooldown && selectedEvents[eventSet].Any(e => e.Prefab.TriggerEventCooldown)) + if (eventSet.TriggerEventCooldown && selectedEventsList.Any(e => e.Prefab.TriggerEventCooldown)) { eventCoolDown = settings.EventCooldown; } @@ -934,12 +1002,15 @@ public void Update(float deltaTime) { ev.Finished += () => { - pendingEventSets.Add(eventSet); - CreateEvents(eventSet); - foreach (Event newEvent in selectedEvents[eventSet]) + EnqueueDeferredAction(() => { - if (!newEvent.Initialized) { newEvent.Init(eventSet); } - } + AddPendingEventSet(eventSet); + CreateEvents(eventSet); + foreach (Event newEvent in GetSelectedEvents(eventSet)) + { + if (!newEvent.Initialized) { newEvent.Init(eventSet); } + } + }); }; } } @@ -948,37 +1019,40 @@ public void Update(float deltaTime) //add child event sets to pending foreach (EventSet childEventSet in eventSet.ChildSets) { - pendingEventSets.Add(childEventSet); + AddPendingEventSet(childEventSet); recheck = true; } } } while (recheck); - foreach (Event ev in activeEvents) + var activeSnapshot = _activeEvents; + foreach (Event ev in activeSnapshot) { if (!ev.IsFinished) { ev.Update(deltaTime); } - else if (ev.Prefab != null && !finishedEvents.Any(e => e.Prefab == ev.Prefab)) + else if (ev.Prefab != null && !IsEventFinished(ev)) { if (level?.LevelData != null && level.LevelData.Type == LevelData.LevelType.Outpost) { if (!level.LevelData.EventHistory.Contains(ev.Prefab.Identifier)) { level.LevelData.EventHistory.Add(ev.Prefab.Identifier); } } - finishedEvents.Add(ev); + AddFinishedEvent(ev); } } - if (QueuedEvents.Count > 0) + if (QueuedEvents.TryDequeue(out var queuedEvent)) { - activeEvents.Add(QueuedEvents.Dequeue()); + AddActiveEvent(queuedEvent); } + + ProcessDeferredActions(); } public void EntitySpawned(Entity entity) { - foreach (var ev in activeEvents) + foreach (var ev in _activeEvents) { if (ev is ScriptedEvent scriptedEvent) { diff --git a/Barotrauma/BarotraumaShared/SharedSource/Events/EventSet.cs b/Barotrauma/BarotraumaShared/SharedSource/Events/EventSet.cs index 9157959613..40a6b47dd7 100644 --- a/Barotrauma/BarotraumaShared/SharedSource/Events/EventSet.cs +++ b/Barotrauma/BarotraumaShared/SharedSource/Events/EventSet.cs @@ -3,6 +3,7 @@ using System.Collections.Generic; using System.Collections.Immutable; using System.Linq; +using System.Threading; using System.Xml.Linq; namespace Barotrauma @@ -60,47 +61,48 @@ public static Sprite GetEventSprite(string identifier) return null; } #endif - - private static readonly Dictionary AllEventPrefabs = new Dictionary(); + private static volatile ImmutableDictionary _allEventPrefabs = + ImmutableDictionary.Empty; public static IEnumerable GetAllEventPrefabs() { - return AllEventPrefabs.Values; + return _allEventPrefabs.Values; } /// /// Finds all the event prefabs (both "normal prefabs" that exists by themselves, present in , and the ones that exists only inside child event sets), - /// and adds them to . + /// and adds them to . /// public static void RefreshAllEventPrefabs() { - AllEventPrefabs.Clear(); + var builder = ImmutableDictionary.CreateBuilder(); foreach (var eventPrefab in EventPrefab.Prefabs) { - AllEventPrefabs.TryAdd(eventPrefab.Identifier, eventPrefab); + builder.TryAdd(eventPrefab.Identifier, eventPrefab); } foreach (var eventSet in Prefabs) { - AddChildEventPrefabs(eventSet); + AddChildEventPrefabs(eventSet, builder); } + Interlocked.Exchange(ref _allEventPrefabs, builder.ToImmutable()); } - private static void AddChildEventPrefabs(EventSet set) + private static void AddChildEventPrefabs(EventSet set, ImmutableDictionary.Builder builder) { foreach (var subEventPrefabs in set.EventPrefabs) { foreach (var eventPrefab in subEventPrefabs.EventPrefabs) { - AllEventPrefabs.TryAdd(eventPrefab.Identifier, eventPrefab); + builder.TryAdd(eventPrefab.Identifier, eventPrefab); } } - foreach (var childSet in set.ChildSets) { AddChildEventPrefabs(childSet); } + foreach (var childSet in set.ChildSets) { AddChildEventPrefabs(childSet, builder); } } public static EventPrefab GetEventPrefab(Identifier identifier) { - return AllEventPrefabs.GetValueOrDefault(identifier); + return _allEventPrefabs.GetValueOrDefault(identifier); } /// diff --git a/Barotrauma/BarotraumaShared/SharedSource/Items/Item.cs b/Barotrauma/BarotraumaShared/SharedSource/Items/Item.cs index 8ad7bfea14..275d188358 100644 --- a/Barotrauma/BarotraumaShared/SharedSource/Items/Item.cs +++ b/Barotrauma/BarotraumaShared/SharedSource/Items/Item.cs @@ -2555,7 +2555,11 @@ public static void UpdatePendingConditionUpdates(float deltaTime) /// public bool IsActive = true; - public bool IsInRemoveQueue; + /// + /// Thread-safe flag indicating whether this item is queued for removal. + /// Uses volatile to ensure memory visibility across threads. + /// + public volatile bool IsInRemoveQueue; public override void Update(float deltaTime, Camera cam) { @@ -4903,12 +4907,11 @@ public override void Remove() StaticFixtures.Clear(); } - foreach (Item it in ItemList) + // Optimized: Remove() returns false if not found, no need for Contains() check + // Using _itemDictionary.Values directly avoids property access overhead + foreach (Item it in _itemDictionary.Values) { - if (it.linkedTo.Contains(this)) - { - it.linkedTo.Remove(this); - } + it.linkedTo.Remove(this); } RemoveProjSpecific(); diff --git a/Barotrauma/BarotraumaShared/SharedSource/Networking/EntitySpawner.cs b/Barotrauma/BarotraumaShared/SharedSource/Networking/EntitySpawner.cs index d95b5bcedf..780b5a5396 100644 --- a/Barotrauma/BarotraumaShared/SharedSource/Networking/EntitySpawner.cs +++ b/Barotrauma/BarotraumaShared/SharedSource/Networking/EntitySpawner.cs @@ -3,6 +3,7 @@ using Barotrauma.Networking; using Microsoft.Xna.Framework; using System; +using System.Collections.Concurrent; using System.Collections.Generic; using System.Linq; @@ -204,7 +205,17 @@ public void OnSpawned(Entity spawnedCharacter) } } - private readonly Queue> spawnOrRemoveQueue; + /// + /// Thread-safe queue for spawn/remove operations. + /// Uses ConcurrentQueue for lock-free concurrent access. + /// + private readonly ConcurrentQueue> spawnOrRemoveQueue; + + /// + /// Thread-safe set for O(1) removal queue lookup. + /// Entities are added when queued for removal and removed after actual removal. + /// + private readonly ConcurrentDictionary removeQueueLookup; public abstract class SpawnOrRemove : NetEntityEvent.IData { @@ -264,7 +275,8 @@ public override string ToString() public EntitySpawner() : base(null, Entity.EntitySpawnerID) { - spawnOrRemoveQueue = new Queue>(); + spawnOrRemoveQueue = new ConcurrentQueue>(); + removeQueueLookup = new ConcurrentDictionary(); } public override string ToString() @@ -358,8 +370,12 @@ public void AddCharacterToSpawnQueue(Identifier speciesName, Vector2 worldPositi public void AddEntityToRemoveQueue(Entity entity) { if (GameMain.NetworkMember != null && GameMain.NetworkMember.IsClient) { return; } - if (entity == null || IsInRemoveQueue(entity) || entity.Removed || entity.IdFreed) { return; } + if (entity == null || entity.Removed || entity.IdFreed) { return; } if (entity is Item item) { AddItemToRemoveQueue(item); return; } + + // Thread-safe check-and-add using ConcurrentDictionary + if (!removeQueueLookup.TryAdd(entity, 0)) { return; } + if (entity is Character) { Character character = entity as Character; @@ -381,7 +397,10 @@ public void AddEntityToRemoveQueue(Entity entity) public void AddItemToRemoveQueue(Item item) { if (GameMain.NetworkMember != null && GameMain.NetworkMember.IsClient) { return; } - if (IsInRemoveQueue(item) || item.Removed) { return; } + if (item.Removed) { return; } + + // Thread-safe check-and-add using ConcurrentDictionary + if (!removeQueueLookup.TryAdd(item, 0)) { return; } spawnOrRemoveQueue.Enqueue(item); item.IsInRemoveQueue = true; @@ -396,11 +415,13 @@ public void AddItemToRemoveQueue(Item item) } /// - /// Are there any entities in the spawn queue that match the given predicate + /// Thread-safe check if any entities in the spawn queue match the given predicate. + /// Uses a snapshot of the queue for iteration. /// public bool IsInSpawnQueue(Predicate predicate) { - foreach (var spawnOrRemove in spawnOrRemoveQueue) + // ConcurrentQueue.ToArray() provides a thread-safe snapshot + foreach (var spawnOrRemove in spawnOrRemoveQueue.ToArray()) { if (spawnOrRemove.TryGet(out IEntitySpawnInfo spawnInfo) && predicate(spawnInfo)) { return true; } } @@ -408,35 +429,45 @@ public bool IsInSpawnQueue(Predicate predicate) } /// - /// How many entities in the spawn queue match the given predicate + /// Thread-safe count of entities in the spawn queue that match the given predicate. + /// Uses a snapshot of the queue for iteration. /// public int CountSpawnQueue(Predicate predicate) { int count = 0; - foreach (var spawnOrRemove in spawnOrRemoveQueue) + // ConcurrentQueue.ToArray() provides a thread-safe snapshot + foreach (var spawnOrRemove in spawnOrRemoveQueue.ToArray()) { if (spawnOrRemove.TryGet(out IEntitySpawnInfo spawnInfo) && predicate(spawnInfo)) { count++; } } return count; } + /// + /// Thread-safe O(1) check if entity is in the remove queue. + /// public bool IsInRemoveQueue(Entity entity) { - foreach (var spawnOrRemove in spawnOrRemoveQueue) - { - if (spawnOrRemove.TryGet(out Entity entityToRemove) && entityToRemove == entity) { return true; } - } - return false; + return removeQueueLookup.ContainsKey(entity); } public void Update(bool createNetworkEvents = true) { if (GameMain.NetworkMember is { IsClient: true }) { return; } - while (spawnOrRemoveQueue.Count > 0) + + // IMPORTANT: Entity creation and removal MUST be sequential! + // - Entity ID allocation is NOT thread-safe (causes ID conflicts) + // - Inventory operations are NOT thread-safe (causes stack overflow/slot conflicts) + // - Entity.Remove() has cascading effects on global state + // + // Optimization: batch dequeue for better cache locality + while (spawnOrRemoveQueue.TryDequeue(out var spawnOrRemove)) { - if (!spawnOrRemoveQueue.TryDequeue(out var spawnOrRemove)) { break; } if (spawnOrRemove.TryGet(out Entity entityToRemove)) { + // Remove from lookup after processing + removeQueueLookup.TryRemove(entityToRemove, out _); + if (entityToRemove is Item item) { item.SendPendingNetworkUpdates(); @@ -465,9 +496,11 @@ public void Update(bool createNetworkEvents = true) public void Reset() { - spawnOrRemoveQueue.Clear(); + // Clear the concurrent queue by draining it + while (spawnOrRemoveQueue.TryDequeue(out _)) { } + removeQueueLookup.Clear(); #if CLIENT - receivedEvents.Clear(); + ResetReceivedEvents(); #endif } } diff --git a/Barotrauma/BarotraumaShared/SharedSource/Physics/PhysicsBodyQueue.cs b/Barotrauma/BarotraumaShared/SharedSource/Physics/PhysicsBodyQueue.cs index a8aec47efc..13fefbace1 100644 --- a/Barotrauma/BarotraumaShared/SharedSource/Physics/PhysicsBodyQueue.cs +++ b/Barotrauma/BarotraumaShared/SharedSource/Physics/PhysicsBodyQueue.cs @@ -1,34 +1,51 @@ -using System; -using System.Collections.Generic; +using System; +using System.Runtime.CompilerServices; using System.Threading; +using System.Threading.Channels; namespace Barotrauma { /// - /// Thread-safe queue for deferring physics operations to the main thread. + /// High-performance lock-free thread-safe queue for deferring physics operations to the main thread. /// This is necessary because Farseer Physics' DynamicTree is not thread-safe, /// and physics operations cannot be safely performed during parallel updates. /// + /// Uses System.Threading.Channels for optimal throughput with single-reader pattern. + /// Channel<T> provides better performance than ConcurrentQueue in producer-consumer scenarios. + /// /// 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() + /// + /// Workflow: + /// + /// ├─> 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 _pendingOperations = new Queue(); + // High-performance unbounded channel optimized for single-reader scenario + private static readonly Channel _channel = Channel.CreateUnbounded( + new UnboundedChannelOptions + { + SingleReader = true, // Only main thread reads - enables optimizations + SingleWriter = false, // Multiple parallel threads may write + AllowSynchronousContinuations = false // Prevent stack dives, improve throughput + }); + + private static readonly ChannelWriter _writer = _channel.Writer; + private static readonly ChannelReader _reader = _channel.Reader; /// /// Thread-local flag indicating whether the current thread is in a parallel physics update context. @@ -49,21 +66,20 @@ public static bool IsInParallelContext /// /// Enqueues a physics operation to be executed on the main thread. - /// This method is thread-safe and can be called from parallel update loops. + /// This method is lock-free and can be safely called from parallel update loops. + /// Uses Channel's optimized TryWrite which is faster than ConcurrentQueue.Enqueue. /// /// The physics operation to defer + [MethodImpl(MethodImplOptions.AggressiveInlining)] public static void Enqueue(Action operation) { if (operation == null) { return; } - lock (_lock) - { - _pendingOperations.Enqueue(operation); - } + _writer.TryWrite(operation); } /// /// 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. + /// This method is lock-free and can be safely called from parallel update loops. /// /// The action that creates the physics body public static void EnqueueCreation(Action createAction) @@ -75,50 +91,49 @@ public static void EnqueueCreation(Action 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. + /// + /// Hot path optimization: Most calls occur outside parallel context, so we check + /// the non-parallel case first to improve branch prediction. /// /// The physics operation to execute + [MethodImpl(MethodImplOptions.AggressiveInlining)] public static void ExecuteOrDefer(Action operation) { if (operation == null) { return; } - if (_isInParallelContext) - { - Enqueue(operation); - } - else + // Hot path: Most calls are outside parallel context - execute immediately + if (!_isInParallelContext) { operation(); + return; } + + // Cold path: In parallel context - defer to queue + _writer.TryWrite(operation); } /// - /// Gets the number of pending physics operations. + /// Gets whether there are any pending physics operations. + /// This is an O(1) operation. /// - public static int PendingCount - { - get - { - lock (_lock) - { - return _pendingOperations.Count; - } - } - } + public static bool HasPending => _reader.TryPeek(out _); + + /// + /// Gets the approximate number of pending physics operations. + /// Note: This may have some overhead compared to the previous atomic counter. + /// Use HasPending for simple empty checks. + /// + public static int PendingCount => _reader.Count; /// /// Processes all pending physics operations. /// Must be called on the main thread, outside of any parallel loops. + /// Uses Channel's optimized TryRead for single-reader scenario. /// public static void ProcessPendingOperations() { - while (true) + while (_reader.TryRead(out Action action)) { - Action action; - lock (_lock) - { - if (_pendingOperations.Count == 0) { break; } - action = _pendingOperations.Dequeue(); - } try { action?.Invoke(); @@ -145,11 +160,7 @@ public static void ProcessPendingCreations() /// public static void Clear() { - lock (_lock) - { - _pendingOperations.Clear(); - } + while (_reader.TryRead(out _)) { } } } } - From 854d7bea1f970948667035be8bef5fa3ffc29b46 Mon Sep 17 00:00:00 2001 From: Eero Date: Mon, 29 Dec 2025 18:20:37 +0800 Subject: [PATCH 02/10] CBT2.0 Make Hull and Level methods thread-safe using ThreadLocal Replaced instance fields with ThreadLocal collections in Hull.GetConnectedHulls and Level.GetCells to ensure thread safety during parallel updates. Methods now return copies of the collections to prevent concurrent modification issues. --- Barotrauma/BarotraumaShared/SharedSource/Map/Hull.cs | 11 +++++++++-- .../SharedSource/Map/Levels/Level.cs | 12 ++++++++++-- 2 files changed, 19 insertions(+), 4 deletions(-) diff --git a/Barotrauma/BarotraumaShared/SharedSource/Map/Hull.cs b/Barotrauma/BarotraumaShared/SharedSource/Map/Hull.cs index 50a1b3b39c..08a7bad978 100644 --- a/Barotrauma/BarotraumaShared/SharedSource/Map/Hull.cs +++ b/Barotrauma/BarotraumaShared/SharedSource/Map/Hull.cs @@ -1214,15 +1214,22 @@ public void RemoveFire(FireSource fire) } } - private readonly HashSet adjacentHulls = new HashSet(); + /// + /// Used in - ThreadLocal for thread safety during parallel updates + /// + private static readonly ThreadLocal> adjacentHullsLocal = + new ThreadLocal>(() => new HashSet()); + public IEnumerable GetConnectedHulls(bool includingThis, int? searchDepth = null, bool ignoreClosedGaps = false) { + var adjacentHulls = adjacentHullsLocal.Value; adjacentHulls.Clear(); int startStep = 0; searchDepth ??= 100; GetAdjacentHulls(adjacentHulls, ref startStep, searchDepth.Value, ignoreClosedGaps); if (!includingThis) { adjacentHulls.Remove(this); } - return adjacentHulls; + // Return a copy to prevent concurrent modification if the caller enumerates while another thread calls this method + return adjacentHulls.ToHashSet(); } private void GetAdjacentHulls(HashSet connectedHulls, ref int step, int searchDepth, bool ignoreClosedGaps = false) diff --git a/Barotrauma/BarotraumaShared/SharedSource/Map/Levels/Level.cs b/Barotrauma/BarotraumaShared/SharedSource/Map/Levels/Level.cs index c2e123bd1b..d0ddf3fe9e 100644 --- a/Barotrauma/BarotraumaShared/SharedSource/Map/Levels/Level.cs +++ b/Barotrauma/BarotraumaShared/SharedSource/Map/Levels/Level.cs @@ -10,6 +10,7 @@ using System.Diagnostics; using System.Globalization; using System.Linq; +using System.Threading; using System.Xml.Linq; using Voronoi2; @@ -3655,9 +3656,15 @@ public List GetAllCells() return cells; } - private readonly List tempCells = new List(); + /// + /// Used in - ThreadLocal for thread safety during parallel updates + /// + private static readonly ThreadLocal> tempCellsLocal = + new ThreadLocal>(() => new List()); + public List GetCells(Vector2 worldPos, int searchDepth = 2) { + var tempCells = tempCellsLocal.Value; tempCells.Clear(); int gridPosX = (int)Math.Floor(worldPos.X / GridCellSize); int gridPosY = (int)Math.Floor(worldPos.Y / GridCellSize); @@ -3712,7 +3719,8 @@ public List GetCells(Vector2 worldPos, int searchDepth = 2) tempCells.AddRange(abyssIsland.Cells); } - return tempCells; + // Return a copy to prevent concurrent modification if the caller enumerates while another thread calls this method + return tempCells.ToList(); } public VoronoiCell GetClosestCell(Vector2 worldPos) From 9474f7654cd003799e0932866e70709ead386099 Mon Sep 17 00:00:00 2001 From: Eero Date: Mon, 29 Dec 2025 18:37:13 +0800 Subject: [PATCH 03/10] CBT2.0.1 Fix event reset and temp cell clearing logic Changed ResetReceivedEvents from partial to regular method in EntitySpawner to ensure proper event queue clearing. Updated Level.cs to clear tempCellsLocal instead of tempCells, addressing potential issues with thread-local storage. --- .../BarotraumaClient/ClientSource/Networking/EntitySpawner.cs | 2 +- Barotrauma/BarotraumaShared/SharedSource/Map/Levels/Level.cs | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/Barotrauma/BarotraumaClient/ClientSource/Networking/EntitySpawner.cs b/Barotrauma/BarotraumaClient/ClientSource/Networking/EntitySpawner.cs index f8c7e7d450..239150c386 100644 --- a/Barotrauma/BarotraumaClient/ClientSource/Networking/EntitySpawner.cs +++ b/Barotrauma/BarotraumaClient/ClientSource/Networking/EntitySpawner.cs @@ -23,7 +23,7 @@ partial class EntitySpawner : Entity, IServerSerializable /// /// Clears all received events from the queue. /// - partial void ResetReceivedEvents() + void ResetReceivedEvents() { while (receivedEventsQueue.TryDequeue(out _)) { } } diff --git a/Barotrauma/BarotraumaShared/SharedSource/Map/Levels/Level.cs b/Barotrauma/BarotraumaShared/SharedSource/Map/Levels/Level.cs index d0ddf3fe9e..f4d5e5d6af 100644 --- a/Barotrauma/BarotraumaShared/SharedSource/Map/Levels/Level.cs +++ b/Barotrauma/BarotraumaShared/SharedSource/Map/Levels/Level.cs @@ -5189,7 +5189,7 @@ public override void Remove() UnsyncedExtraWalls = null; } - tempCells?.Clear(); + tempCellsLocal?.Value?.Clear(); cells = null; cellGrid = null; From 7c618598402a5516289926652c25f083b679ad35 Mon Sep 17 00:00:00 2001 From: Eero Date: Tue, 30 Dec 2025 03:14:27 +0800 Subject: [PATCH 04/10] CBT2.0.2 Add Lua converters for thread-safe and immutable collections Implemented custom Lua converters for various thread-safe lists, ImmutableList, ImmutableHashSet, and ImmutableDictionary types. This allows seamless conversion between C# collections and Lua tables, improving Lua scripting integration with these collection types. --- .../SharedSource/LuaCs/Lua/LuaConverters.cs | 126 +++++++++++++++++- 1 file changed, 124 insertions(+), 2 deletions(-) diff --git a/Barotrauma/BarotraumaShared/SharedSource/LuaCs/Lua/LuaConverters.cs b/Barotrauma/BarotraumaShared/SharedSource/LuaCs/Lua/LuaConverters.cs index e2c038ebeb..4b7845e85a 100644 --- a/Barotrauma/BarotraumaShared/SharedSource/LuaCs/Lua/LuaConverters.cs +++ b/Barotrauma/BarotraumaShared/SharedSource/LuaCs/Lua/LuaConverters.cs @@ -1,4 +1,6 @@ using System; +using System.Collections.Generic; +using System.Linq; using MoonSharp.Interpreter; using Microsoft.Xna.Framework; using FarseerPhysics.Dynamics; @@ -132,7 +134,7 @@ private void RegisterLuaConverters() return new Pair((JobPrefab)v.Table.Get(1).ToObject(), (int)v.Table.Get(2).CastToNumber()); }); - Script.GlobalOptions.CustomConverters.SetClrToScriptCustomConversion((Script script, ulong v) => + Script.GlobalOptions.CustomConverters.SetClrToScriptCustomConversion((Script script, ulong v) => { return DynValue.NewString(v.ToString()); }); @@ -226,6 +228,28 @@ private void RegisterLuaConverters() RegisterEither(); RegisterImmutableArray(); + + + RegisterThreadSafeList(); + RegisterThreadSafeList(); + RegisterThreadSafeList(); + RegisterThreadSafeList(); + RegisterThreadSafeList(); + RegisterThreadSafeList(); + RegisterThreadSafeList(); + RegisterThreadSafeList(); + RegisterThreadSafeList(); + + RegisterImmutableList(); + RegisterImmutableList(); + RegisterImmutableList(); + + RegisterImmutableHashSet(); + RegisterImmutableHashSet(); + RegisterImmutableHashSet(); + + RegisterImmutableDictionary(); + RegisterImmutableDictionary>(); } private void RegisterImmutableArray() @@ -332,7 +356,7 @@ private void RegisterAction() private void RegisterAction() { - Script.GlobalOptions.CustomConverters.SetScriptToClrCustomConversion(DataType.Function, typeof(Action), v => + Script.GlobalOptions.CustomConverters.SetScriptToClrCustomConversion(DataType.Function, typeof(Action), v => { var function = v.Function; return (Action)(() => CallLuaFunction(function)); @@ -389,5 +413,103 @@ private void RegisterFunc() return (T1 a, T2 b, T3 c, T4 d) => function.Call(a, b, c, d).ToObject(); }); } + + private void RegisterThreadSafeList() where TList : IEnumerable + { + Script.GlobalOptions.CustomConverters.SetClrToScriptCustomConversion( + typeof(TList), + (Script script, object obj) => + { + if (obj is IEnumerable enumerable) + { + var table = new Table(script); + int i = 1; + foreach (var item in enumerable) + { + table[i++] = DynValue.FromObject(script, item); + } + return DynValue.NewTable(table); + } + return DynValue.Nil; + } + ); + } + + private void RegisterImmutableList() + { + Script.GlobalOptions.CustomConverters.SetClrToScriptCustomConversion( + typeof(ImmutableList), + (Script script, object obj) => + { + if (obj is ImmutableList list) + { + var table = new Table(script); + int i = 1; + foreach (var item in list) + { + table[i++] = DynValue.FromObject(script, item); + } + return DynValue.NewTable(table); + } + return DynValue.Nil; + } + ); + + // Lua table -> ImmutableList + Script.GlobalOptions.CustomConverters.SetScriptToClrCustomConversion( + DataType.Table, + typeof(ImmutableList), + v => v.ToObject().ToImmutableList() + ); + } + + private void RegisterImmutableHashSet() + { + Script.GlobalOptions.CustomConverters.SetClrToScriptCustomConversion( + typeof(ImmutableHashSet), + (Script script, object obj) => + { + if (obj is ImmutableHashSet set) + { + var table = new Table(script); + int i = 1; + foreach (var item in set) + { + table[i++] = DynValue.FromObject(script, item); + } + return DynValue.NewTable(table); + } + return DynValue.Nil; + } + ); + + // Lua table -> ImmutableHashSet + Script.GlobalOptions.CustomConverters.SetScriptToClrCustomConversion( + DataType.Table, + typeof(ImmutableHashSet), + v => v.ToObject().ToImmutableHashSet() + ); + } + + private void RegisterImmutableDictionary() + { + Script.GlobalOptions.CustomConverters.SetClrToScriptCustomConversion( + typeof(ImmutableDictionary), + (Script script, object obj) => + { + if (obj is ImmutableDictionary dict) + { + var table = new Table(script); + foreach (var kvp in dict) + { + table[DynValue.FromObject(script, kvp.Key)] = + DynValue.FromObject(script, kvp.Value); + } + return DynValue.NewTable(table); + } + return DynValue.Nil; + } + ); + } } } From f4a0d149ca5a79fdf7c619c3f382d3a6f9431897 Mon Sep 17 00:00:00 2001 From: Eero Date: Sun, 4 Jan 2026 00:21:52 +0800 Subject: [PATCH 05/10] CBT2.0.3 #33 --- .../SharedSource/Map/Submarine.cs | 51 +++++++++++++------ 1 file changed, 36 insertions(+), 15 deletions(-) diff --git a/Barotrauma/BarotraumaShared/SharedSource/Map/Submarine.cs b/Barotrauma/BarotraumaShared/SharedSource/Map/Submarine.cs index 2bba31f2e6..da7483a787 100644 --- a/Barotrauma/BarotraumaShared/SharedSource/Map/Submarine.cs +++ b/Barotrauma/BarotraumaShared/SharedSource/Map/Submarine.cs @@ -892,6 +892,8 @@ public static Body PickBody(Vector2 rayStart, Vector2 rayEnd, IEnumerable return null; } + if (GameMain.World == null) return null; + float closestFraction = 1.0f; Vector2 closestNormal = Vector2.Zero; Fixture closestFixture = null; @@ -899,19 +901,28 @@ public static Body PickBody(Vector2 rayStart, Vector2 rayEnd, IEnumerable if (allowInsideFixture) { var aabb = new FarseerPhysics.Collision.AABB(rayStart - Vector2.One * 0.001f, rayStart + Vector2.One * 0.001f); - GameMain.World.QueryAABB((fixture) => + try { - if (!CheckFixtureCollision(fixture, ignoredBodies, collisionCategory, ignoreSensors, customPredicate)) { return true; } + GameMain.World.QueryAABB((fixture) => + { + if (fixture == null || fixture.Body == null) { return true; } + + if (!CheckFixtureCollision(fixture, ignoredBodies, collisionCategory, ignoreSensors, customPredicate)) { return true; } - fixture.Body.GetTransform(out FarseerPhysics.Common.Transform transform); - if (!fixture.Shape.TestPoint(ref transform, ref rayStart)) { return true; } + fixture.Body.GetTransform(out FarseerPhysics.Common.Transform transform); + if (!fixture.Shape.TestPoint(ref transform, ref rayStart)) { return true; } - closestFraction = 0.0f; - closestNormal = Vector2.Normalize(rayEnd - rayStart); - closestFixture = fixture; - if (fixture.Body != null) { closestBody = fixture.Body; } - return false; - }, ref aabb); + closestFraction = 0.0f; + closestNormal = Vector2.Normalize(rayEnd - rayStart); + closestFixture = fixture; + if (fixture.Body != null) { closestBody = fixture.Body; } + return false; + }, ref aabb); + } + catch (NullReferenceException) + { + return null; + } if (closestFraction <= 0.0f) { lastPickedPositionLocal.Value = rayStart; @@ -2293,6 +2304,7 @@ private void GenerateOutdoorNodes() /// public void DisableObstructedWayPoints() { + // Check collisions to level foreach (var node in OutdoorNodes) { @@ -2321,7 +2333,9 @@ public void DisableObstructedWayPoints() /// public void DisableObstructedWayPoints(Submarine otherSub) { - if (otherSub == null) { return; } + + if (otherSub?.PhysicsBody?.FarseerBody == null) return; + if (otherSub == this) { return; } // Check collisions to other subs. foreach (var node in OutdoorNodes) @@ -2338,14 +2352,21 @@ public void DisableObstructedWayPoints(Submarine otherSub) { Vector2 start = ConvertUnits.ToSimUnits(wp.WorldPosition) - otherSub.SimPosition; Vector2 end = ConvertUnits.ToSimUnits(connectedWp.WorldPosition) - otherSub.SimPosition; - var body = PickBody(start, end, null, Physics.CollisionWall, allowInsideFixture: true); - if (body != null) + try { - if (body.UserData is Structure wall && !wall.IsPlatform || body.UserData is Item && body.FixtureList[0].CollisionCategories.HasFlag(Physics.CollisionWall)) + var body = PickBody(start, end, null, Physics.CollisionWall, allowInsideFixture: true); + if (body != null) { - isObstructed = true; + if (body.UserData is Structure wall && !wall.IsPlatform || body.UserData is Item && body.FixtureList?[0].CollisionCategories.HasFlag(Physics.CollisionWall) == true) + { + isObstructed = true; + } } } + catch (NullReferenceException) + { + continue; + } } if (isObstructed) { From caec44c57d9e6303ada2529528a36c52523f4629 Mon Sep 17 00:00:00 2001 From: Eero Date: Thu, 8 Jan 2026 00:26:29 +0800 Subject: [PATCH 06/10] Fix concurrent access issues with ConnectedClients Replaced direct access to GameMain.Server.ConnectedClients with array snapshots in multiple server-side classes to prevent concurrent modification issues during parallel updates. Also updated PhysicsBody and LevelTrigger to avoid static/shared state in parallel contexts, improving thread safety and reliability. --- .../ServerSource/Characters/Character.cs | 11 ++-- .../Characters/CharacterNetworking.cs | 8 ++- .../Events/EventActions/ConversationAction.cs | 14 ++-- .../Events/EventActions/EventLogAction.cs | 64 ++++++++++--------- .../EventActions/EventObjectiveAction.cs | 11 +++- .../Events/EventActions/HighlightAction.cs | 4 +- .../Events/EventActions/MissionAction.cs | 9 ++- .../Events/EventActions/StatusEffectAction.cs | 2 +- .../Items/Components/Signal/CircuitBox.cs | 6 +- .../BarotraumaServer/ServerSource/Map/Hull.cs | 4 +- .../ServerSource/Networking/GameServer.cs | 8 ++- .../Map/Levels/LevelObjects/LevelTrigger.cs | 10 ++- .../SharedSource/Physics/PhysicsBody.cs | 12 ++++ 13 files changed, 106 insertions(+), 57 deletions(-) diff --git a/Barotrauma/BarotraumaServer/ServerSource/Characters/Character.cs b/Barotrauma/BarotraumaServer/ServerSource/Characters/Character.cs index 6559594ec7..4e3f0c5ecf 100644 --- a/Barotrauma/BarotraumaServer/ServerSource/Characters/Character.cs +++ b/Barotrauma/BarotraumaServer/ServerSource/Characters/Character.cs @@ -28,11 +28,14 @@ partial void KillProjSpecific(CauseOfDeathType causeOfDeath, Affliction causeOfD } } + // Create snapshot to avoid concurrent access issues during parallel updates + var clients = GameMain.Server.ConnectedClients.ToArray(); + if (GameMain.Server is { ServerSettings.RespawnMode: RespawnMode.Permadeath } && GameMain.GameSession?.Campaign is MultiPlayerCampaign mpCampaign && causeOfDeath != CauseOfDeathType.Disconnected) { - Client ownerClient = GameMain.Server.ConnectedClients.FirstOrDefault(c => c.Character == this); + Client ownerClient = clients.FirstOrDefault(c => c.Character == this); if (ownerClient != null) { ownerClient.SpectateOnly = true; @@ -51,7 +54,7 @@ partial void KillProjSpecific(CauseOfDeathType causeOfDeath, Affliction causeOfD if (HasAbilityFlag(AbilityFlags.RetainExperienceForNewCharacter)) { - var ownerClient = GameMain.Server.ConnectedClients.Find(c => c.Character == this); + var ownerClient = clients.FirstOrDefault(c => c.Character == this); if (ownerClient != null) { (GameMain.GameSession?.GameMode as MultiPlayerCampaign)?.SaveExperiencePoints(ownerClient); @@ -62,7 +65,7 @@ partial void KillProjSpecific(CauseOfDeathType causeOfDeath, Affliction causeOfD if (CauseOfDeath.Killer != null && CauseOfDeath.Killer.IsTraitor && CauseOfDeath.Killer != this) { - var owner = GameMain.Server.ConnectedClients.Find(c => c.Character == this); + var owner = clients.FirstOrDefault(c => c.Character == this); if (owner != null) { if (!GameMain.LuaCs.Game.overrideTraitors) @@ -71,7 +74,7 @@ partial void KillProjSpecific(CauseOfDeathType causeOfDeath, Affliction causeOfD } } } - foreach (Client client in GameMain.Server.ConnectedClients) + foreach (Client client in clients) { if (client.InGame) { diff --git a/Barotrauma/BarotraumaServer/ServerSource/Characters/CharacterNetworking.cs b/Barotrauma/BarotraumaServer/ServerSource/Characters/CharacterNetworking.cs index 59c91a4a5d..686a10ddad 100644 --- a/Barotrauma/BarotraumaServer/ServerSource/Characters/CharacterNetworking.cs +++ b/Barotrauma/BarotraumaServer/ServerSource/Characters/CharacterNetworking.cs @@ -490,7 +490,9 @@ public virtual void ServerEventWrite(IWriteMessage msg, Client c, NetEntityEvent case ControlEventData controlEventData: Client owner = controlEventData.Owner; msg.WriteBoolean(owner == c && owner.Character == this); - msg.WriteByte(owner != null && owner.Character == this && GameMain.Server.ConnectedClients.Contains(owner) ? owner.SessionId : (byte)0); + // Create snapshot to avoid concurrent access issues during parallel updates + var connectedClients = GameMain.Server.ConnectedClients.ToArray(); + msg.WriteByte(owner != null && owner.Character == this && connectedClients.Contains(owner) ? owner.SessionId : (byte)0); msg.WriteBoolean(info is { RenamingEnabled: true }); break; case CharacterStatusEventData statusEventData: @@ -746,7 +748,9 @@ public void WriteSpawnData(IWriteMessage msg, UInt16 entityId, bool restrictMess return; } - Client ownerClient = GameMain.Server.ConnectedClients.Find(c => c.Character == this && (!c.SpectateOnly || !GameMain.Server.ServerSettings.AllowSpectating)); + // Create snapshot to avoid concurrent access issues during parallel updates + var clients = GameMain.Server.ConnectedClients.ToArray(); + Client ownerClient = clients.FirstOrDefault(c => c.Character == this && (!c.SpectateOnly || !GameMain.Server.ServerSettings.AllowSpectating)); if (ownerClient != null) { msg.WriteBoolean(true); diff --git a/Barotrauma/BarotraumaServer/ServerSource/Events/EventActions/ConversationAction.cs b/Barotrauma/BarotraumaServer/ServerSource/Events/EventActions/ConversationAction.cs index 159c833108..27e63da4f5 100644 --- a/Barotrauma/BarotraumaServer/ServerSource/Events/EventActions/ConversationAction.cs +++ b/Barotrauma/BarotraumaServer/ServerSource/Events/EventActions/ConversationAction.cs @@ -82,10 +82,13 @@ public void IgnoreClient(Client c, float seconds) private bool IsBlockedByAnotherConversation(IEnumerable targets, float duration) { + // Create snapshot to avoid concurrent access issues during parallel updates + var clients = GameMain.Server.ConnectedClients.ToArray(); + if (targets == null || targets.None()) { //if the action doesn't target anyone in specific, it's shown to every client - foreach (var client in GameMain.Server.ConnectedClients) + foreach (var client in clients) { if (IsBlockedByAnotherConversation(client, duration)) { return true; } } @@ -95,7 +98,7 @@ private bool IsBlockedByAnotherConversation(IEnumerable targets, float d foreach (Entity e in targets) { if (e is not Character character || !character.IsRemotePlayer) { continue; } - Client targetClient = GameMain.Server.ConnectedClients.Find(c => c.Character == character); + Client targetClient = clients.FirstOrDefault(c => c.Character == character); if (targetClient != null && IsBlockedByAnotherConversation(targetClient, duration)) { return true; } } } @@ -117,13 +120,16 @@ private bool IsBlockedByAnotherConversation(Client targetClient, float duration) partial void ShowDialog(Character speaker, Character targetCharacter) { targetClients.Clear(); + // Create snapshot to avoid concurrent access issues during parallel updates + var clients = GameMain.Server.ConnectedClients.ToArray(); + if (!TargetTag.IsEmpty) { IEnumerable entities = ParentEvent.GetTargets(TargetTag); foreach (Entity e in entities) { if (e is not Character character || !character.IsRemotePlayer) { continue; } - Client targetClient = GameMain.Server.ConnectedClients.Find(c => c.Character == character); + Client targetClient = clients.FirstOrDefault(c => c.Character == character); if (targetClient != null) { targetClients.Add(targetClient); @@ -135,7 +141,7 @@ partial void ShowDialog(Character speaker, Character targetCharacter) } else { - foreach (Client c in GameMain.Server.ConnectedClients) + foreach (Client c in clients) { if (CanClientReceive(c)) { diff --git a/Barotrauma/BarotraumaServer/ServerSource/Events/EventActions/EventLogAction.cs b/Barotrauma/BarotraumaServer/ServerSource/Events/EventActions/EventLogAction.cs index b6dac422eb..ac005dcadc 100644 --- a/Barotrauma/BarotraumaServer/ServerSource/Events/EventActions/EventLogAction.cs +++ b/Barotrauma/BarotraumaServer/ServerSource/Events/EventActions/EventLogAction.cs @@ -9,48 +9,52 @@ namespace Barotrauma; partial class EventLogAction : EventAction { - partial void AddEntryProjSpecific(EventLog? eventLog, string displayText) +partial void AddEntryProjSpecific(EventLog? eventLog, string displayText) +{ + if (eventLog == null) { return; } + + // Create snapshot to avoid concurrent access issues during parallel updates + var clients = GameMain.Server.ConnectedClients.ToArray(); + + if (!TargetTag.IsEmpty) { - if (eventLog == null) { return; } - if (!TargetTag.IsEmpty) + List targetClients = new List(); + foreach (var target in ParentEvent.GetTargets(TargetTag)) { - List targetClients = new List(); - foreach (var target in ParentEvent.GetTargets(TargetTag)) + if (target is Character character) { - if (target is Character character) - { - var ownerClient = GameMain.Server.ConnectedClients.Find(c => c.Character == character); - if (ownerClient != null) - { - targetClients.Add(ownerClient); - } - } - else + var ownerClient = clients.FirstOrDefault(c => c.Character == character); + if (ownerClient != null) { - DebugConsole.AddWarning($"{target} is not a valid target for an EventLogAction. The target should be a character.", - ParentEvent.Prefab.ContentPackage); + targetClients.Add(ownerClient); } } - if (eventLog!.TryAddEntry(ParentEvent.Prefab.Identifier, Id, displayText, targetClients) && ShowInServerLog) + else { - Log(targetClients); + DebugConsole.AddWarning($"{target} is not a valid target for an EventLogAction. The target should be a character.", + ParentEvent.Prefab.ContentPackage); } } - else + if (eventLog!.TryAddEntry(ParentEvent.Prefab.Identifier, Id, displayText, targetClients) && ShowInServerLog) { - if (eventLog.TryAddEntry(ParentEvent.Prefab.Identifier, Id, displayText, GameMain.Server.ConnectedClients) && ShowInServerLog) - { - Log(targetClients: null); - } + Log(targetClients); } - - void Log(List? targetClients) + } + else + { + if (eventLog.TryAddEntry(ParentEvent.Prefab.Identifier, Id, displayText, clients) && ShowInServerLog) { - string clientStr = targetClients == null || targetClients.None() ? - string.Empty : - $" ({string.Join(", ", targetClients.Select(c => NetworkMember.ClientLogName(c)))})"; - GameServer.Log($"Event \"{ParentEvent.Prefab.Name}\"{clientStr}: " + displayText, - ParentEvent is TraitorEvent ? ServerLog.MessageType.Traitors : ServerLog.MessageType.Chat); + Log(targetClients: null); } } + + void Log(List? targetClients) + { + string clientStr = targetClients == null || targetClients.None() ? + string.Empty : + $" ({string.Join(", ", targetClients.Select(c => NetworkMember.ClientLogName(c)))})"; + GameServer.Log($"Event \"{ParentEvent.Prefab.Name}\"{clientStr}: " + displayText, + ParentEvent is TraitorEvent ? ServerLog.MessageType.Traitors : ServerLog.MessageType.Chat); + } +} } \ No newline at end of file diff --git a/Barotrauma/BarotraumaServer/ServerSource/Events/EventActions/EventObjectiveAction.cs b/Barotrauma/BarotraumaServer/ServerSource/Events/EventActions/EventObjectiveAction.cs index 9b84d31dd5..ade58531b6 100644 --- a/Barotrauma/BarotraumaServer/ServerSource/Events/EventActions/EventObjectiveAction.cs +++ b/Barotrauma/BarotraumaServer/ServerSource/Events/EventActions/EventObjectiveAction.cs @@ -1,3 +1,5 @@ +using System.Linq; + namespace Barotrauma { partial class EventObjectiveAction : EventAction @@ -13,9 +15,12 @@ partial void UpdateProjSpecific() ParentObjectiveId, CanBeCompleted); + // Create snapshot to avoid concurrent access issues during parallel updates + var clients = GameMain.Server.ConnectedClients.ToArray(); + if (TargetTag.IsEmpty) { - foreach (var client in GameMain.Server.ConnectedClients) + foreach (var client in clients) { if (client.Character == null) { continue; } EventManager.ServerWriteObjective(client, objective); @@ -26,11 +31,11 @@ partial void UpdateProjSpecific() foreach (var target in ParentEvent.GetTargets(TargetTag)) { if (target is not Character character) { continue; } - var ownerClient = GameMain.Server.ConnectedClients.Find(c => c.Character == character); + var ownerClient = clients.FirstOrDefault(c => c.Character == character); if (ownerClient == null) { continue; } EventManager.ServerWriteObjective(ownerClient, objective); } } } } -} \ No newline at end of file +} diff --git a/Barotrauma/BarotraumaServer/ServerSource/Events/EventActions/HighlightAction.cs b/Barotrauma/BarotraumaServer/ServerSource/Events/EventActions/HighlightAction.cs index 98c06c3f9d..72f06fbc6e 100644 --- a/Barotrauma/BarotraumaServer/ServerSource/Events/EventActions/HighlightAction.cs +++ b/Barotrauma/BarotraumaServer/ServerSource/Events/EventActions/HighlightAction.cs @@ -14,8 +14,10 @@ partial void SetHighlightProjSpecific(Entity entity, IEnumerable? tar IEnumerable? targetClients = null; if (targetCharacters != null) { + // Create snapshot to avoid concurrent access issues during parallel updates + var clients = GameMain.Server.ConnectedClients.ToArray(); targetClients = targetCharacters - .Select(c => GameMain.Server.ConnectedClients.FirstOrDefault(client => client.Character == c)) + .Select(c => clients.FirstOrDefault(client => client.Character == c)) .Where(c => c != null)!; } GameMain.Server?.CreateEntityEvent(item, new Item.SetHighlightEventData(State, highlightColor, targetClients)); diff --git a/Barotrauma/BarotraumaServer/ServerSource/Events/EventActions/MissionAction.cs b/Barotrauma/BarotraumaServer/ServerSource/Events/EventActions/MissionAction.cs index 5369ec7e13..3025cb7d5d 100644 --- a/Barotrauma/BarotraumaServer/ServerSource/Events/EventActions/MissionAction.cs +++ b/Barotrauma/BarotraumaServer/ServerSource/Events/EventActions/MissionAction.cs @@ -1,5 +1,6 @@ -using Barotrauma.Networking; +using Barotrauma.Networking; using System.Collections.Generic; +using System.Linq; namespace Barotrauma { @@ -22,7 +23,9 @@ public static void NotifyMissionsUnlockedThisRound(Client client) private static void NotifyMissionUnlock(Mission mission) { - foreach (Client client in GameMain.Server.ConnectedClients) + // Create snapshot to avoid concurrent access issues during parallel updates + var clients = GameMain.Server.ConnectedClients.ToArray(); + foreach (Client client in clients) { NotifyMissionUnlock(mission, client); } @@ -40,4 +43,4 @@ private static void NotifyMissionUnlock(Mission mission, Client client) GameMain.Server.ServerPeer.Send(outmsg, client.Connection, DeliveryMethod.Reliable); } } -} \ No newline at end of file +} diff --git a/Barotrauma/BarotraumaServer/ServerSource/Events/EventActions/StatusEffectAction.cs b/Barotrauma/BarotraumaServer/ServerSource/Events/EventActions/StatusEffectAction.cs index a8119efe2c..c72bc512f2 100644 --- a/Barotrauma/BarotraumaServer/ServerSource/Events/EventActions/StatusEffectAction.cs +++ b/Barotrauma/BarotraumaServer/ServerSource/Events/EventActions/StatusEffectAction.cs @@ -25,4 +25,4 @@ private void ServerWrite(IEnumerable targets) } } } -} \ No newline at end of file +} diff --git a/Barotrauma/BarotraumaServer/ServerSource/Items/Components/Signal/CircuitBox.cs b/Barotrauma/BarotraumaServer/ServerSource/Items/Components/Signal/CircuitBox.cs index 9cc01bcbc4..f54733a61c 100644 --- a/Barotrauma/BarotraumaServer/ServerSource/Items/Components/Signal/CircuitBox.cs +++ b/Barotrauma/BarotraumaServer/ServerSource/Items/Components/Signal/CircuitBox.cs @@ -76,7 +76,9 @@ public void SendToAll(CircuitBoxOpcode opcode, INetSerializableStruct data, Func { var (msg, deliveryMethod) = PrepareToSend(opcode, data); - foreach (Client client in GameMain.Server.ConnectedClients) + // Create snapshot to avoid concurrent access issues during parallel updates + var clients = GameMain.Server.ConnectedClients.ToArray(); + foreach (Client client in clients) { if (predicate is not null && !predicate(client)) { continue; } @@ -391,4 +393,4 @@ private void BroadcastSelectionStatus() CreateServerEvent(new CircuitBoxServerUpdateSelection(nodes, wires, ios, labels)); } } -} \ No newline at end of file +} diff --git a/Barotrauma/BarotraumaServer/ServerSource/Map/Hull.cs b/Barotrauma/BarotraumaServer/ServerSource/Map/Hull.cs index cfb0592cdd..1fa02b024c 100644 --- a/Barotrauma/BarotraumaServer/ServerSource/Map/Hull.cs +++ b/Barotrauma/BarotraumaServer/ServerSource/Map/Hull.cs @@ -29,7 +29,9 @@ partial void UpdateProjSpecific(float deltaTime, Camera cam) //don't create updates if all clients are very far from the hull float hullUpdateDistanceSqr = NetConfig.HullUpdateDistance * NetConfig.HullUpdateDistance; - if (!GameMain.Server.ConnectedClients.Any(c => + // Create snapshot to avoid concurrent access issues during parallel updates + var clients = GameMain.Server.ConnectedClients.ToArray(); + if (!clients.Any(c => (c.Character != null && Vector2.DistanceSquared(c.Character.WorldPosition, WorldPosition) < hullUpdateDistanceSqr) || (c.SpectatePos != null && Vector2.DistanceSquared(c.SpectatePos.Value, WorldPosition) < hullUpdateDistanceSqr)) ) { diff --git a/Barotrauma/BarotraumaServer/ServerSource/Networking/GameServer.cs b/Barotrauma/BarotraumaServer/ServerSource/Networking/GameServer.cs index 26eb6b890f..e0ad602a0e 100644 --- a/Barotrauma/BarotraumaServer/ServerSource/Networking/GameServer.cs +++ b/Barotrauma/BarotraumaServer/ServerSource/Networking/GameServer.cs @@ -4799,7 +4799,9 @@ public void UpdateMissionState(Mission mission) public static string CharacterLogName(Character character) { if (character == null) { return "[NULL]"; } - Client client = GameMain.Server.ConnectedClients.Find(c => c.Character == character); + // Create snapshot to avoid concurrent access issues during parallel updates + var clients = GameMain.Server.ConnectedClients.ToArray(); + Client client = clients.FirstOrDefault(c => c.Character == character); return ClientLogName(client, character.LogName); } @@ -4810,8 +4812,8 @@ public static void Log(string line, ServerLog.MessageType messageType) GameMain.LuaCs?.Hook?.Call("serverLog", line, messageType); GameMain.Server.ServerSettings.ServerLog.WriteLine(line, messageType); - - foreach (Client client in GameMain.Server.ConnectedClients) + var clients = GameMain.Server.ConnectedClients.ToArray(); + foreach (Client client in clients) { if (!client.HasPermission(ClientPermissions.ServerLog)) continue; //use sendername as the message type diff --git a/Barotrauma/BarotraumaShared/SharedSource/Map/Levels/LevelObjects/LevelTrigger.cs b/Barotrauma/BarotraumaShared/SharedSource/Map/Levels/LevelObjects/LevelTrigger.cs index 261a217064..4731c255a7 100644 --- a/Barotrauma/BarotraumaShared/SharedSource/Map/Levels/LevelObjects/LevelTrigger.cs +++ b/Barotrauma/BarotraumaShared/SharedSource/Map/Levels/LevelObjects/LevelTrigger.cs @@ -673,13 +673,17 @@ public void Update(float deltaTime) } } - private static readonly List triggerersToRemove = new List(); public static void RemoveInActiveTriggerers(PhysicsBody physicsBody, HashSet triggerers) { if (physicsBody == null) { return; } - triggerersToRemove.Clear(); - foreach (var triggerer in triggerers) + // Use local list instead of static field to avoid concurrent access issues during parallel updates + var triggerersToRemove = new List(); + + // Create snapshot to avoid concurrent modification during enumeration + var triggererSnapshot = triggerers.ToArray(); + + foreach (var triggerer in triggererSnapshot) { if (triggerer.Removed) { diff --git a/Barotrauma/BarotraumaShared/SharedSource/Physics/PhysicsBody.cs b/Barotrauma/BarotraumaShared/SharedSource/Physics/PhysicsBody.cs index cfeac247b2..2cb61b2a04 100644 --- a/Barotrauma/BarotraumaShared/SharedSource/Physics/PhysicsBody.cs +++ b/Barotrauma/BarotraumaShared/SharedSource/Physics/PhysicsBody.cs @@ -834,6 +834,12 @@ public bool SetTransform(Vector2 simPosition, float rotation, bool setPrevTransf if (!IsValidValue(simPosition, "position", -1e10f, 1e10f)) { return false; } if (!IsValidValue(rotation, "rotation")) { return false; } + if (PhysicsBodyQueue.IsInParallelContext) + { + PhysicsBodyQueue.Enqueue(() => SetTransform(simPosition, rotation, setPrevTransform)); + return true; + } + FarseerBody.SetTransform(simPosition, rotation); if (setPrevTransform) { SetPrevTransform(simPosition, rotation); } return true; @@ -848,6 +854,12 @@ public bool SetTransformIgnoreContacts(Vector2 simPosition, float rotation, bool if (!IsValidValue(simPosition, "position", -1e10f, 1e10f)) { return false; } if (!IsValidValue(rotation, "rotation")) { return false; } + if (PhysicsBodyQueue.IsInParallelContext) + { + PhysicsBodyQueue.Enqueue(() => SetTransformIgnoreContacts(simPosition, rotation, setPrevTransform)); + return true; + } + FarseerBody.SetTransformIgnoreContacts(ref simPosition, rotation); if (setPrevTransform) { SetPrevTransform(simPosition, rotation); } return true; From e7e444e9b21c5a881ee8dab764559663e80ce55d Mon Sep 17 00:00:00 2001 From: NotAlwaysTrue <2136846186@qq.com> Date: Fri, 9 Jan 2026 18:09:49 +0800 Subject: [PATCH 07/10] Fixed multiple LINQ using shared resources and cause crashes Added an null check in AIObjectiveManager.cs to avoid accessing removed resources Use shuffledGaps instead of gapList to ensure update order requirement(already in master) Updated parallelism count --- .../Characters/AI/HumanAIController.cs | 12 ++++-- .../AI/Objectives/AIObjectiveManager.cs | 2 +- .../Items/Components/Signal/Connection.cs | 3 +- .../SharedSource/Map/MapEntity.cs | 2 +- .../SharedSource/Screens/GameScreen.cs | 41 +++++-------------- 5 files changed, 24 insertions(+), 36 deletions(-) diff --git a/Barotrauma/BarotraumaShared/SharedSource/Characters/AI/HumanAIController.cs b/Barotrauma/BarotraumaShared/SharedSource/Characters/AI/HumanAIController.cs index 4479bdb81d..e1eb0c40e7 100644 --- a/Barotrauma/BarotraumaShared/SharedSource/Characters/AI/HumanAIController.cs +++ b/Barotrauma/BarotraumaShared/SharedSource/Characters/AI/HumanAIController.cs @@ -382,8 +382,13 @@ bool IsCloseEnoughToTarget(float threshold, bool targetSub = true) } steeringBuffer = Math.Clamp(steeringBuffer, minSteeringBuffer, maxSteeringBuffer); - AnimController.Crouching = shouldCrouch; - CheckCrouching(deltaTime); + // in case of somehow AnimController was a null, eg. something removed AnimController in the middle of an update + if (AnimController != null) + { + AnimController.Crouching = shouldCrouch; + CheckCrouching(deltaTime); + } + Character.ClearInputs(); if (SortTimer > 0.0f) @@ -1638,7 +1643,8 @@ public void AddCombatObjective(AIObjectiveCombat.CombatMode mode, Character targ if (mode == AIObjectiveCombat.CombatMode.None) { return; } if (Character.IsDead || Character.IsIncapacitated || Character.Removed) { return; } if (!Character.IsBot) { return; } - if (ObjectiveManager.Objectives.FirstOrDefault(o => o is AIObjectiveCombat) is AIObjectiveCombat combatObjective) + List ObjectivesLocal = ObjectiveManager.Objectives; + if (ObjectivesLocal.FirstOrDefault(o => o is AIObjectiveCombat) is AIObjectiveCombat combatObjective) { // Don't replace offensive mode with something else if (combatObjective.Mode == AIObjectiveCombat.CombatMode.Offensive && mode != AIObjectiveCombat.CombatMode.Offensive) { return; } diff --git a/Barotrauma/BarotraumaShared/SharedSource/Characters/AI/Objectives/AIObjectiveManager.cs b/Barotrauma/BarotraumaShared/SharedSource/Characters/AI/Objectives/AIObjectiveManager.cs index 83e475c370..ead5b6a873 100644 --- a/Barotrauma/BarotraumaShared/SharedSource/Characters/AI/Objectives/AIObjectiveManager.cs +++ b/Barotrauma/BarotraumaShared/SharedSource/Characters/AI/Objectives/AIObjectiveManager.cs @@ -126,7 +126,7 @@ public void AddObjective(T objective) where T : AIObjective } else { - Objectives.RemoveAll(o => o.GetType() == type); + Objectives.RemoveAll(o => o?.GetType() == type); } Objectives.Add(objective); } diff --git a/Barotrauma/BarotraumaShared/SharedSource/Items/Components/Signal/Connection.cs b/Barotrauma/BarotraumaShared/SharedSource/Items/Components/Signal/Connection.cs index d551502376..37cb33d9c8 100644 --- a/Barotrauma/BarotraumaShared/SharedSource/Items/Components/Signal/Connection.cs +++ b/Barotrauma/BarotraumaShared/SharedSource/Items/Components/Signal/Connection.cs @@ -345,7 +345,8 @@ public void SendSignal(Signal signal) { Connection recipient = wire.OtherConnection(this); if (recipient == null) { continue; } - if (recipient.item == this.item || signal.source?.LastSentSignalRecipients.LastOrDefault() == recipient) { continue; } + List LastSentSignalRecipientsCopy = signal.source?.LastSentSignalRecipients.ToList(); + if (recipient.item == this.item || LastSentSignalRecipientsCopy.LastOrDefault() == recipient) { continue; } signal.source?.LastSentSignalRecipients.Add(recipient); #if CLIENT diff --git a/Barotrauma/BarotraumaShared/SharedSource/Map/MapEntity.cs b/Barotrauma/BarotraumaShared/SharedSource/Map/MapEntity.cs index b61d2e68ef..fb37239343 100644 --- a/Barotrauma/BarotraumaShared/SharedSource/Map/MapEntity.cs +++ b/Barotrauma/BarotraumaShared/SharedSource/Map/MapEntity.cs @@ -816,7 +816,7 @@ public static void UpdateAll(float deltaTime, Camera cam , ParallelOptions paral // Gap update (has order dependencies, keep random order but execute sequentially) var shuffledGaps = gapList.OrderBy(g => Rand.Int(int.MaxValue)).ToList(); - Parallel.ForEach(gapList, parallelOptions, gap => + Parallel.ForEach(shuffledGaps, parallelOptions, gap => { PhysicsBodyQueue.IsInParallelContext = true; try diff --git a/Barotrauma/BarotraumaShared/SharedSource/Screens/GameScreen.cs b/Barotrauma/BarotraumaShared/SharedSource/Screens/GameScreen.cs index e2fddef7ce..be3e4bb124 100644 --- a/Barotrauma/BarotraumaShared/SharedSource/Screens/GameScreen.cs +++ b/Barotrauma/BarotraumaShared/SharedSource/Screens/GameScreen.cs @@ -25,7 +25,7 @@ partial class GameScreen : Screen private static readonly ParallelOptions parallelOptions = new ParallelOptions { - MaxDegreeOfParallelism = Environment.ProcessorCount * 2, + MaxDegreeOfParallelism = Math.Max(4,Environment.ProcessorCount - 1), }; #if CLIENT @@ -259,33 +259,15 @@ public override void Update(double deltaTime) Character.Controlled?.UpdateLocalCursor(cam); #elif SERVER - Parallel.Invoke(parallelOptions, - () => - { - 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; - } - } - ); - + // DO NOT PARALLELIZE THESE TWO OR IT MAY STUCK HERE + // SO FOLLOW THE ORIGINAL SINGLE-THREAD LOGIC STRICTLY + + if (Level.Loaded != null) Level.Loaded.Update((float)deltaTime, Camera.Instance); + + Character.UpdateAll((float)deltaTime, Camera.Instance); + + StatusEffect.UpdateAll((float)deltaTime); + PhysicsBodyQueue.ProcessPendingOperations(); #endif @@ -310,8 +292,6 @@ public override void Update(double deltaTime) MapEntity.UpdateAll((float)deltaTime, Camera.Instance, parallelOptions); - StatusEffect.UpdateAll((float)deltaTime); - #endif #if CLIENT @@ -321,6 +301,7 @@ public override void Update(double deltaTime) #endif //Character.UpdateAnimAll is not thread-safe and must be executed on the main thread Character.UpdateAnimAll((float)deltaTime); + PhysicsBodyQueue.ProcessPendingOperations(); #if CLIENT Ragdoll.UpdateAll((float)deltaTime, cam); From e24024cbb2b306c052fe56e1ea6a8ca408b36441 Mon Sep 17 00:00:00 2001 From: NotAlwaysTrue <2136846186@qq.com> Date: Fri, 16 Jan 2026 17:15:57 +0800 Subject: [PATCH 08/10] Fixed #41/2 Removed min hard cap for MaxDegreeOfParallelism --- .../SharedSource/Screens/GameScreen.cs | 2 +- .../Collision/DynamicTree.cs | 2 +- .../Dynamics/ContactManager.cs | 11 +++++++++-- .../Dynamics/Contacts/Contact.cs | 4 ++-- .../Farseer Physics Engine 3.5/Dynamics/World.cs | 2 +- 5 files changed, 14 insertions(+), 7 deletions(-) diff --git a/Barotrauma/BarotraumaShared/SharedSource/Screens/GameScreen.cs b/Barotrauma/BarotraumaShared/SharedSource/Screens/GameScreen.cs index be3e4bb124..3dc8dfac26 100644 --- a/Barotrauma/BarotraumaShared/SharedSource/Screens/GameScreen.cs +++ b/Barotrauma/BarotraumaShared/SharedSource/Screens/GameScreen.cs @@ -25,7 +25,7 @@ partial class GameScreen : Screen private static readonly ParallelOptions parallelOptions = new ParallelOptions { - MaxDegreeOfParallelism = Math.Max(4,Environment.ProcessorCount - 1), + MaxDegreeOfParallelism = Math.Max(1,Environment.ProcessorCount - 1), }; #if CLIENT diff --git a/Libraries/Farseer Physics Engine 3.5/Collision/DynamicTree.cs b/Libraries/Farseer Physics Engine 3.5/Collision/DynamicTree.cs index 6bc690f097..9753764d61 100644 --- a/Libraries/Farseer Physics Engine 3.5/Collision/DynamicTree.cs +++ b/Libraries/Farseer Physics Engine 3.5/Collision/DynamicTree.cs @@ -1145,4 +1145,4 @@ public void ShiftOrigin(Vector2 newOrigin) } } } -} \ No newline at end of file +} diff --git a/Libraries/Farseer Physics Engine 3.5/Dynamics/ContactManager.cs b/Libraries/Farseer Physics Engine 3.5/Dynamics/ContactManager.cs index 1c47b8c055..8c7d9bd9ea 100644 --- a/Libraries/Farseer Physics Engine 3.5/Dynamics/ContactManager.cs +++ b/Libraries/Farseer Physics Engine 3.5/Dynamics/ContactManager.cs @@ -27,9 +27,10 @@ * 3. This notice may not be removed or altered from any source distribution. */ -using System.Collections.Generic; using FarseerPhysics.Collision; using FarseerPhysics.Dynamics.Contacts; +using System.Collections.Generic; +using System.Threading; namespace FarseerPhysics.Dynamics { @@ -61,6 +62,8 @@ public class ContactManager public int CollideMultithreadThreshold = 64; #endregion + // This will ensure only one thread will work on ContactList so that we wont mess up these stuff + private readonly SemaphoreSlim contactManagerSignal = new SemaphoreSlim(1); /// /// Fires when a contact is created @@ -249,6 +252,8 @@ internal void FindNewContacts() internal void Destroy(Contact contact) { + contactManagerSignal.Wait(100); + Fixture fixtureA = contact.FixtureA; Fixture fixtureB = contact.FixtureB; Body bodyA = fixtureA.Body; @@ -310,6 +315,8 @@ internal void Destroy(Contact contact) // Insert into the pool. contact.Next = _contactPoolList.Next; _contactPoolList.Next = contact; + + contactManagerSignal.Release(); } internal void Collide() @@ -610,4 +617,4 @@ internal void UpdateActiveContacts(ContactEdge ContactList, bool value) } #endif } -} \ No newline at end of file +} diff --git a/Libraries/Farseer Physics Engine 3.5/Dynamics/Contacts/Contact.cs b/Libraries/Farseer Physics Engine 3.5/Dynamics/Contacts/Contact.cs index c79e46edd4..6e2eaaaa04 100644 --- a/Libraries/Farseer Physics Engine 3.5/Dynamics/Contacts/Contact.cs +++ b/Libraries/Farseer Physics Engine 3.5/Dynamics/Contacts/Contact.cs @@ -1,4 +1,4 @@ -// Copyright (c) 2017 Kastellanos Nikolaos +// Copyright (c) 2017 Kastellanos Nikolaos /* Original source Farseer Physics Engine: * Copyright (c) 2014 Ian Qvist, http://farseerphysics.codeplex.com @@ -496,4 +496,4 @@ private enum ContactType #endregion } -} \ No newline at end of file +} diff --git a/Libraries/Farseer Physics Engine 3.5/Dynamics/World.cs b/Libraries/Farseer Physics Engine 3.5/Dynamics/World.cs index 0aa2ef49aa..f64765ad9c 100644 --- a/Libraries/Farseer Physics Engine 3.5/Dynamics/World.cs +++ b/Libraries/Farseer Physics Engine 3.5/Dynamics/World.cs @@ -1726,4 +1726,4 @@ public void Clear() } } -} \ No newline at end of file +} From 086f45510fdbea4feb060e1e52e302c1ae4313cf Mon Sep 17 00:00:00 2001 From: NotAlwaysTrue <2136846186@qq.com> Date: Fri, 16 Jan 2026 17:21:39 +0800 Subject: [PATCH 09/10] Removed PF support on Character --- .../SharedSource/Characters/Character.cs | 16 ++-------------- .../SharedSource/Map/MapEntity.cs | 3 +-- 2 files changed, 3 insertions(+), 16 deletions(-) diff --git a/Barotrauma/BarotraumaShared/SharedSource/Characters/Character.cs b/Barotrauma/BarotraumaShared/SharedSource/Characters/Character.cs index 2ab6d6a68d..763c677ca0 100644 --- a/Barotrauma/BarotraumaShared/SharedSource/Characters/Character.cs +++ b/Barotrauma/BarotraumaShared/SharedSource/Characters/Character.cs @@ -3448,21 +3448,9 @@ public static void UpdateAll(float deltaTime, Camera cam) characterUpdateTick++; - if (characterUpdateTick % CharacterUpdateInterval == 0) + for (int i = 0; i < CharacterList.Count; i++) { - for (int i = 0; i < CharacterList.Count; i++) - { - if (GameMain.LuaCs.Game.UpdatePriorityCharacters.Contains(CharacterList[i])) continue; - - CharacterList[i].Update(deltaTime * CharacterUpdateInterval, cam); - } - } - - foreach (Character character in GameMain.LuaCs.Game.UpdatePriorityCharacters) - { - if (character.Removed) { continue; } - Debug.Assert(character is { Removed: false }); - character.Update(deltaTime, cam); + CharacterList[i].Update(deltaTime , cam); } #if CLIENT diff --git a/Barotrauma/BarotraumaShared/SharedSource/Map/MapEntity.cs b/Barotrauma/BarotraumaShared/SharedSource/Map/MapEntity.cs index fb37239343..64584ef9d3 100644 --- a/Barotrauma/BarotraumaShared/SharedSource/Map/MapEntity.cs +++ b/Barotrauma/BarotraumaShared/SharedSource/Map/MapEntity.cs @@ -841,7 +841,6 @@ public static void UpdateAll(float deltaTime, Camera cam , ParallelOptions paral // Item update (Item.Update() is not thread-safe and must be executed on the main thread) Item.UpdatePendingConditionUpdates(deltaTime); - float scaledDeltaTime = deltaTime * MapEntityUpdateInterval; Item lastUpdatedItem = null; try @@ -852,7 +851,7 @@ public static void UpdateAll(float deltaTime, Camera cam , ParallelOptions paral try { lastUpdatedItem = item; - item.Update(scaledDeltaTime, cam); + item.Update(deltaTime, cam); } finally { From d5d14e96846b389ffbdc0bceb7e15b002e6c9a19 Mon Sep 17 00:00:00 2001 From: NotAlwaysTrue <2136846186@qq.com> Date: Fri, 16 Jan 2026 17:31:15 +0800 Subject: [PATCH 10/10] oops... --- Barotrauma/BarotraumaShared/SharedSource/Map/MapEntity.cs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Barotrauma/BarotraumaShared/SharedSource/Map/MapEntity.cs b/Barotrauma/BarotraumaShared/SharedSource/Map/MapEntity.cs index 64584ef9d3..43b8045b29 100644 --- a/Barotrauma/BarotraumaShared/SharedSource/Map/MapEntity.cs +++ b/Barotrauma/BarotraumaShared/SharedSource/Map/MapEntity.cs @@ -872,7 +872,7 @@ public static void UpdateAll(float deltaTime, Camera cam , ParallelOptions paral // This must be done on the main thread because Farseer Physics is not thread-safe. PhysicsBodyQueue.ProcessPendingOperations(); - UpdateAllProjSpecific(scaledDeltaTime); + UpdateAllProjSpecific(deltaTime); Spawner?.Update(); #if CLIENT