Compare commits
13 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
| f9ad542029 | |||
| b5b25a2ccb | |||
| 25683dcf39 | |||
| d5d14e9684 | |||
| 086f45510f | |||
| e24024cbb2 | |||
| e7e444e9b2 | |||
| caec44c57d | |||
| f4a0d149ca | |||
| 7c61859840 | |||
| 9474f7654c | |||
| 854d7bea1f | |||
| 7b8275100d |
@@ -229,4 +229,4 @@ jobs:
|
||||
files: |
|
||||
${{ env.CI_DIR }}/archives/${{ env.ARCHIVE_BASE_NAME }}_{build,patch}_{windows,linux,mac}_{client,server}.zip
|
||||
${{ env.CI_DIR }}/archives/${{ env.ARCHIVE_BASE_NAME }}_{build,patch}_linux_{client,server}.tar.gz
|
||||
${{ env.CI_DIR }}/archives/${{ env.ARCHIVE_BASE_NAME }}_refs.zip
|
||||
${{ env.CI_DIR }}/archives/${{ env.ARCHIVE_BASE_NAME }}_refs.zip
|
||||
|
||||
@@ -61,3 +61,5 @@ Deploy/DeployAll/PrivateKey.*
|
||||
#Rider
|
||||
*.DotSettings.user
|
||||
.vscode/settings.json
|
||||
.vscode/launch.json
|
||||
.vscode/tasks.json
|
||||
|
||||
@@ -2143,7 +2143,7 @@ namespace Barotrauma
|
||||
if (existingAffliction == null)
|
||||
{
|
||||
existingAffliction = afflictionPrefab.Instantiate(strength);
|
||||
afflictions.Add(existingAffliction, limb);
|
||||
afflictions.TryAdd(existingAffliction, limb);
|
||||
newAdded = true;
|
||||
}
|
||||
existingAffliction.SetStrength(strength);
|
||||
|
||||
@@ -1388,12 +1388,12 @@ namespace Barotrauma
|
||||
if (me.SimPosition.Length() > 2000.0f)
|
||||
{
|
||||
NewMessage("Removed " + me.Name + " (simposition " + me.SimPosition + ")", Color.Orange);
|
||||
MapEntity.MapEntityList.RemoveAt(i);
|
||||
MapEntity.MapEntityList.Remove(me);
|
||||
}
|
||||
else if (!me.ShouldBeSaved)
|
||||
{
|
||||
NewMessage("Removed " + me.Name + " (!ShouldBeSaved)", Color.Orange);
|
||||
MapEntity.MapEntityList.RemoveAt(i);
|
||||
MapEntity.MapEntityList.Remove(me);
|
||||
}
|
||||
else if (me is Item)
|
||||
{
|
||||
|
||||
@@ -28,7 +28,7 @@ namespace Barotrauma
|
||||
|
||||
public void DebugDraw(SpriteBatch spriteBatch)
|
||||
{
|
||||
foreach (Event ev in activeEvents)
|
||||
foreach (Event ev in _activeEvents)
|
||||
{
|
||||
Vector2 drawPos = ev.DebugDrawPos;
|
||||
drawPos.Y = -drawPos.Y;
|
||||
@@ -41,7 +41,7 @@ namespace Barotrauma
|
||||
|
||||
public void DebugDrawHUD(SpriteBatch spriteBatch, float y)
|
||||
{
|
||||
foreach (ScriptedEvent scriptedEvent in activeEvents.Where(ev => !ev.IsFinished && ev is ScriptedEvent).Cast<ScriptedEvent>())
|
||||
foreach (ScriptedEvent scriptedEvent in _activeEvents.Where(ev => !ev.IsFinished && ev is ScriptedEvent).Cast<ScriptedEvent>())
|
||||
{
|
||||
DrawEventTargetTags(spriteBatch, scriptedEvent);
|
||||
}
|
||||
@@ -156,7 +156,7 @@ namespace Barotrauma
|
||||
{
|
||||
if (isGraphHovered || isGraphSelected)
|
||||
{
|
||||
foreach (var timeStamp in timeStamps)
|
||||
foreach (var timeStamp in _timeStamps)
|
||||
{
|
||||
int t = (int)Math.Abs(Math.Round((timeStamp.Time - lastIntensityUpdate) / intensityGraphUpdateInterval));
|
||||
if (t == order)
|
||||
@@ -205,7 +205,7 @@ namespace Barotrauma
|
||||
}
|
||||
|
||||
adjustedYStep = GUI.AdjustForTextScale(12);
|
||||
foreach (EventSet eventSet in pendingEventSets)
|
||||
foreach (EventSet eventSet in _pendingEventSets)
|
||||
{
|
||||
if (Submarine.MainSub == null) { break; }
|
||||
|
||||
@@ -263,7 +263,7 @@ namespace Barotrauma
|
||||
y += yStep;
|
||||
|
||||
adjustedYStep = GUI.AdjustForTextScale(18);
|
||||
foreach (Event ev in activeEvents.Where(ev => !ev.IsFinished || PlayerInput.IsShiftDown()))
|
||||
foreach (Event ev in _activeEvents.Where(ev => !ev.IsFinished || PlayerInput.IsShiftDown()))
|
||||
{
|
||||
GUI.DrawString(spriteBatch, new Vector2(x + 5, y), ev.ToString(), (!ev.IsFinished ? Color.White : Color.Red) * 0.8f, null, 0, GUIStyle.SmallFont);
|
||||
|
||||
@@ -752,4 +752,4 @@ namespace Barotrauma
|
||||
entry.CanBeCompleted);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -867,7 +867,7 @@ namespace Barotrauma
|
||||
{
|
||||
foreach (var stackedItem in item.GetStackedItems())
|
||||
{
|
||||
Item.DeconstructItems.Add(stackedItem);
|
||||
Item.MarkForDeconstruction(stackedItem);
|
||||
}
|
||||
HintManager.OnItemMarkedForDeconstruction(order.OrderGiver);
|
||||
}
|
||||
@@ -875,7 +875,7 @@ namespace Barotrauma
|
||||
{
|
||||
foreach (var stackedItem in item.GetStackedItems())
|
||||
{
|
||||
Item.DeconstructItems.Remove(stackedItem);
|
||||
Item.UnmarkForDeconstruction(stackedItem);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1933,7 +1933,7 @@ namespace Barotrauma.Items.Components
|
||||
|
||||
void CalculateDistance()
|
||||
{
|
||||
pathFinder ??= new PathFinder(WayPoint.WayPointList, false);
|
||||
pathFinder ??= new PathFinder(WayPoint.WayPointList.ToList(), false);
|
||||
var path = pathFinder.FindPath(ConvertUnits.ToSimUnits(transducerPosition), ConvertUnits.ToSimUnits(worldPosition));
|
||||
if (!path.Unreachable)
|
||||
{
|
||||
|
||||
@@ -1892,7 +1892,7 @@ namespace Barotrauma
|
||||
|
||||
}
|
||||
}
|
||||
else if (Item.DeconstructItems.Contains(item) &&
|
||||
else if (Item.IsMarkedForDeconstruction(item) &&
|
||||
OrderPrefab.Prefabs.TryGet(Tags.DeconstructThis, out OrderPrefab deconstructOrder))
|
||||
{
|
||||
DrawSideIcon(deconstructOrder.SymbolSprite, Direction.Right, TextManager.Get("tooltip.markedfordeconstruction"), GUIStyle.Red, out bool mouseOn);
|
||||
|
||||
@@ -471,11 +471,11 @@ namespace Barotrauma
|
||||
|
||||
if (item0 == null && item1 != null)
|
||||
{
|
||||
item0 = Item.ItemList.Find(it => it.GetComponent<ConnectionPanel>()?.DisconnectedWires.Contains(wire) ?? false);
|
||||
item0 = Item.ItemList.FirstOrDefault(it => it.GetComponent<ConnectionPanel>()?.DisconnectedWires.Contains(wire) ?? false);
|
||||
}
|
||||
else if (item0 != null && item1 == null)
|
||||
{
|
||||
item1 = Item.ItemList.Find(it => it.GetComponent<ConnectionPanel>()?.DisconnectedWires.Contains(wire) ?? false);
|
||||
item1 = Item.ItemList.FirstOrDefault(it => it.GetComponent<ConnectionPanel>()?.DisconnectedWires.Contains(wire) ?? false);
|
||||
}
|
||||
if (item0 != null && item1 != null && SelectedList.Contains(item0) && SelectedList.Contains(item1))
|
||||
{
|
||||
|
||||
@@ -105,7 +105,7 @@ namespace Barotrauma
|
||||
|
||||
public static void Draw(SpriteBatch spriteBatch, bool editing = false)
|
||||
{
|
||||
var entitiesToRender = !editing && visibleEntities != null ? visibleEntities : MapEntity.MapEntityList;
|
||||
var entitiesToRender = !editing && visibleEntities != null ? visibleEntities : MapEntity.MapEntityList.ToList();
|
||||
|
||||
foreach (MapEntity e in entitiesToRender)
|
||||
{
|
||||
@@ -115,7 +115,7 @@ namespace Barotrauma
|
||||
|
||||
public static void DrawFront(SpriteBatch spriteBatch, bool editing = false, Predicate<MapEntity> predicate = null)
|
||||
{
|
||||
var entitiesToRender = !editing && visibleEntities != null ? visibleEntities : MapEntity.MapEntityList;
|
||||
var entitiesToRender = !editing && visibleEntities != null ? visibleEntities : MapEntity.MapEntityList.ToList();
|
||||
|
||||
foreach (MapEntity e in entitiesToRender)
|
||||
{
|
||||
@@ -164,7 +164,7 @@ namespace Barotrauma
|
||||
|
||||
public static void DrawDamageable(SpriteBatch spriteBatch, Effect damageEffect, bool editing = false, Predicate<MapEntity> predicate = null)
|
||||
{
|
||||
var entitiesToRender = !editing && visibleEntities != null ? visibleEntities : MapEntity.MapEntityList;
|
||||
var entitiesToRender = !editing && visibleEntities != null ? visibleEntities : MapEntity.MapEntityList.ToList();
|
||||
|
||||
depthSortedDamageable.Clear();
|
||||
|
||||
@@ -197,7 +197,7 @@ namespace Barotrauma
|
||||
|
||||
public static void DrawPaintedColors(SpriteBatch spriteBatch, bool editing = false, Predicate<MapEntity> predicate = null)
|
||||
{
|
||||
var entitiesToRender = !editing && visibleEntities != null ? visibleEntities : MapEntity.MapEntityList;
|
||||
var entitiesToRender = !editing && visibleEntities != null ? visibleEntities : MapEntity.MapEntityList.ToList();
|
||||
|
||||
foreach (MapEntity e in entitiesToRender)
|
||||
{
|
||||
@@ -217,7 +217,7 @@ namespace Barotrauma
|
||||
|
||||
public static void DrawBack(SpriteBatch spriteBatch, bool editing = false, Predicate<MapEntity> predicate = null)
|
||||
{
|
||||
var entitiesToRender = !editing && visibleEntities != null ? visibleEntities : MapEntity.MapEntityList;
|
||||
var entitiesToRender = !editing && visibleEntities != null ? visibleEntities : MapEntity.MapEntityList.ToList();
|
||||
|
||||
foreach (MapEntity e in entitiesToRender)
|
||||
{
|
||||
|
||||
@@ -1,12 +1,32 @@
|
||||
using Barotrauma.Items.Components;
|
||||
using Barotrauma.Networking;
|
||||
using System.Collections.Concurrent;
|
||||
using System.Collections.Generic;
|
||||
|
||||
namespace Barotrauma
|
||||
{
|
||||
partial class EntitySpawner : Entity, IServerSerializable
|
||||
{
|
||||
public readonly List<(Entity entity, bool isRemoval)> receivedEvents = new List<(Entity entity, bool isRemoval)>();
|
||||
/// <summary>
|
||||
/// Thread-safe queue for received entity spawn/remove events from the server.
|
||||
/// </summary>
|
||||
private readonly ConcurrentQueue<(Entity entity, bool isRemoval)> receivedEventsQueue = new ConcurrentQueue<(Entity entity, bool isRemoval)>();
|
||||
|
||||
/// <summary>
|
||||
/// Gets a thread-safe snapshot of received events.
|
||||
/// </summary>
|
||||
public IEnumerable<(Entity entity, bool isRemoval)> GetReceivedEventsSnapshot()
|
||||
{
|
||||
return receivedEventsQueue.ToArray();
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Clears all received events from the queue.
|
||||
/// </summary>
|
||||
void ResetReceivedEvents()
|
||||
{
|
||||
while (receivedEventsQueue.TryDequeue(out _)) { }
|
||||
}
|
||||
|
||||
public void ClientEventRead(IReadMessage message, float sendingTime)
|
||||
{
|
||||
@@ -34,7 +54,7 @@ namespace Barotrauma
|
||||
{
|
||||
DebugConsole.Log("Received entity removal message for ID " + entityId + ". Entity with a matching ID not found.");
|
||||
}
|
||||
receivedEvents.Add((entity, true));
|
||||
receivedEventsQueue.Enqueue((entity, true));
|
||||
}
|
||||
else
|
||||
{
|
||||
@@ -57,7 +77,7 @@ namespace Barotrauma
|
||||
GameAnalyticsManager.AddDesignEvent("ItemFabricated:" + (GameMain.GameSession?.GameMode?.Preset.Identifier ?? "none".ToIdentifier()) + ":" + newItem.Prefab.Identifier);
|
||||
}
|
||||
}
|
||||
receivedEvents.Add((newItem, false));
|
||||
receivedEventsQueue.Enqueue((newItem, false));
|
||||
}
|
||||
break;
|
||||
case (byte)SpawnableType.Character:
|
||||
@@ -68,7 +88,7 @@ namespace Barotrauma
|
||||
}
|
||||
else
|
||||
{
|
||||
receivedEvents.Add((character, false));
|
||||
receivedEventsQueue.Enqueue((character, false));
|
||||
}
|
||||
break;
|
||||
default:
|
||||
|
||||
@@ -3916,7 +3916,7 @@ namespace Barotrauma.Networking
|
||||
{
|
||||
errorLines.Add("");
|
||||
errorLines.Add("EntitySpawner events:");
|
||||
foreach ((Entity entity, bool isRemoval) in Entity.Spawner.receivedEvents)
|
||||
foreach ((Entity entity, bool isRemoval) in Entity.Spawner.GetReceivedEventsSnapshot())
|
||||
{
|
||||
errorLines.Add(
|
||||
(isRemoval ? "Remove " : "Create ") +
|
||||
|
||||
@@ -503,86 +503,102 @@ namespace Barotrauma.Sounds
|
||||
mutex = new object();
|
||||
}
|
||||
|
||||
// Use the playingChannels lock to protect both channel assignment AND OpenAL operations.
|
||||
// This prevents race conditions when multiple threads try to play sounds simultaneously
|
||||
// (e.g., during Parallel.ForEach in MapEntity.UpdateAll).
|
||||
int poolIndex = (int)sound.SourcePoolIndex;
|
||||
object channelsLock = sound.Owner.GetPlayingChannelsLock(sound.SourcePoolIndex);
|
||||
|
||||
#if !DEBUG
|
||||
try
|
||||
{
|
||||
#endif
|
||||
if (mutex != null) { Monitor.Enter(mutex); }
|
||||
if (sound.Owner.CountPlayingInstances(sound) < sound.MaxSimultaneousInstances)
|
||||
lock (channelsLock)
|
||||
{
|
||||
ALSourceIndex = sound.Owner.AssignFreeSourceToChannel(this);
|
||||
}
|
||||
|
||||
if (ALSourceIndex >= 0)
|
||||
{
|
||||
if (!IsStream)
|
||||
if (mutex != null) { Monitor.Enter(mutex); }
|
||||
try
|
||||
{
|
||||
Al.Sourcei(sound.Owner.GetSourceFromIndex(Sound.SourcePoolIndex, ALSourceIndex), Al.Buffer, 0);
|
||||
int alError = Al.GetError();
|
||||
if (alError != Al.NoError)
|
||||
if (sound.Owner.CountPlayingInstancesUnsafe(sound, poolIndex) < sound.MaxSimultaneousInstances)
|
||||
{
|
||||
throw new Exception("Failed to reset source buffer: " + debugName + ", " + Al.GetErrorString(alError));
|
||||
ALSourceIndex = sound.Owner.AssignFreeSourceToChannelUnsafe(this, poolIndex);
|
||||
}
|
||||
|
||||
Sound.FillAlBuffers();
|
||||
if (Sound.Buffers is not { AlBuffer: not 0, AlMuffledBuffer: not 0 }) { return; }
|
||||
|
||||
uint alBuffer = sound.Owner.GetCategoryMuffle(category) || muffled ? Sound.Buffers.AlMuffledBuffer : Sound.Buffers.AlBuffer;
|
||||
Al.Sourcei(sound.Owner.GetSourceFromIndex(Sound.SourcePoolIndex, ALSourceIndex), Al.Buffer, (int)alBuffer);
|
||||
alError = Al.GetError();
|
||||
if (alError != Al.NoError)
|
||||
if (ALSourceIndex >= 0)
|
||||
{
|
||||
throw new Exception("Failed to bind buffer to source (" + ALSourceIndex.ToString() + ":" + sound.Owner.GetSourceFromIndex(Sound.SourcePoolIndex, ALSourceIndex) + "," + alBuffer.ToString() + "): " + debugName + ", " + Al.GetErrorString(alError));
|
||||
}
|
||||
if (!IsStream)
|
||||
{
|
||||
Al.Sourcei(sound.Owner.GetSourceFromIndex(Sound.SourcePoolIndex, ALSourceIndex), Al.Buffer, 0);
|
||||
int alError = Al.GetError();
|
||||
if (alError != Al.NoError)
|
||||
{
|
||||
throw new Exception("Failed to reset source buffer: " + debugName + ", " + Al.GetErrorString(alError));
|
||||
}
|
||||
|
||||
SetProperties();
|
||||
Sound.FillAlBuffers();
|
||||
if (Sound.Buffers is not { AlBuffer: not 0, AlMuffledBuffer: not 0 }) { return; }
|
||||
|
||||
Al.SourcePlay(sound.Owner.GetSourceFromIndex(Sound.SourcePoolIndex, ALSourceIndex));
|
||||
alError = Al.GetError();
|
||||
if (alError != Al.NoError)
|
||||
{
|
||||
throw new Exception("Failed to play source: " + debugName + ", " + Al.GetErrorString(alError));
|
||||
uint alBuffer = sound.Owner.GetCategoryMuffle(category) || muffled ? Sound.Buffers.AlMuffledBuffer : Sound.Buffers.AlBuffer;
|
||||
Al.Sourcei(sound.Owner.GetSourceFromIndex(Sound.SourcePoolIndex, ALSourceIndex), Al.Buffer, (int)alBuffer);
|
||||
alError = Al.GetError();
|
||||
if (alError != Al.NoError)
|
||||
{
|
||||
throw new Exception("Failed to bind buffer to source (" + ALSourceIndex.ToString() + ":" + sound.Owner.GetSourceFromIndex(Sound.SourcePoolIndex, ALSourceIndex) + "," + alBuffer.ToString() + "): " + debugName + ", " + Al.GetErrorString(alError));
|
||||
}
|
||||
|
||||
SetProperties();
|
||||
|
||||
Al.SourcePlay(sound.Owner.GetSourceFromIndex(Sound.SourcePoolIndex, ALSourceIndex));
|
||||
alError = Al.GetError();
|
||||
if (alError != Al.NoError)
|
||||
{
|
||||
throw new Exception("Failed to play source: " + debugName + ", " + Al.GetErrorString(alError));
|
||||
}
|
||||
}
|
||||
else
|
||||
{
|
||||
uint alBuffer = 0;
|
||||
Al.Sourcei(sound.Owner.GetSourceFromIndex(Sound.SourcePoolIndex, ALSourceIndex), Al.Buffer, (int)alBuffer);
|
||||
int alError = Al.GetError();
|
||||
if (alError != Al.NoError)
|
||||
{
|
||||
throw new Exception("Failed to reset source buffer: " + debugName + ", " + Al.GetErrorString(alError));
|
||||
}
|
||||
|
||||
Al.Sourcei(sound.Owner.GetSourceFromIndex(Sound.SourcePoolIndex, ALSourceIndex), Al.Looping, Al.False);
|
||||
alError = Al.GetError();
|
||||
if (alError != Al.NoError)
|
||||
{
|
||||
throw new Exception("Failed to set stream looping state: " + debugName + ", " + Al.GetErrorString(alError));
|
||||
}
|
||||
|
||||
streamShortBuffer = new short[STREAM_BUFFER_SIZE];
|
||||
|
||||
streamBuffers = new uint[4];
|
||||
unqueuedBuffers = new uint[4];
|
||||
streamBufferAmplitudes = new float[4];
|
||||
for (int i = 0; i < 4; i++)
|
||||
{
|
||||
Al.GenBuffer(out streamBuffers[i]);
|
||||
|
||||
alError = Al.GetError();
|
||||
if (alError != Al.NoError)
|
||||
{
|
||||
throw new Exception("Failed to generate stream buffers: " + debugName + ", " + Al.GetErrorString(alError));
|
||||
}
|
||||
|
||||
if (!Al.IsBuffer(streamBuffers[i]))
|
||||
{
|
||||
throw new Exception("Generated streamBuffer[" + i.ToString() + "] is invalid! " + debugName);
|
||||
}
|
||||
}
|
||||
Sound.Owner.InitUpdateChannelThread();
|
||||
SetProperties();
|
||||
}
|
||||
}
|
||||
}
|
||||
else
|
||||
finally
|
||||
{
|
||||
uint alBuffer = 0;
|
||||
Al.Sourcei(sound.Owner.GetSourceFromIndex(Sound.SourcePoolIndex, ALSourceIndex), Al.Buffer, (int)alBuffer);
|
||||
int alError = Al.GetError();
|
||||
if (alError != Al.NoError)
|
||||
{
|
||||
throw new Exception("Failed to reset source buffer: " + debugName + ", " + Al.GetErrorString(alError));
|
||||
}
|
||||
|
||||
Al.Sourcei(sound.Owner.GetSourceFromIndex(Sound.SourcePoolIndex, ALSourceIndex), Al.Looping, Al.False);
|
||||
alError = Al.GetError();
|
||||
if (alError != Al.NoError)
|
||||
{
|
||||
throw new Exception("Failed to set stream looping state: " + debugName + ", " + Al.GetErrorString(alError));
|
||||
}
|
||||
|
||||
streamShortBuffer = new short[STREAM_BUFFER_SIZE];
|
||||
|
||||
streamBuffers = new uint[4];
|
||||
unqueuedBuffers = new uint[4];
|
||||
streamBufferAmplitudes = new float[4];
|
||||
for (int i = 0; i < 4; i++)
|
||||
{
|
||||
Al.GenBuffer(out streamBuffers[i]);
|
||||
|
||||
alError = Al.GetError();
|
||||
if (alError != Al.NoError)
|
||||
{
|
||||
throw new Exception("Failed to generate stream buffers: " + debugName + ", " + Al.GetErrorString(alError));
|
||||
}
|
||||
|
||||
if (!Al.IsBuffer(streamBuffers[i]))
|
||||
{
|
||||
throw new Exception("Generated streamBuffer[" + i.ToString() + "] is invalid! " + debugName);
|
||||
}
|
||||
}
|
||||
Sound.Owner.InitUpdateChannelThread();
|
||||
SetProperties();
|
||||
if (mutex != null) { Monitor.Exit(mutex); }
|
||||
}
|
||||
}
|
||||
#if !DEBUG
|
||||
@@ -591,12 +607,6 @@ namespace Barotrauma.Sounds
|
||||
{
|
||||
throw;
|
||||
}
|
||||
finally
|
||||
{
|
||||
#endif
|
||||
if (mutex != null) { Monitor.Exit(mutex); }
|
||||
#if !DEBUG
|
||||
}
|
||||
#endif
|
||||
|
||||
void SetProperties()
|
||||
|
||||
@@ -417,6 +417,15 @@ namespace Barotrauma.Sounds
|
||||
return sourcePools[(int)poolIndex].ALSources[srcInd];
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Gets the lock object for the playing channels array for a specific pool.
|
||||
/// Used to protect OpenAL operations that need to be atomic with channel assignment.
|
||||
/// </summary>
|
||||
public object GetPlayingChannelsLock(SourcePoolIndex poolIndex)
|
||||
{
|
||||
return playingChannels[(int)poolIndex];
|
||||
}
|
||||
|
||||
public int AssignFreeSourceToChannel(SoundChannel newChannel)
|
||||
{
|
||||
if (Disabled) { return -1; }
|
||||
@@ -427,14 +436,25 @@ namespace Barotrauma.Sounds
|
||||
|
||||
lock (playingChannels[poolIndex])
|
||||
{
|
||||
for (int i = 0; i < playingChannels[poolIndex].Length; i++)
|
||||
return AssignFreeSourceToChannelUnsafe(newChannel, poolIndex);
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Assigns a free source to a channel without locking.
|
||||
/// Caller MUST hold the playingChannels[poolIndex] lock before calling this method.
|
||||
/// </summary>
|
||||
public int AssignFreeSourceToChannelUnsafe(SoundChannel newChannel, int poolIndex)
|
||||
{
|
||||
if (Disabled) { return -1; }
|
||||
|
||||
for (int i = 0; i < playingChannels[poolIndex].Length; i++)
|
||||
{
|
||||
if (playingChannels[poolIndex][i] == null || !playingChannels[poolIndex][i].IsPlaying)
|
||||
{
|
||||
if (playingChannels[poolIndex][i] == null || !playingChannels[poolIndex][i].IsPlaying)
|
||||
{
|
||||
if (playingChannels[poolIndex][i] != null) { playingChannels[poolIndex][i].Dispose(); }
|
||||
playingChannels[poolIndex][i] = newChannel;
|
||||
return i;
|
||||
}
|
||||
if (playingChannels[poolIndex][i] != null) { playingChannels[poolIndex][i].Dispose(); }
|
||||
playingChannels[poolIndex][i] = newChannel;
|
||||
return i;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -476,13 +496,25 @@ namespace Barotrauma.Sounds
|
||||
int count = 0;
|
||||
lock (playingChannels[(int)sound.SourcePoolIndex])
|
||||
{
|
||||
for (int i = 0; i < playingChannels[(int)sound.SourcePoolIndex].Length; i++)
|
||||
count = CountPlayingInstancesUnsafe(sound, (int)sound.SourcePoolIndex);
|
||||
}
|
||||
return count;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Counts playing instances without locking.
|
||||
/// Caller MUST hold the playingChannels[poolIndex] lock before calling this method.
|
||||
/// </summary>
|
||||
public int CountPlayingInstancesUnsafe(Sound sound, int poolIndex)
|
||||
{
|
||||
if (Disabled) { return 0; }
|
||||
int count = 0;
|
||||
for (int i = 0; i < playingChannels[poolIndex].Length; i++)
|
||||
{
|
||||
if (playingChannels[poolIndex][i] != null &&
|
||||
playingChannels[poolIndex][i].Sound.Filename == sound.Filename)
|
||||
{
|
||||
if (playingChannels[(int)sound.SourcePoolIndex][i] != null &&
|
||||
playingChannels[(int)sound.SourcePoolIndex][i].Sound.Filename == sound.Filename)
|
||||
{
|
||||
if (playingChannels[(int)sound.SourcePoolIndex][i].IsPlaying) { count++; };
|
||||
}
|
||||
if (playingChannels[poolIndex][i].IsPlaying) { count++; };
|
||||
}
|
||||
}
|
||||
return count;
|
||||
|
||||
@@ -28,11 +28,14 @@ namespace Barotrauma
|
||||
}
|
||||
}
|
||||
|
||||
// Create snapshot to avoid concurrent access issues during parallel updates
|
||||
var clients = GameMain.Server.ConnectedClients.ToArray();
|
||||
|
||||
if (GameMain.Server is { ServerSettings.RespawnMode: RespawnMode.Permadeath } &&
|
||||
GameMain.GameSession?.Campaign is MultiPlayerCampaign mpCampaign &&
|
||||
causeOfDeath != CauseOfDeathType.Disconnected)
|
||||
{
|
||||
Client ownerClient = GameMain.Server.ConnectedClients.FirstOrDefault(c => c.Character == this);
|
||||
Client ownerClient = clients.FirstOrDefault(c => c.Character == this);
|
||||
if (ownerClient != null)
|
||||
{
|
||||
ownerClient.SpectateOnly = true;
|
||||
@@ -51,7 +54,7 @@ namespace Barotrauma
|
||||
|
||||
if (HasAbilityFlag(AbilityFlags.RetainExperienceForNewCharacter))
|
||||
{
|
||||
var ownerClient = GameMain.Server.ConnectedClients.Find(c => c.Character == this);
|
||||
var ownerClient = clients.FirstOrDefault(c => c.Character == this);
|
||||
if (ownerClient != null)
|
||||
{
|
||||
(GameMain.GameSession?.GameMode as MultiPlayerCampaign)?.SaveExperiencePoints(ownerClient);
|
||||
@@ -62,7 +65,7 @@ namespace Barotrauma
|
||||
|
||||
if (CauseOfDeath.Killer != null && CauseOfDeath.Killer.IsTraitor && CauseOfDeath.Killer != this)
|
||||
{
|
||||
var owner = GameMain.Server.ConnectedClients.Find(c => c.Character == this);
|
||||
var owner = clients.FirstOrDefault(c => c.Character == this);
|
||||
if (owner != null)
|
||||
{
|
||||
if (!LuaCsSetup.Instance.Game.overrideTraitors)
|
||||
@@ -71,11 +74,11 @@ namespace Barotrauma
|
||||
}
|
||||
}
|
||||
}
|
||||
foreach (Client client in GameMain.Server.ConnectedClients)
|
||||
foreach (Client client in clients)
|
||||
{
|
||||
if (client.InGame)
|
||||
{
|
||||
client.PendingPositionUpdates.Enqueue(this);
|
||||
client.TryEnqueuePositionUpdate(this);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -486,7 +486,9 @@ namespace Barotrauma
|
||||
case ControlEventData controlEventData:
|
||||
Client owner = controlEventData.Owner;
|
||||
msg.WriteBoolean(owner == c && owner.Character == this);
|
||||
msg.WriteByte(owner != null && owner.Character == this && GameMain.Server.ConnectedClients.Contains(owner) ? owner.SessionId : (byte)0);
|
||||
// Create snapshot to avoid concurrent access issues during parallel updates
|
||||
var connectedClients = GameMain.Server.ConnectedClients.ToArray();
|
||||
msg.WriteByte(owner != null && owner.Character == this && connectedClients.Contains(owner) ? owner.SessionId : (byte)0);
|
||||
msg.WriteBoolean(info is { RenamingEnabled: true });
|
||||
break;
|
||||
case CharacterStatusEventData statusEventData:
|
||||
@@ -742,7 +744,9 @@ namespace Barotrauma
|
||||
return;
|
||||
}
|
||||
|
||||
Client ownerClient = GameMain.Server.ConnectedClients.Find(c => c.Character == this && (!c.SpectateOnly || !GameMain.Server.ServerSettings.AllowSpectating));
|
||||
// Create snapshot to avoid concurrent access issues during parallel updates
|
||||
var clients = GameMain.Server.ConnectedClients.ToArray();
|
||||
Client ownerClient = clients.FirstOrDefault(c => c.Character == this && (!c.SpectateOnly || !GameMain.Server.ServerSettings.AllowSpectating));
|
||||
if (ownerClient != null)
|
||||
{
|
||||
msg.WriteBoolean(true);
|
||||
|
||||
@@ -82,10 +82,13 @@ namespace Barotrauma
|
||||
|
||||
private bool IsBlockedByAnotherConversation(IEnumerable<Entity> targets, float duration)
|
||||
{
|
||||
// Create snapshot to avoid concurrent access issues during parallel updates
|
||||
var clients = GameMain.Server.ConnectedClients.ToArray();
|
||||
|
||||
if (targets == null || targets.None())
|
||||
{
|
||||
//if the action doesn't target anyone in specific, it's shown to every client
|
||||
foreach (var client in GameMain.Server.ConnectedClients)
|
||||
foreach (var client in clients)
|
||||
{
|
||||
if (IsBlockedByAnotherConversation(client, duration)) { return true; }
|
||||
}
|
||||
@@ -95,7 +98,7 @@ namespace Barotrauma
|
||||
foreach (Entity e in targets)
|
||||
{
|
||||
if (e is not Character character || !character.IsRemotePlayer) { continue; }
|
||||
Client targetClient = GameMain.Server.ConnectedClients.Find(c => c.Character == character);
|
||||
Client targetClient = clients.FirstOrDefault(c => c.Character == character);
|
||||
if (targetClient != null && IsBlockedByAnotherConversation(targetClient, duration)) { return true; }
|
||||
}
|
||||
}
|
||||
@@ -117,13 +120,16 @@ namespace Barotrauma
|
||||
partial void ShowDialog(Character speaker, Character targetCharacter)
|
||||
{
|
||||
targetClients.Clear();
|
||||
// Create snapshot to avoid concurrent access issues during parallel updates
|
||||
var clients = GameMain.Server.ConnectedClients.ToArray();
|
||||
|
||||
if (!TargetTag.IsEmpty)
|
||||
{
|
||||
IEnumerable<Entity> entities = ParentEvent.GetTargets(TargetTag);
|
||||
foreach (Entity e in entities)
|
||||
{
|
||||
if (e is not Character character || !character.IsRemotePlayer) { continue; }
|
||||
Client targetClient = GameMain.Server.ConnectedClients.Find(c => c.Character == character);
|
||||
Client targetClient = clients.FirstOrDefault(c => c.Character == character);
|
||||
if (targetClient != null)
|
||||
{
|
||||
targetClients.Add(targetClient);
|
||||
@@ -135,7 +141,7 @@ namespace Barotrauma
|
||||
}
|
||||
else
|
||||
{
|
||||
foreach (Client c in GameMain.Server.ConnectedClients)
|
||||
foreach (Client c in clients)
|
||||
{
|
||||
if (CanClientReceive(c))
|
||||
{
|
||||
|
||||
@@ -9,48 +9,52 @@ namespace Barotrauma;
|
||||
|
||||
partial class EventLogAction : EventAction
|
||||
{
|
||||
partial void AddEntryProjSpecific(EventLog? eventLog, string displayText)
|
||||
partial void AddEntryProjSpecific(EventLog? eventLog, string displayText)
|
||||
{
|
||||
if (eventLog == null) { return; }
|
||||
|
||||
// Create snapshot to avoid concurrent access issues during parallel updates
|
||||
var clients = GameMain.Server.ConnectedClients.ToArray();
|
||||
|
||||
if (!TargetTag.IsEmpty)
|
||||
{
|
||||
if (eventLog == null) { return; }
|
||||
if (!TargetTag.IsEmpty)
|
||||
List<Client> targetClients = new List<Client>();
|
||||
foreach (var target in ParentEvent.GetTargets(TargetTag))
|
||||
{
|
||||
List<Client> targetClients = new List<Client>();
|
||||
foreach (var target in ParentEvent.GetTargets(TargetTag))
|
||||
if (target is Character character)
|
||||
{
|
||||
if (target is Character character)
|
||||
var ownerClient = clients.FirstOrDefault(c => c.Character == character);
|
||||
if (ownerClient != null)
|
||||
{
|
||||
var ownerClient = GameMain.Server.ConnectedClients.Find(c => c.Character == character);
|
||||
if (ownerClient != null)
|
||||
{
|
||||
targetClients.Add(ownerClient);
|
||||
}
|
||||
}
|
||||
else
|
||||
{
|
||||
DebugConsole.AddWarning($"{target} is not a valid target for an EventLogAction. The target should be a character.",
|
||||
ParentEvent.Prefab.ContentPackage);
|
||||
targetClients.Add(ownerClient);
|
||||
}
|
||||
}
|
||||
if (eventLog!.TryAddEntry(ParentEvent.Prefab.Identifier, Id, displayText, targetClients) && ShowInServerLog)
|
||||
else
|
||||
{
|
||||
Log(targetClients);
|
||||
DebugConsole.AddWarning($"{target} is not a valid target for an EventLogAction. The target should be a character.",
|
||||
ParentEvent.Prefab.ContentPackage);
|
||||
}
|
||||
}
|
||||
else
|
||||
if (eventLog!.TryAddEntry(ParentEvent.Prefab.Identifier, Id, displayText, targetClients) && ShowInServerLog)
|
||||
{
|
||||
if (eventLog.TryAddEntry(ParentEvent.Prefab.Identifier, Id, displayText, GameMain.Server.ConnectedClients) && ShowInServerLog)
|
||||
{
|
||||
Log(targetClients: null);
|
||||
}
|
||||
}
|
||||
|
||||
void Log(List<Client>? targetClients)
|
||||
{
|
||||
string clientStr = targetClients == null || targetClients.None() ?
|
||||
string.Empty :
|
||||
$" ({string.Join(", ", targetClients.Select(c => NetworkMember.ClientLogName(c)))})";
|
||||
GameServer.Log($"Event \"{ParentEvent.Prefab.Name}\"{clientStr}: " + displayText,
|
||||
ParentEvent is TraitorEvent ? ServerLog.MessageType.Traitors : ServerLog.MessageType.Chat);
|
||||
Log(targetClients);
|
||||
}
|
||||
}
|
||||
else
|
||||
{
|
||||
if (eventLog.TryAddEntry(ParentEvent.Prefab.Identifier, Id, displayText, clients) && ShowInServerLog)
|
||||
{
|
||||
Log(targetClients: null);
|
||||
}
|
||||
}
|
||||
|
||||
void Log(List<Client>? targetClients)
|
||||
{
|
||||
string clientStr = targetClients == null || targetClients.None() ?
|
||||
string.Empty :
|
||||
$" ({string.Join(", ", targetClients.Select(c => NetworkMember.ClientLogName(c)))})";
|
||||
GameServer.Log($"Event \"{ParentEvent.Prefab.Name}\"{clientStr}: " + displayText,
|
||||
ParentEvent is TraitorEvent ? ServerLog.MessageType.Traitors : ServerLog.MessageType.Chat);
|
||||
}
|
||||
}
|
||||
}
|
||||
+8
-3
@@ -1,3 +1,5 @@
|
||||
using System.Linq;
|
||||
|
||||
namespace Barotrauma
|
||||
{
|
||||
partial class EventObjectiveAction : EventAction
|
||||
@@ -13,9 +15,12 @@ namespace Barotrauma
|
||||
ParentObjectiveId,
|
||||
CanBeCompleted);
|
||||
|
||||
// Create snapshot to avoid concurrent access issues during parallel updates
|
||||
var clients = GameMain.Server.ConnectedClients.ToArray();
|
||||
|
||||
if (TargetTag.IsEmpty)
|
||||
{
|
||||
foreach (var client in GameMain.Server.ConnectedClients)
|
||||
foreach (var client in clients)
|
||||
{
|
||||
if (client.Character == null) { continue; }
|
||||
EventManager.ServerWriteObjective(client, objective);
|
||||
@@ -26,11 +31,11 @@ namespace Barotrauma
|
||||
foreach (var target in ParentEvent.GetTargets(TargetTag))
|
||||
{
|
||||
if (target is not Character character) { continue; }
|
||||
var ownerClient = GameMain.Server.ConnectedClients.Find(c => c.Character == character);
|
||||
var ownerClient = clients.FirstOrDefault(c => c.Character == character);
|
||||
if (ownerClient == null) { continue; }
|
||||
EventManager.ServerWriteObjective(ownerClient, objective);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -14,8 +14,10 @@ partial class HighlightAction : EventAction
|
||||
IEnumerable<Client>? targetClients = null;
|
||||
if (targetCharacters != null)
|
||||
{
|
||||
// Create snapshot to avoid concurrent access issues during parallel updates
|
||||
var clients = GameMain.Server.ConnectedClients.ToArray();
|
||||
targetClients = targetCharacters
|
||||
.Select(c => GameMain.Server.ConnectedClients.FirstOrDefault(client => client.Character == c))
|
||||
.Select(c => clients.FirstOrDefault(client => client.Character == c))
|
||||
.Where(c => c != null)!;
|
||||
}
|
||||
GameMain.Server?.CreateEntityEvent(item, new Item.SetHighlightEventData(State, highlightColor, targetClients));
|
||||
|
||||
@@ -1,5 +1,6 @@
|
||||
using Barotrauma.Networking;
|
||||
using Barotrauma.Networking;
|
||||
using System.Collections.Generic;
|
||||
using System.Linq;
|
||||
|
||||
namespace Barotrauma
|
||||
{
|
||||
@@ -22,7 +23,9 @@ namespace Barotrauma
|
||||
|
||||
private static void NotifyMissionUnlock(Mission mission)
|
||||
{
|
||||
foreach (Client client in GameMain.Server.ConnectedClients)
|
||||
// Create snapshot to avoid concurrent access issues during parallel updates
|
||||
var clients = GameMain.Server.ConnectedClients.ToArray();
|
||||
foreach (Client client in clients)
|
||||
{
|
||||
NotifyMissionUnlock(mission, client);
|
||||
}
|
||||
@@ -40,4 +43,4 @@ namespace Barotrauma
|
||||
GameMain.Server.ServerPeer.Send(outmsg, client.Connection, DeliveryMethod.Reliable);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -25,4 +25,4 @@ namespace Barotrauma
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -33,7 +33,7 @@ namespace Barotrauma
|
||||
byte selectedOption = inc.ReadByte();
|
||||
bool isIgnore = selectedOption == byte.MaxValue;
|
||||
|
||||
foreach (Event ev in activeEvents)
|
||||
foreach (Event ev in _activeEvents)
|
||||
{
|
||||
if (ev is not ScriptedEvent scriptedEvent) { continue; }
|
||||
|
||||
|
||||
@@ -76,7 +76,9 @@ namespace Barotrauma.Items.Components
|
||||
{
|
||||
var (msg, deliveryMethod) = PrepareToSend(opcode, data);
|
||||
|
||||
foreach (Client client in GameMain.Server.ConnectedClients)
|
||||
// Create snapshot to avoid concurrent access issues during parallel updates
|
||||
var clients = GameMain.Server.ConnectedClients.ToArray();
|
||||
foreach (Client client in clients)
|
||||
{
|
||||
if (predicate is not null && !predicate(client)) { continue; }
|
||||
|
||||
@@ -391,4 +393,4 @@ namespace Barotrauma.Items.Components
|
||||
CreateServerEvent(new CircuitBoxServerUpdateSelection(nodes, wires, ios, labels));
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -211,9 +211,9 @@ namespace Barotrauma
|
||||
#if DEBUG || UNSTABLE
|
||||
DebugConsole.NewMessage($"Client {sender.Name} failed to put \"{item}\" in the inventory of {Owner} (parent inventory: {item.ParentInventory?.Owner.ToString() ?? "null"}). No access.", Color.Yellow);
|
||||
#endif
|
||||
if (item.body != null && !sender.PendingPositionUpdates.Contains(item))
|
||||
if (item.body != null)
|
||||
{
|
||||
sender.PendingPositionUpdates.Enqueue(item);
|
||||
sender.TryEnqueuePositionUpdate(item);
|
||||
}
|
||||
item.PositionUpdateInterval = 0.0f;
|
||||
continue;
|
||||
|
||||
@@ -29,7 +29,9 @@ namespace Barotrauma
|
||||
|
||||
//don't create updates if all clients are very far from the hull
|
||||
float hullUpdateDistanceSqr = NetConfig.HullUpdateDistance * NetConfig.HullUpdateDistance;
|
||||
if (!GameMain.Server.ConnectedClients.Any(c =>
|
||||
// Create snapshot to avoid concurrent access issues during parallel updates
|
||||
var clients = GameMain.Server.ConnectedClients.ToArray();
|
||||
if (!clients.Any(c =>
|
||||
(c.Character != null && Vector2.DistanceSquared(c.Character.WorldPosition, WorldPosition) < hullUpdateDistanceSqr) ||
|
||||
(c.SpectatePos != null && Vector2.DistanceSquared(c.SpectatePos.Value, WorldPosition) < hullUpdateDistanceSqr)) )
|
||||
{
|
||||
|
||||
@@ -64,6 +64,32 @@ namespace Barotrauma.Networking
|
||||
// key = entity, value = NetTime.Now when sending
|
||||
public readonly Dictionary<Entity, float> PositionUpdateLastSent = new Dictionary<Entity, float>();
|
||||
public readonly Queue<Entity> PendingPositionUpdates = new Queue<Entity>();
|
||||
private readonly HashSet<Entity> pendingPositionUpdatesSet = new HashSet<Entity>();
|
||||
|
||||
/// <summary>
|
||||
/// Attempts to enqueue a position update for the given entity. Returns true if the entity was added, false if it was already in the queue.
|
||||
/// Uses HashSet for O(1) lookup instead of Queue.Contains() which is O(n).
|
||||
/// </summary>
|
||||
public bool TryEnqueuePositionUpdate(Entity entity)
|
||||
{
|
||||
if (pendingPositionUpdatesSet.Add(entity))
|
||||
{
|
||||
PendingPositionUpdates.Enqueue(entity);
|
||||
return true;
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Dequeues a position update and removes it from the HashSet tracking.
|
||||
/// </summary>
|
||||
public Entity DequeuePositionUpdate()
|
||||
{
|
||||
if (PendingPositionUpdates.Count == 0) { return null; }
|
||||
var entity = PendingPositionUpdates.Dequeue();
|
||||
pendingPositionUpdatesSet.Remove(entity);
|
||||
return entity;
|
||||
}
|
||||
|
||||
public bool ReadyToStart;
|
||||
|
||||
@@ -126,7 +152,7 @@ namespace Barotrauma.Networking
|
||||
if (!MathUtils.NearlyEqual(karma, syncedKarma, 10.0f))
|
||||
{
|
||||
syncedKarma = karma;
|
||||
GameMain.NetworkMember.LastClientListUpdateID++;
|
||||
GameMain.NetworkMember.IncrementLastClientListUpdateID();
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -353,6 +379,7 @@ namespace Barotrauma.Networking
|
||||
{
|
||||
NeedsMidRoundSync = false;
|
||||
PendingPositionUpdates.Clear();
|
||||
pendingPositionUpdatesSet.Clear();
|
||||
EntityEventLastSent.Clear();
|
||||
LastSentEntityEventID = 0;
|
||||
LastRecvEntityEventID = 0;
|
||||
|
||||
@@ -174,7 +174,7 @@ namespace Barotrauma.Networking
|
||||
StartTime = DateTime.Now;
|
||||
|
||||
OnStarted(transfer);
|
||||
GameMain.Server.LastClientListUpdateID++;
|
||||
GameMain.Server.IncrementLastClientListUpdateID();
|
||||
|
||||
return transfer;
|
||||
}
|
||||
@@ -204,7 +204,7 @@ namespace Barotrauma.Networking
|
||||
|
||||
if (numRemoved > 0 || endedTransfers.Count > 0)
|
||||
{
|
||||
GameMain.Server.LastClientListUpdateID++;
|
||||
GameMain.Server.IncrementLastClientListUpdateID();
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -327,7 +327,7 @@ namespace Barotrauma.Networking
|
||||
}
|
||||
}
|
||||
|
||||
LastClientListUpdateID++;
|
||||
IncrementLastClientListUpdateID();
|
||||
|
||||
if (newClient.Connection == OwnerConnection && OwnerConnection != null)
|
||||
{
|
||||
@@ -742,11 +742,6 @@ namespace Barotrauma.Networking
|
||||
{
|
||||
errorMsg += "\nInner exception: " + e.InnerException.Message + "\n" + e.InnerException.StackTrace.CleanupStackTrace();
|
||||
}
|
||||
|
||||
GameAnalyticsManager.AddErrorEventOnce(
|
||||
"GameServer.Update:ClientWriteFailed" + e.StackTrace.CleanupStackTrace(),
|
||||
GameAnalyticsManager.ErrorSeverity.Error,
|
||||
errorMsg);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1133,9 +1128,6 @@ namespace Barotrauma.Networking
|
||||
|
||||
Log(ClientLogName(c) + " has reported an error: " + errorStr, ServerLog.MessageType.Error);
|
||||
GameAnalyticsManager.AddErrorEventOnce("GameServer.HandleClientError:" + errorStrNoName, GameAnalyticsManager.ErrorSeverity.Error, errorStr);
|
||||
Log(
|
||||
$"Entity event state at client error: pending={EntityEventManager.PendingCreateEventCount}, queued={EntityEventManager.EventCount}, unique={EntityEventManager.UniqueEventCount}, buffered={EntityEventManager.BufferedEventCount}, lastCreated={EntityEventManager.LastCreatedEventID}",
|
||||
ServerLog.MessageType.Error);
|
||||
|
||||
try
|
||||
{
|
||||
@@ -2148,7 +2140,7 @@ namespace Barotrauma.Networking
|
||||
{
|
||||
if (lastSent > NetTime.Now - updateInterval) { continue; }
|
||||
}
|
||||
if (!c.PendingPositionUpdates.Contains(otherCharacter)) { c.PendingPositionUpdates.Enqueue(otherCharacter); }
|
||||
c.TryEnqueuePositionUpdate(otherCharacter);
|
||||
}
|
||||
|
||||
foreach (Submarine sub in Submarine.Loaded)
|
||||
@@ -2157,7 +2149,7 @@ namespace Barotrauma.Networking
|
||||
// (= update is only sent for the docked sub that has the smallest ID, doesn't matter if it's the main sub or a shuttle)
|
||||
if (sub.Info.IsOutpost || sub.DockedTo.Any(s => s.ID < sub.ID)) { continue; }
|
||||
if (sub.PhysicsBody == null || sub.PhysicsBody.BodyType == FarseerPhysics.BodyType.Static) { continue; }
|
||||
if (!c.PendingPositionUpdates.Contains(sub)) { c.PendingPositionUpdates.Enqueue(sub); }
|
||||
c.TryEnqueuePositionUpdate(sub);
|
||||
}
|
||||
|
||||
foreach (Item item in Item.ItemList)
|
||||
@@ -2174,7 +2166,7 @@ namespace Barotrauma.Networking
|
||||
{
|
||||
if (lastSent > NetTime.Now - updateInterval) { continue; }
|
||||
}
|
||||
if (!c.PendingPositionUpdates.Contains(item)) { c.PendingPositionUpdates.Enqueue(item); }
|
||||
c.TryEnqueuePositionUpdate(item);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -2218,7 +2210,7 @@ namespace Barotrauma.Networking
|
||||
entity.Removed ||
|
||||
(entity is Item item && float.IsInfinity(item.PositionUpdateInterval)))
|
||||
{
|
||||
c.PendingPositionUpdates.Dequeue();
|
||||
c.DequeuePositionUpdate();
|
||||
continue;
|
||||
}
|
||||
|
||||
@@ -2240,7 +2232,7 @@ namespace Barotrauma.Networking
|
||||
outmsg.WritePadBits();
|
||||
|
||||
c.PositionUpdateLastSent[entity] = (float)NetTime.Now;
|
||||
c.PendingPositionUpdates.Dequeue();
|
||||
c.DequeuePositionUpdate();
|
||||
}
|
||||
positionUpdateBytes = outmsg.LengthBytes - positionUpdateBytes;
|
||||
|
||||
@@ -3215,7 +3207,7 @@ namespace Barotrauma.Networking
|
||||
initiatedStartGame = false;
|
||||
GameMain.ResetFrameTime();
|
||||
|
||||
LastClientListUpdateID++;
|
||||
IncrementLastClientListUpdateID();
|
||||
|
||||
roundStartTime = DateTime.Now;
|
||||
|
||||
@@ -3514,7 +3506,7 @@ namespace Barotrauma.Networking
|
||||
{
|
||||
var coolDownRemaining = Client.NameChangeCoolDown - timeSinceNameChange;
|
||||
SendDirectChatMessage($"ServerMessage.NameChangeFailedCooldownActive~[seconds]={(int)coolDownRemaining.TotalSeconds}", c);
|
||||
LastClientListUpdateID++;
|
||||
IncrementLastClientListUpdateID();
|
||||
//increment the ID to make sure the current server-side name is treated as the "latest",
|
||||
//and the client correctly reverts back to the old name
|
||||
c.NameId++;
|
||||
@@ -3528,7 +3520,7 @@ namespace Barotrauma.Networking
|
||||
|
||||
if (result != null)
|
||||
{
|
||||
LastClientListUpdateID++;
|
||||
IncrementLastClientListUpdateID();
|
||||
return result.Value;
|
||||
}
|
||||
|
||||
@@ -3545,14 +3537,14 @@ namespace Barotrauma.Networking
|
||||
c.Name = newName;
|
||||
c.RejectedName = string.Empty;
|
||||
SendChatMessage($"ServerMessage.NameChangeSuccessful~[oldname]={oldName}~[newname]={newName}", ChatMessageType.Server);
|
||||
LastClientListUpdateID++;
|
||||
IncrementLastClientListUpdateID();
|
||||
return true;
|
||||
}
|
||||
else
|
||||
{
|
||||
//update client list even if the name cannot be changed to the one sent by the client,
|
||||
//so the client will be informed what their actual name is
|
||||
LastClientListUpdateID++;
|
||||
IncrementLastClientListUpdateID();
|
||||
return false;
|
||||
}
|
||||
}
|
||||
@@ -4791,7 +4783,9 @@ namespace Barotrauma.Networking
|
||||
public static string CharacterLogName(Character character)
|
||||
{
|
||||
if (character == null) { return "[NULL]"; }
|
||||
Client client = GameMain.Server.ConnectedClients.Find(c => c.Character == character);
|
||||
// Create snapshot to avoid concurrent access issues during parallel updates
|
||||
var clients = GameMain.Server.ConnectedClients.ToArray();
|
||||
Client client = clients.FirstOrDefault(c => c.Character == character);
|
||||
return ClientLogName(client, character.LogName);
|
||||
}
|
||||
|
||||
@@ -4802,8 +4796,8 @@ namespace Barotrauma.Networking
|
||||
LuaCsSetup.Instance?.EventService.PublishEvent<IEventServerLog>(x => x.OnServerLog(line, messageType));
|
||||
|
||||
GameMain.Server.ServerSettings.ServerLog.WriteLine(line, messageType);
|
||||
|
||||
foreach (Client client in GameMain.Server.ConnectedClients)
|
||||
var clients = GameMain.Server.ConnectedClients.ToArray();
|
||||
foreach (Client client in clients)
|
||||
{
|
||||
if (!client.HasPermission(ClientPermissions.ServerLog)) continue;
|
||||
//use sendername as the message type
|
||||
@@ -4844,7 +4838,7 @@ namespace Barotrauma.Networking
|
||||
private void UpdateClientLobbies()
|
||||
{
|
||||
// Triggers a call to WriteClientList(), which causes clients to call GameClient.ReadClientList()
|
||||
LastClientListUpdateID++;
|
||||
IncrementLastClientListUpdateID();
|
||||
}
|
||||
|
||||
private List<Client> GetPlayingClients()
|
||||
|
||||
@@ -163,7 +163,7 @@ namespace Barotrauma
|
||||
{
|
||||
client.Character.CharacterHealth.ApplyAffliction(null, new Affliction(herpesAffliction, herpesStrength));
|
||||
GameServer.Log($"{GameServer.ClientLogName(client)} has contracted space herpes due to low karma.", ServerLog.MessageType.Karma);
|
||||
GameMain.NetworkMember.LastClientListUpdateID++;
|
||||
GameMain.NetworkMember.IncrementLastClientListUpdateID();
|
||||
}
|
||||
else if (existingAffliction != null)
|
||||
{
|
||||
|
||||
+63
-53
@@ -5,8 +5,12 @@ using System.Collections.Concurrent;
|
||||
using System.Collections.Generic;
|
||||
using System.Linq;
|
||||
using System.Threading;
|
||||
using System.Threading.Tasks;
|
||||
using static Barotrauma.EosInterface.Ownership;
|
||||
|
||||
|
||||
// DO NOT TOUCH ANYTHING HERE
|
||||
// OR EVERYTHING WILL FAIL
|
||||
namespace Barotrauma.Networking
|
||||
{
|
||||
class ServerEntityEvent : NetEntityEvent
|
||||
@@ -62,42 +66,14 @@ namespace Barotrauma.Networking
|
||||
|
||||
public List<ServerEntityEvent> Events
|
||||
{
|
||||
get
|
||||
{
|
||||
FlushPendingCreates();
|
||||
return events;
|
||||
}
|
||||
get { return events; }
|
||||
}
|
||||
|
||||
public List<ServerEntityEvent> UniqueEvents
|
||||
{
|
||||
get
|
||||
{
|
||||
FlushPendingCreates();
|
||||
return uniqueEvents;
|
||||
}
|
||||
get { return uniqueEvents; }
|
||||
}
|
||||
|
||||
public int PendingCreateEventCount => pendingCreateQueue.Count;
|
||||
public int EventCount
|
||||
{
|
||||
get
|
||||
{
|
||||
FlushPendingCreates();
|
||||
return events.Count;
|
||||
}
|
||||
}
|
||||
public int UniqueEventCount
|
||||
{
|
||||
get
|
||||
{
|
||||
FlushPendingCreates();
|
||||
return uniqueEvents.Count;
|
||||
}
|
||||
}
|
||||
public int BufferedEventCount => bufferedEvents.Count;
|
||||
public UInt16 LastCreatedEventID => ID;
|
||||
|
||||
private class BufferedEvent
|
||||
{
|
||||
public readonly Client Sender;
|
||||
@@ -148,6 +124,11 @@ namespace Barotrauma.Networking
|
||||
|
||||
private readonly ConcurrentQueue<PendingCreateEvent> pendingCreateQueue;
|
||||
|
||||
private readonly Task createEventTask;
|
||||
|
||||
private readonly CancellationTokenSource cancellationTokenSource = new CancellationTokenSource();
|
||||
private readonly SemaphoreSlim eventSignal = new SemaphoreSlim(0);
|
||||
|
||||
public ServerEntityEventManager(GameServer server)
|
||||
{
|
||||
events = new List<ServerEntityEvent>();
|
||||
@@ -157,24 +138,33 @@ namespace Barotrauma.Networking
|
||||
pendingCreateQueue = new ConcurrentQueue<PendingCreateEvent>();
|
||||
lastWarningTime = -10.0;
|
||||
SEM = this;
|
||||
|
||||
createEventTask = Task.Run(() => CreateEventProcessorLoop(cancellationTokenSource.Token));
|
||||
}
|
||||
|
||||
public void FlushPendingCreates()
|
||||
private async Task CreateEventProcessorLoop(CancellationToken token)
|
||||
{
|
||||
if (GameMain.MainThread != null && Thread.CurrentThread != GameMain.MainThread)
|
||||
while (!token.IsCancellationRequested)
|
||||
{
|
||||
throw new InvalidOperationException($"{nameof(ServerEntityEventManager)} pending events must be flushed on the main thread.");
|
||||
try
|
||||
{
|
||||
await eventSignal.WaitAsync(100, token);
|
||||
ProcessPendingCreateEvents();
|
||||
}
|
||||
catch (OperationCanceledException)
|
||||
{
|
||||
break;
|
||||
}
|
||||
}
|
||||
ProcessPendingCreateEvents();
|
||||
}
|
||||
|
||||
private void ProcessPendingCreateEvents()
|
||||
{
|
||||
// CreateEntityEvent can be called from parallel update code. The queue keeps
|
||||
// that enqueue path safe, while this method is only called from the main tick
|
||||
// before reading or writing entity-event state.
|
||||
// Dequeue and process all pending events currently in the queue.
|
||||
// Use a lock to synchronize modifications to shared lists / ID.
|
||||
while (pendingCreateQueue.TryDequeue(out PendingCreateEvent pending))
|
||||
{
|
||||
// The original CreateEvent logic (mostly unchanged) but executed under a lock
|
||||
if (pending == null || pending.Entity == null) { continue; }
|
||||
|
||||
var entity = pending.Entity;
|
||||
@@ -226,18 +216,34 @@ namespace Barotrauma.Networking
|
||||
{
|
||||
if (!ValidateEntity(entity)) { return; }
|
||||
|
||||
// enqueue and let background task handle the rest
|
||||
pendingCreateQueue.Enqueue(new PendingCreateEvent(entity, extraData));
|
||||
|
||||
if (eventSignal.CurrentCount == 0)
|
||||
{
|
||||
eventSignal.Release();
|
||||
}
|
||||
}
|
||||
|
||||
public void Dispose()
|
||||
{
|
||||
ClearPendingCreates();
|
||||
cancellationTokenSource.Cancel();
|
||||
eventSignal.Release();
|
||||
try
|
||||
{
|
||||
createEventTask?.Wait(2000);
|
||||
}
|
||||
catch (AggregateException) { }
|
||||
finally
|
||||
{
|
||||
cancellationTokenSource.Dispose();
|
||||
eventSignal.Dispose();
|
||||
}
|
||||
}
|
||||
|
||||
// Due to intensive access demend and time it takes to refactor, we use try-catch when facing thread-safety issue to skip to next update :(
|
||||
public void Update(List<Client> clients)
|
||||
{
|
||||
FlushPendingCreates();
|
||||
|
||||
foreach (BufferedEvent bufferedEvent in bufferedEvents)
|
||||
{
|
||||
if (bufferedEvent.Character == null || bufferedEvent.Character.IsDead)
|
||||
@@ -323,7 +329,14 @@ namespace Barotrauma.Networking
|
||||
}
|
||||
}
|
||||
|
||||
lastSentToAnyoneTime = events.Find(e => e.ID == lastSentToAnyone)?.CreateTime ?? Timing.TotalTime;
|
||||
try
|
||||
{
|
||||
lastSentToAnyoneTime = events.ToList().Find(e => e.ID == lastSentToAnyone)?.CreateTime ?? Timing.TotalTime;
|
||||
}
|
||||
catch
|
||||
{
|
||||
lastSentToAnyoneTime = Timing.TotalTime;
|
||||
}
|
||||
|
||||
|
||||
if (Timing.TotalTime - lastWarningTime > 5.0 &&
|
||||
@@ -340,7 +353,15 @@ namespace Barotrauma.Networking
|
||||
|
||||
clients.Where(c => c.NeedsMidRoundSync).ForEach(c => { if (NetIdUtils.IdMoreRecent(lastSentToAll, c.FirstNewEventID)) lastSentToAll = (ushort)(c.FirstNewEventID - 1); });
|
||||
|
||||
ServerEntityEvent firstEventToResend = events.Find(e => e.ID == (ushort)(lastSentToAll + 1));
|
||||
ServerEntityEvent firstEventToResend;
|
||||
try
|
||||
{
|
||||
firstEventToResend = events.Find(e => e.ID == (ushort)(lastSentToAll + 1));
|
||||
}
|
||||
catch
|
||||
{
|
||||
firstEventToResend = null;
|
||||
}
|
||||
|
||||
if (firstEventToResend != null &&
|
||||
GameMain.GameSession.RoundDuration > server.ServerSettings.RoundStartSyncDuration &&
|
||||
@@ -429,8 +450,6 @@ namespace Barotrauma.Networking
|
||||
/// </summary>
|
||||
public void Write(in SegmentTableWriter<ServerNetSegment> segmentTable, Client client, IWriteMessage msg, out List<NetEntityEvent> sentEvents)
|
||||
{
|
||||
FlushPendingCreates();
|
||||
|
||||
List<NetEntityEvent> eventsToSync = GetEventsToSync(client);
|
||||
|
||||
if (eventsToSync.Count == 0)
|
||||
@@ -557,8 +576,6 @@ namespace Barotrauma.Networking
|
||||
|
||||
public void InitClientMidRoundSync(Client client)
|
||||
{
|
||||
FlushPendingCreates();
|
||||
|
||||
//no need for midround syncing if no events have been created,
|
||||
//or if the first created unique event is still in the event list
|
||||
if (uniqueEvents.Count == 0 || (events.Count > 0 && events[0].ID == uniqueEvents[0].ID))
|
||||
@@ -676,8 +693,6 @@ namespace Barotrauma.Networking
|
||||
|
||||
public void Clear()
|
||||
{
|
||||
ClearPendingCreates();
|
||||
|
||||
ID = 0;
|
||||
events.Clear();
|
||||
|
||||
@@ -694,10 +709,5 @@ namespace Barotrauma.Networking
|
||||
c.LastSentEntityEventID = 0;
|
||||
}
|
||||
}
|
||||
|
||||
private void ClearPendingCreates()
|
||||
{
|
||||
while (pendingCreateQueue.TryDequeue(out _)) { }
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -279,7 +279,7 @@ namespace Barotrauma.Networking
|
||||
var shuttleGaps = Gap.GapList.FindAll(g => RespawnShuttles.Contains(g.Submarine) && g.ConnectedWall != null);
|
||||
shuttleGaps.ForEach(g => Spawner.AddEntityToRemoveQueue(g));
|
||||
|
||||
var dockingPorts = Item.ItemList.FindAll(i => RespawnShuttles.Contains(i.Submarine) && i.GetComponent<DockingPort>() != null);
|
||||
var dockingPorts = Item.ItemList.Where(i => RespawnShuttles.Contains(i.Submarine) && i.GetComponent<DockingPort>() != null).ToList();
|
||||
dockingPorts.ForEach(d => d.GetComponent<DockingPort>().Undock());
|
||||
|
||||
if (!IsShuttleInsideLevel || DateTime.Now > teamSpecificState.DespawnTime)
|
||||
|
||||
@@ -2,7 +2,11 @@
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.Diagnostics;
|
||||
using System.IO;
|
||||
using System.Linq;
|
||||
using System.Security.Cryptography;
|
||||
using System.Text;
|
||||
using System.Threading.Tasks;
|
||||
|
||||
|
||||
namespace Barotrauma
|
||||
@@ -35,27 +39,7 @@ namespace Barotrauma
|
||||
}
|
||||
public int ConnectClients
|
||||
{
|
||||
get { return GameMain.Server?.ConnectedClients.Count ?? 0; }
|
||||
}
|
||||
|
||||
public int PendingEntityEvents
|
||||
{
|
||||
get { return GameMain.Server?.EntityEventManager?.PendingCreateEventCount ?? 0; }
|
||||
}
|
||||
|
||||
public int EntityEvents
|
||||
{
|
||||
get { return GameMain.Server?.EntityEventManager?.EventCount ?? 0; }
|
||||
}
|
||||
|
||||
public int UniqueEntityEvents
|
||||
{
|
||||
get { return GameMain.Server?.EntityEventManager?.UniqueEventCount ?? 0; }
|
||||
}
|
||||
|
||||
public int BufferedEntityEvents
|
||||
{
|
||||
get { return GameMain.Server?.EntityEventManager?.BufferedEventCount ?? 0; }
|
||||
get { return GameMain.Server.ConnectedClients.Count; }
|
||||
}
|
||||
|
||||
public double RealTickRate
|
||||
@@ -182,10 +166,6 @@ namespace Barotrauma
|
||||
$"Character Count: {CharacterCount}\n" +
|
||||
$"Clients Count {ConnectClients}\n " +
|
||||
$"PhysicsBody Count: {PhysicsBodyCount}\n" +
|
||||
$"Entity Events: {EntityEvents}\n" +
|
||||
$"Unique Entity Events: {UniqueEntityEvents}\n" +
|
||||
$"Pending Entity Events: {PendingEntityEvents}\n" +
|
||||
$"Buffered Entity Events: {BufferedEntityEvents}\n" +
|
||||
$"Tick Rate: {RealTickRate}\n" +
|
||||
$"Min Tick Rate: {TickRateLow}\n" +
|
||||
$"Max Tick Rate: {TickRateHigh}\n" +
|
||||
|
||||
@@ -156,7 +156,8 @@ namespace Barotrauma
|
||||
Reactor reactor = item.GetComponent<Reactor>();
|
||||
if (reactor != null && reactor.Item.Condition > 0.0f) { roundData.Reactors.Add(reactor); }
|
||||
}
|
||||
pathFinder = new PathFinder(WayPoint.WayPointList, false);
|
||||
|
||||
pathFinder = new PathFinder(WayPoint.WayPointList.ToList(), false);
|
||||
cachedDistances.Clear();
|
||||
|
||||
#if CLIENT
|
||||
@@ -323,7 +324,7 @@ namespace Barotrauma
|
||||
|
||||
static CachedDistance CalculateNewCachedDistance(Character c)
|
||||
{
|
||||
pathFinder ??= new PathFinder(WayPoint.WayPointList, false);
|
||||
pathFinder ??= new PathFinder(WayPoint.WayPointList.ToList(), false);
|
||||
var path = pathFinder.FindPath(ConvertUnits.ToSimUnits(c.WorldPosition), ConvertUnits.ToSimUnits(Submarine.MainSub.WorldPosition));
|
||||
if (path.Unreachable) { return null; }
|
||||
return new CachedDistance(c.WorldPosition, Submarine.MainSub.WorldPosition, path.TotalLength, Timing.TotalTime + Rand.Range(1.0f, 5.0f));
|
||||
|
||||
@@ -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
|
||||
{
|
||||
@@ -381,8 +382,13 @@ namespace Barotrauma
|
||||
}
|
||||
steeringBuffer = Math.Clamp(steeringBuffer, minSteeringBuffer, maxSteeringBuffer);
|
||||
|
||||
AnimController.Crouching = shouldCrouch;
|
||||
CheckCrouching(deltaTime);
|
||||
// in case of somehow AnimController was a null, eg. something removed AnimController in the middle of an update
|
||||
if (AnimController != null)
|
||||
{
|
||||
AnimController.Crouching = shouldCrouch;
|
||||
CheckCrouching(deltaTime);
|
||||
}
|
||||
|
||||
Character.ClearInputs();
|
||||
|
||||
if (SortTimer > 0.0f)
|
||||
@@ -1637,7 +1643,8 @@ namespace Barotrauma
|
||||
if (mode == AIObjectiveCombat.CombatMode.None) { return; }
|
||||
if (Character.IsDead || Character.IsIncapacitated || Character.Removed) { return; }
|
||||
if (!Character.IsBot) { return; }
|
||||
if (ObjectiveManager.Objectives.FirstOrDefault(o => o is AIObjectiveCombat) is AIObjectiveCombat combatObjective)
|
||||
List<AIObjective> ObjectivesLocal = ObjectiveManager.Objectives;
|
||||
if (ObjectivesLocal.FirstOrDefault(o => o is AIObjectiveCombat) is AIObjectiveCombat combatObjective)
|
||||
{
|
||||
// Don't replace offensive mode with something else
|
||||
if (combatObjective.Mode == AIObjectiveCombat.CombatMode.Offensive && mode != AIObjectiveCombat.CombatMode.Offensive) { return; }
|
||||
@@ -1817,7 +1824,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 +1834,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);
|
||||
|
||||
+1
-1
@@ -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;
|
||||
}
|
||||
|
||||
+1
-1
@@ -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; }
|
||||
|
||||
+3
-1
@@ -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;
|
||||
|
||||
+1
-1
@@ -122,7 +122,7 @@ namespace Barotrauma
|
||||
}
|
||||
else
|
||||
{
|
||||
Objectives.RemoveAll(o => o.GetType() == type);
|
||||
Objectives.RemoveAll(o => o?.GetType() == type);
|
||||
}
|
||||
Objectives.Add(objective);
|
||||
}
|
||||
|
||||
@@ -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);
|
||||
|
||||
@@ -8,8 +8,10 @@ using FarseerPhysics.Dynamics.Joints;
|
||||
using Microsoft.Xna.Framework;
|
||||
using MoonSharp.Interpreter;
|
||||
using System;
|
||||
using System.Collections.Concurrent;
|
||||
using System.Collections.Generic;
|
||||
using System.Linq;
|
||||
using System.Threading;
|
||||
using System.Xml.Linq;
|
||||
using JointParams = Barotrauma.RagdollParams.JointParams;
|
||||
using LimbParams = Barotrauma.RagdollParams.LimbParams;
|
||||
@@ -26,7 +28,33 @@ namespace Barotrauma
|
||||
/// </summary>
|
||||
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
|
||||
{
|
||||
@@ -47,7 +75,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;
|
||||
|
||||
@@ -469,7 +498,7 @@ namespace Barotrauma
|
||||
|
||||
public Ragdoll(Character character, string seed, RagdollParams ragdollParams = null)
|
||||
{
|
||||
list.Add(this);
|
||||
ListAdd(this);
|
||||
this.character = character;
|
||||
Recreate(ragdollParams ?? RagdollParams);
|
||||
}
|
||||
@@ -746,10 +775,7 @@ namespace Barotrauma
|
||||
{
|
||||
if (!f2.IsSensor)
|
||||
{
|
||||
lock (impactQueue)
|
||||
{
|
||||
impactQueue.Enqueue(new Impact(f1, f2, contact, velocity));
|
||||
}
|
||||
impactQueue.Enqueue(new Impact(f1, f2, contact, velocity));
|
||||
}
|
||||
return true;
|
||||
}
|
||||
@@ -821,10 +847,7 @@ namespace Barotrauma
|
||||
}
|
||||
}
|
||||
|
||||
lock (impactQueue)
|
||||
{
|
||||
impactQueue.Enqueue(new Impact(f1, f2, contact, velocity));
|
||||
}
|
||||
impactQueue.Enqueue(new Impact(f1, f2, contact, velocity));
|
||||
|
||||
return true;
|
||||
}
|
||||
@@ -1291,10 +1314,9 @@ namespace Barotrauma
|
||||
{
|
||||
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.WorldNormal, impact.ImpactPos, impact.Velocity);
|
||||
ApplyImpact(impact.F1, impact.F2, impact.LocalNormal, impact.ImpactPos, impact.Velocity);
|
||||
}
|
||||
|
||||
CheckValidity();
|
||||
@@ -2368,7 +2390,7 @@ namespace Barotrauma
|
||||
LimbJoints = null;
|
||||
}
|
||||
|
||||
list.Remove(this);
|
||||
ListRemove(this);
|
||||
}
|
||||
|
||||
public static void RemoveAll()
|
||||
|
||||
@@ -7,10 +7,12 @@ using FarseerPhysics;
|
||||
using FarseerPhysics.Dynamics;
|
||||
using Microsoft.Xna.Framework;
|
||||
using System;
|
||||
using System.Collections.Concurrent;
|
||||
using System.Collections.Generic;
|
||||
using System.Collections.Immutable;
|
||||
using System.Diagnostics;
|
||||
using System.Linq;
|
||||
using System.Threading;
|
||||
using System.Xml.Linq;
|
||||
#if SERVER
|
||||
using System.Text;
|
||||
@@ -28,12 +30,70 @@ namespace Barotrauma
|
||||
|
||||
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
|
||||
{
|
||||
public static readonly List<Character> CharacterList = new List<Character>();
|
||||
public static readonly ThreadSafeCharacterList CharacterList = new ThreadSafeCharacterList();
|
||||
|
||||
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 MaxDragDistance = 200.0f;
|
||||
@@ -2793,10 +2853,11 @@ namespace Barotrauma
|
||||
}
|
||||
int itemsPerFrame = IsOnPlayerTeam ? 100 : 10;
|
||||
int checkedItemCount = 0;
|
||||
for (int i = 0; i < itemsPerFrame && itemIndex < Item.ItemList.Count; i++, itemIndex++)
|
||||
var cachedItems = Item.GetCachedItemList();
|
||||
for (int i = 0; i < itemsPerFrame && itemIndex < cachedItems.Count; i++, itemIndex++)
|
||||
{
|
||||
checkedItemCount++;
|
||||
var item = Item.ItemList[itemIndex];
|
||||
var item = cachedItems[itemIndex];
|
||||
if (!item.IsInteractable(this)) { continue; }
|
||||
if (ignoredItems != null && ignoredItems.Contains(item)) { continue; }
|
||||
if (item.Submarine == null) { continue; }
|
||||
@@ -2832,10 +2893,10 @@ namespace Barotrauma
|
||||
}
|
||||
}
|
||||
targetItem = _foundItem;
|
||||
bool completed = itemIndex >= Item.ItemList.Count - 1;
|
||||
bool completed = itemIndex >= cachedItems.Count - 1;
|
||||
if (HumanAIController.DebugAI && checkedItemCount > 0 && targetItem != null && StopWatch.ElapsedMilliseconds > 1)
|
||||
{
|
||||
var msg = $"Went through {checkedItemCount} of total {Item.ItemList.Count} items. Found item {targetItem.Name} in {StopWatch.ElapsedMilliseconds} ms. Completed: {completed}";
|
||||
var msg = $"Went through {checkedItemCount} of total {cachedItems.Count} items. Found item {targetItem.Name} in {StopWatch.ElapsedMilliseconds} ms. Completed: {completed}";
|
||||
if (StopWatch.ElapsedMilliseconds > 5)
|
||||
{
|
||||
DebugConsole.ThrowError(msg);
|
||||
@@ -4910,7 +4971,11 @@ namespace Barotrauma
|
||||
HealthUpdateInterval = 0.0f;
|
||||
}
|
||||
|
||||
private readonly List<ISerializableEntity> targets = new List<ISerializableEntity>();
|
||||
// Thread-static to avoid concurrent modification in parallel item updates
|
||||
[ThreadStatic]
|
||||
private static List<ISerializableEntity> t_statusEffectTargets;
|
||||
private static List<ISerializableEntity> StatusEffectTargets => t_statusEffectTargets ??= new List<ISerializableEntity>();
|
||||
|
||||
public void ApplyStatusEffects(ActionType actionType, float deltaTime)
|
||||
{
|
||||
if (actionType == ActionType.OnEating)
|
||||
@@ -4939,6 +5004,7 @@ namespace Barotrauma
|
||||
if (statusEffect.HasTargetType(StatusEffect.TargetType.NearbyItems) ||
|
||||
statusEffect.HasTargetType(StatusEffect.TargetType.NearbyCharacters))
|
||||
{
|
||||
var targets = StatusEffectTargets;
|
||||
targets.Clear();
|
||||
statusEffect.AddNearbyTargets(WorldPosition, targets);
|
||||
statusEffect.Apply(actionType, deltaTime, this, targets);
|
||||
|
||||
@@ -8,12 +8,14 @@ using Barotrauma.Networking;
|
||||
using Microsoft.Xna.Framework;
|
||||
using MoonSharp.Interpreter;
|
||||
using System;
|
||||
using System.Collections.Concurrent;
|
||||
using System.Collections.Generic;
|
||||
using System.Globalization;
|
||||
using System.Globalization;
|
||||
using System.Linq;
|
||||
using System.Threading;
|
||||
using System.Xml.Linq;
|
||||
using static OneOf.Types.TrueFalseOrNull;
|
||||
using MoonSharp.Interpreter;
|
||||
|
||||
namespace Barotrauma
|
||||
{
|
||||
@@ -134,8 +136,9 @@ namespace Barotrauma
|
||||
|
||||
private readonly List<LimbHealth> limbHealths = new List<LimbHealth>();
|
||||
|
||||
private readonly Dictionary<Affliction, LimbHealth> afflictions = new Dictionary<Affliction, LimbHealth>();
|
||||
private readonly HashSet<Affliction> irremovableAfflictions = new HashSet<Affliction>();
|
||||
// Thread-safe afflictions dictionary for concurrent access
|
||||
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 oxygenLowAffliction;
|
||||
private Affliction pressureAffliction;
|
||||
@@ -326,13 +329,13 @@ namespace Barotrauma
|
||||
|
||||
private void InitIrremovableAfflictions()
|
||||
{
|
||||
irremovableAfflictions.Add(bloodlossAffliction = new Affliction(AfflictionPrefab.Bloodloss, 0.0f));
|
||||
irremovableAfflictions.Add(stunAffliction = new Affliction(AfflictionPrefab.Stun, 0.0f));
|
||||
irremovableAfflictions.Add(pressureAffliction = new Affliction(AfflictionPrefab.Pressure, 0.0f));
|
||||
irremovableAfflictions.Add(oxygenLowAffliction = new Affliction(AfflictionPrefab.OxygenLow, 0.0f));
|
||||
foreach (Affliction affliction in irremovableAfflictions)
|
||||
irremovableAfflictions.TryAdd(bloodlossAffliction = new Affliction(AfflictionPrefab.Bloodloss, 0.0f), 0);
|
||||
irremovableAfflictions.TryAdd(stunAffliction = new Affliction(AfflictionPrefab.Stun, 0.0f), 0);
|
||||
irremovableAfflictions.TryAdd(pressureAffliction = new Affliction(AfflictionPrefab.Pressure, 0.0f), 0);
|
||||
irremovableAfflictions.TryAdd(oxygenLowAffliction = new Affliction(AfflictionPrefab.OxygenLow, 0.0f), 0);
|
||||
foreach (Affliction affliction in irremovableAfflictions.Keys)
|
||||
{
|
||||
afflictions.Add(affliction, null);
|
||||
afflictions.TryAdd(affliction, null);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -340,7 +343,7 @@ namespace Barotrauma
|
||||
|
||||
public IReadOnlyCollection<Affliction> GetAllAfflictions()
|
||||
{
|
||||
return afflictions.Keys;
|
||||
return afflictions.Keys.ToList();
|
||||
}
|
||||
|
||||
public IEnumerable<Affliction> GetAllAfflictions(Func<Affliction, bool> limbHealthFilter)
|
||||
@@ -505,19 +508,18 @@ namespace Barotrauma
|
||||
/// </summary>
|
||||
public float GetResistance(AfflictionPrefab afflictionPrefab, LimbType limbType)
|
||||
{
|
||||
lock (afflictions) {
|
||||
// This is a % resistance (0 to 1.0)
|
||||
float resistance = 0.0f;
|
||||
foreach (KeyValuePair<Affliction, LimbHealth> kvp in afflictions)
|
||||
{
|
||||
var affliction = kvp.Key;
|
||||
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);
|
||||
// ConcurrentDictionary is thread-safe, no lock needed
|
||||
// This is a % resistance (0 to 1.0)
|
||||
float resistance = 0.0f;
|
||||
foreach (KeyValuePair<Affliction, LimbHealth> kvp in afflictions)
|
||||
{
|
||||
var affliction = kvp.Key;
|
||||
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);
|
||||
}
|
||||
|
||||
public float GetStatValue(StatTypes statType)
|
||||
@@ -541,20 +543,25 @@ namespace Barotrauma
|
||||
return false;
|
||||
}
|
||||
|
||||
private readonly List<Affliction> matchingAfflictions = new List<Affliction>();
|
||||
// Thread-static to avoid concurrent modification in parallel item updates
|
||||
[ThreadStatic]
|
||||
private static List<Affliction> t_matchingAfflictions;
|
||||
private static List<Affliction> MatchingAfflictions => t_matchingAfflictions ??= new List<Affliction>();
|
||||
|
||||
public void ReduceAllAfflictionsOnAllLimbs(float amount, ActionType? treatmentAction = null)
|
||||
{
|
||||
var matchingAfflictions = MatchingAfflictions;
|
||||
matchingAfflictions.Clear();
|
||||
matchingAfflictions.AddRange(afflictions.Keys);
|
||||
|
||||
ReduceMatchingAfflictions(amount, treatmentAction);
|
||||
ReduceMatchingAfflictions(matchingAfflictions, amount, treatmentAction);
|
||||
}
|
||||
|
||||
public void ReduceAfflictionOnAllLimbs(Identifier afflictionIdOrType, float amount, ActionType? treatmentAction = null, Character attacker = null)
|
||||
{
|
||||
if (afflictionIdOrType.IsEmpty) { throw new ArgumentException($"{nameof(afflictionIdOrType)} is empty"); }
|
||||
|
||||
var matchingAfflictions = MatchingAfflictions;
|
||||
matchingAfflictions.Clear();
|
||||
foreach (var affliction in afflictions)
|
||||
{
|
||||
@@ -564,7 +571,7 @@ namespace Barotrauma
|
||||
}
|
||||
}
|
||||
|
||||
ReduceMatchingAfflictions(amount, treatmentAction, attacker);
|
||||
ReduceMatchingAfflictions(matchingAfflictions, amount, treatmentAction, attacker);
|
||||
}
|
||||
|
||||
private IEnumerable<Affliction> GetAfflictionsForLimb(Limb targetLimb)
|
||||
@@ -574,10 +581,11 @@ namespace Barotrauma
|
||||
{
|
||||
if (targetLimb is null) { throw new ArgumentNullException(nameof(targetLimb)); }
|
||||
|
||||
var matchingAfflictions = MatchingAfflictions;
|
||||
matchingAfflictions.Clear();
|
||||
matchingAfflictions.AddRange(GetAfflictionsForLimb(targetLimb));
|
||||
|
||||
ReduceMatchingAfflictions(amount, treatmentAction);
|
||||
ReduceMatchingAfflictions(matchingAfflictions, amount, treatmentAction);
|
||||
}
|
||||
|
||||
public void ReduceAfflictionOnLimb(Limb targetLimb, Identifier afflictionIdOrType, float amount, ActionType? treatmentAction = null, Character attacker = null)
|
||||
@@ -585,6 +593,7 @@ namespace Barotrauma
|
||||
if (afflictionIdOrType.IsEmpty) { throw new ArgumentException($"{nameof(afflictionIdOrType)} is empty"); }
|
||||
if (targetLimb is null) { throw new ArgumentNullException(nameof(targetLimb)); }
|
||||
|
||||
var matchingAfflictions = MatchingAfflictions;
|
||||
matchingAfflictions.Clear();
|
||||
var targetLimbHealth = limbHealths[targetLimb.HealthIndex];
|
||||
foreach (var affliction in afflictions)
|
||||
@@ -595,10 +604,10 @@ namespace Barotrauma
|
||||
matchingAfflictions.Add(affliction.Key);
|
||||
}
|
||||
}
|
||||
ReduceMatchingAfflictions(amount, treatmentAction, attacker);
|
||||
ReduceMatchingAfflictions(matchingAfflictions, amount, treatmentAction, attacker);
|
||||
}
|
||||
|
||||
private void ReduceMatchingAfflictions(float amount, ActionType? treatmentAction, Character attacker = null)
|
||||
private void ReduceMatchingAfflictions(List<Affliction> matchingAfflictions, float amount, ActionType? treatmentAction, Character attacker = null)
|
||||
{
|
||||
if (matchingAfflictions.Count == 0) { return; }
|
||||
|
||||
@@ -686,12 +695,19 @@ namespace Barotrauma
|
||||
}
|
||||
}
|
||||
|
||||
private readonly static List<Affliction> afflictionsToRemove = new List<Affliction>();
|
||||
private readonly static List<KeyValuePair<Affliction, LimbHealth>> afflictionsToUpdate = new List<KeyValuePair<Affliction, LimbHealth>>();
|
||||
// Thread-static to avoid concurrent modification when multiple characters are updated in parallel
|
||||
[ThreadStatic]
|
||||
private static List<Affliction> t_afflictionsToRemove;
|
||||
[ThreadStatic]
|
||||
private static List<KeyValuePair<Affliction, LimbHealth>> t_afflictionsToUpdate;
|
||||
private static List<Affliction> AfflictionsToRemove => t_afflictionsToRemove ??= new List<Affliction>();
|
||||
private static List<KeyValuePair<Affliction, LimbHealth>> AfflictionsToUpdate => t_afflictionsToUpdate ??= new List<KeyValuePair<Affliction, LimbHealth>>();
|
||||
|
||||
public void SetAllDamage(float damageAmount, float bleedingDamageAmount, float burnDamageAmount)
|
||||
{
|
||||
if (Unkillable || Character.GodMode) { return; }
|
||||
|
||||
var afflictionsToRemove = AfflictionsToRemove;
|
||||
afflictionsToRemove.Clear();
|
||||
afflictionsToRemove.AddRange(afflictions.Keys.Where(a =>
|
||||
a.Prefab.AfflictionType == AfflictionPrefab.InternalDamage.AfflictionType ||
|
||||
@@ -699,14 +715,14 @@ namespace Barotrauma
|
||||
a.Prefab.AfflictionType == AfflictionPrefab.Bleeding.AfflictionType));
|
||||
foreach (var affliction in afflictionsToRemove)
|
||||
{
|
||||
afflictions.Remove(affliction);
|
||||
afflictions.TryRemove(affliction, out _);
|
||||
}
|
||||
|
||||
foreach (LimbHealth limbHealth in limbHealths)
|
||||
{
|
||||
if (damageAmount > 0.0f) { afflictions.Add(AfflictionPrefab.InternalDamage.Instantiate(damageAmount), limbHealth); }
|
||||
if (bleedingDamageAmount > 0.0f && DoesBleed) { afflictions.Add(AfflictionPrefab.Bleeding.Instantiate(bleedingDamageAmount), limbHealth); }
|
||||
if (burnDamageAmount > 0.0f) { afflictions.Add(AfflictionPrefab.Burn.Instantiate(burnDamageAmount), limbHealth); }
|
||||
if (damageAmount > 0.0f) { afflictions.TryAdd(AfflictionPrefab.InternalDamage.Instantiate(damageAmount), limbHealth); }
|
||||
if (bleedingDamageAmount > 0.0f && DoesBleed) { afflictions.TryAdd(AfflictionPrefab.Bleeding.Instantiate(bleedingDamageAmount), limbHealth); }
|
||||
if (burnDamageAmount > 0.0f) { afflictions.TryAdd(AfflictionPrefab.Burn.Instantiate(burnDamageAmount), limbHealth); }
|
||||
}
|
||||
|
||||
RecalculateVitality();
|
||||
@@ -742,26 +758,28 @@ namespace Barotrauma
|
||||
|
||||
public void RemoveAfflictions(Func<Affliction, bool> predicate)
|
||||
{
|
||||
var afflictionsToRemove = AfflictionsToRemove;
|
||||
afflictionsToRemove.Clear();
|
||||
afflictionsToRemove.AddRange(afflictions.Keys.Where(affliction => predicate(affliction)));
|
||||
foreach (var affliction in afflictionsToRemove)
|
||||
{
|
||||
afflictions.Remove(affliction);
|
||||
afflictions.TryRemove(affliction, out _);
|
||||
}
|
||||
CalculateVitality();
|
||||
}
|
||||
|
||||
public void RemoveAllAfflictions()
|
||||
{
|
||||
var afflictionsToRemove = AfflictionsToRemove;
|
||||
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)
|
||||
{
|
||||
//set strength to 0 in case the affliction needs to react to becoming inactive
|
||||
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;
|
||||
}
|
||||
@@ -770,17 +788,18 @@ namespace Barotrauma
|
||||
|
||||
public void RemoveNegativeAfflictions()
|
||||
{
|
||||
var afflictionsToRemove = AfflictionsToRemove;
|
||||
afflictionsToRemove.Clear();
|
||||
afflictionsToRemove.AddRange(afflictions.Keys.Where(a =>
|
||||
!irremovableAfflictions.Contains(a) &&
|
||||
!irremovableAfflictions.ContainsKey(a) &&
|
||||
!a.Prefab.IsBuff &&
|
||||
a.Prefab.AfflictionType != "geneticmaterialbuff" &&
|
||||
a.Prefab.AfflictionType != "geneticmaterialdebuff"));
|
||||
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;
|
||||
}
|
||||
@@ -883,7 +902,7 @@ namespace Barotrauma
|
||||
var copyAffliction = newAffliction.Prefab.Instantiate(
|
||||
Math.Min(newAffliction.Prefab.MaxStrength, modifiedStrength),
|
||||
newAffliction.Source);
|
||||
afflictions.Add(copyAffliction, limbHealth);
|
||||
afflictions.TryAdd(copyAffliction, limbHealth);
|
||||
AchievementManager.OnAfflictionReceived(copyAffliction, Character);
|
||||
MedicalClinic.OnAfflictionCountChanged(Character);
|
||||
|
||||
@@ -920,6 +939,8 @@ namespace Barotrauma
|
||||
|
||||
if (!Character.GodMode)
|
||||
{
|
||||
var afflictionsToRemove = AfflictionsToRemove;
|
||||
var afflictionsToUpdate = AfflictionsToUpdate;
|
||||
afflictionsToRemove.Clear();
|
||||
afflictionsToUpdate.Clear();
|
||||
foreach (KeyValuePair<Affliction, LimbHealth> kvp in afflictions)
|
||||
@@ -928,7 +949,7 @@ namespace Barotrauma
|
||||
if (affliction.Strength <= 0.0f)
|
||||
{
|
||||
AchievementManager.OnAfflictionRemoved(affliction, Character);
|
||||
if (!irremovableAfflictions.Contains(affliction)) { afflictionsToRemove.Add(affliction); }
|
||||
if (!irremovableAfflictions.ContainsKey(affliction)) { afflictionsToRemove.Add(affliction); }
|
||||
continue;
|
||||
}
|
||||
if (affliction.Prefab.Duration > 0.0f)
|
||||
@@ -966,7 +987,7 @@ namespace Barotrauma
|
||||
|
||||
foreach (var affliction in afflictionsToRemove)
|
||||
{
|
||||
afflictions.Remove(affliction);
|
||||
afflictions.TryRemove(affliction, out _);
|
||||
}
|
||||
|
||||
if (afflictionsToRemove.Count is not 0)
|
||||
@@ -1214,9 +1235,14 @@ namespace Barotrauma
|
||||
return (causeOfDeath, strongestAffliction);
|
||||
}
|
||||
|
||||
private readonly List<Affliction> allAfflictions = new List<Affliction>();
|
||||
// Thread-static to avoid concurrent modification in parallel item updates
|
||||
[ThreadStatic]
|
||||
private static List<Affliction> t_allAfflictions;
|
||||
private static List<Affliction> AllAfflictionsList => t_allAfflictions ??= new List<Affliction>();
|
||||
|
||||
private IEnumerable<Affliction> GetAllAfflictions(bool mergeSameAfflictions, Func<Affliction, bool> predicate = null)
|
||||
{
|
||||
var allAfflictions = AllAfflictionsList;
|
||||
allAfflictions.Clear();
|
||||
if (!mergeSameAfflictions)
|
||||
{
|
||||
@@ -1399,10 +1425,17 @@ namespace Barotrauma
|
||||
return MathHelper.Clamp(strength, 0.0f, affliction.Prefab.MaxStrength);
|
||||
}
|
||||
|
||||
private readonly List<Affliction> activeAfflictions = new List<Affliction>();
|
||||
private readonly List<(LimbHealth limbHealth, Affliction affliction)> limbAfflictions = new List<(LimbHealth limbHealth, Affliction affliction)>();
|
||||
// Thread-static to avoid concurrent modification in parallel updates
|
||||
[ThreadStatic]
|
||||
private static List<Affliction> t_activeAfflictions;
|
||||
[ThreadStatic]
|
||||
private static List<(LimbHealth limbHealth, Affliction affliction)> t_limbAfflictions;
|
||||
private static List<Affliction> ActiveAfflictionsList => t_activeAfflictions ??= new List<Affliction>();
|
||||
private static List<(LimbHealth limbHealth, Affliction affliction)> LimbAfflictionsList => t_limbAfflictions ??= new List<(LimbHealth limbHealth, Affliction affliction)>();
|
||||
|
||||
public void ServerWrite(IWriteMessage msg)
|
||||
{
|
||||
var activeAfflictions = ActiveAfflictionsList;
|
||||
activeAfflictions.Clear();
|
||||
foreach (KeyValuePair<Affliction, LimbHealth> kvp in afflictions)
|
||||
{
|
||||
@@ -1428,6 +1461,7 @@ namespace Barotrauma
|
||||
}
|
||||
}
|
||||
|
||||
var limbAfflictions = LimbAfflictionsList;
|
||||
limbAfflictions.Clear();
|
||||
foreach (KeyValuePair<Affliction, LimbHealth> kvp in afflictions)
|
||||
{
|
||||
@@ -1457,8 +1491,9 @@ namespace Barotrauma
|
||||
public void Remove()
|
||||
{
|
||||
RemoveProjSpecific();
|
||||
afflictionsToRemove.Clear();
|
||||
afflictionsToUpdate.Clear();
|
||||
// Clear thread-static lists to help with garbage collection
|
||||
AfflictionsToRemove.Clear();
|
||||
AfflictionsToUpdate.Clear();
|
||||
}
|
||||
|
||||
partial void RemoveProjSpecific();
|
||||
@@ -1533,14 +1568,14 @@ namespace Barotrauma
|
||||
}
|
||||
if (afflictionPredicate != null && !afflictionPredicate.Invoke(afflictionPrefab)) { return; }
|
||||
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)
|
||||
{
|
||||
irremovableAffliction.Strength = strength;
|
||||
}
|
||||
else
|
||||
{
|
||||
afflictions.Add(afflictionPrefab.Instantiate(strength), limbHealth);
|
||||
afflictions.TryAdd(afflictionPrefab.Instantiate(strength), limbHealth);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -797,16 +797,14 @@ namespace Barotrauma
|
||||
return AddDamage(simPosition, afflictions, playSound);
|
||||
}
|
||||
|
||||
private readonly List<DamageModifier> appliedDamageModifiers = new List<DamageModifier>();
|
||||
private readonly List<DamageModifier> tempModifiers = new List<DamageModifier>();
|
||||
private readonly List<Affliction> afflictionsCopy = new List<Affliction>();
|
||||
// Thread-safe: using local variables instead of instance fields to avoid concurrent modification
|
||||
public AttackResult AddDamage(Vector2 simPosition, IEnumerable<Affliction> afflictions, bool playSound, float damageMultiplier = 1, float penetration = 0f, Character attacker = null)
|
||||
{
|
||||
appliedDamageModifiers.Clear();
|
||||
afflictionsCopy.Clear();
|
||||
var appliedDamageModifiers = new List<DamageModifier>();
|
||||
var afflictionsCopy = new List<Affliction>();
|
||||
foreach (var affliction in afflictions)
|
||||
{
|
||||
tempModifiers.Clear();
|
||||
var tempModifiers = new List<DamageModifier>();
|
||||
var newAffliction = affliction;
|
||||
float random = Rand.Value(Rand.RandSync.Unsynced);
|
||||
bool foundMatchingModifier = false;
|
||||
@@ -1022,13 +1020,18 @@ namespace Barotrauma
|
||||
|
||||
partial void UpdateProjSpecific(float deltaTime);
|
||||
|
||||
private readonly List<Body> contactBodies = new List<Body>();
|
||||
// Thread-static to avoid concurrent modification in parallel item updates
|
||||
[ThreadStatic]
|
||||
private static List<Body> t_contactBodies;
|
||||
private static List<Body> ContactBodies => t_contactBodies ??= new List<Body>();
|
||||
|
||||
/// <summary>
|
||||
/// Returns true if the attack successfully hit something. If the distance is not given, it will be calculated.
|
||||
/// </summary>
|
||||
public bool UpdateAttack(float deltaTime, Vector2 attackSimPos, IDamageable damageTarget, out AttackResult attackResult, float distance = -1, Limb targetLimb = null)
|
||||
{
|
||||
attackResult = default;
|
||||
var contactBodies = ContactBodies;
|
||||
Vector2 simPos = ragdoll.SimplePhysicsEnabled ? character.SimPosition : SimPosition;
|
||||
float dist = distance > -1 ? distance : ConvertUnits.ToDisplayUnits(Vector2.Distance(simPos, attackSimPos));
|
||||
bool wasRunning = attack.IsRunning;
|
||||
@@ -1287,7 +1290,11 @@ namespace Barotrauma
|
||||
}
|
||||
}
|
||||
|
||||
private readonly List<ISerializableEntity> targets = new List<ISerializableEntity>();
|
||||
// Thread-static to avoid concurrent modification in parallel item updates
|
||||
[ThreadStatic]
|
||||
private static List<ISerializableEntity> t_statusEffectTargets;
|
||||
private static List<ISerializableEntity> StatusEffectTargets => t_statusEffectTargets ??= new List<ISerializableEntity>();
|
||||
|
||||
public void ApplyStatusEffects(ActionType actionType, float deltaTime)
|
||||
{
|
||||
if (!statusEffects.TryGetValue(actionType, out var statusEffectList)) { return; }
|
||||
@@ -1310,6 +1317,7 @@ namespace Barotrauma
|
||||
if (statusEffect.HasTargetType(StatusEffect.TargetType.NearbyItems) ||
|
||||
statusEffect.HasTargetType(StatusEffect.TargetType.NearbyCharacters))
|
||||
{
|
||||
var targets = StatusEffectTargets;
|
||||
targets.Clear();
|
||||
statusEffect.AddNearbyTargets(WorldPosition, targets);
|
||||
statusEffect.Apply(actionType, deltaTime, character, targets);
|
||||
|
||||
+14
-20
@@ -1,10 +1,12 @@
|
||||
using Microsoft.Xna.Framework;
|
||||
using System.Collections.Concurrent;
|
||||
using System.Collections.Generic;
|
||||
using System.Globalization;
|
||||
using Barotrauma.IO;
|
||||
using System;
|
||||
using System.Diagnostics;
|
||||
using System.Linq;
|
||||
using System.Threading;
|
||||
using System.Xml.Linq;
|
||||
using Barotrauma.Extensions;
|
||||
|
||||
@@ -117,8 +119,9 @@ namespace Barotrauma
|
||||
public virtual AnimationType AnimationType { get; protected set; }
|
||||
/// <summary>
|
||||
/// The cached animations of all the characters that have been loaded.
|
||||
/// Thread-safe cache using ConcurrentDictionary.
|
||||
/// </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")]
|
||||
[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);
|
||||
}
|
||||
|
||||
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()
|
||||
{
|
||||
@@ -262,11 +267,7 @@ namespace Barotrauma
|
||||
}
|
||||
ContentPackage contentPackage = contentPath?.ContentPackage ?? CharacterPrefab.FindBySpeciesName(speciesName)?.ContentPackage;
|
||||
Debug.Assert(contentPackage != null);
|
||||
if (!allAnimations.TryGetValue(speciesName, out Dictionary<string, AnimationParams> animations))
|
||||
{
|
||||
animations = new Dictionary<string, AnimationParams>();
|
||||
allAnimations.Add(speciesName, animations);
|
||||
}
|
||||
var animations = allAnimations.GetOrAdd(speciesName, _ => new ConcurrentDictionary<string, AnimationParams>());
|
||||
string key = fileName ?? contentPath?.Value ?? GetDefaultFileName(animSpecies, 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);
|
||||
}
|
||||
if (!allAnimations.TryGetValue(speciesName, out Dictionary<string, AnimationParams> anims))
|
||||
{
|
||||
anims = new Dictionary<string, AnimationParams>();
|
||||
allAnimations.Add(speciesName, anims);
|
||||
}
|
||||
var anims = allAnimations.GetOrAdd(speciesName, _ => new ConcurrentDictionary<string, AnimationParams>());
|
||||
string fileName = IO.Path.GetFileNameWithoutExtension(fullPath);
|
||||
if (anims.ContainsKey(fileName))
|
||||
{
|
||||
DebugConsole.NewMessage($"[AnimationParams] Removing the old animation of type {animationType}.", Color.Red);
|
||||
anims.Remove(fileName);
|
||||
anims.TryRemove(fileName, out _);
|
||||
}
|
||||
var instance = new T();
|
||||
XElement animationElement = new XElement(GetDefaultFileName(speciesName, animationType), new XAttribute("animationtype", animationType.ToString()));
|
||||
@@ -439,7 +436,7 @@ namespace Barotrauma
|
||||
instance.IsLoaded = instance.Deserialize(animationElement);
|
||||
instance.Save();
|
||||
instance.Load(contentPath, speciesName);
|
||||
anims.Add(fileName, instance);
|
||||
anims.TryAdd(fileName, instance);
|
||||
DebugConsole.NewMessage($"[AnimationParams] New animation file of type {animationType} created.", Color.GhostWhite);
|
||||
return instance;
|
||||
}
|
||||
@@ -467,17 +464,14 @@ namespace Barotrauma
|
||||
{
|
||||
// Update the key by removing and re-adding the animation.
|
||||
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);
|
||||
if (animations != null)
|
||||
{
|
||||
if (!animations.ContainsKey(fileName))
|
||||
{
|
||||
animations.Add(fileName, this);
|
||||
}
|
||||
animations.TryAdd(fileName, this);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
+12
-17
@@ -1,5 +1,6 @@
|
||||
using Microsoft.Xna.Framework;
|
||||
using System;
|
||||
using System.Collections.Concurrent;
|
||||
using System.Collections.Generic;
|
||||
using System.Diagnostics;
|
||||
using System.Xml.Linq;
|
||||
@@ -124,8 +125,9 @@ namespace Barotrauma
|
||||
/// key1: Species name
|
||||
/// key2: File path
|
||||
/// value: Ragdoll parameters
|
||||
/// Thread-safe cache using ConcurrentDictionary.
|
||||
/// </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<LimbParams> Limbs { get; private set; } = new List<LimbParams>();
|
||||
@@ -222,11 +224,7 @@ namespace Barotrauma
|
||||
Debug.Assert(!fileName.IsNullOrWhiteSpace() || !contentPath.IsNullOrWhiteSpace());
|
||||
}
|
||||
Debug.Assert(contentPackage != null);
|
||||
if (!allRagdolls.TryGetValue(speciesName, out Dictionary<string, RagdollParams> ragdolls))
|
||||
{
|
||||
ragdolls = new Dictionary<string, RagdollParams>();
|
||||
allRagdolls.Add(speciesName, ragdolls);
|
||||
}
|
||||
var ragdolls = allRagdolls.GetOrAdd(speciesName, _ => new ConcurrentDictionary<string, RagdollParams>());
|
||||
string key = fileName ?? contentPath?.Value ?? GetDefaultFileName(ragdollSpecies);
|
||||
if (ragdolls.TryGetValue(key, out RagdollParams ragdoll))
|
||||
{
|
||||
@@ -331,10 +329,10 @@ namespace Barotrauma
|
||||
if (allRagdolls.ContainsKey(speciesName))
|
||||
{
|
||||
DebugConsole.NewMessage($"[RagdollParams] Removing the old ragdolls from {speciesName}.", Color.Red);
|
||||
allRagdolls.Remove(speciesName);
|
||||
allRagdolls.TryRemove(speciesName, out _);
|
||||
}
|
||||
var ragdolls = new Dictionary<string, RagdollParams>();
|
||||
allRagdolls.Add(speciesName, ragdolls);
|
||||
var ragdolls = new ConcurrentDictionary<string, RagdollParams>();
|
||||
allRagdolls.TryAdd(speciesName, ragdolls);
|
||||
var instance = new T
|
||||
{
|
||||
doc = new XDocument(mainElement)
|
||||
@@ -345,7 +343,7 @@ namespace Barotrauma
|
||||
instance.IsLoaded = instance.Deserialize(mainElement);
|
||||
instance.Save();
|
||||
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);
|
||||
return instance;
|
||||
}
|
||||
@@ -362,17 +360,14 @@ namespace Barotrauma
|
||||
{
|
||||
// Update the key by removing and re-adding the ragdoll.
|
||||
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);
|
||||
if (ragdolls != null)
|
||||
{
|
||||
if (!ragdolls.ContainsKey(fileName))
|
||||
{
|
||||
ragdolls.Add(fileName, this);
|
||||
}
|
||||
ragdolls.TryAdd(fileName, this);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -1484,4 +1479,4 @@ namespace Barotrauma
|
||||
}
|
||||
#endregion
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,6 +1,8 @@
|
||||
using Barotrauma.Abilities;
|
||||
using System;
|
||||
using System.Collections.Concurrent;
|
||||
using System.Collections.Generic;
|
||||
using System.Threading;
|
||||
|
||||
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>
|
||||
/// Checks talents for a given AbilityObject taking into account non-stackable talents.
|
||||
|
||||
+3
-3
@@ -1,4 +1,4 @@
|
||||
using System.Xml.Linq;
|
||||
using System.Xml.Linq;
|
||||
|
||||
namespace Barotrauma
|
||||
{
|
||||
@@ -21,7 +21,7 @@ namespace Barotrauma
|
||||
var npcConversationCollection = new NPCConversationCollection(this, mainElement);
|
||||
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);
|
||||
}
|
||||
@@ -42,4 +42,4 @@ namespace Barotrauma
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1478,7 +1478,7 @@ namespace Barotrauma
|
||||
newItemName = args[2];
|
||||
}
|
||||
|
||||
var oldItem = Item.ItemList.FindAll(it => it.Name == args[0]).ElementAtOrDefault(itemIndex);
|
||||
var oldItem = Item.ItemList.Where(it => it.Name == args[0]).ElementAtOrDefault(itemIndex);
|
||||
if (oldItem == null)
|
||||
{
|
||||
ThrowError($"Could not find an item with the name {args[0]} (index {itemIndex}).");
|
||||
@@ -1852,7 +1852,7 @@ namespace Barotrauma
|
||||
|
||||
commands.Add(new Command("power", "power: Immediately powers up the submarine's nuclear reactor.", (string[] args) =>
|
||||
{
|
||||
Item reactorItem = Item.ItemList.Find(i => i.GetComponent<Reactor>() != null);
|
||||
Item reactorItem = Item.ItemList.FirstOrDefault(i => i.GetComponent<Reactor>() != null);
|
||||
if (reactorItem == null) { return; }
|
||||
|
||||
var reactor = reactorItem.GetComponent<Reactor>();
|
||||
@@ -3230,7 +3230,7 @@ namespace Barotrauma
|
||||
if (args.Length > spawnLocationIndex + 1)
|
||||
{
|
||||
if (!int.TryParse(args[spawnLocationIndex + 1], NumberStyles.Any, CultureInfo.InvariantCulture, out amount)) { amount = 1; }
|
||||
amount = Math.Min(amount, 100);
|
||||
amount = Math.Min(amount, 100000);
|
||||
}
|
||||
|
||||
if (args.Length > spawnLocationIndex + 2)
|
||||
|
||||
@@ -1,7 +1,8 @@
|
||||
using Barotrauma.Networking;
|
||||
using Microsoft.Xna.Framework;
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.Collections.Immutable;
|
||||
using System.Threading;
|
||||
|
||||
namespace Barotrauma
|
||||
{
|
||||
@@ -10,11 +11,22 @@ namespace Barotrauma
|
||||
/// </summary>
|
||||
class UnlockPathAction : EventAction
|
||||
{
|
||||
private static readonly HashSet<LocationConnection> pathsUnlockedThisRound = new HashSet<LocationConnection>();
|
||||
private static volatile ImmutableHashSet<LocationConnection> _pathsUnlockedThisRound =
|
||||
ImmutableHashSet<LocationConnection>.Empty;
|
||||
|
||||
public static void ResetPathsUnlockedThisRound()
|
||||
{
|
||||
pathsUnlockedThisRound.Clear();
|
||||
_pathsUnlockedThisRound = ImmutableHashSet<LocationConnection>.Empty;
|
||||
}
|
||||
|
||||
private static void AddUnlockedPath(LocationConnection connection)
|
||||
{
|
||||
ImmutableHashSet<LocationConnection> original, updated;
|
||||
do
|
||||
{
|
||||
original = _pathsUnlockedThisRound;
|
||||
updated = original.Add(connection);
|
||||
} while (Interlocked.CompareExchange(ref _pathsUnlockedThisRound, updated, original) != original);
|
||||
}
|
||||
|
||||
public UnlockPathAction(ScriptedEvent parentEvent, ContentXElement element) : base(parentEvent, element) { }
|
||||
@@ -40,7 +52,7 @@ namespace Barotrauma
|
||||
{
|
||||
if (!connection.Locked) { continue; }
|
||||
connection.Locked = false;
|
||||
pathsUnlockedThisRound.Add(connection);
|
||||
AddUnlockedPath(connection);
|
||||
#if SERVER
|
||||
NotifyUnlock(connection);
|
||||
#else
|
||||
@@ -61,7 +73,7 @@ namespace Barotrauma
|
||||
#if SERVER
|
||||
public static void NotifyPathsUnlockedThisRound(Client client)
|
||||
{
|
||||
foreach (LocationConnection connection in pathsUnlockedThisRound)
|
||||
foreach (LocationConnection connection in _pathsUnlockedThisRound)
|
||||
{
|
||||
NotifyUnlock(connection, client);
|
||||
}
|
||||
@@ -85,4 +97,4 @@ namespace Barotrauma
|
||||
}
|
||||
#endif
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -3,8 +3,11 @@ using Barotrauma.Items.Components;
|
||||
using FarseerPhysics;
|
||||
using Microsoft.Xna.Framework;
|
||||
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
|
||||
@@ -43,7 +46,7 @@ namespace Barotrauma
|
||||
|
||||
private Level level;
|
||||
|
||||
private readonly List<Sprite> preloadedSprites = new List<Sprite>();
|
||||
private volatile ImmutableList<Sprite> _preloadedSprites = ImmutableList<Sprite>.Empty;
|
||||
|
||||
//The "intensity" of the current situation (a value between 0.0 - 1.0).
|
||||
//High when a disaster has struck, low when nothing special is going on.
|
||||
@@ -83,14 +86,18 @@ namespace Barotrauma
|
||||
private float crewAwayResetTimer;
|
||||
private float crewAwayDuration;
|
||||
|
||||
private readonly List<EventSet> pendingEventSets = new List<EventSet>();
|
||||
// volatile + ImmutableCollections
|
||||
private volatile ImmutableList<EventSet> _pendingEventSets = ImmutableList<EventSet>.Empty;
|
||||
|
||||
private readonly Dictionary<EventSet, List<Event>> selectedEvents = new Dictionary<EventSet, List<Event>>();
|
||||
private volatile ImmutableDictionary<EventSet, ImmutableList<Event>> _selectedEvents =
|
||||
ImmutableDictionary<EventSet, ImmutableList<Event>>.Empty;
|
||||
|
||||
private readonly List<Event> activeEvents = new List<Event>();
|
||||
private volatile ImmutableList<Event> _activeEvents = ImmutableList<Event>.Empty;
|
||||
|
||||
private readonly HashSet<Event> finishedEvents = new HashSet<Event>();
|
||||
private readonly HashSet<Identifier> nonRepeatableEvents = new HashSet<Identifier>();
|
||||
private volatile ImmutableHashSet<Event> _finishedEvents = ImmutableHashSet<Event>.Empty;
|
||||
private volatile ImmutableHashSet<Identifier> _nonRepeatableEvents = ImmutableHashSet<Identifier>.Empty;
|
||||
|
||||
private volatile ImmutableQueue<Action> _deferredActions = ImmutableQueue<Action>.Empty;
|
||||
|
||||
|
||||
#if DEBUG && SERVER
|
||||
@@ -112,10 +119,10 @@ namespace Barotrauma
|
||||
|
||||
public IEnumerable<Event> ActiveEvents
|
||||
{
|
||||
get { return activeEvents; }
|
||||
get { return _activeEvents; }
|
||||
}
|
||||
|
||||
public readonly Queue<Event> QueuedEvents = new Queue<Event>();
|
||||
public readonly ConcurrentQueue<Event> QueuedEvents = new ConcurrentQueue<Event>();
|
||||
|
||||
public readonly Queue<Identifier> QueuedEventsForNextRound = new Queue<Identifier>();
|
||||
|
||||
@@ -131,8 +138,8 @@ namespace Barotrauma
|
||||
}
|
||||
}
|
||||
|
||||
private readonly List<TimeStamp> timeStamps = new List<TimeStamp>();
|
||||
public void AddTimeStamp(Event e) => timeStamps.Add(new TimeStamp(e));
|
||||
private volatile ImmutableList<TimeStamp> _timeStamps = ImmutableList<TimeStamp>.Empty;
|
||||
public void AddTimeStamp(Event e) => AtomicUpdate(ref _timeStamps, list => list.Add(new TimeStamp(e)));
|
||||
|
||||
public readonly EventLog EventLog = new EventLog();
|
||||
|
||||
@@ -143,6 +150,72 @@ namespace Barotrauma
|
||||
|
||||
public bool Enabled = true;
|
||||
|
||||
private static T AtomicUpdate<T>(ref T location, Func<T, T> updateFunc) where T : class
|
||||
{
|
||||
T original, updated;
|
||||
do
|
||||
{
|
||||
original = Volatile.Read(ref location);
|
||||
updated = updateFunc(original);
|
||||
} while (Interlocked.CompareExchange(ref location, updated, original) != original);
|
||||
return updated;
|
||||
}
|
||||
|
||||
// activeEvents
|
||||
private void AddActiveEvent(Event ev) => AtomicUpdate(ref _activeEvents, list => list.Add(ev));
|
||||
private void ClearActiveEvents() => _activeEvents = ImmutableList<Event>.Empty;
|
||||
|
||||
// pendingEventSets
|
||||
private void AddPendingEventSet(EventSet eventSet) =>
|
||||
AtomicUpdate(ref _pendingEventSets, list => list.Contains(eventSet) ? list : list.Add(eventSet));
|
||||
private void RemovePendingEventSetAt(int index) =>
|
||||
AtomicUpdate(ref _pendingEventSets, list => index < list.Count ? list.RemoveAt(index) : list);
|
||||
private void ClearPendingEventSets() => _pendingEventSets = ImmutableList<EventSet>.Empty;
|
||||
|
||||
// selectedEvents
|
||||
private void AddSelectedEvent(EventSet eventSet, Event ev) =>
|
||||
AtomicUpdate(ref _selectedEvents, dict =>
|
||||
{
|
||||
var currentList = dict.GetValueOrDefault(eventSet, ImmutableList<Event>.Empty);
|
||||
return dict.SetItem(eventSet, currentList.Add(ev));
|
||||
});
|
||||
private void RemoveSelectedEventSet(EventSet eventSet) =>
|
||||
AtomicUpdate(ref _selectedEvents, dict => dict.Remove(eventSet));
|
||||
private void ClearSelectedEvents() =>
|
||||
_selectedEvents = ImmutableDictionary<EventSet, ImmutableList<Event>>.Empty;
|
||||
private ImmutableList<Event> GetSelectedEvents(EventSet eventSet) =>
|
||||
_selectedEvents.GetValueOrDefault(eventSet, ImmutableList<Event>.Empty);
|
||||
private bool HasSelectedEvents(EventSet eventSet) => _selectedEvents.ContainsKey(eventSet);
|
||||
|
||||
// finishedEvents
|
||||
private void AddFinishedEvent(Event ev) => AtomicUpdate(ref _finishedEvents, set => set.Add(ev));
|
||||
private void ClearFinishedEvents() => _finishedEvents = ImmutableHashSet<Event>.Empty;
|
||||
private bool IsEventFinished(Event ev) => _finishedEvents.Contains(ev);
|
||||
|
||||
// nonRepeatableEvents
|
||||
private void AddNonRepeatableEvent(Identifier id) => AtomicUpdate(ref _nonRepeatableEvents, set => set.Add(id));
|
||||
private void ClearNonRepeatableEvents() => _nonRepeatableEvents = ImmutableHashSet<Identifier>.Empty;
|
||||
|
||||
// preloadedSprites
|
||||
private void AddPreloadedSprite(Sprite sprite) => AtomicUpdate(ref _preloadedSprites, list => list.Add(sprite));
|
||||
private void ClearPreloadedSprites()
|
||||
{
|
||||
var sprites = Interlocked.Exchange(ref _preloadedSprites, ImmutableList<Sprite>.Empty);
|
||||
foreach (var s in sprites) { s.Remove(); }
|
||||
}
|
||||
|
||||
// timeStamps
|
||||
private void ClearTimeStamps() => _timeStamps = ImmutableList<TimeStamp>.Empty;
|
||||
|
||||
private void EnqueueDeferredAction(Action action) =>
|
||||
AtomicUpdate(ref _deferredActions, queue => queue.Enqueue(action));
|
||||
private void ProcessDeferredActions()
|
||||
{
|
||||
var actions = Interlocked.Exchange(ref _deferredActions, ImmutableQueue<Action>.Empty);
|
||||
foreach (var action in actions) { action(); }
|
||||
}
|
||||
|
||||
|
||||
private MTRandom random;
|
||||
public int RandomSeed { get; private set; }
|
||||
|
||||
@@ -152,15 +225,15 @@ namespace Barotrauma
|
||||
|
||||
if (isClient) { return; }
|
||||
|
||||
timeStamps.Clear();
|
||||
pendingEventSets.Clear();
|
||||
selectedEvents.Clear();
|
||||
activeEvents.Clear();
|
||||
ClearTimeStamps();
|
||||
ClearPendingEventSets();
|
||||
ClearSelectedEvents();
|
||||
ClearActiveEvents();
|
||||
#if SERVER
|
||||
MissionAction.ResetMissionsUnlockedThisRound();
|
||||
UnlockPathAction.ResetPathsUnlockedThisRound();
|
||||
#endif
|
||||
pathFinder = new PathFinder(WayPoint.WayPointList, false);
|
||||
pathFinder = new PathFinder(WayPoint.WayPointList.ToList(), false);
|
||||
totalPathLength = 0.0f;
|
||||
if (level != null)
|
||||
{
|
||||
@@ -235,8 +308,8 @@ namespace Barotrauma
|
||||
|
||||
void AddSet(EventSet eventSet)
|
||||
{
|
||||
if (pendingEventSets.Contains(eventSet)) { return; }
|
||||
pendingEventSets.Add(eventSet);
|
||||
if (_pendingEventSets.Contains(eventSet)) { return; }
|
||||
AddPendingEventSet(eventSet);
|
||||
CreateEvents(eventSet);
|
||||
}
|
||||
|
||||
@@ -287,7 +360,7 @@ namespace Barotrauma
|
||||
{
|
||||
foreach (EventPrefab ep in eventSet.EventPrefabs.SelectMany(e => e.EventPrefabs))
|
||||
{
|
||||
nonRepeatableEvents.Add(ep.Identifier);
|
||||
AddNonRepeatableEvent(ep.Identifier);
|
||||
}
|
||||
}
|
||||
foreach (EventSet childSet in eventSet.ChildSets)
|
||||
@@ -332,13 +405,13 @@ namespace Barotrauma
|
||||
|
||||
public void ActivateEvent(Event newEvent)
|
||||
{
|
||||
activeEvents.Add(newEvent);
|
||||
AddActiveEvent(newEvent);
|
||||
newEvent.Init();
|
||||
}
|
||||
|
||||
public void ClearEvents()
|
||||
{
|
||||
activeEvents.Clear();
|
||||
ClearActiveEvents();
|
||||
}
|
||||
|
||||
private void SelectSettings()
|
||||
@@ -391,7 +464,8 @@ namespace Barotrauma
|
||||
|
||||
public IEnumerable<ContentFile> GetFilesToPreload()
|
||||
{
|
||||
foreach (List<Event> eventList in selectedEvents.Values)
|
||||
var snapshot = _selectedEvents;
|
||||
foreach (ImmutableList<Event> eventList in snapshot.Values)
|
||||
{
|
||||
foreach (Event ev in eventList)
|
||||
{
|
||||
@@ -444,13 +518,13 @@ namespace Barotrauma
|
||||
|
||||
foreach (ContentFile file in filesToPreload)
|
||||
{
|
||||
file.Preload(preloadedSprites.Add);
|
||||
file.Preload(AddPreloadedSprite);
|
||||
}
|
||||
}
|
||||
|
||||
public void TriggerOnEndRoundActions()
|
||||
{
|
||||
foreach (var ev in activeEvents)
|
||||
foreach (var ev in _activeEvents)
|
||||
{
|
||||
(ev as ScriptedEvent)?.OnRoundEndAction?.Update(1.0f);
|
||||
}
|
||||
@@ -458,17 +532,16 @@ namespace Barotrauma
|
||||
|
||||
public void EndRound()
|
||||
{
|
||||
pendingEventSets.Clear();
|
||||
selectedEvents.Clear();
|
||||
activeEvents.Clear();
|
||||
QueuedEvents.Clear();
|
||||
finishedEvents.Clear();
|
||||
nonRepeatableEvents.Clear();
|
||||
ClearPendingEventSets();
|
||||
ClearSelectedEvents();
|
||||
ClearActiveEvents();
|
||||
while (QueuedEvents.TryDequeue(out _)) { } // 清空 ConcurrentQueue
|
||||
ClearFinishedEvents();
|
||||
ClearNonRepeatableEvents();
|
||||
|
||||
preloadedSprites.ForEach(s => s.Remove());
|
||||
preloadedSprites.Clear();
|
||||
ClearPreloadedSprites();
|
||||
|
||||
timeStamps.Clear();
|
||||
ClearTimeStamps();
|
||||
|
||||
pathFinder = null;
|
||||
}
|
||||
@@ -484,7 +557,7 @@ namespace Barotrauma
|
||||
{
|
||||
if (registerFinishedOnly)
|
||||
{
|
||||
foreach (var finishedEvent in finishedEvents)
|
||||
foreach (var finishedEvent in _finishedEvents)
|
||||
{
|
||||
EventSet parentSet = finishedEvent.ParentSet;
|
||||
if (parentSet == null) { continue; }
|
||||
@@ -499,7 +572,7 @@ namespace Barotrauma
|
||||
}
|
||||
}
|
||||
|
||||
level.LevelData.EventHistory.AddRange(selectedEvents.Values
|
||||
level.LevelData.EventHistory.AddRange(_selectedEvents.Values
|
||||
.SelectMany(v => v)
|
||||
.Select(e => e.Prefab.Identifier)
|
||||
.Where(eventId => Register(eventId) && !level.LevelData.EventHistory.Contains(eventId)));
|
||||
@@ -509,14 +582,14 @@ namespace Barotrauma
|
||||
level.LevelData.EventHistory.RemoveRange(0, level.LevelData.EventHistory.Count - MaxEventHistory);
|
||||
}
|
||||
}
|
||||
level.LevelData.NonRepeatableEvents.AddRange(nonRepeatableEvents.Where(eventId => Register(eventId) && !level.LevelData.NonRepeatableEvents.Contains(eventId)));
|
||||
level.LevelData.NonRepeatableEvents.AddRange(_nonRepeatableEvents.Where(eventId => Register(eventId) && !level.LevelData.NonRepeatableEvents.Contains(eventId)));
|
||||
|
||||
if (!registerFinishedOnly)
|
||||
{
|
||||
level.LevelData.FinishedEvents.Clear();
|
||||
}
|
||||
|
||||
bool Register(Identifier eventId) => !registerFinishedOnly || finishedEvents.Any(fe => fe.Prefab.Identifier == eventId);
|
||||
bool Register(Identifier eventId) => !registerFinishedOnly || _finishedEvents.Any(fe => fe.Prefab.Identifier == eventId);
|
||||
}
|
||||
|
||||
public void SkipEventCooldown()
|
||||
@@ -534,7 +607,7 @@ namespace Barotrauma
|
||||
|
||||
private void CreateEvents(EventSet eventSet)
|
||||
{
|
||||
selectedEvents.Remove(eventSet);
|
||||
RemoveSelectedEventSet(eventSet);
|
||||
if (level == null) { return; }
|
||||
if (level.LevelData.HasHuntingGrounds && eventSet.DisableInHuntingGrounds) { return; }
|
||||
if (eventSet.Exhaustible && level.LevelData.IsEventSetExhausted(eventSet)) { return; }
|
||||
@@ -601,11 +674,7 @@ namespace Barotrauma
|
||||
if (newEvent == null) { continue; }
|
||||
if (i < spawnPosFilter.Count) { newEvent.SpawnPosFilter = spawnPosFilter[i]; }
|
||||
DebugConsole.NewMessage($"Initialized event {newEvent}", debugOnly: true);
|
||||
if (!selectedEvents.ContainsKey(eventSet))
|
||||
{
|
||||
selectedEvents.Add(eventSet, new List<Event>());
|
||||
}
|
||||
selectedEvents[eventSet].Add(newEvent);
|
||||
AddSelectedEvent(eventSet, newEvent);
|
||||
unusedEvents.Remove(subEventPrefab);
|
||||
}
|
||||
}
|
||||
@@ -644,11 +713,7 @@ namespace Barotrauma
|
||||
var newEvent = eventPrefab.CreateInstance(RandomSeed);
|
||||
if (newEvent == null) { continue; }
|
||||
if (i < spawnPosFilter.Count) { newEvent.SpawnPosFilter = spawnPosFilter[i]; }
|
||||
if (!selectedEvents.ContainsKey(eventSet))
|
||||
{
|
||||
selectedEvents.Add(eventSet, new List<Event>());
|
||||
}
|
||||
selectedEvents[eventSet].Add(newEvent);
|
||||
AddSelectedEvent(eventSet, newEvent);
|
||||
}
|
||||
|
||||
var location = GetEventLocation();
|
||||
@@ -840,9 +905,10 @@ namespace Barotrauma
|
||||
|
||||
if (!eventsInitialized)
|
||||
{
|
||||
foreach (var eventSet in selectedEvents.Keys)
|
||||
var selectedSnapshot = _selectedEvents;
|
||||
foreach (var eventSet in selectedSnapshot.Keys)
|
||||
{
|
||||
foreach (var ev in selectedEvents[eventSet])
|
||||
foreach (var ev in selectedSnapshot[eventSet])
|
||||
{
|
||||
ev.Init(eventSet);
|
||||
}
|
||||
@@ -913,23 +979,25 @@ namespace Barotrauma
|
||||
{
|
||||
recheck = false;
|
||||
//activate pending event sets that can be activated
|
||||
for (int i = pendingEventSets.Count - 1; i >= 0; i--)
|
||||
var pendingSnapshot = _pendingEventSets;
|
||||
for (int i = pendingSnapshot.Count - 1; i >= 0; i--)
|
||||
{
|
||||
var eventSet = pendingEventSets[i];
|
||||
var eventSet = pendingSnapshot[i];
|
||||
if (eventCoolDown > 0.0f && !eventSet.IgnoreCoolDown) { continue; }
|
||||
if (currentIntensity > eventThreshold && !eventSet.IgnoreIntensity) { continue; }
|
||||
if (!CanStartEventSet(eventSet)) { continue; }
|
||||
|
||||
pendingEventSets.RemoveAt(i);
|
||||
RemovePendingEventSetAt(i);
|
||||
|
||||
if (selectedEvents.ContainsKey(eventSet))
|
||||
var selectedEventsList = GetSelectedEvents(eventSet);
|
||||
if (selectedEventsList.Count > 0)
|
||||
{
|
||||
//start events in this set
|
||||
foreach (Event ev in selectedEvents[eventSet])
|
||||
foreach (Event ev in selectedEventsList)
|
||||
{
|
||||
activeEvents.Add(ev);
|
||||
AddActiveEvent(ev);
|
||||
eventThreshold = settings.DefaultEventThreshold;
|
||||
if (eventSet.TriggerEventCooldown && selectedEvents[eventSet].Any(e => e.Prefab.TriggerEventCooldown))
|
||||
if (eventSet.TriggerEventCooldown && selectedEventsList.Any(e => e.Prefab.TriggerEventCooldown))
|
||||
{
|
||||
eventCoolDown = settings.EventCooldown;
|
||||
}
|
||||
@@ -937,12 +1005,15 @@ namespace Barotrauma
|
||||
{
|
||||
ev.Finished += () =>
|
||||
{
|
||||
pendingEventSets.Add(eventSet);
|
||||
CreateEvents(eventSet);
|
||||
foreach (Event newEvent in selectedEvents[eventSet])
|
||||
EnqueueDeferredAction(() =>
|
||||
{
|
||||
if (!newEvent.Initialized) { newEvent.Init(eventSet); }
|
||||
}
|
||||
AddPendingEventSet(eventSet);
|
||||
CreateEvents(eventSet);
|
||||
foreach (Event newEvent in GetSelectedEvents(eventSet))
|
||||
{
|
||||
if (!newEvent.Initialized) { newEvent.Init(eventSet); }
|
||||
}
|
||||
});
|
||||
};
|
||||
}
|
||||
}
|
||||
@@ -951,37 +1022,40 @@ namespace Barotrauma
|
||||
//add child event sets to pending
|
||||
foreach (EventSet childEventSet in eventSet.ChildSets)
|
||||
{
|
||||
pendingEventSets.Add(childEventSet);
|
||||
AddPendingEventSet(childEventSet);
|
||||
recheck = true;
|
||||
}
|
||||
}
|
||||
} while (recheck);
|
||||
|
||||
foreach (Event ev in activeEvents)
|
||||
var activeSnapshot = _activeEvents;
|
||||
foreach (Event ev in activeSnapshot)
|
||||
{
|
||||
if (!ev.IsFinished)
|
||||
{
|
||||
ev.Update(deltaTime);
|
||||
}
|
||||
else if (ev.Prefab != null && !finishedEvents.Any(e => e.Prefab == ev.Prefab))
|
||||
else if (ev.Prefab != null && !IsEventFinished(ev))
|
||||
{
|
||||
if (level?.LevelData != null && level.LevelData.Type == LevelData.LevelType.Outpost)
|
||||
{
|
||||
if (!level.LevelData.EventHistory.Contains(ev.Prefab.Identifier)) { level.LevelData.EventHistory.Add(ev.Prefab.Identifier); }
|
||||
}
|
||||
finishedEvents.Add(ev);
|
||||
AddFinishedEvent(ev);
|
||||
}
|
||||
}
|
||||
|
||||
if (QueuedEvents.Count > 0)
|
||||
if (QueuedEvents.TryDequeue(out var queuedEvent))
|
||||
{
|
||||
activeEvents.Add(QueuedEvents.Dequeue());
|
||||
AddActiveEvent(queuedEvent);
|
||||
}
|
||||
|
||||
ProcessDeferredActions();
|
||||
}
|
||||
|
||||
public void EntitySpawned(Entity entity)
|
||||
{
|
||||
foreach (var ev in activeEvents)
|
||||
foreach (var ev in _activeEvents)
|
||||
{
|
||||
if (ev is ScriptedEvent scriptedEvent)
|
||||
{
|
||||
|
||||
@@ -3,6 +3,7 @@ using System;
|
||||
using System.Collections.Generic;
|
||||
using System.Collections.Immutable;
|
||||
using System.Linq;
|
||||
using System.Threading;
|
||||
using System.Xml.Linq;
|
||||
|
||||
namespace Barotrauma
|
||||
@@ -60,47 +61,48 @@ namespace Barotrauma
|
||||
return null;
|
||||
}
|
||||
#endif
|
||||
|
||||
private static readonly Dictionary<Identifier, EventPrefab> AllEventPrefabs = new Dictionary<Identifier, EventPrefab>();
|
||||
private static volatile ImmutableDictionary<Identifier, EventPrefab> _allEventPrefabs =
|
||||
ImmutableDictionary<Identifier, EventPrefab>.Empty;
|
||||
|
||||
public static IEnumerable<EventPrefab> GetAllEventPrefabs()
|
||||
{
|
||||
return AllEventPrefabs.Values;
|
||||
return _allEventPrefabs.Values;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Finds all the event prefabs (both "normal prefabs" that exists by themselves, present in <see cref="EventPrefab.Prefabs"/>, and the ones that exists only inside child event sets),
|
||||
/// and adds them to <see cref="AllEventPrefabs"/>.
|
||||
/// and adds them to <see cref="_allEventPrefabs"/>.
|
||||
/// </summary>
|
||||
public static void RefreshAllEventPrefabs()
|
||||
{
|
||||
AllEventPrefabs.Clear();
|
||||
var builder = ImmutableDictionary.CreateBuilder<Identifier, EventPrefab>();
|
||||
foreach (var eventPrefab in EventPrefab.Prefabs)
|
||||
{
|
||||
AllEventPrefabs.TryAdd(eventPrefab.Identifier, eventPrefab);
|
||||
builder.TryAdd(eventPrefab.Identifier, eventPrefab);
|
||||
}
|
||||
foreach (var eventSet in Prefabs)
|
||||
{
|
||||
AddChildEventPrefabs(eventSet);
|
||||
AddChildEventPrefabs(eventSet, builder);
|
||||
}
|
||||
Interlocked.Exchange(ref _allEventPrefabs, builder.ToImmutable());
|
||||
}
|
||||
|
||||
private static void AddChildEventPrefabs(EventSet set)
|
||||
private static void AddChildEventPrefabs(EventSet set, ImmutableDictionary<Identifier, EventPrefab>.Builder builder)
|
||||
{
|
||||
foreach (var subEventPrefabs in set.EventPrefabs)
|
||||
{
|
||||
foreach (var eventPrefab in subEventPrefabs.EventPrefabs)
|
||||
{
|
||||
AllEventPrefabs.TryAdd(eventPrefab.Identifier, eventPrefab);
|
||||
builder.TryAdd(eventPrefab.Identifier, eventPrefab);
|
||||
}
|
||||
}
|
||||
|
||||
foreach (var childSet in set.ChildSets) { AddChildEventPrefabs(childSet); }
|
||||
foreach (var childSet in set.ChildSets) { AddChildEventPrefabs(childSet, builder); }
|
||||
}
|
||||
|
||||
public static EventPrefab GetEventPrefab(Identifier identifier)
|
||||
{
|
||||
return AllEventPrefabs.GetValueOrDefault(identifier);
|
||||
return _allEventPrefabs.GetValueOrDefault(identifier);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
|
||||
@@ -41,7 +41,7 @@ namespace Barotrauma
|
||||
|
||||
protected override void InitEventSpecific(EventSet parentSet)
|
||||
{
|
||||
var matchingItems = Item.ItemList.FindAll(i => i.Condition > 0.0f && targetItemIdentifiers.Contains(i.Prefab.Identifier));
|
||||
var matchingItems = Item.ItemList.Where(i => i.Condition > 0.0f && targetItemIdentifiers.Contains(i.Prefab.Identifier)).ToList();
|
||||
int itemAmount = Rand.Range(minItemAmount, maxItemAmount, Rand.RandSync.ServerAndClient);
|
||||
for (int i = 0; i < itemAmount; i++)
|
||||
{
|
||||
|
||||
@@ -111,7 +111,7 @@ namespace Barotrauma
|
||||
{
|
||||
if (!itemTag.IsEmpty)
|
||||
{
|
||||
var itemsToDestroy = Item.ItemList.FindAll(it => it.Submarine?.Info.Type != SubmarineType.Player && it.HasTag(itemTag));
|
||||
var itemsToDestroy = Item.ItemList.Where(it => it.Submarine?.Info.Type != SubmarineType.Player && it.HasTag(itemTag)).ToList();
|
||||
if (!itemsToDestroy.Any())
|
||||
{
|
||||
DebugConsole.ThrowError($"Error in mission \"{Prefab.Identifier}\". Could not find an item with the tag \"{itemTag}\".",
|
||||
|
||||
@@ -181,7 +181,7 @@ namespace Barotrauma
|
||||
return;
|
||||
}
|
||||
destructibleItems.Clear();
|
||||
destructibleItems.AddRange(Item.ItemList.FindAll(it => it.HasTag(destructibleItemTag)));
|
||||
destructibleItems.AddRange(Item.ItemList.Where(it => it.HasTag(destructibleItemTag)));
|
||||
if (destructibleItems.None())
|
||||
{
|
||||
DebugConsole.ThrowError($"Error in end mission \"{Prefab.Identifier}\". Could not find any destructible items with the tag \"{spawnPointTag}\".",
|
||||
|
||||
@@ -283,7 +283,7 @@ namespace Barotrauma
|
||||
|
||||
if (!IsClient)
|
||||
{
|
||||
PathFinder pathFinder = new PathFinder(WayPoint.WayPointList, false);
|
||||
PathFinder pathFinder = new PathFinder(WayPoint.WayPointList.ToList(), false);
|
||||
var path = pathFinder.FindPath(ConvertUnits.ToSimUnits(patrolPos), ConvertUnits.ToSimUnits(preferredSpawnPos));
|
||||
if (!path.Unreachable)
|
||||
{
|
||||
|
||||
@@ -128,7 +128,7 @@ namespace Barotrauma
|
||||
{
|
||||
foreach (var stackedItem in item.GetStackedItems())
|
||||
{
|
||||
Item.DeconstructItems.Add(stackedItem);
|
||||
Item.MarkForDeconstruction(stackedItem);
|
||||
}
|
||||
#if CLIENT
|
||||
HintManager.OnItemMarkedForDeconstruction(order.OrderGiver);
|
||||
@@ -138,7 +138,7 @@ namespace Barotrauma
|
||||
{
|
||||
foreach (var stackedItem in item.GetStackedItems())
|
||||
{
|
||||
Item.DeconstructItems.Remove(stackedItem);
|
||||
Item.UnmarkForDeconstruction(stackedItem);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1089,7 +1089,7 @@ namespace Barotrauma
|
||||
|
||||
//Clear the grids to allow for garbage collection
|
||||
Powered.Grids.Clear();
|
||||
Powered.ChangedConnections.Clear();
|
||||
Powered.ClearChangedConnections();
|
||||
|
||||
try
|
||||
{
|
||||
@@ -1146,6 +1146,7 @@ namespace Barotrauma
|
||||
EventManager?.EndRound();
|
||||
StatusEffect.StopAll();
|
||||
AfflictionPrefab.ClearAllEffects();
|
||||
PhysicsBodyQueue.Clear();
|
||||
IsRunning = false;
|
||||
|
||||
#if CLIENT
|
||||
|
||||
@@ -5,6 +5,7 @@ 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
|
||||
@@ -24,11 +25,8 @@ namespace Barotrauma.Items.Components
|
||||
Right
|
||||
}
|
||||
|
||||
private static readonly List<DockingPort> list = new List<DockingPort>();
|
||||
public static IEnumerable<DockingPort> List
|
||||
{
|
||||
get { return list; }
|
||||
}
|
||||
private static readonly ConcurrentDictionary<DockingPort, byte> _dockingPortDict = new ConcurrentDictionary<DockingPort, byte>();
|
||||
public static IEnumerable<DockingPort> List => _dockingPortDict.Keys;
|
||||
|
||||
private Sprite overlaySprite;
|
||||
private float dockingState;
|
||||
@@ -168,7 +166,7 @@ namespace Barotrauma.Items.Components
|
||||
|
||||
IsActive = true;
|
||||
|
||||
list.Add(this);
|
||||
_dockingPortDict.TryAdd(this, 0);
|
||||
}
|
||||
|
||||
public override void FlipX(bool relativeToSub)
|
||||
@@ -200,7 +198,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);
|
||||
@@ -532,8 +530,8 @@ namespace Barotrauma.Items.Components
|
||||
wire.TryConnect(recipient, addNode: false);
|
||||
|
||||
//Flag connections to be updated
|
||||
Powered.ChangedConnections.Add(powerConnection);
|
||||
Powered.ChangedConnections.Add(recipient);
|
||||
Powered.MarkConnectionChanged(powerConnection);
|
||||
Powered.MarkConnectionChanged(recipient);
|
||||
}
|
||||
|
||||
private void CreateDoorBody()
|
||||
@@ -1007,7 +1005,7 @@ namespace Barotrauma.Items.Components
|
||||
Connection powerConnection = Item.Connections.Find(c => c.IsPower);
|
||||
if (powerConnection != null)
|
||||
{
|
||||
Powered.ChangedConnections.Add(powerConnection);
|
||||
Powered.MarkConnectionChanged(powerConnection);
|
||||
}
|
||||
|
||||
if (doorBody != null)
|
||||
@@ -1151,7 +1149,7 @@ namespace Barotrauma.Items.Components
|
||||
protected override void RemoveComponentSpecific()
|
||||
{
|
||||
base.RemoveComponentSpecific();
|
||||
list.Remove(this);
|
||||
_dockingPortDict.TryRemove(this, out _);
|
||||
hulls[0]?.Remove(); hulls[0] = null;
|
||||
hulls[1]?.Remove(); hulls[1] = null;
|
||||
gap?.Remove(); gap = null;
|
||||
|
||||
@@ -2,6 +2,7 @@
|
||||
using FarseerPhysics;
|
||||
using Microsoft.Xna.Framework;
|
||||
using System;
|
||||
using System.Collections.Concurrent;
|
||||
using System.Collections.Generic;
|
||||
using System.Linq;
|
||||
using FarseerPhysics.Dynamics;
|
||||
@@ -14,9 +15,9 @@ namespace Barotrauma.Items.Components
|
||||
{
|
||||
partial class Door : Pickable, IDrawableComponent, IServerSerializable
|
||||
{
|
||||
private static readonly HashSet<Door> doorList = new HashSet<Door>();
|
||||
private static readonly ConcurrentDictionary<Door, byte> _doorDict = new ConcurrentDictionary<Door, byte>();
|
||||
|
||||
public static IReadOnlyCollection<Door> DoorList { get { return doorList; } }
|
||||
public static ICollection<Door> DoorList => _doorDict.Keys;
|
||||
|
||||
private Gap linkedGap;
|
||||
private bool isOpen;
|
||||
@@ -277,7 +278,7 @@ namespace Barotrauma.Items.Components
|
||||
}
|
||||
|
||||
IsActive = true;
|
||||
doorList.Add(this);
|
||||
_doorDict.TryAdd(this, 0);
|
||||
}
|
||||
|
||||
public override void OnItemLoaded()
|
||||
@@ -313,13 +314,21 @@ namespace Barotrauma.Items.Components
|
||||
|
||||
public override void Move(Vector2 amount, bool ignoreContacts = false)
|
||||
{
|
||||
if (ignoreContacts)
|
||||
// Defer physics operation if in parallel context (Farseer is not thread-safe)
|
||||
if (Body != null)
|
||||
{
|
||||
Body?.SetTransformIgnoreContacts(Body.SimPosition + ConvertUnits.ToSimUnits(amount), 0.0f);
|
||||
}
|
||||
else
|
||||
{
|
||||
Body?.SetTransform(Body.SimPosition + ConvertUnits.ToSimUnits(amount), 0.0f);
|
||||
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));
|
||||
}
|
||||
}
|
||||
#if CLIENT
|
||||
UpdateConvexHulls();
|
||||
@@ -669,7 +678,7 @@ namespace Barotrauma.Items.Components
|
||||
convexHull2?.Remove();
|
||||
#endif
|
||||
|
||||
doorList.Remove(this);
|
||||
_doorDict.TryRemove(this, out _);
|
||||
}
|
||||
|
||||
private bool CheckSubmarinesInDoorWay()
|
||||
@@ -785,13 +794,19 @@ 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)
|
||||
{
|
||||
body.SetTransformIgnoreContacts(new Vector2(body.SimPosition.X, item.SimPosition.Y + dir * doorRectSimSize.Y * 2.0f), body.Rotation);
|
||||
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));
|
||||
}
|
||||
else
|
||||
{
|
||||
body.SetTransformIgnoreContacts(new Vector2(item.SimPosition.X + dir * doorRectSimSize.X * 1.2f, body.SimPosition.Y), body.Rotation);
|
||||
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));
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -3,6 +3,7 @@ using Barotrauma.Networking;
|
||||
using FarseerPhysics;
|
||||
using Microsoft.Xna.Framework;
|
||||
using System;
|
||||
using System.Collections.Concurrent;
|
||||
using System.Collections.Generic;
|
||||
using System.Linq;
|
||||
|
||||
@@ -10,11 +11,8 @@ namespace Barotrauma.Items.Components
|
||||
{
|
||||
partial class ElectricalDischarger : Powered, IServerSerializable
|
||||
{
|
||||
private static readonly List<ElectricalDischarger> list = new List<ElectricalDischarger>();
|
||||
public static IEnumerable<ElectricalDischarger> List
|
||||
{
|
||||
get { return list; }
|
||||
}
|
||||
private static readonly ConcurrentDictionary<ElectricalDischarger, byte> _dischargerDict = new ConcurrentDictionary<ElectricalDischarger, byte>();
|
||||
public static IEnumerable<ElectricalDischarger> List => _dischargerDict.Keys;
|
||||
|
||||
const int MaxNodes = 100;
|
||||
const float MaxNodeDistance = 150.0f;
|
||||
@@ -115,7 +113,7 @@ namespace Barotrauma.Items.Components
|
||||
public ElectricalDischarger(Item item, ContentXElement element) :
|
||||
base(item, element)
|
||||
{
|
||||
list.Add(this);
|
||||
_dischargerDict.TryAdd(this, 0);
|
||||
|
||||
foreach (var subElement in element.Elements())
|
||||
{
|
||||
@@ -604,7 +602,7 @@ namespace Barotrauma.Items.Components
|
||||
protected override void RemoveComponentSpecific()
|
||||
{
|
||||
base.RemoveComponentSpecific();
|
||||
list.Remove(this);
|
||||
_dischargerDict.TryRemove(this, out _);
|
||||
}
|
||||
|
||||
public void ServerEventWrite(IWriteMessage msg, Client c, NetEntityEvent.IData extraData = null)
|
||||
|
||||
@@ -9,6 +9,7 @@ using System;
|
||||
using System.Collections.Generic;
|
||||
using System.Collections.Immutable;
|
||||
using System.Linq;
|
||||
using System.Threading;
|
||||
using System.Xml.Linq;
|
||||
|
||||
namespace Barotrauma.Items.Components
|
||||
@@ -485,7 +486,10 @@ namespace Barotrauma.Items.Components
|
||||
}
|
||||
else
|
||||
{
|
||||
item.body.ResetDynamics();
|
||||
// Calculate target position
|
||||
Vector2 targetPos;
|
||||
Submarine forceSubmarine = picker.Submarine;
|
||||
|
||||
Limb heldHand, arm;
|
||||
if (picker.Inventory.IsInLimbSlot(item, InvSlotType.LeftHand))
|
||||
{
|
||||
@@ -503,17 +507,42 @@ 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(heldHand.SimPosition + diff, 0.0f, forceSubmarine: picker.Submarine);
|
||||
}
|
||||
else
|
||||
{
|
||||
item.SetTransform(picker.SimPosition, 0.0f, forceSubmarine: picker.Submarine);
|
||||
item.SetTransform(targetPos, 0.0f, forceSubmarine: forceSubmarine);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -616,12 +645,13 @@ namespace Barotrauma.Items.Components
|
||||
return CanBeAttached(user, out _);
|
||||
}
|
||||
|
||||
private static List<Item> tempOverlappingItems = new List<Item>();
|
||||
private static readonly ThreadLocal<List<Item>> tempOverlappingItems = new ThreadLocal<List<Item>>(() => new List<Item>());
|
||||
|
||||
private bool CanBeAttached(Character user, out IEnumerable<Item> overlappingItems)
|
||||
{
|
||||
tempOverlappingItems.Clear();
|
||||
overlappingItems = tempOverlappingItems;
|
||||
var overlapping = tempOverlappingItems.Value;
|
||||
overlapping.Clear();
|
||||
overlappingItems = overlapping;
|
||||
if (!attachable || !Reattachable) { return false; }
|
||||
|
||||
//can be attached anywhere in sub editor
|
||||
@@ -664,9 +694,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; }
|
||||
tempOverlappingItems.Add(otherItem);
|
||||
overlapping.Add(otherItem);
|
||||
}
|
||||
if (tempOverlappingItems.Any()) { return false; }
|
||||
if (overlapping.Any()) { return false; }
|
||||
}
|
||||
|
||||
//can be attached anywhere inside hulls
|
||||
|
||||
+30
-5
@@ -13,6 +13,12 @@ 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
|
||||
@@ -86,13 +92,16 @@ 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)
|
||||
{
|
||||
trigger.SetTransformIgnoreContacts(item.SimPosition, 0.0f);
|
||||
PhysicsBodyQueue.ExecuteOrDefer(() => capturedTrigger.SetTransformIgnoreContacts(capturedPos, 0.0f));
|
||||
}
|
||||
else
|
||||
{
|
||||
trigger.SetTransform(item.SimPosition, 0.0f);
|
||||
PhysicsBodyQueue.ExecuteOrDefer(() => capturedTrigger.SetTransform(capturedPos, 0.0f));
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -109,13 +118,29 @@ namespace Barotrauma.Items.Components
|
||||
}
|
||||
else
|
||||
{
|
||||
if (trigger == null)
|
||||
if (trigger == null && !triggerBodyCreationQueued)
|
||||
{
|
||||
CreateTriggerBody();
|
||||
// Queue the physics body creation to be processed on the main thread.
|
||||
// This is necessary because physics body creation is not thread-safe
|
||||
// and Update() may be called from a parallel loop.
|
||||
triggerBodyCreationQueued = true;
|
||||
PhysicsBodyQueue.EnqueueCreation(() =>
|
||||
{
|
||||
// Double-check that trigger hasn't been created yet
|
||||
// (in case this was called multiple times before queue processing)
|
||||
if (trigger == null && !item.Removed)
|
||||
{
|
||||
CreateTriggerBody();
|
||||
}
|
||||
triggerBodyCreationQueued = false;
|
||||
});
|
||||
}
|
||||
if (trigger != null && Vector2.DistanceSquared(item.SimPosition, trigger.SimPosition) > 0.01f)
|
||||
{
|
||||
trigger.SetTransform(item.SimPosition, 0.0f);
|
||||
// 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));
|
||||
}
|
||||
IsActive = false;
|
||||
}
|
||||
|
||||
@@ -4,6 +4,7 @@ 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 +26,7 @@ namespace Barotrauma.Items.Components
|
||||
|
||||
private readonly HashSet<Entity> hitTargets = new HashSet<Entity>();
|
||||
|
||||
private readonly Queue<Fixture> impactQueue = new Queue<Fixture>();
|
||||
private readonly ConcurrentQueue<Fixture> impactQueue = new ConcurrentQueue<Fixture>();
|
||||
|
||||
public Character User { get; private set; }
|
||||
|
||||
@@ -191,17 +192,16 @@ namespace Barotrauma.Items.Components
|
||||
{
|
||||
if (!item.body.Enabled)
|
||||
{
|
||||
impactQueue.Clear();
|
||||
while (impactQueue.TryDequeue(out _)) { } // Clear queue
|
||||
return;
|
||||
}
|
||||
if (picker == null || !picker.HeldItems.Contains(item))
|
||||
{
|
||||
impactQueue.Clear();
|
||||
while (impactQueue.TryDequeue(out _)) { } // Clear queue
|
||||
IsActive = false;
|
||||
}
|
||||
while (impactQueue.Count > 0)
|
||||
while (impactQueue.TryDequeue(out var impact))
|
||||
{
|
||||
var impact = impactQueue.Dequeue();
|
||||
HandleImpact(impact);
|
||||
}
|
||||
//in case handling the impact does something to the picker
|
||||
@@ -301,7 +301,7 @@ namespace Barotrauma.Items.Components
|
||||
|
||||
private void RestoreCollision()
|
||||
{
|
||||
impactQueue.Clear();
|
||||
while (impactQueue.TryDequeue(out _)) { } // Clear queue
|
||||
item.body.FarseerBody.OnCollision -= OnCollision;
|
||||
item.body.CollisionCategories = Physics.CollisionItem;
|
||||
item.body.CollidesWith = Physics.DefaultItemCollidesWith;
|
||||
|
||||
@@ -4,6 +4,7 @@ using Microsoft.Xna.Framework;
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.Linq;
|
||||
using System.Threading;
|
||||
using Barotrauma.Extensions;
|
||||
using Barotrauma.MapCreatures.Behavior;
|
||||
|
||||
@@ -315,7 +316,7 @@ namespace Barotrauma.Items.Components
|
||||
|
||||
partial void UseProjSpecific(float deltaTime, Vector2 raystart);
|
||||
|
||||
private static readonly List<Body> hitBodies = new List<Body>();
|
||||
private static readonly ThreadLocal<List<Body>> hitBodies = new ThreadLocal<List<Body>>(() => 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)
|
||||
@@ -373,13 +374,13 @@ namespace Barotrauma.Items.Components
|
||||
},
|
||||
allowInsideFixture: true);
|
||||
|
||||
hitBodies.Clear();
|
||||
hitBodies.AddRange(bodies.Distinct());
|
||||
hitBodies.Value.Clear();
|
||||
hitBodies.Value.AddRange(bodies.Distinct());
|
||||
|
||||
lastPickedFraction = Submarine.LastPickedFraction;
|
||||
Type lastHitType = null;
|
||||
hitCharacters.Clear();
|
||||
foreach (Body body in hitBodies)
|
||||
foreach (Body body in hitBodies.Value)
|
||||
{
|
||||
Type bodyType = body.UserData?.GetType();
|
||||
if (!RepairThroughWalls && bodyType != null && bodyType != lastHitType)
|
||||
@@ -897,48 +898,49 @@ namespace Barotrauma.Items.Components
|
||||
}
|
||||
}
|
||||
|
||||
private static List<ISerializableEntity> currentTargets = new List<ISerializableEntity>();
|
||||
private static readonly ThreadLocal<List<ISerializableEntity>> currentTargets = new ThreadLocal<List<ISerializableEntity>>(() => 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)
|
||||
{
|
||||
currentTargets.Clear();
|
||||
targets.Clear();
|
||||
effect.SetUser(user);
|
||||
if (effect.HasTargetType(StatusEffect.TargetType.UseTarget))
|
||||
{
|
||||
if (targetItem != null)
|
||||
{
|
||||
currentTargets.AddRange(targetItem.AllPropertyObjects);
|
||||
targets.AddRange(targetItem.AllPropertyObjects);
|
||||
}
|
||||
if (structure != null)
|
||||
{
|
||||
currentTargets.Add(structure);
|
||||
targets.Add(structure);
|
||||
}
|
||||
if (character != null)
|
||||
{
|
||||
currentTargets.Add(character);
|
||||
targets.Add(character);
|
||||
}
|
||||
effect.Apply(actionType, deltaTime, item, currentTargets);
|
||||
effect.Apply(actionType, deltaTime, item, targets);
|
||||
}
|
||||
else if (effect.HasTargetType(StatusEffect.TargetType.Character))
|
||||
{
|
||||
currentTargets.Add(user);
|
||||
effect.Apply(actionType, deltaTime, item, currentTargets);
|
||||
targets.Add(user);
|
||||
effect.Apply(actionType, deltaTime, item, targets);
|
||||
}
|
||||
else if (effect.HasTargetType(StatusEffect.TargetType.Limb))
|
||||
{
|
||||
currentTargets.Add(limb);
|
||||
effect.Apply(actionType, deltaTime, item, currentTargets);
|
||||
targets.Add(limb);
|
||||
effect.Apply(actionType, deltaTime, item, targets);
|
||||
}
|
||||
|
||||
#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 currentTargets)
|
||||
foreach (ISerializableEntity target in targets)
|
||||
{
|
||||
if (target is not Door door) { continue; }
|
||||
if (!door.CanBeWelded || !door.Item.IsInteractable(user)) { continue; }
|
||||
|
||||
@@ -949,7 +949,8 @@ 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)
|
||||
{
|
||||
foreach (ItemComponent ic in item.Components)
|
||||
// Use ToArray() snapshot for thread-safe iteration
|
||||
foreach (ItemComponent ic in item.Components.ToArray())
|
||||
{
|
||||
if (ic.statusEffectLists == null || !ic.statusEffectLists.TryGetValue(ActionType.OnBroken, out List<StatusEffect> brokenEffects)) { continue; }
|
||||
foreach (var brokenEffect in brokenEffects)
|
||||
|
||||
@@ -890,7 +890,8 @@ namespace Barotrauma.Items.Components
|
||||
RelatedItem containableItem = FindContainableItem(containedItem);
|
||||
if (containableItem != null && containableItem.SetActive)
|
||||
{
|
||||
foreach (var ic in containedItem.Components)
|
||||
// Use ToArray() snapshot for thread-safe iteration
|
||||
foreach (var ic in containedItem.Components.ToArray())
|
||||
{
|
||||
ic.IsActive = active;
|
||||
}
|
||||
@@ -1016,7 +1017,11 @@ namespace Barotrauma.Items.Components
|
||||
if (flippedX ^ flippedY) { rotation = -rotation; }
|
||||
rotation += -item.RotationRad;
|
||||
}
|
||||
contained.Item.body.FarseerBody.SetTransformIgnoreContacts(ref simPos, rotation);
|
||||
// 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.UpdateDrawPosition(interpolate: false);
|
||||
}
|
||||
catch (Exception e)
|
||||
|
||||
@@ -1,17 +1,19 @@
|
||||
using System.Collections.Generic;
|
||||
using System.Collections.Concurrent;
|
||||
using System.Collections.Generic;
|
||||
using System.Xml.Linq;
|
||||
|
||||
namespace Barotrauma.Items.Components
|
||||
{
|
||||
partial class Ladder : ItemComponent
|
||||
{
|
||||
public static List<Ladder> List { get; } = new List<Ladder>();
|
||||
private static readonly ConcurrentDictionary<Ladder, byte> _ladderDict = new ConcurrentDictionary<Ladder, byte>();
|
||||
public static IEnumerable<Ladder> List => _ladderDict.Keys;
|
||||
|
||||
public Ladder(Item item, ContentXElement element)
|
||||
: base(item, element)
|
||||
{
|
||||
InitProjSpecific(element);
|
||||
List.Add(this);
|
||||
_ladderDict.TryAdd(this, 0);
|
||||
}
|
||||
|
||||
partial void InitProjSpecific(ContentXElement element);
|
||||
@@ -28,7 +30,7 @@ namespace Barotrauma.Items.Components
|
||||
{
|
||||
base.RemoveComponentSpecific();
|
||||
RemoveProjSpecific();
|
||||
List.Remove(this);
|
||||
_ladderDict.TryRemove(this, out _);
|
||||
}
|
||||
|
||||
partial void RemoveProjSpecific();
|
||||
|
||||
@@ -8,6 +8,7 @@ using System.ComponentModel;
|
||||
using System.Globalization;
|
||||
using System.Linq;
|
||||
using System.Xml.Linq;
|
||||
using System.Linq;
|
||||
|
||||
namespace Barotrauma.Items.Components
|
||||
{
|
||||
@@ -688,12 +689,14 @@ namespace Barotrauma.Items.Components
|
||||
|
||||
item.SendSignal(new Signal(MathHelper.ToDegrees(targetRotation).ToString("G", CultureInfo.InvariantCulture), sender: User), positionOut);
|
||||
|
||||
for (int i = item.LastSentSignalRecipients.Count - 1; i >= 0; i--)
|
||||
// Use ToList() snapshot for thread-safe iteration
|
||||
var signalRecipients = item.LastSentSignalRecipients.ToList();
|
||||
for (int i = signalRecipients.Count - 1; i >= 0; i--)
|
||||
{
|
||||
if (item.LastSentSignalRecipients[i].Item.Condition <= 0.0f || item.LastSentSignalRecipients[i].IsPower) { continue; }
|
||||
if (item.LastSentSignalRecipients[i].Item.Prefab.FocusOnSelected)
|
||||
if (signalRecipients[i].Item.Condition <= 0.0f || signalRecipients[i].IsPower) { continue; }
|
||||
if (signalRecipients[i].Item.Prefab.FocusOnSelected)
|
||||
{
|
||||
return item.LastSentSignalRecipients[i].Item;
|
||||
return signalRecipients[i].Item;
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -1,14 +1,17 @@
|
||||
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
|
||||
{
|
||||
public static List<Sonar> SonarList = new List<Sonar>();
|
||||
private static readonly ConcurrentDictionary<Sonar, byte> _sonarDict = new ConcurrentDictionary<Sonar, byte>();
|
||||
public static IEnumerable<Sonar> SonarList => _sonarDict.Keys;
|
||||
|
||||
public enum Mode
|
||||
{
|
||||
@@ -169,7 +172,7 @@ namespace Barotrauma.Items.Components
|
||||
IsActive = true;
|
||||
InitProjSpecific(element);
|
||||
CurrentMode = Mode.Passive;
|
||||
SonarList.Add(this);
|
||||
_sonarDict.TryAdd(this, 0);
|
||||
}
|
||||
|
||||
partial void InitProjSpecific(ContentXElement element);
|
||||
@@ -291,13 +294,15 @@ namespace Barotrauma.Items.Components
|
||||
return currentPingIndex != -1 && (character == null || characterUsable);
|
||||
}
|
||||
|
||||
private static readonly Dictionary<string, List<Character>> targetGroups = new Dictionary<string, List<Character>>();
|
||||
private static readonly ThreadLocal<Dictionary<string, List<Character>>> targetGroups =
|
||||
new ThreadLocal<Dictionary<string, List<Character>>>(() => new Dictionary<string, List<Character>>());
|
||||
|
||||
public override bool CrewAIOperate(float deltaTime, Character character, AIObjectiveOperateItem objective)
|
||||
{
|
||||
if (currentMode == Mode.Passive || !aiPingCheckPending) { return false; }
|
||||
|
||||
foreach (List<Character> targetGroup in targetGroups.Values)
|
||||
var groups = targetGroups.Value;
|
||||
foreach (List<Character> targetGroup in groups.Values)
|
||||
{
|
||||
targetGroup.Clear();
|
||||
}
|
||||
@@ -310,14 +315,14 @@ namespace Barotrauma.Items.Components
|
||||
|
||||
#warning This is not the best key for a dictionary.
|
||||
string directionName = GetDirectionName(c.WorldPosition - item.WorldPosition).Value;
|
||||
if (!targetGroups.ContainsKey(directionName))
|
||||
if (!groups.ContainsKey(directionName))
|
||||
{
|
||||
targetGroups.Add(directionName, new List<Character>());
|
||||
groups.Add(directionName, new List<Character>());
|
||||
}
|
||||
targetGroups[directionName].Add(c);
|
||||
groups[directionName].Add(c);
|
||||
}
|
||||
|
||||
foreach (KeyValuePair<string, List<Character>> targetGroup in targetGroups)
|
||||
foreach (KeyValuePair<string, List<Character>> targetGroup in groups)
|
||||
{
|
||||
if (!targetGroup.Value.Any()) { continue; }
|
||||
string dialogTag = "DialogSonarTarget";
|
||||
@@ -401,7 +406,7 @@ namespace Barotrauma.Items.Components
|
||||
|
||||
MineralClusters = null;
|
||||
#endif
|
||||
SonarList.Remove(this);
|
||||
_sonarDict.TryRemove(this, out _);
|
||||
}
|
||||
|
||||
|
||||
|
||||
@@ -637,7 +637,7 @@ namespace Barotrauma.Items.Components
|
||||
|
||||
if (pathFinder == null)
|
||||
{
|
||||
pathFinder = new PathFinder(WayPoint.WayPointList, false)
|
||||
pathFinder = new PathFinder(WayPoint.WayPointList.ToList(), false)
|
||||
{
|
||||
GetNodePenalty = GetNodePenalty
|
||||
};
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
using Barotrauma.Extensions;
|
||||
using Microsoft.Xna.Framework;
|
||||
using System;
|
||||
using System.Collections.Concurrent;
|
||||
using System.Collections.Generic;
|
||||
using System.Linq;
|
||||
using System.Xml.Linq;
|
||||
@@ -13,10 +14,12 @@ namespace Barotrauma.Items.Components
|
||||
|
||||
private readonly HashSet<Connection> signalConnections = new HashSet<Connection>();
|
||||
|
||||
private readonly Dictionary<Connection, bool> connectionDirty = new Dictionary<Connection, bool>();
|
||||
private readonly ConcurrentDictionary<Connection, bool> connectionDirty = new ConcurrentDictionary<Connection, bool>();
|
||||
|
||||
//a list of connections a given connection is connected to, either directly or via other power transfer components
|
||||
private readonly Dictionary<Connection, HashSet<Connection>> connectedRecipients = new Dictionary<Connection, HashSet<Connection>>();
|
||||
//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 float overloadCooldownTimer;
|
||||
private const float OverloadCooldown = 5.0f;
|
||||
@@ -132,7 +135,7 @@ namespace Barotrauma.Items.Components
|
||||
|
||||
partial void InitProjectSpecific(XElement element);
|
||||
|
||||
private static readonly HashSet<PowerTransfer> recipientsToRefresh = new HashSet<PowerTransfer>();
|
||||
private static readonly System.Collections.Concurrent.ConcurrentDictionary<PowerTransfer, byte> _recipientsToRefresh = new System.Collections.Concurrent.ConcurrentDictionary<PowerTransfer, byte>();
|
||||
public override void UpdateBroken(float deltaTime, Camera cam)
|
||||
{
|
||||
base.UpdateBroken(deltaTime, cam);
|
||||
@@ -144,20 +147,21 @@ namespace Barotrauma.Items.Components
|
||||
powerLoad = 0.0f;
|
||||
currPowerConsumption = 0.0f;
|
||||
SetAllConnectionsDirty();
|
||||
recipientsToRefresh.Clear();
|
||||
foreach (HashSet<Connection> recipientList in connectedRecipients.Values)
|
||||
_recipientsToRefresh.Clear();
|
||||
// Take snapshot for thread-safe iteration (no locks needed with ConcurrentDictionary)
|
||||
foreach (var recipientDict in connectedRecipients.Values)
|
||||
{
|
||||
foreach (Connection c in recipientList)
|
||||
foreach (Connection c in recipientDict.Keys)
|
||||
{
|
||||
if (c.Item == item) { continue; }
|
||||
var recipientPowerTransfer = c.Item.GetComponent<PowerTransfer>();
|
||||
if (recipientPowerTransfer != null)
|
||||
{
|
||||
recipientsToRefresh.Add(recipientPowerTransfer);
|
||||
_recipientsToRefresh.TryAdd(recipientPowerTransfer, 0);
|
||||
}
|
||||
}
|
||||
}
|
||||
foreach (PowerTransfer recipientPowerTransfer in recipientsToRefresh)
|
||||
foreach (PowerTransfer recipientPowerTransfer in _recipientsToRefresh.Keys)
|
||||
{
|
||||
recipientPowerTransfer.SetAllConnectionsDirty();
|
||||
recipientPowerTransfer.RefreshConnections();
|
||||
@@ -304,58 +308,56 @@ namespace Barotrauma.Items.Components
|
||||
protected void RefreshConnections()
|
||||
{
|
||||
var connections = item.Connections;
|
||||
foreach (Connection c in connections)
|
||||
if (connections == null) { return; }
|
||||
|
||||
// Take a snapshot of connections for thread-safe iteration
|
||||
var connectionSnapshot = connections.ToList();
|
||||
foreach (Connection c in connectionSnapshot)
|
||||
{
|
||||
if (!connectionDirty.ContainsKey(c))
|
||||
if (!connectionDirty.TryGetValue(c, out bool isDirty))
|
||||
{
|
||||
connectionDirty[c] = true;
|
||||
isDirty = true;
|
||||
}
|
||||
else if (!connectionDirty[c])
|
||||
|
||||
if (!isDirty)
|
||||
{
|
||||
continue;
|
||||
}
|
||||
|
||||
//find all connections that are connected to this one (directly or via another PowerTransfer)
|
||||
HashSet<Connection> tempConnected;
|
||||
if (!connectedRecipients.ContainsKey(c))
|
||||
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)
|
||||
{
|
||||
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; }
|
||||
}
|
||||
var pt = recipient.Item.GetComponent<PowerTransfer>();
|
||||
if (pt != null) { pt.connectionDirty[recipient] = true; }
|
||||
}
|
||||
|
||||
tempConnected.Add(c);
|
||||
tempConnected.TryAdd(c, 0);
|
||||
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)
|
||||
foreach (Connection recipient in tempConnected)
|
||||
// Take snapshot for thread-safe iteration (no locks needed)
|
||||
var tempConnectedSnapshot = tempConnected.Keys.ToList();
|
||||
foreach (Connection recipient in tempConnectedSnapshot)
|
||||
{
|
||||
if (recipient == c) { continue; }
|
||||
var recipientPowerTransfer = recipient.Item.GetComponent<PowerTransfer>();
|
||||
if (recipientPowerTransfer == null) { continue; }
|
||||
if (!recipientPowerTransfer.connectedRecipients.ContainsKey(recipient))
|
||||
|
||||
var recipientSet = recipientPowerTransfer.connectedRecipients.GetOrAdd(recipient, _ => new ConcurrentDictionary<Connection, byte>());
|
||||
recipientSet.Clear();
|
||||
foreach (var connection in tempConnectedSnapshot)
|
||||
{
|
||||
recipientPowerTransfer.connectedRecipients.Add(recipient, new HashSet<Connection>());
|
||||
}
|
||||
else
|
||||
{
|
||||
recipientPowerTransfer.connectedRecipients[recipient].Clear();
|
||||
}
|
||||
foreach (var connection in tempConnected)
|
||||
{
|
||||
recipientPowerTransfer.connectedRecipients[recipient].Add(connection);
|
||||
recipientSet.TryAdd(connection, 0);
|
||||
}
|
||||
recipientPowerTransfer.connectionDirty[recipient] = false;
|
||||
}
|
||||
@@ -364,19 +366,20 @@ namespace Barotrauma.Items.Components
|
||||
}
|
||||
}
|
||||
|
||||
//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)
|
||||
//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)
|
||||
{
|
||||
var recipients = c.Recipients;
|
||||
// Take snapshot for thread-safe iteration
|
||||
var recipients = c.Recipients.ToList();
|
||||
|
||||
foreach (Connection recipient in recipients)
|
||||
{
|
||||
if (recipient == null || connected.Contains(recipient)) { continue; }
|
||||
if (recipient == null || connected.ContainsKey(recipient)) { continue; }
|
||||
|
||||
Item it = recipient.Item;
|
||||
if (it == null || it.Condition <= 0.0f) { continue; }
|
||||
|
||||
connected.Add(recipient);
|
||||
connected.TryAdd(recipient, 0);
|
||||
|
||||
var powerTransfer = it.GetComponent<PowerTransfer>();
|
||||
if (powerTransfer != null && powerTransfer.CanTransfer && powerTransfer.IsActive)
|
||||
@@ -394,10 +397,14 @@ namespace Barotrauma.Items.Components
|
||||
connectionDirty[c] = true;
|
||||
if (c.IsPower)
|
||||
{
|
||||
ChangedConnections.Add(c);
|
||||
MarkConnectionChanged(c);
|
||||
if (connectedRecipients.TryGetValue(c, out var recipients))
|
||||
{
|
||||
recipients.Where(c => c.IsPower).ForEach(c => ChangedConnections.Add(c));
|
||||
// No lock needed - ConcurrentDictionary.Keys is thread-safe
|
||||
foreach (var conn in recipients.Keys.Where(conn => conn.IsPower))
|
||||
{
|
||||
MarkConnectionChanged(conn);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -410,10 +417,14 @@ namespace Barotrauma.Items.Components
|
||||
connectionDirty[connection] = true;
|
||||
if (connection.IsPower)
|
||||
{
|
||||
ChangedConnections.Add(connection);
|
||||
MarkConnectionChanged(connection);
|
||||
if (connectedRecipients.TryGetValue(connection, out var recipients))
|
||||
{
|
||||
recipients.Where(c => c.IsPower).ForEach(c => ChangedConnections.Add(c));
|
||||
// No lock needed - ConcurrentDictionary.Keys is thread-safe
|
||||
foreach (var conn in recipients.Keys.Where(conn => conn.IsPower))
|
||||
{
|
||||
MarkConnectionChanged(conn);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -452,16 +463,19 @@ namespace Barotrauma.Items.Components
|
||||
public override void ReceiveSignal(Signal signal, Connection connection)
|
||||
{
|
||||
if (item.Condition <= 0.0f || connection.IsPower) { return; }
|
||||
if (!connectedRecipients.ContainsKey(connection)) { return; }
|
||||
if (!connectedRecipients.TryGetValue(connection, out var recipients)) { return; }
|
||||
if (!signalConnections.Contains(connection)) { return; }
|
||||
|
||||
foreach (Connection recipient in connectedRecipients[connection])
|
||||
// No lock needed - ConcurrentDictionary.Keys is thread-safe
|
||||
// Use ToList() snapshot for thread-safe iteration
|
||||
foreach (Connection recipient in recipients.Keys.ToList())
|
||||
{
|
||||
if (recipient.Item == item || recipient.Item == signal.source) { continue; }
|
||||
|
||||
signal.source?.LastSentSignalRecipients.Add(recipient);
|
||||
|
||||
foreach (ItemComponent ic in recipient.Item.Components)
|
||||
// Use ToArray() snapshot for thread-safe iteration
|
||||
foreach (ItemComponent ic in recipient.Item.Components.ToArray())
|
||||
{
|
||||
//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
|
||||
@@ -471,7 +485,8 @@ namespace Barotrauma.Items.Components
|
||||
|
||||
if (recipient.Effects != null && signal.value != "0" && !string.IsNullOrEmpty(signal.value))
|
||||
{
|
||||
foreach (StatusEffect effect in recipient.Effects)
|
||||
// Use ToArray() snapshot for thread-safe iteration
|
||||
foreach (StatusEffect effect in recipient.Effects.ToArray())
|
||||
{
|
||||
recipient.Item.ApplyStatusEffect(effect, ActionType.OnUse, 1.0f);
|
||||
}
|
||||
@@ -484,7 +499,7 @@ namespace Barotrauma.Items.Components
|
||||
base.RemoveComponentSpecific();
|
||||
connectedRecipients?.Clear();
|
||||
connectionDirty?.Clear();
|
||||
recipientsToRefresh.Clear();
|
||||
_recipientsToRefresh.Clear();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,4 +1,6 @@
|
||||
using System;
|
||||
using System.Collections.Concurrent;
|
||||
using System.Threading;
|
||||
using Microsoft.Xna.Framework;
|
||||
using System.Collections.Generic;
|
||||
using System.Linq;
|
||||
@@ -62,17 +64,77 @@ namespace Barotrauma.Items.Components
|
||||
protected const float UpdateInterval = (float)Timing.Step;
|
||||
|
||||
/// <summary>
|
||||
/// List of all powered ItemComponents
|
||||
/// List of all powered ItemComponents (thread-safe)
|
||||
/// </summary>
|
||||
private static readonly List<Powered> poweredList = new List<Powered>();
|
||||
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;
|
||||
|
||||
public static IEnumerable<Powered> PoweredList
|
||||
{
|
||||
get { return 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);
|
||||
}
|
||||
|
||||
public static readonly HashSet<Connection> ChangedConnections = new HashSet<Connection>();
|
||||
/// <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 readonly static Dictionary<int, GridInfo> Grids = new Dictionary<int, GridInfo>();
|
||||
/// <summary>
|
||||
/// Thread-safe grid dictionary
|
||||
/// </summary>
|
||||
public readonly static ConcurrentDictionary<int, GridInfo> Grids = new ConcurrentDictionary<int, GridInfo>();
|
||||
|
||||
/// <summary>
|
||||
/// The amount of power currently consumed by the item. Negative values mean that the item is providing power to connected items
|
||||
@@ -209,7 +271,8 @@ namespace Barotrauma.Items.Components
|
||||
public Powered(Item item, ContentXElement element)
|
||||
: base(item, element)
|
||||
{
|
||||
poweredList.Add(this);
|
||||
_poweredDict.TryAdd(this, 0);
|
||||
InvalidatePoweredListCache();
|
||||
InitProjectSpecific(element);
|
||||
}
|
||||
|
||||
@@ -322,17 +385,20 @@ 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 ChangedConnections)
|
||||
foreach (Connection c in changedSnapshot)
|
||||
{
|
||||
if (c.Grid != null)
|
||||
{
|
||||
Grids.Remove(c.Grid.ID);
|
||||
Grids.TryRemove(c.Grid.ID, out _);
|
||||
c.Grid = null;
|
||||
}
|
||||
}
|
||||
|
||||
foreach (Connection c in ChangedConnections)
|
||||
foreach (Connection c in changedSnapshot)
|
||||
{
|
||||
//Make sure the connection grid hasn't been resolved by another connection update
|
||||
//Ensure the connection has other connections
|
||||
@@ -346,7 +412,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)
|
||||
@@ -361,7 +427,7 @@ namespace Barotrauma.Items.Components
|
||||
|
||||
Grids.Clear();
|
||||
|
||||
foreach (Powered powered in poweredList)
|
||||
foreach (Powered powered in PoweredList)
|
||||
{
|
||||
if (powered.Item.Condition <= 0f) { continue; }
|
||||
|
||||
@@ -392,7 +458,7 @@ namespace Barotrauma.Items.Components
|
||||
}
|
||||
|
||||
//Clear changed connections after each update
|
||||
ChangedConnections.Clear();
|
||||
ClearChangedConnections();
|
||||
}
|
||||
|
||||
private static GridInfo PropagateGrid(Connection conn)
|
||||
@@ -422,8 +488,8 @@ namespace Barotrauma.Items.Components
|
||||
c.Grid = grid;
|
||||
grid.AddConnection(c);
|
||||
|
||||
//Add on recipients
|
||||
foreach (Connection otherC in c.Recipients)
|
||||
//Add on recipients - use ToList() snapshot for thread-safe iteration
|
||||
foreach (Connection otherC in c.Recipients.ToList())
|
||||
{
|
||||
//Only add valid connections
|
||||
if (otherC.Grid != grid && (otherC.Grid == null || !Grids.ContainsKey(otherC.Grid.ID)) && ValidPowerConnection(c, otherC))
|
||||
@@ -494,7 +560,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)
|
||||
@@ -730,7 +796,8 @@ namespace Barotrauma.Items.Components
|
||||
{
|
||||
if (item.Connections != null && powerIn != null)
|
||||
{
|
||||
foreach (Connection recipient in powerIn.Recipients)
|
||||
// Use ToList() snapshot for thread-safe iteration
|
||||
foreach (Connection recipient in powerIn.Recipients.ToList())
|
||||
{
|
||||
if (!recipient.IsPower || !recipient.IsOutput) { continue; }
|
||||
if (recipient.Item?.GetComponent<PowerContainer>() is PowerContainer battery)
|
||||
@@ -750,13 +817,14 @@ namespace Barotrauma.Items.Components
|
||||
{
|
||||
if (c.IsPower && c.Grid != null)
|
||||
{
|
||||
ChangedConnections.Add(c);
|
||||
MarkConnectionChanged(c);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
base.RemoveComponentSpecific();
|
||||
poweredList.Remove(this);
|
||||
_poweredDict.TryRemove(this, out _);
|
||||
InvalidatePoweredListCache();
|
||||
}
|
||||
}
|
||||
|
||||
@@ -780,9 +848,9 @@ namespace Barotrauma.Items.Components
|
||||
Connections.Remove(c);
|
||||
|
||||
//Remove the grid if it has no devices
|
||||
if (Connections.Count == 0 && Powered.Grids.ContainsKey(ID))
|
||||
if (Connections.Count == 0)
|
||||
{
|
||||
Powered.Grids.Remove(ID);
|
||||
Powered.Grids.TryRemove(ID, out _);
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -5,6 +5,7 @@ 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;
|
||||
@@ -72,7 +73,7 @@ namespace Barotrauma.Items.Components
|
||||
|
||||
public const float WaterDragCoefficient = 0.1f;
|
||||
|
||||
private readonly Queue<Impact> impactQueue = new Queue<Impact>();
|
||||
private readonly ConcurrentQueue<Impact> impactQueue = new ConcurrentQueue<Impact>();
|
||||
|
||||
private bool removePending;
|
||||
|
||||
@@ -850,9 +851,8 @@ namespace Barotrauma.Items.Components
|
||||
DisableProjectileCollisions();
|
||||
}
|
||||
}
|
||||
while (impactQueue.Count > 0)
|
||||
while (impactQueue.TryDequeue(out var impact))
|
||||
{
|
||||
var impact = impactQueue.Dequeue();
|
||||
HandleProjectileCollision(impact.Fixture, impact.Normal, impact.LinearVelocity);
|
||||
}
|
||||
|
||||
|
||||
@@ -457,7 +457,8 @@ namespace Barotrauma.Items.Components
|
||||
|
||||
item.SendSignal(conditionSignal, "condition_out");
|
||||
|
||||
foreach (var component in item.Components)
|
||||
// Use ToArray() snapshot for thread-safe iteration
|
||||
foreach (var component in item.Components.ToArray())
|
||||
{
|
||||
if (component is IDeteriorateUnderStress deteriorateUnderStress)
|
||||
{
|
||||
@@ -714,7 +715,8 @@ namespace Barotrauma.Items.Components
|
||||
#endif
|
||||
|
||||
if (LastActiveTime > Timing.TotalTime) { return true; }
|
||||
foreach (ItemComponent ic in item.Components)
|
||||
// Use ToArray() snapshot for thread-safe iteration
|
||||
foreach (ItemComponent ic in item.Components.ToArray())
|
||||
{
|
||||
if (ic is Fabricator || ic is Deconstructor)
|
||||
{
|
||||
@@ -762,7 +764,8 @@ namespace Barotrauma.Items.Components
|
||||
|
||||
private float GetDeteriorationDelayMultiplier()
|
||||
{
|
||||
foreach (ItemComponent ic in item.Components)
|
||||
// Use ToArray() snapshot for thread-safe iteration
|
||||
foreach (ItemComponent ic in item.Components.ToArray())
|
||||
{
|
||||
if (ic is Engine engine)
|
||||
{
|
||||
|
||||
@@ -227,13 +227,14 @@ namespace Barotrauma.Items.Components
|
||||
public void SetRecipientsDirty()
|
||||
{
|
||||
recipientsDirty = true;
|
||||
if (IsPower) { Powered.ChangedConnections.Add(this); }
|
||||
if (IsPower) { Powered.MarkConnectionChanged(this); }
|
||||
}
|
||||
|
||||
private void RefreshRecipients()
|
||||
{
|
||||
recipients.Clear();
|
||||
foreach (var wire in wires)
|
||||
// Use ToArray() snapshot for thread-safe iteration
|
||||
foreach (var wire in wires.ToArray())
|
||||
{
|
||||
Connection recipient = wire.OtherConnection(this);
|
||||
if (recipient != null) { recipients.Add(recipient); }
|
||||
@@ -272,8 +273,8 @@ namespace Barotrauma.Items.Components
|
||||
//Check if both connections belong to a larger grid
|
||||
if (prevOtherConnection.recipients.Count > 1 && recipients.Count > 1)
|
||||
{
|
||||
Powered.ChangedConnections.Add(prevOtherConnection);
|
||||
Powered.ChangedConnections.Add(this);
|
||||
Powered.MarkConnectionChanged(prevOtherConnection);
|
||||
Powered.MarkConnectionChanged(this);
|
||||
}
|
||||
else if (recipients.Count > 1)
|
||||
{
|
||||
@@ -289,7 +290,7 @@ namespace Barotrauma.Items.Components
|
||||
else if (Grid.Connections.Count == 2)
|
||||
{
|
||||
//Delete the grid as these were the only 2 devices
|
||||
Powered.Grids.Remove(Grid.ID);
|
||||
Powered.Grids.TryRemove(Grid.ID, out _);
|
||||
Grid = null;
|
||||
prevOtherConnection.Grid = null;
|
||||
}
|
||||
@@ -330,8 +331,8 @@ namespace Barotrauma.Items.Components
|
||||
else
|
||||
{
|
||||
//Flag change so that proper grids can be formed
|
||||
Powered.ChangedConnections.Add(this);
|
||||
Powered.ChangedConnections.Add(otherConnection);
|
||||
Powered.MarkConnectionChanged(this);
|
||||
Powered.MarkConnectionChanged(otherConnection);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -344,11 +345,13 @@ namespace Barotrauma.Items.Components
|
||||
{
|
||||
LastSentSignal = signal;
|
||||
enumeratingWires = true;
|
||||
foreach (var wire in wires)
|
||||
// Use ToArray() snapshot for thread-safe iteration
|
||||
foreach (var wire in wires.ToArray())
|
||||
{
|
||||
Connection recipient = wire.OtherConnection(this);
|
||||
if (recipient == null) { continue; }
|
||||
if (recipient.item == this.item || signal.source?.LastSentSignalRecipients.LastOrDefault() == recipient) { continue; }
|
||||
List<Connection> LastSentSignalRecipientsCopy = signal.source?.LastSentSignalRecipients.ToList();
|
||||
if (recipient.item == this.item || LastSentSignalRecipientsCopy.LastOrDefault() == recipient) { continue; }
|
||||
|
||||
signal.source?.LastSentSignalRecipients.Add(recipient);
|
||||
#if CLIENT
|
||||
@@ -357,12 +360,12 @@ namespace Barotrauma.Items.Components
|
||||
SendSignalIntoConnection(signal, recipient);
|
||||
}
|
||||
|
||||
foreach (CircuitBoxConnection connection in CircuitBoxConnections)
|
||||
foreach (CircuitBoxConnection connection in CircuitBoxConnections.ToArray())
|
||||
{
|
||||
connection.ReceiveSignal(signal);
|
||||
}
|
||||
enumeratingWires = false;
|
||||
foreach (var removedWire in removedWires)
|
||||
foreach (var removedWire in removedWires.ToArray())
|
||||
{
|
||||
wires.Remove(removedWire);
|
||||
}
|
||||
@@ -373,14 +376,16 @@ namespace Barotrauma.Items.Components
|
||||
{
|
||||
conn.LastReceivedSignal = signal;
|
||||
|
||||
foreach (ItemComponent ic in conn.item.Components)
|
||||
// Use ToArray() snapshot for thread-safe iteration
|
||||
foreach (ItemComponent ic in conn.item.Components.ToArray())
|
||||
{
|
||||
ic.ReceiveSignal(signal, conn);
|
||||
}
|
||||
|
||||
if (conn.Effects == null || signal.value == "0") { return; }
|
||||
|
||||
foreach (StatusEffect effect in conn.Effects)
|
||||
// Use ToArray() snapshot for thread-safe iteration
|
||||
foreach (StatusEffect effect in conn.Effects.ToArray())
|
||||
{
|
||||
conn.Item.ApplyStatusEffect(effect, ActionType.OnUse, (float)Timing.Step);
|
||||
}
|
||||
@@ -390,13 +395,15 @@ namespace Barotrauma.Items.Components
|
||||
{
|
||||
if (IsPower && Grid != null)
|
||||
{
|
||||
Powered.ChangedConnections.Add(this);
|
||||
foreach (Connection c in recipients)
|
||||
Powered.MarkConnectionChanged(this);
|
||||
// Use ToArray() snapshot for thread-safe iteration
|
||||
foreach (Connection c in recipients.ToArray())
|
||||
{
|
||||
Powered.ChangedConnections.Add(c);
|
||||
Powered.MarkConnectionChanged(c);
|
||||
}
|
||||
}
|
||||
foreach (var wire in wires)
|
||||
// Use ToArray() snapshot for thread-safe iteration
|
||||
foreach (var wire in wires.ToArray())
|
||||
{
|
||||
wire.RemoveConnection(this);
|
||||
recipientsDirty = true;
|
||||
@@ -404,7 +411,7 @@ namespace Barotrauma.Items.Components
|
||||
|
||||
if (enumeratingWires)
|
||||
{
|
||||
foreach (var wire in wires)
|
||||
foreach (var wire in wires.ToArray())
|
||||
{
|
||||
removedWires.Add(wire);
|
||||
}
|
||||
@@ -447,7 +454,8 @@ namespace Barotrauma.Items.Components
|
||||
{
|
||||
XElement newElement = new XElement(IsOutput ? "output" : "input", new XAttribute("name", Name));
|
||||
|
||||
foreach (var wire in wires.OrderBy(w => w.Item.ID))
|
||||
// Use ToArray() snapshot before OrderBy for thread-safe iteration
|
||||
foreach (var wire in wires.ToArray().OrderBy(w => w.Item.ID))
|
||||
{
|
||||
newElement.Add(new XElement("link",
|
||||
new XAttribute("w", wire.Item.ID.ToString()),
|
||||
|
||||
+13
-7
@@ -148,14 +148,16 @@ namespace Barotrauma.Items.Components
|
||||
Vector2 wireNodeOffset = item.Submarine == null ? Vector2.Zero : item.Submarine.HiddenSubPosition + amount;
|
||||
foreach (Connection c in Connections)
|
||||
{
|
||||
foreach (Wire wire in c.Wires)
|
||||
// Use ToArray() snapshot for thread-safe iteration
|
||||
foreach (Wire wire in c.Wires.ToArray())
|
||||
{
|
||||
if (wire == null) { continue; }
|
||||
TryMoveWire(wire);
|
||||
}
|
||||
}
|
||||
|
||||
foreach (var wire in DisconnectedWires)
|
||||
// Use ToList() snapshot for thread-safe iteration
|
||||
foreach (var wire in DisconnectedWires.ToList())
|
||||
{
|
||||
TryMoveWire(wire);
|
||||
}
|
||||
@@ -387,7 +389,7 @@ namespace Barotrauma.Items.Components
|
||||
}
|
||||
foreach (var connection in Connections)
|
||||
{
|
||||
Powered.ChangedConnections.Remove(connection);
|
||||
Powered.UnmarkConnectionChanged(connection);
|
||||
connection.Recipients.Clear();
|
||||
}
|
||||
Connections.Clear();
|
||||
@@ -412,15 +414,19 @@ namespace Barotrauma.Items.Components
|
||||
msg.WriteByte((byte)Connections.Count);
|
||||
foreach (Connection connection in Connections)
|
||||
{
|
||||
msg.WriteVariableUInt32((uint)connection.Wires.Count);
|
||||
foreach (Wire wire in connection.Wires)
|
||||
// Use ToArray() snapshot for thread-safe iteration
|
||||
var wiresSnapshot = connection.Wires.ToArray();
|
||||
msg.WriteVariableUInt32((uint)wiresSnapshot.Length);
|
||||
foreach (Wire wire in wiresSnapshot)
|
||||
{
|
||||
msg.WriteUInt16(wire?.Item == null ? (ushort)0 : wire.Item.ID);
|
||||
}
|
||||
}
|
||||
|
||||
msg.WriteUInt16((ushort)DisconnectedWires.Count);
|
||||
foreach (Wire disconnectedWire in DisconnectedWires)
|
||||
// Use ToList() snapshot for thread-safe iteration
|
||||
var disconnectedSnapshot = DisconnectedWires.ToList();
|
||||
msg.WriteUInt16((ushort)disconnectedSnapshot.Count);
|
||||
foreach (Wire disconnectedWire in disconnectedSnapshot)
|
||||
{
|
||||
msg.WriteUInt16(disconnectedWire.Item.ID);
|
||||
}
|
||||
|
||||
+19
-11
@@ -1,6 +1,8 @@
|
||||
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
|
||||
@@ -25,7 +27,8 @@ namespace Barotrauma.Items.Components
|
||||
private int signalQueueSize;
|
||||
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;
|
||||
|
||||
@@ -40,7 +43,8 @@ namespace Barotrauma.Items.Components
|
||||
delay = value;
|
||||
delayTicks = (int)(delay / Timing.Step);
|
||||
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)
|
||||
{
|
||||
if (signalQueue.Count == 0)
|
||||
if (signalQueue.IsEmpty)
|
||||
{
|
||||
IsActive = false;
|
||||
return;
|
||||
}
|
||||
|
||||
foreach (var val in signalQueue)
|
||||
// Use ToArray() snapshot for thread-safe iteration
|
||||
foreach (var val in signalQueue.ToArray())
|
||||
{
|
||||
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;
|
||||
item.SendSignal(new Signal(signalOut.Signal.value, sender: signalOut.Signal.sender, strength: signalOut.Signal.strength), "signal_out");
|
||||
if (signalOut.SendDuration <= 0)
|
||||
@@ -100,11 +104,15 @@ namespace Barotrauma.Items.Components
|
||||
{
|
||||
case "signal_in":
|
||||
if (signalQueue.Count >= signalQueueSize) { return; }
|
||||
if (ResetWhenSignalReceived) { prevQueuedSignal = null; signalQueue.Clear(); }
|
||||
if (ResetWhenDifferentSignalReceived && signalQueue.Count > 0 && signalQueue.Peek().Signal.value != signal.value)
|
||||
if (ResetWhenSignalReceived)
|
||||
{
|
||||
prevQueuedSignal = null;
|
||||
while (signalQueue.TryDequeue(out _)) { }
|
||||
}
|
||||
if (ResetWhenDifferentSignalReceived && signalQueue.TryPeek(out var peekSignal) && peekSignal.Signal.value != signal.value)
|
||||
{
|
||||
prevQueuedSignal = null;
|
||||
signalQueue.Clear();
|
||||
while (signalQueue.TryDequeue(out _)) { }
|
||||
}
|
||||
|
||||
if (prevQueuedSignal != null &&
|
||||
@@ -127,10 +135,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.Count > 0 && newDelay != Delay)
|
||||
if (!signalQueue.IsEmpty && newDelay != Delay)
|
||||
{
|
||||
prevQueuedSignal = null;
|
||||
signalQueue.Clear();
|
||||
while (signalQueue.TryDequeue(out _)) { }
|
||||
}
|
||||
Delay = newDelay;
|
||||
}
|
||||
|
||||
@@ -2,6 +2,7 @@
|
||||
using Microsoft.Xna.Framework;
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.Collections.Immutable;
|
||||
using System.Linq;
|
||||
using System.Xml.Linq;
|
||||
|
||||
@@ -31,7 +32,8 @@ namespace Barotrauma.Items.Components
|
||||
|
||||
private float thirdInverseMax = 0, loadEqnConstant = 0;
|
||||
|
||||
private static readonly Dictionary<string, string> connectionPairs = new Dictionary<string, string>
|
||||
// Thread-safe immutable dictionary for connection pairs (read-only after initialization)
|
||||
private static readonly ImmutableDictionary<string, string> connectionPairs = new Dictionary<string, string>
|
||||
{
|
||||
{ "power_in", "power_out"},
|
||||
{ "signal_in", "signal_out" },
|
||||
@@ -40,7 +42,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; } }
|
||||
|
||||
|
||||
@@ -2,6 +2,7 @@
|
||||
using Barotrauma.Networking;
|
||||
using Microsoft.Xna.Framework;
|
||||
using System;
|
||||
using System.Collections.Concurrent;
|
||||
using System.Collections.Generic;
|
||||
using System.Globalization;
|
||||
using System.Linq;
|
||||
@@ -13,7 +14,8 @@ namespace Barotrauma.Items.Components
|
||||
{
|
||||
partial class WifiComponent : ItemComponent, IServerSerializable, IClientSerializable
|
||||
{
|
||||
private static readonly List<WifiComponent> list = new List<WifiComponent>();
|
||||
private static readonly ConcurrentDictionary<WifiComponent, byte> _wifiDict = new ConcurrentDictionary<WifiComponent, byte>();
|
||||
private static IEnumerable<WifiComponent> AllWifiComponents => _wifiDict.Keys;
|
||||
|
||||
const int ChannelMemorySize = 10;
|
||||
|
||||
@@ -114,7 +116,7 @@ namespace Barotrauma.Items.Components
|
||||
public WifiComponent(Item item, ContentXElement element)
|
||||
: base (item, element)
|
||||
{
|
||||
list.Add(this);
|
||||
_wifiDict.TryAdd(this, 0);
|
||||
IsActive = true;
|
||||
}
|
||||
|
||||
@@ -159,7 +161,7 @@ namespace Barotrauma.Items.Components
|
||||
/// </summary>
|
||||
public IEnumerable<WifiComponent> GetReceiversInRange()
|
||||
{
|
||||
return list.Where(w => w != this && w.CanReceive(this));
|
||||
return AllWifiComponents.Where(w => w != this && w.CanReceive(this));
|
||||
}
|
||||
|
||||
public bool CanReceive(WifiComponent sender)
|
||||
@@ -188,7 +190,7 @@ namespace Barotrauma.Items.Components
|
||||
/// </summary>
|
||||
public IEnumerable<WifiComponent> GetTransmittersInRange()
|
||||
{
|
||||
return list.Where(w => w != this && w.CanTransmit(this));
|
||||
return AllWifiComponents.Where(w => w != this && w.CanTransmit(this));
|
||||
}
|
||||
|
||||
public bool CanTransmit(WifiComponent sender)
|
||||
@@ -278,7 +280,8 @@ namespace Barotrauma.Items.Components
|
||||
|
||||
if (signal.source != null)
|
||||
{
|
||||
foreach (Connection receiver in wifiComp.item.LastSentSignalRecipients)
|
||||
// Use ToList() snapshot for thread-safe iteration
|
||||
foreach (Connection receiver in wifiComp.item.LastSentSignalRecipients.ToList())
|
||||
{
|
||||
if (!signal.source.LastSentSignalRecipients.Contains(receiver))
|
||||
{
|
||||
@@ -369,7 +372,7 @@ namespace Barotrauma.Items.Components
|
||||
protected override void RemoveComponentSpecific()
|
||||
{
|
||||
base.RemoveComponentSpecific();
|
||||
list.Remove(this);
|
||||
_wifiDict.TryRemove(this, out _);
|
||||
}
|
||||
|
||||
public override XElement Save(XElement parentElement)
|
||||
|
||||
@@ -250,7 +250,8 @@ namespace Barotrauma.Items.Components
|
||||
{
|
||||
if (connections[0] != null && connections[1] != null)
|
||||
{
|
||||
foreach (ItemComponent ic in item.Components)
|
||||
// Use ToArray() snapshot for thread-safe iteration
|
||||
foreach (ItemComponent ic in item.Components.ToArray())
|
||||
{
|
||||
if (ic == this) { continue; }
|
||||
|
||||
@@ -723,11 +724,11 @@ namespace Barotrauma.Items.Components
|
||||
|
||||
if (item0 == null && item1 != null)
|
||||
{
|
||||
item0 = Item.ItemList.Find(it => it.GetComponent<ConnectionPanel>()?.DisconnectedWires.Contains(this) ?? false);
|
||||
item0 = Item.ItemList.FirstOrDefault(it => it.GetComponent<ConnectionPanel>()?.DisconnectedWires.Contains(this) ?? false);
|
||||
}
|
||||
else if (item0 != null && item1 == null)
|
||||
{
|
||||
item1 = Item.ItemList.Find(it => it.GetComponent<ConnectionPanel>()?.DisconnectedWires.Contains(this) ?? false);
|
||||
item1 = Item.ItemList.FirstOrDefault(it => it.GetComponent<ConnectionPanel>()?.DisconnectedWires.Contains(this) ?? false);
|
||||
}
|
||||
|
||||
if (item0 == null || item1 == null || nodes.Count == 0) { return; }
|
||||
|
||||
@@ -73,6 +73,11 @@ 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
|
||||
@@ -83,7 +88,7 @@ namespace Barotrauma.Items.Components
|
||||
{
|
||||
if (radius == value) { return; }
|
||||
radius = value;
|
||||
if (PhysicsBody != null) { RefreshPhysicsBodySize(); }
|
||||
if (PhysicsBody != null) { QueuePhysicsBodyRefresh(); }
|
||||
}
|
||||
}
|
||||
|
||||
@@ -97,7 +102,7 @@ namespace Barotrauma.Items.Components
|
||||
{
|
||||
if (width == value) { return; }
|
||||
width = value;
|
||||
if (PhysicsBody != null) { RefreshPhysicsBodySize(); }
|
||||
if (PhysicsBody != null) { QueuePhysicsBodyRefresh(); }
|
||||
}
|
||||
}
|
||||
|
||||
@@ -111,10 +116,28 @@ namespace Barotrauma.Items.Components
|
||||
{
|
||||
if (height == value) { return; }
|
||||
height = value;
|
||||
if (PhysicsBody != null) { RefreshPhysicsBodySize(); }
|
||||
if (PhysicsBody != null) { QueuePhysicsBodyRefresh(); }
|
||||
}
|
||||
}
|
||||
|
||||
/// <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;
|
||||
@@ -289,13 +312,18 @@ 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)
|
||||
{
|
||||
PhysicsBody.SetTransformIgnoreContacts(item.SimPosition + offset, -item.RotationRad);
|
||||
PhysicsBodyQueue.ExecuteOrDefer(() => capturedBody.SetTransformIgnoreContacts(capturedPos, capturedRot));
|
||||
}
|
||||
else
|
||||
{
|
||||
PhysicsBody.SetTransform(item.SimPosition + offset, -item.RotationRad);
|
||||
PhysicsBodyQueue.ExecuteOrDefer(() => capturedBody.SetTransform(capturedPos, capturedRot));
|
||||
}
|
||||
PhysicsBody.UpdateDrawPosition();
|
||||
}
|
||||
|
||||
@@ -381,7 +381,8 @@ namespace Barotrauma
|
||||
|
||||
if (Owner is not Item it) { return; }
|
||||
|
||||
foreach (var c in it.Components)
|
||||
// Use ToArray() snapshot for thread-safe iteration
|
||||
foreach (var c in it.Components.ToArray())
|
||||
{
|
||||
c.OnInventoryChanged();
|
||||
}
|
||||
|
||||
@@ -18,6 +18,13 @@ using System.Linq;
|
||||
using System.Xml.Linq;
|
||||
using static Barotrauma.CharacterHealth;
|
||||
using static Barotrauma.MedicalClinic;
|
||||
using Barotrauma.Extensions;
|
||||
using Barotrauma.MapCreatures.Behavior;
|
||||
using MoonSharp.Interpreter;
|
||||
using System.Collections.Immutable;
|
||||
using System.Threading;
|
||||
using Barotrauma.Abilities;
|
||||
using HarmonyLib;
|
||||
|
||||
#if CLIENT
|
||||
using Microsoft.Xna.Framework.Graphics;
|
||||
@@ -30,57 +37,172 @@ namespace Barotrauma
|
||||
#region Lists
|
||||
|
||||
/// <summary>
|
||||
/// A list of every item that exists somewhere in the world. Note that there can be a huge number of items in the list,
|
||||
/// and you probably shouldn't be enumerating it to find some that match some specific criteria (unless that's done very, very sparsely or during initialization).
|
||||
/// Thread-safe dictionary of all items by ID.
|
||||
/// </summary>
|
||||
public static readonly List<Item> ItemList = new List<Item>();
|
||||
private static readonly ConcurrentDictionary<ushort, Item> _itemDictionary = new ConcurrentDictionary<ushort, Item>();
|
||||
|
||||
private static readonly HashSet<Item> _dangerousItems = new HashSet<Item>();
|
||||
/// <summary>
|
||||
/// Provides thread-safe enumeration over all items.
|
||||
/// </summary>
|
||||
public static ICollection<Item> ItemList => _itemDictionary.Values;
|
||||
|
||||
/// <summary>
|
||||
/// Thread-safe item lookup by ID.
|
||||
/// </summary>
|
||||
public static Item GetItemById(ushort id)
|
||||
{
|
||||
_itemDictionary.TryGetValue(id, out var item);
|
||||
return item;
|
||||
}
|
||||
|
||||
// Thread-safe optimized item collections using Immutable + atomic swap pattern
|
||||
private static volatile ImmutableHashSet<Item> _dangerousItems = ImmutableHashSet<Item>.Empty;
|
||||
private static volatile ImmutableHashSet<Item> _repairableItems = ImmutableHashSet<Item>.Empty;
|
||||
private static volatile ImmutableHashSet<Item> _cleanableItems = ImmutableHashSet<Item>.Empty;
|
||||
private static volatile ImmutableHashSet<Item> _sonarVisibleItems = ImmutableHashSet<Item>.Empty;
|
||||
private static volatile ImmutableHashSet<Item> _turretTargetItems = ImmutableHashSet<Item>.Empty;
|
||||
private static volatile ImmutableHashSet<Item> _chairItems = ImmutableHashSet<Item>.Empty;
|
||||
|
||||
// DeconstructItems uses ConcurrentDictionary to simulate a thread-safe HashSet
|
||||
private static readonly ConcurrentDictionary<Item, byte> _deconstructItems = new ConcurrentDictionary<Item, byte>();
|
||||
|
||||
public static IReadOnlyCollection<Item> DangerousItems => _dangerousItems;
|
||||
|
||||
private static readonly List<Item> _repairableItems = new List<Item>();
|
||||
|
||||
/// <summary>
|
||||
/// Items that have one more more Repairable component
|
||||
/// </summary>
|
||||
public static IReadOnlyCollection<Item> RepairableItems => _repairableItems;
|
||||
|
||||
private static readonly List<Item> _cleanableItems = new List<Item>();
|
||||
|
||||
/// <summary>
|
||||
/// Items that may potentially need to be cleaned up (pickable, not attached to a wall, and not inside a valid container)
|
||||
/// </summary>
|
||||
public static IReadOnlyCollection<Item> CleanableItems => _cleanableItems;
|
||||
|
||||
private static readonly HashSet<Item> _deconstructItems = new HashSet<Item>();
|
||||
|
||||
/// <summary>
|
||||
/// Items that have been marked for deconstruction
|
||||
/// Items that have been marked for deconstruction. Thread-safe collection.
|
||||
/// </summary>
|
||||
public static HashSet<Item> DeconstructItems => _deconstructItems;
|
||||
|
||||
private static readonly List<Item> _sonarVisibleItems = new List<Item>();
|
||||
public static ICollection<Item> DeconstructItems => _deconstructItems.Keys;
|
||||
|
||||
/// <summary>
|
||||
/// Items whose <see cref="ItemPrefab.SonarSize"/> is larger than 0
|
||||
/// </summary>
|
||||
public static IReadOnlyCollection<Item> SonarVisibleItems => _sonarVisibleItems;
|
||||
|
||||
private static readonly List<Item> _turretTargetItems = new List<Item>();
|
||||
|
||||
/// <summary>
|
||||
/// Items whose <see cref="ItemPrefab.IsAITurretTarget"/> is true.
|
||||
/// </summary>
|
||||
public static IReadOnlyCollection<Item> TurretTargetItems => _turretTargetItems;
|
||||
|
||||
private static readonly List<Item> _chairItems = new List<Item>();
|
||||
|
||||
/// <summary>
|
||||
/// Items that have the tag <see cref="Tags.ChairItem"/>. Which is an oddly specific thing, but useful as an optimization for NPC AI.
|
||||
/// </summary>
|
||||
public static IReadOnlyCollection<Item> ChairItems => _chairItems;
|
||||
|
||||
#region Thread-safe collection helpers
|
||||
|
||||
/// <summary>
|
||||
/// Atomically adds an item to an immutable set using compare-and-swap.
|
||||
/// </summary>
|
||||
private static void AddToImmutableSet(ref ImmutableHashSet<Item> location, Item item)
|
||||
{
|
||||
ImmutableHashSet<Item> original, updated;
|
||||
do
|
||||
{
|
||||
original = location;
|
||||
updated = original.Add(item);
|
||||
if (ReferenceEquals(original, updated)) return; // Already exists
|
||||
}
|
||||
while (Interlocked.CompareExchange(ref location, updated, original) != original);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Atomically removes an item from an immutable set using compare-and-swap.
|
||||
/// </summary>
|
||||
private static void RemoveFromImmutableSet(ref ImmutableHashSet<Item> location, Item item)
|
||||
{
|
||||
ImmutableHashSet<Item> original, updated;
|
||||
do
|
||||
{
|
||||
original = location;
|
||||
updated = original.Remove(item);
|
||||
if (ReferenceEquals(original, updated)) return; // Doesn't exist
|
||||
}
|
||||
while (Interlocked.CompareExchange(ref location, updated, original) != original);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Marks an item for deconstruction (thread-safe).
|
||||
/// </summary>
|
||||
public static void MarkForDeconstruction(Item item)
|
||||
{
|
||||
_deconstructItems.TryAdd(item, 0);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Unmarks an item for deconstruction (thread-safe).
|
||||
/// </summary>
|
||||
public static void UnmarkForDeconstruction(Item item)
|
||||
{
|
||||
_deconstructItems.TryRemove(item, out _);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Checks if an item is marked for deconstruction (thread-safe).
|
||||
/// </summary>
|
||||
public static bool IsMarkedForDeconstruction(Item item)
|
||||
{
|
||||
return _deconstructItems.ContainsKey(item);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Clears all item collections (thread-safe). Used during unloading.
|
||||
/// </summary>
|
||||
public static void ClearAllItemCollections()
|
||||
{
|
||||
_itemDictionary.Clear();
|
||||
_dangerousItems = ImmutableHashSet<Item>.Empty;
|
||||
_repairableItems = ImmutableHashSet<Item>.Empty;
|
||||
_cleanableItems = ImmutableHashSet<Item>.Empty;
|
||||
_sonarVisibleItems = ImmutableHashSet<Item>.Empty;
|
||||
_turretTargetItems = ImmutableHashSet<Item>.Empty;
|
||||
_chairItems = ImmutableHashSet<Item>.Empty;
|
||||
_deconstructItems.Clear();
|
||||
while (_pendingConditionUpdates.TryDequeue(out _)) { }
|
||||
_cachedItemList = null;
|
||||
_cachedItemListVersion = -1;
|
||||
}
|
||||
|
||||
// Cached item list for indexed access (used by AI systems)
|
||||
private static volatile List<Item> _cachedItemList;
|
||||
private static volatile int _cachedItemListVersion = -1;
|
||||
private static volatile int _itemListVersion;
|
||||
|
||||
/// <summary>
|
||||
/// Gets a cached list snapshot of all items for indexed access.
|
||||
/// The list is refreshed when items are added or removed.
|
||||
/// Thread-safe but may return slightly stale data.
|
||||
/// </summary>
|
||||
public static List<Item> GetCachedItemList()
|
||||
{
|
||||
int currentVersion = _itemListVersion;
|
||||
if (_cachedItemList == null || _cachedItemListVersion != currentVersion)
|
||||
{
|
||||
_cachedItemList = _itemDictionary.Values.ToList();
|
||||
_cachedItemListVersion = currentVersion;
|
||||
}
|
||||
return _cachedItemList;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Called when items are added or removed to invalidate the cached list.
|
||||
/// </summary>
|
||||
private static void InvalidateCachedItemList()
|
||||
{
|
||||
Interlocked.Increment(ref _itemListVersion);
|
||||
}
|
||||
|
||||
#endregion
|
||||
|
||||
#endregion
|
||||
|
||||
public new ItemPrefab Prefab => base.Prefab as ItemPrefab;
|
||||
@@ -182,7 +304,12 @@ namespace Barotrauma
|
||||
|
||||
private bool transformDirty = true;
|
||||
|
||||
private static readonly List<Item> itemsWithPendingConditionUpdates = new List<Item>();
|
||||
private static readonly ConcurrentQueue<Item> _pendingConditionUpdates = new ConcurrentQueue<Item>();
|
||||
|
||||
/// <summary>
|
||||
/// Flag to avoid duplicate enqueue for pending condition updates.
|
||||
/// </summary>
|
||||
private volatile bool _hasPendingConditionUpdate;
|
||||
|
||||
private float lastSentCondition;
|
||||
private float sendConditionUpdateTimer;
|
||||
@@ -850,11 +977,11 @@ namespace Barotrauma
|
||||
isDangerous = value;
|
||||
if (!value)
|
||||
{
|
||||
_dangerousItems.Remove(this);
|
||||
RemoveFromImmutableSet(ref _dangerousItems, this);
|
||||
}
|
||||
else
|
||||
{
|
||||
_dangerousItems.Add(this);
|
||||
AddToImmutableSet(ref _dangerousItems, this);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -1403,12 +1530,13 @@ namespace Barotrauma
|
||||
}
|
||||
|
||||
InsertToList();
|
||||
ItemList.Add(this);
|
||||
if (Prefab.IsDangerous) { _dangerousItems.Add(this); }
|
||||
if (Repairables.Any()) { _repairableItems.Add(this); }
|
||||
if (Prefab.SonarSize > 0.0f) { _sonarVisibleItems.Add(this); }
|
||||
if (Prefab.IsAITurretTarget) { _turretTargetItems.Add(this); }
|
||||
if (Prefab.Tags.Contains(Barotrauma.Tags.ChairItem)) { _chairItems.Add(this); }
|
||||
_itemDictionary.TryAdd(ID, this);
|
||||
InvalidateCachedItemList();
|
||||
if (Prefab.IsDangerous) { AddToImmutableSet(ref _dangerousItems, this); }
|
||||
if (Repairables.Any()) { AddToImmutableSet(ref _repairableItems, this); }
|
||||
if (Prefab.SonarSize > 0.0f) { AddToImmutableSet(ref _sonarVisibleItems, this); }
|
||||
if (Prefab.IsAITurretTarget) { AddToImmutableSet(ref _turretTargetItems, this); }
|
||||
if (Prefab.Tags.Contains(Barotrauma.Tags.ChairItem)) { AddToImmutableSet(ref _chairItems, this); }
|
||||
CheckCleanable();
|
||||
|
||||
DebugConsole.Log("Created " + Name + " (" + ID + ")");
|
||||
@@ -1702,7 +1830,13 @@ namespace Barotrauma
|
||||
try
|
||||
{
|
||||
#endif
|
||||
body.SetTransformIgnoreContacts(simPosition, rotation, setPrevTransform);
|
||||
// Defer physics operation if in parallel context (Farseer is not thread-safe)
|
||||
var capturedBody = body;
|
||||
var capturedSimPos = simPosition;
|
||||
var capturedRotation = rotation;
|
||||
var capturedSetPrevTransform = setPrevTransform;
|
||||
PhysicsBodyQueue.ExecuteOrDefer(() =>
|
||||
capturedBody.SetTransformIgnoreContacts(capturedSimPos, capturedRotation, capturedSetPrevTransform));
|
||||
#if DEBUG
|
||||
}
|
||||
catch (Exception e)
|
||||
@@ -1759,14 +1893,11 @@ namespace Barotrauma
|
||||
Prefab.PreferredContainers.Any() &&
|
||||
(container == null || container.HasTag(Barotrauma.Tags.AllowCleanup)))
|
||||
{
|
||||
if (!_cleanableItems.Contains(this))
|
||||
{
|
||||
_cleanableItems.Add(this);
|
||||
}
|
||||
AddToImmutableSet(ref _cleanableItems, this);
|
||||
}
|
||||
else
|
||||
{
|
||||
_cleanableItems.Remove(this);
|
||||
RemoveFromImmutableSet(ref _cleanableItems, this);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1782,13 +1913,19 @@ namespace Barotrauma
|
||||
|
||||
if (ItemList != null && body != null)
|
||||
{
|
||||
// Defer physics operation if in parallel context (Farseer is not thread-safe)
|
||||
var capturedBody = body;
|
||||
var capturedNewPos = body.SimPosition + ConvertUnits.ToSimUnits(amount);
|
||||
var capturedRotation = body.Rotation;
|
||||
if (ignoreContacts)
|
||||
{
|
||||
body.SetTransformIgnoreContacts(body.SimPosition + ConvertUnits.ToSimUnits(amount), body.Rotation);
|
||||
PhysicsBodyQueue.ExecuteOrDefer(() =>
|
||||
capturedBody.SetTransformIgnoreContacts(capturedNewPos, capturedRotation));
|
||||
}
|
||||
else
|
||||
{
|
||||
body.SetTransform(body.SimPosition + ConvertUnits.ToSimUnits(amount), body.Rotation);
|
||||
PhysicsBodyQueue.ExecuteOrDefer(() =>
|
||||
capturedBody.SetTransform(capturedNewPos, capturedRotation));
|
||||
}
|
||||
}
|
||||
foreach (ItemComponent ic in components)
|
||||
@@ -2304,9 +2441,10 @@ namespace Barotrauma
|
||||
{
|
||||
needsConditionUpdate = true;
|
||||
}
|
||||
if (needsConditionUpdate && !itemsWithPendingConditionUpdates.Contains(this))
|
||||
if (needsConditionUpdate && !_hasPendingConditionUpdate)
|
||||
{
|
||||
itemsWithPendingConditionUpdates.Add(this);
|
||||
_hasPendingConditionUpdate = true;
|
||||
_pendingConditionUpdates.Enqueue(this);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -2330,10 +2468,11 @@ namespace Barotrauma
|
||||
{
|
||||
if (c.IsPower)
|
||||
{
|
||||
Powered.ChangedConnections.Add(c);
|
||||
foreach (Connection conn in c.Recipients)
|
||||
Powered.MarkConnectionChanged(c);
|
||||
// Use ToList() snapshot for thread-safe iteration
|
||||
foreach (Connection conn in c.Recipients.ToList())
|
||||
{
|
||||
Powered.ChangedConnections.Add(conn);
|
||||
Powered.MarkConnectionChanged(conn);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -2362,9 +2501,9 @@ namespace Barotrauma
|
||||
public void SendPendingNetworkUpdates()
|
||||
{
|
||||
if (!(GameMain.NetworkMember is { IsServer: true })) { return; }
|
||||
if (!itemsWithPendingConditionUpdates.Contains(this)) { return; }
|
||||
if (!_hasPendingConditionUpdate) { return; }
|
||||
SendPendingNetworkUpdatesInternal();
|
||||
itemsWithPendingConditionUpdates.Remove(this);
|
||||
_hasPendingConditionUpdate = false;
|
||||
}
|
||||
|
||||
private void SendPendingNetworkUpdatesInternal()
|
||||
@@ -2393,21 +2532,35 @@ namespace Barotrauma
|
||||
public static void UpdatePendingConditionUpdates(float deltaTime)
|
||||
{
|
||||
if (GameMain.NetworkMember is not { IsServer: true }) { return; }
|
||||
for (int i = 0; i < itemsWithPendingConditionUpdates.Count; i++)
|
||||
|
||||
int count = _pendingConditionUpdates.Count;
|
||||
for (int i = 0; i < count; i++)
|
||||
{
|
||||
var item = itemsWithPendingConditionUpdates[i];
|
||||
if (!_pendingConditionUpdates.TryDequeue(out var item)) { break; }
|
||||
|
||||
if (item == null || item.Removed)
|
||||
{
|
||||
itemsWithPendingConditionUpdates.RemoveAt(i--);
|
||||
item._hasPendingConditionUpdate = false;
|
||||
continue;
|
||||
}
|
||||
|
||||
if (item.Submarine is { Loading: true })
|
||||
{
|
||||
// Re-enqueue, still loading
|
||||
_pendingConditionUpdates.Enqueue(item);
|
||||
continue;
|
||||
}
|
||||
if (item.Submarine is { Loading: true }) { continue; }
|
||||
|
||||
item.sendConditionUpdateTimer -= deltaTime;
|
||||
if (item.sendConditionUpdateTimer <= 0.0f)
|
||||
{
|
||||
item.SendPendingNetworkUpdatesInternal();
|
||||
itemsWithPendingConditionUpdates.RemoveAt(i--);
|
||||
item._hasPendingConditionUpdate = false;
|
||||
}
|
||||
else
|
||||
{
|
||||
// Not ready yet, re-enqueue
|
||||
_pendingConditionUpdates.Enqueue(item);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -2417,7 +2570,11 @@ namespace Barotrauma
|
||||
/// </summary>
|
||||
public bool IsActive = true;
|
||||
|
||||
public bool IsInRemoveQueue;
|
||||
/// <summary>
|
||||
/// Thread-safe flag indicating whether this item is queued for removal.
|
||||
/// Uses volatile to ensure memory visibility across threads.
|
||||
/// </summary>
|
||||
public volatile bool IsInRemoveQueue;
|
||||
|
||||
public override void Update(float deltaTime, Camera cam)
|
||||
{
|
||||
@@ -2437,7 +2594,12 @@ namespace Barotrauma
|
||||
if (item != this)
|
||||
{
|
||||
item.body.Enabled = false;
|
||||
item.body.SetTransformIgnoreContacts(this.SimPosition, body.Rotation);
|
||||
// Defer physics operation if in parallel context (Farseer is not thread-safe)
|
||||
var capturedItemBody = item.body;
|
||||
var capturedSimPos = this.SimPosition;
|
||||
var capturedRotation = body.Rotation;
|
||||
PhysicsBodyQueue.ExecuteOrDefer(() =>
|
||||
capturedItemBody.SetTransformIgnoreContacts(capturedSimPos, capturedRotation));
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -2629,17 +2791,25 @@ namespace Barotrauma
|
||||
FindHull();
|
||||
}
|
||||
|
||||
// Defer physics transform operations if in parallel context.
|
||||
// Farseer's DynamicTree is not thread-safe.
|
||||
if (Submarine == null && prevSub != null)
|
||||
{
|
||||
body.SetTransformIgnoreContacts(body.SimPosition + prevSub.SimPosition, body.Rotation);
|
||||
Vector2 newPos = body.SimPosition + prevSub.SimPosition;
|
||||
float rotation = body.Rotation;
|
||||
PhysicsBodyQueue.ExecuteOrDefer(() => body.SetTransformIgnoreContacts(newPos, rotation));
|
||||
}
|
||||
else if (Submarine != null && prevSub == null)
|
||||
{
|
||||
body.SetTransformIgnoreContacts(body.SimPosition - Submarine.SimPosition, body.Rotation);
|
||||
Vector2 newPos = body.SimPosition - Submarine.SimPosition;
|
||||
float rotation = body.Rotation;
|
||||
PhysicsBodyQueue.ExecuteOrDefer(() => body.SetTransformIgnoreContacts(newPos, rotation));
|
||||
}
|
||||
else if (Submarine != null && prevSub != null && Submarine != prevSub)
|
||||
{
|
||||
body.SetTransformIgnoreContacts(body.SimPosition + prevSub.SimPosition - Submarine.SimPosition, body.Rotation);
|
||||
Vector2 newPos = body.SimPosition + prevSub.SimPosition - Submarine.SimPosition;
|
||||
float rotation = body.Rotation;
|
||||
PhysicsBodyQueue.ExecuteOrDefer(() => body.SetTransformIgnoreContacts(newPos, rotation));
|
||||
}
|
||||
|
||||
if (Submarine != prevSub)
|
||||
@@ -2855,7 +3025,8 @@ namespace Barotrauma
|
||||
foreach (Connection c in connectionPanel.Connections)
|
||||
{
|
||||
if (connectionFilter != null && !connectionFilter(c)) { continue; }
|
||||
foreach (Connection recipient in c.Recipients)
|
||||
// Use ToList() snapshot for thread-safe iteration
|
||||
foreach (Connection recipient in c.Recipients.ToList())
|
||||
{
|
||||
var component = recipient.Item.GetComponent<T>();
|
||||
if (component != null)
|
||||
@@ -2888,7 +3059,8 @@ namespace Barotrauma
|
||||
foreach (Connection c in connectionPanel.Connections)
|
||||
{
|
||||
if (connectionFilter != null && !connectionFilter(c)) { continue; }
|
||||
foreach (Connection recipient in c.Recipients)
|
||||
// Use ToList() snapshot for thread-safe iteration
|
||||
foreach (Connection recipient in c.Recipients.ToList())
|
||||
{
|
||||
var component = recipient.Item.GetComponent<T>();
|
||||
if (component != null && !connectedComponents.Contains(component))
|
||||
@@ -2942,12 +3114,13 @@ namespace Barotrauma
|
||||
alreadySearched.Add(c);
|
||||
static IEnumerable<Connection> GetRecipients(Connection c)
|
||||
{
|
||||
foreach (Connection recipient in c.Recipients)
|
||||
// Use ToList() snapshot for thread-safe iteration
|
||||
foreach (Connection recipient in c.Recipients.ToList())
|
||||
{
|
||||
yield return recipient;
|
||||
}
|
||||
//check circuit box inputs/outputs this connection is connected to
|
||||
foreach (var circuitBoxConnection in c.CircuitBoxConnections)
|
||||
foreach (var circuitBoxConnection in c.CircuitBoxConnections.ToArray())
|
||||
{
|
||||
yield return circuitBoxConnection.Connection;
|
||||
}
|
||||
@@ -3087,7 +3260,8 @@ namespace Barotrauma
|
||||
if (signal.stepsTaken > 5 && signal.source != null)
|
||||
{
|
||||
int duplicateRecipients = 0;
|
||||
foreach (var recipient in signal.source.LastSentSignalRecipients)
|
||||
// Use ToList() snapshot for thread-safe iteration
|
||||
foreach (var recipient in signal.source.LastSentSignalRecipients.ToList())
|
||||
{
|
||||
if (recipient == connection)
|
||||
{
|
||||
@@ -3532,20 +3706,45 @@ namespace Barotrauma
|
||||
if (body != null)
|
||||
{
|
||||
IsActive = true;
|
||||
body.Enabled = true;
|
||||
body.PhysEnabled = true;
|
||||
body.ResetDynamics();
|
||||
if (dropper != null)
|
||||
|
||||
// Physics body operations must be deferred if we're in a parallel update context,
|
||||
// because Farseer Physics is not thread-safe.
|
||||
if (PhysicsBodyQueue.IsInParallelContext)
|
||||
{
|
||||
if (body.Removed)
|
||||
// Capture the values we need for the deferred operation
|
||||
var capturedBody = body;
|
||||
var capturedDropperSimPos = dropper?.SimPosition ?? Microsoft.Xna.Framework.Vector2.Zero;
|
||||
var capturedSetTransform = setTransform && dropper != null;
|
||||
|
||||
PhysicsBodyQueue.Enqueue(() =>
|
||||
{
|
||||
DebugConsole.ThrowError(
|
||||
"Failed to drop the item \"" + Name + "\" (body has been removed"
|
||||
+ (Removed ? ", item has been removed)" : ")"));
|
||||
}
|
||||
else if (setTransform)
|
||||
if (capturedBody.Removed || Removed) { return; }
|
||||
capturedBody.Enabled = true;
|
||||
capturedBody.PhysEnabled = true;
|
||||
capturedBody.ResetDynamics();
|
||||
if (capturedSetTransform)
|
||||
{
|
||||
capturedBody.SetTransformIgnoreContacts(capturedDropperSimPos, 0.0f);
|
||||
}
|
||||
});
|
||||
}
|
||||
else
|
||||
{
|
||||
body.Enabled = true;
|
||||
body.PhysEnabled = true;
|
||||
body.ResetDynamics();
|
||||
if (dropper != null)
|
||||
{
|
||||
body.SetTransformIgnoreContacts(dropper.SimPosition, 0.0f);
|
||||
if (body.Removed)
|
||||
{
|
||||
DebugConsole.ThrowError(
|
||||
"Failed to drop the item \"" + Name + "\" (body has been removed"
|
||||
+ (Removed ? ", item has been removed)" : ")"));
|
||||
}
|
||||
else if (setTransform)
|
||||
{
|
||||
body.SetTransformIgnoreContacts(dropper.SimPosition, 0.0f);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -3556,7 +3755,22 @@ namespace Barotrauma
|
||||
{
|
||||
if (setTransform)
|
||||
{
|
||||
SetTransform(Container.SimPosition, 0.0f);
|
||||
// Defer SetTransform if in parallel context
|
||||
if (PhysicsBodyQueue.IsInParallelContext)
|
||||
{
|
||||
var capturedContainerSimPos = Container.SimPosition;
|
||||
PhysicsBodyQueue.Enqueue(() =>
|
||||
{
|
||||
if (!Removed)
|
||||
{
|
||||
SetTransform(capturedContainerSimPos, 0.0f);
|
||||
}
|
||||
});
|
||||
}
|
||||
else
|
||||
{
|
||||
SetTransform(Container.SimPosition, 0.0f);
|
||||
}
|
||||
}
|
||||
Container.RemoveContained(this);
|
||||
Container = null;
|
||||
@@ -4220,7 +4434,7 @@ namespace Barotrauma
|
||||
}
|
||||
}
|
||||
|
||||
if (element.GetAttributeBool("markedfordeconstruction", false)) { _deconstructItems.Add(item); }
|
||||
if (element.GetAttributeBool("markedfordeconstruction", false)) { _deconstructItems.TryAdd(item, 0); }
|
||||
|
||||
float prevRotation = item.Rotation;
|
||||
if (element.GetAttributeBool("flippedx", false)) { item.FlipX(relativeToSub: false, force: true); }
|
||||
@@ -4516,7 +4730,7 @@ namespace Barotrauma
|
||||
new XAttribute("name", Prefab.OriginalName),
|
||||
new XAttribute("identifier", Prefab.Identifier),
|
||||
new XAttribute("ID", ID),
|
||||
new XAttribute("markedfordeconstruction", _deconstructItems.Contains(this)));
|
||||
new XAttribute("markedfordeconstruction", _deconstructItems.ContainsKey(this)));
|
||||
|
||||
if (PendingItemSwap != null)
|
||||
{
|
||||
@@ -4702,12 +4916,11 @@ namespace Barotrauma
|
||||
StaticFixtures.Clear();
|
||||
}
|
||||
|
||||
foreach (Item it in ItemList)
|
||||
// Optimized: Remove() returns false if not found, no need for Contains() check
|
||||
// Using _itemDictionary.Values directly avoids property access overhead
|
||||
foreach (Item it in _itemDictionary.Values)
|
||||
{
|
||||
if (it.linkedTo.Contains(this))
|
||||
{
|
||||
it.linkedTo.Remove(this);
|
||||
}
|
||||
it.linkedTo.Remove(this);
|
||||
}
|
||||
|
||||
RemoveProjSpecific();
|
||||
@@ -4715,14 +4928,16 @@ namespace Barotrauma
|
||||
|
||||
private void RemoveFromLists()
|
||||
{
|
||||
ItemList.Remove(this);
|
||||
_dangerousItems.Remove(this);
|
||||
_repairableItems.Remove(this);
|
||||
_sonarVisibleItems.Remove(this);
|
||||
_cleanableItems.Remove(this);
|
||||
_deconstructItems.Remove(this);
|
||||
_turretTargetItems.Remove(this);
|
||||
_chairItems.Remove(this);
|
||||
_itemDictionary.TryRemove(ID, out _);
|
||||
InvalidateCachedItemList();
|
||||
RemoveFromImmutableSet(ref _dangerousItems, this);
|
||||
RemoveFromImmutableSet(ref _repairableItems, this);
|
||||
RemoveFromImmutableSet(ref _sonarVisibleItems, this);
|
||||
RemoveFromImmutableSet(ref _cleanableItems, this);
|
||||
_deconstructItems.TryRemove(this, out _);
|
||||
RemoveFromImmutableSet(ref _turretTargetItems, this);
|
||||
RemoveFromImmutableSet(ref _chairItems, this);
|
||||
_hasPendingConditionUpdate = false;
|
||||
RemoveFromDroppedStack(allowClientExecute: true);
|
||||
}
|
||||
|
||||
|
||||
+124
-2
@@ -1,4 +1,6 @@
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.Linq;
|
||||
using MoonSharp.Interpreter;
|
||||
using Microsoft.Xna.Framework;
|
||||
using FarseerPhysics.Dynamics;
|
||||
@@ -157,7 +159,7 @@ namespace Barotrauma
|
||||
return new Pair<JobPrefab, int>((JobPrefab)v.Table.Get(1).ToObject(), (int)v.Table.Get(2).CastToNumber());
|
||||
});
|
||||
|
||||
Script.GlobalOptions.CustomConverters.SetClrToScriptCustomConversion<ulong>((Script script, ulong v) =>
|
||||
Script.GlobalOptions.CustomConverters.SetClrToScriptCustomConversion<ulong>((Script script, ulong v) =>
|
||||
{
|
||||
return DynValue.NewString(v.ToString());
|
||||
});
|
||||
@@ -251,6 +253,28 @@ namespace Barotrauma
|
||||
RegisterEither<Address, AccountId>();
|
||||
|
||||
RegisterImmutableArray<FactionPrefab.HireableCharacter>();
|
||||
|
||||
|
||||
RegisterThreadSafeList<Character, ThreadSafeCharacterList>();
|
||||
RegisterThreadSafeList<WayPoint, ThreadSafeWayPointList>();
|
||||
RegisterThreadSafeList<Submarine, ThreadSafeSubmarineList>();
|
||||
RegisterThreadSafeList<MapEntity, ThreadSafeMapEntityList>();
|
||||
RegisterThreadSafeList<Hull, ThreadSafeHullList>();
|
||||
RegisterThreadSafeList<Gap, ThreadSafeGapList>();
|
||||
RegisterThreadSafeList<Structure, ThreadSafeStructureList>();
|
||||
RegisterThreadSafeList<AITarget, ThreadSafeAITargetList>();
|
||||
RegisterThreadSafeList<PhysicsBody, ThreadSafePhysicsBodyList>();
|
||||
|
||||
RegisterImmutableList<Event>();
|
||||
RegisterImmutableList<EventSet>();
|
||||
RegisterImmutableList<Sprite>();
|
||||
|
||||
RegisterImmutableHashSet<Event>();
|
||||
RegisterImmutableHashSet<Identifier>();
|
||||
RegisterImmutableHashSet<LocationConnection>();
|
||||
|
||||
RegisterImmutableDictionary<Identifier, EventPrefab>();
|
||||
RegisterImmutableDictionary<EventSet, ImmutableList<Event>>();
|
||||
}
|
||||
|
||||
private static void RegisterImmutableArray<T>()
|
||||
@@ -357,7 +381,7 @@ namespace Barotrauma
|
||||
|
||||
private void RegisterAction()
|
||||
{
|
||||
Script.GlobalOptions.CustomConverters.SetScriptToClrCustomConversion(DataType.Function, typeof(Action), v =>
|
||||
Script.GlobalOptions.CustomConverters.SetScriptToClrCustomConversion(DataType.Function, typeof(Action), v =>
|
||||
{
|
||||
var function = v.Function;
|
||||
return (Action)(() => Call(function));
|
||||
@@ -414,5 +438,103 @@ namespace Barotrauma
|
||||
return (T1 a, T2 b, T3 c, T4 d) => function.Call(a, b, c, d).ToObject<T5>();
|
||||
});
|
||||
}
|
||||
|
||||
private void RegisterThreadSafeList<TItem, TList>() where TList : IEnumerable<TItem>
|
||||
{
|
||||
Script.GlobalOptions.CustomConverters.SetClrToScriptCustomConversion(
|
||||
typeof(TList),
|
||||
(Script script, object obj) =>
|
||||
{
|
||||
if (obj is IEnumerable<TItem> enumerable)
|
||||
{
|
||||
var table = new Table(script);
|
||||
int i = 1;
|
||||
foreach (var item in enumerable)
|
||||
{
|
||||
table[i++] = DynValue.FromObject(script, item);
|
||||
}
|
||||
return DynValue.NewTable(table);
|
||||
}
|
||||
return DynValue.Nil;
|
||||
}
|
||||
);
|
||||
}
|
||||
|
||||
private void RegisterImmutableList<T>()
|
||||
{
|
||||
Script.GlobalOptions.CustomConverters.SetClrToScriptCustomConversion(
|
||||
typeof(ImmutableList<T>),
|
||||
(Script script, object obj) =>
|
||||
{
|
||||
if (obj is ImmutableList<T> list)
|
||||
{
|
||||
var table = new Table(script);
|
||||
int i = 1;
|
||||
foreach (var item in list)
|
||||
{
|
||||
table[i++] = DynValue.FromObject(script, item);
|
||||
}
|
||||
return DynValue.NewTable(table);
|
||||
}
|
||||
return DynValue.Nil;
|
||||
}
|
||||
);
|
||||
|
||||
// Lua table -> ImmutableList<T>
|
||||
Script.GlobalOptions.CustomConverters.SetScriptToClrCustomConversion(
|
||||
DataType.Table,
|
||||
typeof(ImmutableList<T>),
|
||||
v => v.ToObject<T[]>().ToImmutableList()
|
||||
);
|
||||
}
|
||||
|
||||
private void RegisterImmutableHashSet<T>()
|
||||
{
|
||||
Script.GlobalOptions.CustomConverters.SetClrToScriptCustomConversion(
|
||||
typeof(ImmutableHashSet<T>),
|
||||
(Script script, object obj) =>
|
||||
{
|
||||
if (obj is ImmutableHashSet<T> set)
|
||||
{
|
||||
var table = new Table(script);
|
||||
int i = 1;
|
||||
foreach (var item in set)
|
||||
{
|
||||
table[i++] = DynValue.FromObject(script, item);
|
||||
}
|
||||
return DynValue.NewTable(table);
|
||||
}
|
||||
return DynValue.Nil;
|
||||
}
|
||||
);
|
||||
|
||||
// Lua table -> ImmutableHashSet<T>
|
||||
Script.GlobalOptions.CustomConverters.SetScriptToClrCustomConversion(
|
||||
DataType.Table,
|
||||
typeof(ImmutableHashSet<T>),
|
||||
v => v.ToObject<T[]>().ToImmutableHashSet()
|
||||
);
|
||||
}
|
||||
|
||||
private void RegisterImmutableDictionary<TKey, TValue>()
|
||||
{
|
||||
Script.GlobalOptions.CustomConverters.SetClrToScriptCustomConversion(
|
||||
typeof(ImmutableDictionary<TKey, TValue>),
|
||||
(Script script, object obj) =>
|
||||
{
|
||||
if (obj is ImmutableDictionary<TKey, TValue> dict)
|
||||
{
|
||||
var table = new Table(script);
|
||||
foreach (var kvp in dict)
|
||||
{
|
||||
table[DynValue.FromObject(script, kvp.Key)] =
|
||||
DynValue.FromObject(script, kvp.Value);
|
||||
}
|
||||
return DynValue.NewTable(table);
|
||||
}
|
||||
return DynValue.Nil;
|
||||
}
|
||||
);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -4,6 +4,7 @@ using System.Collections.Generic;
|
||||
using System.Diagnostics;
|
||||
using System.Globalization;
|
||||
using System.Linq;
|
||||
using System.Threading;
|
||||
using System.Xml.Linq;
|
||||
using Barotrauma.Extensions;
|
||||
using Barotrauma.Items.Components;
|
||||
@@ -14,6 +15,55 @@ using Microsoft.Xna.Framework;
|
||||
|
||||
namespace Barotrauma.MapCreatures.Behavior
|
||||
{
|
||||
/// <summary>
|
||||
/// Thread-safe wrapper for BallastFloraBehavior list operations.
|
||||
/// Uses copy-on-write pattern for lock-free reads.
|
||||
/// </summary>
|
||||
internal class ThreadSafeBallastFloraList : IEnumerable<BallastFloraBehavior>
|
||||
{
|
||||
private volatile List<BallastFloraBehavior> _list = new List<BallastFloraBehavior>();
|
||||
private readonly object _writeLock = new object();
|
||||
|
||||
public int Count => _list.Count;
|
||||
|
||||
public void Add(BallastFloraBehavior entity)
|
||||
{
|
||||
lock (_writeLock)
|
||||
{
|
||||
var newList = new List<BallastFloraBehavior>(_list) { entity };
|
||||
Interlocked.Exchange(ref _list, newList);
|
||||
}
|
||||
}
|
||||
|
||||
public bool Remove(BallastFloraBehavior entity)
|
||||
{
|
||||
lock (_writeLock)
|
||||
{
|
||||
var newList = new List<BallastFloraBehavior>(_list);
|
||||
bool removed = newList.Remove(entity);
|
||||
if (removed)
|
||||
{
|
||||
Interlocked.Exchange(ref _list, newList);
|
||||
}
|
||||
return removed;
|
||||
}
|
||||
}
|
||||
|
||||
public void Clear()
|
||||
{
|
||||
Interlocked.Exchange(ref _list, new List<BallastFloraBehavior>());
|
||||
}
|
||||
|
||||
public IEnumerator<BallastFloraBehavior> GetEnumerator() => _list.GetEnumerator();
|
||||
System.Collections.IEnumerator System.Collections.IEnumerable.GetEnumerator() => GetEnumerator();
|
||||
|
||||
// LINQ-friendly methods
|
||||
public List<BallastFloraBehavior> ToList() => new List<BallastFloraBehavior>(_list);
|
||||
public bool Any() => _list.Any();
|
||||
public bool Any(Func<BallastFloraBehavior, bool> predicate) => _list.Any(predicate);
|
||||
public IEnumerable<BallastFloraBehavior> Where(Func<BallastFloraBehavior, bool> predicate) => _list.Where(predicate);
|
||||
}
|
||||
|
||||
class BallastFloraBranch : VineTile
|
||||
{
|
||||
public readonly BallastFloraBehavior? ParentBallastFlora;
|
||||
@@ -132,7 +182,7 @@ namespace Barotrauma.MapCreatures.Behavior
|
||||
public List<Tuple<Vector2, Vector2>> debugSearchLines = new List<Tuple<Vector2, Vector2>>();
|
||||
#endif
|
||||
|
||||
private readonly static List<BallastFloraBehavior> _entityList = new List<BallastFloraBehavior>();
|
||||
private readonly static ThreadSafeBallastFloraList _entityList = new ThreadSafeBallastFloraList();
|
||||
public static IEnumerable<BallastFloraBehavior> EntityList => _entityList;
|
||||
|
||||
public enum NetworkHeader
|
||||
@@ -307,6 +357,12 @@ namespace Barotrauma.MapCreatures.Behavior
|
||||
public readonly List<BallastFloraBranch> Branches = new List<BallastFloraBranch>();
|
||||
private BallastFloraBranch? root;
|
||||
private readonly List<Body> bodies = new List<Body>();
|
||||
|
||||
/// <summary>
|
||||
/// Branches that need physics bodies created on the main thread.
|
||||
/// </summary>
|
||||
private readonly List<BallastFloraBranch> pendingBodyCreations = new List<BallastFloraBranch>();
|
||||
private readonly object pendingBodyCreationsLock = new object();
|
||||
|
||||
private bool isDead;
|
||||
|
||||
@@ -347,7 +403,8 @@ namespace Barotrauma.MapCreatures.Behavior
|
||||
}
|
||||
}
|
||||
UpdateConnections(branch);
|
||||
CreateBody(branch);
|
||||
// OnMapLoaded runs on the main thread, so we can create bodies immediately
|
||||
CreateBody(branch, immediate: true);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -998,10 +1055,52 @@ namespace Barotrauma.MapCreatures.Behavior
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Create a body for a branch which works as the hitbox for flamer
|
||||
/// Queue a physics body creation for a branch.
|
||||
/// The actual body will be created on the main thread to ensure thread safety.
|
||||
/// </summary>
|
||||
/// <param name="branch"></param>
|
||||
private void CreateBody(BallastFloraBranch branch)
|
||||
/// <param name="branch">The branch to create a body for</param>
|
||||
/// <param name="immediate">If true, create the body immediately (only safe when called from main thread)</param>
|
||||
private void CreateBody(BallastFloraBranch branch, bool immediate = false)
|
||||
{
|
||||
if (immediate)
|
||||
{
|
||||
CreateBodyImmediate(branch);
|
||||
return;
|
||||
}
|
||||
|
||||
lock (pendingBodyCreationsLock)
|
||||
{
|
||||
pendingBodyCreations.Add(branch);
|
||||
}
|
||||
PhysicsBodyQueue.EnqueueCreation(() => ProcessPendingBodyCreations());
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Process all pending body creations on the main thread.
|
||||
/// This ensures Farseer Physics operations are thread-safe.
|
||||
/// </summary>
|
||||
private void ProcessPendingBodyCreations()
|
||||
{
|
||||
List<BallastFloraBranch> branchesToProcess;
|
||||
lock (pendingBodyCreationsLock)
|
||||
{
|
||||
if (pendingBodyCreations.Count == 0) { return; }
|
||||
branchesToProcess = new List<BallastFloraBranch>(pendingBodyCreations);
|
||||
pendingBodyCreations.Clear();
|
||||
}
|
||||
|
||||
foreach (var branch in branchesToProcess)
|
||||
{
|
||||
if (branch.Removed) { continue; }
|
||||
CreateBodyImmediate(branch);
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Actually create the physics body for a branch.
|
||||
/// Must be called on the main thread.
|
||||
/// </summary>
|
||||
private void CreateBodyImmediate(BallastFloraBranch branch)
|
||||
{
|
||||
Rectangle rect = branch.Rect;
|
||||
Vector2 pos = Parent.Position + Offset + branch.Position;
|
||||
|
||||
@@ -1,5 +1,6 @@
|
||||
using Microsoft.Xna.Framework;
|
||||
using System;
|
||||
using System.Collections.Concurrent;
|
||||
using System.Collections.Generic;
|
||||
using System.Diagnostics;
|
||||
using Barotrauma.IO;
|
||||
@@ -20,10 +21,10 @@ namespace Barotrauma
|
||||
|
||||
public const ushort MaxEntityCount = ushort.MaxValue - 4; //ushort.MaxValue - 4 because the 4 values above are reserved values
|
||||
|
||||
private static readonly Dictionary<ushort, Entity> dictionary = new Dictionary<ushort, Entity>();
|
||||
private static readonly ConcurrentDictionary<ushort, Entity> dictionary = new ConcurrentDictionary<ushort, Entity>();
|
||||
public static IReadOnlyCollection<Entity> GetEntities()
|
||||
{
|
||||
return dictionary.Values;
|
||||
return (IReadOnlyCollection<Entity>)dictionary.Values;
|
||||
}
|
||||
|
||||
public static int EntityCount => dictionary.Count;
|
||||
@@ -122,13 +123,11 @@ namespace Barotrauma
|
||||
//give a unique ID
|
||||
ID = DetermineID(id, submarine);
|
||||
|
||||
if (dictionary.ContainsKey(ID))
|
||||
if (!dictionary.TryAdd(ID, this))
|
||||
{
|
||||
throw new Exception($"ID {ID} is taken by {dictionary[ID]}");
|
||||
}
|
||||
|
||||
dictionary.Add(ID, this);
|
||||
|
||||
CreationStackTrace = "";
|
||||
#if DEBUG
|
||||
var st = new StackTrace(skipFrames: 2, fNeedFileInfo: true);
|
||||
@@ -147,7 +146,6 @@ namespace Barotrauma
|
||||
CreationStackTrace += $"{fileName}@{fileLineNumber}; ";
|
||||
}
|
||||
#endif
|
||||
#warning TODO: consider removing this mutex, entity creation probably shouldn't be multithreaded
|
||||
lock (creationCounterMutex)
|
||||
{
|
||||
CreationIndex = creationCounter;
|
||||
@@ -261,7 +259,7 @@ namespace Barotrauma
|
||||
DebugConsole.ThrowError($"Error while removing item \"{item}\"", exception);
|
||||
}
|
||||
}
|
||||
Item.ItemList.Clear();
|
||||
Item.ClearAllItemCollections();
|
||||
}
|
||||
if (Character.CharacterList.Count > 0)
|
||||
{
|
||||
@@ -325,7 +323,7 @@ namespace Barotrauma
|
||||
}
|
||||
else
|
||||
{
|
||||
dictionary.Remove(ID);
|
||||
dictionary.TryRemove(ID, out _);
|
||||
}
|
||||
IdFreed = true;
|
||||
}
|
||||
|
||||
@@ -7,6 +7,7 @@ using Microsoft.Xna.Framework;
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.Linq;
|
||||
using System.Threading;
|
||||
|
||||
namespace Barotrauma
|
||||
{
|
||||
@@ -648,7 +649,11 @@ namespace Barotrauma
|
||||
}
|
||||
}
|
||||
|
||||
private static readonly Dictionary<Structure, float> damagedStructures = new Dictionary<Structure, float>();
|
||||
// ThreadLocal for thread-safe structure damage tracking
|
||||
private static readonly ThreadLocal<Dictionary<Structure, float>> damagedStructuresLocal =
|
||||
new ThreadLocal<Dictionary<Structure, float>>(() => new Dictionary<Structure, float>());
|
||||
private static Dictionary<Structure, float> damagedStructures => damagedStructuresLocal.Value;
|
||||
|
||||
/// <summary>
|
||||
/// Returns a dictionary where the keys are the structures that took damage and the values are the amount of damage taken
|
||||
/// </summary>
|
||||
|
||||
@@ -10,13 +10,71 @@ using System;
|
||||
using System.Collections.Concurrent;
|
||||
using System.Collections.Generic;
|
||||
using System.Linq;
|
||||
using System.Threading;
|
||||
using System.Xml.Linq;
|
||||
|
||||
namespace Barotrauma
|
||||
{
|
||||
/// <summary>
|
||||
/// Thread-safe wrapper for Gap list operations.
|
||||
/// Uses copy-on-write pattern for lock-free reads.
|
||||
/// </summary>
|
||||
internal class ThreadSafeGapList : IEnumerable<Gap>
|
||||
{
|
||||
private volatile List<Gap> _list = new List<Gap>();
|
||||
private readonly object _writeLock = new object();
|
||||
|
||||
public int Count => _list.Count;
|
||||
|
||||
public void Add(Gap gap)
|
||||
{
|
||||
lock (_writeLock)
|
||||
{
|
||||
var newList = new List<Gap>(_list) { gap };
|
||||
Interlocked.Exchange(ref _list, newList);
|
||||
}
|
||||
}
|
||||
|
||||
public bool Remove(Gap gap)
|
||||
{
|
||||
lock (_writeLock)
|
||||
{
|
||||
var newList = new List<Gap>(_list);
|
||||
bool removed = newList.Remove(gap);
|
||||
if (removed)
|
||||
{
|
||||
Interlocked.Exchange(ref _list, newList);
|
||||
}
|
||||
return removed;
|
||||
}
|
||||
}
|
||||
|
||||
public void Clear()
|
||||
{
|
||||
Interlocked.Exchange(ref _list, new List<Gap>());
|
||||
}
|
||||
|
||||
public bool Contains(Gap gap) => _list.Contains(gap);
|
||||
|
||||
public Gap this[int index] => _list[index];
|
||||
|
||||
public IEnumerator<Gap> GetEnumerator() => _list.GetEnumerator();
|
||||
System.Collections.IEnumerator System.Collections.IEnumerable.GetEnumerator() => GetEnumerator();
|
||||
|
||||
// LINQ-friendly methods
|
||||
public List<Gap> ToList() => new List<Gap>(_list);
|
||||
public Gap FirstOrDefault(Func<Gap, bool> predicate) => _list.FirstOrDefault(predicate);
|
||||
public Gap Find(Predicate<Gap> predicate) => _list.Find(predicate);
|
||||
public List<Gap> FindAll(Predicate<Gap> predicate) => _list.FindAll(predicate);
|
||||
public IEnumerable<Gap> Where(Func<Gap, bool> predicate) => _list.Where(predicate);
|
||||
public bool Any() => _list.Any();
|
||||
public bool Any(Func<Gap, bool> predicate) => _list.Any(predicate);
|
||||
public IOrderedEnumerable<Gap> OrderBy<TKey>(Func<Gap, TKey> keySelector) => _list.OrderBy(keySelector);
|
||||
}
|
||||
|
||||
partial class Gap : MapEntity, ISerializableEntity
|
||||
{
|
||||
public static List<Gap> GapList = new List<Gap>();
|
||||
public static ThreadSafeGapList GapList = new ThreadSafeGapList();
|
||||
|
||||
const float MaxFlowForce = 500.0f;
|
||||
|
||||
|
||||
@@ -6,6 +6,7 @@ using System;
|
||||
using System.Collections.Generic;
|
||||
using System.Globalization;
|
||||
using System.Linq;
|
||||
using System.Threading;
|
||||
using System.Xml.Linq;
|
||||
using Barotrauma.MapCreatures.Behavior;
|
||||
using Barotrauma.Items.Components;
|
||||
@@ -13,6 +14,116 @@ using Barotrauma.Extensions;
|
||||
|
||||
namespace Barotrauma
|
||||
{
|
||||
/// <summary>
|
||||
/// Thread-safe wrapper for Hull list operations.
|
||||
/// Uses copy-on-write pattern for lock-free reads.
|
||||
/// </summary>
|
||||
internal class ThreadSafeHullList : IEnumerable<Hull>
|
||||
{
|
||||
private volatile List<Hull> _list = new List<Hull>();
|
||||
private readonly object _writeLock = new object();
|
||||
|
||||
public int Count => _list.Count;
|
||||
|
||||
public void Add(Hull hull)
|
||||
{
|
||||
lock (_writeLock)
|
||||
{
|
||||
var newList = new List<Hull>(_list) { hull };
|
||||
Interlocked.Exchange(ref _list, newList);
|
||||
}
|
||||
}
|
||||
|
||||
public bool Remove(Hull hull)
|
||||
{
|
||||
lock (_writeLock)
|
||||
{
|
||||
var newList = new List<Hull>(_list);
|
||||
bool removed = newList.Remove(hull);
|
||||
if (removed)
|
||||
{
|
||||
Interlocked.Exchange(ref _list, newList);
|
||||
}
|
||||
return removed;
|
||||
}
|
||||
}
|
||||
|
||||
public void Clear()
|
||||
{
|
||||
Interlocked.Exchange(ref _list, new List<Hull>());
|
||||
}
|
||||
|
||||
public bool Contains(Hull hull) => _list.Contains(hull);
|
||||
|
||||
public Hull this[int index] => _list[index];
|
||||
|
||||
public IEnumerator<Hull> GetEnumerator() => _list.GetEnumerator();
|
||||
System.Collections.IEnumerator System.Collections.IEnumerable.GetEnumerator() => GetEnumerator();
|
||||
|
||||
// LINQ-friendly methods
|
||||
public List<Hull> ToList() => new List<Hull>(_list);
|
||||
public Hull FirstOrDefault(Func<Hull, bool> predicate) => _list.FirstOrDefault(predicate);
|
||||
public Hull Find(Predicate<Hull> predicate) => _list.Find(predicate);
|
||||
public List<Hull> FindAll(Predicate<Hull> predicate) => _list.FindAll(predicate);
|
||||
public IEnumerable<Hull> Where(Func<Hull, bool> predicate) => _list.Where(predicate);
|
||||
public bool Any() => _list.Any();
|
||||
public bool Any(Func<Hull, bool> predicate) => _list.Any(predicate);
|
||||
public bool Exists(Predicate<Hull> predicate) => _list.Exists(predicate);
|
||||
public void ForEach(Action<Hull> action) => _list.ForEach(action);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Thread-safe wrapper for EntityGrid list operations.
|
||||
/// Uses copy-on-write pattern for lock-free reads.
|
||||
/// </summary>
|
||||
internal class ThreadSafeEntityGridList : IEnumerable<EntityGrid>
|
||||
{
|
||||
private volatile List<EntityGrid> _list = new List<EntityGrid>();
|
||||
private readonly object _writeLock = new object();
|
||||
|
||||
public int Count => _list.Count;
|
||||
|
||||
public void Add(EntityGrid grid)
|
||||
{
|
||||
lock (_writeLock)
|
||||
{
|
||||
var newList = new List<EntityGrid>(_list) { grid };
|
||||
Interlocked.Exchange(ref _list, newList);
|
||||
}
|
||||
}
|
||||
|
||||
public bool Remove(EntityGrid grid)
|
||||
{
|
||||
lock (_writeLock)
|
||||
{
|
||||
var newList = new List<EntityGrid>(_list);
|
||||
bool removed = newList.Remove(grid);
|
||||
if (removed)
|
||||
{
|
||||
Interlocked.Exchange(ref _list, newList);
|
||||
}
|
||||
return removed;
|
||||
}
|
||||
}
|
||||
|
||||
public void Clear()
|
||||
{
|
||||
Interlocked.Exchange(ref _list, new List<EntityGrid>());
|
||||
}
|
||||
|
||||
public EntityGrid this[int index] => _list[index];
|
||||
|
||||
public IEnumerator<EntityGrid> GetEnumerator() => _list.GetEnumerator();
|
||||
System.Collections.IEnumerator System.Collections.IEnumerable.GetEnumerator() => GetEnumerator();
|
||||
|
||||
// LINQ-friendly methods
|
||||
public List<EntityGrid> ToList() => new List<EntityGrid>(_list);
|
||||
public EntityGrid FirstOrDefault(Func<EntityGrid, bool> predicate) => _list.FirstOrDefault(predicate);
|
||||
public EntityGrid Find(Predicate<EntityGrid> predicate) => _list.Find(predicate);
|
||||
public IEnumerable<EntityGrid> Where(Func<EntityGrid, bool> predicate) => _list.Where(predicate);
|
||||
public bool Any() => _list.Any();
|
||||
}
|
||||
|
||||
partial class BackgroundSection
|
||||
{
|
||||
public Rectangle Rect;
|
||||
@@ -113,8 +224,8 @@ namespace Barotrauma
|
||||
|
||||
partial class Hull : MapEntity, ISerializableEntity, IServerSerializable
|
||||
{
|
||||
public readonly static List<Hull> HullList = new List<Hull>();
|
||||
public readonly static List<EntityGrid> EntityGrids = new List<EntityGrid>();
|
||||
public readonly static ThreadSafeHullList HullList = new ThreadSafeHullList();
|
||||
public readonly static ThreadSafeEntityGridList EntityGrids = new ThreadSafeEntityGridList();
|
||||
|
||||
public static bool ShowHulls = true;
|
||||
|
||||
@@ -1107,15 +1218,22 @@ namespace Barotrauma
|
||||
}
|
||||
}
|
||||
|
||||
private readonly HashSet<Hull> adjacentHulls = new HashSet<Hull>();
|
||||
/// <summary>
|
||||
/// Used in <see cref="GetConnectedHulls"/> - ThreadLocal for thread safety during parallel updates
|
||||
/// </summary>
|
||||
private static readonly ThreadLocal<HashSet<Hull>> adjacentHullsLocal =
|
||||
new ThreadLocal<HashSet<Hull>>(() => new HashSet<Hull>());
|
||||
|
||||
public IEnumerable<Hull> GetConnectedHulls(bool includingThis, int? searchDepth = null, bool ignoreClosedGaps = false)
|
||||
{
|
||||
var adjacentHulls = adjacentHullsLocal.Value;
|
||||
adjacentHulls.Clear();
|
||||
int startStep = 0;
|
||||
searchDepth ??= 100;
|
||||
GetAdjacentHulls(adjacentHulls, ref startStep, searchDepth.Value, ignoreClosedGaps);
|
||||
if (!includingThis) { adjacentHulls.Remove(this); }
|
||||
return adjacentHulls;
|
||||
// Return a copy to prevent concurrent modification if the caller enumerates while another thread calls this method
|
||||
return adjacentHulls.ToHashSet();
|
||||
}
|
||||
|
||||
private void GetAdjacentHulls(HashSet<Hull> connectedHulls, ref int step, int searchDepth, bool ignoreClosedGaps = false)
|
||||
@@ -1137,13 +1255,18 @@ namespace Barotrauma
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Used in <see cref="GetApproximateDistance"/>
|
||||
/// Used in <see cref="GetApproximateDistance"/> - ThreadLocal for thread safety
|
||||
/// </summary>
|
||||
private static readonly Dictionary<Hull, float> cachedDistances = [];
|
||||
private static readonly ThreadLocal<Dictionary<Hull, float>> cachedDistancesLocal =
|
||||
new ThreadLocal<Dictionary<Hull, float>>(() => new Dictionary<Hull, float>());
|
||||
/// <summary>
|
||||
/// Used in <see cref="GetApproximateDistance"/>
|
||||
/// Used in <see cref="GetApproximateDistance"/> - ThreadLocal for thread safety
|
||||
/// </summary>
|
||||
private static readonly PriorityQueue<(Hull hull, Vector2 pos), float> priorityQueue = new PriorityQueue<(Hull hull, Vector2 pos), float>();
|
||||
private static readonly ThreadLocal<PriorityQueue<(Hull hull, Vector2 pos), float>> priorityQueueLocal =
|
||||
new ThreadLocal<PriorityQueue<(Hull hull, Vector2 pos), float>>(() => new PriorityQueue<(Hull hull, Vector2 pos), float>());
|
||||
|
||||
private static Dictionary<Hull, float> cachedDistances => cachedDistancesLocal.Value;
|
||||
private static PriorityQueue<(Hull hull, Vector2 pos), float> priorityQueue => priorityQueueLocal.Value;
|
||||
|
||||
/// <summary>
|
||||
/// Approximate distance from this hull to the target hull, moving through open gaps without passing through walls.
|
||||
|
||||
@@ -11,6 +11,7 @@ using System.Collections.Immutable;
|
||||
using System.Diagnostics;
|
||||
using System.Globalization;
|
||||
using System.Linq;
|
||||
using System.Threading;
|
||||
using System.Xml.Linq;
|
||||
using Voronoi2;
|
||||
|
||||
@@ -3657,9 +3658,15 @@ namespace Barotrauma
|
||||
return cells;
|
||||
}
|
||||
|
||||
private readonly List<VoronoiCell> tempCells = new List<VoronoiCell>();
|
||||
/// <summary>
|
||||
/// Used in <see cref="GetCells"/> - ThreadLocal for thread safety during parallel updates
|
||||
/// </summary>
|
||||
private static readonly ThreadLocal<List<VoronoiCell>> tempCellsLocal =
|
||||
new ThreadLocal<List<VoronoiCell>>(() => new List<VoronoiCell>());
|
||||
|
||||
public List<VoronoiCell> GetCells(Vector2 worldPos, int searchDepth = 2)
|
||||
{
|
||||
var tempCells = tempCellsLocal.Value;
|
||||
tempCells.Clear();
|
||||
int gridPosX = (int)Math.Floor(worldPos.X / GridCellSize);
|
||||
int gridPosY = (int)Math.Floor(worldPos.Y / GridCellSize);
|
||||
@@ -3714,7 +3721,8 @@ namespace Barotrauma
|
||||
tempCells.AddRange(abyssIsland.Cells);
|
||||
}
|
||||
|
||||
return tempCells;
|
||||
// Return a copy to prevent concurrent modification if the caller enumerates while another thread calls this method
|
||||
return tempCells.ToList();
|
||||
}
|
||||
|
||||
public VoronoiCell GetClosestCell(Vector2 worldPos)
|
||||
@@ -4784,7 +4792,7 @@ namespace Barotrauma
|
||||
// BeaconStation.FlipX();
|
||||
// }
|
||||
|
||||
Item sonarItem = Item.ItemList.Find(it => it.Submarine == BeaconStation && it.GetComponent<Sonar>() != null);
|
||||
Item sonarItem = Item.ItemList.FirstOrDefault(it => it.Submarine == BeaconStation && it.GetComponent<Sonar>() != null);
|
||||
if (sonarItem == null)
|
||||
{
|
||||
DebugConsole.ThrowError($"No sonar found in the beacon station \"{beaconStationName}\"!");
|
||||
@@ -4804,7 +4812,7 @@ namespace Barotrauma
|
||||
throw new InvalidOperationException("Failed to prepare beacon station (no beacon station in the level).");
|
||||
}
|
||||
|
||||
List<Item> beaconItems = Item.ItemList.FindAll(it => it.Submarine == BeaconStation);
|
||||
List<Item> beaconItems = Item.ItemList.Where(it => it.Submarine == BeaconStation).ToList();
|
||||
|
||||
Item reactorItem = beaconItems.Find(it => it.GetComponent<Reactor>() != null);
|
||||
Reactor reactorComponent = null;
|
||||
@@ -4850,7 +4858,7 @@ namespace Barotrauma
|
||||
if (BeaconStation?.Info?.BeaconStationInfo is { AllowDisconnectedWires: false }) { return; }
|
||||
|
||||
if (disconnectWireProbability <= 0.0f) { return; }
|
||||
List<Item> beaconItems = Item.ItemList.FindAll(it => it.Submarine == BeaconStation);
|
||||
List<Item> beaconItems = Item.ItemList.Where(it => it.Submarine == BeaconStation).ToList();
|
||||
foreach (Item item in beaconItems.Where(it => it.GetComponent<Wire>() != null).ToList())
|
||||
{
|
||||
if (item.NonInteractable || item.InvulnerableToDamage) { continue; }
|
||||
@@ -4888,7 +4896,7 @@ namespace Barotrauma
|
||||
|
||||
if (breakDeviceProbability <= 0.0f) { return; }
|
||||
//break powered items
|
||||
List<Item> beaconItems = Item.ItemList.FindAll(it => it.Submarine == BeaconStation);
|
||||
List<Item> beaconItems = Item.ItemList.Where(it => it.Submarine == BeaconStation).ToList();
|
||||
foreach (Item item in beaconItems.Where(it => it.Components.Any(c => c is Powered) && it.Components.Any(c => c is Repairable)))
|
||||
{
|
||||
if (item.NonInteractable || item.InvulnerableToDamage) { continue; }
|
||||
@@ -5203,7 +5211,7 @@ namespace Barotrauma
|
||||
UnsyncedExtraWalls = null;
|
||||
}
|
||||
|
||||
tempCells?.Clear();
|
||||
tempCellsLocal?.Value?.Clear();
|
||||
cells = null;
|
||||
cellGrid = null;
|
||||
|
||||
|
||||
@@ -673,13 +673,17 @@ namespace Barotrauma
|
||||
}
|
||||
}
|
||||
|
||||
private static readonly List<Entity> triggerersToRemove = new List<Entity>();
|
||||
public static void RemoveInActiveTriggerers(PhysicsBody physicsBody, HashSet<Entity> triggerers)
|
||||
{
|
||||
if (physicsBody == null) { return; }
|
||||
|
||||
triggerersToRemove.Clear();
|
||||
foreach (var triggerer in triggerers)
|
||||
// Use local list instead of static field to avoid concurrent access issues during parallel updates
|
||||
var triggerersToRemove = new List<Entity>();
|
||||
|
||||
// Create snapshot to avoid concurrent modification during enumeration
|
||||
var triggererSnapshot = triggerers.ToArray();
|
||||
|
||||
foreach (var triggerer in triggererSnapshot)
|
||||
{
|
||||
if (triggerer.Removed)
|
||||
{
|
||||
|
||||
@@ -1371,7 +1371,7 @@ namespace Barotrauma
|
||||
{
|
||||
foreach (TakenItem takenItem in takenItems)
|
||||
{
|
||||
Item item = Item.ItemList.Find(it => takenItem.Matches(it));
|
||||
Item item = Item.ItemList.FirstOrDefault(it => takenItem.Matches(it));
|
||||
item?.Remove();
|
||||
}
|
||||
}
|
||||
|
||||
@@ -6,14 +6,112 @@ using System.Collections.Generic;
|
||||
using System.Diagnostics;
|
||||
using System.Linq;
|
||||
using System.Reflection;
|
||||
using System.Threading;
|
||||
using System.Threading.Tasks;
|
||||
using System.Xml.Linq;
|
||||
using static OneOf.Types.TrueFalseOrNull;
|
||||
|
||||
namespace Barotrauma
|
||||
{
|
||||
/// <summary>
|
||||
/// Thread-safe wrapper for MapEntity list operations.
|
||||
/// Uses copy-on-write pattern for lock-free reads.
|
||||
/// </summary>
|
||||
internal class ThreadSafeMapEntityList : IEnumerable<MapEntity>
|
||||
{
|
||||
private volatile List<MapEntity> _list = new List<MapEntity>();
|
||||
private readonly object _writeLock = new object();
|
||||
|
||||
public int Count => _list.Count;
|
||||
|
||||
public void Add(MapEntity entity)
|
||||
{
|
||||
lock (_writeLock)
|
||||
{
|
||||
var newList = new List<MapEntity>(_list) { entity };
|
||||
Interlocked.Exchange(ref _list, newList);
|
||||
}
|
||||
}
|
||||
|
||||
public void Insert(int index, MapEntity entity)
|
||||
{
|
||||
lock (_writeLock)
|
||||
{
|
||||
var newList = new List<MapEntity>(_list);
|
||||
newList.Insert(index, entity);
|
||||
Interlocked.Exchange(ref _list, newList);
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Atomically inserts an entity at a position determined by the insertAction.
|
||||
/// The insertAction is executed within the lock to ensure thread-safety.
|
||||
/// </summary>
|
||||
public void InsertWithAction(MapEntity entity, Action<List<MapEntity>, MapEntity> insertAction)
|
||||
{
|
||||
lock (_writeLock)
|
||||
{
|
||||
var newList = new List<MapEntity>(_list);
|
||||
insertAction(newList, entity);
|
||||
Interlocked.Exchange(ref _list, newList);
|
||||
}
|
||||
}
|
||||
|
||||
public bool Remove(MapEntity entity)
|
||||
{
|
||||
lock (_writeLock)
|
||||
{
|
||||
var newList = new List<MapEntity>(_list);
|
||||
bool removed = newList.Remove(entity);
|
||||
if (removed)
|
||||
{
|
||||
Interlocked.Exchange(ref _list, newList);
|
||||
}
|
||||
return removed;
|
||||
}
|
||||
}
|
||||
|
||||
public int RemoveAll(Predicate<MapEntity> match)
|
||||
{
|
||||
lock (_writeLock)
|
||||
{
|
||||
var newList = new List<MapEntity>(_list);
|
||||
int count = newList.RemoveAll(match);
|
||||
if (count > 0)
|
||||
{
|
||||
Interlocked.Exchange(ref _list, newList);
|
||||
}
|
||||
return count;
|
||||
}
|
||||
}
|
||||
|
||||
public void Clear()
|
||||
{
|
||||
Interlocked.Exchange(ref _list, new List<MapEntity>());
|
||||
}
|
||||
|
||||
public bool Contains(MapEntity entity) => _list.Contains(entity);
|
||||
|
||||
public MapEntity this[int index] => _list[index];
|
||||
|
||||
public IEnumerator<MapEntity> GetEnumerator() => _list.GetEnumerator();
|
||||
System.Collections.IEnumerator System.Collections.IEnumerable.GetEnumerator() => GetEnumerator();
|
||||
|
||||
// LINQ-friendly methods that work on a snapshot
|
||||
public List<MapEntity> ToList() => new List<MapEntity>(_list);
|
||||
public MapEntity FirstOrDefault(Func<MapEntity, bool> predicate) => _list.FirstOrDefault(predicate);
|
||||
public MapEntity Find(Predicate<MapEntity> predicate) => _list.Find(predicate);
|
||||
public List<MapEntity> FindAll(Predicate<MapEntity> predicate) => _list.FindAll(predicate);
|
||||
public IEnumerable<MapEntity> Where(Func<MapEntity, bool> predicate) => _list.Where(predicate);
|
||||
public bool Any(Func<MapEntity, bool> predicate) => _list.Any(predicate);
|
||||
public bool Exists(Predicate<MapEntity> predicate) => _list.Exists(predicate);
|
||||
public IOrderedEnumerable<MapEntity> OrderBy<TKey>(Func<MapEntity, TKey> keySelector) => _list.OrderBy(keySelector);
|
||||
public void ForEach(Action<MapEntity> action) => _list.ForEach(action);
|
||||
}
|
||||
|
||||
abstract partial class MapEntity : Entity, ISpatialEntity
|
||||
{
|
||||
public readonly static List<MapEntity> MapEntityList = new List<MapEntity>();
|
||||
public readonly static ThreadSafeMapEntityList MapEntityList = new ThreadSafeMapEntityList();
|
||||
|
||||
public readonly MapEntityPrefab Prefab;
|
||||
|
||||
@@ -559,45 +657,51 @@ namespace Barotrauma
|
||||
return;
|
||||
}
|
||||
|
||||
//sort damageable walls by sprite depth:
|
||||
//necessary because rendering the damage effect starts a new sprite batch and breaks the order otherwise
|
||||
int i = 0;
|
||||
if (this is Structure { DrawDamageEffect: true } structure)
|
||||
// Use atomic insertion to ensure thread-safety
|
||||
MapEntityList.InsertWithAction(this, (list, entity) =>
|
||||
{
|
||||
//insertion sort according to draw depth
|
||||
float drawDepth = structure.SpriteDepth;
|
||||
while (i < MapEntityList.Count)
|
||||
int i = 0;
|
||||
|
||||
//sort damageable walls by sprite depth:
|
||||
//necessary because rendering the damage effect starts a new sprite batch and breaks the order otherwise
|
||||
if (entity is Structure { DrawDamageEffect: true } structure)
|
||||
{
|
||||
float otherDrawDepth = (MapEntityList[i] as Structure)?.SpriteDepth ?? 1.0f;
|
||||
if (otherDrawDepth < drawDepth) { break; }
|
||||
i++;
|
||||
}
|
||||
MapEntityList.Insert(i, this);
|
||||
return;
|
||||
}
|
||||
|
||||
i = 0;
|
||||
while (i < MapEntityList.Count)
|
||||
{
|
||||
i++;
|
||||
if (MapEntityList[i - 1]?.Prefab == Prefab)
|
||||
{
|
||||
MapEntityList.Insert(i, this);
|
||||
//insertion sort according to draw depth
|
||||
float drawDepth = structure.SpriteDepth;
|
||||
while (i < list.Count)
|
||||
{
|
||||
float otherDrawDepth = (list[i] as Structure)?.SpriteDepth ?? 1.0f;
|
||||
if (otherDrawDepth < drawDepth) { break; }
|
||||
i++;
|
||||
}
|
||||
list.Insert(i, entity);
|
||||
return;
|
||||
}
|
||||
}
|
||||
|
||||
i = 0;
|
||||
var mapEntity = (MapEntity)entity;
|
||||
while (i < list.Count)
|
||||
{
|
||||
i++;
|
||||
if (list[i - 1]?.Prefab == mapEntity.Prefab)
|
||||
{
|
||||
list.Insert(i, entity);
|
||||
return;
|
||||
}
|
||||
}
|
||||
|
||||
#if CLIENT
|
||||
i = 0;
|
||||
while (i < MapEntityList.Count)
|
||||
{
|
||||
i++;
|
||||
Sprite existingSprite = MapEntityList[i - 1].Sprite;
|
||||
if (existingSprite == null) { continue; }
|
||||
if (existingSprite.Texture == this.Sprite.Texture) { break; }
|
||||
}
|
||||
i = 0;
|
||||
while (i < list.Count)
|
||||
{
|
||||
i++;
|
||||
Sprite existingSprite = list[i - 1].Sprite;
|
||||
if (existingSprite == null) { continue; }
|
||||
if (existingSprite.Texture == mapEntity.Sprite?.Texture) { break; }
|
||||
}
|
||||
#endif
|
||||
MapEntityList.Insert(i, this);
|
||||
list.Insert(i, entity);
|
||||
});
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
@@ -642,7 +746,7 @@ namespace Barotrauma
|
||||
/// </summary>
|
||||
public static void UpdateAll(float deltaTime, Camera cam, ParallelOptions parallelOptions)
|
||||
{
|
||||
mapEntityUpdateTick++;
|
||||
Random rand = new Random();
|
||||
#if CLIENT
|
||||
var sw = new System.Diagnostics.Stopwatch();
|
||||
sw.Start();
|
||||
@@ -655,59 +759,67 @@ namespace Barotrauma
|
||||
List<Gap> gapList = Gap.GapList.ToList();
|
||||
|
||||
// This should never break again... right?
|
||||
|
||||
//update gaps in random order, because otherwise in rooms with multiple gaps
|
||||
//the water/air will always tend to flow through the first gap in the list,
|
||||
//which may lead to weird behavior like water draining down only through
|
||||
//one gap in a room even if there are several
|
||||
int n = gapList.Count;
|
||||
while (n > 1)
|
||||
{
|
||||
n--;
|
||||
int k = Rand.Int(n + 1);
|
||||
int k = rand.Next(n + 1);
|
||||
(gapList[n], gapList[k]) = (gapList[k], gapList[n]);
|
||||
}
|
||||
|
||||
var itemList = Item.ItemList.ToList();
|
||||
|
||||
int mapEntityUpdateInterval = Math.Max(MapEntityUpdateInterval, 1);
|
||||
int poweredUpdateInterval = Math.Max(PoweredUpdateInterval, 1);
|
||||
|
||||
if (mapEntityUpdateTick % mapEntityUpdateInterval == 0)
|
||||
{
|
||||
float mapEntityDeltaTime = deltaTime * mapEntityUpdateInterval;
|
||||
|
||||
Parallel.Invoke(parallelOptions,
|
||||
() =>
|
||||
// First phase: parallel updates that have no order dependencies
|
||||
Parallel.Invoke(parallelOptions,
|
||||
() =>
|
||||
{
|
||||
Parallel.ForEach(hullList, parallelOptions, hull =>
|
||||
{
|
||||
Parallel.ForEach(hullList, parallelOptions, hull =>
|
||||
{
|
||||
hull.Update(mapEntityDeltaTime, cam);
|
||||
});
|
||||
},
|
||||
() =>
|
||||
{
|
||||
Parallel.ForEach(structureList, parallelOptions, structure =>
|
||||
{
|
||||
structure.Update(mapEntityDeltaTime, cam);
|
||||
});
|
||||
hull.Update(deltaTime, cam);
|
||||
});
|
||||
}
|
||||
|
||||
},
|
||||
// Structure parallel update
|
||||
() =>
|
||||
{
|
||||
Parallel.ForEach(structureList, parallelOptions, structure =>
|
||||
{
|
||||
PhysicsBodyQueue.IsInParallelContext = true;
|
||||
try
|
||||
{
|
||||
structure.Update(deltaTime, cam);
|
||||
}
|
||||
finally
|
||||
{
|
||||
PhysicsBodyQueue.IsInParallelContext = false;
|
||||
}
|
||||
});
|
||||
},
|
||||
() =>
|
||||
//update gaps in random order, because otherwise in rooms with multiple gaps
|
||||
//the water/air will always tend to flow through the first gap in the list,
|
||||
//which may lead to weird behavior like water draining down only through
|
||||
//one gap in a room even if there are several
|
||||
|
||||
foreach (Gap gap in gapList)
|
||||
{
|
||||
gap.ResetWaterFlowThisFrame();
|
||||
}
|
||||
// moved waterflow reset here to see if we can reduce at least some time
|
||||
{
|
||||
// PLEASE WORK
|
||||
Parallel.ForEach(gapList, parallelOptions, gap =>
|
||||
{
|
||||
gap.ResetWaterFlowThisFrame();
|
||||
gap.Update(deltaTime, cam);
|
||||
});
|
||||
},
|
||||
// Powered components update
|
||||
() =>
|
||||
{
|
||||
Powered.UpdatePower(deltaTime);
|
||||
}
|
||||
);
|
||||
|
||||
foreach (Gap gap in gapList)
|
||||
{
|
||||
gap.Update(deltaTime, cam);
|
||||
}
|
||||
|
||||
if (mapEntityUpdateTick % poweredUpdateInterval == 0)
|
||||
{
|
||||
Powered.UpdatePower(deltaTime * poweredUpdateInterval);
|
||||
}
|
||||
// Process any physics operations queued during Hull/Structure updates.
|
||||
// BallastFlora growth (from Hull.Update) may queue physics body creations/transforms.
|
||||
PhysicsBodyQueue.ProcessPendingOperations();
|
||||
|
||||
#if CLIENT
|
||||
// Hull Cheats need to be executed after Hull update
|
||||
@@ -723,42 +835,27 @@ namespace Barotrauma
|
||||
// Item update (Item.Update() is not thread-safe and must be executed on the main thread)
|
||||
Item.UpdatePendingConditionUpdates(deltaTime);
|
||||
|
||||
if (mapEntityUpdateTick % mapEntityUpdateInterval == 0)
|
||||
{
|
||||
float itemDeltaTime = deltaTime * mapEntityUpdateInterval;
|
||||
Item lastUpdatedItem = null;
|
||||
Item lastUpdatedItem = null;
|
||||
|
||||
try
|
||||
try
|
||||
{
|
||||
Parallel.ForEach(itemList, parallelOptions, item =>
|
||||
{
|
||||
foreach (Item item in itemList)
|
||||
{
|
||||
if (LuaCsSetup.Instance.Game.UpdatePriorityItems.Contains(item)) { continue; }
|
||||
lastUpdatedItem = item;
|
||||
item.Update(itemDeltaTime, cam);
|
||||
}
|
||||
}
|
||||
catch (InvalidOperationException e)
|
||||
{
|
||||
GameAnalyticsManager.AddErrorEventOnce(
|
||||
"MapEntity.UpdateAll:ItemUpdateInvalidOperation",
|
||||
GameAnalyticsManager.ErrorSeverity.Critical,
|
||||
$"Error while updating item {lastUpdatedItem?.Name ?? "null"}: {e.Message}");
|
||||
throw new InvalidOperationException($"Error while updating item {lastUpdatedItem?.Name ?? "null"}", innerException: e);
|
||||
}
|
||||
lastUpdatedItem = item;
|
||||
item.Update(deltaTime, cam);
|
||||
});
|
||||
}
|
||||
|
||||
foreach (var item in LuaCsSetup.Instance.Game.UpdatePriorityItems)
|
||||
catch (InvalidOperationException e)
|
||||
{
|
||||
if (item.Removed) { continue; }
|
||||
|
||||
item.Update(deltaTime, cam);
|
||||
GameAnalyticsManager.AddErrorEventOnce(
|
||||
"MapEntity.UpdateAll:ItemUpdateInvalidOperation",
|
||||
GameAnalyticsManager.ErrorSeverity.Critical,
|
||||
$"Error while updating item {lastUpdatedItem?.Name ?? "null"}: {e.Message}");
|
||||
throw new InvalidOperationException($"Error while updating item {lastUpdatedItem?.Name ?? "null"}", innerException: e);
|
||||
}
|
||||
|
||||
if (mapEntityUpdateTick % mapEntityUpdateInterval == 0)
|
||||
{
|
||||
UpdateAllProjSpecific(deltaTime * mapEntityUpdateInterval);
|
||||
Spawner?.Update();
|
||||
}
|
||||
UpdateAllProjSpecific(deltaTime);
|
||||
Spawner?.Update();
|
||||
|
||||
#if CLIENT
|
||||
sw.Stop();
|
||||
|
||||
@@ -7,6 +7,7 @@ using Microsoft.Xna.Framework;
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.Linq;
|
||||
using System.Threading;
|
||||
using System.Xml.Linq;
|
||||
using System.Collections.Immutable;
|
||||
using Barotrauma.Abilities;
|
||||
@@ -18,6 +19,63 @@ using Barotrauma.Lights;
|
||||
|
||||
namespace Barotrauma
|
||||
{
|
||||
/// <summary>
|
||||
/// Thread-safe wrapper for Structure list operations.
|
||||
/// Uses copy-on-write pattern for lock-free reads.
|
||||
/// </summary>
|
||||
internal class ThreadSafeStructureList : IEnumerable<Structure>
|
||||
{
|
||||
private volatile List<Structure> _list = new List<Structure>();
|
||||
private readonly object _writeLock = new object();
|
||||
|
||||
public int Count => _list.Count;
|
||||
|
||||
public void Add(Structure structure)
|
||||
{
|
||||
lock (_writeLock)
|
||||
{
|
||||
var newList = new List<Structure>(_list) { structure };
|
||||
Interlocked.Exchange(ref _list, newList);
|
||||
}
|
||||
}
|
||||
|
||||
public bool Remove(Structure structure)
|
||||
{
|
||||
lock (_writeLock)
|
||||
{
|
||||
var newList = new List<Structure>(_list);
|
||||
bool removed = newList.Remove(structure);
|
||||
if (removed)
|
||||
{
|
||||
Interlocked.Exchange(ref _list, newList);
|
||||
}
|
||||
return removed;
|
||||
}
|
||||
}
|
||||
|
||||
public void Clear()
|
||||
{
|
||||
Interlocked.Exchange(ref _list, new List<Structure>());
|
||||
}
|
||||
|
||||
public bool Contains(Structure structure) => _list.Contains(structure);
|
||||
|
||||
public Structure this[int index] => _list[index];
|
||||
|
||||
public IEnumerator<Structure> GetEnumerator() => _list.GetEnumerator();
|
||||
System.Collections.IEnumerator System.Collections.IEnumerable.GetEnumerator() => GetEnumerator();
|
||||
|
||||
// LINQ-friendly methods
|
||||
public List<Structure> ToList() => new List<Structure>(_list);
|
||||
public Structure FirstOrDefault(Func<Structure, bool> predicate) => _list.FirstOrDefault(predicate);
|
||||
public Structure Find(Predicate<Structure> predicate) => _list.Find(predicate);
|
||||
public List<Structure> FindAll(Predicate<Structure> predicate) => _list.FindAll(predicate);
|
||||
public IEnumerable<Structure> Where(Func<Structure, bool> predicate) => _list.Where(predicate);
|
||||
public bool Any() => _list.Any();
|
||||
public bool Any(Func<Structure, bool> predicate) => _list.Any(predicate);
|
||||
public void ForEach(Action<Structure> action) => _list.ForEach(action);
|
||||
}
|
||||
|
||||
partial class WallSection : IIgnorable
|
||||
{
|
||||
public Rectangle rect;
|
||||
@@ -48,7 +106,7 @@ namespace Barotrauma
|
||||
partial class Structure : MapEntity, IDamageable, IServerSerializable, ISerializableEntity
|
||||
{
|
||||
public const int WallSectionSize = 96;
|
||||
public static List<Structure> WallList = new List<Structure>();
|
||||
public static ThreadSafeStructureList WallList = new ThreadSafeStructureList();
|
||||
|
||||
const float LeakThreshold = 0.1f;
|
||||
const float BigGapThreshold = 0.7f;
|
||||
|
||||
@@ -10,12 +10,71 @@ using System.Collections.Generic;
|
||||
using System.Collections.Immutable;
|
||||
using System.Diagnostics;
|
||||
using System.Linq;
|
||||
using System.Threading;
|
||||
using System.Xml.Linq;
|
||||
using Barotrauma.PerkBehaviors;
|
||||
using Voronoi2;
|
||||
|
||||
namespace Barotrauma
|
||||
{
|
||||
/// <summary>
|
||||
/// Thread-safe wrapper for Submarine list operations.
|
||||
/// Uses copy-on-write pattern for lock-free reads.
|
||||
/// </summary>
|
||||
internal class ThreadSafeSubmarineList : IEnumerable<Submarine>
|
||||
{
|
||||
private volatile List<Submarine> _list = new List<Submarine>();
|
||||
private readonly object _writeLock = new object();
|
||||
|
||||
public int Count => _list.Count;
|
||||
|
||||
public void Add(Submarine submarine)
|
||||
{
|
||||
lock (_writeLock)
|
||||
{
|
||||
var newList = new List<Submarine>(_list) { submarine };
|
||||
Interlocked.Exchange(ref _list, newList);
|
||||
}
|
||||
}
|
||||
|
||||
public bool Remove(Submarine submarine)
|
||||
{
|
||||
lock (_writeLock)
|
||||
{
|
||||
var newList = new List<Submarine>(_list);
|
||||
bool removed = newList.Remove(submarine);
|
||||
if (removed)
|
||||
{
|
||||
Interlocked.Exchange(ref _list, newList);
|
||||
}
|
||||
return removed;
|
||||
}
|
||||
}
|
||||
|
||||
public void Clear()
|
||||
{
|
||||
Interlocked.Exchange(ref _list, new List<Submarine>());
|
||||
}
|
||||
|
||||
public bool Contains(Submarine submarine) => _list.Contains(submarine);
|
||||
|
||||
public Submarine this[int index] => _list[index];
|
||||
|
||||
public IEnumerator<Submarine> GetEnumerator() => _list.GetEnumerator();
|
||||
System.Collections.IEnumerator System.Collections.IEnumerable.GetEnumerator() => GetEnumerator();
|
||||
|
||||
// LINQ-friendly methods
|
||||
public List<Submarine> ToList() => new List<Submarine>(_list);
|
||||
public Submarine FirstOrDefault(Func<Submarine, bool> predicate) => _list.FirstOrDefault(predicate);
|
||||
public Submarine Find(Predicate<Submarine> predicate) => _list.Find(predicate);
|
||||
public List<Submarine> FindAll(Predicate<Submarine> predicate) => _list.FindAll(predicate);
|
||||
public IEnumerable<Submarine> Where(Func<Submarine, bool> predicate) => _list.Where(predicate);
|
||||
public bool Any() => _list.Any();
|
||||
public bool Any(Func<Submarine, bool> predicate) => _list.Any(predicate);
|
||||
public float Sum(Func<Submarine, float> selector) => _list.Sum(selector);
|
||||
public IEnumerable<TResult> Select<TResult>(Func<Submarine, TResult> selector) => _list.Select(selector);
|
||||
}
|
||||
|
||||
public enum Direction : byte
|
||||
{
|
||||
None = 0, Left = 1, Right = 2
|
||||
@@ -71,7 +130,7 @@ namespace Barotrauma
|
||||
get { return MainSubs[0]; }
|
||||
set { MainSubs[0] = value; }
|
||||
}
|
||||
private static readonly List<Submarine> loaded = new List<Submarine>();
|
||||
private static readonly ThreadSafeSubmarineList loaded = new ThreadSafeSubmarineList();
|
||||
|
||||
private readonly Identifier upgradeEventIdentifier;
|
||||
|
||||
@@ -96,10 +155,11 @@ namespace Barotrauma
|
||||
}
|
||||
}
|
||||
|
||||
private static Vector2 lastPickedPosition;
|
||||
private static float lastPickedFraction;
|
||||
private static Fixture lastPickedFixture;
|
||||
private static Vector2 lastPickedNormal;
|
||||
// ThreadLocal for thread-safe ray casting results
|
||||
private static readonly ThreadLocal<Vector2> lastPickedPositionLocal = new ThreadLocal<Vector2>();
|
||||
private static readonly ThreadLocal<float> lastPickedFractionLocal = new ThreadLocal<float>();
|
||||
private static readonly ThreadLocal<Fixture> lastPickedFixtureLocal = new ThreadLocal<Fixture>();
|
||||
private static readonly ThreadLocal<Vector2> lastPickedNormalLocal = new ThreadLocal<Vector2>();
|
||||
|
||||
private Vector2 prevPosition;
|
||||
|
||||
@@ -113,22 +173,22 @@ namespace Barotrauma
|
||||
|
||||
public static Vector2 LastPickedPosition
|
||||
{
|
||||
get { return lastPickedPosition; }
|
||||
get { return lastPickedPositionLocal.Value; }
|
||||
}
|
||||
|
||||
public static float LastPickedFraction
|
||||
{
|
||||
get { return lastPickedFraction; }
|
||||
get { return lastPickedFractionLocal.Value; }
|
||||
}
|
||||
|
||||
public static Fixture LastPickedFixture
|
||||
{
|
||||
get { return lastPickedFixture; }
|
||||
get { return lastPickedFixtureLocal.Value; }
|
||||
}
|
||||
|
||||
public static Vector2 LastPickedNormal
|
||||
{
|
||||
get { return lastPickedNormal; }
|
||||
get { return lastPickedNormalLocal.Value; }
|
||||
}
|
||||
|
||||
public bool Loading
|
||||
@@ -145,7 +205,7 @@ namespace Barotrauma
|
||||
|
||||
public List<WayPoint> ForcedOutpostModuleWayPoints = new List<WayPoint>();
|
||||
|
||||
public static List<Submarine> Loaded
|
||||
public static ThreadSafeSubmarineList Loaded
|
||||
{
|
||||
get { return loaded; }
|
||||
}
|
||||
@@ -832,6 +892,8 @@ namespace Barotrauma
|
||||
return null;
|
||||
}
|
||||
|
||||
if (GameMain.World == null) return null;
|
||||
|
||||
float closestFraction = 1.0f;
|
||||
Vector2 closestNormal = Vector2.Zero;
|
||||
Fixture closestFixture = null;
|
||||
@@ -839,25 +901,34 @@ namespace Barotrauma
|
||||
if (allowInsideFixture)
|
||||
{
|
||||
var aabb = new FarseerPhysics.Collision.AABB(rayStart - Vector2.One * 0.001f, rayStart + Vector2.One * 0.001f);
|
||||
GameMain.World.QueryAABB((fixture) =>
|
||||
try
|
||||
{
|
||||
if (!CheckFixtureCollision(fixture, ignoredBodies, collisionCategory, ignoreSensors, customPredicate)) { return true; }
|
||||
GameMain.World.QueryAABB((fixture) =>
|
||||
{
|
||||
if (fixture == null || fixture.Body == null) { return true; }
|
||||
|
||||
if (!CheckFixtureCollision(fixture, ignoredBodies, collisionCategory, ignoreSensors, customPredicate)) { return true; }
|
||||
|
||||
fixture.Body.GetTransform(out FarseerPhysics.Common.Transform transform);
|
||||
if (!fixture.Shape.TestPoint(ref transform, ref rayStart)) { return true; }
|
||||
fixture.Body.GetTransform(out FarseerPhysics.Common.Transform transform);
|
||||
if (!fixture.Shape.TestPoint(ref transform, ref rayStart)) { return true; }
|
||||
|
||||
closestFraction = 0.0f;
|
||||
closestNormal = Vector2.Normalize(rayEnd - rayStart);
|
||||
closestFixture = fixture;
|
||||
if (fixture.Body != null) { closestBody = fixture.Body; }
|
||||
return false;
|
||||
}, ref aabb);
|
||||
closestFraction = 0.0f;
|
||||
closestNormal = Vector2.Normalize(rayEnd - rayStart);
|
||||
closestFixture = fixture;
|
||||
if (fixture.Body != null) { closestBody = fixture.Body; }
|
||||
return false;
|
||||
}, ref aabb);
|
||||
}
|
||||
catch (NullReferenceException)
|
||||
{
|
||||
return null;
|
||||
}
|
||||
if (closestFraction <= 0.0f)
|
||||
{
|
||||
lastPickedPosition = rayStart;
|
||||
lastPickedFraction = closestFraction;
|
||||
lastPickedFixture = closestFixture;
|
||||
lastPickedNormal = closestNormal;
|
||||
lastPickedPositionLocal.Value = rayStart;
|
||||
lastPickedFractionLocal.Value = closestFraction;
|
||||
lastPickedFixtureLocal.Value = closestFixture;
|
||||
lastPickedNormalLocal.Value = closestNormal;
|
||||
return closestBody;
|
||||
}
|
||||
}
|
||||
@@ -876,16 +947,22 @@ namespace Barotrauma
|
||||
return fraction;
|
||||
}, rayStart, rayEnd, collisionCategory ?? Category.All);
|
||||
|
||||
lastPickedPosition = rayStart + (rayEnd - rayStart) * closestFraction;
|
||||
lastPickedFraction = closestFraction;
|
||||
lastPickedFixture = closestFixture;
|
||||
lastPickedNormal = closestNormal;
|
||||
lastPickedPositionLocal.Value = rayStart + (rayEnd - rayStart) * closestFraction;
|
||||
lastPickedFractionLocal.Value = closestFraction;
|
||||
lastPickedFixtureLocal.Value = closestFixture;
|
||||
lastPickedNormalLocal.Value = closestNormal;
|
||||
|
||||
return closestBody;
|
||||
}
|
||||
|
||||
private static readonly Dictionary<Body, float> bodyDist = new Dictionary<Body, float>();
|
||||
private static readonly List<Body> bodies = new List<Body>();
|
||||
// ThreadLocal for thread-safe body picking
|
||||
private static readonly ThreadLocal<Dictionary<Body, float>> bodyDistLocal =
|
||||
new ThreadLocal<Dictionary<Body, float>>(() => new Dictionary<Body, float>());
|
||||
private static readonly ThreadLocal<List<Body>> bodiesLocal =
|
||||
new ThreadLocal<List<Body>>(() => new List<Body>());
|
||||
|
||||
private static Dictionary<Body, float> bodyDist => bodyDistLocal.Value;
|
||||
private static List<Body> bodies => bodiesLocal.Value;
|
||||
|
||||
public static float LastPickedBodyDist(Body body)
|
||||
{
|
||||
@@ -919,10 +996,10 @@ namespace Barotrauma
|
||||
}
|
||||
if (fraction < closestFraction)
|
||||
{
|
||||
lastPickedPosition = rayStart + (rayEnd - rayStart) * fraction;
|
||||
lastPickedFraction = fraction;
|
||||
lastPickedNormal = normal;
|
||||
lastPickedFixture = fixture;
|
||||
lastPickedPositionLocal.Value = rayStart + (rayEnd - rayStart) * fraction;
|
||||
lastPickedFractionLocal.Value = fraction;
|
||||
lastPickedNormalLocal.Value = normal;
|
||||
lastPickedFixtureLocal.Value = fixture;
|
||||
}
|
||||
//continue
|
||||
return -1;
|
||||
@@ -940,10 +1017,10 @@ namespace Barotrauma
|
||||
if (!fixture.Shape.TestPoint(ref transform, ref rayStart)) { return true; }
|
||||
|
||||
closestFraction = 0.0f;
|
||||
lastPickedPosition = rayStart;
|
||||
lastPickedFraction = 0.0f;
|
||||
lastPickedNormal = Vector2.Normalize(rayEnd - rayStart);
|
||||
lastPickedFixture = fixture;
|
||||
lastPickedPositionLocal.Value = rayStart;
|
||||
lastPickedFractionLocal.Value = 0.0f;
|
||||
lastPickedNormalLocal.Value = Vector2.Normalize(rayEnd - rayStart);
|
||||
lastPickedFixtureLocal.Value = fixture;
|
||||
bodies.Add(fixture.Body);
|
||||
bodyDist[fixture.Body] = 0.0f;
|
||||
return false;
|
||||
@@ -1011,7 +1088,7 @@ namespace Barotrauma
|
||||
|
||||
if (Vector2.DistanceSquared(rayStart, rayEnd) < 0.01f)
|
||||
{
|
||||
lastPickedPosition = rayEnd;
|
||||
lastPickedPositionLocal.Value = rayEnd;
|
||||
return null;
|
||||
}
|
||||
|
||||
@@ -1053,10 +1130,10 @@ namespace Barotrauma
|
||||
, rayStart, rayEnd);
|
||||
|
||||
|
||||
lastPickedPosition = rayStart + (rayEnd - rayStart) * closestFraction;
|
||||
lastPickedFraction = closestFraction;
|
||||
lastPickedFixture = closestFixture;
|
||||
lastPickedNormal = closestNormal;
|
||||
lastPickedPositionLocal.Value = rayStart + (rayEnd - rayStart) * closestFraction;
|
||||
lastPickedFractionLocal.Value = closestFraction;
|
||||
lastPickedFixtureLocal.Value = closestFixture;
|
||||
lastPickedNormalLocal.Value = closestNormal;
|
||||
return closestBody;
|
||||
}
|
||||
|
||||
@@ -1077,7 +1154,7 @@ namespace Barotrauma
|
||||
|
||||
Item.UpdateHulls();
|
||||
|
||||
List<Item> bodyItems = Item.ItemList.FindAll(it => it.Submarine == this && it.body != null);
|
||||
List<Item> bodyItems = Item.ItemList.Where(it => it.Submarine == this && it.body != null).ToList();
|
||||
List<MapEntity> subEntities = MapEntity.MapEntityList.FindAll(me => me.Submarine == this);
|
||||
|
||||
foreach (MapEntity e in subEntities)
|
||||
@@ -1511,9 +1588,9 @@ namespace Barotrauma
|
||||
public List<WayPoint> GetWaypoints(bool alsoFromConnectedSubs) => GetEntities(alsoFromConnectedSubs, WayPoint.WayPointList);
|
||||
public List<Structure> GetWalls(bool alsoFromConnectedSubs) => GetEntities(alsoFromConnectedSubs, Structure.WallList);
|
||||
|
||||
public List<T> GetEntities<T>(bool includingConnectedSubs, List<T> list) where T : MapEntity
|
||||
public List<T> GetEntities<T>(bool includingConnectedSubs, IEnumerable<T> list) where T : MapEntity
|
||||
{
|
||||
return list.FindAll(e => IsEntityFoundOnThisSub(e, includingConnectedSubs));
|
||||
return list.Where(e => IsEntityFoundOnThisSub(e, includingConnectedSubs)).ToList();
|
||||
}
|
||||
|
||||
public List<(ItemContainer container, int freeSlots)> GetCargoContainers()
|
||||
@@ -1538,11 +1615,6 @@ namespace Barotrauma
|
||||
return containers;
|
||||
}
|
||||
|
||||
public IEnumerable<T> GetEntities<T>(bool includingConnectedSubs, IEnumerable<T> list) where T : MapEntity
|
||||
{
|
||||
return list.Where(e => IsEntityFoundOnThisSub(e, includingConnectedSubs));
|
||||
}
|
||||
|
||||
public bool IsEntityFoundOnThisSub(MapEntity entity, bool includingConnectedSubs, bool allowDifferentTeam = false, bool allowDifferentType = false)
|
||||
{
|
||||
if (entity == null) { return false; }
|
||||
@@ -1665,9 +1737,8 @@ namespace Barotrauma
|
||||
HiddenSubPosition += Vector2.UnitY * GameMain.GameSession.LevelData.Size.Y;
|
||||
}
|
||||
|
||||
for (int i = 0; i < loaded.Count; i++)
|
||||
foreach (Submarine sub in loaded)
|
||||
{
|
||||
Submarine sub = loaded[i];
|
||||
HiddenSubPosition =
|
||||
new Vector2(
|
||||
//1st sub on the left side, 2nd on the right, etc
|
||||
@@ -1799,10 +1870,9 @@ namespace Barotrauma
|
||||
}
|
||||
entityGrid = Hull.GenerateEntityGrid(this);
|
||||
|
||||
for (int i = 0; i < MapEntity.MapEntityList.Count; i++)
|
||||
foreach (MapEntity me in MapEntity.MapEntityList.Where(e => e.Submarine == this))
|
||||
{
|
||||
if (MapEntity.MapEntityList[i].Submarine != this) { continue; }
|
||||
MapEntity.MapEntityList[i].Move(HiddenSubPosition, ignoreContacts: true);
|
||||
me.Move(HiddenSubPosition, ignoreContacts: true);
|
||||
}
|
||||
|
||||
Loading = false;
|
||||
@@ -2147,7 +2217,7 @@ namespace Barotrauma
|
||||
DebugConsole.ThrowError("Error while removing \"" + item.Name + "\"!", e);
|
||||
}
|
||||
}
|
||||
Item.ItemList.Clear();
|
||||
Item.ClearAllItemCollections();
|
||||
}
|
||||
|
||||
Ragdoll.RemoveAll();
|
||||
@@ -2156,7 +2226,7 @@ namespace Barotrauma
|
||||
GameMain.World = null;
|
||||
|
||||
Powered.Grids.Clear();
|
||||
Powered.ChangedConnections.Clear();
|
||||
Powered.ClearChangedConnections();
|
||||
|
||||
GC.Collect();
|
||||
|
||||
@@ -2196,7 +2266,7 @@ namespace Barotrauma
|
||||
|
||||
ConnectedDockingPorts?.Clear();
|
||||
|
||||
Powered.ChangedConnections.Clear();
|
||||
Powered.ClearChangedConnections();
|
||||
Powered.Grids.Clear();
|
||||
|
||||
loaded.Remove(this);
|
||||
@@ -2233,6 +2303,7 @@ namespace Barotrauma
|
||||
/// </summary>
|
||||
public void DisableObstructedWayPoints()
|
||||
{
|
||||
|
||||
// Check collisions to level
|
||||
foreach (var node in OutdoorNodes)
|
||||
{
|
||||
@@ -2261,7 +2332,9 @@ namespace Barotrauma
|
||||
/// </summary>
|
||||
public void DisableObstructedWayPoints(Submarine otherSub)
|
||||
{
|
||||
if (otherSub == null) { return; }
|
||||
|
||||
if (otherSub?.PhysicsBody?.FarseerBody == null) return;
|
||||
|
||||
if (otherSub == this) { return; }
|
||||
// Check collisions to other subs.
|
||||
foreach (var node in OutdoorNodes)
|
||||
@@ -2278,14 +2351,21 @@ namespace Barotrauma
|
||||
{
|
||||
Vector2 start = ConvertUnits.ToSimUnits(wp.WorldPosition) - otherSub.SimPosition;
|
||||
Vector2 end = ConvertUnits.ToSimUnits(connectedWp.WorldPosition) - otherSub.SimPosition;
|
||||
var body = PickBody(start, end, null, Physics.CollisionWall, allowInsideFixture: true);
|
||||
if (body != null)
|
||||
try
|
||||
{
|
||||
if (body.UserData is Structure wall && !wall.IsPlatform || body.UserData is Item && body.FixtureList[0].CollisionCategories.HasFlag(Physics.CollisionWall))
|
||||
var body = PickBody(start, end, null, Physics.CollisionWall, allowInsideFixture: true);
|
||||
if (body != null)
|
||||
{
|
||||
isObstructed = true;
|
||||
if (body.UserData is Structure wall && !wall.IsPlatform || body.UserData is Item && body.FixtureList?[0].CollisionCategories.HasFlag(Physics.CollisionWall) == true)
|
||||
{
|
||||
isObstructed = true;
|
||||
}
|
||||
}
|
||||
}
|
||||
catch (NullReferenceException)
|
||||
{
|
||||
continue;
|
||||
}
|
||||
}
|
||||
if (isObstructed)
|
||||
{
|
||||
|
||||
@@ -5,17 +5,75 @@ using Microsoft.Xna.Framework;
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.Linq;
|
||||
using System.Threading;
|
||||
using System.Xml.Linq;
|
||||
using Barotrauma.Extensions;
|
||||
|
||||
namespace Barotrauma
|
||||
{
|
||||
/// <summary>
|
||||
/// Thread-safe wrapper for WayPoint list operations.
|
||||
/// Uses copy-on-write pattern for lock-free reads.
|
||||
/// </summary>
|
||||
internal class ThreadSafeWayPointList : IEnumerable<WayPoint>
|
||||
{
|
||||
private volatile List<WayPoint> _list = new List<WayPoint>();
|
||||
private readonly object _writeLock = new object();
|
||||
|
||||
public int Count => _list.Count;
|
||||
|
||||
public void Add(WayPoint waypoint)
|
||||
{
|
||||
lock (_writeLock)
|
||||
{
|
||||
var newList = new List<WayPoint>(_list) { waypoint };
|
||||
Interlocked.Exchange(ref _list, newList);
|
||||
}
|
||||
}
|
||||
|
||||
public bool Remove(WayPoint waypoint)
|
||||
{
|
||||
lock (_writeLock)
|
||||
{
|
||||
var newList = new List<WayPoint>(_list);
|
||||
bool removed = newList.Remove(waypoint);
|
||||
if (removed)
|
||||
{
|
||||
Interlocked.Exchange(ref _list, newList);
|
||||
}
|
||||
return removed;
|
||||
}
|
||||
}
|
||||
|
||||
public void Clear()
|
||||
{
|
||||
Interlocked.Exchange(ref _list, new List<WayPoint>());
|
||||
}
|
||||
|
||||
public bool Contains(WayPoint waypoint) => _list.Contains(waypoint);
|
||||
|
||||
public WayPoint this[int index] => _list[index];
|
||||
|
||||
public IEnumerator<WayPoint> GetEnumerator() => _list.GetEnumerator();
|
||||
System.Collections.IEnumerator System.Collections.IEnumerable.GetEnumerator() => GetEnumerator();
|
||||
|
||||
// LINQ-friendly methods
|
||||
public List<WayPoint> ToList() => new List<WayPoint>(_list);
|
||||
public WayPoint FirstOrDefault(Func<WayPoint, bool> predicate) => _list.FirstOrDefault(predicate);
|
||||
public WayPoint Find(Predicate<WayPoint> predicate) => _list.Find(predicate);
|
||||
public List<WayPoint> FindAll(Predicate<WayPoint> predicate) => _list.FindAll(predicate);
|
||||
public IEnumerable<WayPoint> Where(Func<WayPoint, bool> predicate) => _list.Where(predicate);
|
||||
public bool Any() => _list.Any();
|
||||
public bool Any(Func<WayPoint, bool> predicate) => _list.Any(predicate);
|
||||
public bool Exists(Predicate<WayPoint> predicate) => _list.Exists(predicate);
|
||||
}
|
||||
|
||||
[Flags]
|
||||
public enum SpawnType { Path = 0, Human = 1, Enemy = 2, Cargo = 4, Corpse = 8, Submarine = 16, ExitPoint = 32, Disabled = 64 };
|
||||
|
||||
partial class WayPoint : MapEntity
|
||||
{
|
||||
public static List<WayPoint> WayPointList = new List<WayPoint>();
|
||||
public static ThreadSafeWayPointList WayPointList = new ThreadSafeWayPointList();
|
||||
|
||||
public static bool ShowWayPoints = true, ShowSpawnPoints = true;
|
||||
|
||||
@@ -936,7 +994,7 @@ namespace Barotrauma
|
||||
|
||||
public static WayPoint GetRandom(SpawnType spawnType = SpawnType.Human, JobPrefab assignedJob = null, Submarine sub = null, bool useSyncedRand = false, string spawnPointTag = null, bool ignoreSubmarine = false)
|
||||
{
|
||||
return WayPointList.GetRandom(wp =>
|
||||
return WayPointList.ToList().GetRandom(wp =>
|
||||
(ignoreSubmarine || wp.Submarine == sub) &&
|
||||
//checking for the disabled flag is not strictly necessary because we check for equality of the spawn type,
|
||||
//but lets do that anyway in case we change the handling of the spawn type at some point
|
||||
|
||||
@@ -62,7 +62,7 @@ namespace Barotrauma.Networking
|
||||
DebugConsole.Log($"Changed client {Name}'s team to {teamID}.");
|
||||
if (GameMain.NetworkMember != null && GameMain.NetworkMember.IsServer)
|
||||
{
|
||||
GameMain.NetworkMember.LastClientListUpdateID++;
|
||||
GameMain.NetworkMember.IncrementLastClientListUpdateID();
|
||||
}
|
||||
teamID = value;
|
||||
}
|
||||
@@ -86,7 +86,7 @@ namespace Barotrauma.Networking
|
||||
{
|
||||
if (GameMain.NetworkMember != null && GameMain.NetworkMember.IsServer)
|
||||
{
|
||||
GameMain.NetworkMember.LastClientListUpdateID++;
|
||||
GameMain.NetworkMember.IncrementLastClientListUpdateID();
|
||||
if (value != null)
|
||||
{
|
||||
CharacterID = value.ID;
|
||||
@@ -154,7 +154,7 @@ namespace Barotrauma.Networking
|
||||
#endif
|
||||
if (GameMain.NetworkMember != null && GameMain.NetworkMember.IsServer)
|
||||
{
|
||||
GameMain.NetworkMember.LastClientListUpdateID++;
|
||||
GameMain.NetworkMember.IncrementLastClientListUpdateID();
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -178,7 +178,7 @@ namespace Barotrauma.Networking
|
||||
{
|
||||
if (GameMain.NetworkMember != null && GameMain.NetworkMember.IsServer)
|
||||
{
|
||||
GameMain.NetworkMember.LastClientListUpdateID++;
|
||||
GameMain.NetworkMember.IncrementLastClientListUpdateID();
|
||||
}
|
||||
inGame = value;
|
||||
}
|
||||
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user