diff --git a/Barotrauma/BarotraumaShared/SharedSource/GameSession/GameSession.cs b/Barotrauma/BarotraumaShared/SharedSource/GameSession/GameSession.cs
index 1af627a98..c23a81644 100644
--- a/Barotrauma/BarotraumaShared/SharedSource/GameSession/GameSession.cs
+++ b/Barotrauma/BarotraumaShared/SharedSource/GameSession/GameSession.cs
@@ -1153,6 +1153,7 @@ namespace Barotrauma
EventManager?.EndRound();
StatusEffect.StopAll();
AfflictionPrefab.ClearAllEffects();
+ PhysicsBodyQueue.Clear();
IsRunning = false;
#if CLIENT
diff --git a/Barotrauma/BarotraumaShared/SharedSource/Items/Components/Holdable/LevelResource.cs b/Barotrauma/BarotraumaShared/SharedSource/Items/Components/Holdable/LevelResource.cs
index 5f03a6163..797ec14e7 100644
--- a/Barotrauma/BarotraumaShared/SharedSource/Items/Components/Holdable/LevelResource.cs
+++ b/Barotrauma/BarotraumaShared/SharedSource/Items/Components/Holdable/LevelResource.cs
@@ -13,6 +13,12 @@ namespace Barotrauma.Items.Components
private Holdable holdable;
private float deattachTimer;
+
+ ///
+ /// Flag to prevent multiple queued creation requests.
+ /// Uses volatile to ensure visibility across threads.
+ ///
+ private volatile bool triggerBodyCreationQueued;
[Serialize(1.0f, IsPropertySaveable.No, description: "How long it takes to deattach the item from the level walls (in seconds).")]
public float DeattachDuration
@@ -109,9 +115,22 @@ namespace Barotrauma.Items.Components
}
else
{
- if (trigger == null)
+ if (trigger == null && !triggerBodyCreationQueued)
{
- CreateTriggerBody();
+ // Queue the physics body creation to be processed on the main thread.
+ // This is necessary because physics body creation is not thread-safe
+ // and Update() may be called from a parallel loop.
+ triggerBodyCreationQueued = true;
+ PhysicsBodyQueue.EnqueueCreation(() =>
+ {
+ // Double-check that trigger hasn't been created yet
+ // (in case this was called multiple times before queue processing)
+ if (trigger == null && !item.Removed)
+ {
+ CreateTriggerBody();
+ }
+ triggerBodyCreationQueued = false;
+ });
}
if (trigger != null && Vector2.DistanceSquared(item.SimPosition, trigger.SimPosition) > 0.01f)
{
diff --git a/Barotrauma/BarotraumaShared/SharedSource/Items/Components/TriggerComponent.cs b/Barotrauma/BarotraumaShared/SharedSource/Items/Components/TriggerComponent.cs
index b2ff80ee6..c839d488c 100644
--- a/Barotrauma/BarotraumaShared/SharedSource/Items/Components/TriggerComponent.cs
+++ b/Barotrauma/BarotraumaShared/SharedSource/Items/Components/TriggerComponent.cs
@@ -73,6 +73,11 @@ namespace Barotrauma.Items.Components
public PhysicsBody PhysicsBody { get; private set; }
+ ///
+ /// Flag to prevent multiple queued refresh requests.
+ ///
+ private volatile bool physicsBodyRefreshQueued;
+
private float radius;
[Editable, Serialize(0.0f, IsPropertySaveable.Yes)]
public float Radius
@@ -83,7 +88,7 @@ namespace Barotrauma.Items.Components
{
if (radius == value) { return; }
radius = value;
- if (PhysicsBody != null) { RefreshPhysicsBodySize(); }
+ if (PhysicsBody != null) { QueuePhysicsBodyRefresh(); }
}
}
@@ -97,7 +102,7 @@ namespace Barotrauma.Items.Components
{
if (width == value) { return; }
width = value;
- if (PhysicsBody != null) { RefreshPhysicsBodySize(); }
+ if (PhysicsBody != null) { QueuePhysicsBodyRefresh(); }
}
}
@@ -111,10 +116,28 @@ namespace Barotrauma.Items.Components
{
if (height == value) { return; }
height = value;
- if (PhysicsBody != null) { RefreshPhysicsBodySize(); }
+ if (PhysicsBody != null) { QueuePhysicsBodyRefresh(); }
}
}
+ ///
+ /// Queue the physics body refresh to be executed on the main thread.
+ /// This is necessary because physics body operations are not thread-safe.
+ ///
+ private void QueuePhysicsBodyRefresh()
+ {
+ if (physicsBodyRefreshQueued) { return; }
+ physicsBodyRefreshQueued = true;
+ PhysicsBodyQueue.EnqueueCreation(() =>
+ {
+ if (!item.Removed)
+ {
+ RefreshPhysicsBodySize();
+ }
+ physicsBodyRefreshQueued = false;
+ });
+ }
+
private float currentRadius, currentWidth, currentHeight;
private Vector2 bodyOffset;
diff --git a/Barotrauma/BarotraumaShared/SharedSource/Map/Creatures/BallastFloraBehavior.cs b/Barotrauma/BarotraumaShared/SharedSource/Map/Creatures/BallastFloraBehavior.cs
index a06585116..351083203 100644
--- a/Barotrauma/BarotraumaShared/SharedSource/Map/Creatures/BallastFloraBehavior.cs
+++ b/Barotrauma/BarotraumaShared/SharedSource/Map/Creatures/BallastFloraBehavior.cs
@@ -307,6 +307,12 @@ namespace Barotrauma.MapCreatures.Behavior
public readonly List Branches = new List();
private BallastFloraBranch? root;
private readonly List bodies = new List();
+
+ ///
+ /// Branches that need physics bodies created on the main thread.
+ ///
+ private readonly List pendingBodyCreations = new List();
+ private readonly object pendingBodyCreationsLock = new object();
private bool isDead;
@@ -347,7 +353,8 @@ namespace Barotrauma.MapCreatures.Behavior
}
}
UpdateConnections(branch);
- CreateBody(branch);
+ // OnMapLoaded runs on the main thread, so we can create bodies immediately
+ CreateBody(branch, immediate: true);
}
}
@@ -998,10 +1005,52 @@ namespace Barotrauma.MapCreatures.Behavior
}
///
- /// Create a body for a branch which works as the hitbox for flamer
+ /// Queue a physics body creation for a branch.
+ /// The actual body will be created on the main thread to ensure thread safety.
///
- ///
- private void CreateBody(BallastFloraBranch branch)
+ /// The branch to create a body for
+ /// If true, create the body immediately (only safe when called from main thread)
+ private void CreateBody(BallastFloraBranch branch, bool immediate = false)
+ {
+ if (immediate)
+ {
+ CreateBodyImmediate(branch);
+ return;
+ }
+
+ lock (pendingBodyCreationsLock)
+ {
+ pendingBodyCreations.Add(branch);
+ }
+ PhysicsBodyQueue.EnqueueCreation(() => ProcessPendingBodyCreations());
+ }
+
+ ///
+ /// Process all pending body creations on the main thread.
+ /// This ensures Farseer Physics operations are thread-safe.
+ ///
+ private void ProcessPendingBodyCreations()
+ {
+ List branchesToProcess;
+ lock (pendingBodyCreationsLock)
+ {
+ if (pendingBodyCreations.Count == 0) { return; }
+ branchesToProcess = new List(pendingBodyCreations);
+ pendingBodyCreations.Clear();
+ }
+
+ foreach (var branch in branchesToProcess)
+ {
+ if (branch.Removed) { continue; }
+ CreateBodyImmediate(branch);
+ }
+ }
+
+ ///
+ /// Actually create the physics body for a branch.
+ /// Must be called on the main thread.
+ ///
+ private void CreateBodyImmediate(BallastFloraBranch branch)
{
Rectangle rect = branch.Rect;
Vector2 pos = Parent.Position + Offset + branch.Position;
diff --git a/Barotrauma/BarotraumaShared/SharedSource/Map/MapEntity.cs b/Barotrauma/BarotraumaShared/SharedSource/Map/MapEntity.cs
index 8c839934b..572990b3b 100644
--- a/Barotrauma/BarotraumaShared/SharedSource/Map/MapEntity.cs
+++ b/Barotrauma/BarotraumaShared/SharedSource/Map/MapEntity.cs
@@ -686,6 +686,10 @@ namespace Barotrauma
}
);
+ // Process any physics body creation operations queued during Hull/Structure updates.
+ // BallastFlora growth (from Hull.Update) may queue physics body creations.
+ PhysicsBodyQueue.ProcessPendingCreations();
+
#if CLIENT
// Hull Cheats need to be executed after Hull update
Hull.UpdateCheats(deltaTime, cam);
@@ -727,6 +731,10 @@ namespace Barotrauma
throw new InvalidOperationException($"Error while updating item {lastUpdatedItem?.Name ?? "null"}", innerException: e);
}
+ // Process any physics body creation operations that were queued during the parallel update.
+ // This must be done on the main thread because Farseer Physics is not thread-safe.
+ PhysicsBodyQueue.ProcessPendingCreations();
+
UpdateAllProjSpecific(scaledDeltaTime);
Spawner?.Update();
diff --git a/Barotrauma/BarotraumaShared/SharedSource/Physics/PhysicsBodyQueue.cs b/Barotrauma/BarotraumaShared/SharedSource/Physics/PhysicsBodyQueue.cs
new file mode 100644
index 000000000..64e20e51f
--- /dev/null
+++ b/Barotrauma/BarotraumaShared/SharedSource/Physics/PhysicsBodyQueue.cs
@@ -0,0 +1,82 @@
+using System;
+using System.Collections.Generic;
+
+namespace Barotrauma
+{
+ ///
+ /// Thread-safe queue for deferring physics body creation operations to the main thread.
+ /// This is necessary because Farseer Physics' DynamicTree is not thread-safe,
+ /// and physics bodies cannot be safely created during parallel item updates.
+ ///
+ static class PhysicsBodyQueue
+ {
+ private static readonly object _lock = new object();
+ private static readonly Queue _pendingCreations = new Queue();
+
+ ///
+ /// Enqueues a physics body creation action to be executed on the main thread.
+ /// This method is thread-safe and can be called from parallel update loops.
+ ///
+ /// The action that creates the physics body
+ public static void EnqueueCreation(Action createAction)
+ {
+ if (createAction == null) { return; }
+ lock (_lock)
+ {
+ _pendingCreations.Enqueue(createAction);
+ }
+ }
+
+ ///
+ /// Gets the number of pending physics body creation operations.
+ ///
+ public static int PendingCount
+ {
+ get
+ {
+ lock (_lock)
+ {
+ return _pendingCreations.Count;
+ }
+ }
+ }
+
+ ///
+ /// Processes all pending physics body creation operations.
+ /// Must be called on the main thread, outside of any parallel loops.
+ ///
+ public static void ProcessPendingCreations()
+ {
+ while (true)
+ {
+ Action action;
+ lock (_lock)
+ {
+ if (_pendingCreations.Count == 0) { break; }
+ action = _pendingCreations.Dequeue();
+ }
+ try
+ {
+ action?.Invoke();
+ }
+ catch (Exception e)
+ {
+ DebugConsole.ThrowError($"Error processing deferred physics body creation: {e.Message}", e);
+ }
+ }
+ }
+
+ ///
+ /// Clears all pending physics body creation operations.
+ /// Should be called when ending a round or cleaning up.
+ ///
+ public static void Clear()
+ {
+ lock (_lock)
+ {
+ _pendingCreations.Clear();
+ }
+ }
+ }
+}
+