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

This reverts commit 046483b9da.
This commit is contained in:
NotAlwaysTrue
2026-04-30 21:59:54 +08:00
parent 02689d0d86
commit 25683dcf39
85 changed files with 2413 additions and 779 deletions
@@ -1,26 +1,56 @@
using Barotrauma.Items.Components;
using Microsoft.Xna.Framework;
using System;
using System.Collections.Concurrent;
using System.Collections.Generic;
using System.Linq;
using System.Threading;
namespace Barotrauma
{
class DelayedListElement
{
public readonly long Id;
public readonly DelayedEffect Parent;
public readonly Entity Entity;
public Vector2? WorldPosition;
private Vector2? _worldPosition;
private readonly object _worldPositionLock = new object();
public Vector2? WorldPosition
{
get
{
lock (_worldPositionLock)
{
return _worldPosition;
}
}
set
{
lock (_worldPositionLock)
{
_worldPosition = value;
}
}
}
/// <summary>
/// Should the delayed effect attempt to determine the position of the effect based on the targets, or just use the position that was passed to the constructor.
/// </summary>
public bool GetPositionBasedOnTargets;
public readonly Vector2? StartPosition;
public readonly List<ISerializableEntity> Targets;
public float Delay;
private volatile float _delay;
public float Delay
{
get => _delay;
set => _delay = value;
}
public DelayedListElement(DelayedEffect parentEffect, Entity parentEntity, IEnumerable<ISerializableEntity> targets, float delay, Vector2? worldPosition, Vector2? startPosition)
{
Id = Interlocked.Increment(ref DelayedEffect._delayElementIdCounter);
Parent = parentEffect;
Entity = parentEntity;
Targets = new List<ISerializableEntity>(targets);
@@ -29,9 +59,19 @@ namespace Barotrauma
StartPosition = startPosition;
}
}
class DelayedEffect : StatusEffect
{
public static readonly List<DelayedListElement> DelayList = new List<DelayedListElement>();
// Thread-safe counter for generating unique IDs for DelayedListElement
internal static long _delayElementIdCounter;
// Thread-safe dictionary for delayed effects
public static readonly ConcurrentDictionary<long, DelayedListElement> DelayListDict = new ConcurrentDictionary<long, DelayedListElement>();
/// <summary>
/// Provides a thread-safe enumerable view of the delay list for iteration.
/// </summary>
public static IEnumerable<DelayedListElement> DelayList => DelayListDict.Values;
private enum DelayTypes
{
@@ -62,9 +102,10 @@ namespace Barotrauma
if (this.type != type || !HasRequiredItems(entity)) { return; }
if (!Stackable)
{
foreach (var existingEffect in DelayList)
// Thread-safe iteration over ConcurrentDictionary
foreach (var kvp in DelayListDict)
{
if (existingEffect.Parent == this && existingEffect.Targets.FirstOrDefault() == target)
if (kvp.Value.Parent == this && kvp.Value.Targets.FirstOrDefault() == target)
{
return;
}
@@ -72,18 +113,19 @@ namespace Barotrauma
}
if (!IsValidTarget(target)) { return; }
currentTargets.Clear();
currentTargets.Add(target);
if (!HasRequiredConditions(currentTargets)) { return; }
var targets = CurrentTargets;
targets.Clear();
targets.Add(target);
if (!HasRequiredConditions(targets)) { return; }
switch (delayType)
{
case DelayTypes.Timer:
var newDelayListElement = new DelayedListElement(this, entity, currentTargets, delay, worldPosition ?? GetPosition(entity, currentTargets, worldPosition), startPosition: null)
var newDelayListElement = new DelayedListElement(this, entity, targets, delay, worldPosition ?? GetPosition(entity, targets, worldPosition), startPosition: null)
{
GetPositionBasedOnTargets = worldPosition == null
};
DelayList.Add(newDelayListElement);
DelayListDict.TryAdd(newDelayListElement.Id, newDelayListElement);
break;
case DelayTypes.ReachCursor:
Projectile projectile = (entity as Item)?.GetComponent<Projectile>();
@@ -105,7 +147,8 @@ namespace Barotrauma
return;
}
DelayList.Add(new DelayedListElement(this, entity, currentTargets, Vector2.Distance(entity.WorldPosition, projectile.User.CursorWorldPosition), worldPosition, entity.WorldPosition));
var reachCursorElement = new DelayedListElement(this, entity, targets, Vector2.Distance(entity.WorldPosition, projectile.User.CursorWorldPosition), worldPosition, entity.WorldPosition);
DelayListDict.TryAdd(reachCursorElement.Id, reachCursorElement);
break;
}
}
@@ -119,25 +162,28 @@ namespace Barotrauma
if (delayType == DelayTypes.ReachCursor && Character.Controlled == null) { return; }
if (!Stackable)
{
foreach (var existingEffect in DelayList)
// Thread-safe iteration over ConcurrentDictionary
foreach (var kvp in DelayListDict)
{
if (existingEffect.Parent == this && existingEffect.Targets.SequenceEqual(targets)) { return; }
if (kvp.Value.Parent == this && kvp.Value.Targets.SequenceEqual(targets)) { return; }
}
}
currentTargets.Clear();
var localTargets = CurrentTargets;
localTargets.Clear();
foreach (ISerializableEntity target in targets)
{
if (!IsValidTarget(target)) { continue; }
currentTargets.Add(target);
localTargets.Add(target);
}
if (!HasRequiredConditions(currentTargets)) { return; }
if (!HasRequiredConditions(localTargets)) { return; }
switch (delayType)
{
case DelayTypes.Timer:
DelayList.Add(new DelayedListElement(this, entity, currentTargets, delay, worldPosition, null));
var timerElement = new DelayedListElement(this, entity, localTargets, delay, worldPosition, null);
DelayListDict.TryAdd(timerElement.Id, timerElement);
break;
case DelayTypes.ReachCursor:
Projectile projectile = (entity as Item)?.GetComponent<Projectile>();
@@ -161,19 +207,21 @@ namespace Barotrauma
return;
}
DelayList.Add(new DelayedListElement(this, entity, currentTargets, Vector2.Distance(entity.WorldPosition, user.CursorWorldPosition), worldPosition, entity.WorldPosition));
var reachCursorElement = new DelayedListElement(this, entity, localTargets, Vector2.Distance(entity.WorldPosition, user.CursorWorldPosition), worldPosition, entity.WorldPosition);
DelayListDict.TryAdd(reachCursorElement.Id, reachCursorElement);
break;
}
}
public static void Update(float deltaTime)
{
for (int i = DelayList.Count - 1; i >= 0; i--)
// Thread-safe iteration over ConcurrentDictionary
foreach (var kvp in DelayListDict)
{
DelayedListElement element = DelayList[i];
DelayedListElement element = kvp.Value;
if (element.Parent.CheckConditionalAlways && !element.Parent.HasRequiredConditions(element.Targets))
{
DelayList.Remove(element);
DelayListDict.TryRemove(element.Id, out _);
continue;
}
@@ -187,7 +235,7 @@ namespace Barotrauma
//keep refreshing the position until the effect runs (so e.g. a delayed effect runs at the last known position of a monster before it despawned)
if (element.GetPositionBasedOnTargets && element.Entity is { Removed: false })
{
element.WorldPosition = element.Parent.GetPosition(element.Entity, element.Parent.currentTargets);
element.WorldPosition = element.Parent.GetPosition(element.Entity, element.Parent.CurrentTargets);
}
continue;
}
@@ -198,8 +246,8 @@ namespace Barotrauma
}
element.Parent.Apply(deltaTime, element.Entity, element.Targets, element.WorldPosition);
DelayList.Remove(element);
DelayListDict.TryRemove(element.Id, out _);
}
}
}
}
}
@@ -7,15 +7,18 @@ using FarseerPhysics.Dynamics;
using Microsoft.Xna.Framework;
using Steamworks;
using System;
using System.Collections.Concurrent;
using System.Collections.Generic;
using System.Collections.Immutable;
using System.Linq;
using System.Threading;
using System.Xml.Linq;
namespace Barotrauma
{
class DurationListElement
{
public readonly long Id;
public readonly StatusEffect Parent;
public readonly Entity Entity;
public float Duration
@@ -24,12 +27,24 @@ namespace Barotrauma
private set;
}
public readonly List<ISerializableEntity> Targets;
public Character User { get; private set; }
private volatile Character _user;
public Character User
{
get => _user;
private set => _user = value;
}
public float Timer;
private volatile float _timer;
public float Timer
{
get => _timer;
set => _timer = value;
}
public DurationListElement(StatusEffect parentEffect, Entity parentEntity, IEnumerable<ISerializableEntity> targets, float duration, Character user)
{
Id = Interlocked.Increment(ref StatusEffect._durationElementIdCounter);
Parent = parentEffect;
Entity = parentEntity;
Targets = new List<ISerializableEntity>(targets);
@@ -39,8 +54,9 @@ namespace Barotrauma
public void Reset(float duration, Character newUser)
{
Timer = Duration = duration;
User = newUser;
Duration = duration;
Volatile.Write(ref _timer, duration);
_user = newUser;
}
}
@@ -601,14 +617,23 @@ namespace Barotrauma
private readonly float lifeTime;
private float lifeTimer;
private Dictionary<Entity, float> intervalTimers;
private ConcurrentDictionary<Entity, float> intervalTimers;
/// <summary>
/// Makes the effect only execute once. After it has executed, it'll never execute again (during the same round).
/// </summary>
private readonly bool oneShot;
public static readonly List<DurationListElement> DurationList = new List<DurationListElement>();
// Thread-safe counter for generating unique IDs for DurationListElement
internal static long _durationElementIdCounter;
// Thread-safe dictionary for duration effects
public static readonly ConcurrentDictionary<long, DurationListElement> DurationListDict = new ConcurrentDictionary<long, DurationListElement>();
/// <summary>
/// Provides a thread-safe enumerable view of the duration list for iteration.
/// </summary>
public static IEnumerable<DurationListElement> DurationList => DurationListDict.Values;
/// <summary>
/// Only applicable for StatusEffects with a duration or delay. Should the conditional checks only be done when the effect triggers,
@@ -1631,22 +1656,52 @@ namespace Barotrauma
}
}
private static readonly List<Entity> intervalsToRemove = new List<Entity>();
// Thread-local list to avoid contention when cleaning up removed entities
[ThreadStatic]
private static List<Entity> _threadLocalIntervalsToRemove;
private static List<Entity> IntervalsToRemove
{
get
{
_threadLocalIntervalsToRemove ??= new List<Entity>();
return _threadLocalIntervalsToRemove;
}
}
public bool ShouldWaitForInterval(Entity entity, float deltaTime)
{
if (Interval > 0.0f && entity != null && intervalTimers != null)
if (Interval > 0.0f && entity != null)
{
if (intervalTimers.ContainsKey(entity))
// Thread-safe lazy initialization
if (intervalTimers == null)
{
intervalTimers[entity] -= deltaTime;
if (intervalTimers[entity] > 0.0f) { return true; }
Interlocked.CompareExchange(ref intervalTimers, new ConcurrentDictionary<Entity, float>(), null);
}
intervalsToRemove.Clear();
intervalsToRemove.AddRange(intervalTimers.Keys.Where(e => e.Removed));
foreach (var toRemove in intervalsToRemove)
if (intervalTimers.TryGetValue(entity, out float currentTimer))
{
intervalTimers.Remove(toRemove);
float newTimer = currentTimer - deltaTime;
if (newTimer > 0.0f)
{
intervalTimers.AddOrUpdate(entity, newTimer, (_, __) => newTimer);
return true;
}
}
// Clean up removed entities using thread-local list
var toRemove = IntervalsToRemove;
toRemove.Clear();
foreach (var key in intervalTimers.Keys)
{
if (key.Removed)
{
toRemove.Add(key);
}
}
foreach (var key in toRemove)
{
intervalTimers.TryRemove(key, out _);
}
}
return false;
@@ -1662,7 +1717,7 @@ namespace Barotrauma
if (Duration > 0.0f && !Stackable)
{
//ignore if not stackable and there's already an identical statuseffect
DurationListElement existingEffect = DurationList.Find(d => d.Parent == this && d.Targets.FirstOrDefault() == target);
DurationListElement existingEffect = FindExistingDurationEffect(target);
if (existingEffect != null)
{
if (ResetDurationWhenReapplied)
@@ -1673,30 +1728,74 @@ namespace Barotrauma
}
}
currentTargets.Clear();
currentTargets.Add(target);
if (!HasRequiredConditions(currentTargets)) { return; }
Apply(deltaTime, entity, currentTargets, worldPosition);
var targets = CurrentTargets;
targets.Clear();
targets.Add(target);
if (!HasRequiredConditions(targets)) { return; }
Apply(deltaTime, entity, targets, worldPosition);
}
protected readonly List<ISerializableEntity> currentTargets = new List<ISerializableEntity>();
// Thread-local list to avoid contention when collecting targets
[ThreadStatic]
private static List<ISerializableEntity> _threadLocalCurrentTargets;
protected List<ISerializableEntity> CurrentTargets
{
get
{
_threadLocalCurrentTargets ??= new List<ISerializableEntity>();
return _threadLocalCurrentTargets;
}
}
/// <summary>
/// Thread-safe method to find an existing duration effect for a single target.
/// </summary>
private DurationListElement FindExistingDurationEffect(ISerializableEntity target)
{
foreach (var element in DurationListDict.Values)
{
if (element.Parent == this && element.Targets.FirstOrDefault() == target)
{
return element;
}
}
return null;
}
/// <summary>
/// Thread-safe method to find an existing duration effect for multiple targets.
/// </summary>
private DurationListElement FindExistingDurationEffect(IReadOnlyList<ISerializableEntity> targets)
{
foreach (var element in DurationListDict.Values)
{
if (element.Parent == this && element.Targets.SequenceEqual(targets))
{
return element;
}
}
return null;
}
public virtual void Apply(ActionType type, float deltaTime, Entity entity, IReadOnlyList<ISerializableEntity> targets, Vector2? worldPosition = null)
{
if (Disabled) { return; }
if (this.type != type) { return; }
if (ShouldWaitForInterval(entity, deltaTime)) { return; }
currentTargets.Clear();
var localTargets = CurrentTargets;
localTargets.Clear();
foreach (ISerializableEntity target in targets)
{
if (!IsValidTarget(target)) { continue; }
currentTargets.Add(target);
localTargets.Add(target);
}
if (TargetIdentifiers != null && currentTargets.Count == 0) { return; }
if (TargetIdentifiers != null && localTargets.Count == 0) { return; }
bool hasRequiredItems = HasRequiredItems(entity);
if (!hasRequiredItems || !HasRequiredConditions(currentTargets))
if (!hasRequiredItems || !HasRequiredConditions(localTargets))
{
#if CLIENT
if (!hasRequiredItems && playSoundOnRequiredItemFailure)
@@ -1710,15 +1809,15 @@ namespace Barotrauma
if (Duration > 0.0f && !Stackable)
{
//ignore if not stackable and there's already an identical statuseffect
DurationListElement existingEffect = DurationList.Find(d => d.Parent == this && d.Targets.SequenceEqual(currentTargets));
DurationListElement existingEffect = FindExistingDurationEffect(localTargets);
if (existingEffect != null)
{
existingEffect?.Reset(Math.Max(existingEffect.Timer, Duration), user);
existingEffect.Reset(Math.Max(existingEffect.Timer, Duration), user);
return;
}
}
Apply(deltaTime, entity, currentTargets, worldPosition);
Apply(deltaTime, entity, localTargets, worldPosition);
}
private Hull GetHull(Entity entity)
@@ -1945,7 +2044,8 @@ namespace Barotrauma
if (Duration > 0.0f)
{
DurationList.Add(new DurationListElement(this, entity, targets, Duration, user));
var element = new DurationListElement(this, entity, targets, Duration, user);
DurationListDict.TryAdd(element.Id, element);
}
else
{
@@ -2461,7 +2561,7 @@ namespace Barotrauma
}
if (Interval > 0.0f && entity != null)
{
intervalTimers ??= new Dictionary<Entity, float>();
intervalTimers ??= new ConcurrentDictionary<Entity, float>();
intervalTimers[entity] = Interval;
}
}
@@ -2858,13 +2958,15 @@ namespace Barotrauma
UpdateAllProjSpecific(deltaTime);
DelayedEffect.Update(deltaTime);
for (int i = DurationList.Count - 1; i >= 0; i--)
// Thread-safe iteration over ConcurrentDictionary
foreach (var kvp in DurationListDict)
{
DurationListElement element = DurationList[i];
DurationListElement element = kvp.Value;
if (element.Parent.CheckConditionalAlways && !element.Parent.HasRequiredConditions(element.Targets))
{
DurationList.RemoveAt(i);
DurationListDict.TryRemove(element.Id, out _);
continue;
}
@@ -2873,7 +2975,7 @@ namespace Barotrauma
(t is Limb limb && (limb.character == null || limb.character.Removed)));
if (element.Targets.Count == 0)
{
DurationList.RemoveAt(i);
DurationListDict.TryRemove(element.Id, out _);
continue;
}
@@ -2968,7 +3070,7 @@ namespace Barotrauma
element.Timer -= deltaTime;
if (element.Timer > 0.0f) { continue; }
DurationList.Remove(element);
DurationListDict.TryRemove(element.Id, out _);
}
}
@@ -3068,8 +3170,8 @@ namespace Barotrauma
public static void StopAll()
{
CoroutineManager.StopCoroutines("statuseffect");
DelayedEffect.DelayList.Clear();
DurationList.Clear();
DelayedEffect.DelayListDict.Clear();
DurationListDict.Clear();
}
public void AddTag(Identifier tag)