Revert "OBT1.1.0 Merge branch 'dev_pte' into dev"

This reverts commit 177cf89756, reversing
changes made to 42ba733cd4.
This commit is contained in:
Eero
2025-12-29 11:18:11 +08:00
parent 177cf89756
commit 046483b9da
86 changed files with 800 additions and 2496 deletions
@@ -5,7 +5,6 @@ using FarseerPhysics.Dynamics;
using FarseerPhysics.Dynamics.Joints;
using Microsoft.Xna.Framework;
using System;
using System.Collections.Concurrent;
using System.Collections.Generic;
using System.Linq;
#if CLIENT
@@ -25,8 +24,11 @@ namespace Barotrauma.Items.Components
Right
}
private static readonly ConcurrentDictionary<DockingPort, byte> _dockingPortDict = new ConcurrentDictionary<DockingPort, byte>();
public static IEnumerable<DockingPort> List => _dockingPortDict.Keys;
private static readonly List<DockingPort> list = new List<DockingPort>();
public static IEnumerable<DockingPort> List
{
get { return list; }
}
private Sprite overlaySprite;
private float dockingState;
@@ -166,7 +168,7 @@ namespace Barotrauma.Items.Components
IsActive = true;
_dockingPortDict.TryAdd(this, 0);
list.Add(this);
}
public override void FlipX(bool relativeToSub)
@@ -198,7 +200,7 @@ namespace Barotrauma.Items.Components
{
float closestDist = float.MaxValue;
DockingPort closestPort = null;
foreach (DockingPort port in List)
foreach (DockingPort port in list)
{
if (port == this || port.item.Submarine == item.Submarine || port.IsHorizontal != IsHorizontal) { continue; }
float xDist = Math.Abs(port.item.WorldPosition.X - item.WorldPosition.X);
@@ -530,8 +532,8 @@ namespace Barotrauma.Items.Components
wire.TryConnect(recipient, addNode: false);
//Flag connections to be updated
Powered.MarkConnectionChanged(powerConnection);
Powered.MarkConnectionChanged(recipient);
Powered.ChangedConnections.Add(powerConnection);
Powered.ChangedConnections.Add(recipient);
}
private void CreateDoorBody()
@@ -1005,7 +1007,7 @@ namespace Barotrauma.Items.Components
Connection powerConnection = Item.Connections.Find(c => c.IsPower);
if (powerConnection != null)
{
Powered.MarkConnectionChanged(powerConnection);
Powered.ChangedConnections.Add(powerConnection);
}
if (doorBody != null)
@@ -1149,7 +1151,7 @@ namespace Barotrauma.Items.Components
protected override void RemoveComponentSpecific()
{
base.RemoveComponentSpecific();
_dockingPortDict.TryRemove(this, out _);
list.Remove(this);
hulls[0]?.Remove(); hulls[0] = null;
hulls[1]?.Remove(); hulls[1] = null;
gap?.Remove(); gap = null;
@@ -2,7 +2,6 @@
using FarseerPhysics;
using Microsoft.Xna.Framework;
using System;
using System.Collections.Concurrent;
using System.Collections.Generic;
using System.Linq;
using FarseerPhysics.Dynamics;
@@ -15,9 +14,9 @@ namespace Barotrauma.Items.Components
{
partial class Door : Pickable, IDrawableComponent, IServerSerializable
{
private static readonly ConcurrentDictionary<Door, byte> _doorDict = new ConcurrentDictionary<Door, byte>();
private static readonly HashSet<Door> doorList = new HashSet<Door>();
public static ICollection<Door> DoorList => _doorDict.Keys;
public static IReadOnlyCollection<Door> DoorList { get { return doorList; } }
private Gap linkedGap;
private bool isOpen;
@@ -278,7 +277,7 @@ namespace Barotrauma.Items.Components
}
IsActive = true;
_doorDict.TryAdd(this, 0);
doorList.Add(this);
}
public override void OnItemLoaded()
@@ -314,21 +313,13 @@ namespace Barotrauma.Items.Components
public override void Move(Vector2 amount, bool ignoreContacts = false)
{
// Defer physics operation if in parallel context (Farseer is not thread-safe)
if (Body != null)
if (ignoreContacts)
{
var capturedBody = Body;
var capturedNewPos = Body.SimPosition + ConvertUnits.ToSimUnits(amount);
if (ignoreContacts)
{
PhysicsBodyQueue.ExecuteOrDefer(() =>
capturedBody.SetTransformIgnoreContacts(capturedNewPos, 0.0f));
}
else
{
PhysicsBodyQueue.ExecuteOrDefer(() =>
capturedBody.SetTransform(capturedNewPos, 0.0f));
}
Body?.SetTransformIgnoreContacts(Body.SimPosition + ConvertUnits.ToSimUnits(amount), 0.0f);
}
else
{
Body?.SetTransform(Body.SimPosition + ConvertUnits.ToSimUnits(amount), 0.0f);
}
#if CLIENT
UpdateConvexHulls();
@@ -678,7 +669,7 @@ namespace Barotrauma.Items.Components
convexHull2?.Remove();
#endif
_doorDict.TryRemove(this, out _);
doorList.Remove(this);
}
private bool CheckSubmarinesInDoorWay()
@@ -794,19 +785,13 @@ namespace Barotrauma.Items.Components
//immediately teleport it to the correct side
if (Math.Sign(diff) != dir)
{
// Defer physics operation if in parallel context (Farseer is not thread-safe)
var capturedBody = body;
if (IsHorizontal)
{
Vector2 newPos = new Vector2(body.SimPosition.X, item.SimPosition.Y + dir * doorRectSimSize.Y * 2.0f);
float rotation = body.Rotation;
PhysicsBodyQueue.ExecuteOrDefer(() => capturedBody.SetTransformIgnoreContacts(newPos, rotation));
body.SetTransformIgnoreContacts(new Vector2(body.SimPosition.X, item.SimPosition.Y + dir * doorRectSimSize.Y * 2.0f), body.Rotation);
}
else
{
Vector2 newPos = new Vector2(item.SimPosition.X + dir * doorRectSimSize.X * 1.2f, body.SimPosition.Y);
float rotation = body.Rotation;
PhysicsBodyQueue.ExecuteOrDefer(() => capturedBody.SetTransformIgnoreContacts(newPos, rotation));
body.SetTransformIgnoreContacts(new Vector2(item.SimPosition.X + dir * doorRectSimSize.X * 1.2f, body.SimPosition.Y), body.Rotation);
}
}
@@ -3,7 +3,6 @@ using Barotrauma.Networking;
using FarseerPhysics;
using Microsoft.Xna.Framework;
using System;
using System.Collections.Concurrent;
using System.Collections.Generic;
using System.Linq;
@@ -11,8 +10,11 @@ namespace Barotrauma.Items.Components
{
partial class ElectricalDischarger : Powered, IServerSerializable
{
private static readonly ConcurrentDictionary<ElectricalDischarger, byte> _dischargerDict = new ConcurrentDictionary<ElectricalDischarger, byte>();
public static IEnumerable<ElectricalDischarger> List => _dischargerDict.Keys;
private static readonly List<ElectricalDischarger> list = new List<ElectricalDischarger>();
public static IEnumerable<ElectricalDischarger> List
{
get { return list; }
}
const int MaxNodes = 100;
const float MaxNodeDistance = 150.0f;
@@ -113,7 +115,7 @@ namespace Barotrauma.Items.Components
public ElectricalDischarger(Item item, ContentXElement element) :
base(item, element)
{
_dischargerDict.TryAdd(this, 0);
list.Add(this);
foreach (var subElement in element.Elements())
{
@@ -602,7 +604,7 @@ namespace Barotrauma.Items.Components
protected override void RemoveComponentSpecific()
{
base.RemoveComponentSpecific();
_dischargerDict.TryRemove(this, out _);
list.Remove(this);
}
public void ServerEventWrite(IWriteMessage msg, Client c, NetEntityEvent.IData extraData = null)
@@ -9,7 +9,6 @@ using System;
using System.Collections.Generic;
using System.Collections.Immutable;
using System.Linq;
using System.Threading;
using System.Xml.Linq;
namespace Barotrauma.Items.Components
@@ -486,10 +485,7 @@ namespace Barotrauma.Items.Components
}
else
{
// Calculate target position
Vector2 targetPos;
Submarine forceSubmarine = picker.Submarine;
item.body.ResetDynamics();
Limb heldHand, arm;
if (picker.Inventory.IsInLimbSlot(item, InvSlotType.LeftHand))
{
@@ -507,42 +503,17 @@ 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(targetPos, 0.0f, forceSubmarine: forceSubmarine);
item.SetTransform(heldHand.SimPosition + diff, 0.0f, forceSubmarine: picker.Submarine);
}
else
{
item.SetTransform(picker.SimPosition, 0.0f, forceSubmarine: picker.Submarine);
}
}
}
@@ -645,13 +616,12 @@ namespace Barotrauma.Items.Components
return CanBeAttached(user, out _);
}
private static readonly ThreadLocal<List<Item>> tempOverlappingItems = new ThreadLocal<List<Item>>(() => new List<Item>());
private static List<Item> tempOverlappingItems = new List<Item>();
private bool CanBeAttached(Character user, out IEnumerable<Item> overlappingItems)
{
var overlapping = tempOverlappingItems.Value;
overlapping.Clear();
overlappingItems = overlapping;
tempOverlappingItems.Clear();
overlappingItems = tempOverlappingItems;
if (!attachable || !Reattachable) { return false; }
//can be attached anywhere in sub editor
@@ -694,9 +664,9 @@ namespace Barotrauma.Items.Components
}
if (attachPos.X + size.X < worldRect.X || attachPos.X - size.X > worldRect.Right) { continue; }
if (attachPos.Y - size.Y > worldRect.Y || attachPos.Y + size.Y < worldRect.Y - worldRect.Height) { continue; }
overlapping.Add(otherItem);
tempOverlappingItems.Add(otherItem);
}
if (overlapping.Any()) { return false; }
if (tempOverlappingItems.Any()) { return false; }
}
//can be attached anywhere inside hulls
@@ -13,12 +13,6 @@ namespace Barotrauma.Items.Components
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
@@ -92,16 +86,13 @@ namespace Barotrauma.Items.Components
{
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));
trigger.SetTransformIgnoreContacts(item.SimPosition, 0.0f);
}
else
{
PhysicsBodyQueue.ExecuteOrDefer(() => capturedTrigger.SetTransform(capturedPos, 0.0f));
trigger.SetTransform(item.SimPosition, 0.0f);
}
}
}
@@ -118,29 +109,13 @@ namespace Barotrauma.Items.Components
}
else
{
if (trigger == null && !triggerBodyCreationQueued)
if (trigger == null)
{
// 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;
});
CreateTriggerBody();
}
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));
trigger.SetTransform(item.SimPosition, 0.0f);
}
IsActive = false;
}
@@ -3,7 +3,6 @@ using FarseerPhysics.Dynamics;
using FarseerPhysics.Dynamics.Contacts;
using Microsoft.Xna.Framework;
using System;
using System.Collections.Concurrent;
using System.Collections.Generic;
using System.Collections.Immutable;
using System.Linq;
@@ -25,7 +24,7 @@ namespace Barotrauma.Items.Components
private readonly HashSet<Entity> hitTargets = new HashSet<Entity>();
private readonly ConcurrentQueue<Fixture> impactQueue = new ConcurrentQueue<Fixture>();
private readonly Queue<Fixture> impactQueue = new Queue<Fixture>();
public Character User { get; private set; }
@@ -191,16 +190,17 @@ namespace Barotrauma.Items.Components
{
if (!item.body.Enabled)
{
while (impactQueue.TryDequeue(out _)) { } // Clear queue
impactQueue.Clear();
return;
}
if (picker == null || !picker.HeldItems.Contains(item))
{
while (impactQueue.TryDequeue(out _)) { } // Clear queue
impactQueue.Clear();
IsActive = false;
}
while (impactQueue.TryDequeue(out var impact))
while (impactQueue.Count > 0)
{
var impact = impactQueue.Dequeue();
HandleImpact(impact);
}
//in case handling the impact does something to the picker
@@ -300,7 +300,7 @@ namespace Barotrauma.Items.Components
private void RestoreCollision()
{
while (impactQueue.TryDequeue(out _)) { } // Clear queue
impactQueue.Clear();
item.body.FarseerBody.OnCollision -= OnCollision;
item.body.CollisionCategories = Physics.CollisionItem;
item.body.CollidesWith = Physics.DefaultItemCollidesWith;
@@ -4,7 +4,6 @@ using Microsoft.Xna.Framework;
using System;
using System.Collections.Generic;
using System.Linq;
using System.Threading;
using Barotrauma.Extensions;
using Barotrauma.MapCreatures.Behavior;
@@ -316,7 +315,7 @@ namespace Barotrauma.Items.Components
partial void UseProjSpecific(float deltaTime, Vector2 raystart);
private static readonly ThreadLocal<List<Body>> hitBodies = new ThreadLocal<List<Body>>(() => new List<Body>());
private static readonly List<Body> hitBodies = new List<Body>();
private readonly HashSet<Character> hitCharacters = new HashSet<Character>();
private readonly List<FireSource> fireSourcesInRange = new List<FireSource>();
private void Repair(Vector2 rayStart, Vector2 rayEnd, float deltaTime, Character user, float degreeOfSuccess, List<Body> ignoredBodies)
@@ -374,13 +373,13 @@ namespace Barotrauma.Items.Components
},
allowInsideFixture: true);
hitBodies.Value.Clear();
hitBodies.Value.AddRange(bodies.Distinct());
hitBodies.Clear();
hitBodies.AddRange(bodies.Distinct());
lastPickedFraction = Submarine.LastPickedFraction;
Type lastHitType = null;
hitCharacters.Clear();
foreach (Body body in hitBodies.Value)
foreach (Body body in hitBodies)
{
Type bodyType = body.UserData?.GetType();
if (!RepairThroughWalls && bodyType != null && bodyType != lastHitType)
@@ -898,49 +897,48 @@ namespace Barotrauma.Items.Components
}
}
private static readonly ThreadLocal<List<ISerializableEntity>> currentTargets = new ThreadLocal<List<ISerializableEntity>>(() => new List<ISerializableEntity>());
private static List<ISerializableEntity> currentTargets = new List<ISerializableEntity>();
private void ApplyStatusEffectsOnTarget(Character user, float deltaTime, ActionType actionType, Item targetItem = null, Character character = null, Limb limb = null, Structure structure = null)
{
if (statusEffectLists == null) { return; }
if (!statusEffectLists.TryGetValue(actionType, out List<StatusEffect> statusEffects)) { return; }
var targets = currentTargets.Value;
foreach (StatusEffect effect in statusEffects)
{
targets.Clear();
currentTargets.Clear();
effect.SetUser(user);
if (effect.HasTargetType(StatusEffect.TargetType.UseTarget))
{
if (targetItem != null)
{
targets.AddRange(targetItem.AllPropertyObjects);
currentTargets.AddRange(targetItem.AllPropertyObjects);
}
if (structure != null)
{
targets.Add(structure);
currentTargets.Add(structure);
}
if (character != null)
{
targets.Add(character);
currentTargets.Add(character);
}
effect.Apply(actionType, deltaTime, item, targets);
effect.Apply(actionType, deltaTime, item, currentTargets);
}
else if (effect.HasTargetType(StatusEffect.TargetType.Character))
{
targets.Add(user);
effect.Apply(actionType, deltaTime, item, targets);
currentTargets.Add(user);
effect.Apply(actionType, deltaTime, item, currentTargets);
}
else if (effect.HasTargetType(StatusEffect.TargetType.Limb))
{
targets.Add(limb);
effect.Apply(actionType, deltaTime, item, targets);
currentTargets.Add(limb);
effect.Apply(actionType, deltaTime, item, currentTargets);
}
#if CLIENT
if (user == null) { return; }
// Hard-coded progress bars for welding doors stuck.
// A general purpose system could be better, but it would most likely require changes in the way we define the status effects in xml.
foreach (ISerializableEntity target in targets)
foreach (ISerializableEntity target in currentTargets)
{
if (target is not Door door) { continue; }
if (!door.CanBeWelded || !door.Item.IsInteractable(user)) { continue; }
@@ -949,8 +949,7 @@ namespace Barotrauma.Items.Components
//if any of the effects reduce the item's condition, set the user for OnBroken effects as well
if (reducesCondition && user != null && type != ActionType.OnBroken)
{
// Use ToArray() snapshot for thread-safe iteration
foreach (ItemComponent ic in item.Components.ToArray())
foreach (ItemComponent ic in item.Components)
{
if (ic.statusEffectLists == null || !ic.statusEffectLists.TryGetValue(ActionType.OnBroken, out List<StatusEffect> brokenEffects)) { continue; }
foreach (var brokenEffect in brokenEffects)
@@ -884,8 +884,7 @@ namespace Barotrauma.Items.Components
RelatedItem containableItem = FindContainableItem(containedItem);
if (containableItem != null && containableItem.SetActive)
{
// Use ToArray() snapshot for thread-safe iteration
foreach (var ic in containedItem.Components.ToArray())
foreach (var ic in containedItem.Components)
{
ic.IsActive = active;
}
@@ -1011,11 +1010,7 @@ namespace Barotrauma.Items.Components
if (flippedX ^ flippedY) { rotation = -rotation; }
rotation += -item.RotationRad;
}
// Defer physics operation if in parallel context (Farseer is not thread-safe)
var capturedBody = contained.Item.body.FarseerBody;
var capturedSimPos = simPos;
var capturedRotation = rotation;
PhysicsBodyQueue.ExecuteOrDefer(() => capturedBody.SetTransformIgnoreContacts(ref capturedSimPos, capturedRotation));
contained.Item.body.FarseerBody.SetTransformIgnoreContacts(ref simPos, rotation);
contained.Item.body.UpdateDrawPosition(interpolate: false);
}
catch (Exception e)
@@ -1,19 +1,17 @@
using System.Collections.Concurrent;
using System.Collections.Generic;
using System.Collections.Generic;
using System.Xml.Linq;
namespace Barotrauma.Items.Components
{
partial class Ladder : ItemComponent
{
private static readonly ConcurrentDictionary<Ladder, byte> _ladderDict = new ConcurrentDictionary<Ladder, byte>();
public static IEnumerable<Ladder> List => _ladderDict.Keys;
public static List<Ladder> List { get; } = new List<Ladder>();
public Ladder(Item item, ContentXElement element)
: base(item, element)
{
InitProjSpecific(element);
_ladderDict.TryAdd(this, 0);
List.Add(this);
}
partial void InitProjSpecific(ContentXElement element);
@@ -30,7 +28,7 @@ namespace Barotrauma.Items.Components
{
base.RemoveComponentSpecific();
RemoveProjSpecific();
_ladderDict.TryRemove(this, out _);
List.Remove(this);
}
partial void RemoveProjSpecific();
@@ -5,7 +5,6 @@ using System;
using System.Collections.Generic;
using System.Globalization;
using System.Xml.Linq;
using System.Linq;
namespace Barotrauma.Items.Components
{
@@ -498,14 +497,12 @@ namespace Barotrauma.Items.Components
item.SendSignal(new Signal(MathHelper.ToDegrees(targetRotation).ToString("G", CultureInfo.InvariantCulture), sender: user), positionOut);
// Use ToList() snapshot for thread-safe iteration
var signalRecipients = item.LastSentSignalRecipients.ToList();
for (int i = signalRecipients.Count - 1; i >= 0; i--)
for (int i = item.LastSentSignalRecipients.Count - 1; i >= 0; i--)
{
if (signalRecipients[i].Item.Condition <= 0.0f || signalRecipients[i].IsPower) { continue; }
if (signalRecipients[i].Item.Prefab.FocusOnSelected)
if (item.LastSentSignalRecipients[i].Item.Condition <= 0.0f || item.LastSentSignalRecipients[i].IsPower) { continue; }
if (item.LastSentSignalRecipients[i].Item.Prefab.FocusOnSelected)
{
return signalRecipients[i].Item;
return item.LastSentSignalRecipients[i].Item;
}
}
@@ -1,17 +1,14 @@
using Barotrauma.Networking;
using Microsoft.Xna.Framework;
using System;
using System.Collections.Concurrent;
using System.Collections.Generic;
using System.Linq;
using System.Threading;
namespace Barotrauma.Items.Components
{
partial class Sonar : Powered, IServerSerializable, IClientSerializable
{
private static readonly ConcurrentDictionary<Sonar, byte> _sonarDict = new ConcurrentDictionary<Sonar, byte>();
public static IEnumerable<Sonar> SonarList => _sonarDict.Keys;
public static List<Sonar> SonarList = new List<Sonar>();
public enum Mode
{
@@ -172,7 +169,7 @@ namespace Barotrauma.Items.Components
IsActive = true;
InitProjSpecific(element);
CurrentMode = Mode.Passive;
_sonarDict.TryAdd(this, 0);
SonarList.Add(this);
}
partial void InitProjSpecific(ContentXElement element);
@@ -294,15 +291,13 @@ namespace Barotrauma.Items.Components
return currentPingIndex != -1 && (character == null || characterUsable);
}
private static readonly ThreadLocal<Dictionary<string, List<Character>>> targetGroups =
new ThreadLocal<Dictionary<string, List<Character>>>(() => new Dictionary<string, List<Character>>());
private static readonly Dictionary<string, List<Character>> targetGroups = new Dictionary<string, List<Character>>();
public override bool CrewAIOperate(float deltaTime, Character character, AIObjectiveOperateItem objective)
{
if (currentMode == Mode.Passive || !aiPingCheckPending) { return false; }
var groups = targetGroups.Value;
foreach (List<Character> targetGroup in groups.Values)
foreach (List<Character> targetGroup in targetGroups.Values)
{
targetGroup.Clear();
}
@@ -315,14 +310,14 @@ namespace Barotrauma.Items.Components
#warning This is not the best key for a dictionary.
string directionName = GetDirectionName(c.WorldPosition - item.WorldPosition).Value;
if (!groups.ContainsKey(directionName))
if (!targetGroups.ContainsKey(directionName))
{
groups.Add(directionName, new List<Character>());
targetGroups.Add(directionName, new List<Character>());
}
groups[directionName].Add(c);
targetGroups[directionName].Add(c);
}
foreach (KeyValuePair<string, List<Character>> targetGroup in groups)
foreach (KeyValuePair<string, List<Character>> targetGroup in targetGroups)
{
if (!targetGroup.Value.Any()) { continue; }
string dialogTag = "DialogSonarTarget";
@@ -406,7 +401,7 @@ namespace Barotrauma.Items.Components
MineralClusters = null;
#endif
_sonarDict.TryRemove(this, out _);
SonarList.Remove(this);
}
@@ -637,7 +637,7 @@ namespace Barotrauma.Items.Components
if (pathFinder == null)
{
pathFinder = new PathFinder(WayPoint.WayPointList.ToList(), false)
pathFinder = new PathFinder(WayPoint.WayPointList, false)
{
GetNodePenalty = GetNodePenalty
};
@@ -1,7 +1,6 @@
using Barotrauma.Extensions;
using Microsoft.Xna.Framework;
using System;
using System.Collections.Concurrent;
using System.Collections.Generic;
using System.Linq;
using System.Xml.Linq;
@@ -14,12 +13,10 @@ namespace Barotrauma.Items.Components
private readonly HashSet<Connection> signalConnections = new HashSet<Connection>();
private readonly ConcurrentDictionary<Connection, bool> connectionDirty = new ConcurrentDictionary<Connection, bool>();
private readonly Dictionary<Connection, bool> connectionDirty = new Dictionary<Connection, bool>();
//a list of connections a given connection is connected to, either directly or via other power transfer components
//Uses ConcurrentDictionary<Connection, byte> as a thread-safe HashSet replacement
private readonly ConcurrentDictionary<Connection, ConcurrentDictionary<Connection, byte>> connectedRecipients =
new ConcurrentDictionary<Connection, ConcurrentDictionary<Connection, byte>>();
private readonly Dictionary<Connection, HashSet<Connection>> connectedRecipients = new Dictionary<Connection, HashSet<Connection>>();
private float overloadCooldownTimer;
private const float OverloadCooldown = 5.0f;
@@ -135,7 +132,7 @@ namespace Barotrauma.Items.Components
partial void InitProjectSpecific(XElement element);
private static readonly System.Collections.Concurrent.ConcurrentDictionary<PowerTransfer, byte> _recipientsToRefresh = new System.Collections.Concurrent.ConcurrentDictionary<PowerTransfer, byte>();
private static readonly HashSet<PowerTransfer> recipientsToRefresh = new HashSet<PowerTransfer>();
public override void UpdateBroken(float deltaTime, Camera cam)
{
base.UpdateBroken(deltaTime, cam);
@@ -147,21 +144,20 @@ namespace Barotrauma.Items.Components
powerLoad = 0.0f;
currPowerConsumption = 0.0f;
SetAllConnectionsDirty();
_recipientsToRefresh.Clear();
// Take snapshot for thread-safe iteration (no locks needed with ConcurrentDictionary)
foreach (var recipientDict in connectedRecipients.Values)
recipientsToRefresh.Clear();
foreach (HashSet<Connection> recipientList in connectedRecipients.Values)
{
foreach (Connection c in recipientDict.Keys)
foreach (Connection c in recipientList)
{
if (c.Item == item) { continue; }
var recipientPowerTransfer = c.Item.GetComponent<PowerTransfer>();
if (recipientPowerTransfer != null)
{
_recipientsToRefresh.TryAdd(recipientPowerTransfer, 0);
recipientsToRefresh.Add(recipientPowerTransfer);
}
}
}
foreach (PowerTransfer recipientPowerTransfer in _recipientsToRefresh.Keys)
foreach (PowerTransfer recipientPowerTransfer in recipientsToRefresh)
{
recipientPowerTransfer.SetAllConnectionsDirty();
recipientPowerTransfer.RefreshConnections();
@@ -308,56 +304,58 @@ namespace Barotrauma.Items.Components
protected void RefreshConnections()
{
var connections = item.Connections;
if (connections == null) { return; }
// Take a snapshot of connections for thread-safe iteration
var connectionSnapshot = connections.ToList();
foreach (Connection c in connectionSnapshot)
foreach (Connection c in connections)
{
if (!connectionDirty.TryGetValue(c, out bool isDirty))
if (!connectionDirty.ContainsKey(c))
{
connectionDirty[c] = true;
isDirty = true;
}
if (!isDirty)
else if (!connectionDirty[c])
{
continue;
}
//find all connections that are connected to this one (directly or via another PowerTransfer)
var tempConnected = connectedRecipients.GetOrAdd(c, _ => new ConcurrentDictionary<Connection, byte>());
// Get previous recipients and clear
var previousRecipients = tempConnected.Keys.ToList();
tempConnected.Clear();
//mark all previous recipients as dirty
foreach (Connection recipient in previousRecipients)
HashSet<Connection> tempConnected;
if (!connectedRecipients.ContainsKey(c))
{
var pt = recipient.Item.GetComponent<PowerTransfer>();
if (pt != null) { pt.connectionDirty[recipient] = true; }
tempConnected = new HashSet<Connection>();
connectedRecipients.Add(c, tempConnected);
}
else
{
tempConnected = connectedRecipients[c];
tempConnected.Clear();
//mark all previous recipients as dirty
foreach (Connection recipient in tempConnected)
{
var pt = recipient.Item.GetComponent<PowerTransfer>();
if (pt != null) { pt.connectionDirty[recipient] = true; }
}
}
tempConnected.TryAdd(c, 0);
tempConnected.Add(c);
if (item.Condition > 0.0f)
{
GetConnected(c, tempConnected);
//go through all the PowerTransfers that we're connected to and set their connections to match the ones we just calculated
//(no need to go through the recursive GetConnected method again)
// Take snapshot for thread-safe iteration (no locks needed)
var tempConnectedSnapshot = tempConnected.Keys.ToList();
foreach (Connection recipient in tempConnectedSnapshot)
foreach (Connection recipient in tempConnected)
{
if (recipient == c) { continue; }
var recipientPowerTransfer = recipient.Item.GetComponent<PowerTransfer>();
if (recipientPowerTransfer == null) { continue; }
var recipientSet = recipientPowerTransfer.connectedRecipients.GetOrAdd(recipient, _ => new ConcurrentDictionary<Connection, byte>());
recipientSet.Clear();
foreach (var connection in tempConnectedSnapshot)
if (!recipientPowerTransfer.connectedRecipients.ContainsKey(recipient))
{
recipientSet.TryAdd(connection, 0);
recipientPowerTransfer.connectedRecipients.Add(recipient, new HashSet<Connection>());
}
else
{
recipientPowerTransfer.connectedRecipients[recipient].Clear();
}
foreach (var connection in tempConnected)
{
recipientPowerTransfer.connectedRecipients[recipient].Add(connection);
}
recipientPowerTransfer.connectionDirty[recipient] = false;
}
@@ -366,20 +364,19 @@ namespace Barotrauma.Items.Components
}
}
//Finds all the connections that can receive a signal sent into the given connection and stores them in the concurrent dictionary.
private void GetConnected(Connection c, ConcurrentDictionary<Connection, byte> connected)
//Finds all the connections that can receive a signal sent into the given connection and stores them in the hashset.
private void GetConnected(Connection c, HashSet<Connection> connected)
{
// Take snapshot for thread-safe iteration
var recipients = c.Recipients.ToList();
var recipients = c.Recipients;
foreach (Connection recipient in recipients)
{
if (recipient == null || connected.ContainsKey(recipient)) { continue; }
if (recipient == null || connected.Contains(recipient)) { continue; }
Item it = recipient.Item;
if (it == null || it.Condition <= 0.0f) { continue; }
connected.TryAdd(recipient, 0);
connected.Add(recipient);
var powerTransfer = it.GetComponent<PowerTransfer>();
if (powerTransfer != null && powerTransfer.CanTransfer && powerTransfer.IsActive)
@@ -397,14 +394,10 @@ namespace Barotrauma.Items.Components
connectionDirty[c] = true;
if (c.IsPower)
{
MarkConnectionChanged(c);
ChangedConnections.Add(c);
if (connectedRecipients.TryGetValue(c, out var recipients))
{
// No lock needed - ConcurrentDictionary.Keys is thread-safe
foreach (var conn in recipients.Keys.Where(conn => conn.IsPower))
{
MarkConnectionChanged(conn);
}
recipients.Where(c => c.IsPower).ForEach(c => ChangedConnections.Add(c));
}
}
}
@@ -417,14 +410,10 @@ namespace Barotrauma.Items.Components
connectionDirty[connection] = true;
if (connection.IsPower)
{
MarkConnectionChanged(connection);
ChangedConnections.Add(connection);
if (connectedRecipients.TryGetValue(connection, out var recipients))
{
// No lock needed - ConcurrentDictionary.Keys is thread-safe
foreach (var conn in recipients.Keys.Where(conn => conn.IsPower))
{
MarkConnectionChanged(conn);
}
recipients.Where(c => c.IsPower).ForEach(c => ChangedConnections.Add(c));
}
}
}
@@ -463,19 +452,16 @@ namespace Barotrauma.Items.Components
public override void ReceiveSignal(Signal signal, Connection connection)
{
if (item.Condition <= 0.0f || connection.IsPower) { return; }
if (!connectedRecipients.TryGetValue(connection, out var recipients)) { return; }
if (!connectedRecipients.ContainsKey(connection)) { return; }
if (!signalConnections.Contains(connection)) { return; }
// No lock needed - ConcurrentDictionary.Keys is thread-safe
// Use ToList() snapshot for thread-safe iteration
foreach (Connection recipient in recipients.Keys.ToList())
foreach (Connection recipient in connectedRecipients[connection])
{
if (recipient.Item == item || recipient.Item == signal.source) { continue; }
signal.source?.LastSentSignalRecipients.Add(recipient);
// Use ToArray() snapshot for thread-safe iteration
foreach (ItemComponent ic in recipient.Item.Components.ToArray())
foreach (ItemComponent ic in recipient.Item.Components)
{
//other junction boxes don't need to receive the signal in the pass-through signal connections
//because we relay it straight to the connected items without going through the whole chain of junction boxes
@@ -485,8 +471,7 @@ namespace Barotrauma.Items.Components
if (recipient.Effects != null && signal.value != "0" && !string.IsNullOrEmpty(signal.value))
{
// Use ToArray() snapshot for thread-safe iteration
foreach (StatusEffect effect in recipient.Effects.ToArray())
foreach (StatusEffect effect in recipient.Effects)
{
recipient.Item.ApplyStatusEffect(effect, ActionType.OnUse, 1.0f);
}
@@ -499,7 +484,7 @@ namespace Barotrauma.Items.Components
base.RemoveComponentSpecific();
connectedRecipients?.Clear();
connectionDirty?.Clear();
_recipientsToRefresh.Clear();
recipientsToRefresh.Clear();
}
}
}
@@ -1,6 +1,4 @@
using System;
using System.Collections.Concurrent;
using System.Threading;
using Microsoft.Xna.Framework;
using System.Collections.Generic;
using System.Linq;
@@ -64,77 +62,17 @@ namespace Barotrauma.Items.Components
protected const float UpdateInterval = (float)Timing.Step;
/// <summary>
/// List of all powered ItemComponents (thread-safe)
/// List of all powered ItemComponents
/// </summary>
private static readonly ConcurrentDictionary<Powered, byte> _poweredDict = new ConcurrentDictionary<Powered, byte>();
/// <summary>
/// Cached list for iteration - updated when collection changes
/// </summary>
private static volatile List<Powered> _cachedPoweredList;
private static int _poweredListVersion;
private static readonly List<Powered> poweredList = new List<Powered>();
public static IEnumerable<Powered> PoweredList
{
get
{
var cached = _cachedPoweredList;
if (cached != null) return cached;
return GetCachedPoweredList();
}
}
private static List<Powered> GetCachedPoweredList()
{
var newList = _poweredDict.Keys.ToList();
_cachedPoweredList = newList;
return newList;
}
private static void InvalidatePoweredListCache()
{
_cachedPoweredList = null;
Interlocked.Increment(ref _poweredListVersion);
get { return poweredList; }
}
/// <summary>
/// Thread-safe set of changed connections
/// </summary>
private static readonly ConcurrentDictionary<Connection, byte> _changedConnections = new ConcurrentDictionary<Connection, byte>();
/// <summary>
/// Gets all changed connections (snapshot)
/// </summary>
public static ICollection<Connection> ChangedConnections => _changedConnections.Keys;
/// <summary>
/// Add a connection to the changed set
/// </summary>
public static void MarkConnectionChanged(Connection c)
{
_changedConnections.TryAdd(c, 0);
}
/// <summary>
/// Clear all changed connections
/// </summary>
public static void ClearChangedConnections()
{
_changedConnections.Clear();
}
/// <summary>
/// Remove a connection from the changed set
/// </summary>
public static void UnmarkConnectionChanged(Connection c)
{
_changedConnections.TryRemove(c, out _);
}
public static readonly HashSet<Connection> ChangedConnections = new HashSet<Connection>();
/// <summary>
/// Thread-safe grid dictionary
/// </summary>
public readonly static ConcurrentDictionary<int, GridInfo> Grids = new ConcurrentDictionary<int, GridInfo>();
public readonly static Dictionary<int, GridInfo> Grids = new Dictionary<int, GridInfo>();
/// <summary>
/// The amount of power currently consumed by the item. Negative values mean that the item is providing power to connected items
@@ -271,8 +209,7 @@ namespace Barotrauma.Items.Components
public Powered(Item item, ContentXElement element)
: base(item, element)
{
_poweredDict.TryAdd(this, 0);
InvalidatePoweredListCache();
poweredList.Add(this);
InitProjectSpecific(element);
}
@@ -385,20 +322,17 @@ namespace Barotrauma.Items.Components
//don't use cache if there are no existing grids
if (Grids.Count > 0 && useCache)
{
// Take a snapshot of changed connections for iteration
var changedSnapshot = ChangedConnections.ToList();
//delete all grids that were affected
foreach (Connection c in changedSnapshot)
foreach (Connection c in ChangedConnections)
{
if (c.Grid != null)
{
Grids.TryRemove(c.Grid.ID, out _);
Grids.Remove(c.Grid.ID);
c.Grid = null;
}
}
foreach (Connection c in changedSnapshot)
foreach (Connection c in ChangedConnections)
{
//Make sure the connection grid hasn't been resolved by another connection update
//Ensure the connection has other connections
@@ -412,7 +346,7 @@ namespace Barotrauma.Items.Components
else
{
//Clear all grid IDs from connections
foreach (Powered powered in PoweredList)
foreach (Powered powered in poweredList)
{
//Only check devices with connectors
if (powered.powerIn != null)
@@ -427,7 +361,7 @@ namespace Barotrauma.Items.Components
Grids.Clear();
foreach (Powered powered in PoweredList)
foreach (Powered powered in poweredList)
{
if (powered.Item.Condition <= 0f) { continue; }
@@ -458,7 +392,7 @@ namespace Barotrauma.Items.Components
}
//Clear changed connections after each update
ClearChangedConnections();
ChangedConnections.Clear();
}
private static GridInfo PropagateGrid(Connection conn)
@@ -488,8 +422,8 @@ namespace Barotrauma.Items.Components
c.Grid = grid;
grid.AddConnection(c);
//Add on recipients - use ToList() snapshot for thread-safe iteration
foreach (Connection otherC in c.Recipients.ToList())
//Add on recipients
foreach (Connection otherC in c.Recipients)
{
//Only add valid connections
if (otherC.Grid != grid && (otherC.Grid == null || !Grids.ContainsKey(otherC.Grid.ID)) && ValidPowerConnection(c, otherC))
@@ -560,7 +494,7 @@ namespace Barotrauma.Items.Components
}
//Determine if devices are adding a load or providing power, also resolve solo nodes
foreach (Powered powered in PoweredList)
foreach (Powered powered in poweredList)
{
//Make voltage decay to ensure the device powers down.
//This only effects devices with no power input (whose voltage is set by other means, e.g. status effects from a contained battery)
@@ -796,8 +730,7 @@ namespace Barotrauma.Items.Components
{
if (item.Connections != null && powerIn != null)
{
// Use ToList() snapshot for thread-safe iteration
foreach (Connection recipient in powerIn.Recipients.ToList())
foreach (Connection recipient in powerIn.Recipients)
{
if (!recipient.IsPower || !recipient.IsOutput) { continue; }
if (recipient.Item?.GetComponent<PowerContainer>() is PowerContainer battery)
@@ -817,14 +750,13 @@ namespace Barotrauma.Items.Components
{
if (c.IsPower && c.Grid != null)
{
MarkConnectionChanged(c);
ChangedConnections.Add(c);
}
}
}
base.RemoveComponentSpecific();
_poweredDict.TryRemove(this, out _);
InvalidatePoweredListCache();
poweredList.Remove(this);
}
}
@@ -848,9 +780,9 @@ namespace Barotrauma.Items.Components
Connections.Remove(c);
//Remove the grid if it has no devices
if (Connections.Count == 0)
if (Connections.Count == 0 && Powered.Grids.ContainsKey(ID))
{
Powered.Grids.TryRemove(ID, out _);
Powered.Grids.Remove(ID);
}
}
@@ -5,7 +5,6 @@ using FarseerPhysics.Dynamics.Contacts;
using FarseerPhysics.Dynamics.Joints;
using Microsoft.Xna.Framework;
using System;
using System.Collections.Concurrent;
using System.Collections.Generic;
using System.Collections.Immutable;
using System.Linq;
@@ -73,7 +72,7 @@ namespace Barotrauma.Items.Components
public const float WaterDragCoefficient = 0.1f;
private readonly ConcurrentQueue<Impact> impactQueue = new ConcurrentQueue<Impact>();
private readonly Queue<Impact> impactQueue = new Queue<Impact>();
private bool removePending;
@@ -841,8 +840,9 @@ namespace Barotrauma.Items.Components
DisableProjectileCollisions();
}
}
while (impactQueue.TryDequeue(out var impact))
while (impactQueue.Count > 0)
{
var impact = impactQueue.Dequeue();
HandleProjectileCollision(impact.Fixture, impact.Normal, impact.LinearVelocity);
}
@@ -456,8 +456,7 @@ namespace Barotrauma.Items.Components
item.SendSignal(conditionSignal, "condition_out");
// Use ToArray() snapshot for thread-safe iteration
foreach (var component in item.Components.ToArray())
foreach (var component in item.Components)
{
if (component is IDeteriorateUnderStress deteriorateUnderStress)
{
@@ -714,8 +713,7 @@ namespace Barotrauma.Items.Components
#endif
if (LastActiveTime > Timing.TotalTime) { return true; }
// Use ToArray() snapshot for thread-safe iteration
foreach (ItemComponent ic in item.Components.ToArray())
foreach (ItemComponent ic in item.Components)
{
if (ic is Fabricator || ic is Deconstructor)
{
@@ -763,8 +761,7 @@ namespace Barotrauma.Items.Components
private float GetDeteriorationDelayMultiplier()
{
// Use ToArray() snapshot for thread-safe iteration
foreach (ItemComponent ic in item.Components.ToArray())
foreach (ItemComponent ic in item.Components)
{
if (ic is Engine engine)
{
@@ -222,14 +222,13 @@ namespace Barotrauma.Items.Components
public void SetRecipientsDirty()
{
recipientsDirty = true;
if (IsPower) { Powered.MarkConnectionChanged(this); }
if (IsPower) { Powered.ChangedConnections.Add(this); }
}
private void RefreshRecipients()
{
recipients.Clear();
// Use ToArray() snapshot for thread-safe iteration
foreach (var wire in wires.ToArray())
foreach (var wire in wires)
{
Connection recipient = wire.OtherConnection(this);
if (recipient != null) { recipients.Add(recipient); }
@@ -268,8 +267,8 @@ namespace Barotrauma.Items.Components
//Check if both connections belong to a larger grid
if (prevOtherConnection.recipients.Count > 1 && recipients.Count > 1)
{
Powered.MarkConnectionChanged(prevOtherConnection);
Powered.MarkConnectionChanged(this);
Powered.ChangedConnections.Add(prevOtherConnection);
Powered.ChangedConnections.Add(this);
}
else if (recipients.Count > 1)
{
@@ -285,7 +284,7 @@ namespace Barotrauma.Items.Components
else if (Grid.Connections.Count == 2)
{
//Delete the grid as these were the only 2 devices
Powered.Grids.TryRemove(Grid.ID, out _);
Powered.Grids.Remove(Grid.ID);
Grid = null;
prevOtherConnection.Grid = null;
}
@@ -326,8 +325,8 @@ namespace Barotrauma.Items.Components
else
{
//Flag change so that proper grids can be formed
Powered.MarkConnectionChanged(this);
Powered.MarkConnectionChanged(otherConnection);
Powered.ChangedConnections.Add(this);
Powered.ChangedConnections.Add(otherConnection);
}
}
@@ -340,8 +339,7 @@ namespace Barotrauma.Items.Components
{
LastSentSignal = signal;
enumeratingWires = true;
// Use ToArray() snapshot for thread-safe iteration
foreach (var wire in wires.ToArray())
foreach (var wire in wires)
{
Connection recipient = wire.OtherConnection(this);
if (recipient == null) { continue; }
@@ -356,14 +354,14 @@ namespace Barotrauma.Items.Components
GameMain.LuaCs.Hook.Call("signalReceived." + recipient.item.Prefab.Identifier, signal, recipient);
}
foreach (CircuitBoxConnection connection in CircuitBoxConnections.ToArray())
foreach (CircuitBoxConnection connection in CircuitBoxConnections)
{
connection.ReceiveSignal(signal);
GameMain.LuaCs.Hook.Call("signalReceived", signal, connection.Connection);
GameMain.LuaCs.Hook.Call("signalReceived." + connection.Connection.Item.Prefab.Identifier, signal, connection);
}
enumeratingWires = false;
foreach (var removedWire in removedWires.ToArray())
foreach (var removedWire in removedWires)
{
wires.Remove(removedWire);
}
@@ -374,16 +372,14 @@ namespace Barotrauma.Items.Components
{
conn.LastReceivedSignal = signal;
// Use ToArray() snapshot for thread-safe iteration
foreach (ItemComponent ic in conn.item.Components.ToArray())
foreach (ItemComponent ic in conn.item.Components)
{
ic.ReceiveSignal(signal, conn);
}
if (conn.Effects == null || signal.value == "0") { return; }
// Use ToArray() snapshot for thread-safe iteration
foreach (StatusEffect effect in conn.Effects.ToArray())
foreach (StatusEffect effect in conn.Effects)
{
conn.Item.ApplyStatusEffect(effect, ActionType.OnUse, (float)Timing.Step);
}
@@ -393,15 +389,13 @@ namespace Barotrauma.Items.Components
{
if (IsPower && Grid != null)
{
Powered.MarkConnectionChanged(this);
// Use ToArray() snapshot for thread-safe iteration
foreach (Connection c in recipients.ToArray())
Powered.ChangedConnections.Add(this);
foreach (Connection c in recipients)
{
Powered.MarkConnectionChanged(c);
Powered.ChangedConnections.Add(c);
}
}
// Use ToArray() snapshot for thread-safe iteration
foreach (var wire in wires.ToArray())
foreach (var wire in wires)
{
wire.RemoveConnection(this);
recipientsDirty = true;
@@ -409,7 +403,7 @@ namespace Barotrauma.Items.Components
if (enumeratingWires)
{
foreach (var wire in wires.ToArray())
foreach (var wire in wires)
{
removedWires.Add(wire);
}
@@ -452,8 +446,7 @@ namespace Barotrauma.Items.Components
{
XElement newElement = new XElement(IsOutput ? "output" : "input", new XAttribute("name", Name));
// Use ToArray() snapshot before OrderBy for thread-safe iteration
foreach (var wire in wires.ToArray().OrderBy(w => w.Item.ID))
foreach (var wire in wires.OrderBy(w => w.Item.ID))
{
newElement.Add(new XElement("link",
new XAttribute("w", wire.Item.ID.ToString()),
@@ -148,16 +148,14 @@ namespace Barotrauma.Items.Components
Vector2 wireNodeOffset = item.Submarine == null ? Vector2.Zero : item.Submarine.HiddenSubPosition + amount;
foreach (Connection c in Connections)
{
// Use ToArray() snapshot for thread-safe iteration
foreach (Wire wire in c.Wires.ToArray())
foreach (Wire wire in c.Wires)
{
if (wire == null) { continue; }
TryMoveWire(wire);
}
}
// Use ToList() snapshot for thread-safe iteration
foreach (var wire in DisconnectedWires.ToList())
foreach (var wire in DisconnectedWires)
{
TryMoveWire(wire);
}
@@ -389,7 +387,7 @@ namespace Barotrauma.Items.Components
}
foreach (var connection in Connections)
{
Powered.UnmarkConnectionChanged(connection);
Powered.ChangedConnections.Remove(connection);
connection.Recipients.Clear();
}
Connections.Clear();
@@ -414,19 +412,15 @@ namespace Barotrauma.Items.Components
msg.WriteByte((byte)Connections.Count);
foreach (Connection connection in Connections)
{
// Use ToArray() snapshot for thread-safe iteration
var wiresSnapshot = connection.Wires.ToArray();
msg.WriteVariableUInt32((uint)wiresSnapshot.Length);
foreach (Wire wire in wiresSnapshot)
msg.WriteVariableUInt32((uint)connection.Wires.Count);
foreach (Wire wire in connection.Wires)
{
msg.WriteUInt16(wire?.Item == null ? (ushort)0 : wire.Item.ID);
}
}
// Use ToList() snapshot for thread-safe iteration
var disconnectedSnapshot = DisconnectedWires.ToList();
msg.WriteUInt16((ushort)disconnectedSnapshot.Count);
foreach (Wire disconnectedWire in disconnectedSnapshot)
msg.WriteUInt16((ushort)DisconnectedWires.Count);
foreach (Wire disconnectedWire in DisconnectedWires)
{
msg.WriteUInt16(disconnectedWire.Item.ID);
}
@@ -1,8 +1,6 @@
using System;
using System.Collections.Concurrent;
using System.Collections.Generic;
using System.Globalization;
using System.Linq;
using System.Xml.Linq;
using Microsoft.Xna.Framework;
namespace Barotrauma.Items.Components
@@ -27,8 +25,7 @@ namespace Barotrauma.Items.Components
private int signalQueueSize;
private int delayTicks;
// Thread-safe queue for concurrent access
private readonly ConcurrentQueue<DelayedSignal> signalQueue = new ConcurrentQueue<DelayedSignal>();
private readonly Queue<DelayedSignal> signalQueue = new Queue<DelayedSignal>();
private DelayedSignal prevQueuedSignal;
@@ -43,8 +40,7 @@ namespace Barotrauma.Items.Components
delay = value;
delayTicks = (int)(delay / Timing.Step);
signalQueueSize = Math.Max(delayTicks, 1) * 2;
// ConcurrentQueue doesn't have Clear(), drain it instead
while (signalQueue.TryDequeue(out _)) { }
signalQueue.Clear();
}
}
@@ -70,19 +66,19 @@ namespace Barotrauma.Items.Components
public override void Update(float deltaTime, Camera cam)
{
if (signalQueue.IsEmpty)
if (signalQueue.Count == 0)
{
IsActive = false;
return;
}
// Use ToArray() snapshot for thread-safe iteration
foreach (var val in signalQueue.ToArray())
foreach (var val in signalQueue)
{
val.SendTimer -= 1;
}
while (signalQueue.TryPeek(out var signalOut) && signalOut.SendTimer <= 0)
while (signalQueue.Count > 0 && signalQueue.Peek().SendTimer <= 0)
{
var signalOut = signalQueue.Peek();
signalOut.SendDuration -= 1;
item.SendSignal(new Signal(signalOut.Signal.value, sender: signalOut.Signal.sender, strength: signalOut.Signal.strength), "signal_out");
if (signalOut.SendDuration <= 0)
@@ -104,15 +100,11 @@ namespace Barotrauma.Items.Components
{
case "signal_in":
if (signalQueue.Count >= signalQueueSize) { return; }
if (ResetWhenSignalReceived)
{
prevQueuedSignal = null;
while (signalQueue.TryDequeue(out _)) { }
}
if (ResetWhenDifferentSignalReceived && signalQueue.TryPeek(out var peekSignal) && peekSignal.Signal.value != signal.value)
if (ResetWhenSignalReceived) { prevQueuedSignal = null; signalQueue.Clear(); }
if (ResetWhenDifferentSignalReceived && signalQueue.Count > 0 && signalQueue.Peek().Signal.value != signal.value)
{
prevQueuedSignal = null;
while (signalQueue.TryDequeue(out _)) { }
signalQueue.Clear();
}
if (prevQueuedSignal != null &&
@@ -135,10 +127,10 @@ namespace Barotrauma.Items.Components
if (float.TryParse(signal.value, NumberStyles.Any, CultureInfo.InvariantCulture, out float newDelay))
{
newDelay = MathHelper.Clamp(newDelay, 0, 60);
if (!signalQueue.IsEmpty && newDelay != Delay)
if (signalQueue.Count > 0 && newDelay != Delay)
{
prevQueuedSignal = null;
while (signalQueue.TryDequeue(out _)) { }
signalQueue.Clear();
}
Delay = newDelay;
}
@@ -2,7 +2,6 @@
using Microsoft.Xna.Framework;
using System;
using System.Collections.Generic;
using System.Collections.Immutable;
using System.Linq;
using System.Xml.Linq;
@@ -32,8 +31,7 @@ namespace Barotrauma.Items.Components
private float thirdInverseMax = 0, loadEqnConstant = 0;
// Thread-safe immutable dictionary for connection pairs (read-only after initialization)
private static readonly ImmutableDictionary<string, string> connectionPairs = new Dictionary<string, string>
private static readonly Dictionary<string, string> connectionPairs = new Dictionary<string, string>
{
{ "power_in", "power_out"},
{ "signal_in", "signal_out" },
@@ -42,7 +40,7 @@ namespace Barotrauma.Items.Components
{ "signal_in3", "signal_out3" },
{ "signal_in4", "signal_out4" },
{ "signal_in5", "signal_out5" }
}.ToImmutableDictionary();
};
protected override PowerPriority Priority { get { return PowerPriority.Relay; } }
@@ -1,7 +1,6 @@
using Barotrauma.Networking;
using Microsoft.Xna.Framework;
using System;
using System.Collections.Concurrent;
using System.Collections.Generic;
using System.Globalization;
using System.Linq;
@@ -11,8 +10,7 @@ namespace Barotrauma.Items.Components
{
partial class WifiComponent : ItemComponent, IServerSerializable, IClientSerializable
{
private static readonly ConcurrentDictionary<WifiComponent, byte> _wifiDict = new ConcurrentDictionary<WifiComponent, byte>();
private static IEnumerable<WifiComponent> AllWifiComponents => _wifiDict.Keys;
private static readonly List<WifiComponent> list = new List<WifiComponent>();
const int ChannelMemorySize = 10;
@@ -113,7 +111,7 @@ namespace Barotrauma.Items.Components
public WifiComponent(Item item, ContentXElement element)
: base (item, element)
{
_wifiDict.TryAdd(this, 0);
list.Add(this);
IsActive = true;
}
@@ -158,7 +156,7 @@ namespace Barotrauma.Items.Components
/// </summary>
public IEnumerable<WifiComponent> GetReceiversInRange()
{
return AllWifiComponents.Where(w => w != this && w.CanReceive(this));
return list.Where(w => w != this && w.CanReceive(this));
}
public bool CanReceive(WifiComponent sender)
@@ -187,7 +185,7 @@ namespace Barotrauma.Items.Components
/// </summary>
public IEnumerable<WifiComponent> GetTransmittersInRange()
{
return AllWifiComponents.Where(w => w != this && w.CanTransmit(this));
return list.Where(w => w != this && w.CanTransmit(this));
}
public bool CanTransmit(WifiComponent sender)
@@ -277,8 +275,7 @@ namespace Barotrauma.Items.Components
if (signal.source != null)
{
// Use ToList() snapshot for thread-safe iteration
foreach (Connection receiver in wifiComp.item.LastSentSignalRecipients.ToList())
foreach (Connection receiver in wifiComp.item.LastSentSignalRecipients)
{
if (!signal.source.LastSentSignalRecipients.Contains(receiver))
{
@@ -369,7 +366,7 @@ namespace Barotrauma.Items.Components
protected override void RemoveComponentSpecific()
{
base.RemoveComponentSpecific();
_wifiDict.TryRemove(this, out _);
list.Remove(this);
}
public override XElement Save(XElement parentElement)
@@ -250,8 +250,7 @@ namespace Barotrauma.Items.Components
{
if (connections[0] != null && connections[1] != null)
{
// Use ToArray() snapshot for thread-safe iteration
foreach (ItemComponent ic in item.Components.ToArray())
foreach (ItemComponent ic in item.Components)
{
if (ic == this) { continue; }
@@ -724,11 +723,11 @@ namespace Barotrauma.Items.Components
if (item0 == null && item1 != null)
{
item0 = Item.ItemList.FirstOrDefault(it => it.GetComponent<ConnectionPanel>()?.DisconnectedWires.Contains(this) ?? false);
item0 = Item.ItemList.Find(it => it.GetComponent<ConnectionPanel>()?.DisconnectedWires.Contains(this) ?? false);
}
else if (item0 != null && item1 == null)
{
item1 = Item.ItemList.FirstOrDefault(it => it.GetComponent<ConnectionPanel>()?.DisconnectedWires.Contains(this) ?? false);
item1 = Item.ItemList.Find(it => it.GetComponent<ConnectionPanel>()?.DisconnectedWires.Contains(this) ?? false);
}
if (item0 == null || item1 == null || nodes.Count == 0) { return; }
@@ -73,11 +73,6 @@ namespace Barotrauma.Items.Components
public PhysicsBody PhysicsBody { get; private set; }
/// <summary>
/// Flag to prevent multiple queued refresh requests.
/// </summary>
private volatile bool physicsBodyRefreshQueued;
private float radius;
[Editable, Serialize(0.0f, IsPropertySaveable.Yes)]
public float Radius
@@ -88,7 +83,7 @@ namespace Barotrauma.Items.Components
{
if (radius == value) { return; }
radius = value;
if (PhysicsBody != null) { QueuePhysicsBodyRefresh(); }
if (PhysicsBody != null) { RefreshPhysicsBodySize(); }
}
}
@@ -102,7 +97,7 @@ namespace Barotrauma.Items.Components
{
if (width == value) { return; }
width = value;
if (PhysicsBody != null) { QueuePhysicsBodyRefresh(); }
if (PhysicsBody != null) { RefreshPhysicsBodySize(); }
}
}
@@ -116,28 +111,10 @@ namespace Barotrauma.Items.Components
{
if (height == value) { return; }
height = value;
if (PhysicsBody != null) { QueuePhysicsBodyRefresh(); }
if (PhysicsBody != null) { RefreshPhysicsBodySize(); }
}
}
/// <summary>
/// Queue the physics body refresh to be executed on the main thread.
/// This is necessary because physics body operations are not thread-safe.
/// </summary>
private void QueuePhysicsBodyRefresh()
{
if (physicsBodyRefreshQueued) { return; }
physicsBodyRefreshQueued = true;
PhysicsBodyQueue.EnqueueCreation(() =>
{
if (!item.Removed)
{
RefreshPhysicsBodySize();
}
physicsBodyRefreshQueued = false;
});
}
private float currentRadius, currentWidth, currentHeight;
private Vector2 bodyOffset;
@@ -312,18 +289,13 @@ namespace Barotrauma.Items.Components
Matrix transform = Matrix.CreateRotationZ(-item.RotationRad);
offset = Vector2.Transform(offset, transform);
}
// Defer physics operations if in parallel context (Farseer is not thread-safe)
var capturedBody = PhysicsBody;
var capturedPos = item.SimPosition + offset;
var capturedRot = -item.RotationRad;
if (ignoreContacts)
{
PhysicsBodyQueue.ExecuteOrDefer(() => capturedBody.SetTransformIgnoreContacts(capturedPos, capturedRot));
PhysicsBody.SetTransformIgnoreContacts(item.SimPosition + offset, -item.RotationRad);
}
else
{
PhysicsBodyQueue.ExecuteOrDefer(() => capturedBody.SetTransform(capturedPos, capturedRot));
PhysicsBody.SetTransform(item.SimPosition + offset, -item.RotationRad);
}
PhysicsBody.UpdateDrawPosition();
}