Make collections thread-safe for AI and character systems
Refactored various collections (lists, dictionaries, queues, and caches) in AI, character, animation, and item component systems to use thread-safe patterns and concurrent data structures. This includes introducing copy-on-write wrappers, ConcurrentDictionary, ConcurrentQueue, and ThreadLocal where appropriate to ensure safe concurrent access and mutation, improving stability in multi-threaded scenarios.
This commit is contained in:
@@ -1,13 +1,67 @@
|
|||||||
using Microsoft.Xna.Framework;
|
using Microsoft.Xna.Framework;
|
||||||
using System;
|
using System;
|
||||||
using System.Collections.Generic;
|
using System.Collections.Generic;
|
||||||
|
using System.Linq;
|
||||||
|
using System.Threading;
|
||||||
using System.Xml.Linq;
|
using System.Xml.Linq;
|
||||||
|
|
||||||
namespace Barotrauma
|
namespace Barotrauma
|
||||||
{
|
{
|
||||||
|
/// <summary>
|
||||||
|
/// Thread-safe wrapper for AITarget list operations.
|
||||||
|
/// Uses copy-on-write pattern for lock-free reads.
|
||||||
|
/// </summary>
|
||||||
|
class ThreadSafeAITargetList : IEnumerable<AITarget>
|
||||||
|
{
|
||||||
|
private volatile List<AITarget> _list = new List<AITarget>();
|
||||||
|
private readonly object _writeLock = new object();
|
||||||
|
|
||||||
|
public int Count => _list.Count;
|
||||||
|
|
||||||
|
public void Add(AITarget target)
|
||||||
|
{
|
||||||
|
lock (_writeLock)
|
||||||
|
{
|
||||||
|
var newList = new List<AITarget>(_list) { target };
|
||||||
|
Interlocked.Exchange(ref _list, newList);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
public bool Remove(AITarget target)
|
||||||
|
{
|
||||||
|
lock (_writeLock)
|
||||||
|
{
|
||||||
|
var newList = new List<AITarget>(_list);
|
||||||
|
bool removed = newList.Remove(target);
|
||||||
|
if (removed)
|
||||||
|
{
|
||||||
|
Interlocked.Exchange(ref _list, newList);
|
||||||
|
}
|
||||||
|
return removed;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
public void Clear()
|
||||||
|
{
|
||||||
|
Interlocked.Exchange(ref _list, new List<AITarget>());
|
||||||
|
}
|
||||||
|
|
||||||
|
public bool Contains(AITarget target) => _list.Contains(target);
|
||||||
|
|
||||||
|
public AITarget this[int index] => _list[index];
|
||||||
|
|
||||||
|
public IEnumerator<AITarget> GetEnumerator() => _list.GetEnumerator();
|
||||||
|
System.Collections.IEnumerator System.Collections.IEnumerable.GetEnumerator() => GetEnumerator();
|
||||||
|
|
||||||
|
public List<AITarget> ToList() => new List<AITarget>(_list);
|
||||||
|
public AITarget FirstOrDefault(Func<AITarget, bool> predicate) => _list.FirstOrDefault(predicate);
|
||||||
|
public IEnumerable<AITarget> Where(Func<AITarget, bool> predicate) => _list.Where(predicate);
|
||||||
|
public bool Any(Func<AITarget, bool> predicate) => _list.Any(predicate);
|
||||||
|
}
|
||||||
|
|
||||||
partial class AITarget
|
partial class AITarget
|
||||||
{
|
{
|
||||||
public static List<AITarget> List = new List<AITarget>();
|
public static ThreadSafeAITargetList List = new ThreadSafeAITargetList();
|
||||||
|
|
||||||
private Entity entity;
|
private Entity entity;
|
||||||
public Entity Entity
|
public Entity Entity
|
||||||
|
|||||||
@@ -5,6 +5,7 @@ using Microsoft.Xna.Framework;
|
|||||||
using System;
|
using System;
|
||||||
using System.Collections.Generic;
|
using System.Collections.Generic;
|
||||||
using System.Linq;
|
using System.Linq;
|
||||||
|
using System.Threading;
|
||||||
|
|
||||||
namespace Barotrauma
|
namespace Barotrauma
|
||||||
{
|
{
|
||||||
@@ -1817,7 +1818,9 @@ namespace Barotrauma
|
|||||||
public static bool HasDivingMask(Character character, float conditionPercentage = 0, bool requireOxygenTank = true)
|
public static bool HasDivingMask(Character character, float conditionPercentage = 0, bool requireOxygenTank = true)
|
||||||
=> HasItem(character, Tags.LightDivingGear, out _, requireOxygenTank ? Tags.OxygenSource : Identifier.Empty, conditionPercentage, requireEquipped: true);
|
=> HasItem(character, Tags.LightDivingGear, out _, requireOxygenTank ? Tags.OxygenSource : Identifier.Empty, conditionPercentage, requireEquipped: true);
|
||||||
|
|
||||||
private static List<Item> matchingItems = new List<Item>();
|
// ThreadLocal to ensure thread safety - each thread gets its own list instance
|
||||||
|
private static readonly ThreadLocal<List<Item>> matchingItemsLocal = new ThreadLocal<List<Item>>(() => new List<Item>());
|
||||||
|
private static List<Item> matchingItems => matchingItemsLocal.Value;
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// Note: uses a single list for matching items. The item is reused each time when the method is called. So if you use the method twice, and then refer to the first items, you'll actually get the second.
|
/// Note: uses a single list for matching items. The item is reused each time when the method is called. So if you use the method twice, and then refer to the first items, you'll actually get the second.
|
||||||
@@ -1825,15 +1828,16 @@ namespace Barotrauma
|
|||||||
/// </summary>
|
/// </summary>
|
||||||
public static bool HasItem(Character character, Identifier tagOrIdentifier, out IEnumerable<Item> items, Identifier containedTag = default, float conditionPercentage = 0, bool requireEquipped = false, bool recursive = true, Func<Item, bool> predicate = null)
|
public static bool HasItem(Character character, Identifier tagOrIdentifier, out IEnumerable<Item> items, Identifier containedTag = default, float conditionPercentage = 0, bool requireEquipped = false, bool recursive = true, Func<Item, bool> predicate = null)
|
||||||
{
|
{
|
||||||
matchingItems.Clear();
|
var localMatchingItems = matchingItems;
|
||||||
items = matchingItems;
|
localMatchingItems.Clear();
|
||||||
|
items = localMatchingItems;
|
||||||
if (character?.Inventory == null) { return false; }
|
if (character?.Inventory == null) { return false; }
|
||||||
matchingItems = character.Inventory.FindAllItems(i => (i.Prefab.Identifier == tagOrIdentifier || i.HasTag(tagOrIdentifier)) &&
|
character.Inventory.FindAllItems(i => (i.Prefab.Identifier == tagOrIdentifier || i.HasTag(tagOrIdentifier)) &&
|
||||||
i.ConditionPercentage >= conditionPercentage &&
|
i.ConditionPercentage >= conditionPercentage &&
|
||||||
(!requireEquipped || character.HasEquippedItem(i)) &&
|
(!requireEquipped || character.HasEquippedItem(i)) &&
|
||||||
(predicate == null || predicate(i)), recursive, matchingItems);
|
(predicate == null || predicate(i)), recursive, localMatchingItems);
|
||||||
items = matchingItems;
|
items = localMatchingItems;
|
||||||
foreach (var item in matchingItems)
|
foreach (var item in localMatchingItems)
|
||||||
{
|
{
|
||||||
if (item == null) { continue; }
|
if (item == null) { continue; }
|
||||||
|
|
||||||
|
|||||||
@@ -1,4 +1,5 @@
|
|||||||
using System;
|
using System;
|
||||||
|
using System.Collections.Concurrent;
|
||||||
using System.Collections.Generic;
|
using System.Collections.Generic;
|
||||||
using Barotrauma.IO;
|
using Barotrauma.IO;
|
||||||
using System.Linq;
|
using System.Linq;
|
||||||
@@ -9,7 +10,8 @@ namespace Barotrauma
|
|||||||
{
|
{
|
||||||
class NPCConversationCollection : Prefab
|
class NPCConversationCollection : Prefab
|
||||||
{
|
{
|
||||||
public static readonly Dictionary<LanguageIdentifier, PrefabCollection<NPCConversationCollection>> Collections = new Dictionary<LanguageIdentifier, PrefabCollection<NPCConversationCollection>>();
|
// Thread-safe dictionary for language-based collections
|
||||||
|
public static readonly ConcurrentDictionary<LanguageIdentifier, PrefabCollection<NPCConversationCollection>> Collections = new ConcurrentDictionary<LanguageIdentifier, PrefabCollection<NPCConversationCollection>>();
|
||||||
|
|
||||||
public readonly LanguageIdentifier Language;
|
public readonly LanguageIdentifier Language;
|
||||||
|
|
||||||
@@ -160,7 +162,24 @@ namespace Barotrauma
|
|||||||
return currentFlags;
|
return currentFlags;
|
||||||
}
|
}
|
||||||
|
|
||||||
private static readonly List<NPCConversation> previousConversations = new List<NPCConversation>();
|
// Thread-safe previous conversations tracking using copy-on-write pattern
|
||||||
|
private static volatile List<NPCConversation> _previousConversations = new List<NPCConversation>();
|
||||||
|
private static readonly object _previousConversationsLock = new object();
|
||||||
|
private static List<NPCConversation> previousConversations => _previousConversations;
|
||||||
|
|
||||||
|
private static void AddToPreviousConversations(NPCConversation conversation)
|
||||||
|
{
|
||||||
|
lock (_previousConversationsLock)
|
||||||
|
{
|
||||||
|
var newList = new List<NPCConversation>(_previousConversations);
|
||||||
|
newList.Insert(0, conversation);
|
||||||
|
if (newList.Count > MaxPreviousConversations)
|
||||||
|
{
|
||||||
|
newList.RemoveAt(MaxPreviousConversations);
|
||||||
|
}
|
||||||
|
_previousConversations = newList;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
public static List<(Character speaker, string line)> CreateRandom(List<Character> availableSpeakers)
|
public static List<(Character speaker, string line)> CreateRandom(List<Character> availableSpeakers)
|
||||||
{
|
{
|
||||||
@@ -281,8 +300,7 @@ namespace Barotrauma
|
|||||||
|
|
||||||
if (baseConversation == null)
|
if (baseConversation == null)
|
||||||
{
|
{
|
||||||
previousConversations.Insert(0, selectedConversation);
|
AddToPreviousConversations(selectedConversation);
|
||||||
if (previousConversations.Count > MaxPreviousConversations) previousConversations.RemoveAt(MaxPreviousConversations);
|
|
||||||
}
|
}
|
||||||
lineList.Add((speaker, selectedConversation.Line));
|
lineList.Add((speaker, selectedConversation.Line));
|
||||||
CreateConversation(availableSpeakers, assignedSpeakers, selectedConversation, lineList, availableConversations);
|
CreateConversation(availableSpeakers, assignedSpeakers, selectedConversation, lineList, availableConversations);
|
||||||
|
|||||||
+3
-1
@@ -1,5 +1,6 @@
|
|||||||
#nullable enable
|
#nullable enable
|
||||||
using Microsoft.Xna.Framework;
|
using Microsoft.Xna.Framework;
|
||||||
|
using System.Collections.Concurrent;
|
||||||
using System.Collections.Generic;
|
using System.Collections.Generic;
|
||||||
using System.Linq;
|
using System.Linq;
|
||||||
using Barotrauma.Items.Components;
|
using Barotrauma.Items.Components;
|
||||||
@@ -68,8 +69,9 @@ namespace Barotrauma
|
|||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// When did the character last inspect whether some other character has stolen items on them?
|
/// When did the character last inspect whether some other character has stolen items on them?
|
||||||
|
/// Thread-safe dictionary for concurrent access.
|
||||||
/// </summary>
|
/// </summary>
|
||||||
private static readonly Dictionary<Character, double> lastInspectionTimes = new Dictionary<Character, double>();
|
private static readonly ConcurrentDictionary<Character, double> lastInspectionTimes = new ConcurrentDictionary<Character, double>();
|
||||||
|
|
||||||
private const float NormalInspectionInterval = 120.0f;
|
private const float NormalInspectionInterval = 120.0f;
|
||||||
private const float CriminalInspectionInterval = 30.0f;
|
private const float CriminalInspectionInterval = 30.0f;
|
||||||
|
|||||||
@@ -5,8 +5,10 @@ using FarseerPhysics.Dynamics.Contacts;
|
|||||||
using FarseerPhysics.Dynamics.Joints;
|
using FarseerPhysics.Dynamics.Joints;
|
||||||
using Microsoft.Xna.Framework;
|
using Microsoft.Xna.Framework;
|
||||||
using System;
|
using System;
|
||||||
|
using System.Collections.Concurrent;
|
||||||
using System.Collections.Generic;
|
using System.Collections.Generic;
|
||||||
using System.Linq;
|
using System.Linq;
|
||||||
|
using System.Threading;
|
||||||
using System.Xml.Linq;
|
using System.Xml.Linq;
|
||||||
using Barotrauma.Extensions;
|
using Barotrauma.Extensions;
|
||||||
using LimbParams = Barotrauma.RagdollParams.LimbParams;
|
using LimbParams = Barotrauma.RagdollParams.LimbParams;
|
||||||
@@ -25,7 +27,33 @@ namespace Barotrauma
|
|||||||
/// </summary>
|
/// </summary>
|
||||||
const float MaxImpactDamage = 0.1f;
|
const float MaxImpactDamage = 0.1f;
|
||||||
|
|
||||||
private static readonly List<Ragdoll> list = new List<Ragdoll>();
|
// Thread-safe list using copy-on-write pattern (ConcurrentBag doesn't support indexer/Remove)
|
||||||
|
private static volatile List<Ragdoll> _list = new List<Ragdoll>();
|
||||||
|
private static readonly object _listLock = new object();
|
||||||
|
private static List<Ragdoll> list => _list;
|
||||||
|
|
||||||
|
private static void ListAdd(Ragdoll ragdoll)
|
||||||
|
{
|
||||||
|
lock (_listLock)
|
||||||
|
{
|
||||||
|
var newList = new List<Ragdoll>(_list) { ragdoll };
|
||||||
|
Interlocked.Exchange(ref _list, newList);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private static bool ListRemove(Ragdoll ragdoll)
|
||||||
|
{
|
||||||
|
lock (_listLock)
|
||||||
|
{
|
||||||
|
var newList = new List<Ragdoll>(_list);
|
||||||
|
bool removed = newList.Remove(ragdoll);
|
||||||
|
if (removed)
|
||||||
|
{
|
||||||
|
Interlocked.Exchange(ref _list, newList);
|
||||||
|
}
|
||||||
|
return removed;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
struct Impact
|
struct Impact
|
||||||
{
|
{
|
||||||
@@ -45,7 +73,8 @@ namespace Barotrauma
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
private readonly Queue<Impact> impactQueue = new Queue<Impact>();
|
// Thread-safe queue for physics collision callbacks
|
||||||
|
private readonly ConcurrentQueue<Impact> impactQueue = new ConcurrentQueue<Impact>();
|
||||||
|
|
||||||
protected Hull currentHull;
|
protected Hull currentHull;
|
||||||
|
|
||||||
@@ -467,7 +496,7 @@ namespace Barotrauma
|
|||||||
|
|
||||||
public Ragdoll(Character character, string seed, RagdollParams ragdollParams = null)
|
public Ragdoll(Character character, string seed, RagdollParams ragdollParams = null)
|
||||||
{
|
{
|
||||||
list.Add(this);
|
ListAdd(this);
|
||||||
this.character = character;
|
this.character = character;
|
||||||
Recreate(ragdollParams ?? RagdollParams);
|
Recreate(ragdollParams ?? RagdollParams);
|
||||||
}
|
}
|
||||||
@@ -744,10 +773,7 @@ namespace Barotrauma
|
|||||||
{
|
{
|
||||||
if (!f2.IsSensor)
|
if (!f2.IsSensor)
|
||||||
{
|
{
|
||||||
lock (impactQueue)
|
impactQueue.Enqueue(new Impact(f1, f2, contact, velocity));
|
||||||
{
|
|
||||||
impactQueue.Enqueue(new Impact(f1, f2, contact, velocity));
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
return true;
|
return true;
|
||||||
}
|
}
|
||||||
@@ -819,10 +845,7 @@ namespace Barotrauma
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
lock (impactQueue)
|
impactQueue.Enqueue(new Impact(f1, f2, contact, velocity));
|
||||||
{
|
|
||||||
impactQueue.Enqueue(new Impact(f1, f2, contact, velocity));
|
|
||||||
}
|
|
||||||
|
|
||||||
return true;
|
return true;
|
||||||
}
|
}
|
||||||
@@ -1274,9 +1297,8 @@ namespace Barotrauma
|
|||||||
{
|
{
|
||||||
if (!character.Enabled || character.Removed || Frozen || Invalid || Collider == null || Collider.Removed) { return; }
|
if (!character.Enabled || character.Removed || Frozen || Invalid || Collider == null || Collider.Removed) { return; }
|
||||||
|
|
||||||
while (impactQueue.Count > 0)
|
while (impactQueue.TryDequeue(out var impact))
|
||||||
{
|
{
|
||||||
var impact = impactQueue.Dequeue();
|
|
||||||
ApplyImpact(impact.F1, impact.F2, impact.LocalNormal, impact.ImpactPos, impact.Velocity);
|
ApplyImpact(impact.F1, impact.F2, impact.LocalNormal, impact.ImpactPos, impact.Velocity);
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -2325,7 +2347,7 @@ namespace Barotrauma
|
|||||||
LimbJoints = null;
|
LimbJoints = null;
|
||||||
}
|
}
|
||||||
|
|
||||||
list.Remove(this);
|
ListRemove(this);
|
||||||
}
|
}
|
||||||
|
|
||||||
public static void RemoveAll()
|
public static void RemoveAll()
|
||||||
|
|||||||
@@ -7,10 +7,12 @@ using FarseerPhysics;
|
|||||||
using FarseerPhysics.Dynamics;
|
using FarseerPhysics.Dynamics;
|
||||||
using Microsoft.Xna.Framework;
|
using Microsoft.Xna.Framework;
|
||||||
using System;
|
using System;
|
||||||
|
using System.Collections.Concurrent;
|
||||||
using System.Collections.Generic;
|
using System.Collections.Generic;
|
||||||
using System.Collections.Immutable;
|
using System.Collections.Immutable;
|
||||||
using System.Diagnostics;
|
using System.Diagnostics;
|
||||||
using System.Linq;
|
using System.Linq;
|
||||||
|
using System.Threading;
|
||||||
using System.Xml.Linq;
|
using System.Xml.Linq;
|
||||||
#if SERVER
|
#if SERVER
|
||||||
using System.Text;
|
using System.Text;
|
||||||
@@ -28,12 +30,70 @@ namespace Barotrauma
|
|||||||
|
|
||||||
public readonly record struct TalentResistanceIdentifier(Identifier ResistanceIdentifier, Identifier TalentIdentifier);
|
public readonly record struct TalentResistanceIdentifier(Identifier ResistanceIdentifier, Identifier TalentIdentifier);
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Thread-safe wrapper for character list operations.
|
||||||
|
/// Provides lock-free read operations and synchronized write operations.
|
||||||
|
/// </summary>
|
||||||
|
class ThreadSafeCharacterList : IEnumerable<Character>
|
||||||
|
{
|
||||||
|
private volatile List<Character> _list = new List<Character>();
|
||||||
|
private readonly object _writeLock = new object();
|
||||||
|
|
||||||
|
public int Count => _list.Count;
|
||||||
|
|
||||||
|
public void Add(Character character)
|
||||||
|
{
|
||||||
|
lock (_writeLock)
|
||||||
|
{
|
||||||
|
var newList = new List<Character>(_list) { character };
|
||||||
|
Interlocked.Exchange(ref _list, newList);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
public bool Remove(Character character)
|
||||||
|
{
|
||||||
|
lock (_writeLock)
|
||||||
|
{
|
||||||
|
var newList = new List<Character>(_list);
|
||||||
|
bool removed = newList.Remove(character);
|
||||||
|
if (removed)
|
||||||
|
{
|
||||||
|
Interlocked.Exchange(ref _list, newList);
|
||||||
|
}
|
||||||
|
return removed;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
public void Clear()
|
||||||
|
{
|
||||||
|
Interlocked.Exchange(ref _list, new List<Character>());
|
||||||
|
}
|
||||||
|
|
||||||
|
public bool Contains(Character character) => _list.Contains(character);
|
||||||
|
|
||||||
|
public Character this[int index] => _list[index];
|
||||||
|
|
||||||
|
public IEnumerator<Character> GetEnumerator() => _list.GetEnumerator();
|
||||||
|
System.Collections.IEnumerator System.Collections.IEnumerable.GetEnumerator() => GetEnumerator();
|
||||||
|
|
||||||
|
// LINQ-friendly snapshot for complex queries
|
||||||
|
public List<Character> ToList() => new List<Character>(_list);
|
||||||
|
|
||||||
|
public Character FirstOrDefault(Func<Character, bool> predicate) => _list.FirstOrDefault(predicate);
|
||||||
|
public Character Find(Predicate<Character> predicate) => _list.Find(predicate);
|
||||||
|
public List<Character> FindAll(Predicate<Character> predicate) => _list.FindAll(predicate);
|
||||||
|
public IEnumerable<Character> Where(Func<Character, bool> predicate) => _list.Where(predicate);
|
||||||
|
public bool Any(Func<Character, bool> predicate) => _list.Any(predicate);
|
||||||
|
public bool None(Func<Character, bool> predicate) => !_list.Any(predicate);
|
||||||
|
public int CountWhere(Func<Character, bool> predicate) => _list.Count(predicate);
|
||||||
|
}
|
||||||
|
|
||||||
partial class Character : Entity, IDamageable, ISerializableEntity, IClientSerializable, IServerPositionSync
|
partial class Character : Entity, IDamageable, ISerializableEntity, IClientSerializable, IServerPositionSync
|
||||||
{
|
{
|
||||||
public static readonly List<Character> CharacterList = new List<Character>();
|
public static readonly ThreadSafeCharacterList CharacterList = new ThreadSafeCharacterList();
|
||||||
|
|
||||||
public static int CharacterUpdateInterval = 1;
|
public static int CharacterUpdateInterval = 1;
|
||||||
private static int characterUpdateTick = 1;
|
private static volatile int characterUpdateTick = 1;
|
||||||
|
|
||||||
public const float MaxHighlightDistance = 150.0f;
|
public const float MaxHighlightDistance = 150.0f;
|
||||||
public const float MaxDragDistance = 200.0f;
|
public const float MaxDragDistance = 200.0f;
|
||||||
|
|||||||
@@ -3,15 +3,13 @@ using Barotrauma.Extensions;
|
|||||||
using Barotrauma.Networking;
|
using Barotrauma.Networking;
|
||||||
using Microsoft.Xna.Framework;
|
using Microsoft.Xna.Framework;
|
||||||
using System;
|
using System;
|
||||||
|
using System.Collections.Concurrent;
|
||||||
using System.Collections.Generic;
|
using System.Collections.Generic;
|
||||||
using System.Globalization;
|
using System.Globalization;
|
||||||
using System.Linq;
|
using System.Linq;
|
||||||
|
using System.Threading;
|
||||||
using System.Xml.Linq;
|
using System.Xml.Linq;
|
||||||
using Barotrauma.Networking;
|
|
||||||
using Barotrauma.Extensions;
|
|
||||||
using System.Globalization;
|
|
||||||
using MoonSharp.Interpreter;
|
using MoonSharp.Interpreter;
|
||||||
using Barotrauma.Abilities;
|
|
||||||
|
|
||||||
namespace Barotrauma
|
namespace Barotrauma
|
||||||
{
|
{
|
||||||
@@ -132,8 +130,9 @@ namespace Barotrauma
|
|||||||
|
|
||||||
private readonly List<LimbHealth> limbHealths = new List<LimbHealth>();
|
private readonly List<LimbHealth> limbHealths = new List<LimbHealth>();
|
||||||
|
|
||||||
private readonly Dictionary<Affliction, LimbHealth> afflictions = new Dictionary<Affliction, LimbHealth>();
|
// Thread-safe afflictions dictionary for concurrent access
|
||||||
private readonly HashSet<Affliction> irremovableAfflictions = new HashSet<Affliction>();
|
private readonly ConcurrentDictionary<Affliction, LimbHealth> afflictions = new ConcurrentDictionary<Affliction, LimbHealth>();
|
||||||
|
private readonly ConcurrentDictionary<Affliction, byte> irremovableAfflictions = new ConcurrentDictionary<Affliction, byte>();
|
||||||
private Affliction bloodlossAffliction;
|
private Affliction bloodlossAffliction;
|
||||||
private Affliction oxygenLowAffliction;
|
private Affliction oxygenLowAffliction;
|
||||||
private Affliction pressureAffliction;
|
private Affliction pressureAffliction;
|
||||||
@@ -324,13 +323,13 @@ namespace Barotrauma
|
|||||||
|
|
||||||
private void InitIrremovableAfflictions()
|
private void InitIrremovableAfflictions()
|
||||||
{
|
{
|
||||||
irremovableAfflictions.Add(bloodlossAffliction = new Affliction(AfflictionPrefab.Bloodloss, 0.0f));
|
irremovableAfflictions.TryAdd(bloodlossAffliction = new Affliction(AfflictionPrefab.Bloodloss, 0.0f), 0);
|
||||||
irremovableAfflictions.Add(stunAffliction = new Affliction(AfflictionPrefab.Stun, 0.0f));
|
irremovableAfflictions.TryAdd(stunAffliction = new Affliction(AfflictionPrefab.Stun, 0.0f), 0);
|
||||||
irremovableAfflictions.Add(pressureAffliction = new Affliction(AfflictionPrefab.Pressure, 0.0f));
|
irremovableAfflictions.TryAdd(pressureAffliction = new Affliction(AfflictionPrefab.Pressure, 0.0f), 0);
|
||||||
irremovableAfflictions.Add(oxygenLowAffliction = new Affliction(AfflictionPrefab.OxygenLow, 0.0f));
|
irremovableAfflictions.TryAdd(oxygenLowAffliction = new Affliction(AfflictionPrefab.OxygenLow, 0.0f), 0);
|
||||||
foreach (Affliction affliction in irremovableAfflictions)
|
foreach (Affliction affliction in irremovableAfflictions.Keys)
|
||||||
{
|
{
|
||||||
afflictions.Add(affliction, null);
|
afflictions.TryAdd(affliction, null);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -338,7 +337,7 @@ namespace Barotrauma
|
|||||||
|
|
||||||
public IReadOnlyCollection<Affliction> GetAllAfflictions()
|
public IReadOnlyCollection<Affliction> GetAllAfflictions()
|
||||||
{
|
{
|
||||||
return afflictions.Keys;
|
return afflictions.Keys.ToList();
|
||||||
}
|
}
|
||||||
|
|
||||||
public IEnumerable<Affliction> GetAllAfflictions(Func<Affliction, bool> limbHealthFilter)
|
public IEnumerable<Affliction> GetAllAfflictions(Func<Affliction, bool> limbHealthFilter)
|
||||||
@@ -503,19 +502,18 @@ namespace Barotrauma
|
|||||||
/// </summary>
|
/// </summary>
|
||||||
public float GetResistance(AfflictionPrefab afflictionPrefab, LimbType limbType)
|
public float GetResistance(AfflictionPrefab afflictionPrefab, LimbType limbType)
|
||||||
{
|
{
|
||||||
lock (afflictions) {
|
// ConcurrentDictionary is thread-safe, no lock needed
|
||||||
// This is a % resistance (0 to 1.0)
|
// This is a % resistance (0 to 1.0)
|
||||||
float resistance = 0.0f;
|
float resistance = 0.0f;
|
||||||
foreach (KeyValuePair<Affliction, LimbHealth> kvp in afflictions)
|
foreach (KeyValuePair<Affliction, LimbHealth> kvp in afflictions)
|
||||||
{
|
{
|
||||||
var affliction = kvp.Key;
|
var affliction = kvp.Key;
|
||||||
resistance += affliction.GetResistance(afflictionPrefab.Identifier, limbType);
|
resistance += affliction.GetResistance(afflictionPrefab.Identifier, limbType);
|
||||||
}
|
|
||||||
// This is a multiplier, ie. 0.0 = 100% resistance and 1.0 = 0% resistance
|
|
||||||
float abilityResistanceMultiplier = Character.GetAbilityResistance(afflictionPrefab);
|
|
||||||
// The returned value is calculated to be a % resistance again
|
|
||||||
return 1 - ((1 - resistance) * abilityResistanceMultiplier);
|
|
||||||
}
|
}
|
||||||
|
// This is a multiplier, ie. 0.0 = 100% resistance and 1.0 = 0% resistance
|
||||||
|
float abilityResistanceMultiplier = Character.GetAbilityResistance(afflictionPrefab);
|
||||||
|
// The returned value is calculated to be a % resistance again
|
||||||
|
return 1 - ((1 - resistance) * abilityResistanceMultiplier);
|
||||||
}
|
}
|
||||||
|
|
||||||
public float GetStatValue(StatTypes statType)
|
public float GetStatValue(StatTypes statType)
|
||||||
@@ -696,14 +694,14 @@ namespace Barotrauma
|
|||||||
a.Prefab.AfflictionType == AfflictionPrefab.Bleeding.AfflictionType));
|
a.Prefab.AfflictionType == AfflictionPrefab.Bleeding.AfflictionType));
|
||||||
foreach (var affliction in afflictionsToRemove)
|
foreach (var affliction in afflictionsToRemove)
|
||||||
{
|
{
|
||||||
afflictions.Remove(affliction);
|
afflictions.TryRemove(affliction, out _);
|
||||||
}
|
}
|
||||||
|
|
||||||
foreach (LimbHealth limbHealth in limbHealths)
|
foreach (LimbHealth limbHealth in limbHealths)
|
||||||
{
|
{
|
||||||
if (damageAmount > 0.0f) { afflictions.Add(AfflictionPrefab.InternalDamage.Instantiate(damageAmount), limbHealth); }
|
if (damageAmount > 0.0f) { afflictions.TryAdd(AfflictionPrefab.InternalDamage.Instantiate(damageAmount), limbHealth); }
|
||||||
if (bleedingDamageAmount > 0.0f && DoesBleed) { afflictions.Add(AfflictionPrefab.Bleeding.Instantiate(bleedingDamageAmount), limbHealth); }
|
if (bleedingDamageAmount > 0.0f && DoesBleed) { afflictions.TryAdd(AfflictionPrefab.Bleeding.Instantiate(bleedingDamageAmount), limbHealth); }
|
||||||
if (burnDamageAmount > 0.0f) { afflictions.Add(AfflictionPrefab.Burn.Instantiate(burnDamageAmount), limbHealth); }
|
if (burnDamageAmount > 0.0f) { afflictions.TryAdd(AfflictionPrefab.Burn.Instantiate(burnDamageAmount), limbHealth); }
|
||||||
}
|
}
|
||||||
|
|
||||||
RecalculateVitality();
|
RecalculateVitality();
|
||||||
@@ -743,7 +741,7 @@ namespace Barotrauma
|
|||||||
afflictionsToRemove.AddRange(afflictions.Keys.Where(affliction => predicate(affliction)));
|
afflictionsToRemove.AddRange(afflictions.Keys.Where(affliction => predicate(affliction)));
|
||||||
foreach (var affliction in afflictionsToRemove)
|
foreach (var affliction in afflictionsToRemove)
|
||||||
{
|
{
|
||||||
afflictions.Remove(affliction);
|
afflictions.TryRemove(affliction, out _);
|
||||||
}
|
}
|
||||||
CalculateVitality();
|
CalculateVitality();
|
||||||
}
|
}
|
||||||
@@ -751,14 +749,14 @@ namespace Barotrauma
|
|||||||
public void RemoveAllAfflictions()
|
public void RemoveAllAfflictions()
|
||||||
{
|
{
|
||||||
afflictionsToRemove.Clear();
|
afflictionsToRemove.Clear();
|
||||||
afflictionsToRemove.AddRange(afflictions.Keys.Where(a => !irremovableAfflictions.Contains(a)));
|
afflictionsToRemove.AddRange(afflictions.Keys.Where(a => !irremovableAfflictions.ContainsKey(a)));
|
||||||
foreach (var affliction in afflictionsToRemove)
|
foreach (var affliction in afflictionsToRemove)
|
||||||
{
|
{
|
||||||
//set strength to 0 in case the affliction needs to react to becoming inactive
|
//set strength to 0 in case the affliction needs to react to becoming inactive
|
||||||
affliction.Strength = 0.0f;
|
affliction.Strength = 0.0f;
|
||||||
afflictions.Remove(affliction);
|
afflictions.TryRemove(affliction, out _);
|
||||||
}
|
}
|
||||||
foreach (Affliction affliction in irremovableAfflictions)
|
foreach (Affliction affliction in irremovableAfflictions.Keys)
|
||||||
{
|
{
|
||||||
affliction.Strength = 0.0f;
|
affliction.Strength = 0.0f;
|
||||||
}
|
}
|
||||||
@@ -769,15 +767,15 @@ namespace Barotrauma
|
|||||||
{
|
{
|
||||||
afflictionsToRemove.Clear();
|
afflictionsToRemove.Clear();
|
||||||
afflictionsToRemove.AddRange(afflictions.Keys.Where(a =>
|
afflictionsToRemove.AddRange(afflictions.Keys.Where(a =>
|
||||||
!irremovableAfflictions.Contains(a) &&
|
!irremovableAfflictions.ContainsKey(a) &&
|
||||||
!a.Prefab.IsBuff &&
|
!a.Prefab.IsBuff &&
|
||||||
a.Prefab.AfflictionType != "geneticmaterialbuff" &&
|
a.Prefab.AfflictionType != "geneticmaterialbuff" &&
|
||||||
a.Prefab.AfflictionType != "geneticmaterialdebuff"));
|
a.Prefab.AfflictionType != "geneticmaterialdebuff"));
|
||||||
foreach (var affliction in afflictionsToRemove)
|
foreach (var affliction in afflictionsToRemove)
|
||||||
{
|
{
|
||||||
afflictions.Remove(affliction);
|
afflictions.TryRemove(affliction, out _);
|
||||||
}
|
}
|
||||||
foreach (Affliction affliction in irremovableAfflictions)
|
foreach (Affliction affliction in irremovableAfflictions.Keys)
|
||||||
{
|
{
|
||||||
affliction.Strength = 0.0f;
|
affliction.Strength = 0.0f;
|
||||||
}
|
}
|
||||||
@@ -869,7 +867,7 @@ namespace Barotrauma
|
|||||||
var copyAffliction = newAffliction.Prefab.Instantiate(
|
var copyAffliction = newAffliction.Prefab.Instantiate(
|
||||||
Math.Min(newAffliction.Prefab.MaxStrength, newAffliction.Strength * (100.0f / MaxVitality) * (1f - GetResistance(newAffliction.Prefab, limbType))),
|
Math.Min(newAffliction.Prefab.MaxStrength, newAffliction.Strength * (100.0f / MaxVitality) * (1f - GetResistance(newAffliction.Prefab, limbType))),
|
||||||
newAffliction.Source);
|
newAffliction.Source);
|
||||||
afflictions.Add(copyAffliction, limbHealth);
|
afflictions.TryAdd(copyAffliction, limbHealth);
|
||||||
AchievementManager.OnAfflictionReceived(copyAffliction, Character);
|
AchievementManager.OnAfflictionReceived(copyAffliction, Character);
|
||||||
MedicalClinic.OnAfflictionCountChanged(Character);
|
MedicalClinic.OnAfflictionCountChanged(Character);
|
||||||
|
|
||||||
@@ -914,7 +912,7 @@ namespace Barotrauma
|
|||||||
if (affliction.Strength <= 0.0f)
|
if (affliction.Strength <= 0.0f)
|
||||||
{
|
{
|
||||||
AchievementManager.OnAfflictionRemoved(affliction, Character);
|
AchievementManager.OnAfflictionRemoved(affliction, Character);
|
||||||
if (!irremovableAfflictions.Contains(affliction)) { afflictionsToRemove.Add(affliction); }
|
if (!irremovableAfflictions.ContainsKey(affliction)) { afflictionsToRemove.Add(affliction); }
|
||||||
continue;
|
continue;
|
||||||
}
|
}
|
||||||
if (affliction.Prefab.Duration > 0.0f)
|
if (affliction.Prefab.Duration > 0.0f)
|
||||||
@@ -952,7 +950,7 @@ namespace Barotrauma
|
|||||||
|
|
||||||
foreach (var affliction in afflictionsToRemove)
|
foreach (var affliction in afflictionsToRemove)
|
||||||
{
|
{
|
||||||
afflictions.Remove(affliction);
|
afflictions.TryRemove(affliction, out _);
|
||||||
}
|
}
|
||||||
|
|
||||||
if (afflictionsToRemove.Count is not 0)
|
if (afflictionsToRemove.Count is not 0)
|
||||||
@@ -1519,14 +1517,14 @@ namespace Barotrauma
|
|||||||
}
|
}
|
||||||
if (afflictionPredicate != null && !afflictionPredicate.Invoke(afflictionPrefab)) { return; }
|
if (afflictionPredicate != null && !afflictionPredicate.Invoke(afflictionPrefab)) { return; }
|
||||||
float strength = afflictionElement.GetAttributeFloat("strength", 0.0f);
|
float strength = afflictionElement.GetAttributeFloat("strength", 0.0f);
|
||||||
var irremovableAffliction = irremovableAfflictions.FirstOrDefault(a => a.Prefab == afflictionPrefab);
|
var irremovableAffliction = irremovableAfflictions.Keys.FirstOrDefault(a => a.Prefab == afflictionPrefab);
|
||||||
if (irremovableAffliction != null)
|
if (irremovableAffliction != null)
|
||||||
{
|
{
|
||||||
irremovableAffliction.Strength = strength;
|
irremovableAffliction.Strength = strength;
|
||||||
}
|
}
|
||||||
else
|
else
|
||||||
{
|
{
|
||||||
afflictions.Add(afflictionPrefab.Instantiate(strength), limbHealth);
|
afflictions.TryAdd(afflictionPrefab.Instantiate(strength), limbHealth);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
+14
-20
@@ -1,10 +1,12 @@
|
|||||||
using Microsoft.Xna.Framework;
|
using Microsoft.Xna.Framework;
|
||||||
|
using System.Collections.Concurrent;
|
||||||
using System.Collections.Generic;
|
using System.Collections.Generic;
|
||||||
using System.Globalization;
|
using System.Globalization;
|
||||||
using Barotrauma.IO;
|
using Barotrauma.IO;
|
||||||
using System;
|
using System;
|
||||||
using System.Diagnostics;
|
using System.Diagnostics;
|
||||||
using System.Linq;
|
using System.Linq;
|
||||||
|
using System.Threading;
|
||||||
using System.Xml.Linq;
|
using System.Xml.Linq;
|
||||||
using Barotrauma.Extensions;
|
using Barotrauma.Extensions;
|
||||||
|
|
||||||
@@ -117,8 +119,9 @@ namespace Barotrauma
|
|||||||
public virtual AnimationType AnimationType { get; protected set; }
|
public virtual AnimationType AnimationType { get; protected set; }
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// The cached animations of all the characters that have been loaded.
|
/// The cached animations of all the characters that have been loaded.
|
||||||
|
/// Thread-safe cache using ConcurrentDictionary.
|
||||||
/// </summary>
|
/// </summary>
|
||||||
private static readonly Dictionary<Identifier, Dictionary<string, AnimationParams>> allAnimations = new Dictionary<Identifier, Dictionary<string, AnimationParams>>();
|
private static readonly ConcurrentDictionary<Identifier, ConcurrentDictionary<string, AnimationParams>> allAnimations = new ConcurrentDictionary<Identifier, ConcurrentDictionary<string, AnimationParams>>();
|
||||||
|
|
||||||
[Header("Movement")]
|
[Header("Movement")]
|
||||||
[Serialize(1.0f, IsPropertySaveable.Yes), Editable(DecimalCount = 2, MinValueFloat = 0, MaxValueFloat = Ragdoll.MAX_SPEED, ValueStep = 0.1f)]
|
[Serialize(1.0f, IsPropertySaveable.Yes), Editable(DecimalCount = 2, MinValueFloat = 0, MaxValueFloat = Ragdoll.MAX_SPEED, ValueStep = 0.1f)]
|
||||||
@@ -244,7 +247,9 @@ namespace Barotrauma
|
|||||||
return GetAnimParams<T>(speciesName, animSpecies, fallbackSpecies: character.Prefab.GetBaseCharacterSpeciesName(speciesName), animType, file, throwErrors);
|
return GetAnimParams<T>(speciesName, animSpecies, fallbackSpecies: character.Prefab.GetBaseCharacterSpeciesName(speciesName), animType, file, throwErrors);
|
||||||
}
|
}
|
||||||
|
|
||||||
private static readonly List<string> errorMessages = new List<string>();
|
// ThreadLocal for thread-safe error message collection during animation loading
|
||||||
|
private static readonly ThreadLocal<List<string>> errorMessagesLocal = new ThreadLocal<List<string>>(() => new List<string>());
|
||||||
|
private static List<string> errorMessages => errorMessagesLocal.Value;
|
||||||
|
|
||||||
private static T GetAnimParams<T>(Identifier speciesName, Identifier animSpecies, Identifier fallbackSpecies, AnimationType animType, Either<string, ContentPath> file, bool throwErrors = true) where T : AnimationParams, new()
|
private static T GetAnimParams<T>(Identifier speciesName, Identifier animSpecies, Identifier fallbackSpecies, AnimationType animType, Either<string, ContentPath> file, bool throwErrors = true) where T : AnimationParams, new()
|
||||||
{
|
{
|
||||||
@@ -262,11 +267,7 @@ namespace Barotrauma
|
|||||||
}
|
}
|
||||||
ContentPackage contentPackage = contentPath?.ContentPackage ?? CharacterPrefab.FindBySpeciesName(speciesName)?.ContentPackage;
|
ContentPackage contentPackage = contentPath?.ContentPackage ?? CharacterPrefab.FindBySpeciesName(speciesName)?.ContentPackage;
|
||||||
Debug.Assert(contentPackage != null);
|
Debug.Assert(contentPackage != null);
|
||||||
if (!allAnimations.TryGetValue(speciesName, out Dictionary<string, AnimationParams> animations))
|
var animations = allAnimations.GetOrAdd(speciesName, _ => new ConcurrentDictionary<string, AnimationParams>());
|
||||||
{
|
|
||||||
animations = new Dictionary<string, AnimationParams>();
|
|
||||||
allAnimations.Add(speciesName, animations);
|
|
||||||
}
|
|
||||||
string key = fileName ?? contentPath?.Value ?? GetDefaultFileName(animSpecies, animType);
|
string key = fileName ?? contentPath?.Value ?? GetDefaultFileName(animSpecies, animType);
|
||||||
if (animations.TryGetValue(key, out AnimationParams anim) && anim.AnimationType == animType)
|
if (animations.TryGetValue(key, out AnimationParams anim) && anim.AnimationType == animType)
|
||||||
{
|
{
|
||||||
@@ -418,16 +419,12 @@ namespace Barotrauma
|
|||||||
{
|
{
|
||||||
throw new Exception("Cannot create an animation file of type " + animationType);
|
throw new Exception("Cannot create an animation file of type " + animationType);
|
||||||
}
|
}
|
||||||
if (!allAnimations.TryGetValue(speciesName, out Dictionary<string, AnimationParams> anims))
|
var anims = allAnimations.GetOrAdd(speciesName, _ => new ConcurrentDictionary<string, AnimationParams>());
|
||||||
{
|
|
||||||
anims = new Dictionary<string, AnimationParams>();
|
|
||||||
allAnimations.Add(speciesName, anims);
|
|
||||||
}
|
|
||||||
string fileName = IO.Path.GetFileNameWithoutExtension(fullPath);
|
string fileName = IO.Path.GetFileNameWithoutExtension(fullPath);
|
||||||
if (anims.ContainsKey(fileName))
|
if (anims.ContainsKey(fileName))
|
||||||
{
|
{
|
||||||
DebugConsole.NewMessage($"[AnimationParams] Removing the old animation of type {animationType}.", Color.Red);
|
DebugConsole.NewMessage($"[AnimationParams] Removing the old animation of type {animationType}.", Color.Red);
|
||||||
anims.Remove(fileName);
|
anims.TryRemove(fileName, out _);
|
||||||
}
|
}
|
||||||
var instance = new T();
|
var instance = new T();
|
||||||
XElement animationElement = new XElement(GetDefaultFileName(speciesName, animationType), new XAttribute("animationtype", animationType.ToString()));
|
XElement animationElement = new XElement(GetDefaultFileName(speciesName, animationType), new XAttribute("animationtype", animationType.ToString()));
|
||||||
@@ -439,7 +436,7 @@ namespace Barotrauma
|
|||||||
instance.IsLoaded = instance.Deserialize(animationElement);
|
instance.IsLoaded = instance.Deserialize(animationElement);
|
||||||
instance.Save();
|
instance.Save();
|
||||||
instance.Load(contentPath, speciesName);
|
instance.Load(contentPath, speciesName);
|
||||||
anims.Add(fileName, instance);
|
anims.TryAdd(fileName, instance);
|
||||||
DebugConsole.NewMessage($"[AnimationParams] New animation file of type {animationType} created.", Color.GhostWhite);
|
DebugConsole.NewMessage($"[AnimationParams] New animation file of type {animationType} created.", Color.GhostWhite);
|
||||||
return instance;
|
return instance;
|
||||||
}
|
}
|
||||||
@@ -467,17 +464,14 @@ namespace Barotrauma
|
|||||||
{
|
{
|
||||||
// Update the key by removing and re-adding the animation.
|
// Update the key by removing and re-adding the animation.
|
||||||
string fileName = FileNameWithoutExtension;
|
string fileName = FileNameWithoutExtension;
|
||||||
if (allAnimations.TryGetValue(SpeciesName, out Dictionary<string, AnimationParams> animations))
|
if (allAnimations.TryGetValue(SpeciesName, out ConcurrentDictionary<string, AnimationParams> animations))
|
||||||
{
|
{
|
||||||
animations.Remove(fileName);
|
animations.TryRemove(fileName, out _);
|
||||||
}
|
}
|
||||||
base.UpdatePath(newPath);
|
base.UpdatePath(newPath);
|
||||||
if (animations != null)
|
if (animations != null)
|
||||||
{
|
{
|
||||||
if (!animations.ContainsKey(fileName))
|
animations.TryAdd(fileName, this);
|
||||||
{
|
|
||||||
animations.Add(fileName, this);
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
+11
-16
@@ -1,5 +1,6 @@
|
|||||||
using Microsoft.Xna.Framework;
|
using Microsoft.Xna.Framework;
|
||||||
using System;
|
using System;
|
||||||
|
using System.Collections.Concurrent;
|
||||||
using System.Collections.Generic;
|
using System.Collections.Generic;
|
||||||
using System.Diagnostics;
|
using System.Diagnostics;
|
||||||
using System.Xml.Linq;
|
using System.Xml.Linq;
|
||||||
@@ -124,8 +125,9 @@ namespace Barotrauma
|
|||||||
/// key1: Species name
|
/// key1: Species name
|
||||||
/// key2: File path
|
/// key2: File path
|
||||||
/// value: Ragdoll parameters
|
/// value: Ragdoll parameters
|
||||||
|
/// Thread-safe cache using ConcurrentDictionary.
|
||||||
/// </summary>
|
/// </summary>
|
||||||
private static readonly Dictionary<Identifier, Dictionary<string, RagdollParams>> allRagdolls = new Dictionary<Identifier, Dictionary<string, RagdollParams>>();
|
private static readonly ConcurrentDictionary<Identifier, ConcurrentDictionary<string, RagdollParams>> allRagdolls = new ConcurrentDictionary<Identifier, ConcurrentDictionary<string, RagdollParams>>();
|
||||||
|
|
||||||
public List<ColliderParams> Colliders { get; private set; } = new List<ColliderParams>();
|
public List<ColliderParams> Colliders { get; private set; } = new List<ColliderParams>();
|
||||||
public List<LimbParams> Limbs { get; private set; } = new List<LimbParams>();
|
public List<LimbParams> Limbs { get; private set; } = new List<LimbParams>();
|
||||||
@@ -222,11 +224,7 @@ namespace Barotrauma
|
|||||||
Debug.Assert(!fileName.IsNullOrWhiteSpace() || !contentPath.IsNullOrWhiteSpace());
|
Debug.Assert(!fileName.IsNullOrWhiteSpace() || !contentPath.IsNullOrWhiteSpace());
|
||||||
}
|
}
|
||||||
Debug.Assert(contentPackage != null);
|
Debug.Assert(contentPackage != null);
|
||||||
if (!allRagdolls.TryGetValue(speciesName, out Dictionary<string, RagdollParams> ragdolls))
|
var ragdolls = allRagdolls.GetOrAdd(speciesName, _ => new ConcurrentDictionary<string, RagdollParams>());
|
||||||
{
|
|
||||||
ragdolls = new Dictionary<string, RagdollParams>();
|
|
||||||
allRagdolls.Add(speciesName, ragdolls);
|
|
||||||
}
|
|
||||||
string key = fileName ?? contentPath?.Value ?? GetDefaultFileName(ragdollSpecies);
|
string key = fileName ?? contentPath?.Value ?? GetDefaultFileName(ragdollSpecies);
|
||||||
if (ragdolls.TryGetValue(key, out RagdollParams ragdoll))
|
if (ragdolls.TryGetValue(key, out RagdollParams ragdoll))
|
||||||
{
|
{
|
||||||
@@ -331,10 +329,10 @@ namespace Barotrauma
|
|||||||
if (allRagdolls.ContainsKey(speciesName))
|
if (allRagdolls.ContainsKey(speciesName))
|
||||||
{
|
{
|
||||||
DebugConsole.NewMessage($"[RagdollParams] Removing the old ragdolls from {speciesName}.", Color.Red);
|
DebugConsole.NewMessage($"[RagdollParams] Removing the old ragdolls from {speciesName}.", Color.Red);
|
||||||
allRagdolls.Remove(speciesName);
|
allRagdolls.TryRemove(speciesName, out _);
|
||||||
}
|
}
|
||||||
var ragdolls = new Dictionary<string, RagdollParams>();
|
var ragdolls = new ConcurrentDictionary<string, RagdollParams>();
|
||||||
allRagdolls.Add(speciesName, ragdolls);
|
allRagdolls.TryAdd(speciesName, ragdolls);
|
||||||
var instance = new T
|
var instance = new T
|
||||||
{
|
{
|
||||||
doc = new XDocument(mainElement)
|
doc = new XDocument(mainElement)
|
||||||
@@ -345,7 +343,7 @@ namespace Barotrauma
|
|||||||
instance.IsLoaded = instance.Deserialize(mainElement);
|
instance.IsLoaded = instance.Deserialize(mainElement);
|
||||||
instance.Save();
|
instance.Save();
|
||||||
instance.Load(contentPath, speciesName);
|
instance.Load(contentPath, speciesName);
|
||||||
ragdolls.Add(instance.FileNameWithoutExtension, instance);
|
ragdolls.TryAdd(instance.FileNameWithoutExtension, instance);
|
||||||
DebugConsole.NewMessage("[RagdollParams] New default ragdoll params successfully created at " + fullPath, Color.NavajoWhite);
|
DebugConsole.NewMessage("[RagdollParams] New default ragdoll params successfully created at " + fullPath, Color.NavajoWhite);
|
||||||
return instance;
|
return instance;
|
||||||
}
|
}
|
||||||
@@ -362,17 +360,14 @@ namespace Barotrauma
|
|||||||
{
|
{
|
||||||
// Update the key by removing and re-adding the ragdoll.
|
// Update the key by removing and re-adding the ragdoll.
|
||||||
string fileName = FileNameWithoutExtension;
|
string fileName = FileNameWithoutExtension;
|
||||||
if (allRagdolls.TryGetValue(SpeciesName, out Dictionary<string, RagdollParams> ragdolls))
|
if (allRagdolls.TryGetValue(SpeciesName, out ConcurrentDictionary<string, RagdollParams> ragdolls))
|
||||||
{
|
{
|
||||||
ragdolls.Remove(fileName);
|
ragdolls.TryRemove(fileName, out _);
|
||||||
}
|
}
|
||||||
base.UpdatePath(fullPath);
|
base.UpdatePath(fullPath);
|
||||||
if (ragdolls != null)
|
if (ragdolls != null)
|
||||||
{
|
{
|
||||||
if (!ragdolls.ContainsKey(fileName))
|
ragdolls.TryAdd(fileName, this);
|
||||||
{
|
|
||||||
ragdolls.Add(fileName, this);
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,6 +1,8 @@
|
|||||||
using Barotrauma.Abilities;
|
using Barotrauma.Abilities;
|
||||||
using System;
|
using System;
|
||||||
|
using System.Collections.Concurrent;
|
||||||
using System.Collections.Generic;
|
using System.Collections.Generic;
|
||||||
|
using System.Threading;
|
||||||
|
|
||||||
namespace Barotrauma
|
namespace Barotrauma
|
||||||
{
|
{
|
||||||
@@ -72,7 +74,9 @@ namespace Barotrauma
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
private static readonly HashSet<Identifier> checkedNonStackableTalents = new();
|
// ThreadLocal for thread-safe talent checking
|
||||||
|
private static readonly ThreadLocal<HashSet<Identifier>> checkedNonStackableTalentsLocal = new ThreadLocal<HashSet<Identifier>>(() => new HashSet<Identifier>());
|
||||||
|
private static HashSet<Identifier> checkedNonStackableTalents => checkedNonStackableTalentsLocal.Value;
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// Checks talents for a given AbilityObject taking into account non-stackable talents.
|
/// Checks talents for a given AbilityObject taking into account non-stackable talents.
|
||||||
|
|||||||
+2
-2
@@ -1,4 +1,4 @@
|
|||||||
using System.Xml.Linq;
|
using System.Xml.Linq;
|
||||||
|
|
||||||
namespace Barotrauma
|
namespace Barotrauma
|
||||||
{
|
{
|
||||||
@@ -21,7 +21,7 @@ namespace Barotrauma
|
|||||||
var npcConversationCollection = new NPCConversationCollection(this, mainElement);
|
var npcConversationCollection = new NPCConversationCollection(this, mainElement);
|
||||||
if (!NPCConversationCollection.Collections.ContainsKey(npcConversationCollection.Language))
|
if (!NPCConversationCollection.Collections.ContainsKey(npcConversationCollection.Language))
|
||||||
{
|
{
|
||||||
NPCConversationCollection.Collections.Add(npcConversationCollection.Language, new PrefabCollection<NPCConversationCollection>());
|
NPCConversationCollection.Collections.TryAdd(npcConversationCollection.Language, new PrefabCollection<NPCConversationCollection>());
|
||||||
}
|
}
|
||||||
NPCConversationCollection.Collections[npcConversationCollection.Language].Add(npcConversationCollection, allowOverriding);
|
NPCConversationCollection.Collections[npcConversationCollection.Language].Add(npcConversationCollection, allowOverriding);
|
||||||
}
|
}
|
||||||
|
|||||||
+19
-11
@@ -1,6 +1,8 @@
|
|||||||
using System;
|
using System;
|
||||||
|
using System.Collections.Concurrent;
|
||||||
using System.Collections.Generic;
|
using System.Collections.Generic;
|
||||||
using System.Globalization;
|
using System.Globalization;
|
||||||
|
using System.Linq;
|
||||||
using System.Xml.Linq;
|
using System.Xml.Linq;
|
||||||
using Microsoft.Xna.Framework;
|
using Microsoft.Xna.Framework;
|
||||||
namespace Barotrauma.Items.Components
|
namespace Barotrauma.Items.Components
|
||||||
@@ -25,7 +27,8 @@ namespace Barotrauma.Items.Components
|
|||||||
private int signalQueueSize;
|
private int signalQueueSize;
|
||||||
private int delayTicks;
|
private int delayTicks;
|
||||||
|
|
||||||
private readonly Queue<DelayedSignal> signalQueue = new Queue<DelayedSignal>();
|
// Thread-safe queue for concurrent access
|
||||||
|
private readonly ConcurrentQueue<DelayedSignal> signalQueue = new ConcurrentQueue<DelayedSignal>();
|
||||||
|
|
||||||
private DelayedSignal prevQueuedSignal;
|
private DelayedSignal prevQueuedSignal;
|
||||||
|
|
||||||
@@ -40,7 +43,8 @@ namespace Barotrauma.Items.Components
|
|||||||
delay = value;
|
delay = value;
|
||||||
delayTicks = (int)(delay / Timing.Step);
|
delayTicks = (int)(delay / Timing.Step);
|
||||||
signalQueueSize = Math.Max(delayTicks, 1) * 2;
|
signalQueueSize = Math.Max(delayTicks, 1) * 2;
|
||||||
signalQueue.Clear();
|
// ConcurrentQueue doesn't have Clear(), drain it instead
|
||||||
|
while (signalQueue.TryDequeue(out _)) { }
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -66,19 +70,19 @@ namespace Barotrauma.Items.Components
|
|||||||
|
|
||||||
public override void Update(float deltaTime, Camera cam)
|
public override void Update(float deltaTime, Camera cam)
|
||||||
{
|
{
|
||||||
if (signalQueue.Count == 0)
|
if (signalQueue.IsEmpty)
|
||||||
{
|
{
|
||||||
IsActive = false;
|
IsActive = false;
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
foreach (var val in signalQueue)
|
// Use ToArray() snapshot for thread-safe iteration
|
||||||
|
foreach (var val in signalQueue.ToArray())
|
||||||
{
|
{
|
||||||
val.SendTimer -= 1;
|
val.SendTimer -= 1;
|
||||||
}
|
}
|
||||||
while (signalQueue.Count > 0 && signalQueue.Peek().SendTimer <= 0)
|
while (signalQueue.TryPeek(out var signalOut) && signalOut.SendTimer <= 0)
|
||||||
{
|
{
|
||||||
var signalOut = signalQueue.Peek();
|
|
||||||
signalOut.SendDuration -= 1;
|
signalOut.SendDuration -= 1;
|
||||||
item.SendSignal(new Signal(signalOut.Signal.value, sender: signalOut.Signal.sender, strength: signalOut.Signal.strength), "signal_out");
|
item.SendSignal(new Signal(signalOut.Signal.value, sender: signalOut.Signal.sender, strength: signalOut.Signal.strength), "signal_out");
|
||||||
if (signalOut.SendDuration <= 0)
|
if (signalOut.SendDuration <= 0)
|
||||||
@@ -100,11 +104,15 @@ namespace Barotrauma.Items.Components
|
|||||||
{
|
{
|
||||||
case "signal_in":
|
case "signal_in":
|
||||||
if (signalQueue.Count >= signalQueueSize) { return; }
|
if (signalQueue.Count >= signalQueueSize) { return; }
|
||||||
if (ResetWhenSignalReceived) { prevQueuedSignal = null; signalQueue.Clear(); }
|
if (ResetWhenSignalReceived)
|
||||||
if (ResetWhenDifferentSignalReceived && signalQueue.Count > 0 && signalQueue.Peek().Signal.value != signal.value)
|
|
||||||
{
|
{
|
||||||
prevQueuedSignal = null;
|
prevQueuedSignal = null;
|
||||||
signalQueue.Clear();
|
while (signalQueue.TryDequeue(out _)) { }
|
||||||
|
}
|
||||||
|
if (ResetWhenDifferentSignalReceived && signalQueue.TryPeek(out var peekSignal) && peekSignal.Signal.value != signal.value)
|
||||||
|
{
|
||||||
|
prevQueuedSignal = null;
|
||||||
|
while (signalQueue.TryDequeue(out _)) { }
|
||||||
}
|
}
|
||||||
|
|
||||||
if (prevQueuedSignal != null &&
|
if (prevQueuedSignal != null &&
|
||||||
@@ -127,10 +135,10 @@ namespace Barotrauma.Items.Components
|
|||||||
if (float.TryParse(signal.value, NumberStyles.Any, CultureInfo.InvariantCulture, out float newDelay))
|
if (float.TryParse(signal.value, NumberStyles.Any, CultureInfo.InvariantCulture, out float newDelay))
|
||||||
{
|
{
|
||||||
newDelay = MathHelper.Clamp(newDelay, 0, 60);
|
newDelay = MathHelper.Clamp(newDelay, 0, 60);
|
||||||
if (signalQueue.Count > 0 && newDelay != Delay)
|
if (!signalQueue.IsEmpty && newDelay != Delay)
|
||||||
{
|
{
|
||||||
prevQueuedSignal = null;
|
prevQueuedSignal = null;
|
||||||
signalQueue.Clear();
|
while (signalQueue.TryDequeue(out _)) { }
|
||||||
}
|
}
|
||||||
Delay = newDelay;
|
Delay = newDelay;
|
||||||
}
|
}
|
||||||
|
|||||||
Reference in New Issue
Block a user