Files
LuaCsForBarotraumaEP/Barotrauma/BarotraumaShared/SharedSource/Items/Components/Holdable/LevelResource.cs
Eero 49355fe32b Unstable Add thread-safe queue for deferred physics body creation
Introduces PhysicsBodyQueue to safely defer physics body creation to the main thread, addressing thread-safety issues with Farseer Physics during parallel updates. Updates LevelResource, TriggerComponent, BallastFloraBehavior, and MapEntity to use the queue for all physics body creation and refresh operations, ensuring they are processed outside of parallel loops. Also adds cleanup of the queue at round end.
2025-12-28 14:42:17 +08:00

187 lines
6.4 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)
{
if (ignoreContacts)
{
trigger.SetTransformIgnoreContacts(item.SimPosition, 0.0f);
}
else
{
trigger.SetTransform(item.SimPosition, 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)
{
trigger.SetTransform(item.SimPosition, 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;
}
}
}
}