diff --git a/.github/workflows/publish-release.yml b/.github/workflows/publish-release.yml index db119d882..3b7949d12 100644 --- a/.github/workflows/publish-release.yml +++ b/.github/workflows/publish-release.yml @@ -4,6 +4,20 @@ name: Publish release on: workflow_dispatch: + inputs: + target: + description: "The git ref to checkout, build from and release" + required: true + type: string + tag: + description: "The tag of the release" + required: true + type: string + prerelease: + description: "Prerelease" + required: false + default: false + type: boolean workflow_call: inputs: target: @@ -23,13 +37,13 @@ on: env: CI_DIR: 2049ef39-42a2-46d2-b513-ee6d2e3a7b15 RELEASES: | - windows:client:Windows/Client windows:server:Windows/Server - linux:client:Linux/Client linux:server:Linux/Server - mac:client:Mac/Client/Barotrauma.app/Contents/MacOS mac:server:Mac/Server - ARCHIVE_BASE_NAME: luacsforbarotrauma + windows:client:Windows/Client + linux:client:Linux/Client + mac:client:Mac/Client/Barotrauma.app/Contents/MacOS + ARCHIVE_BASE_NAME: luacsforbarotraumaEP # XXX: these file names are subject to shell expansion. # Be careful when using special characters. ARCHIVE_FILES_SERVER: | diff --git a/.gitignore b/.gitignore index 8a9348a93..b4b5f1901 100644 --- a/.gitignore +++ b/.gitignore @@ -60,3 +60,4 @@ Deploy/DeployAll/PrivateKey.* #Rider *.DotSettings.user +.vscode/settings.json diff --git a/Barotrauma/BarotraumaClient/ClientSource/DebugConsole.cs b/Barotrauma/BarotraumaClient/ClientSource/DebugConsole.cs index 47486a84b..e1ffe6bc9 100644 --- a/Barotrauma/BarotraumaClient/ClientSource/DebugConsole.cs +++ b/Barotrauma/BarotraumaClient/ClientSource/DebugConsole.cs @@ -3557,6 +3557,11 @@ namespace Barotrauma ContentPackageManager.RegularPackages.Select(p => p.Name).ToArray() })); + commands.Add(new Command("ShowServerPerf", "Immediately log server performance info", (string[] args) => + { + // TODO: Not yet :) + })); + #if WINDOWS commands.Add(new Command("startdedicatedserver", "", (string[] args) => { @@ -3590,6 +3595,14 @@ namespace Barotrauma } }));*/ + AssignOnClientExecute( + "ShowServerPerf", + (string[] args) => + { + GameMain.Client?.SendConsoleCommand("ShowServerPerf"); + } + ); + AssignOnClientExecute( "giveperm", (string[] args) => diff --git a/Barotrauma/BarotraumaClient/ClientSource/Screens/MainMenuScreen/MainMenuScreen.cs b/Barotrauma/BarotraumaClient/ClientSource/Screens/MainMenuScreen/MainMenuScreen.cs index 730343d8a..6bb1ee978 100644 --- a/Barotrauma/BarotraumaClient/ClientSource/Screens/MainMenuScreen/MainMenuScreen.cs +++ b/Barotrauma/BarotraumaClient/ClientSource/Screens/MainMenuScreen/MainMenuScreen.cs @@ -49,6 +49,9 @@ namespace Barotrauma private GUITextBox serverNameBox, passwordBox, maxPlayersBox; private GUITickBox isPublicBox, wrongPasswordBanBox, karmaBox; private GUIDropDown languageDropdown, serverExecutableDropdown; +#if DEBUG + private GUITickBox lenientHandshakeBox; +#endif private readonly GUIButton joinServerButton, hostServerButton; private readonly GUIFrame modsButtonContainer; @@ -1127,6 +1130,13 @@ namespace Barotrauma int ownerKey = Math.Max(CryptoRandom.Instance.Next(), 1); arguments.Add("-ownerkey"); arguments.Add(ownerKey.ToString()); +#if DEBUG + if (lenientHandshakeBox.Selected) + { + arguments.Add("-lenienthandshake"); + NetConfig.UseLenientHandshake = true; + } +#endif if (NetConfig.UseLenientHandshake) { @@ -1601,6 +1611,14 @@ namespace Barotrauma ToolTip = TextManager.Get("hostserverkarmasettingtooltip") }; +#if DEBUG + lenientHandshakeBox = new GUITickBox(new RectTransform(new Vector2(0.5f, 1.0f), tickboxAreaLower.RectTransform), "DEBUG: Lenient server startup timeouts") + { + Selected = true, + ToolTip = "Start with more lenient Lidgren handshake timeouts. The server is more likely to start even when running multiple instances on the same machine under heavy load." + }; +#endif + tickboxAreaLower.RectTransform.IsFixedSize = true; //spacing diff --git a/Barotrauma/BarotraumaServer/ServerSource/DebugConsole.cs b/Barotrauma/BarotraumaServer/ServerSource/DebugConsole.cs index 7cfc867d5..76bcea768 100644 --- a/Barotrauma/BarotraumaServer/ServerSource/DebugConsole.cs +++ b/Barotrauma/BarotraumaServer/ServerSource/DebugConsole.cs @@ -2748,6 +2748,19 @@ namespace Barotrauma } ); + commands.Add(new Command("ShowServerPerf", "Immediately log server performance info in ServerMessage", (string[] args) => + { + GameServer.Log(PerformanceMonitor.PM.ToString(), ServerLog.MessageType.ServerMessage); + })); + + AssignOnClientRequestExecute( + "ShowServerPerf", + (senderClient, cursorWorldPos, args) => + { + GameMain.Server.SendConsoleMessage(PerformanceMonitor.PM.ToString(), senderClient); + } + ); + #if DEBUG commands.Add(new Command("spamevents", "A debug command that creates a ton of entity events.", (string[] args) => { diff --git a/Barotrauma/BarotraumaServer/ServerSource/GameMain.cs b/Barotrauma/BarotraumaServer/ServerSource/GameMain.cs index a78426763..d6ba7e980 100644 --- a/Barotrauma/BarotraumaServer/ServerSource/GameMain.cs +++ b/Barotrauma/BarotraumaServer/ServerSource/GameMain.cs @@ -13,6 +13,7 @@ using System.Xml.Linq; using MoonSharp.Interpreter; using System.Net; using Barotrauma.Extensions; +using System.Threading.Tasks; using Barotrauma.LuaCs.Events; namespace Barotrauma @@ -329,8 +330,10 @@ namespace Barotrauma } Stopwatch performanceCounterTimer = Stopwatch.StartNew(); - stopwatch = Stopwatch.StartNew(); + + PerformanceMonitor PM = new PerformanceMonitor(); + long prevTicks = stopwatch.ElapsedTicks; while (ShouldRun) { @@ -380,6 +383,7 @@ namespace Barotrauma Timing.Accumulator -= Timing.Step; updateCount++; + PM.Update(); } #if !DEBUG @@ -427,6 +431,9 @@ namespace Barotrauma updateCount = 0; } } + + PerformanceMonitor.PM.Dispose(); + stopwatch.Stop(); CloseServer(); diff --git a/Barotrauma/BarotraumaServer/ServerSource/LuaCs/LuaCsInstaller.cs b/Barotrauma/BarotraumaServer/ServerSource/LuaCs/LuaCsInstaller.cs index b4a00dc11..2ed47b660 100644 --- a/Barotrauma/BarotraumaServer/ServerSource/LuaCs/LuaCsInstaller.cs +++ b/Barotrauma/BarotraumaServer/ServerSource/LuaCs/LuaCsInstaller.cs @@ -14,7 +14,7 @@ namespace Barotrauma if (luaPackage == null) { - GameMain.Server.SendChatMessage("Couldn't find the LuaCsForBarotrauma content package.", ChatMessageType.ServerMessageBox); + GameMain.Server.SendChatMessage("Couldn't find the ProjectEP package.", ChatMessageType.ServerMessageBox); return; } @@ -52,7 +52,7 @@ namespace Barotrauma } catch (UnauthorizedAccessException e) { - LuaCsLogger.LogError($"Unauthorized file access exception. This usually means you already have LuaCs installed. ${e}", LuaCsMessageOrigin.LuaCs); + LuaCsLogger.LogError($"Unauthorized file access exception. This usually means you already have ProjectEP installed. ${e}", LuaCsMessageOrigin.LuaCs); return; } @@ -63,7 +63,7 @@ namespace Barotrauma return; } - GameMain.Server.SendChatMessage("Client-Side LuaCs installed, restart your game to apply changes.", ChatMessageType.ServerMessageBox); + GameMain.Server.SendChatMessage("Client-Side ProjectEP installed, restart your game to apply changes.", ChatMessageType.ServerMessageBox); } } } diff --git a/Barotrauma/BarotraumaServer/ServerSource/Networking/GameServer.cs b/Barotrauma/BarotraumaServer/ServerSource/Networking/GameServer.cs index 1553753cd..ace0c1a8f 100644 --- a/Barotrauma/BarotraumaServer/ServerSource/Networking/GameServer.cs +++ b/Barotrauma/BarotraumaServer/ServerSource/Networking/GameServer.cs @@ -1153,7 +1153,9 @@ namespace Barotrauma.Networking } else { - KickClient(c, errorStr); + //Is it necessary to kick a client for a non-existing entity? + //there are plenty of things have been done if received an non-existing entity update. + //KickClient(c, errorStr); } } @@ -1217,18 +1219,25 @@ namespace Barotrauma.Networking errorLines.Add(""); errorLines.Add("EntitySpawner events:"); - foreach (var entityEvent in entityEventManager.UniqueEvents) + try { - if (entityEvent.Entity is EntitySpawner) + foreach (var entityEvent in entityEventManager.UniqueEvents.ToList()) { - var spawnData = entityEvent.Data as EntitySpawner.SpawnOrRemove; - errorLines.Add( - entityEvent.ID + ": " + - (spawnData is EntitySpawner.RemoveEntity ? "Remove " : "Create ") + - spawnData.Entity.ToString() + - " (" + spawnData.ID + ", " + spawnData.Entity.ID + ")"); + if (entityEvent.Entity is EntitySpawner) + { + var spawnData = entityEvent.Data as EntitySpawner.SpawnOrRemove; + errorLines.Add( + entityEvent.ID + ": " + + (spawnData is EntitySpawner.RemoveEntity ? "Remove " : "Create ") + + spawnData.Entity.ToString() + + " (" + spawnData.ID + ", " + spawnData.Entity.ID + ")"); + } } } + catch + { + errorLines.Add("Failed to write EntitySpawner events."); + } errorLines.Add(""); errorLines.Add("Last debug messages:"); diff --git a/Barotrauma/BarotraumaServer/ServerSource/Networking/NetEntityEvent/ServerEntityEventManager.cs b/Barotrauma/BarotraumaServer/ServerSource/Networking/NetEntityEvent/ServerEntityEventManager.cs index f49f20697..f27ed0706 100644 --- a/Barotrauma/BarotraumaServer/ServerSource/Networking/NetEntityEvent/ServerEntityEventManager.cs +++ b/Barotrauma/BarotraumaServer/ServerSource/Networking/NetEntityEvent/ServerEntityEventManager.cs @@ -1,15 +1,22 @@ using Barotrauma.Extensions; using Microsoft.Xna.Framework; using System; +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 { private IServerSerializable serializable; - + #if DEBUG public string StackTrace; #endif @@ -44,6 +51,8 @@ namespace Barotrauma.Networking class ServerEntityEventManager : NetEntityEventManager { + static public ServerEntityEventManager SEM; + private readonly List events; //list of unique events (i.e. !IsDuplicate) created during the round @@ -102,60 +111,137 @@ namespace Barotrauma.Networking private readonly GameServer server; private double lastEventCountHighWarning; - - public ServerEntityEventManager(GameServer server) + private class PendingCreateEvent { - events = new List(); - - this.server = server; - - bufferedEvents = new List(); - - uniqueEvents = new List(); - - lastWarningTime = -10.0; + public IServerSerializable Entity; + public NetEntityEvent.IData Data; + public PendingCreateEvent(IServerSerializable entity, NetEntityEvent.IData data) + { + Entity = entity; + Data = data; + } } + private readonly ConcurrentQueue 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(); + this.server = server; + bufferedEvents = new List(); + uniqueEvents = new List(); + pendingCreateQueue = new ConcurrentQueue(); + lastWarningTime = -10.0; + SEM = this; + + createEventTask = Task.Run(() => CreateEventProcessorLoop(cancellationTokenSource.Token)); + } + + private async Task CreateEventProcessorLoop(CancellationToken token) + { + while (!token.IsCancellationRequested) + { + try + { + await eventSignal.WaitAsync(100, token); + ProcessPendingCreateEvents(); + } + catch (OperationCanceledException) + { + break; + } + } + } + + private void ProcessPendingCreateEvents() + { + // 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; + var extraData = pending.Data; + + var newEvent = new ServerEntityEvent(entity, (UInt16)(ID + 1)); + if (extraData != null) newEvent.SetData(extraData); + + bool inGameClientsPresent = server.ConnectedClients.Count(c => c.InGame) > 0; + //remove old events that have been sent to all clients, they are redundant now + // keep at least one event in the list (lastSentToAll == e.ID) so we can use it to keep track of the latest ID + // and events less than 15 seconds old to give disconnected clients a bit of time to reconnect without getting desynced + if (GameMain.GameSession.RoundDuration > server.ServerSettings.RoundStartSyncDuration) + { + events.RemoveAll(e => + (NetIdUtils.IdMoreRecent(lastSentToAll, e.ID) || !inGameClientsPresent) && + e.CreateTime < Timing.TotalTime - server.ServerSettings.EventRemovalTime); + } + + bool duplicateFound = false; + for (int i = events.Count - 1; i >= 0; i--) + { + //we already have an identical event that's waiting to be sent + // -> no need to add a new one + if (events[i].IsDuplicate(newEvent) && !events[i].Sent) + { + duplicateFound = true; + break; + } + } + + if (duplicateFound) { continue; } + + ID++; + + events.Add(newEvent); + + if (!uniqueEvents.Any(e => e.IsDuplicate(newEvent))) + { + //create a copy of the event and give it a new ID + var uniqueEvent = new ServerEntityEvent(entity, (UInt16)(uniqueEvents.Count + 1)); + uniqueEvent.SetData(extraData); + + uniqueEvents.Add(uniqueEvent); + } + } + } public void CreateEvent(IServerSerializable entity, NetEntityEvent.IData extraData = null) { if (!ValidateEntity(entity)) { return; } - var newEvent = new ServerEntityEvent(entity, (UInt16)(ID + 1)); - if (extraData != null) newEvent.SetData(extraData); - - bool inGameClientsPresent = server.ConnectedClients.Count(c => c.InGame) > 0; - - //remove old events that have been sent to all clients, they are redundant now - // keep at least one event in the list (lastSentToAll == e.ID) so we can use it to keep track of the latest ID - // and events less than 15 seconds old to give disconnected clients a bit of time to reconnect without getting desynced - if (GameMain.GameSession.RoundDuration > server.ServerSettings.RoundStartSyncDuration) - { - events.RemoveAll(e => - (NetIdUtils.IdMoreRecent(lastSentToAll, e.ID) || !inGameClientsPresent) && - e.CreateTime < Timing.TotalTime - server.ServerSettings.EventRemovalTime); - } + // enqueue and let background task handle the rest + pendingCreateQueue.Enqueue(new PendingCreateEvent(entity, extraData)); - for (int i = events.Count - 1; i >= 0; i--) + if (eventSignal.CurrentCount == 0) { - //we already have an identical event that's waiting to be sent - // -> no need to add a new one - if (events[i].IsDuplicate(newEvent) && !events[i].Sent) return; - } - - ID++; - - events.Add(newEvent); - - if (!uniqueEvents.Any(e => e.IsDuplicate(newEvent))) - { - //create a copy of the event and give it a new ID - var uniqueEvent = new ServerEntityEvent(entity, (UInt16)(uniqueEvents.Count + 1)); - uniqueEvent.SetData(extraData); - - uniqueEvents.Add(uniqueEvent); + eventSignal.Release(); } } + public void Dispose() + { + 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 clients) { foreach (BufferedEvent bufferedEvent in bufferedEvents) @@ -203,29 +289,58 @@ namespace Barotrauma.Networking bufferedEvent.IsProcessed = true; } - var inGameClients = clients.FindAll(c => c.InGame && !c.NeedsMidRoundSync); - if (inGameClients.Count > 0) + List inGameClients = null; + List midRoundSyncClients = null; + Client ownerClient = null; + + foreach (var c in clients) { - lastSentToAnyone = inGameClients[0].LastRecvEntityEventID; - lastSentToAll = inGameClients[0].LastRecvEntityEventID; - - if (server.OwnerConnection != null) + if (c.InGame) { - var owner = clients.Find(c => c.Connection == server.OwnerConnection); - if (owner != null) + if (c.NeedsMidRoundSync) { - lastSentToAll = owner.LastRecvEntityEventID; + (midRoundSyncClients ??= new List()).Add(c); + } + else + { + (inGameClients ??= new List()).Add(c); } } - inGameClients.ForEach(c => + if (server.OwnerConnection != null && c.Connection == server.OwnerConnection) { - if (NetIdUtils.IdMoreRecent(lastSentToAll, c.LastRecvEntityEventID)) { lastSentToAll = c.LastRecvEntityEventID; } - if (NetIdUtils.IdMoreRecent(c.LastRecvEntityEventID, lastSentToAnyone)) { lastSentToAnyone = c.LastRecvEntityEventID; } - }); - lastSentToAnyoneTime = events.Find(e => e.ID == lastSentToAnyone)?.CreateTime ?? Timing.TotalTime; + ownerClient = c; + } + } - if (Timing.TotalTime - lastWarningTime > 5.0 && - Timing.TotalTime - lastSentToAnyoneTime > 10.0 && + if (inGameClients != null && inGameClients.Count > 0) + { + lastSentToAnyone = inGameClients[0].LastRecvEntityEventID; + lastSentToAll = ownerClient?.LastRecvEntityEventID ?? inGameClients[0].LastRecvEntityEventID; + + foreach (var c in inGameClients) + { + if (NetIdUtils.IdMoreRecent(lastSentToAll, c.LastRecvEntityEventID)) + { + lastSentToAll = c.LastRecvEntityEventID; + } + if (NetIdUtils.IdMoreRecent(c.LastRecvEntityEventID, lastSentToAnyone)) + { + lastSentToAnyone = c.LastRecvEntityEventID; + } + } + + try + { + lastSentToAnyoneTime = events.ToList().Find(e => e.ID == lastSentToAnyone)?.CreateTime ?? Timing.TotalTime; + } + catch + { + lastSentToAnyoneTime = Timing.TotalTime; + } + + + if (Timing.TotalTime - lastWarningTime > 5.0 && + Timing.TotalTime - lastSentToAnyoneTime > 10.0 && GameMain.GameSession.RoundDuration > server.ServerSettings.RoundStartSyncDuration) { lastWarningTime = Timing.TotalTime; @@ -235,10 +350,19 @@ namespace Barotrauma.Networking events.ForEach(e => e.ResetCreateTime()); //TODO: reset clients if this happens, maybe do it if a majority are behind rather than all of them? } - + 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 && ((lastSentToAnyoneTime - firstEventToResend.CreateTime) > server.ServerSettings.OldReceivedEventKickTime || (Timing.TotalTime - firstEventToResend.CreateTime) > server.ServerSettings.OldEventKickTime)) @@ -247,19 +371,19 @@ namespace Barotrauma.Networking // kick everyone that hasn't received it yet, this is way too old // UNLESS the event was created when the client was still midround syncing, // in which case we'll wait until the timeout runs out before kicking the client - List toKick = inGameClients.FindAll(c => + List toKick = inGameClients.FindAll(c => NetIdUtils.IdMoreRecent((UInt16)(lastSentToAll + 1), c.LastRecvEntityEventID) && (!c.NeedsMidRoundSync || firstEventToResend.CreateTime > c.MidRoundSyncTimeOut || lastSentToAnyoneTime > c.MidRoundSyncTimeOut || Timing.TotalTime > c.MidRoundSyncTimeOut + 10.0)); toKick.ForEach(c => - { - DebugConsole.NewMessage(c.Name + " was kicked because they were expecting a very old network event (" + (c.LastRecvEntityEventID + 1).ToString() + ")", Color.Red); - GameServer.Log(GameServer.ClientLogName(c) + " was kicked because they were expecting a very old network event (" - + (c.LastRecvEntityEventID + 1).ToString() + - " (created " + (Timing.TotalTime - firstEventToResend.CreateTime).ToString("0.##") + " s ago, " + - (lastSentToAnyoneTime - firstEventToResend.CreateTime).ToString("0.##") + " s older than last event sent to anyone)" + - " Events queued: " + events.Count + ", last sent to all: " + lastSentToAll, ServerLog.MessageType.Error); - server.DisconnectClient(c, PeerDisconnectPacket.WithReason(DisconnectReason.ExcessiveDesyncOldEvent)); - } + { + DebugConsole.NewMessage(c.Name + " was kicked because they were expecting a very old network event (" + (c.LastRecvEntityEventID + 1).ToString() + ")", Color.Red); + GameServer.Log(GameServer.ClientLogName(c) + " was kicked because they were expecting a very old network event (" + + (c.LastRecvEntityEventID + 1).ToString() + + " (created " + (Timing.TotalTime - firstEventToResend.CreateTime).ToString("0.##") + " s ago, " + + (lastSentToAnyoneTime - firstEventToResend.CreateTime).ToString("0.##") + " s older than last event sent to anyone)" + + " Events queued: " + events.Count + ", last sent to all: " + lastSentToAll, ServerLog.MessageType.Error); + server.DisconnectClient(c, PeerDisconnectPacket.WithReason(DisconnectReason.ExcessiveDesyncOldEvent)); + } ); } @@ -277,11 +401,21 @@ namespace Barotrauma.Networking } } - var timedOutClients = clients.FindAll(c => c.Connection != GameMain.Server.OwnerConnection && c.InGame && c.NeedsMidRoundSync && Timing.TotalTime > c.MidRoundSyncTimeOut); - foreach (Client timedOutClient in timedOutClients) + if (midRoundSyncClients != null) { - GameServer.Log("Disconnecting client " + GameServer.ClientLogName(timedOutClient) + ". Syncing the client with the server took too long.", ServerLog.MessageType.Error); - GameMain.Server.DisconnectClient(timedOutClient, PeerDisconnectPacket.WithReason(DisconnectReason.SyncTimeout)); + foreach (var c in midRoundSyncClients) + { + if (NetIdUtils.IdMoreRecent(lastSentToAll, c.FirstNewEventID)) + { + lastSentToAll = (ushort)(c.FirstNewEventID - 1); + } + + if (c.Connection != GameMain.Server.OwnerConnection && Timing.TotalTime > c.MidRoundSyncTimeOut) + { + GameServer.Log("Disconnecting client " + GameServer.ClientLogName(c) + ". Syncing took too long.", ServerLog.MessageType.Error); + GameMain.Server.DisconnectClient(c, PeerDisconnectPacket.WithReason(DisconnectReason.SyncTimeout)); + } + } } bufferedEvents.RemoveAll(b => b.IsProcessed); @@ -346,7 +480,7 @@ namespace Barotrauma.Networking if (client.NeedsMidRoundSync) { - segmentTable.StartNewSegment(ServerNetSegment.EntityEventInitial); + segmentTable.StartNewSegment(ServerNetSegment.EntityEventInitial); msg.WriteUInt16(client.UnreceivedEntityEventCount); msg.WriteUInt16(client.FirstNewEventID); @@ -553,10 +687,10 @@ namespace Barotrauma.Networking { var clientEntity = entity as IClientSerializable; if (clientEntity == null) return; - + clientEntity.ServerEventRead(buffer, sender); } - + public void Clear() { ID = 0; diff --git a/Barotrauma/BarotraumaServer/ServerSource/PerformenceMonitor.cs b/Barotrauma/BarotraumaServer/ServerSource/PerformenceMonitor.cs new file mode 100644 index 000000000..f67ce80ed --- /dev/null +++ b/Barotrauma/BarotraumaServer/ServerSource/PerformenceMonitor.cs @@ -0,0 +1,180 @@ +using Barotrauma.Networking; +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 +{ + public class PerformanceMonitor + { + static public PerformanceMonitor PM; + + private Stopwatch PMStopwatch = new Stopwatch(); + + private double tickratetimer = 0; + + private double tickrate60stimer = 0; + + private static Queue tickrate60s = new Queue(61); + + public int ItemCount + { + get{ return Item.ItemList.Count; } + } + + public int CharacterCount + { + get { return Character.CharacterList.Count; } + } + + public int PhysicsBodyCount + { + get { return PhysicsBody.List.Count; } + } + public int ConnectClients + { + get { return GameMain.Server.ConnectedClients.Count; } + } + + public double RealTickRate + { + get; set; + } + + public long TotalTicks + { + get;set; + } + + public int LastSecondTicks + { + get; set; + } = 0; + + public float AverageTickRate + { + get + { + return TotalTicks / (float)TotalTimeElapsed * 1000; + } + } + + public double AverageTickRate10s + { + get + { + return tickrate60s.Count > 0 ? tickrate60s.Average() : 60; + } + } + + public double TotalTimeElapsed + { + get + { + return PMStopwatch.Elapsed.TotalMilliseconds; + } + } + + public TimeSpan TimeElapsed + { + get + { + return TimeSpan.FromMilliseconds(TotalTimeElapsed); + } + } + + public float MemoryUsage + { + get + { + Process proc = Process.GetCurrentProcess(); + float memory = MathF.Round(proc.PrivateMemorySize64 / (1024 * 1024), 2); + proc.Dispose(); + + return memory; + } + } + + public double TickRateLow + { + get; set; + } + + public double TickRateHigh + { + get; set; + } + + public PerformanceMonitor() + { + PM = this; + RealTickRate = 60; + TotalTicks = 0; + LastSecondTicks = 60; + TickRateLow = 60; + TickRateHigh = 60; + PMStopwatch.Start(); + } + + public void Update() + { + TotalTicks += 1; + LastSecondTicks += 1; + if (tickrate60s.Count > 60) + { + tickrate60s.Dequeue(); + } + if (TotalTimeElapsed - 1000 >= tickratetimer) + { + RealTickRate = LastSecondTicks / (TotalTimeElapsed - tickratetimer) * 1000; + tickrate60s.Enqueue(RealTickRate); + tickratetimer = TotalTimeElapsed; + LastSecondTicks = 0; + } + if (TotalTimeElapsed - 60000 >= tickrate60stimer) + { + GameServer.Log(PM.ToString(), ServerLog.MessageType.ServerMessage); + TickRateLow = 60; + TickRateHigh = 60; + tickrate60stimer = TotalTimeElapsed; + } + if (RealTickRate > TickRateHigh) + { + TickRateHigh = RealTickRate; + } + if (RealTickRate < TickRateLow) + { + TickRateLow = RealTickRate; + } + } + + public void Dispose() + { + PMStopwatch.Reset(); + PM = null; + } + override public string ToString() + { + return $"Server Performance Info \n" + + $"Item Count: {ItemCount}\n" + + $"Character Count: {CharacterCount}\n" + + $"Clients Count {ConnectClients}\n " + + $"PhysicsBody Count: {PhysicsBodyCount}\n" + + $"Tick Rate: {RealTickRate}\n" + + $"Min Tick Rate: {TickRateLow}\n" + + $"Max Tick Rate: {TickRateHigh}\n" + + $"Total Ticks: {TotalTicks}\n" + + $"All time Average Tick Rate: {AverageTickRate}\n" + + $"60s Average Tick Rate: {AverageTickRate10s}\n" + + $"Server Run Time: {TimeElapsed}\n" + + $"Memory Usage: {MemoryUsage}\n"; + } + + } +} diff --git a/Barotrauma/BarotraumaServer/ServerSource/Program.cs b/Barotrauma/BarotraumaServer/ServerSource/Program.cs index 35e2503ef..1f5a9727b 100644 --- a/Barotrauma/BarotraumaServer/ServerSource/Program.cs +++ b/Barotrauma/BarotraumaServer/ServerSource/Program.cs @@ -64,7 +64,7 @@ namespace Barotrauma GameMain.ShouldRun = false; }; #endif - Console.WriteLine("Barotrauma Dedicated Server " + GameMain.Version + + Console.WriteLine("Barotrauma Dedicated Server(EP) " + GameMain.Version + " (" + AssemblyInfo.BuildString + ", branch " + AssemblyInfo.GitBranch + ", revision " + AssemblyInfo.GitRevision + ")"); if (Console.IsOutputRedirected) { diff --git a/Barotrauma/BarotraumaShared/SharedSource/Characters/Animation/Ragdoll.cs b/Barotrauma/BarotraumaShared/SharedSource/Characters/Animation/Ragdoll.cs index f82af2cbd..3e11c6e7f 100644 --- a/Barotrauma/BarotraumaShared/SharedSource/Characters/Animation/Ragdoll.cs +++ b/Barotrauma/BarotraumaShared/SharedSource/Characters/Animation/Ragdoll.cs @@ -1193,6 +1193,10 @@ namespace Barotrauma public void Teleport(Vector2 moveAmount, Vector2 velocityChange, bool detachProjectiles = true) { + // Hopefully this will fix some crashes :( + // If Collider was null then no need to procced: nothing is there already + if (Collider == null) { return; } + foreach (Limb limb in Limbs) { if (limb.IsSevered) { continue; } @@ -1214,6 +1218,7 @@ namespace Barotrauma character.DisableImpactDamageTimer = 0.25f; + // Why they did null check below but didn't do it here???? SetPosition(Collider.SimPosition + moveAmount); character.CursorPosition += moveAmount; @@ -2235,8 +2240,9 @@ namespace Barotrauma if (limb == null) { // Didn't seek or find a (valid) limb of the matching type. If there's multiple limbs of the same type, check the other limbs. - foreach (var l in limbs) + foreach (var l in Limbs) { + if (l == null) { continue; } if (l.Removed) { continue; } if (useSecondaryType) { diff --git a/Barotrauma/BarotraumaShared/SharedSource/Characters/Character.cs b/Barotrauma/BarotraumaShared/SharedSource/Characters/Character.cs index d9388cd3c..9337648a7 100644 --- a/Barotrauma/BarotraumaShared/SharedSource/Characters/Character.cs +++ b/Barotrauma/BarotraumaShared/SharedSource/Characters/Character.cs @@ -3442,21 +3442,9 @@ namespace Barotrauma characterUpdateTick++; - if (characterUpdateTick % CharacterUpdateInterval == 0) + for (int i = 0; i < CharacterList.Count; i++) { - for (int i = 0; i < CharacterList.Count; i++) - { - if (LuaCsSetup.Instance.Game.UpdatePriorityCharacters.Contains(CharacterList[i])) continue; - - CharacterList[i].Update(deltaTime * CharacterUpdateInterval, cam); - } - } - - foreach (Character character in LuaCsSetup.Instance.Game.UpdatePriorityCharacters) - { - if (character.Removed) { continue; } - Debug.Assert(character is { Removed: false }); - character.Update(deltaTime, cam); + CharacterList[i].Update(deltaTime, cam); } #if CLIENT diff --git a/Barotrauma/BarotraumaShared/SharedSource/Characters/Health/CharacterHealth.cs b/Barotrauma/BarotraumaShared/SharedSource/Characters/Health/CharacterHealth.cs index 59510440e..36b87f4bc 100644 --- a/Barotrauma/BarotraumaShared/SharedSource/Characters/Health/CharacterHealth.cs +++ b/Barotrauma/BarotraumaShared/SharedSource/Characters/Health/CharacterHealth.cs @@ -505,17 +505,19 @@ namespace Barotrauma /// public float GetResistance(AfflictionPrefab afflictionPrefab, LimbType limbType) { - // This is a % resistance (0 to 1.0) - float resistance = 0.0f; - foreach (KeyValuePair kvp in afflictions) - { - var affliction = kvp.Key; - resistance += affliction.GetResistance(afflictionPrefab.Identifier, limbType); + lock (afflictions) { + // This is a % resistance (0 to 1.0) + float resistance = 0.0f; + foreach (KeyValuePair 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); } - // 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) diff --git a/Barotrauma/BarotraumaShared/SharedSource/Characters/HumanPrefab.cs b/Barotrauma/BarotraumaShared/SharedSource/Characters/HumanPrefab.cs index a8ce33b63..d3e69b401 100644 --- a/Barotrauma/BarotraumaShared/SharedSource/Characters/HumanPrefab.cs +++ b/Barotrauma/BarotraumaShared/SharedSource/Characters/HumanPrefab.cs @@ -282,16 +282,27 @@ namespace Barotrauma #if SERVER if (GameMain.Server != null && Entity.Spawner != null && createNetworkEvents) { - if (GameMain.Server.EntityEventManager.UniqueEvents.Any(ev => ev.Entity == item)) + try { - string errorMsg = $"Error while spawning job items. Item {item.Name} created network events before the spawn event had been created."; - DebugConsole.ThrowError(errorMsg); - GameAnalyticsManager.AddErrorEventOnce("Job.InitializeJobItem:EventsBeforeSpawning", GameAnalyticsManager.ErrorSeverity.Error, errorMsg); - GameMain.Server.EntityEventManager.UniqueEvents.RemoveAll(ev => ev.Entity == item); - GameMain.Server.EntityEventManager.Events.RemoveAll(ev => ev.Entity == item); + if (GameMain.Server.EntityEventManager.UniqueEvents.ToList().Any(ev => ev.Entity == item)) + { + string errorMsg = $"Error while spawning job items. Item {item.Name} created network events before the spawn event had been created."; + DebugConsole.ThrowError(errorMsg); + GameAnalyticsManager.AddErrorEventOnce("Job.InitializeJobItem:EventsBeforeSpawning", GameAnalyticsManager.ErrorSeverity.Error, errorMsg); + GameMain.Server.EntityEventManager.UniqueEvents.RemoveAll(ev => ev.Entity == item); + GameMain.Server.EntityEventManager.Events.RemoveAll(ev => ev.Entity == item); + } + } + catch + { +#if SERVER + Networking.GameServer.Log("Try making UniqueEvents snapshot failed", Networking.ServerLog.MessageType.Error); +#endif + } + finally + { + Entity.Spawner.CreateNetworkEvent(new EntitySpawner.SpawnEntity(item)); } - - Entity.Spawner.CreateNetworkEvent(new EntitySpawner.SpawnEntity(item)); } #endif if (itemElement.GetAttributeBool("equip", false)) diff --git a/Barotrauma/BarotraumaShared/SharedSource/Characters/Jobs/Job.cs b/Barotrauma/BarotraumaShared/SharedSource/Characters/Jobs/Job.cs index 8ea5d7839..c23fc5fe2 100644 --- a/Barotrauma/BarotraumaShared/SharedSource/Characters/Jobs/Job.cs +++ b/Barotrauma/BarotraumaShared/SharedSource/Characters/Jobs/Job.cs @@ -170,16 +170,28 @@ namespace Barotrauma #if SERVER if (GameMain.Server != null && Entity.Spawner != null) { - if (GameMain.Server.EntityEventManager.UniqueEvents.Any(ev => ev.Entity == item)) + try { - string errorMsg = $"Error while spawning job items. Item {item.Name} created network events before the spawn event had been created."; - DebugConsole.ThrowError(errorMsg); - GameAnalyticsManager.AddErrorEventOnce("Job.InitializeJobItem:EventsBeforeSpawning", GameAnalyticsManager.ErrorSeverity.Error, errorMsg); - GameMain.Server.EntityEventManager.UniqueEvents.RemoveAll(ev => ev.Entity == item); - GameMain.Server.EntityEventManager.Events.RemoveAll(ev => ev.Entity == item); + if (GameMain.Server.EntityEventManager.UniqueEvents.ToList().Any(ev => ev.Entity == item)) + { + string errorMsg = $"Error while spawning job items. Item {item.Name} created network events before the spawn event had been created."; + DebugConsole.ThrowError(errorMsg); + GameAnalyticsManager.AddErrorEventOnce("Job.InitializeJobItem:EventsBeforeSpawning", GameAnalyticsManager.ErrorSeverity.Error, errorMsg); + GameMain.Server.EntityEventManager.UniqueEvents.RemoveAll(ev => ev.Entity == item); + GameMain.Server.EntityEventManager.Events.RemoveAll(ev => ev.Entity == item); + } } - - Entity.Spawner.CreateNetworkEvent(new EntitySpawner.SpawnEntity(item)); + catch + { +#if SERVER + Networking.GameServer.Log("Try making UniqueEvents snapshot failed", Networking.ServerLog.MessageType.Error); +#endif + } + finally + { + Entity.Spawner.CreateNetworkEvent(new EntitySpawner.SpawnEntity(item)); + } + } #endif diff --git a/Barotrauma/BarotraumaShared/SharedSource/Items/Components/Machines/Deconstructor.cs b/Barotrauma/BarotraumaShared/SharedSource/Items/Components/Machines/Deconstructor.cs index 15aaefb9d..b1fb95656 100644 --- a/Barotrauma/BarotraumaShared/SharedSource/Items/Components/Machines/Deconstructor.cs +++ b/Barotrauma/BarotraumaShared/SharedSource/Items/Components/Machines/Deconstructor.cs @@ -210,6 +210,8 @@ namespace Barotrauma.Items.Components amountMultiplier = (int)itemCreationMultiplier.Value; } + ApplyDeconstructionStatusEffects(targetItem, ActionType.OnDeconstructed, 1f); + if (targetItem.Prefab.RandomDeconstructionOutput) { int amount = targetItem.Prefab.RandomDeconstructionOutputAmount; diff --git a/Barotrauma/BarotraumaShared/SharedSource/Map/Gap.cs b/Barotrauma/BarotraumaShared/SharedSource/Map/Gap.cs index 9457b77df..0dbb7f23a 100644 --- a/Barotrauma/BarotraumaShared/SharedSource/Map/Gap.cs +++ b/Barotrauma/BarotraumaShared/SharedSource/Map/Gap.cs @@ -7,6 +7,7 @@ using FarseerPhysics.Dynamics; using Microsoft.Xna.Framework; using MoonSharp.Interpreter; using System; +using System.Collections.Concurrent; using System.Collections.Generic; using System.Linq; using System.Xml.Linq; @@ -747,7 +748,6 @@ namespace Barotrauma waterFlowThisFrame = 0.0f; } - private static readonly HashSet checkedHulls = new HashSet(); /// /// Simulates water flow from the source to all the hulls it's connected to across the sub, as if the water was coming directly from outside. @@ -755,7 +755,7 @@ namespace Barotrauma /// void SimulateWaterFlowFromOutsideToConnectedHulls(Hull hull, float maxFlow, float deltaTime) { - checkedHulls.Clear(); + List checkedHulls = new List(); checkedHulls.Add(hull); foreach (var connectedGap in hull.ConnectedGaps) { @@ -766,7 +766,7 @@ namespace Barotrauma } } - static void SimulateWaterFlowFromOutsideToConnectedHullsRecursive(Hull targetHull, Gap gap, HashSet checkedHulls, Hull originHull, float maxFlow, float deltaTime) + static void SimulateWaterFlowFromOutsideToConnectedHullsRecursive(Hull targetHull, Gap gap, List checkedHulls, Hull originHull, float maxFlow, float deltaTime) { const float decay = 0.95f; @@ -814,7 +814,12 @@ namespace Barotrauma if (outsideCollisionBlocker == null) { return false; } if (IsRoomToRoom || Submarine == null || open <= 0.0f || linkedTo.Count == 0 || linkedTo[0] is not Hull) { - outsideCollisionBlocker.Enabled = false; + SingleThreadWorker.GlobalWorker.AddAction(() => + { + if (outsideCollisionBlocker == null) { return; } + outsideCollisionBlocker.Enabled = false; + }); + return false; } @@ -997,8 +1002,6 @@ namespace Barotrauma base.Remove(); GapList.Remove(this); - checkedHulls.Clear(); - foreach (Hull hull in Hull.HullList) { hull.ConnectedGaps.Remove(this); diff --git a/Barotrauma/BarotraumaShared/SharedSource/Map/Levels/LevelObjects/LevelTrigger.cs b/Barotrauma/BarotraumaShared/SharedSource/Map/Levels/LevelObjects/LevelTrigger.cs index ec3bafffc..dbf04847d 100644 --- a/Barotrauma/BarotraumaShared/SharedSource/Map/Levels/LevelObjects/LevelTrigger.cs +++ b/Barotrauma/BarotraumaShared/SharedSource/Map/Levels/LevelObjects/LevelTrigger.cs @@ -479,7 +479,7 @@ namespace Barotrauma { foreach (Fixture fixture in triggerBody.FarseerBody.FixtureList) { - ContactEdge contactEdge = fixture.Body.ContactList; + ContactEdge contactEdge = fixture.Body.ContactList == null ? null: fixture.Body.ContactList.CreateCopy(); while (contactEdge != null) { if (contactEdge.Contact != null && diff --git a/Barotrauma/BarotraumaShared/SharedSource/Map/MapEntity.cs b/Barotrauma/BarotraumaShared/SharedSource/Map/MapEntity.cs index eb906563d..cc04729fc 100644 --- a/Barotrauma/BarotraumaShared/SharedSource/Map/MapEntity.cs +++ b/Barotrauma/BarotraumaShared/SharedSource/Map/MapEntity.cs @@ -6,6 +6,7 @@ using System.Collections.Generic; using System.Diagnostics; using System.Linq; using System.Reflection; +using System.Threading.Tasks; using System.Xml.Linq; namespace Barotrauma @@ -29,7 +30,7 @@ namespace Barotrauma protected readonly List Upgrades = new List(); public readonly HashSet DisallowedUpgradeSet = new HashSet(); - + [Editable, Serialize("", IsPropertySaveable.Yes)] public string DisallowedUpgrades { @@ -84,11 +85,11 @@ namespace Barotrauma public bool IsHighlighted { get { return isHighlighted || ExternalHighlight; } - set + set { if (value != isHighlighted) { - isHighlighted = value; + isHighlighted = value; CheckIsHighlighted(); } } @@ -531,7 +532,7 @@ namespace Barotrauma } if (originalWire.Connections.Any(c => c != null) && - (cloneWire.Connections[0] == null || cloneWire.Connections[1] == null) && + (cloneWire.Connections[0] == null || cloneWire.Connections[1] == null) && cloneItem.GetComponent() == null) { if (!clones.Any(c => (c as Item)?.GetComponent()?.DisconnectedWires.Contains(cloneWire) ?? false)) @@ -639,98 +640,106 @@ namespace Barotrauma /// /// Call Update() on every object in Entity.list /// - public static void UpdateAll(float deltaTime, Camera cam) + public static void UpdateAll(float deltaTime, Camera cam, ParallelOptions parallelOptions) { - mapEntityUpdateTick++; - #if CLIENT var sw = new System.Diagnostics.Stopwatch(); sw.Start(); #endif - if (mapEntityUpdateTick % MapEntityUpdateInterval == 0) - { - foreach (Hull hull in Hull.HullList) + // Buffer lists to avoid repeated allocations + var hullList = Hull.HullList.ToList(); + var structureList = Structure.WallList.ToList(); + + List gapList = Gap.GapList.ToList(); + List shuffledGaps = new List(gapList?.OrderBy(g => Rand.Int(int.MaxValue))); + // In case if it failed, but why it would fail? + shuffledGaps = shuffledGaps ?? gapList; + + var itemList = Item.ItemList.ToList(); + + // First phase: parallel updates that have no order dependencies + Parallel.Invoke(parallelOptions, + () => { - hull.Update(deltaTime * MapEntityUpdateInterval, cam); + // basically nothing here is thread-safe so + foreach (var hull in hullList) + { + hull.Update(deltaTime, cam); + } + }, + // Structure parallel update + () => + { + Parallel.ForEach(structureList, parallelOptions, structure => + { + structure.Update(deltaTime, cam); + }); + }, + () => + //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 + + // moved waterflow reset here to see if we can reduce at least some time + { + // PLEASE WORK + Parallel.ForEach(shuffledGaps, parallelOptions, gap => + { + gap.ResetWaterFlowThisFrame(); + gap.Update(deltaTime, cam); + }); + }, + // Powered components update + () => + { + Powered.UpdatePower(deltaTime); } + ); + + SingleThreadWorker.GlobalWorker.RunActions(); + #if CLIENT - Hull.UpdateCheats(deltaTime * MapEntityUpdateInterval, cam); + // Hull Cheats need to be executed after Hull update + Hull.UpdateCheats(deltaTime, cam); #endif - foreach (Structure structure in Structure.WallList) - { - structure.Update(deltaTime * MapEntityUpdateInterval, cam); - } - } - - foreach (Gap gap in Gap.GapList) - { - gap.ResetWaterFlowThisFrame(); - } - //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 Gap.GapList.OrderBy(g => Rand.Int(int.MaxValue))) - { - gap.Update(deltaTime, cam); - } - - if (mapEntityUpdateTick % PoweredUpdateInterval == 0) - { - Powered.UpdatePower(deltaTime * PoweredUpdateInterval); - } - #if CLIENT sw.Stop(); GameMain.PerformanceCounter.AddElapsedTicks("Update:MapEntity:Misc", sw.ElapsedTicks); sw.Restart(); #endif + // Item update (Item.Update() is not thread-safe and must be executed on the main thread) Item.UpdatePendingConditionUpdates(deltaTime); - if (mapEntityUpdateTick % MapEntityUpdateInterval == 0) - { - Item lastUpdatedItem = null; - try + Item lastUpdatedItem = null; + + try + { + foreach (Item item in itemList) { - foreach (Item item in Item.ItemList) - { - if (LuaCsSetup.Instance.Game.UpdatePriorityItems.Contains(item)) { continue; } - lastUpdatedItem = item; - item.Update(deltaTime * MapEntityUpdateInterval, 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); } + UpdateAllProjSpecific(deltaTime); + Spawner?.Update(); + #if CLIENT sw.Stop(); GameMain.PerformanceCounter.AddElapsedTicks("Update:MapEntity:Items", sw.ElapsedTicks); - sw.Restart(); #endif - - if (mapEntityUpdateTick % MapEntityUpdateInterval == 0) - { - UpdateAllProjSpecific(deltaTime * MapEntityUpdateInterval); - - Spawner?.Update(); - } } static partial void UpdateAllProjSpecific(float deltaTime); @@ -783,7 +792,7 @@ namespace Barotrauma var tags = element.GetAttributeIdentifierArray("tags", Array.Empty()); if (tags.Contains(Tags.HiddenItemContainer)) { - containsHiddenContainers = true; + containsHiddenContainers = true; break; } } @@ -828,7 +837,7 @@ namespace Barotrauma } } } - else if (t == typeof(Item) && !containsHiddenContainers && identifier == "vent" && + else if (t == typeof(Item) && !containsHiddenContainers && identifier == "vent" && submarine.Info.Type == SubmarineType.Player && !submarine.Info.HasTag(SubmarineTag.Shuttle)) { if (!hiddenContainerCreated) diff --git a/Barotrauma/BarotraumaShared/SharedSource/Map/SubmarineBody.cs b/Barotrauma/BarotraumaShared/SharedSource/Map/SubmarineBody.cs index cf1dec2ef..ce8a8c320 100644 --- a/Barotrauma/BarotraumaShared/SharedSource/Map/SubmarineBody.cs +++ b/Barotrauma/BarotraumaShared/SharedSource/Map/SubmarineBody.cs @@ -1,5 +1,6 @@ using Barotrauma.Extensions; using Barotrauma.Items.Components; +using Barotrauma.Networking; using FarseerPhysics; using FarseerPhysics.Common; using FarseerPhysics.Dynamics; @@ -426,7 +427,17 @@ namespace Barotrauma if (points[0].Y > Body.SimPosition.Y && !Character.CharacterList.Any(c => c.Submarine == otherSubmarine && !c.IsIncapacitated && c.TeamID == otherSubmarine.TeamID)) { - otherSubmarine.GetConnectedSubs().ForEach(s => s.SubBody.forceUpwardsTimer += deltaTime); + try + { + otherSubmarine.GetConnectedSubs().ToList().ForEach(s => s.SubBody.forceUpwardsTimer += deltaTime); + } + catch + { +#if SERVER + GameServer.Log("Try making UniqueEvents snapshot failed", ServerLog.MessageType.Error); +#endif + } + break; } } diff --git a/Barotrauma/BarotraumaShared/SharedSource/Networking/ChildServerRelay.cs b/Barotrauma/BarotraumaShared/SharedSource/Networking/ChildServerRelay.cs index 865280f5f..7ddfe996a 100644 --- a/Barotrauma/BarotraumaShared/SharedSource/Networking/ChildServerRelay.cs +++ b/Barotrauma/BarotraumaShared/SharedSource/Networking/ChildServerRelay.cs @@ -174,7 +174,9 @@ namespace Barotrauma.Networking exceptionMsg += " Child process has not exited."; } #endif +#if !DEBUG throw new Exception(exceptionMsg); +#endif } } diff --git a/Barotrauma/BarotraumaShared/SharedSource/Screens/GameScreen.cs b/Barotrauma/BarotraumaShared/SharedSource/Screens/GameScreen.cs index 754bb6d26..f413c54f1 100644 --- a/Barotrauma/BarotraumaShared/SharedSource/Screens/GameScreen.cs +++ b/Barotrauma/BarotraumaShared/SharedSource/Screens/GameScreen.cs @@ -1,9 +1,11 @@ -//#define RUN_PHYSICS_IN_SEPARATE_THREAD - -using Microsoft.Xna.Framework; +using Microsoft.Xna.Framework; using System.Threading; using FarseerPhysics.Dynamics; using FarseerPhysics; +using System.Threading.Tasks; +using System.Linq; +using System.Collections.Generic; +using System; #if DEBUG && CLIENT @@ -16,8 +18,11 @@ namespace Barotrauma { partial class GameScreen : Screen { - private object updateLock = new object(); - private double physicsTime; + // Use default instead. Hopefully this wont cause issues in long-running servers. + private static readonly ParallelOptions parallelOptions = new ParallelOptions + { + //MaxDegreeOfParallelism = Math.Max(4, Environment.ProcessorCount - 1) + }; #if CLIENT private readonly Camera cam; @@ -54,13 +59,13 @@ namespace Barotrauma #if CLIENT if (Character.Controlled != null) { - cam.Position = Character.Controlled.WorldPosition; - cam.UpdateTransform(true); + Cam.Position = Character.Controlled.WorldPosition; + Cam.UpdateTransform(true); } else if (Submarine.MainSub != null) { - cam.Position = Submarine.MainSub.WorldPosition; - cam.UpdateTransform(true); + Cam.Position = Submarine.MainSub.WorldPosition; + Cam.UpdateTransform(true); } GameMain.GameSession?.CrewManager?.ResetCrewListOpenState(); ChatBox.ResetChatBoxOpenState(); @@ -69,14 +74,6 @@ namespace Barotrauma MapEntity.ClearHighlightedEntities(); -#if RUN_PHYSICS_IN_SEPARATE_THREAD - var physicsThread = new Thread(ExecutePhysics) - { - Name = "Physics thread", - IsBackground = true - }; - physicsThread.Start(); -#endif } public override void Deselect() @@ -104,19 +101,16 @@ namespace Barotrauma /// public override void Update(double deltaTime) { -#if RUN_PHYSICS_IN_SEPARATE_THREAD - physicsTime += deltaTime; - lock (updateLock) - { -#endif + var submarines = Submarine.Loaded.ToList(); + var physicsBodies = PhysicsBody.List.ToList(); #if DEBUG && CLIENT if (GameMain.GameSession != null && !DebugConsole.IsOpen && GUI.KeyboardDispatcher.Subscriber == null) { if (GameMain.GameSession.Level != null && GameMain.GameSession.Submarine != null) { - Submarine closestSub = Submarine.FindClosest(cam.WorldViewCenter) ?? GameMain.GameSession.Submarine; + Submarine closestSub = Submarine.FindClosest(Cam.WorldViewCenter) ?? GameMain.GameSession.Submarine; Vector2 targetMovement = Vector2.Zero; if (PlayerInput.KeyDown(Keys.I)) { targetMovement.Y += 1.0f; } @@ -138,38 +132,38 @@ namespace Barotrauma GameTime += deltaTime; - foreach (PhysicsBody body in PhysicsBody.List) + Parallel.ForEach(physicsBodies, parallelOptions, body => { - //update character (colliders) regardless if they're enabled or not, so that the draw position is updated - //necessary to sync the character's position even if the character is ragdolled and the collider is disabled - if ((body.Enabled || body.UserData is Character) && - body.BodyType != BodyType.Static) - { - body.Update(); - } - } + if ((body.Enabled || body.UserData is Character) && + body.BodyType != BodyType.Static) + { + body.Update(); + } + }); + MapEntity.ClearHighlightedEntities(); #if CLIENT var sw = new System.Diagnostics.Stopwatch(); sw.Start(); #endif - GameMain.GameSession?.Update((float)deltaTime); - #if CLIENT sw.Stop(); GameMain.PerformanceCounter.AddElapsedTicks("Update:GameSession", sw.ElapsedTicks); - sw.Restart(); + sw.Restart(); + + GameMain.ParticleManager?.Update((float)deltaTime); - GameMain.ParticleManager.Update((float)deltaTime); - sw.Stop(); - GameMain.PerformanceCounter.AddElapsedTicks("Update:Particles", sw.ElapsedTicks); - sw.Restart(); + GameMain.PerformanceCounter.AddElapsedTicks("Update:Particle", sw.ElapsedTicks); + sw.Restart(); - if (Level.Loaded != null) Level.Loaded.Update((float)deltaTime, cam); +#endif + if (Level.Loaded != null) { Level.Loaded.Update((float)deltaTime, Cam); } + +#if CLIENT sw.Stop(); GameMain.PerformanceCounter.AddElapsedTicks("Update:Level", sw.ElapsedTicks); @@ -177,7 +171,7 @@ namespace Barotrauma { if (controlled.SelectedItem != null && controlled.CanInteractWith(controlled.SelectedItem)) { - controlled.SelectedItem.UpdateHUD(cam, controlled, (float)deltaTime); + controlled.SelectedItem.UpdateHUD(Cam, controlled, (float)deltaTime); } if (controlled.Inventory != null) { @@ -185,7 +179,7 @@ namespace Barotrauma { if (controlled.HasEquippedItem(item)) { - item.UpdateHUD(cam, controlled, (float)deltaTime); + item.UpdateHUD(Cam, controlled, (float)deltaTime); } } } @@ -193,12 +187,8 @@ namespace Barotrauma sw.Restart(); - Character.UpdateAll((float)deltaTime, cam); -#elif SERVER - if (Level.Loaded != null) Level.Loaded.Update((float)deltaTime, Camera.Instance); - Character.UpdateAll((float)deltaTime, Camera.Instance); #endif - + Character.UpdateAll((float)deltaTime, Cam); #if CLIENT sw.Stop(); @@ -206,9 +196,11 @@ namespace Barotrauma sw.Restart(); #endif + //StatusEffect.UpdateAll is not thread-safe and must be executed on the main thread StatusEffect.UpdateAll((float)deltaTime); #if CLIENT + sw.Stop(); GameMain.PerformanceCounter.AddElapsedTicks("Update:StatusEffects", sw.ElapsedTicks); sw.Restart(); @@ -219,9 +211,6 @@ namespace Barotrauma Vector2 targetPos = Lights.LightManager.ViewTarget.WorldPosition; if (Lights.LightManager.ViewTarget == Character.Controlled) { - //take the NetworkPositionErrorOffset into account, meaning the camera is positioned - //where we've smoothed out the draw position of the character after a positional correction, - //instead of where the character's collider actually is targetPos += ConvertUnits.ToDisplayUnits(Character.Controlled.AnimController.Collider.NetworkPositionErrorOffset); if (CharacterHealth.OpenHealthWindow != null || CrewManager.IsCommandInterfaceOpen || ConversationAction.IsDialogOpen) { @@ -236,48 +225,43 @@ namespace Barotrauma } Vector2 screenOffset = screenTargetPos - new Vector2(GameMain.GraphicsWidth / 2, GameMain.GraphicsHeight / 2); screenOffset.Y = -screenOffset.Y; - targetPos -= screenOffset / cam.Zoom; + targetPos -= screenOffset / Cam.Zoom; } } - cam.TargetPos = targetPos; + Cam.TargetPos = targetPos; } - cam.MoveCamera((float)deltaTime, allowZoom: GUI.MouseOn == null && !Inventory.IsMouseOnInventory); + Cam.MoveCamera((float)deltaTime, allowZoom: GUI.MouseOn == null && !Inventory.IsMouseOnInventory); - Character.Controlled?.UpdateLocalCursor(cam); + Character.Controlled?.UpdateLocalCursor(Cam); #endif - foreach (Submarine sub in Submarine.Loaded) + foreach (Submarine sub in submarines) { sub.SetPrevTransform(sub.Position); } - foreach (PhysicsBody body in PhysicsBody.List) + Parallel.ForEach(physicsBodies, parallelOptions, body => { - if (body.Enabled && body.BodyType != FarseerPhysics.BodyType.Static) - { - body.SetPrevTransform(body.SimPosition, body.Rotation); + if (body.Enabled && body.BodyType != FarseerPhysics.BodyType.Static) + { + body.SetPrevTransform(body.SimPosition, body.Rotation); } - } + }); -#if CLIENT - MapEntity.UpdateAll((float)deltaTime, cam); -#elif SERVER - MapEntity.UpdateAll((float)deltaTime, Camera.Instance); -#endif + MapEntity.UpdateAll((float)deltaTime, Cam, parallelOptions); #if CLIENT sw.Stop(); GameMain.PerformanceCounter.AddElapsedTicks("Update:MapEntity", sw.ElapsedTicks); sw.Restart(); #endif + //Character.UpdateAnimAll is not thread-safe and must be executed on the main thread Character.UpdateAnimAll((float)deltaTime); -#if CLIENT - Ragdoll.UpdateAll((float)deltaTime, cam); -#elif SERVER - Ragdoll.UpdateAll((float)deltaTime, Camera.Instance); -#endif + + Ragdoll.UpdateAll((float)deltaTime, Cam); + SingleThreadWorker.GlobalWorker.RunActions(); #if CLIENT sw.Stop(); @@ -285,7 +269,7 @@ namespace Barotrauma sw.Restart(); #endif - foreach (Submarine sub in Submarine.Loaded) + foreach (Submarine sub in submarines) { sub.Update((float)deltaTime); } @@ -295,8 +279,6 @@ namespace Barotrauma GameMain.PerformanceCounter.AddElapsedTicks("Update:Submarine", sw.ElapsedTicks); sw.Restart(); #endif - -#if !RUN_PHYSICS_IN_SEPARATE_THREAD try { GameMain.World.Step((float)Timing.Step); @@ -307,35 +289,14 @@ namespace Barotrauma DebugConsole.ThrowError(errorMsg, e); GameAnalyticsManager.AddErrorEventOnce("GameScreen.Update:WorldLockedException" + e.Message, GameAnalyticsManager.ErrorSeverity.Critical, errorMsg); } -#endif - #if CLIENT sw.Stop(); GameMain.PerformanceCounter.AddElapsedTicks("Update:Physics", sw.ElapsedTicks); #endif UpdateProjSpecific(deltaTime); - -#if RUN_PHYSICS_IN_SEPARATE_THREAD - } -#endif } partial void UpdateProjSpecific(double deltaTime); - - private void ExecutePhysics() - { - while (true) - { - while (physicsTime >= Timing.Step) - { - lock (updateLock) - { - GameMain.World.Step((float)Timing.Step); - physicsTime -= Timing.Step; - } - } - } - } } } diff --git a/Barotrauma/BarotraumaShared/SharedSource/SingleThreadWorker.cs b/Barotrauma/BarotraumaShared/SharedSource/SingleThreadWorker.cs new file mode 100644 index 000000000..8beef7b85 --- /dev/null +++ b/Barotrauma/BarotraumaShared/SharedSource/SingleThreadWorker.cs @@ -0,0 +1,55 @@ +using System; +using System.Collections.Concurrent; + +namespace Barotrauma +{ + public class SingleThreadWorker + { + private ConcurrentQueue ActionQueue; + + public static SingleThreadWorker GlobalWorker = new SingleThreadWorker(); + + /// + /// Initilize a SingleThreadWorker + /// SingleThreadWorker or STW for short is a FIFO queue ensure single-thread execution of a series of actions. + /// + public SingleThreadWorker() + { + ActionQueue = new ConcurrentQueue(); + } + + /// + /// Add a pending action in a STW queue + /// + /// + public void AddAction(Action action) + { + ActionQueue.Enqueue(action); + } + + /// + /// Run all pending actions in the STW queue + /// + [STAThread] + public void RunActions() + { + while (ActionQueue.TryDequeue(out Action action)) + { + try + { + action(); + } + catch (Exception e) + { + // Just try-catch and do nothing but print errorlogs. We cannot afford crashing the game. + ConsoleColor originalForeground = Console.ForegroundColor; + Console.ForegroundColor = ConsoleColor.Yellow; + Console.WriteLine($"WARNING: Error occurred when running Single Thread Actions. " + + $"If the server didn't crash or stop responding then this should be fine \n{e}"); + Console.ForegroundColor = Console.ForegroundColor; + } + } + } + + } +} diff --git a/Libraries/Farseer Physics Engine 3.5/Dynamics/Body.cs b/Libraries/Farseer Physics Engine 3.5/Dynamics/Body.cs index 81faa0121..9a7ad1056 100644 --- a/Libraries/Farseer Physics Engine 3.5/Dynamics/Body.cs +++ b/Libraries/Farseer Physics Engine 3.5/Dynamics/Body.cs @@ -29,6 +29,7 @@ using System; +using System.Collections.Concurrent; using System.Collections.Generic; using System.Diagnostics; using System.Linq; @@ -349,6 +350,7 @@ namespace FarseerPhysics.Dynamics } } + /// /// Create all proxies. /// @@ -1409,4 +1411,4 @@ namespace FarseerPhysics.Dynamics return body; } } -} \ No newline at end of file +} diff --git a/README.md b/README.md index dfde2babb..4216a9c6b 100644 --- a/README.md +++ b/README.md @@ -1,11 +1,24 @@ -# LuaCsForBarotrauma -This is a Barotrauma modification that adds Lua and Cs modding support. +# LuaCsForBarotrauma Enhanced Performence Project -### Online Documentation: https://evilfactory.github.io/LuaCsForBarotrauma -### VS Code Documentation: https://gitee.com/zhurengong/btlua-docs -### Discord: https://discord.gg/f9zvNNuxu9 +> ⚠ **Warning:** This release is only available for server-side use and is not recommended to run on the client. Make sure that compatibility is adequately tested before deployment. -### This project uses a fork of Moonsharp: https://github.com/evilfactory/moonsharp +This is a LuaCsForBarotrauma modification that adds Multi-Thread and Multi-Core support. + +### This project uses a fork of Moonsharp from EvilFactory: https://github.com/evilfactory/moonsharp +### This project WILL heavily modify original LuaCsForBarotrauma/Barotrauma code. + +## Main Contributors + +| Contributor | Role | +|-------------|------| +| [NotAlwaysTrue](https://github.com/NotAlwaysTrue) | Project Lead +| [Eero](https://github.com/eiaol) | Project Engineer | + +## Acknowledgements + +- [LuaCsForBarotrauma](https://github.com/evilfactory/LuaCsForBarotrauma) - Original Lua/C# modding framework +- [EvilFactory](https://github.com/evilfactory) - Moonsharp fork maintainer +- [FakeFish Ltd](https://www.barotraumagame.com/) - Barotrauma developers # Barotrauma @@ -34,3 +47,53 @@ If you're interested in working on the code, either to develop mods or to contri - [.NET 8 SDK](https://docs.microsoft.com/en-us/dotnet/core/install/linux) ### macOS - [Visual Studio 2022 for Mac](https://visualstudio.microsoft.com/vs/mac/) + +# LuaCsForBarotrauma 性能增强项目 + +> ⚠ **重要警告:** 本版本仅适用于服务器端使用,不建议在客户端运行。请确保在部署前已充分测试兼容性。 + +这是一个 LuaCsForBarotrauma 的修改版本,添加了多线程和多核支持。 + +### 本项目使用 EvilFactory 的 Moonsharp 分支:https://github.com/evilfactory/moonsharp +### 本项目将对原版 LuaCsForBarotrauma/Barotrauma 代码进行大量修改。 + +## 主要贡献者 + +| 贡献者 | 角色 | +|--------|------| +| [NotAlwaysTrue](https://github.com/NotAlwaysTrue) | 项目负责人 | +| [Eero](https://github.com/eiaol) | 项目责任工程师 | + +## 致谢 + +- [LuaCsForBarotrauma](https://github.com/evilfactory/LuaCsForBarotrauma) - 原版 Lua/C# 模组框架 +- [EvilFactory](https://github.com/evilfactory) - Moonsharp 分支维护者 +- [FakeFish Ltd](https://www.barotraumagame.com/) - Barotrauma 开发商 + +# Barotrauma + +版权所有 © FakeFish Ltd 2017-2024 + +下载源代码前,请阅读 [最终用户许可协议](EULA.txt)。 + +如果您有问题或需要报告问题,请查看我们的[贡献指南](https://github.com/Regalis11/Barotrauma/blob/master/CONTRIBUTING.md)。 + +如果您有兴趣参与代码开发,无论是开发模组还是向仓库贡献内容,您也可以在[贡献指南](https://github.com/Regalis11/Barotrauma/blob/master/CONTRIBUTING.md)中找到相关说明。 + +## 链接: + +**官方网站:** www.barotraumagame.com + +**Steam 论坛:** https://steamcommunity.com/app/602960/discussions/ + +**Discord:** https://discordapp.com/invite/undertow + +**Wiki:** https://barotraumagame.com/wiki/Main_Page + +## 环境要求: +### Windows +- 支持 C# 10 的 [Visual Studio](https://www.visualstudio.com/vs/community/)(推荐 VS 2022 或更高版本) +### Linux +- [.NET 6 SDK](https://docs.microsoft.com/en-us/dotnet/core/install/linux) +### macOS +- [Visual Studio 2022 for Mac](https://visualstudio.microsoft.com/vs/mac/)