Sync with upstream

This commit is contained in:
NotAlwaysTrue
2026-06-16 22:09:05 +08:00
26 changed files with 831 additions and 315 deletions

View File

@@ -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: |

1
.gitignore vendored
View File

@@ -60,3 +60,4 @@ Deploy/DeployAll/PrivateKey.*
#Rider
*.DotSettings.user
.vscode/settings.json

View File

@@ -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) =>

View File

@@ -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

View File

@@ -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) =>
{

View File

@@ -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();

View File

@@ -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);
}
}
}

View File

@@ -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:");

View File

@@ -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<ServerEntityEvent> 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<ServerEntityEvent>();
this.server = server;
bufferedEvents = new List<BufferedEvent>();
uniqueEvents = new List<ServerEntityEvent>();
lastWarningTime = -10.0;
public IServerSerializable Entity;
public NetEntityEvent.IData Data;
public PendingCreateEvent(IServerSerializable entity, NetEntityEvent.IData data)
{
Entity = entity;
Data = data;
}
}
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>();
this.server = server;
bufferedEvents = new List<BufferedEvent>();
uniqueEvents = new List<ServerEntityEvent>();
pendingCreateQueue = new ConcurrentQueue<PendingCreateEvent>();
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<Client> 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<Client> inGameClients = null;
List<Client> 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<Client>()).Add(c);
}
else
{
(inGameClients ??= new List<Client>()).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<Client> toKick = inGameClients.FindAll(c =>
List<Client> 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;

View File

@@ -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<double> tickrate60s = new Queue<double>(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";
}
}
}

View File

@@ -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)
{

View File

@@ -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)
{

View File

@@ -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

View File

@@ -505,17 +505,19 @@ namespace Barotrauma
/// </summary>
public float GetResistance(AfflictionPrefab afflictionPrefab, LimbType limbType)
{
// 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);
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);
}
// 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)

View File

@@ -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))

View File

@@ -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

View File

@@ -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;

View File

@@ -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<Hull> checkedHulls = new HashSet<Hull>();
/// <summary>
/// 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
/// </summary>
void SimulateWaterFlowFromOutsideToConnectedHulls(Hull hull, float maxFlow, float deltaTime)
{
checkedHulls.Clear();
List<Hull> checkedHulls = new List<Hull>();
checkedHulls.Add(hull);
foreach (var connectedGap in hull.ConnectedGaps)
{
@@ -766,7 +766,7 @@ namespace Barotrauma
}
}
static void SimulateWaterFlowFromOutsideToConnectedHullsRecursive(Hull targetHull, Gap gap, HashSet<Hull> checkedHulls, Hull originHull, float maxFlow, float deltaTime)
static void SimulateWaterFlowFromOutsideToConnectedHullsRecursive(Hull targetHull, Gap gap, List<Hull> 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);

View File

@@ -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 &&

View File

@@ -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<Upgrade> Upgrades = new List<Upgrade>();
public readonly HashSet<Identifier> DisallowedUpgradeSet = new HashSet<Identifier>();
[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<DockingPort>() == null)
{
if (!clones.Any(c => (c as Item)?.GetComponent<ConnectionPanel>()?.DisconnectedWires.Contains(cloneWire) ?? false))
@@ -639,98 +640,106 @@ namespace Barotrauma
/// <summary>
/// Call Update() on every object in Entity.list
/// </summary>
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<Gap> gapList = Gap.GapList.ToList();
List<Gap> shuffledGaps = new List<Gap>(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<Identifier>());
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)

View File

@@ -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;
}
}

View File

@@ -174,7 +174,9 @@ namespace Barotrauma.Networking
exceptionMsg += " Child process has not exited.";
}
#endif
#if !DEBUG
throw new Exception(exceptionMsg);
#endif
}
}

View File

@@ -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
/// </summary>
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;
}
}
}
}
}
}

View File

@@ -0,0 +1,55 @@
using System;
using System.Collections.Concurrent;
namespace Barotrauma
{
public class SingleThreadWorker
{
private ConcurrentQueue<Action> ActionQueue;
public static SingleThreadWorker GlobalWorker = new SingleThreadWorker();
/// <summary>
/// Initilize a SingleThreadWorker
/// SingleThreadWorker or STW for short is a FIFO queue ensure single-thread execution of a series of actions.
/// </summary>
public SingleThreadWorker()
{
ActionQueue = new ConcurrentQueue<Action>();
}
/// <summary>
/// Add a pending action in a STW queue
/// </summary>
/// <param name="action"></param>
public void AddAction(Action action)
{
ActionQueue.Enqueue(action);
}
/// <summary>
/// Run all pending actions in the STW queue
/// </summary>
[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;
}
}
}
}
}

View File

@@ -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
}
}
/// <summary>
/// Create all proxies.
/// </summary>
@@ -1409,4 +1411,4 @@ namespace FarseerPhysics.Dynamics
return body;
}
}
}
}

View File

@@ -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/)