diff --git a/Barotrauma/BarotraumaShared/SharedSource/Items/Components/Holdable/Holdable.cs b/Barotrauma/BarotraumaShared/SharedSource/Items/Components/Holdable/Holdable.cs
index 429554361..405b0c9b8 100644
--- a/Barotrauma/BarotraumaShared/SharedSource/Items/Components/Holdable/Holdable.cs
+++ b/Barotrauma/BarotraumaShared/SharedSource/Items/Components/Holdable/Holdable.cs
@@ -486,7 +486,10 @@ namespace Barotrauma.Items.Components
}
else
{
- item.body.ResetDynamics();
+ // Calculate target position
+ Vector2 targetPos;
+ Submarine forceSubmarine = picker.Submarine;
+
Limb heldHand, arm;
if (picker.Inventory.IsInLimbSlot(item, InvSlotType.LeftHand))
{
@@ -504,17 +507,42 @@ namespace Barotrauma.Items.Components
Vector2 diff = new Vector2(
(heldHand.SimPosition.X - arm.SimPosition.X) / 2f,
(heldHand.SimPosition.Y - arm.SimPosition.Y) / 2.5f);
+ targetPos = heldHand.SimPosition + diff;
+ }
+ else
+ {
+ targetPos = picker.SimPosition;
+ }
+ // Defer physics operations if in parallel context
+ if (PhysicsBodyQueue.IsInParallelContext)
+ {
+ var capturedBody = item.body;
+ var capturedItem = item;
+ var capturedTargetPos = targetPos;
+ var capturedForceSubmarine = forceSubmarine;
+
+ PhysicsBodyQueue.Enqueue(() =>
+ {
+ if (capturedBody.Removed || capturedItem.Removed) { return; }
+ capturedBody.ResetDynamics();
+ //we have forced the item to be in the same sub as the dropper above,
+ //and are placing it to the position of the hands in "local" coordinates
+ //which may be outside the sub if the character is e.g. standing half-way through the airlock
+ // -> let's use the forceSubmarine argument ensure the item is still considered to be in the sub's coordinate space,
+ // or it will end up in a weird state and seemingly disappear
+ capturedItem.SetTransform(capturedTargetPos, 0.0f, forceSubmarine: capturedForceSubmarine);
+ });
+ }
+ else
+ {
+ item.body.ResetDynamics();
//we have forced the item to be in the same sub as the dropper above,
//and are placing it to the position of the hands in "local" coordinates
//which may be outside the sub if the character is e.g. standing half-way through the airlock
// -> let's use the forceSubmarine argument ensure the item is still considered to be in the sub's coordinate space,
// or it will end up in a weird state and seemingly disappear
- item.SetTransform(heldHand.SimPosition + diff, 0.0f, forceSubmarine: picker.Submarine);
- }
- else
- {
- item.SetTransform(picker.SimPosition, 0.0f, forceSubmarine: picker.Submarine);
+ item.SetTransform(targetPos, 0.0f, forceSubmarine: forceSubmarine);
}
}
}
diff --git a/Barotrauma/BarotraumaShared/SharedSource/Items/Item.cs b/Barotrauma/BarotraumaShared/SharedSource/Items/Item.cs
index c2576bb48..2ce5e3d2d 100644
--- a/Barotrauma/BarotraumaShared/SharedSource/Items/Item.cs
+++ b/Barotrauma/BarotraumaShared/SharedSource/Items/Item.cs
@@ -3669,20 +3669,45 @@ namespace Barotrauma
if (body != null)
{
IsActive = true;
- body.Enabled = true;
- body.PhysEnabled = true;
- body.ResetDynamics();
- if (dropper != null)
+
+ // Physics body operations must be deferred if we're in a parallel update context,
+ // because Farseer Physics is not thread-safe.
+ if (PhysicsBodyQueue.IsInParallelContext)
{
- if (body.Removed)
+ // Capture the values we need for the deferred operation
+ var capturedBody = body;
+ var capturedDropperSimPos = dropper?.SimPosition ?? Microsoft.Xna.Framework.Vector2.Zero;
+ var capturedSetTransform = setTransform && dropper != null;
+
+ PhysicsBodyQueue.Enqueue(() =>
{
- DebugConsole.ThrowError(
- "Failed to drop the item \"" + Name + "\" (body has been removed"
- + (Removed ? ", item has been removed)" : ")"));
- }
- else if (setTransform)
+ if (capturedBody.Removed || Removed) { return; }
+ capturedBody.Enabled = true;
+ capturedBody.PhysEnabled = true;
+ capturedBody.ResetDynamics();
+ if (capturedSetTransform)
+ {
+ capturedBody.SetTransformIgnoreContacts(capturedDropperSimPos, 0.0f);
+ }
+ });
+ }
+ else
+ {
+ body.Enabled = true;
+ body.PhysEnabled = true;
+ body.ResetDynamics();
+ if (dropper != null)
{
- body.SetTransformIgnoreContacts(dropper.SimPosition, 0.0f);
+ if (body.Removed)
+ {
+ DebugConsole.ThrowError(
+ "Failed to drop the item \"" + Name + "\" (body has been removed"
+ + (Removed ? ", item has been removed)" : ")"));
+ }
+ else if (setTransform)
+ {
+ body.SetTransformIgnoreContacts(dropper.SimPosition, 0.0f);
+ }
}
}
}
@@ -3693,7 +3718,22 @@ namespace Barotrauma
{
if (setTransform)
{
- SetTransform(Container.SimPosition, 0.0f);
+ // Defer SetTransform if in parallel context
+ if (PhysicsBodyQueue.IsInParallelContext)
+ {
+ var capturedContainerSimPos = Container.SimPosition;
+ PhysicsBodyQueue.Enqueue(() =>
+ {
+ if (!Removed)
+ {
+ SetTransform(capturedContainerSimPos, 0.0f);
+ }
+ });
+ }
+ else
+ {
+ SetTransform(Container.SimPosition, 0.0f);
+ }
}
Container.RemoveContained(this);
Container = null;
diff --git a/Barotrauma/BarotraumaShared/SharedSource/Map/MapEntity.cs b/Barotrauma/BarotraumaShared/SharedSource/Map/MapEntity.cs
index 572990b3b..39c77c116 100644
--- a/Barotrauma/BarotraumaShared/SharedSource/Map/MapEntity.cs
+++ b/Barotrauma/BarotraumaShared/SharedSource/Map/MapEntity.cs
@@ -660,7 +660,15 @@ namespace Barotrauma
{
Parallel.ForEach(hullList, parallelOptions, hull =>
{
- hull.Update(deltaTime, cam);
+ PhysicsBodyQueue.IsInParallelContext = true;
+ try
+ {
+ hull.Update(deltaTime, cam);
+ }
+ finally
+ {
+ PhysicsBodyQueue.IsInParallelContext = false;
+ }
});
},
// Structure parallel update
@@ -668,7 +676,15 @@ namespace Barotrauma
{
Parallel.ForEach(structureList, parallelOptions, structure =>
{
- structure.Update(deltaTime, cam);
+ PhysicsBodyQueue.IsInParallelContext = true;
+ try
+ {
+ structure.Update(deltaTime, cam);
+ }
+ finally
+ {
+ PhysicsBodyQueue.IsInParallelContext = false;
+ }
});
},
// Gap reset (must be done before update)
@@ -686,9 +702,9 @@ 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();
+ // Process any physics operations queued during Hull/Structure updates.
+ // BallastFlora growth (from Hull.Update) may queue physics body creations/transforms.
+ PhysicsBodyQueue.ProcessPendingOperations();
#if CLIENT
// Hull Cheats need to be executed after Hull update
@@ -699,8 +715,19 @@ namespace Barotrauma
var shuffledGaps = gapList.OrderBy(g => Rand.Int(int.MaxValue)).ToList();
Parallel.ForEach(gapList, parallelOptions, gap =>
{
- gap.Update(deltaTime, cam);
+ PhysicsBodyQueue.IsInParallelContext = true;
+ try
+ {
+ gap.Update(deltaTime, cam);
+ }
+ finally
+ {
+ PhysicsBodyQueue.IsInParallelContext = false;
+ }
});
+
+ // Process any physics operations queued during Gap updates.
+ PhysicsBodyQueue.ProcessPendingOperations();
#if CLIENT
sw.Stop();
@@ -718,8 +745,16 @@ namespace Barotrauma
{
Parallel.ForEach(itemList, parallelOptions, item =>
{
- lastUpdatedItem = item;
- item.Update(scaledDeltaTime, cam);
+ PhysicsBodyQueue.IsInParallelContext = true;
+ try
+ {
+ lastUpdatedItem = item;
+ item.Update(scaledDeltaTime, cam);
+ }
+ finally
+ {
+ PhysicsBodyQueue.IsInParallelContext = false;
+ }
});
}
catch (InvalidOperationException e)
@@ -731,9 +766,9 @@ 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.
+ // Process any physics 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();
+ PhysicsBodyQueue.ProcessPendingOperations();
UpdateAllProjSpecific(scaledDeltaTime);
Spawner?.Update();
diff --git a/Barotrauma/BarotraumaShared/SharedSource/Physics/PhysicsBodyQueue.cs b/Barotrauma/BarotraumaShared/SharedSource/Physics/PhysicsBodyQueue.cs
index 64e20e51f..a8aec47ef 100644
--- a/Barotrauma/BarotraumaShared/SharedSource/Physics/PhysicsBodyQueue.cs
+++ b/Barotrauma/BarotraumaShared/SharedSource/Physics/PhysicsBodyQueue.cs
@@ -1,17 +1,65 @@
using System;
using System.Collections.Generic;
+using System.Threading;
namespace Barotrauma
{
///
- /// Thread-safe queue for deferring physics body creation operations to the main thread.
+ /// Thread-safe queue for deferring physics 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.
+ /// and physics operations cannot be safely performed during parallel updates.
+ ///
+ /// 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()
static class PhysicsBodyQueue
{
private static readonly object _lock = new object();
- private static readonly Queue _pendingCreations = new Queue();
+ private static readonly Queue _pendingOperations = new Queue();
+
+ ///
+ /// Thread-local flag indicating whether the current thread is in a parallel physics update context.
+ /// When true, physics operations should be deferred using this queue instead of executing immediately.
+ ///
+ [ThreadStatic]
+ private static bool _isInParallelContext;
+
+ ///
+ /// Gets or sets whether the current thread is in a parallel update context.
+ /// When true, physics operations should be queued instead of executed immediately.
+ ///
+ public static bool IsInParallelContext
+ {
+ get => _isInParallelContext;
+ set => _isInParallelContext = value;
+ }
+
+ ///
+ /// Enqueues a physics operation to be executed on the main thread.
+ /// This method is thread-safe and can be called from parallel update loops.
+ ///
+ /// The physics operation to defer
+ public static void Enqueue(Action operation)
+ {
+ if (operation == null) { return; }
+ lock (_lock)
+ {
+ _pendingOperations.Enqueue(operation);
+ }
+ }
///
/// Enqueues a physics body creation action to be executed on the main thread.
@@ -20,15 +68,31 @@ namespace Barotrauma
/// The action that creates the physics body
public static void EnqueueCreation(Action createAction)
{
- if (createAction == null) { return; }
- lock (_lock)
+ Enqueue(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.
+ ///
+ /// The physics operation to execute
+ public static void ExecuteOrDefer(Action operation)
+ {
+ if (operation == null) { return; }
+
+ if (_isInParallelContext)
{
- _pendingCreations.Enqueue(createAction);
+ Enqueue(operation);
+ }
+ else
+ {
+ operation();
}
}
///
- /// Gets the number of pending physics body creation operations.
+ /// Gets the number of pending physics operations.
///
public static int PendingCount
{
@@ -36,24 +100,24 @@ namespace Barotrauma
{
lock (_lock)
{
- return _pendingCreations.Count;
+ return _pendingOperations.Count;
}
}
}
///
- /// Processes all pending physics body creation operations.
+ /// Processes all pending physics operations.
/// Must be called on the main thread, outside of any parallel loops.
///
- public static void ProcessPendingCreations()
+ public static void ProcessPendingOperations()
{
while (true)
{
Action action;
lock (_lock)
{
- if (_pendingCreations.Count == 0) { break; }
- action = _pendingCreations.Dequeue();
+ if (_pendingOperations.Count == 0) { break; }
+ action = _pendingOperations.Dequeue();
}
try
{
@@ -61,20 +125,29 @@ namespace Barotrauma
}
catch (Exception e)
{
- DebugConsole.ThrowError($"Error processing deferred physics body creation: {e.Message}", e);
+ DebugConsole.ThrowError($"Error processing deferred physics operation: {e.Message}", e);
}
}
}
///
- /// Clears all pending physics body creation operations.
+ /// Legacy method for backwards compatibility.
+ /// Calls ProcessPendingOperations().
+ ///
+ public static void ProcessPendingCreations()
+ {
+ ProcessPendingOperations();
+ }
+
+ ///
+ /// Clears all pending physics operations.
/// Should be called when ending a round or cleaning up.
///
public static void Clear()
{
lock (_lock)
{
- _pendingCreations.Clear();
+ _pendingOperations.Clear();
}
}
}
diff --git a/Barotrauma/BarotraumaShared/SharedSource/Screens/GameScreen.cs b/Barotrauma/BarotraumaShared/SharedSource/Screens/GameScreen.cs
index 11b3ce9ad..577324da2 100644
--- a/Barotrauma/BarotraumaShared/SharedSource/Screens/GameScreen.cs
+++ b/Barotrauma/BarotraumaShared/SharedSource/Screens/GameScreen.cs
@@ -178,9 +178,23 @@ namespace Barotrauma
Parallel.Invoke(parallelOptions,
() => GameMain.ParticleManager.Update((float)deltaTime),
- () => { if (Level.Loaded != null) Level.Loaded.Update((float)deltaTime, cam); }
+ () =>
+ {
+ PhysicsBodyQueue.IsInParallelContext = true;
+ try
+ {
+ if (Level.Loaded != null) Level.Loaded.Update((float)deltaTime, cam);
+ }
+ finally
+ {
+ PhysicsBodyQueue.IsInParallelContext = false;
+ }
+ }
);
+ // Process any physics operations queued during Level update
+ PhysicsBodyQueue.ProcessPendingOperations();
+
sw.Stop();
GameMain.PerformanceCounter.AddElapsedTicks("Update:Particles+Level", sw.ElapsedTicks);
@@ -246,9 +260,34 @@ namespace Barotrauma
#elif SERVER
Parallel.Invoke(parallelOptions,
- () => { if (Level.Loaded != null) Level.Loaded.Update((float)deltaTime, Camera.Instance); },
- () => Character.UpdateAll((float)deltaTime, Camera.Instance)
+ () =>
+ {
+ 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;
+ }
+ }
);
+
+ // Process any physics operations queued during parallel updates
+ PhysicsBodyQueue.ProcessPendingOperations();
#endif
var submarines = Submarine.Loaded.ToList();