diff --git a/Barotrauma/BarotraumaServer/ServerSource/Characters/Character.cs b/Barotrauma/BarotraumaServer/ServerSource/Characters/Character.cs index 6559594ec..4e3f0c5ec 100644 --- a/Barotrauma/BarotraumaServer/ServerSource/Characters/Character.cs +++ b/Barotrauma/BarotraumaServer/ServerSource/Characters/Character.cs @@ -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 (!GameMain.LuaCs.Game.overrideTraitors) @@ -71,7 +74,7 @@ namespace Barotrauma } } } - foreach (Client client in GameMain.Server.ConnectedClients) + foreach (Client client in clients) { if (client.InGame) { diff --git a/Barotrauma/BarotraumaServer/ServerSource/Characters/CharacterNetworking.cs b/Barotrauma/BarotraumaServer/ServerSource/Characters/CharacterNetworking.cs index 59c91a4a5..686a10dda 100644 --- a/Barotrauma/BarotraumaServer/ServerSource/Characters/CharacterNetworking.cs +++ b/Barotrauma/BarotraumaServer/ServerSource/Characters/CharacterNetworking.cs @@ -490,7 +490,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: @@ -746,7 +748,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); diff --git a/Barotrauma/BarotraumaServer/ServerSource/Events/EventActions/ConversationAction.cs b/Barotrauma/BarotraumaServer/ServerSource/Events/EventActions/ConversationAction.cs index 159c83310..27e63da4f 100644 --- a/Barotrauma/BarotraumaServer/ServerSource/Events/EventActions/ConversationAction.cs +++ b/Barotrauma/BarotraumaServer/ServerSource/Events/EventActions/ConversationAction.cs @@ -82,10 +82,13 @@ namespace Barotrauma private bool IsBlockedByAnotherConversation(IEnumerable 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 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)) { diff --git a/Barotrauma/BarotraumaServer/ServerSource/Events/EventActions/EventLogAction.cs b/Barotrauma/BarotraumaServer/ServerSource/Events/EventActions/EventLogAction.cs index b6dac422e..ac005dcad 100644 --- a/Barotrauma/BarotraumaServer/ServerSource/Events/EventActions/EventLogAction.cs +++ b/Barotrauma/BarotraumaServer/ServerSource/Events/EventActions/EventLogAction.cs @@ -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 targetClients = new List(); + foreach (var target in ParentEvent.GetTargets(TargetTag)) { - List targetClients = new List(); - 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? 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? 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); + } +} } \ No newline at end of file diff --git a/Barotrauma/BarotraumaServer/ServerSource/Events/EventActions/EventObjectiveAction.cs b/Barotrauma/BarotraumaServer/ServerSource/Events/EventActions/EventObjectiveAction.cs index 9b84d31dd..ade58531b 100644 --- a/Barotrauma/BarotraumaServer/ServerSource/Events/EventActions/EventObjectiveAction.cs +++ b/Barotrauma/BarotraumaServer/ServerSource/Events/EventActions/EventObjectiveAction.cs @@ -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); } } } } -} \ No newline at end of file +} diff --git a/Barotrauma/BarotraumaServer/ServerSource/Events/EventActions/HighlightAction.cs b/Barotrauma/BarotraumaServer/ServerSource/Events/EventActions/HighlightAction.cs index 98c06c3f9..72f06fbc6 100644 --- a/Barotrauma/BarotraumaServer/ServerSource/Events/EventActions/HighlightAction.cs +++ b/Barotrauma/BarotraumaServer/ServerSource/Events/EventActions/HighlightAction.cs @@ -14,8 +14,10 @@ partial class HighlightAction : EventAction IEnumerable? 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)); diff --git a/Barotrauma/BarotraumaServer/ServerSource/Events/EventActions/MissionAction.cs b/Barotrauma/BarotraumaServer/ServerSource/Events/EventActions/MissionAction.cs index 5369ec7e1..3025cb7d5 100644 --- a/Barotrauma/BarotraumaServer/ServerSource/Events/EventActions/MissionAction.cs +++ b/Barotrauma/BarotraumaServer/ServerSource/Events/EventActions/MissionAction.cs @@ -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); } } -} \ No newline at end of file +} diff --git a/Barotrauma/BarotraumaServer/ServerSource/Events/EventActions/StatusEffectAction.cs b/Barotrauma/BarotraumaServer/ServerSource/Events/EventActions/StatusEffectAction.cs index a8119efe2..c72bc512f 100644 --- a/Barotrauma/BarotraumaServer/ServerSource/Events/EventActions/StatusEffectAction.cs +++ b/Barotrauma/BarotraumaServer/ServerSource/Events/EventActions/StatusEffectAction.cs @@ -25,4 +25,4 @@ namespace Barotrauma } } } -} \ No newline at end of file +} diff --git a/Barotrauma/BarotraumaServer/ServerSource/Items/Components/Signal/CircuitBox.cs b/Barotrauma/BarotraumaServer/ServerSource/Items/Components/Signal/CircuitBox.cs index 9cc01bcbc..f54733a61 100644 --- a/Barotrauma/BarotraumaServer/ServerSource/Items/Components/Signal/CircuitBox.cs +++ b/Barotrauma/BarotraumaServer/ServerSource/Items/Components/Signal/CircuitBox.cs @@ -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)); } } -} \ No newline at end of file +} diff --git a/Barotrauma/BarotraumaServer/ServerSource/Map/Hull.cs b/Barotrauma/BarotraumaServer/ServerSource/Map/Hull.cs index cfb0592cd..1fa02b024 100644 --- a/Barotrauma/BarotraumaServer/ServerSource/Map/Hull.cs +++ b/Barotrauma/BarotraumaServer/ServerSource/Map/Hull.cs @@ -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)) ) { diff --git a/Barotrauma/BarotraumaServer/ServerSource/Networking/GameServer.cs b/Barotrauma/BarotraumaServer/ServerSource/Networking/GameServer.cs index 26eb6b890..e0ad602a0 100644 --- a/Barotrauma/BarotraumaServer/ServerSource/Networking/GameServer.cs +++ b/Barotrauma/BarotraumaServer/ServerSource/Networking/GameServer.cs @@ -4799,7 +4799,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); } @@ -4810,8 +4812,8 @@ namespace Barotrauma.Networking GameMain.LuaCs?.Hook?.Call("serverLog", 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 diff --git a/Barotrauma/BarotraumaShared/SharedSource/Map/Levels/LevelObjects/LevelTrigger.cs b/Barotrauma/BarotraumaShared/SharedSource/Map/Levels/LevelObjects/LevelTrigger.cs index 261a21706..4731c255a 100644 --- a/Barotrauma/BarotraumaShared/SharedSource/Map/Levels/LevelObjects/LevelTrigger.cs +++ b/Barotrauma/BarotraumaShared/SharedSource/Map/Levels/LevelObjects/LevelTrigger.cs @@ -673,13 +673,17 @@ namespace Barotrauma } } - private static readonly List triggerersToRemove = new List(); public static void RemoveInActiveTriggerers(PhysicsBody physicsBody, HashSet 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(); + + // Create snapshot to avoid concurrent modification during enumeration + var triggererSnapshot = triggerers.ToArray(); + + foreach (var triggerer in triggererSnapshot) { if (triggerer.Removed) { diff --git a/Barotrauma/BarotraumaShared/SharedSource/Physics/PhysicsBody.cs b/Barotrauma/BarotraumaShared/SharedSource/Physics/PhysicsBody.cs index cfeac247b..2cb61b2a0 100644 --- a/Barotrauma/BarotraumaShared/SharedSource/Physics/PhysicsBody.cs +++ b/Barotrauma/BarotraumaShared/SharedSource/Physics/PhysicsBody.cs @@ -834,6 +834,12 @@ namespace Barotrauma if (!IsValidValue(simPosition, "position", -1e10f, 1e10f)) { return false; } if (!IsValidValue(rotation, "rotation")) { return false; } + if (PhysicsBodyQueue.IsInParallelContext) + { + PhysicsBodyQueue.Enqueue(() => SetTransform(simPosition, rotation, setPrevTransform)); + return true; + } + FarseerBody.SetTransform(simPosition, rotation); if (setPrevTransform) { SetPrevTransform(simPosition, rotation); } return true; @@ -848,6 +854,12 @@ namespace Barotrauma if (!IsValidValue(simPosition, "position", -1e10f, 1e10f)) { return false; } if (!IsValidValue(rotation, "rotation")) { return false; } + if (PhysicsBodyQueue.IsInParallelContext) + { + PhysicsBodyQueue.Enqueue(() => SetTransformIgnoreContacts(simPosition, rotation, setPrevTransform)); + return true; + } + FarseerBody.SetTransformIgnoreContacts(ref simPosition, rotation); if (setPrevTransform) { SetPrevTransform(simPosition, rotation); } return true;