Files
LuaCsForBarotraumaEP/Barotrauma/BarotraumaShared/SharedSource/Items/Components/Holdable/LevelResource.cs
Eero 1db14631df Defer physics transforms to ensure thread safety
Refactored multiple components to defer Farseer physics transform operations using PhysicsBodyQueue, preventing unsafe calls from parallel contexts. This change addresses thread safety issues with Farseer's DynamicTree and ensures transforms are executed in a safe context. Also increased the spawn amount limit in DebugConsole from 100 to 100000.
2025-12-28 17:14:16 +08:00

193 lines
6.9 KiB
C#

using Barotrauma.Networking;
using FarseerPhysics;
using Microsoft.Xna.Framework;
using System;
using System.Linq;
namespace Barotrauma.Items.Components
{
partial class LevelResource : ItemComponent, IServerSerializable
{
private PhysicsBody trigger;
private Holdable holdable;
private float deattachTimer;
/// <summary>
/// Flag to prevent multiple queued creation requests.
/// Uses volatile to ensure visibility across threads.
/// </summary>
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
{
get;
set;
}
[Serialize(0.0f, IsPropertySaveable.No, description: "How far along the item is to being deattached. When the timer goes above DeattachDuration, the item is deattached.")]
public float DeattachTimer
{
get { return deattachTimer; }
set
{
//clients don't deattach the item until the server says so (handled in ClientRead)
if (GameMain.NetworkMember != null && GameMain.NetworkMember.IsClient)
{
return;
}
if (holdable == null) { return; }
deattachTimer = Math.Max(0.0f, value);
#if SERVER
if (deattachTimer >= DeattachDuration)
{
if (holdable.Attached) { item.CreateServerEvent(this); }
holdable.DeattachFromWall();
}
else if (Math.Abs(lastSentDeattachTimer - deattachTimer) > 0.1f)
{
item.CreateServerEvent(this);
lastSentDeattachTimer = deattachTimer;
}
#else
if (deattachTimer >= DeattachDuration)
{
if (holdable.Attached)
{
//we don't need info of every collected resource, we can get a good sample size just by logging a small sample
if (GameAnalyticsManager.ShouldLogRandomSample())
{
GameAnalyticsManager.AddDesignEvent("ResourceCollected:" + (GameMain.GameSession?.GameMode?.Preset.Identifier.Value ?? "none") + ":" + item.Prefab.Identifier);
}
holdable.DeattachFromWall();
}
trigger.Enabled = false;
}
#endif
}
}
[Serialize(1.0f, IsPropertySaveable.No, description: "How much the position of the item can vary from the wall the item spawns on.")]
public float RandomOffsetFromWall
{
get;
set;
}
public bool Attached
{
get { return holdable != null && holdable.Attached; }
}
public LevelResource(Item item, ContentXElement element) : base(item, element)
{
IsActive = true;
}
public override void Move(Vector2 amount, bool ignoreContacts = false)
{
if (trigger != null && amount.LengthSquared() > 0.00001f)
{
// Defer physics operation if in parallel context (Farseer is not thread-safe)
var capturedTrigger = trigger;
var capturedPos = item.SimPosition;
if (ignoreContacts)
{
PhysicsBodyQueue.ExecuteOrDefer(() => capturedTrigger.SetTransformIgnoreContacts(capturedPos, 0.0f));
}
else
{
PhysicsBodyQueue.ExecuteOrDefer(() => capturedTrigger.SetTransform(capturedPos, 0.0f));
}
}
}
public override void Update(float deltaTime, Camera cam)
{
if (holdable != null && !holdable.Attached)
{
if (trigger != null)
{
trigger.Enabled = false;
}
IsActive = false;
}
else
{
if (trigger == null && !triggerBodyCreationQueued)
{
// 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)
{
// Defer physics operation if in parallel context (Farseer is not thread-safe)
var capturedTrigger = trigger;
var capturedPos = item.SimPosition;
PhysicsBodyQueue.ExecuteOrDefer(() => capturedTrigger.SetTransform(capturedPos, 0.0f));
}
IsActive = false;
}
}
public override void OnItemLoaded()
{
holdable = item.GetComponent<Holdable>();
if (holdable == null)
{
IsActive = false;
return;
}
holdable.Reattachable = false;
if (RequiredItems.Any())
{
holdable.PickingTime = float.MaxValue;
}
}
private void CreateTriggerBody()
{
System.Diagnostics.Debug.Assert(trigger == null, "LevelResource trigger already created!");
var body = item.body ?? holdable?.Body;
if (body != null && Attached)
{
trigger = new PhysicsBody(body.Width, body.Height, body.Radius,
body.Density,
BodyType.Static,
Physics.CollisionWall,
Physics.CollisionNone,
findNewContacts: false)
{
UserData = item
};
trigger.FarseerBody.SetIsSensor(true);
}
}
protected override void RemoveComponentSpecific()
{
if (trigger != null)
{
trigger.Remove();
trigger = null;
}
}
}
}