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,13 +1,67 @@
using Microsoft.Xna.Framework;
using System;
using System.Collections.Generic;
using System.Linq;
using System.Threading;
using System.Xml.Linq;
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
{
public static List<AITarget> List = new List<AITarget>();
public static ThreadSafeAITargetList List = new ThreadSafeAITargetList();
private Entity entity;
public Entity Entity
@@ -5,6 +5,7 @@ using Microsoft.Xna.Framework;
using System;
using System.Collections.Generic;
using System.Linq;
using System.Threading;
namespace Barotrauma
{
@@ -1817,7 +1818,9 @@ namespace Barotrauma
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);
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>
/// 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>
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();
items = matchingItems;
var localMatchingItems = matchingItems;
localMatchingItems.Clear();
items = localMatchingItems;
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 &&
(!requireEquipped || character.HasEquippedItem(i)) &&
(predicate == null || predicate(i)), recursive, matchingItems);
items = matchingItems;
foreach (var item in matchingItems)
(predicate == null || predicate(i)), recursive, localMatchingItems);
items = localMatchingItems;
foreach (var item in localMatchingItems)
{
if (item == null) { continue; }
@@ -1,4 +1,5 @@
using System;
using System.Collections.Concurrent;
using System.Collections.Generic;
using Barotrauma.IO;
using System.Linq;
@@ -9,7 +10,8 @@ namespace Barotrauma
{
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;
@@ -160,7 +162,24 @@ namespace Barotrauma
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)
{
@@ -281,8 +300,7 @@ namespace Barotrauma
if (baseConversation == null)
{
previousConversations.Insert(0, selectedConversation);
if (previousConversations.Count > MaxPreviousConversations) previousConversations.RemoveAt(MaxPreviousConversations);
AddToPreviousConversations(selectedConversation);
}
lineList.Add((speaker, selectedConversation.Line));
CreateConversation(availableSpeakers, assignedSpeakers, selectedConversation, lineList, availableConversations);
@@ -119,7 +119,7 @@ namespace Barotrauma
protected override bool CheckObjectiveState()
{
if (item.IgnoreByAI(character) || Item.DeconstructItems.Contains(item))
if (item.IgnoreByAI(character) || Item.IsMarkedForDeconstruction(item))
{
Abandon = true;
}
@@ -114,7 +114,7 @@ namespace Barotrauma
if (!allowUnloading) { return false; }
if (requireValidContainer && !IsValidContainer(item.Container, character)) { return false; }
}
if (ignoreItemsMarkedForDeconstruction && Item.DeconstructItems.Contains(item)) { return false; }
if (ignoreItemsMarkedForDeconstruction && Item.IsMarkedForDeconstruction(item)) { return false; }
if (!item.HasAccess(character)) { return false; }
if (character != null && !IsItemInsideValidSubmarine(item, character)) { return false; }
if (item.HasBallastFloraInHull) { return false; }
@@ -1,5 +1,6 @@
#nullable enable
using Microsoft.Xna.Framework;
using System.Collections.Concurrent;
using System.Collections.Generic;
using System.Linq;
using Barotrauma.Items.Components;
@@ -68,8 +69,9 @@ namespace Barotrauma
/// <summary>
/// When did the character last inspect whether some other character has stolen items on them?
/// Thread-safe dictionary for concurrent access.
/// </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 CriminalInspectionInterval = 30.0f;
@@ -440,7 +440,7 @@ namespace Barotrauma
if (Identifier == Tags.DeconstructThis && item.AllowDeconstruct)
{
if (item.AllowDeconstruct && !Item.DeconstructItems.Contains(item) &&
if (item.AllowDeconstruct && !Item.IsMarkedForDeconstruction(item) &&
//only allow deconstructing if there are no deconstruction recipes (= deconstructing yields nothing), or deconstruction recipes that
(item.Prefab.DeconstructItems.None() ||
item.Prefab.DeconstructItems.Any(deconstructItem =>
@@ -454,7 +454,7 @@ namespace Barotrauma
}
else if (Identifier == Tags.DontDeconstructThis)
{
if (Item.DeconstructItems.Contains(item)) { return true; }
if (Item.IsMarkedForDeconstruction(item)) { return true; }
}
ImmutableArray<Identifier> targetItems = GetTargetItems(option);