From b4e462aaec004965e3010af734792d01e85e2dc3 Mon Sep 17 00:00:00 2001 From: TwinBuilderOne Date: Tue, 6 Dec 2022 06:33:59 -0600 Subject: [PATCH 01/40] Draft 1 --- .../Processors/PlayerSyncFinishedProcessor.cs | 2 +- .../GameLogic/InitialSyncTimerData.cs | 39 ------ NitroxServer/GameLogic/PlayerManager.cs | 115 +++++++++--------- 3 files changed, 58 insertions(+), 98 deletions(-) delete mode 100644 NitroxServer/GameLogic/InitialSyncTimerData.cs diff --git a/NitroxServer/Communication/Packets/Processors/PlayerSyncFinishedProcessor.cs b/NitroxServer/Communication/Packets/Processors/PlayerSyncFinishedProcessor.cs index 7d1dc3c37d..57f43c8cc9 100644 --- a/NitroxServer/Communication/Packets/Processors/PlayerSyncFinishedProcessor.cs +++ b/NitroxServer/Communication/Packets/Processors/PlayerSyncFinishedProcessor.cs @@ -15,7 +15,7 @@ public PlayerSyncFinishedProcessor(PlayerManager playerManager) public override void Process(PlayerSyncFinished packet, Player player) { - playerManager.FinishProcessingReservation(); + playerManager.SyncFinishedCallback(); } } } diff --git a/NitroxServer/GameLogic/InitialSyncTimerData.cs b/NitroxServer/GameLogic/InitialSyncTimerData.cs deleted file mode 100644 index 07b4a56438..0000000000 --- a/NitroxServer/GameLogic/InitialSyncTimerData.cs +++ /dev/null @@ -1,39 +0,0 @@ -using System; -using System.Collections.Generic; -using System.Linq; -using System.Text; -using System.Threading.Tasks; -using NitroxModel.MultiplayerSession; -using NitroxServer.Communication; - -namespace NitroxServer.GameLogic -{ - /// - /// Contains data used in InitialSyncTimer callback - /// - /// For use with - /// - internal class InitialSyncTimerData - { - public readonly NitroxConnection Connection; - public readonly AuthenticationContext Context; - public readonly int MaxCounter; - - /// - /// Keeps track of how many times the timer has elapsed - /// - public int Counter = 0; - - /// - /// Set to true if disposing the timer - /// - public bool Disposing = false; - - public InitialSyncTimerData(NitroxConnection connection, AuthenticationContext context, int initialSyncTimeout) - { - Connection = connection; - Context = context; - MaxCounter = (int)Math.Ceiling(initialSyncTimeout / 200f); - } - } -} diff --git a/NitroxServer/GameLogic/PlayerManager.cs b/NitroxServer/GameLogic/PlayerManager.cs index ea04fed85c..5352d3bedd 100644 --- a/NitroxServer/GameLogic/PlayerManager.cs +++ b/NitroxServer/GameLogic/PlayerManager.cs @@ -1,8 +1,10 @@ using System; using System.Collections.Generic; using System.Linq; +using System.Runtime.Remoting.Messaging; using System.Text.RegularExpressions; using System.Threading; +using System.Threading.Tasks; using NitroxModel.DataStructures; using NitroxModel.DataStructures.GameLogic; using NitroxModel.DataStructures.Unity; @@ -25,9 +27,7 @@ public class PlayerManager private readonly ThreadSafeSet reservedPlayerNames = new("Player"); // "Player" is often used to identify the local player and should not be used by any user private ThreadSafeQueue> JoinQueue { get; set; } = new(); - private bool PlayerCurrentlyJoining { get; set; } - - private Timer initialSyncTimer; + public Action SyncFinishedCallback { get; private set; } private readonly ServerConfig serverConfig; private ushort currentPlayerId; @@ -38,6 +38,8 @@ public PlayerManager(List players, ServerConfig serverConfig) currentPlayerId = players.Count == 0 ? (ushort)0 : players.Max(x => x.Id); this.serverConfig = serverConfig; + + _ = JoinQueueLoop(); } public List GetConnectedPlayers() @@ -54,7 +56,8 @@ public MultiplayerSessionReservation ReservePlayerContext( NitroxConnection connection, PlayerSettings playerSettings, AuthenticationContext authenticationContext, - string correlationId) + string correlationId, + bool ignoreJoinQueue = false) { if (reservedPlayerNames.Count >= serverConfig.MaxConnections) { @@ -75,7 +78,7 @@ public MultiplayerSessionReservation ReservePlayerContext( return new MultiplayerSessionReservation(correlationId, rejectedState); } - if (PlayerCurrentlyJoining) + if (!ignoreJoinQueue) { if (JoinQueue.Any(pair => ReferenceEquals(pair.Key, connection))) { @@ -124,48 +127,67 @@ public MultiplayerSessionReservation ReservePlayerContext( reservations.Add(reservationKey, playerContext); assetPackage.ReservationKey = reservationKey; - PlayerCurrentlyJoining = true; - - InitialSyncTimerData timerData = new InitialSyncTimerData(connection, authenticationContext, serverConfig.InitialSyncTimeout); - initialSyncTimer = new Timer(InitialSyncTimerElapsed, timerData, 0, 200); - - return new MultiplayerSessionReservation(correlationId, playerId, reservationKey); } - - private void InitialSyncTimerElapsed(object state) + + private async Task JoinQueueLoop() { - if (state is InitialSyncTimerData timerData && !timerData.Disposing) + while (true) { - allPlayersByName.TryGetValue(timerData.Context.Username, out Player player); - - if (timerData.Connection.State < NitroxConnectionState.Connected) + try { - if (player == null) // player can cancel the joining process before this timer elapses + while (!JoinQueue.Any()) + { + await Task.Delay(10); + } + + var pair = JoinQueue.Dequeue(); + NitroxConnection connection = pair.Key; + MultiplayerSessionReservationRequest request = pair.Value; + + + MultiplayerSessionReservation reservation = ReservePlayerContext(connection, + request.PlayerSettings, + request.AuthenticationContext, + request.CorrelationId, + ignoreJoinQueue: true); + + connection.SendPacket(reservation); + + CancellationTokenSource source = new(); + + Task timerTask = Task.Run(() => + { + Task.Delay(serverConfig.InitialSyncTimeout).Wait(source.Token); + + Log.Info("Timer expired"); + + SyncFinishedCallback = null; + + allPlayersByName.TryGetValue(request.AuthenticationContext.Username, out Player player); + player?.SendPacket(new PlayerKicked("An error occured while loading, initial sync took too long to complete")); + PlayerDisconnected(connection); + }); + + SyncFinishedCallback = () => + { + source.Cancel(); + Log.Info($"Player {request.AuthenticationContext.Username} joined successfully. Remaining requests: {JoinQueue.Count}"); + }; + + try { - Log.Error("Player was nulled while joining"); - PlayerDisconnected(timerData.Connection); + await timerTask; } - else + catch (TaskCanceledException) { - player.SendPacket(new PlayerKicked("An error occured while loading, Initial sync took too long to complete")); - PlayerDisconnected(player.Connection); - SendPacketToOtherPlayers(new Disconnect(player.Id), player); + Log.Info("Timer cancelled"); } - timerData.Disposing = true; - FinishProcessingReservation(); } - - if (timerData.Counter >= timerData.MaxCounter) + catch (Exception e) { - Log.Error("An unexpected Error occured during InitialSync"); - PlayerDisconnected(timerData.Connection); - - timerData.Disposing = true; - initialSyncTimer.Dispose(); // Looped long enough to require an override + Log.Error($"Unexpected error during player connection: {e}"); } - - timerData.Counter++; } } @@ -253,29 +275,6 @@ public void PlayerDisconnected(NitroxConnection connection) } } - public void FinishProcessingReservation() - { - initialSyncTimer.Dispose(); - PlayerCurrentlyJoining = false; - - Log.Info($"Finished processing reservation. Remaining requests: {JoinQueue.Count}"); - - // Tell next client that it can start joining. - if (JoinQueue.Count > 0) - { - KeyValuePair keyValuePair = JoinQueue.Dequeue(); - NitroxConnection requestConnection = keyValuePair.Key; - MultiplayerSessionReservationRequest reservationRequest = keyValuePair.Value; - - MultiplayerSessionReservation reservation = ReservePlayerContext(requestConnection, - reservationRequest.PlayerSettings, - reservationRequest.AuthenticationContext, - reservationRequest.CorrelationId); - - requestConnection.SendPacket(reservation); - } - } - public bool TryGetPlayerByName(string playerName, out Player foundPlayer) { foundPlayer = null; From 2521bac519f1ec934bf831c04666380474e881eb Mon Sep 17 00:00:00 2001 From: TwinBuilderOne Date: Wed, 7 Dec 2022 18:55:37 -0600 Subject: [PATCH 02/40] Finished join queue loop --- .../Processors/PlayerSyncFinishedProcessor.cs | 2 +- NitroxServer/GameLogic/PlayerManager.cs | 70 ++++++++++++------- 2 files changed, 46 insertions(+), 26 deletions(-) diff --git a/NitroxServer/Communication/Packets/Processors/PlayerSyncFinishedProcessor.cs b/NitroxServer/Communication/Packets/Processors/PlayerSyncFinishedProcessor.cs index 57f43c8cc9..f556860b60 100644 --- a/NitroxServer/Communication/Packets/Processors/PlayerSyncFinishedProcessor.cs +++ b/NitroxServer/Communication/Packets/Processors/PlayerSyncFinishedProcessor.cs @@ -15,7 +15,7 @@ public PlayerSyncFinishedProcessor(PlayerManager playerManager) public override void Process(PlayerSyncFinished packet, Player player) { - playerManager.SyncFinishedCallback(); + playerManager.SyncFinishedCallback?.Invoke(); } } } diff --git a/NitroxServer/GameLogic/PlayerManager.cs b/NitroxServer/GameLogic/PlayerManager.cs index 5352d3bedd..0e6b493a92 100644 --- a/NitroxServer/GameLogic/PlayerManager.cs +++ b/NitroxServer/GameLogic/PlayerManager.cs @@ -1,7 +1,6 @@ using System; using System.Collections.Generic; using System.Linq; -using System.Runtime.Remoting.Messaging; using System.Text.RegularExpressions; using System.Threading; using System.Threading.Tasks; @@ -10,7 +9,6 @@ using NitroxModel.DataStructures.Unity; using NitroxModel.DataStructures.Util; using NitroxModel.Helper; -using NitroxModel.Logger; using NitroxModel.MultiplayerSession; using NitroxModel.Packets; using NitroxServer.Communication; @@ -132,13 +130,15 @@ public MultiplayerSessionReservation ReservePlayerContext( private async Task JoinQueueLoop() { + const int REFRESH_DELAY = 10; + while (true) { try { while (!JoinQueue.Any()) { - await Task.Delay(10); + await Task.Delay(REFRESH_DELAY); } var pair = JoinQueue.Dequeue(); @@ -154,35 +154,55 @@ private async Task JoinQueueLoop() connection.SendPacket(reservation); - CancellationTokenSource source = new(); - - Task timerTask = Task.Run(() => - { - Task.Delay(serverConfig.InitialSyncTimeout).Wait(source.Token); - - Log.Info("Timer expired"); - - SyncFinishedCallback = null; - - allPlayersByName.TryGetValue(request.AuthenticationContext.Username, out Player player); - player?.SendPacket(new PlayerKicked("An error occured while loading, initial sync took too long to complete")); - PlayerDisconnected(connection); - }); + CancellationTokenSource source = new(serverConfig.InitialSyncTimeout); + bool syncFinished = false; SyncFinishedCallback = () => { - source.Cancel(); - Log.Info($"Player {request.AuthenticationContext.Username} joined successfully. Remaining requests: {JoinQueue.Count}"); + syncFinished = true; }; - try + await Task.Run(() => { - await timerTask; - } - catch (TaskCanceledException) + while (!source.IsCancellationRequested) + { + if (syncFinished) + { + return true; + } + else + { + Task.Delay(REFRESH_DELAY).Wait(); + } + } + + return false; + + // We use ContinueWith to avoid having to try/catch a TaskCanceledException + }).ContinueWith(task => { - Log.Info("Timer cancelled"); - } + if (task.IsFaulted) + { + throw task.Exception; + } + + if (task.IsCanceled || !task.Result) + { + Log.Info($"Inital sync timed out for player {request.AuthenticationContext.Username}"); + SyncFinishedCallback = null; + + allPlayersByName.TryGetValue(request.AuthenticationContext.Username, out Player player); + if (connection.State == NitroxConnectionState.Connected) + { + connection.SendPacket(new PlayerKicked("An error occured while loading, initial sync took too long to complete")); + } + PlayerDisconnected(connection); + } + else + { + Log.Info($"Player {request.AuthenticationContext.Username} joined successfully. Remaining requests: {JoinQueue.Count}"); + } + }); } catch (Exception e) { From d6404e6a9d07634eb9505d6d1973df68eed79ee5 Mon Sep 17 00:00:00 2001 From: TwinBuilderOne Date: Wed, 7 Dec 2022 21:20:32 -0600 Subject: [PATCH 03/40] Enter join queue after base game finishes loading --- .../MultiplayerSessionManager.cs | 6 - .../Processors/PlayerSyncTimeoutProcessor.cs | 22 +++ .../MultiplayerSessionReservationState.cs | 5 +- NitroxModel/Packets/PlayerSyncTimeout.cs | 3 + .../Communication/Packets/PacketHandler.cs | 9 +- ...layerJoiningMultiplayerSessionProcessor.cs | 118 +------------ NitroxServer/GameLogic/PlayerManager.cs | 165 ++++++++++++++---- .../Serialization/World/WorldPersistence.cs | 2 +- 8 files changed, 165 insertions(+), 165 deletions(-) create mode 100644 NitroxClient/Communication/Packets/Processors/PlayerSyncTimeoutProcessor.cs create mode 100644 NitroxModel/Packets/PlayerSyncTimeout.cs diff --git a/NitroxClient/Communication/MultiplayerSession/MultiplayerSessionManager.cs b/NitroxClient/Communication/MultiplayerSession/MultiplayerSessionManager.cs index 89622f9968..02c6404f2e 100644 --- a/NitroxClient/Communication/MultiplayerSession/MultiplayerSessionManager.cs +++ b/NitroxClient/Communication/MultiplayerSession/MultiplayerSessionManager.cs @@ -93,12 +93,6 @@ public void RequestSessionReservation(PlayerSettings playerSettings, Authenticat public void ProcessReservationResponsePacket(MultiplayerSessionReservation reservation) { - if (reservation.ReservationState == MultiplayerSessionReservationState.ENQUEUED_IN_JOIN_QUEUE) - { - Log.InGame(Language.main.Get("Nitrox_Waiting")); - return; - } - Reservation = reservation; CurrentState.NegotiateReservationAsync(this); } diff --git a/NitroxClient/Communication/Packets/Processors/PlayerSyncTimeoutProcessor.cs b/NitroxClient/Communication/Packets/Processors/PlayerSyncTimeoutProcessor.cs new file mode 100644 index 0000000000..8b08d13f16 --- /dev/null +++ b/NitroxClient/Communication/Packets/Processors/PlayerSyncTimeoutProcessor.cs @@ -0,0 +1,22 @@ +using System.Collections; +using NitroxClient.Communication.Packets.Processors.Abstract; +using NitroxClient.MonoBehaviours; +using NitroxModel.Packets; +using UnityEngine; + +namespace NitroxClient.Communication.Packets.Processors; + +public class PlayerSyncTimeoutProcessor : ClientPacketProcessor +{ + public override void Process(PlayerSyncTimeout packet) + { + Multiplayer.Main.StartCoroutine(TimeoutRoutine()); + } + + private IEnumerator TimeoutRoutine() + { + Log.InGame("Error: Initial sync timeout, closing game"); + yield return new WaitForSecondsRealtime(5); + IngameMenu.main.QuitGame(false); + } +} diff --git a/NitroxModel/MultiplayerSession/MultiplayerSessionReservationState.cs b/NitroxModel/MultiplayerSession/MultiplayerSessionReservationState.cs index 0bf6509381..81ec3cf10d 100644 --- a/NitroxModel/MultiplayerSession/MultiplayerSessionReservationState.cs +++ b/NitroxModel/MultiplayerSession/MultiplayerSessionReservationState.cs @@ -23,11 +23,8 @@ public enum MultiplayerSessionReservationState [Description("The server is using hardcore gamemode, player is dead.")] HARDCORE_PLAYER_DEAD = 1 << 4, - [Description("Another user is currently joining the server.")] - ENQUEUED_IN_JOIN_QUEUE = 1 << 5, - [Description("The player name is invalid, It must not contain any space or doubtful characters\n Allowed characters : A-Z a-z 0-9 _ . -\nLength : [3, 25]")] - INCORRECT_USERNAME = 1 << 6 + INCORRECT_USERNAME = 1 << 5 } public static class MultiplayerSessionReservationStateExtensions diff --git a/NitroxModel/Packets/PlayerSyncTimeout.cs b/NitroxModel/Packets/PlayerSyncTimeout.cs new file mode 100644 index 0000000000..e8e552899f --- /dev/null +++ b/NitroxModel/Packets/PlayerSyncTimeout.cs @@ -0,0 +1,3 @@ +namespace NitroxModel.Packets; + +public class PlayerSyncTimeout : Packet { } diff --git a/NitroxServer/Communication/Packets/PacketHandler.cs b/NitroxServer/Communication/Packets/PacketHandler.cs index b06e7304ec..566f00db02 100644 --- a/NitroxServer/Communication/Packets/PacketHandler.cs +++ b/NitroxServer/Communication/Packets/PacketHandler.cs @@ -1,4 +1,5 @@ using System; +using System.Linq; using NitroxModel.Core; using NitroxModel.DataStructures.Util; using NitroxModel.Packets; @@ -23,13 +24,13 @@ public PacketHandler(PlayerManager playerManager, DefaultServerPacketProcessor p public void Process(Packet packet, NitroxConnection connection) { Player player = playerManager.GetPlayer(connection); - if (player == null) + if (player != null) { - ProcessUnauthenticated(packet, connection); + ProcessAuthenticated(packet, player); } - else + else if (!playerManager.GetQueuedPlayers().Contains(connection)) { - ProcessAuthenticated(packet, player); + ProcessUnauthenticated(packet, connection); } } diff --git a/NitroxServer/Communication/Packets/Processors/PlayerJoiningMultiplayerSessionProcessor.cs b/NitroxServer/Communication/Packets/Processors/PlayerJoiningMultiplayerSessionProcessor.cs index fc43e5d37e..6b2f706288 100644 --- a/NitroxServer/Communication/Packets/Processors/PlayerJoiningMultiplayerSessionProcessor.cs +++ b/NitroxServer/Communication/Packets/Processors/PlayerJoiningMultiplayerSessionProcessor.cs @@ -1,133 +1,21 @@ -using System.Collections.Generic; -using System.Linq; -using System.Net; -using NitroxModel.DataStructures; -using NitroxModel.DataStructures.GameLogic; -using NitroxModel.DataStructures.Util; -using NitroxModel.Packets; +using NitroxModel.Packets; using NitroxServer.Communication.Packets.Processors.Abstract; using NitroxServer.GameLogic; -using NitroxServer.Serialization.World; namespace NitroxServer.Communication.Packets.Processors { public class PlayerJoiningMultiplayerSessionProcessor : UnauthenticatedPacketProcessor { private readonly PlayerManager playerManager; - private readonly ScheduleKeeper scheduleKeeper; - private readonly EventTriggerer eventTriggerer; - private readonly World world; - public PlayerJoiningMultiplayerSessionProcessor(ScheduleKeeper scheduleKeeper, EventTriggerer eventTriggerer, PlayerManager playerManager, World world) + public PlayerJoiningMultiplayerSessionProcessor(PlayerManager playerManager) { - this.scheduleKeeper = scheduleKeeper; - this.eventTriggerer = eventTriggerer; this.playerManager = playerManager; - this.world = world; } public override void Process(PlayerJoiningMultiplayerSession packet, NitroxConnection connection) { - Player player = playerManager.PlayerConnected(connection, packet.ReservationKey, out bool wasBrandNewPlayer); - - NitroxId assignedEscapePodId = world.EscapePodManager.AssignPlayerToEscapePod(player.Id, out Optional newlyCreatedEscapePod); - if (newlyCreatedEscapePod.HasValue) - { - AddEscapePod addEscapePod = new(newlyCreatedEscapePod.Value); - playerManager.SendPacketToOtherPlayers(addEscapePod, player); - } - - List equippedItems = player.GetEquipment(); - List techTypes = equippedItems.Select(equippedItem => equippedItem.TechType).ToList(); - List inventoryItems = GetInventoryItems(player.GameObjectId); - - PlayerJoinedMultiplayerSession playerJoinedPacket = new(player.PlayerContext, player.SubRootId, techTypes, inventoryItems); - playerManager.SendPacketToOtherPlayers(playerJoinedPacket, player); - - // Make players on localhost admin by default. - if (IPAddress.IsLoopback(connection.Endpoint.Address)) - { - player.Permissions = Perms.ADMIN; - } - - List simulations = world.EntitySimulation.AssignGlobalRootEntities(player).ToList(); - IEnumerable vehicles = world.VehicleManager.GetVehicles(); - foreach (VehicleModel vehicle in vehicles) - { - if (world.SimulationOwnershipData.TryToAcquire(vehicle.Id, player, SimulationLockType.TRANSIENT)) - { - simulations.Add(vehicle.Id); - } - } - - InitialPlayerSync initialPlayerSync = new(player.GameObjectId, - wasBrandNewPlayer, - world.EscapePodManager.GetEscapePods(), - assignedEscapePodId, - equippedItems, - GetAllModules(world.InventoryManager.GetAllModules(), player.GetModules()), - world.BaseManager.GetBasePiecesForNewlyConnectedPlayer(), - vehicles, - world.InventoryManager.GetAllInventoryItems(), - world.InventoryManager.GetAllStorageSlotItems(), - player.UsedItems, - player.QuickSlotsBinding, - world.GameData.PDAState.GetInitialPDAData(), - world.GameData.StoryGoals.GetInitialStoryGoalData(scheduleKeeper), - player.CompletedGoals, - player.Position, - player.Rotation, - player.SubRootId, - player.Stats, - GetRemotePlayerData(player), - world.EntityManager.GetGlobalRootEntities(), - simulations, - world.GameMode, - player.Permissions, - player.PingInstancePreferences.ToDictionary(m => m.Key, m => m.Value) - ); - - player.SendPacket(new TimeChange(eventTriggerer.ElapsedSeconds, true)); - player.SendPacket(initialPlayerSync); - } - - private List GetRemotePlayerData(Player player) - { - List playerData = new(); - - foreach (Player otherPlayer in playerManager.GetConnectedPlayers()) - { - if (!player.Equals(otherPlayer)) - { - List equippedItems = otherPlayer.GetEquipment(); - List techTypes = equippedItems.Select(equippedItem => equippedItem.TechType).ToList(); - - InitialRemotePlayerData remotePlayer = new(otherPlayer.PlayerContext, otherPlayer.Position, otherPlayer.SubRootId, techTypes); - playerData.Add(remotePlayer); - } - } - - return playerData; - } - - private List GetAllModules(ICollection globalModules, List playerModules) - { - List modulesToSync = new(); - modulesToSync.AddRange(globalModules); - modulesToSync.AddRange(playerModules); - return modulesToSync; - } - - private List GetInventoryItems(NitroxId playerID) - { - List inventoryItems = world.InventoryManager.GetAllInventoryItems().Where(item => item.ContainerId.Equals(playerID)).ToList(); - - for (int index = 0; index < inventoryItems.Count; index++) //Also add batteries from tools to inventory items. - { - inventoryItems.AddRange(world.InventoryManager.GetAllStorageSlotItems().Where(item => item.ContainerId.Equals(inventoryItems[index].ItemId))); - } - - return inventoryItems; + playerManager.AddToJoinQueue(connection, packet.ReservationKey); } } } diff --git a/NitroxServer/GameLogic/PlayerManager.cs b/NitroxServer/GameLogic/PlayerManager.cs index 0e6b493a92..a431f7152c 100644 --- a/NitroxServer/GameLogic/PlayerManager.cs +++ b/NitroxServer/GameLogic/PlayerManager.cs @@ -1,6 +1,7 @@ using System; using System.Collections.Generic; using System.Linq; +using System.Net; using System.Text.RegularExpressions; using System.Threading; using System.Threading.Tasks; @@ -13,28 +14,32 @@ using NitroxModel.Packets; using NitroxServer.Communication; using NitroxServer.Serialization; +using NitroxServer.Serialization.World; namespace NitroxServer.GameLogic { // TODO: These methods are a little chunky. Need to look at refactoring just to clean them up and get them around 30 lines a piece. public class PlayerManager { + private readonly World world; + private readonly ThreadSafeDictionary allPlayersByName; private readonly ThreadSafeDictionary assetsByConnection = new(); private readonly ThreadSafeDictionary reservations = new(); private readonly ThreadSafeSet reservedPlayerNames = new("Player"); // "Player" is often used to identify the local player and should not be used by any user - private ThreadSafeQueue> JoinQueue { get; set; } = new(); + private ThreadSafeQueue<(NitroxConnection, string)> JoinQueue { get; set; } = new(); public Action SyncFinishedCallback { get; private set; } private readonly ServerConfig serverConfig; private ushort currentPlayerId; - public PlayerManager(List players, ServerConfig serverConfig) + public PlayerManager(List players, World world, ServerConfig serverConfig) { allPlayersByName = new ThreadSafeDictionary(players.ToDictionary(x => x.Name), false); currentPlayerId = players.Count == 0 ? (ushort)0 : players.Max(x => x.Id); + this.world = world; this.serverConfig = serverConfig; _ = JoinQueueLoop(); @@ -50,12 +55,16 @@ public IEnumerable GetAllPlayers() return allPlayersByName.Values; } + public IEnumerable GetQueuedPlayers() + { + return JoinQueue.Select(tuple => tuple.Item1); + } + public MultiplayerSessionReservation ReservePlayerContext( NitroxConnection connection, PlayerSettings playerSettings, AuthenticationContext authenticationContext, - string correlationId, - bool ignoreJoinQueue = false) + string correlationId) { if (reservedPlayerNames.Count >= serverConfig.MaxConnections) { @@ -76,21 +85,6 @@ public MultiplayerSessionReservation ReservePlayerContext( return new MultiplayerSessionReservation(correlationId, rejectedState); } - if (!ignoreJoinQueue) - { - if (JoinQueue.Any(pair => ReferenceEquals(pair.Key, connection))) - { - // Don't enqueue the request if there is already another enqueued request by the same user - return new MultiplayerSessionReservation(correlationId, MultiplayerSessionReservationState.REJECTED); - } - - JoinQueue.Enqueue(new KeyValuePair( - connection, - new MultiplayerSessionReservationRequest(correlationId, playerSettings, authenticationContext))); - - return new MultiplayerSessionReservation(correlationId, MultiplayerSessionReservationState.ENQUEUED_IN_JOIN_QUEUE); - } - string playerName = authenticationContext.Username; allPlayersByName.TryGetValue(playerName, out Player player); @@ -132,6 +126,8 @@ private async Task JoinQueueLoop() { const int REFRESH_DELAY = 10; + string GetLogName(NitroxConnection connection) => assetsByConnection.TryGetValue(connection, out ConnectionAssets assets) ? assets.Player.Name : $"at {connection.Endpoint}"; + while (true) { try @@ -141,18 +137,9 @@ private async Task JoinQueueLoop() await Task.Delay(REFRESH_DELAY); } - var pair = JoinQueue.Dequeue(); - NitroxConnection connection = pair.Key; - MultiplayerSessionReservationRequest request = pair.Value; + (NitroxConnection connection, string reservationKey) = JoinQueue.Dequeue(); - - MultiplayerSessionReservation reservation = ReservePlayerContext(connection, - request.PlayerSettings, - request.AuthenticationContext, - request.CorrelationId, - ignoreJoinQueue: true); - - connection.SendPacket(reservation); + SendInitialSync(connection, reservationKey); CancellationTokenSource source = new(serverConfig.InitialSyncTimeout); bool syncFinished = false; @@ -188,19 +175,18 @@ await Task.Run(() => if (task.IsCanceled || !task.Result) { - Log.Info($"Inital sync timed out for player {request.AuthenticationContext.Username}"); + Log.Info($"Inital sync timed out for player {GetLogName(connection)}"); SyncFinishedCallback = null; - allPlayersByName.TryGetValue(request.AuthenticationContext.Username, out Player player); if (connection.State == NitroxConnectionState.Connected) { - connection.SendPacket(new PlayerKicked("An error occured while loading, initial sync took too long to complete")); + connection.SendPacket(new PlayerSyncTimeout()); } PlayerDisconnected(connection); } else { - Log.Info($"Player {request.AuthenticationContext.Username} joined successfully. Remaining requests: {JoinQueue.Count}"); + Log.Info($"Player {GetLogName(connection)} joined successfully. Remaining requests: {JoinQueue.Count}"); } }); } @@ -211,10 +197,119 @@ await Task.Run(() => } } + public void AddToJoinQueue(NitroxConnection connection, string reservationKey) + { + JoinQueue.Enqueue((connection, reservationKey)); + } + + private void SendInitialSync(NitroxConnection connection, string reservationKey) + { + List GetRemotePlayerData(Player player) + { + List playerData = new(); + + foreach (Player otherPlayer in GetConnectedPlayers()) + { + if (!player.Equals(otherPlayer)) + { + List equippedItems = otherPlayer.GetEquipment(); + List techTypes = equippedItems.Select(equippedItem => equippedItem.TechType).ToList(); + + InitialRemotePlayerData remotePlayer = new(otherPlayer.PlayerContext, otherPlayer.Position, otherPlayer.SubRootId, techTypes); + playerData.Add(remotePlayer); + } + } + + return playerData; + } + + List GetAllModules(ICollection globalModules, List playerModules) + { + List modulesToSync = new(); + modulesToSync.AddRange(globalModules); + modulesToSync.AddRange(playerModules); + return modulesToSync; + } + + List GetInventoryItems(NitroxId playerID) + { + List inventoryItems = world.InventoryManager.GetAllInventoryItems().Where(item => item.ContainerId.Equals(playerID)).ToList(); + + for (int index = 0; index < inventoryItems.Count; index++) //Also add batteries from tools to inventory items. + { + inventoryItems.AddRange(world.InventoryManager.GetAllStorageSlotItems().Where(item => item.ContainerId.Equals(inventoryItems[index].ItemId))); + } + + return inventoryItems; + } + + Player player = PlayerConnected(connection, reservationKey, out bool wasBrandNewPlayer); + + NitroxId assignedEscapePodId = world.EscapePodManager.AssignPlayerToEscapePod(player.Id, out Optional newlyCreatedEscapePod); + if (newlyCreatedEscapePod.HasValue) + { + AddEscapePod addEscapePod = new(newlyCreatedEscapePod.Value); + SendPacketToOtherPlayers(addEscapePod, player); + } + + List equippedItems = player.GetEquipment(); + List techTypes = equippedItems.Select(equippedItem => equippedItem.TechType).ToList(); + List inventoryItems = GetInventoryItems(player.GameObjectId); + + PlayerJoinedMultiplayerSession playerJoinedPacket = new(player.PlayerContext, player.SubRootId, techTypes, inventoryItems); + SendPacketToOtherPlayers(playerJoinedPacket, player); + + // Make players on localhost admin by default. + if (IPAddress.IsLoopback(connection.Endpoint.Address)) + { + player.Permissions = Perms.ADMIN; + } + + List simulations = world.EntitySimulation.AssignGlobalRootEntities(player).ToList(); + IEnumerable vehicles = world.VehicleManager.GetVehicles(); + foreach (VehicleModel vehicle in vehicles) + { + if (world.SimulationOwnershipData.TryToAcquire(vehicle.Id, player, SimulationLockType.TRANSIENT)) + { + simulations.Add(vehicle.Id); + } + } + + InitialPlayerSync initialPlayerSync = new(player.GameObjectId, + wasBrandNewPlayer, + world.EscapePodManager.GetEscapePods(), + assignedEscapePodId, + equippedItems, + GetAllModules(world.InventoryManager.GetAllModules(), player.GetModules()), + world.BaseManager.GetBasePiecesForNewlyConnectedPlayer(), + vehicles, + world.InventoryManager.GetAllInventoryItems(), + world.InventoryManager.GetAllStorageSlotItems(), + player.UsedItems, + player.QuickSlotsBinding, + world.GameData.PDAState.GetInitialPDAData(), + world.GameData.StoryGoals.GetInitialStoryGoalData(world.ScheduleKeeper), + player.CompletedGoals, + player.Position, + player.Rotation, + player.SubRootId, + player.Stats, + GetRemotePlayerData(player), + world.EntityManager.GetGlobalRootEntities(), + simulations, + world.GameMode, + player.Permissions, + player.PingInstancePreferences.ToDictionary(m => m.Key, m => m.Value) + ); + + player.SendPacket(new TimeChange(world.EventTriggerer.ElapsedSeconds, true)); + player.SendPacket(initialPlayerSync); + } + public void NonPlayerDisconnected(NitroxConnection connection) { // Remove any requests sent by the connection from the join queue - JoinQueue = new(JoinQueue.Where(pair => !Equals(pair.Key, connection))); + JoinQueue = new(JoinQueue.Where(tuple => !Equals(tuple.Item1, connection))); } public Player PlayerConnected(NitroxConnection connection, string reservationKey, out bool wasBrandNewPlayer) diff --git a/NitroxServer/Serialization/World/WorldPersistence.cs b/NitroxServer/Serialization/World/WorldPersistence.cs index 933100302e..55b4c690f7 100644 --- a/NitroxServer/Serialization/World/WorldPersistence.cs +++ b/NitroxServer/Serialization/World/WorldPersistence.cs @@ -187,7 +187,6 @@ public World CreateWorld(PersistedWorldData pWorldData, ServerGameMode gameMode) World world = new() { SimulationOwnershipData = new SimulationOwnershipData(), - PlayerManager = new PlayerManager(pWorldData.PlayerData.GetPlayers(), config), BaseManager = new BaseManager(pWorldData.BaseData.PartiallyConstructedPieces, pWorldData.BaseData.CompletedBasePieceHistory), @@ -200,6 +199,7 @@ public World CreateWorld(PersistedWorldData pWorldData, ServerGameMode gameMode) Seed = seed }; + world.PlayerManager = new PlayerManager(pWorldData.PlayerData.GetPlayers(), world, config); world.EventTriggerer = new EventTriggerer(world.PlayerManager, pWorldData.WorldData.GameData.PDAState, pWorldData.WorldData.GameData.StoryGoals, seed, pWorldData.WorldData.GameData.StoryTiming.ElapsedTime, pWorldData.WorldData.GameData.StoryTiming.AuroraExplosionTime, pWorldData.WorldData.GameData.StoryTiming.AuroraWarningTime); world.VehicleManager = new VehicleManager(pWorldData.WorldData.VehicleData.Vehicles, world.InventoryManager, world.SimulationOwnershipData); world.ScheduleKeeper = new ScheduleKeeper(pWorldData.WorldData.GameData.PDAState, pWorldData.WorldData.GameData.StoryGoals, world.EventTriggerer, world.PlayerManager); From 02ee6c626469f00ba280d3d28365aab119e79589 Mon Sep 17 00:00:00 2001 From: Coding-Hen Date: Sat, 10 Dec 2022 12:32:56 +0000 Subject: [PATCH 04/40] Changed OnLateUpdate to UpdatePings (#1913) --- NitroxPatcher/Patches/Dynamic/uGUI_Pings_UpdatePings_Patch.cs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/NitroxPatcher/Patches/Dynamic/uGUI_Pings_UpdatePings_Patch.cs b/NitroxPatcher/Patches/Dynamic/uGUI_Pings_UpdatePings_Patch.cs index c5cbf7b187..f7c1ad16e9 100644 --- a/NitroxPatcher/Patches/Dynamic/uGUI_Pings_UpdatePings_Patch.cs +++ b/NitroxPatcher/Patches/Dynamic/uGUI_Pings_UpdatePings_Patch.cs @@ -1,4 +1,4 @@ -using System.Collections.Generic; +using System.Collections.Generic; using System.Linq; using System.Reflection; using System.Reflection.Emit; From 4cb25d9c09055200b4c3ddb0335a8a85263b00ca Mon Sep 17 00:00:00 2001 From: TwinBuilderOne Date: Sat, 18 Mar 2023 10:56:36 -0500 Subject: [PATCH 05/40] Move initial sync code to the new spot after the merge reset it --- ...layerJoiningMultiplayerSessionProcessor.cs | 109 +----------------- NitroxServer/GameLogic/PlayerManager.cs | 90 +++++++-------- 2 files changed, 40 insertions(+), 159 deletions(-) diff --git a/NitroxServer/Communication/Packets/Processors/PlayerJoiningMultiplayerSessionProcessor.cs b/NitroxServer/Communication/Packets/Processors/PlayerJoiningMultiplayerSessionProcessor.cs index 70319c0102..f5509a4daa 100644 --- a/NitroxServer/Communication/Packets/Processors/PlayerJoiningMultiplayerSessionProcessor.cs +++ b/NitroxServer/Communication/Packets/Processors/PlayerJoiningMultiplayerSessionProcessor.cs @@ -1,17 +1,6 @@ -using System.Collections.Generic; -using System.Linq; -using System.Net; -using NitroxModel.DataStructures; -using NitroxModel.DataStructures.GameLogic; -using NitroxModel.DataStructures.GameLogic.Entities; -using NitroxModel.DataStructures.Unity; -using NitroxModel.DataStructures.Util; -using NitroxModel.MultiplayerSession; using NitroxModel.Packets; using NitroxServer.Communication.Packets.Processors.Abstract; using NitroxServer.GameLogic; -using NitroxServer.GameLogic.Entities; -using NitroxServer.Serialization.World; namespace NitroxServer.Communication.Packets.Processors { @@ -19,110 +8,14 @@ public class PlayerJoiningMultiplayerSessionProcessor : UnauthenticatedPacketPro { private readonly PlayerManager playerManager; - private readonly ScheduleKeeper scheduleKeeper; - private readonly StoryManager storyManager; - private readonly World world; - private readonly EntityRegistry entityRegistry; - - public PlayerJoiningMultiplayerSessionProcessor(ScheduleKeeper scheduleKeeper, StoryManager storyManager, PlayerManager playerManager, World world, EntityRegistry entityRegistry) + public PlayerJoiningMultiplayerSessionProcessor(PlayerManager playerManager) { - this.scheduleKeeper = scheduleKeeper; - this.storyManager = storyManager; this.playerManager = playerManager; - this.world = world; - this.entityRegistry = entityRegistry; } public override void Process(PlayerJoiningMultiplayerSession packet, NitroxConnection connection) { playerManager.AddToJoinQueue(connection, packet.ReservationKey); - - Player player = playerManager.PlayerConnected(connection, packet.ReservationKey, out bool wasBrandNewPlayer); - NitroxId assignedEscapePodId = world.EscapePodManager.AssignPlayerToEscapePod(player.Id, out Optional newlyCreatedEscapePod); - - if (newlyCreatedEscapePod.HasValue) - { - CellEntities spawnNewEscapePod = new(newlyCreatedEscapePod.Value); - playerManager.SendPacketToOtherPlayers(spawnNewEscapePod, player); - } - - List equippedItems = player.GetEquipment(); - List techTypes = equippedItems.Select(equippedItem => equippedItem.TechType).ToList(); - - PlayerJoinedMultiplayerSession playerJoinedPacket = new(player.PlayerContext, player.SubRootId, techTypes); - playerManager.SendPacketToOtherPlayers(playerJoinedPacket, player); - - // Make players on localhost admin by default. - if (IPAddress.IsLoopback(connection.Endpoint.Address)) - { - player.Permissions = Perms.ADMIN; - } - - List simulations = world.EntitySimulation.AssignGlobalRootEntities(player).ToList(); - - if (wasBrandNewPlayer) - { - SetupPlayerEntity(player); - } - else - { - RespawnExistingEntity(player); - } - - InitialPlayerSync initialPlayerSync = new(player.GameObjectId, - wasBrandNewPlayer, - assignedEscapePodId, - equippedItems, - world.BaseManager.GetBasePiecesForNewlyConnectedPlayer(), - world.InventoryManager.GetAllStorageSlotItems(), - player.UsedItems, - player.QuickSlotsBindingIds, - world.GameData.PDAState.GetInitialPDAData(), - world.GameData.StoryGoals.GetInitialStoryGoalData(scheduleKeeper, player), - player.Position, - player.Rotation, - player.SubRootId, - player.Stats, - GetOtherPlayers(player), - world.WorldEntityManager.GetGlobalRootEntities(), - simulations, - world.GameMode, - player.Permissions, - new(new(player.PingInstancePreferences), player.PinnedRecipePreferences.ToList()), - storyManager.GetTimeData() - ); - - player.SendPacket(initialPlayerSync); - } - - private IEnumerable GetOtherPlayers(Player player) - { - return playerManager.GetConnectedPlayers().Where(p => p != player) - .Select(p => p.PlayerContext); - } - - private void SetupPlayerEntity(Player player) - { - NitroxTransform transform = new(player.Position, player.Rotation, NitroxVector3.One); - - PlayerWorldEntity playerEntity = new PlayerWorldEntity(transform, 0, null, false, null, true, player.GameObjectId, NitroxTechType.None, null, null, new List()); - entityRegistry.AddEntity(playerEntity); - world.WorldEntityManager.TrackEntityInTheWorld(playerEntity); - playerManager.SendPacketToOtherPlayers(new CellEntities(playerEntity), player); - } - - private void RespawnExistingEntity(Player player) - { - Optional playerEntity = entityRegistry.GetEntityById(player.PlayerContext.PlayerNitroxId); - - if (playerEntity.HasValue) - { - playerManager.SendPacketToOtherPlayers(new CellEntities(playerEntity.Value, true), player); - } - else - { - Log.Error($"Unable to find player entity for {player.Name}"); - } } } } diff --git a/NitroxServer/GameLogic/PlayerManager.cs b/NitroxServer/GameLogic/PlayerManager.cs index a55ed9dcb6..0f824f5212 100644 --- a/NitroxServer/GameLogic/PlayerManager.cs +++ b/NitroxServer/GameLogic/PlayerManager.cs @@ -7,12 +7,14 @@ using System.Threading.Tasks; using NitroxModel.DataStructures; using NitroxModel.DataStructures.GameLogic; +using NitroxModel.DataStructures.GameLogic.Entities; using NitroxModel.DataStructures.Unity; using NitroxModel.DataStructures.Util; using NitroxModel.Helper; using NitroxModel.MultiplayerSession; using NitroxModel.Packets; using NitroxServer.Communication; +using NitroxServer.GameLogic.Entities; using NitroxServer.Serialization; using NitroxServer.Serialization.World; @@ -204,59 +206,49 @@ public void AddToJoinQueue(NitroxConnection connection, string reservationKey) private void SendInitialSync(NitroxConnection connection, string reservationKey) { - List GetRemotePlayerData(Player player) + IEnumerable GetOtherPlayers(Player player) { - List playerData = new(); - - foreach (Player otherPlayer in GetConnectedPlayers()) - { - if (!player.Equals(otherPlayer)) - { - List equippedItems = otherPlayer.GetEquipment(); - List techTypes = equippedItems.Select(equippedItem => equippedItem.TechType).ToList(); - - InitialRemotePlayerData remotePlayer = new(otherPlayer.PlayerContext, otherPlayer.Position, otherPlayer.SubRootId, techTypes); - playerData.Add(remotePlayer); - } - } - - return playerData; + return GetConnectedPlayers().Where(p => p != player) + .Select(p => p.PlayerContext); } - List GetAllModules(ICollection globalModules, List playerModules) + void SetupPlayerEntity(Player player) { - List modulesToSync = new(); - modulesToSync.AddRange(globalModules); - modulesToSync.AddRange(playerModules); - return modulesToSync; + NitroxTransform transform = new(player.Position, player.Rotation, NitroxVector3.One); + + PlayerWorldEntity playerEntity = new PlayerWorldEntity(transform, 0, null, false, null, true, player.GameObjectId, NitroxTechType.None, null, null, new List()); + world.EntityRegistry.AddEntity(playerEntity); + world.WorldEntityManager.TrackEntityInTheWorld(playerEntity); + SendPacketToOtherPlayers(new CellEntities(playerEntity), player); } - List GetInventoryItems(NitroxId playerID) + void RespawnExistingEntity(Player player) { - List inventoryItems = world.InventoryManager.GetAllInventoryItems().Where(item => item.ContainerId.Equals(playerID)).ToList(); + Optional playerEntity = world.EntityRegistry.GetEntityById(player.PlayerContext.PlayerNitroxId); - for (int index = 0; index < inventoryItems.Count; index++) //Also add batteries from tools to inventory items. + if (playerEntity.HasValue) { - inventoryItems.AddRange(world.InventoryManager.GetAllStorageSlotItems().Where(item => item.ContainerId.Equals(inventoryItems[index].ItemId))); + SendPacketToOtherPlayers(new CellEntities(playerEntity.Value, true), player); + } + else + { + Log.Error($"Unable to find player entity for {player.Name}"); } - - return inventoryItems; } Player player = PlayerConnected(connection, reservationKey, out bool wasBrandNewPlayer); + NitroxId assignedEscapePodId = world.EscapePodManager.AssignPlayerToEscapePod(player.Id, out Optional newlyCreatedEscapePod); - NitroxId assignedEscapePodId = world.EscapePodManager.AssignPlayerToEscapePod(player.Id, out Optional newlyCreatedEscapePod); if (newlyCreatedEscapePod.HasValue) { - AddEscapePod addEscapePod = new(newlyCreatedEscapePod.Value); - SendPacketToOtherPlayers(addEscapePod, player); + CellEntities spawnNewEscapePod = new(newlyCreatedEscapePod.Value); + SendPacketToOtherPlayers(spawnNewEscapePod, player); } List equippedItems = player.GetEquipment(); List techTypes = equippedItems.Select(equippedItem => equippedItem.TechType).ToList(); - List inventoryItems = GetInventoryItems(player.GameObjectId); - PlayerJoinedMultiplayerSession playerJoinedPacket = new(player.PlayerContext, player.SubRootId, techTypes, inventoryItems); + PlayerJoinedMultiplayerSession playerJoinedPacket = new(player.PlayerContext, player.SubRootId, techTypes); SendPacketToOtherPlayers(playerJoinedPacket, player); // Make players on localhost admin by default. @@ -266,43 +258,39 @@ List GetInventoryItems(NitroxId playerID) } List simulations = world.EntitySimulation.AssignGlobalRootEntities(player).ToList(); - IEnumerable vehicles = world.VehicleManager.GetVehicles(); - foreach (VehicleModel vehicle in vehicles) + + if (wasBrandNewPlayer) { - if (world.SimulationOwnershipData.TryToAcquire(vehicle.Id, player, SimulationLockType.TRANSIENT)) - { - simulations.Add(vehicle.Id); - } + SetupPlayerEntity(player); + } + else + { + RespawnExistingEntity(player); } InitialPlayerSync initialPlayerSync = new(player.GameObjectId, wasBrandNewPlayer, - world.EscapePodManager.GetEscapePods(), - assignedEscapePodId, - equippedItems, - GetAllModules(world.InventoryManager.GetAllModules(), player.GetModules()), + assignedEscapePodId, + equippedItems, world.BaseManager.GetBasePiecesForNewlyConnectedPlayer(), - vehicles, - world.InventoryManager.GetAllInventoryItems(), world.InventoryManager.GetAllStorageSlotItems(), player.UsedItems, - player.QuickSlotsBinding, + player.QuickSlotsBindingIds, world.GameData.PDAState.GetInitialPDAData(), - world.GameData.StoryGoals.GetInitialStoryGoalData(world.ScheduleKeeper), - player.CompletedGoals, + world.GameData.StoryGoals.GetInitialStoryGoalData(world.ScheduleKeeper, player), player.Position, player.Rotation, player.SubRootId, player.Stats, - GetRemotePlayerData(player), - world.EntityManager.GetGlobalRootEntities(), + GetOtherPlayers(player), + world.WorldEntityManager.GetGlobalRootEntities(), simulations, world.GameMode, player.Permissions, - player.PingInstancePreferences.ToDictionary(m => m.Key, m => m.Value) + new(new(player.PingInstancePreferences), player.PinnedRecipePreferences.ToList()), + world.StoryManager.GetTimeData() ); - player.SendPacket(new TimeChange(world.EventTriggerer.ElapsedSeconds, true)); player.SendPacket(initialPlayerSync); } From eddd834874e5c42f1d3b2ad34cca9bf57ed2b6eb Mon Sep 17 00:00:00 2001 From: TwinBuilderOne Date: Sat, 18 Mar 2023 11:18:23 -0500 Subject: [PATCH 06/40] Fix broken in-game log for join queue stage --- .../ConnectionState/SessionReserved.cs | 4 +++- .../MultiplayerSession/MultiplayerSessionManager.cs | 9 +-------- 2 files changed, 4 insertions(+), 9 deletions(-) diff --git a/NitroxClient/Communication/MultiplayerSession/ConnectionState/SessionReserved.cs b/NitroxClient/Communication/MultiplayerSession/ConnectionState/SessionReserved.cs index e37024124a..8dfd21fe5c 100644 --- a/NitroxClient/Communication/MultiplayerSession/ConnectionState/SessionReserved.cs +++ b/NitroxClient/Communication/MultiplayerSession/ConnectionState/SessionReserved.cs @@ -1,4 +1,4 @@ -using System; +using System; using NitroxClient.Communication.Abstract; using NitroxModel.Packets; @@ -40,6 +40,8 @@ private void EnterMultiplayerSession(IMultiplayerSessionConnectionContext sessio PlayerJoiningMultiplayerSession packet = new PlayerJoiningMultiplayerSession(correlationId, reservationKey); client.Send(packet); + + Log.InGame(Language.main.Get("Nitrox_Waiting")); } private void ChangeState(IMultiplayerSessionConnectionContext sessionConnectionContext) diff --git a/NitroxClient/Communication/MultiplayerSession/MultiplayerSessionManager.cs b/NitroxClient/Communication/MultiplayerSession/MultiplayerSessionManager.cs index 30a8b061d4..5b683c9dcc 100644 --- a/NitroxClient/Communication/MultiplayerSession/MultiplayerSessionManager.cs +++ b/NitroxClient/Communication/MultiplayerSession/MultiplayerSessionManager.cs @@ -1,4 +1,4 @@ -using System; +using System; using System.Collections.Generic; using System.Threading.Tasks; using NitroxClient.Communication.Abstract; @@ -77,13 +77,6 @@ public void ProcessSessionPolicy(MultiplayerSessionPolicy policy) public void RequestSessionReservation(PlayerSettings playerSettings, AuthenticationContext authenticationContext) { - // If a reservation has already been sent (in which case the client is enqueued in the join queue) - if (CurrentState.CurrentStage == MultiplayerSessionConnectionStage.AWAITING_SESSION_RESERVATION) - { - Log.InGame(Language.main.Get("Nitrox_Waiting")); - return; - } - PlayerSettings = playerSettings; AuthenticationContext = authenticationContext; CurrentState.NegotiateReservationAsync(this); From 5dc17a9b6367c9f25b9ce194346361cd819279fc Mon Sep 17 00:00:00 2001 From: TwinBuilderOne Date: Sat, 18 Mar 2023 11:43:57 -0500 Subject: [PATCH 07/40] Use WaitScreen to stay on loading screen --- .../MultiplayerSession/ConnectionState/SessionReserved.cs | 2 -- .../Packets/Processors/InitialPlayerSyncProcessor.cs | 3 ++- NitroxClient/MonoBehaviours/Multiplayer.cs | 8 +++++++- 3 files changed, 9 insertions(+), 4 deletions(-) diff --git a/NitroxClient/Communication/MultiplayerSession/ConnectionState/SessionReserved.cs b/NitroxClient/Communication/MultiplayerSession/ConnectionState/SessionReserved.cs index 8dfd21fe5c..32e880904b 100644 --- a/NitroxClient/Communication/MultiplayerSession/ConnectionState/SessionReserved.cs +++ b/NitroxClient/Communication/MultiplayerSession/ConnectionState/SessionReserved.cs @@ -40,8 +40,6 @@ private void EnterMultiplayerSession(IMultiplayerSessionConnectionContext sessio PlayerJoiningMultiplayerSession packet = new PlayerJoiningMultiplayerSession(correlationId, reservationKey); client.Send(packet); - - Log.InGame(Language.main.Get("Nitrox_Waiting")); } private void ChangeState(IMultiplayerSessionConnectionContext sessionConnectionContext) diff --git a/NitroxClient/Communication/Packets/Processors/InitialPlayerSyncProcessor.cs b/NitroxClient/Communication/Packets/Processors/InitialPlayerSyncProcessor.cs index 0092035b2b..2e29f8c38d 100644 --- a/NitroxClient/Communication/Packets/Processors/InitialPlayerSyncProcessor.cs +++ b/NitroxClient/Communication/Packets/Processors/InitialPlayerSyncProcessor.cs @@ -1,4 +1,4 @@ -using System; +using System; using System.Collections; using System.Collections.Generic; using NitroxClient.Communication.Abstract; @@ -31,6 +31,7 @@ public InitialPlayerSyncProcessor(IPacketSender packetSender, IEnumerable /// True if multiplayer is loaded and client is connected to a server. @@ -93,6 +94,11 @@ public static IEnumerator LoadAsync() yield return Main.StartCoroutine(Main.StartSession()); WaitScreen.Remove(item); + WaitScreen.ManualWaitItem joinQueueItem = WaitScreen.Add(Language.main.Get("Nitrox_Waiting")); + Main.InsideJoinQueue = true; + yield return new WaitUntil(() => !Main.InsideJoinQueue); + WaitScreen.Remove(joinQueueItem); + yield return new WaitUntil(() => Main.InitialSyncCompleted); SetLoadingComplete(); From 1717850a01e661c42646267a620e2dacff120e9d Mon Sep 17 00:00:00 2001 From: TwinBuilderOne Date: Sat, 18 Mar 2023 11:44:15 -0500 Subject: [PATCH 08/40] Reduce default initial sync timeout --- NitroxServer/Serialization/ServerConfig.cs | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/NitroxServer/Serialization/ServerConfig.cs b/NitroxServer/Serialization/ServerConfig.cs index ea8858fd6e..bb3cd52ed7 100644 --- a/NitroxServer/Serialization/ServerConfig.cs +++ b/NitroxServer/Serialization/ServerConfig.cs @@ -1,4 +1,4 @@ -using NitroxModel.DataStructures.GameLogic; +using NitroxModel.DataStructures.GameLogic; using NitroxModel.Helper; using NitroxModel.Serialization; using NitroxModel.Server; @@ -10,7 +10,7 @@ public class ServerConfig : NitroxConfig { private int maxConnectionsSetting = 100; - private int initialSyncTimeoutSetting = 300000; + private int initialSyncTimeoutSetting = 120000; [PropertyDescription("Set to true to Cache entities for the whole map on next run. \nWARNING! Will make server load take longer on the cache run but players will gain a performance boost when entering new areas.")] public bool CreateFullEntityCache = false; From 86010a6e865558a61e7007604d2c947d955f9051 Mon Sep 17 00:00:00 2001 From: TwinBuilderOne Date: Sat, 15 Apr 2023 10:08:31 -0500 Subject: [PATCH 09/40] Remove unnecessary using --- NitroxServer/GameLogic/PlayerManager.cs | 1 - 1 file changed, 1 deletion(-) diff --git a/NitroxServer/GameLogic/PlayerManager.cs b/NitroxServer/GameLogic/PlayerManager.cs index 0f824f5212..a27fece12f 100644 --- a/NitroxServer/GameLogic/PlayerManager.cs +++ b/NitroxServer/GameLogic/PlayerManager.cs @@ -14,7 +14,6 @@ using NitroxModel.MultiplayerSession; using NitroxModel.Packets; using NitroxServer.Communication; -using NitroxServer.GameLogic.Entities; using NitroxServer.Serialization; using NitroxServer.Serialization.World; From 17e9c09db955e171c50d96a67f4bea9eef5a5c46 Mon Sep 17 00:00:00 2001 From: TwinBuilderOne Date: Mon, 1 Jan 2024 19:03:35 -0600 Subject: [PATCH 10/40] It almost worked (it works now) --- NitroxServer/GameLogic/PlayerManager.cs | 24 ++++++++++++------------ 1 file changed, 12 insertions(+), 12 deletions(-) diff --git a/NitroxServer/GameLogic/PlayerManager.cs b/NitroxServer/GameLogic/PlayerManager.cs index 4f13e657af..a5be561166 100644 --- a/NitroxServer/GameLogic/PlayerManager.cs +++ b/NitroxServer/GameLogic/PlayerManager.cs @@ -15,6 +15,7 @@ using NitroxModel.Packets; using NitroxModel.Server; using NitroxServer.Communication; +using NitroxServer.GameLogic.Bases; using NitroxServer.Serialization; using NitroxServer.Serialization.World; @@ -134,19 +135,18 @@ private async Task JoinQueueLoop() { const int REFRESH_DELAY = 10; - string GetLogName(NitroxConnection connection) => assetsByConnection.TryGetValue(connection, out ConnectionAssets assets) ? assets.Player.Name : $"at {connection.Endpoint}"; - while (true) { try { - while (!JoinQueue.Any()) + while (JoinQueue.Count == 0) { await Task.Delay(REFRESH_DELAY); } (NitroxConnection connection, string reservationKey) = JoinQueue.Dequeue(); + Log.Info($"Starting sync for player {reservations[reservationKey].PlayerName}"); SendInitialSync(connection, reservationKey); CancellationTokenSource source = new(serverConfig.InitialSyncTimeout); @@ -183,7 +183,7 @@ await Task.Run(() => if (task.IsCanceled || !task.Result) { - Log.Info($"Inital sync timed out for player {GetLogName(connection)}"); + Log.Info($"Inital sync timed out for player {reservations[reservationKey].PlayerName}"); SyncFinishedCallback = null; if (connection.State == NitroxConnectionState.Connected) @@ -194,7 +194,7 @@ await Task.Run(() => } else { - Log.Info($"Player {GetLogName(connection)} joined successfully. Remaining requests: {JoinQueue.Count}"); + Log.Info($"Player {assetsByConnection[connection].Player.Name} joined successfully. Remaining requests: {JoinQueue.Count}"); } }); } @@ -207,6 +207,7 @@ await Task.Run(() => public void AddToJoinQueue(NitroxConnection connection, string reservationKey) { + Log.Info($"Added player {reservations[reservationKey].PlayerName} to queue"); JoinQueue.Enqueue((connection, reservationKey)); } @@ -222,10 +223,10 @@ void SetupPlayerEntity(Player player) { NitroxTransform transform = new(player.Position, player.Rotation, NitroxVector3.One); - PlayerWorldEntity playerEntity = new PlayerWorldEntity(transform, 0, null, false, null, true, player.GameObjectId, NitroxTechType.None, null, null, new List()); + PlayerWorldEntity playerEntity = new PlayerWorldEntity(transform, 0, null, false, player.GameObjectId, NitroxTechType.None, null, null, new List()); world.EntityRegistry.AddEntity(playerEntity); world.WorldEntityManager.TrackEntityInTheWorld(playerEntity); - SendPacketToOtherPlayers(new CellEntities(playerEntity), player); + SendPacketToOtherPlayers(new SpawnEntities(playerEntity), player); } void RespawnExistingEntity(Player player) @@ -234,7 +235,7 @@ void RespawnExistingEntity(Player player) if (playerEntity.HasValue) { - SendPacketToOtherPlayers(new CellEntities(playerEntity.Value, true), player); + SendPacketToOtherPlayers(new SpawnEntities(playerEntity.Value, true), player); } else { @@ -247,7 +248,7 @@ void RespawnExistingEntity(Player player) if (newlyCreatedEscapePod.HasValue) { - CellEntities spawnNewEscapePod = new(newlyCreatedEscapePod.Value); + SpawnEntities spawnNewEscapePod = new(newlyCreatedEscapePod.Value); SendPacketToOtherPlayers(spawnNewEscapePod, player); } @@ -278,8 +279,6 @@ void RespawnExistingEntity(Player player) wasBrandNewPlayer, assignedEscapePodId, equippedItems, - world.BaseManager.GetBasePiecesForNewlyConnectedPlayer(), - world.InventoryManager.GetAllStorageSlotItems(), player.UsedItems, player.QuickSlotsBindingIds, world.GameData.PDAState.GetInitialPDAData(), @@ -294,7 +293,8 @@ void RespawnExistingEntity(Player player) world.GameMode, player.Permissions, new(new(player.PingInstancePreferences), player.PinnedRecipePreferences.ToList()), - world.StoryManager.GetTimeData() + world.StoryManager.GetTimeData(), + BuildingManager.GetEntitiesOperations(world.WorldEntityManager.GetGlobalRootEntities(true)) ); player.SendPacket(initialPlayerSync); From 5c9ffef3c06f21205c346c9dee96c22e2b2d2fff Mon Sep 17 00:00:00 2001 From: TwinBuilderOne Date: Tue, 6 Feb 2024 21:15:20 -0600 Subject: [PATCH 11/40] Show modal when timed out --- .../Processors/PlayerSyncTimeoutProcessor.cs | 25 +++++++++++++------ 1 file changed, 17 insertions(+), 8 deletions(-) diff --git a/NitroxClient/Communication/Packets/Processors/PlayerSyncTimeoutProcessor.cs b/NitroxClient/Communication/Packets/Processors/PlayerSyncTimeoutProcessor.cs index 8b08d13f16..b17f860119 100644 --- a/NitroxClient/Communication/Packets/Processors/PlayerSyncTimeoutProcessor.cs +++ b/NitroxClient/Communication/Packets/Processors/PlayerSyncTimeoutProcessor.cs @@ -1,22 +1,31 @@ -using System.Collections; +using NitroxClient.Communication.Abstract; using NitroxClient.Communication.Packets.Processors.Abstract; using NitroxClient.MonoBehaviours; +using NitroxClient.MonoBehaviours.Gui.InGame; using NitroxModel.Packets; -using UnityEngine; namespace NitroxClient.Communication.Packets.Processors; public class PlayerSyncTimeoutProcessor : ClientPacketProcessor { - public override void Process(PlayerSyncTimeout packet) + private readonly IMultiplayerSession session; + + public PlayerSyncTimeoutProcessor(IMultiplayerSession session) { - Multiplayer.Main.StartCoroutine(TimeoutRoutine()); + this.session = session; } - private IEnumerator TimeoutRoutine() + public override void Process(PlayerSyncTimeout packet) { - Log.InGame("Error: Initial sync timeout, closing game"); - yield return new WaitForSecondsRealtime(5); - IngameMenu.main.QuitGame(false); + // This will finish the loading screen + Multiplayer.Main.InsideJoinQueue = false; + Multiplayer.Main.InitialSyncCompleted = true; + + session.Disconnect(); + + // TODO: make this translatable + string message = "Initial sync timed out"; + + Modal.Get().Show(message); } } From 932edb93b1512010b906e8de532147feb64e0445 Mon Sep 17 00:00:00 2001 From: TwinBuilderOne Date: Wed, 7 Feb 2024 20:09:12 -0600 Subject: [PATCH 12/40] Tweaks and fixes --- .../Processors/InitialPlayerSyncProcessor.cs | 12 +++++--- .../Processors/PlayerSyncTimeoutProcessor.cs | 10 ++----- NitroxClient/MonoBehaviours/Multiplayer.cs | 30 +++++++++++++------ NitroxServer/GameLogic/PlayerManager.cs | 2 +- 4 files changed, 32 insertions(+), 22 deletions(-) diff --git a/NitroxClient/Communication/Packets/Processors/InitialPlayerSyncProcessor.cs b/NitroxClient/Communication/Packets/Processors/InitialPlayerSyncProcessor.cs index 45dbbca945..0726800e03 100644 --- a/NitroxClient/Communication/Packets/Processors/InitialPlayerSyncProcessor.cs +++ b/NitroxClient/Communication/Packets/Processors/InitialPlayerSyncProcessor.cs @@ -1,11 +1,11 @@ -using System; -using System.Collections; -using System.Collections.Generic; using NitroxClient.Communication.Abstract; using NitroxClient.Communication.Packets.Processors.Abstract; using NitroxClient.GameLogic.InitialSync.Abstract; using NitroxClient.MonoBehaviours; using NitroxModel.Packets; +using System; +using System.Collections; +using System.Collections.Generic; namespace NitroxClient.Communication.Packets.Processors { @@ -31,8 +31,12 @@ public InitialPlayerSyncProcessor(IPacketSender packetSender, IEnumerable().Show(message); } } diff --git a/NitroxClient/MonoBehaviours/Multiplayer.cs b/NitroxClient/MonoBehaviours/Multiplayer.cs index 8476ebf638..c30258f1a4 100644 --- a/NitroxClient/MonoBehaviours/Multiplayer.cs +++ b/NitroxClient/MonoBehaviours/Multiplayer.cs @@ -1,6 +1,3 @@ -using System; -using System.Collections; -using System.Collections.Generic; using NitroxClient.Communication; using NitroxClient.Communication.Abstract; using NitroxClient.Communication.MultiplayerSession; @@ -15,6 +12,9 @@ using NitroxModel.Core; using NitroxModel.Packets; using NitroxModel.Packets.Processors.Abstract; +using System; +using System.Collections; +using System.Collections.Generic; using UnityEngine; using UnityEngine.SceneManagement; using UWE; @@ -32,7 +32,7 @@ public class Multiplayer : MonoBehaviour private GameLogic.Terrain terrain; public bool InitialSyncCompleted { get; set; } - public bool InsideJoinQueue { get; set; } + public bool TimedOut { get; set; } /// /// True if multiplayer is loaded and client is connected to a server. @@ -86,6 +86,7 @@ public static void SubnauticaLoadingCompleted() if (Active) { Main.InitialSyncCompleted = false; + Main.TimedOut = false; Main.StartCoroutine(LoadAsync()); } else @@ -109,13 +110,24 @@ public static IEnumerator LoadAsync() yield return Main.StartCoroutine(Main.StartSession()); WaitScreen.Remove(item); - WaitScreen.ManualWaitItem joinQueueItem = WaitScreen.Add(Language.main.Get("Nitrox_Waiting")); - Main.InsideJoinQueue = true; - yield return new WaitUntil(() => !Main.InsideJoinQueue); - WaitScreen.Remove(joinQueueItem); - + Log.InGame(Language.main.Get("Nitrox_Waiting")); yield return new WaitUntil(() => Main.InitialSyncCompleted); + if (Main.TimedOut) + { + int timer = 5; + + while (timer > 0) + { + Log.InGame($"Initial sync timed out. Quitting game in {timer} second{(timer > 1 ? "s" : "")}…"); + yield return new WaitForSecondsRealtime(1); + timer--; + } + + IngameMenu.main.QuitGame(false); + yield break; + } + SetLoadingComplete(); } diff --git a/NitroxServer/GameLogic/PlayerManager.cs b/NitroxServer/GameLogic/PlayerManager.cs index 65f9196ba4..b41cdb3c77 100644 --- a/NitroxServer/GameLogic/PlayerManager.cs +++ b/NitroxServer/GameLogic/PlayerManager.cs @@ -183,7 +183,7 @@ await Task.Run(() => if (task.IsCanceled || !task.Result) { - Log.Info($"Inital sync timed out for player {reservations[reservationKey].PlayerName}"); + Log.Info($"Initial sync timed out for player {assetsByConnection[connection].Player.Name}"); SyncFinishedCallback = null; if (connection.State == NitroxConnectionState.Connected) From 708c8cf555e8bfb7b858df62b5a7bfbc98565e74 Mon Sep 17 00:00:00 2001 From: TwinBuilderOne Date: Fri, 9 Feb 2024 06:07:54 -0600 Subject: [PATCH 13/40] Instantly continue the queue if a client disconnects while syncing --- NitroxServer/GameLogic/PlayerManager.cs | 31 ++++++++++++++++++------- 1 file changed, 22 insertions(+), 9 deletions(-) diff --git a/NitroxServer/GameLogic/PlayerManager.cs b/NitroxServer/GameLogic/PlayerManager.cs index b41cdb3c77..acc4fc2533 100644 --- a/NitroxServer/GameLogic/PlayerManager.cs +++ b/NitroxServer/GameLogic/PlayerManager.cs @@ -145,12 +145,14 @@ private async Task JoinQueueLoop() } (NitroxConnection connection, string reservationKey) = JoinQueue.Dequeue(); + string name = reservations[reservationKey].PlayerName; - Log.Info($"Starting sync for player {reservations[reservationKey].PlayerName}"); + Log.Info($"Starting sync for player {name}"); SendInitialSync(connection, reservationKey); CancellationTokenSource source = new(serverConfig.InitialSyncTimeout); bool syncFinished = false; + bool disconnected = false; SyncFinishedCallback = () => { @@ -165,6 +167,11 @@ await Task.Run(() => { return true; } + else if (connection.State == NitroxConnectionState.Disconnected) + { + disconnected = true; + return false; + } else { Task.Delay(REFRESH_DELAY).Wait(); @@ -181,9 +188,15 @@ await Task.Run(() => throw task.Exception; } + if (disconnected) + { + Log.Info($"Player {name} disconnected while syncing"); + return; + } + if (task.IsCanceled || !task.Result) { - Log.Info($"Initial sync timed out for player {assetsByConnection[connection].Player.Name}"); + Log.Info($"Initial sync timed out for player {name}"); SyncFinishedCallback = null; if (connection.State == NitroxConnectionState.Connected) @@ -194,7 +207,7 @@ await Task.Run(() => } else { - Log.Info($"Player {assetsByConnection[connection].Player.Name} joined successfully. Remaining requests: {JoinQueue.Count}"); + Log.Info($"Player {name} joined successfully. Remaining requests: {JoinQueue.Count}"); } }); } @@ -303,12 +316,6 @@ void RespawnExistingEntity(Player player) player.SendPacket(initialPlayerSync); } - public void NonPlayerDisconnected(NitroxConnection connection) - { - // Remove any requests sent by the connection from the join queue - JoinQueue = new(JoinQueue.Where(tuple => !Equals(tuple.Item1, connection))); - } - public Player PlayerConnected(NitroxConnection connection, string reservationKey, out bool wasBrandNewPlayer) { PlayerContext playerContext = reservations[reservationKey]; @@ -387,6 +394,12 @@ public void PlayerDisconnected(NitroxConnection connection) } } + public void NonPlayerDisconnected(NitroxConnection connection) + { + // They may have been queued, so just erase their entry + JoinQueue = new(JoinQueue.Where(tuple => !Equals(tuple.Item1, connection))); + } + public bool TryGetPlayerByName(string playerName, out Player foundPlayer) { foundPlayer = null; From 2eb654a31e3f1c28705b5eed6f99f7c2890ef4df Mon Sep 17 00:00:00 2001 From: TwinBuilderOne Date: Fri, 9 Feb 2024 16:48:44 -0600 Subject: [PATCH 14/40] Send info about the queue to the client --- .../Processors/JoinQueueInfoProcessor.cs | 25 +++++++++++++++++++ NitroxModel/Packets/JoinQueueInfo.cs | 18 +++++++++++++ NitroxServer/GameLogic/PlayerManager.cs | 19 ++++++++++++++ 3 files changed, 62 insertions(+) create mode 100644 NitroxClient/Communication/Packets/Processors/JoinQueueInfoProcessor.cs create mode 100644 NitroxModel/Packets/JoinQueueInfo.cs diff --git a/NitroxClient/Communication/Packets/Processors/JoinQueueInfoProcessor.cs b/NitroxClient/Communication/Packets/Processors/JoinQueueInfoProcessor.cs new file mode 100644 index 0000000000..5afdb05899 --- /dev/null +++ b/NitroxClient/Communication/Packets/Processors/JoinQueueInfoProcessor.cs @@ -0,0 +1,25 @@ +using NitroxClient.Communication.Packets.Processors.Abstract; +using NitroxModel.Packets; +using System; + +namespace NitroxClient.Communication.Packets.Processors; + +public class JoinQueueInfoProcessor : ClientPacketProcessor +{ + public override void Process(JoinQueueInfo packet) + { + Log.InGame($"You are at position #{packet.Position} in the queue."); + + if (packet.ShowMaximumWait) + { + Log.InGame($"You could have to wait up to {MillisToMinutes(packet.Position * packet.Timeout)} minutes, but that is very unlikely."); + Log.InGame($"The maximum wait time per person is {MillisToMinutes(packet.Timeout)} minutes."); + } + } + + private static string MillisToMinutes(int milliseconds) + { + double minutes = milliseconds / 60000.0; + return Math.Round(minutes, 1).ToString(); + } +} diff --git a/NitroxModel/Packets/JoinQueueInfo.cs b/NitroxModel/Packets/JoinQueueInfo.cs new file mode 100644 index 0000000000..a7e93bdc91 --- /dev/null +++ b/NitroxModel/Packets/JoinQueueInfo.cs @@ -0,0 +1,18 @@ +using System; + +namespace NitroxModel.Packets; + +[Serializable] +public class JoinQueueInfo : Packet +{ + public int Position { get; } + public int Timeout { get; } + public bool ShowMaximumWait { get; } + + public JoinQueueInfo(int position, int timeout, bool showMaximumWait) + { + Position = position; + Timeout = timeout; + ShowMaximumWait = showMaximumWait; + } +} diff --git a/NitroxServer/GameLogic/PlayerManager.cs b/NitroxServer/GameLogic/PlayerManager.cs index acc4fc2533..7b9a84d2d8 100644 --- a/NitroxServer/GameLogic/PlayerManager.cs +++ b/NitroxServer/GameLogic/PlayerManager.cs @@ -32,6 +32,7 @@ public class PlayerManager private readonly ThreadSafeSet reservedPlayerNames = new("Player"); // "Player" is often used to identify the local player and should not be used by any user private ThreadSafeQueue<(NitroxConnection, string)> JoinQueue { get; set; } = new(); + private bool queueIdle = false; public Action SyncFinishedCallback { get; private set; } private readonly ServerConfig serverConfig; @@ -141,12 +142,25 @@ private async Task JoinQueueLoop() { while (JoinQueue.Count == 0) { + queueIdle = true; await Task.Delay(REFRESH_DELAY); } + queueIdle = false; + (NitroxConnection connection, string reservationKey) = JoinQueue.Dequeue(); string name = reservations[reservationKey].PlayerName; + // Do this after dequeueing because everyone's position shifts forward + { + (NitroxConnection, string)[] array = [.. JoinQueue]; + for (int i = 0; i < array.Length; i++) + { + (NitroxConnection c, _) = array[i]; + c.SendPacket(new JoinQueueInfo(i + 1, serverConfig.InitialSyncTimeout, false)); + } + } + Log.Info($"Starting sync for player {name}"); SendInitialSync(connection, reservationKey); @@ -220,6 +234,11 @@ await Task.Run(() => public void AddToJoinQueue(NitroxConnection connection, string reservationKey) { + if (!queueIdle) + { + connection.SendPacket(new JoinQueueInfo(JoinQueue.Count + 1, serverConfig.InitialSyncTimeout, true)); + } + Log.Info($"Added player {reservations[reservationKey].PlayerName} to queue"); JoinQueue.Enqueue((connection, reservationKey)); } From 1b5920f0810a998b1c7e9877f60279179b00bbb9 Mon Sep 17 00:00:00 2001 From: TwinBuilderOne Date: Mon, 12 Feb 2024 06:16:14 -0600 Subject: [PATCH 15/40] Remove join queue "lobby" --- .../Packets/Processors/JoinQueueInfoProcessor.cs | 1 - NitroxClient/MonoBehaviours/Multiplayer.cs | 6 ++++-- 2 files changed, 4 insertions(+), 3 deletions(-) diff --git a/NitroxClient/Communication/Packets/Processors/JoinQueueInfoProcessor.cs b/NitroxClient/Communication/Packets/Processors/JoinQueueInfoProcessor.cs index 5afdb05899..96cb0a7b08 100644 --- a/NitroxClient/Communication/Packets/Processors/JoinQueueInfoProcessor.cs +++ b/NitroxClient/Communication/Packets/Processors/JoinQueueInfoProcessor.cs @@ -12,7 +12,6 @@ public override void Process(JoinQueueInfo packet) if (packet.ShowMaximumWait) { - Log.InGame($"You could have to wait up to {MillisToMinutes(packet.Position * packet.Timeout)} minutes, but that is very unlikely."); Log.InGame($"The maximum wait time per person is {MillisToMinutes(packet.Timeout)} minutes."); } } diff --git a/NitroxClient/MonoBehaviours/Multiplayer.cs b/NitroxClient/MonoBehaviours/Multiplayer.cs index c30258f1a4..e054c58450 100644 --- a/NitroxClient/MonoBehaviours/Multiplayer.cs +++ b/NitroxClient/MonoBehaviours/Multiplayer.cs @@ -106,12 +106,14 @@ public static IEnumerator LoadAsync() WaitScreen.Remove(worldSettleItem); - WaitScreen.ManualWaitItem item = WaitScreen.Add(Language.main.Get("Nitrox_JoiningSession")); + WaitScreen.ManualWaitItem joiningItem = WaitScreen.Add(Language.main.Get("Nitrox_JoiningSession")); yield return Main.StartCoroutine(Main.StartSession()); - WaitScreen.Remove(item); + WaitScreen.Remove(joiningItem); + WaitScreen.ManualWaitItem waitingItem = WaitScreen.Add(Language.main.Get("Nitrox_Waiting")); Log.InGame(Language.main.Get("Nitrox_Waiting")); yield return new WaitUntil(() => Main.InitialSyncCompleted); + WaitScreen.Remove(waitingItem); if (Main.TimedOut) { From 708f753334b01cc7f0e757e739ed9feffb4b7a80 Mon Sep 17 00:00:00 2001 From: TwinBuilderOne Date: Mon, 12 Feb 2024 16:51:09 -0600 Subject: [PATCH 16/40] Some cleanups --- .../MultiplayerSessionManager.cs | 1 - .../Processors/InitialPlayerSyncProcessor.cs | 10 ++++------ NitroxClient/MonoBehaviours/Multiplayer.cs | 6 +++--- NitroxServer/GameLogic/PlayerManager.cs | 20 +++++++++---------- 4 files changed, 17 insertions(+), 20 deletions(-) diff --git a/NitroxClient/Communication/MultiplayerSession/MultiplayerSessionManager.cs b/NitroxClient/Communication/MultiplayerSession/MultiplayerSessionManager.cs index 649e48badc..6fd52aaec0 100644 --- a/NitroxClient/Communication/MultiplayerSession/MultiplayerSessionManager.cs +++ b/NitroxClient/Communication/MultiplayerSession/MultiplayerSessionManager.cs @@ -1,5 +1,4 @@ using System; -using System.Collections.Generic; using System.Threading.Tasks; using NitroxClient.Communication.Abstract; using NitroxClient.Communication.MultiplayerSession.ConnectionState; diff --git a/NitroxClient/Communication/Packets/Processors/InitialPlayerSyncProcessor.cs b/NitroxClient/Communication/Packets/Processors/InitialPlayerSyncProcessor.cs index 0726800e03..2377b152de 100644 --- a/NitroxClient/Communication/Packets/Processors/InitialPlayerSyncProcessor.cs +++ b/NitroxClient/Communication/Packets/Processors/InitialPlayerSyncProcessor.cs @@ -1,11 +1,11 @@ +using System; +using System.Collections; +using System.Collections.Generic; using NitroxClient.Communication.Abstract; using NitroxClient.Communication.Packets.Processors.Abstract; using NitroxClient.GameLogic.InitialSync.Abstract; using NitroxClient.MonoBehaviours; using NitroxModel.Packets; -using System; -using System.Collections; -using System.Collections.Generic; namespace NitroxClient.Communication.Packets.Processors { @@ -13,7 +13,7 @@ public class InitialPlayerSyncProcessor : ClientPacketProcessor processors; - private readonly HashSet alreadyRan = new(); + private readonly HashSet alreadyRan = []; private InitialPlayerSync packet; private WaitScreen.ManualWaitItem loadingMultiplayerWaitItem; @@ -33,8 +33,6 @@ public override void Process(InitialPlayerSync packet) this.packet = packet; loadingMultiplayerWaitItem = WaitScreen.Add(Language.main.Get("Nitrox_SyncingWorld")); - - // Clarifies the client is out of the queue since WaitScreen doesn't actually do anything Log.InGame(Language.main.Get("Nitrox_SyncingWorld")); cumulativeProcessorsRan = 0; diff --git a/NitroxClient/MonoBehaviours/Multiplayer.cs b/NitroxClient/MonoBehaviours/Multiplayer.cs index e054c58450..7bf0439082 100644 --- a/NitroxClient/MonoBehaviours/Multiplayer.cs +++ b/NitroxClient/MonoBehaviours/Multiplayer.cs @@ -1,3 +1,6 @@ +using System; +using System.Collections; +using System.Collections.Generic; using NitroxClient.Communication; using NitroxClient.Communication.Abstract; using NitroxClient.Communication.MultiplayerSession; @@ -12,9 +15,6 @@ using NitroxModel.Core; using NitroxModel.Packets; using NitroxModel.Packets.Processors.Abstract; -using System; -using System.Collections; -using System.Collections.Generic; using UnityEngine; using UnityEngine.SceneManagement; using UWE; diff --git a/NitroxServer/GameLogic/PlayerManager.cs b/NitroxServer/GameLogic/PlayerManager.cs index 7b9a84d2d8..329a80e4a9 100644 --- a/NitroxServer/GameLogic/PlayerManager.cs +++ b/NitroxServer/GameLogic/PlayerManager.cs @@ -1,3 +1,10 @@ +using System; +using System.Collections.Generic; +using System.Linq; +using System.Net; +using System.Text.RegularExpressions; +using System.Threading; +using System.Threading.Tasks; using NitroxModel.DataStructures; using NitroxModel.DataStructures.GameLogic; using NitroxModel.DataStructures.GameLogic.Entities; @@ -11,13 +18,6 @@ using NitroxServer.GameLogic.Bases; using NitroxServer.Serialization; using NitroxServer.Serialization.World; -using System; -using System.Collections.Generic; -using System.Linq; -using System.Net; -using System.Text.RegularExpressions; -using System.Threading; -using System.Threading.Tasks; namespace NitroxServer.GameLogic { @@ -27,8 +27,8 @@ public class PlayerManager private readonly World world; private readonly ThreadSafeDictionary allPlayersByName; - private readonly ThreadSafeDictionary assetsByConnection = new(); - private readonly ThreadSafeDictionary reservations = new(); + private readonly ThreadSafeDictionary assetsByConnection = []; + private readonly ThreadSafeDictionary reservations = []; private readonly ThreadSafeSet reservedPlayerNames = new("Player"); // "Player" is often used to identify the local player and should not be used by any user private ThreadSafeQueue<(NitroxConnection, string)> JoinQueue { get; set; } = new(); @@ -255,7 +255,7 @@ void SetupPlayerEntity(Player player) { NitroxTransform transform = new(player.Position, player.Rotation, NitroxVector3.One); - PlayerWorldEntity playerEntity = new PlayerWorldEntity(transform, 0, null, false, player.GameObjectId, NitroxTechType.None, null, null, new List()); + PlayerWorldEntity playerEntity = new PlayerWorldEntity(transform, 0, null, false, player.GameObjectId, NitroxTechType.None, null, null, []); world.EntityRegistry.AddEntity(playerEntity); world.WorldEntityManager.TrackEntityInTheWorld(playerEntity); SendPacketToOtherPlayers(new SpawnEntities(playerEntity), player); From 34b2c18b4988f9be72863a55d3f6939cceadec51 Mon Sep 17 00:00:00 2001 From: TwinBuilderOne Date: Mon, 12 Feb 2024 16:59:27 -0600 Subject: [PATCH 17/40] Revert a file that wasn't actually modified --- .../MultiplayerSession/ConnectionState/SessionReserved.cs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/NitroxClient/Communication/MultiplayerSession/ConnectionState/SessionReserved.cs b/NitroxClient/Communication/MultiplayerSession/ConnectionState/SessionReserved.cs index 32e880904b..e37024124a 100644 --- a/NitroxClient/Communication/MultiplayerSession/ConnectionState/SessionReserved.cs +++ b/NitroxClient/Communication/MultiplayerSession/ConnectionState/SessionReserved.cs @@ -1,4 +1,4 @@ -using System; +using System; using NitroxClient.Communication.Abstract; using NitroxModel.Packets; From efba962b391b8e3c2cab5978720fa3e1fbfb47c3 Mon Sep 17 00:00:00 2001 From: SpaceMonkeyy86 Date: Tue, 7 Jan 2025 14:43:16 -0600 Subject: [PATCH 18/40] Apply Jannify's patch --- .../LiteNetLib/LiteNetLibServer.cs | 2 +- NitroxServer/Communication/NitroxServer.cs | 31 +- .../Communication/Packets/PacketHandler.cs | 8 +- ...layerJoiningMultiplayerSessionProcessor.cs | 8 +- .../Processors/PlayerSyncFinishedProcessor.cs | 6 +- NitroxServer/GameLogic/JoiningManager.cs | 252 ++++++++ NitroxServer/GameLogic/PlayerManager.cs | 573 ++++++------------ .../Serialization/World/WorldPersistence.cs | 2 +- 8 files changed, 454 insertions(+), 428 deletions(-) create mode 100644 NitroxServer/GameLogic/JoiningManager.cs diff --git a/NitroxServer/Communication/LiteNetLib/LiteNetLibServer.cs b/NitroxServer/Communication/LiteNetLib/LiteNetLibServer.cs index 57e8d7fae6..4cf0db3252 100644 --- a/NitroxServer/Communication/LiteNetLib/LiteNetLibServer.cs +++ b/NitroxServer/Communication/LiteNetLib/LiteNetLibServer.cs @@ -18,7 +18,7 @@ public class LiteNetLibServer : NitroxServer private readonly EventBasedNetListener listener; private readonly NetManager server; - public LiteNetLibServer(PacketHandler packetHandler, PlayerManager playerManager, EntitySimulation entitySimulation, ServerConfig serverConfig) : base(packetHandler, playerManager, entitySimulation, serverConfig) + public LiteNetLibServer(PacketHandler packetHandler, PlayerManager playerManager, JoiningManager joiningManager, EntitySimulation entitySimulation, ServerConfig serverConfig) : base(packetHandler, playerManager, joiningManager, entitySimulation, serverConfig) { listener = new EventBasedNetListener(); server = new NetManager(listener); diff --git a/NitroxServer/Communication/NitroxServer.cs b/NitroxServer/Communication/NitroxServer.cs index e653fa9381..d628a22eb8 100644 --- a/NitroxServer/Communication/NitroxServer.cs +++ b/NitroxServer/Communication/NitroxServer.cs @@ -1,4 +1,4 @@ -using System; +using System; using System.Collections.Generic; using NitroxModel.DataStructures; using NitroxModel.Packets; @@ -25,11 +25,13 @@ static NitroxServer() protected readonly EntitySimulation entitySimulation; protected readonly Dictionary connectionsByRemoteIdentifier = new(); protected readonly PlayerManager playerManager; + protected readonly JoiningManager joiningManager; - public NitroxServer(PacketHandler packetHandler, PlayerManager playerManager, EntitySimulation entitySimulation, ServerConfig serverConfig) + public NitroxServer(PacketHandler packetHandler, PlayerManager playerManager, JoiningManager joiningManager, EntitySimulation entitySimulation, ServerConfig serverConfig) { this.packetHandler = packetHandler; this.playerManager = playerManager; + this.joiningManager = joiningManager; this.entitySimulation = entitySimulation; portNumber = serverConfig.ServerPort; @@ -46,24 +48,23 @@ protected void ClientDisconnected(INitroxConnection connection) { Player player = playerManager.GetPlayer(connection); - if (player != null) + if (player == null) { - playerManager.PlayerDisconnected(connection); + joiningManager.JoiningPlayerDisconnected(connection); + return; + } - Disconnect disconnect = new(player.Id); - playerManager.SendPacketToAllPlayers(disconnect); + playerManager.PlayerDisconnected(connection); - List ownershipChanges = entitySimulation.CalculateSimulationChangesFromPlayerDisconnect(player); + Disconnect disconnect = new(player.Id); + playerManager.SendPacketToAllPlayers(disconnect); - if (ownershipChanges.Count > 0) - { - SimulationOwnershipChange ownershipChange = new(ownershipChanges); - playerManager.SendPacketToAllPlayers(ownershipChange); - } - } - else + List ownershipChanges = entitySimulation.CalculateSimulationChangesFromPlayerDisconnect(player); + + if (ownershipChanges.Count > 0) { - playerManager.NonPlayerDisconnected(connection); + SimulationOwnershipChange ownershipChange = new(ownershipChanges); + playerManager.SendPacketToAllPlayers(ownershipChange); } } diff --git a/NitroxServer/Communication/Packets/PacketHandler.cs b/NitroxServer/Communication/Packets/PacketHandler.cs index 47e7529427..449cd893a0 100644 --- a/NitroxServer/Communication/Packets/PacketHandler.cs +++ b/NitroxServer/Communication/Packets/PacketHandler.cs @@ -1,6 +1,6 @@ using System; -using System.Linq; using System.Collections.Generic; +using System.Linq; using NitroxModel.Core; using NitroxModel.Packets; using NitroxModel.Packets.Processors.Abstract; @@ -13,13 +13,15 @@ namespace NitroxServer.Communication.Packets public class PacketHandler { private readonly PlayerManager playerManager; + private readonly JoiningManager joiningManager; private readonly DefaultServerPacketProcessor defaultServerPacketProcessor; private readonly Dictionary packetProcessorAuthCache = new(); private readonly Dictionary packetProcessorUnauthCache = new(); - public PacketHandler(PlayerManager playerManager, DefaultServerPacketProcessor packetProcessor) + public PacketHandler(PlayerManager playerManager, JoiningManager joiningManager, DefaultServerPacketProcessor packetProcessor) { this.playerManager = playerManager; + this.joiningManager = joiningManager; defaultServerPacketProcessor = packetProcessor; } @@ -30,7 +32,7 @@ public void Process(Packet packet, INitroxConnection connection) { ProcessAuthenticated(packet, player); } - else if (!playerManager.GetQueuedPlayers().Contains(connection)) + else if (!joiningManager.GetQueuedPlayers().Contains(connection)) { ProcessUnauthenticated(packet, connection); } diff --git a/NitroxServer/Communication/Packets/Processors/PlayerJoiningMultiplayerSessionProcessor.cs b/NitroxServer/Communication/Packets/Processors/PlayerJoiningMultiplayerSessionProcessor.cs index cbeefe27fa..3f6a687d65 100644 --- a/NitroxServer/Communication/Packets/Processors/PlayerJoiningMultiplayerSessionProcessor.cs +++ b/NitroxServer/Communication/Packets/Processors/PlayerJoiningMultiplayerSessionProcessor.cs @@ -6,16 +6,16 @@ namespace NitroxServer.Communication.Packets.Processors { public class PlayerJoiningMultiplayerSessionProcessor : UnauthenticatedPacketProcessor { - private readonly PlayerManager playerManager; + private readonly JoiningManager joiningManager; - public PlayerJoiningMultiplayerSessionProcessor(PlayerManager playerManager) + public PlayerJoiningMultiplayerSessionProcessor(JoiningManager joiningManager) { - this.playerManager = playerManager; + this.joiningManager = joiningManager; } public override void Process(PlayerJoiningMultiplayerSession packet, INitroxConnection connection) { - playerManager.AddToJoinQueue(connection, packet.ReservationKey); + joiningManager.AddToJoinQueue(connection, packet.ReservationKey); } } } diff --git a/NitroxServer/Communication/Packets/Processors/PlayerSyncFinishedProcessor.cs b/NitroxServer/Communication/Packets/Processors/PlayerSyncFinishedProcessor.cs index 45a0982408..c250c05e30 100644 --- a/NitroxServer/Communication/Packets/Processors/PlayerSyncFinishedProcessor.cs +++ b/NitroxServer/Communication/Packets/Processors/PlayerSyncFinishedProcessor.cs @@ -7,10 +7,12 @@ namespace NitroxServer.Communication.Packets.Processors public class PlayerSyncFinishedProcessor : AuthenticatedPacketProcessor { private readonly PlayerManager playerManager; + private readonly JoiningManager joiningManager; - public PlayerSyncFinishedProcessor(PlayerManager playerManager) + public PlayerSyncFinishedProcessor(PlayerManager playerManager, JoiningManager joiningManager) { this.playerManager = playerManager; + this.joiningManager = joiningManager; } public override void Process(PlayerSyncFinished packet, Player player) @@ -21,7 +23,7 @@ public override void Process(PlayerSyncFinished packet, Player player) Server.Instance.ResumeServer(); } - playerManager.SyncFinishedCallback?.Invoke(); + joiningManager.SyncFinishedCallback?.Invoke(); } } } diff --git a/NitroxServer/GameLogic/JoiningManager.cs b/NitroxServer/GameLogic/JoiningManager.cs new file mode 100644 index 0000000000..15362a9665 --- /dev/null +++ b/NitroxServer/GameLogic/JoiningManager.cs @@ -0,0 +1,252 @@ +using System; +using System.Collections.Generic; +using System.Linq; +using System.Threading; +using System.Threading.Tasks; +using NitroxModel; +using NitroxModel.DataStructures; +using NitroxModel.DataStructures.GameLogic; +using NitroxModel.DataStructures.GameLogic.Entities; +using NitroxModel.DataStructures.Unity; +using NitroxModel.DataStructures.Util; +using NitroxModel.Helper; +using NitroxModel.MultiplayerSession; +using NitroxModel.Packets; +using NitroxServer.Communication; +using NitroxServer.GameLogic.Bases; +using NitroxServer.GameLogic.Entities; +using NitroxServer.Serialization; +using NitroxServer.Serialization.World; + +namespace NitroxServer.GameLogic; + +public sealed class JoiningManager +{ + private readonly PlayerManager playerManager; + private readonly ServerConfig serverConfig; + private readonly World world; + private readonly EntityRegistry entityRegistry; + private readonly StoryManager storyManager; + private readonly ScheduleKeeper scheduleKeeper; + + private ThreadSafeQueue<(INitroxConnection, string)> JoinQueue { get; set; } = new(); + private bool queueIdle; + public Action SyncFinishedCallback { get; private set; } + + public JoiningManager(PlayerManager playerManager, ServerConfig serverConfig, World world, EntityRegistry entityRegistry, StoryManager storyManager, ScheduleKeeper scheduleKeeper) + { + this.playerManager = playerManager; + this.serverConfig = serverConfig; + this.world = world; + this.entityRegistry = entityRegistry; + this.storyManager = storyManager; + this.scheduleKeeper = scheduleKeeper; + + Task.Run(JoinQueueLoop).ContinueWithHandleError(); + } + + private async Task JoinQueueLoop() + { + const int REFRESH_DELAY = 10; + + while (true) + { + try + { + while (JoinQueue.Count == 0) + { + queueIdle = true; + await Task.Delay(REFRESH_DELAY); + } + + queueIdle = false; + + (INitroxConnection connection, string reservationKey) = JoinQueue.Dequeue(); + string name = playerManager.GetPlayerContext(reservationKey).PlayerName; + + // Do this after dequeueing because everyone's position shifts forward + { + (INitroxConnection, string)[] array = [.. JoinQueue]; + for (int i = 0; i < array.Length; i++) + { + (INitroxConnection c, _) = array[i]; + c.SendPacket(new JoinQueueInfo(i + 1, serverConfig.InitialSyncTimeout, false)); + } + } + + Log.Info($"Starting sync for player {name}"); + SendInitialSync(connection, reservationKey); + + CancellationTokenSource source = new(serverConfig.InitialSyncTimeout); + bool syncFinished = false; + bool disconnected = false; + + SyncFinishedCallback = () => { syncFinished = true; }; + + await Task.Run(() => + { + while (!source.IsCancellationRequested) + { + if (syncFinished) + { + return true; + } + + if (connection.State == NitroxConnectionState.Disconnected) + { + disconnected = true; + return false; + } + + Task.Delay(REFRESH_DELAY).Wait(); + } + + return false; + }) + // We use ContinueWith to avoid having to try/catch a TaskCanceledException + .ContinueWith(task => + { + if (task.IsFaulted) + { + throw task.Exception; + } + + if (disconnected) + { + Log.Info($"Player {name} disconnected while syncing"); + return; + } + + if (task.IsCanceled || !task.Result) + { + Log.Info($"Initial sync timed out for player {name}"); + SyncFinishedCallback = null; + + if (connection.State == NitroxConnectionState.Connected) + { + connection.SendPacket(new PlayerSyncTimeout()); + } + playerManager.PlayerDisconnected(connection); + } + else + { + Log.Info($"Player {name} joined successfully. Remaining requests: {JoinQueue.Count}"); + BroadcastPlayerJoined(playerManager.GetPlayer(connection)); + } + }); + } + catch (Exception e) + { + Log.Error($"Unexpected error during player connection: {e}"); + } + } + } + + public void AddToJoinQueue(INitroxConnection connection, string reservationKey) + { + if (!queueIdle) + { + connection.SendPacket(new JoinQueueInfo(JoinQueue.Count + 1, serverConfig.InitialSyncTimeout, true)); + } + + Log.Info($"Added player {playerManager.GetPlayerContext(reservationKey).PlayerName} to queue"); + JoinQueue.Enqueue((connection, reservationKey)); + } + + public IEnumerable GetQueuedPlayers() + { + return JoinQueue.Select(tuple => tuple.Item1); + } + + + private void SendInitialSync(INitroxConnection connection, string reservationKey) + { + IEnumerable GetOtherPlayers(Player player) + { + return playerManager.GetConnectedPlayers().Where(p => p != player) + .Select(p => p.PlayerContext); + } + + PlayerWorldEntity SetupPlayerEntity(Player player) + { + NitroxTransform transform = new(player.Position, player.Rotation, NitroxVector3.One); + + PlayerWorldEntity playerEntity = new(transform, 0, null, false, player.GameObjectId, NitroxTechType.None, null, null, new List()); + entityRegistry.AddOrUpdate(playerEntity); + world.WorldEntityManager.TrackEntityInTheWorld(playerEntity); + return playerEntity; + } + + PlayerWorldEntity RespawnExistingEntity(Player player) + { + if (entityRegistry.TryGetEntityById(player.PlayerContext.PlayerNitroxId, out PlayerWorldEntity playerWorldEntity)) + { + return playerWorldEntity; + } + Log.Error($"Unable to find player entity for {player.Name}. Re-creating one"); + return SetupPlayerEntity(player); + } + + Player player = playerManager.PlayerConnected(connection, reservationKey, out bool wasBrandNewPlayer); + NitroxId assignedEscapePodId = world.EscapePodManager.AssignPlayerToEscapePod(player.Id, out Optional newlyCreatedEscapePod); + + if (newlyCreatedEscapePod.HasValue) + { + SpawnEntities spawnNewEscapePod = new(newlyCreatedEscapePod.Value); + playerManager.SendPacketToOtherPlayers(spawnNewEscapePod, player); + } + + // Make players on localhost admin by default. + if (connection.Endpoint.Address.IsLocalhost()) + { + Log.Info($"Granted admin to '{player.Name}' because they're playing on the host machine"); + player.Permissions = Perms.ADMIN; + } + + List simulations = world.EntitySimulation.AssignGlobalRootEntitiesAndGetData(player); + + player.Entity = wasBrandNewPlayer ? SetupPlayerEntity(player) : RespawnExistingEntity(player); + + List globalRootEntities = world.WorldEntityManager.GetGlobalRootEntities(true); + bool isFirstPlayer = playerManager.GetConnectedPlayers().Count == 1; + + InitialPlayerSync initialPlayerSync = new(player.GameObjectId, + wasBrandNewPlayer, + assignedEscapePodId, + player.EquippedItems, + player.UsedItems, + player.QuickSlotsBindingIds, + world.GameData.PDAState.GetInitialPDAData(), + world.GameData.StoryGoals.GetInitialStoryGoalData(scheduleKeeper, player), + player.Position, + player.Rotation, + player.SubRootId, + player.Stats, + GetOtherPlayers(player), + globalRootEntities, + simulations, + player.GameMode, + player.Permissions, + wasBrandNewPlayer ? IntroCinematicMode.LOADING : IntroCinematicMode.COMPLETED, + new(new(player.PingInstancePreferences), player.PinnedRecipePreferences.ToList()), + storyManager.GetTimeData(), + isFirstPlayer, + BuildingManager.GetEntitiesOperations(globalRootEntities), + serverConfig.KeepInventoryOnDeath + ); + + player.SendPacket(initialPlayerSync); + } + + public void JoiningPlayerDisconnected(INitroxConnection connection) + { + // They may have been queued, so just erase their entry + JoinQueue = new ThreadSafeQueue<(INitroxConnection, string)>(JoinQueue.Where(tuple => !Equals(tuple.Item1, connection))); + } + + public void BroadcastPlayerJoined(Player player) + { + PlayerJoinedMultiplayerSession playerJoinedPacket = new(player.PlayerContext, player.SubRootId, player.Entity); + playerManager.SendPacketToOtherPlayers(playerJoinedPacket, player); + } +} diff --git a/NitroxServer/GameLogic/PlayerManager.cs b/NitroxServer/GameLogic/PlayerManager.cs index c1ef826f9b..08d231ef39 100644 --- a/NitroxServer/GameLogic/PlayerManager.cs +++ b/NitroxServer/GameLogic/PlayerManager.cs @@ -2,12 +2,8 @@ using System.Collections.Generic; using System.Linq; using System.Text.RegularExpressions; -using System.Threading; -using System.Threading.Tasks; -using NitroxModel.Core; using NitroxModel.DataStructures; using NitroxModel.DataStructures.GameLogic; -using NitroxModel.DataStructures.GameLogic.Entities; using NitroxModel.DataStructures.Unity; using NitroxModel.DataStructures.Util; using NitroxModel.Helper; @@ -15,473 +11,246 @@ using NitroxModel.Packets; using NitroxModel.Server; using NitroxServer.Communication; -using NitroxServer.GameLogic.Bases; -using NitroxServer.GameLogic.Entities; using NitroxServer.Serialization; -using NitroxServer.Serialization.World; -namespace NitroxServer.GameLogic +namespace NitroxServer.GameLogic; + +// TODO: These methods are a little chunky. Need to look at refactoring just to clean them up and get them around 30 lines a piece. +public class PlayerManager { - // TODO: These methods are a little chunky. Need to look at refactoring just to clean them up and get them around 30 lines a piece. - public class PlayerManager + private readonly ServerConfig serverConfig; + + private readonly ThreadSafeDictionary allPlayersByName; + private readonly ThreadSafeDictionary connectedPlayersById = []; + private readonly ThreadSafeDictionary assetsByConnection = new(); + private readonly ThreadSafeDictionary reservations = new(); + private readonly ThreadSafeSet reservedPlayerNames = new("Player"); // "Player" is often used to identify the local player and should not be used by any user + + private ThreadSafeQueue<(INitroxConnection, string)> JoinQueue { get; set; } = new(); + private bool queueIdle = false; + public Action SyncFinishedCallback { get; private set; } + + private ushort currentPlayerId; + + public PlayerManager(List players, ServerConfig serverConfig) { - private readonly World world; - private readonly ServerConfig serverConfig; + allPlayersByName = new ThreadSafeDictionary(players.ToDictionary(x => x.Name), false); + currentPlayerId = players.Count == 0 ? (ushort)0 : players.Max(x => x.Id); - private readonly ThreadSafeDictionary allPlayersByName; - private readonly ThreadSafeDictionary connectedPlayersById = []; - private readonly ThreadSafeDictionary assetsByConnection = new(); - private readonly ThreadSafeDictionary reservations = new(); - private readonly ThreadSafeSet reservedPlayerNames = new("Player"); // "Player" is often used to identify the local player and should not be used by any user + this.serverConfig = serverConfig; + } - private ThreadSafeQueue<(INitroxConnection, string)> JoinQueue { get; set; } = new(); - private bool queueIdle = false; - public Action SyncFinishedCallback { get; private set; } + public IEnumerable GetAllPlayers() => allPlayersByName.Values; - private ushort currentPlayerId; + public IEnumerable ConnectedPlayers() + { + return assetsByConnection.Values + .Where(assetPackage => assetPackage.Player != null) + .Select(assetPackage => assetPackage.Player); + } - public PlayerManager(List players, World world, ServerConfig serverConfig) - { - allPlayersByName = new ThreadSafeDictionary(players.ToDictionary(x => x.Name), false); - currentPlayerId = players.Count == 0 ? (ushort)0 : players.Max(x => x.Id); + public List GetConnectedPlayers() => ConnectedPlayers().ToList(); - this.world = world; - this.serverConfig = serverConfig; + public List GetConnectedPlayersExcept(Player excludePlayer) + { + return ConnectedPlayers().Where(player => player != excludePlayer).ToList(); + } - _ = JoinQueueLoop(); - } + public Player GetPlayer(INitroxConnection connection) + { + return assetsByConnection.TryGetValue(connection, out ConnectionAssets assetPackage) ? assetPackage.Player : null; + } - public List GetConnectedPlayers() - { - return ConnectedPlayers().ToList(); - } + public PlayerContext GetPlayerContext(string reservationKey) + { + return reservations.TryGetValue(reservationKey, out PlayerContext playerContext) ? playerContext : null; + } - public List GetConnectedPlayersExcept(Player excludePlayer) + public MultiplayerSessionReservation ReservePlayerContext( + INitroxConnection connection, + PlayerSettings playerSettings, + AuthenticationContext authenticationContext, + string correlationId) + { + if (reservedPlayerNames.Count >= serverConfig.MaxConnections) { - return ConnectedPlayers().Where(player => player != excludePlayer).ToList(); + MultiplayerSessionReservationState rejectedState = MultiplayerSessionReservationState.REJECTED | MultiplayerSessionReservationState.SERVER_PLAYER_CAPACITY_REACHED; + return new MultiplayerSessionReservation(correlationId, rejectedState); } - public IEnumerable GetAllPlayers() + if (!string.IsNullOrEmpty(serverConfig.ServerPassword) && (!authenticationContext.ServerPassword.HasValue || authenticationContext.ServerPassword.Value != serverConfig.ServerPassword)) { - return allPlayersByName.Values; + MultiplayerSessionReservationState rejectedState = MultiplayerSessionReservationState.REJECTED | MultiplayerSessionReservationState.AUTHENTICATION_FAILED; + return new MultiplayerSessionReservation(correlationId, rejectedState); } - public IEnumerable GetQueuedPlayers() + //https://regex101.com/r/eTWiEs/2/ + if (!Regex.IsMatch(authenticationContext.Username, @"^[a-zA-Z0-9._-]{3,25}$")) { - return JoinQueue.Select(tuple => tuple.Item1); + MultiplayerSessionReservationState rejectedState = MultiplayerSessionReservationState.REJECTED | MultiplayerSessionReservationState.INCORRECT_USERNAME; + return new MultiplayerSessionReservation(correlationId, rejectedState); } - public MultiplayerSessionReservation ReservePlayerContext( - INitroxConnection connection, - PlayerSettings playerSettings, - AuthenticationContext authenticationContext, - string correlationId) - { - if (reservedPlayerNames.Count >= serverConfig.MaxConnections) - { - MultiplayerSessionReservationState rejectedState = MultiplayerSessionReservationState.REJECTED | MultiplayerSessionReservationState.SERVER_PLAYER_CAPACITY_REACHED; - return new MultiplayerSessionReservation(correlationId, rejectedState); - } - - if (!string.IsNullOrEmpty(serverConfig.ServerPassword) && (!authenticationContext.ServerPassword.HasValue || authenticationContext.ServerPassword.Value != serverConfig.ServerPassword)) - { - MultiplayerSessionReservationState rejectedState = MultiplayerSessionReservationState.REJECTED | MultiplayerSessionReservationState.AUTHENTICATION_FAILED; - return new MultiplayerSessionReservation(correlationId, rejectedState); - } - - //https://regex101.com/r/eTWiEs/2/ - if (!Regex.IsMatch(authenticationContext.Username, @"^[a-zA-Z0-9._-]{3,25}$")) - { - MultiplayerSessionReservationState rejectedState = MultiplayerSessionReservationState.REJECTED | MultiplayerSessionReservationState.INCORRECT_USERNAME; - return new MultiplayerSessionReservation(correlationId, rejectedState); - } - - string playerName = authenticationContext.Username; - - allPlayersByName.TryGetValue(playerName, out Player player); - if (player?.IsPermaDeath == true && serverConfig.IsHardcore) - { - MultiplayerSessionReservationState rejectedState = MultiplayerSessionReservationState.REJECTED | MultiplayerSessionReservationState.HARDCORE_PLAYER_DEAD; - return new MultiplayerSessionReservation(correlationId, rejectedState); - } - - if (reservedPlayerNames.Contains(playerName)) - { - MultiplayerSessionReservationState rejectedState = MultiplayerSessionReservationState.REJECTED | MultiplayerSessionReservationState.UNIQUE_PLAYER_NAME_CONSTRAINT_VIOLATED; - return new MultiplayerSessionReservation(correlationId, rejectedState); - } - - assetsByConnection.TryGetValue(connection, out ConnectionAssets assetPackage); - if (assetPackage == null) - { - assetPackage = new ConnectionAssets(); - assetsByConnection.Add(connection, assetPackage); - reservedPlayerNames.Add(playerName); - } - - bool hasSeenPlayerBefore = player != null; - ushort playerId = hasSeenPlayerBefore ? player.Id : ++currentPlayerId; - NitroxId playerNitroxId = hasSeenPlayerBefore ? player.GameObjectId : new NitroxId(); - NitroxGameMode gameMode = hasSeenPlayerBefore ? player.GameMode : serverConfig.GameMode; - IntroCinematicMode introCinematicMode = hasSeenPlayerBefore ? IntroCinematicMode.COMPLETED : IntroCinematicMode.LOADING; - - // TODO: At some point, store the muted state of a player - PlayerContext playerContext = new(playerName, playerId, playerNitroxId, !hasSeenPlayerBefore, playerSettings, false, gameMode, null, introCinematicMode); - string reservationKey = Guid.NewGuid().ToString(); - - reservations.Add(reservationKey, playerContext); - assetPackage.ReservationKey = reservationKey; - - return new MultiplayerSessionReservation(correlationId, playerId, reservationKey); - } + string playerName = authenticationContext.Username; - private async Task JoinQueueLoop() + allPlayersByName.TryGetValue(playerName, out Player player); + if (player?.IsPermaDeath == true && serverConfig.IsHardcore) { - const int REFRESH_DELAY = 10; - - while (true) - { - try - { - while (JoinQueue.Count == 0) - { - queueIdle = true; - await Task.Delay(REFRESH_DELAY); - } - - queueIdle = false; - - (INitroxConnection connection, string reservationKey) = JoinQueue.Dequeue(); - string name = reservations[reservationKey].PlayerName; - - // Do this after dequeueing because everyone's position shifts forward - { - (INitroxConnection, string)[] array = [.. JoinQueue]; - for (int i = 0; i < array.Length; i++) - { - (INitroxConnection c, _) = array[i]; - c.SendPacket(new JoinQueueInfo(i + 1, serverConfig.InitialSyncTimeout, false)); - } - } - - Log.Info($"Starting sync for player {name}"); - SendInitialSync(connection, reservationKey); - - CancellationTokenSource source = new(serverConfig.InitialSyncTimeout); - bool syncFinished = false; - bool disconnected = false; - - SyncFinishedCallback = () => - { - syncFinished = true; - }; - - await Task.Run(() => - { - while (!source.IsCancellationRequested) - { - if (syncFinished) - { - return true; - } - else if (connection.State == NitroxConnectionState.Disconnected) - { - disconnected = true; - return false; - } - else - { - Task.Delay(REFRESH_DELAY).Wait(); - } - } - - return false; - - // We use ContinueWith to avoid having to try/catch a TaskCanceledException - }).ContinueWith(task => - { - if (task.IsFaulted) - { - throw task.Exception; - } - - if (disconnected) - { - Log.Info($"Player {name} disconnected while syncing"); - return; - } - - if (task.IsCanceled || !task.Result) - { - Log.Info($"Initial sync timed out for player {name}"); - SyncFinishedCallback = null; - - if (connection.State == NitroxConnectionState.Connected) - { - connection.SendPacket(new PlayerSyncTimeout()); - } - PlayerDisconnected(connection); - } - else - { - Log.Info($"Player {name} joined successfully. Remaining requests: {JoinQueue.Count}"); - BroadcastPlayerJoined(assetsByConnection[connection].Player); - } - }); - } - catch (Exception e) - { - Log.Error($"Unexpected error during player connection: {e}"); - } - } + MultiplayerSessionReservationState rejectedState = MultiplayerSessionReservationState.REJECTED | MultiplayerSessionReservationState.HARDCORE_PLAYER_DEAD; + return new MultiplayerSessionReservation(correlationId, rejectedState); } - public void AddToJoinQueue(INitroxConnection connection, string reservationKey) + if (reservedPlayerNames.Contains(playerName)) { - if (!queueIdle) - { - connection.SendPacket(new JoinQueueInfo(JoinQueue.Count + 1, serverConfig.InitialSyncTimeout, true)); - } - - Log.Info($"Added player {reservations[reservationKey].PlayerName} to queue"); - JoinQueue.Enqueue((connection, reservationKey)); + MultiplayerSessionReservationState rejectedState = MultiplayerSessionReservationState.REJECTED | MultiplayerSessionReservationState.UNIQUE_PLAYER_NAME_CONSTRAINT_VIOLATED; + return new MultiplayerSessionReservation(correlationId, rejectedState); } - private void SendInitialSync(INitroxConnection connection, string reservationKey) + assetsByConnection.TryGetValue(connection, out ConnectionAssets assetPackage); + if (assetPackage == null) { - // XXX: initialize in constructor once circular dependency is resolved - EntityRegistry entityRegistry = NitroxServiceLocator.LocateService(); - StoryManager storyManager = NitroxServiceLocator.LocateService(); - ScheduleKeeper scheduleKeeper = NitroxServiceLocator.LocateService(); + assetPackage = new ConnectionAssets(); + assetsByConnection.Add(connection, assetPackage); + reservedPlayerNames.Add(playerName); + } - IEnumerable GetOtherPlayers(Player player) - { - return GetConnectedPlayers().Where(p => p != player) - .Select(p => p.PlayerContext); - } + bool hasSeenPlayerBefore = player != null; + ushort playerId = hasSeenPlayerBefore ? player.Id : ++currentPlayerId; + NitroxId playerNitroxId = hasSeenPlayerBefore ? player.GameObjectId : new NitroxId(); + NitroxGameMode gameMode = hasSeenPlayerBefore ? player.GameMode : serverConfig.GameMode; + IntroCinematicMode introCinematicMode = hasSeenPlayerBefore ? IntroCinematicMode.COMPLETED : IntroCinematicMode.LOADING; - PlayerWorldEntity SetupPlayerEntity(Player player) - { - NitroxTransform transform = new(player.Position, player.Rotation, NitroxVector3.One); + // TODO: At some point, store the muted state of a player + PlayerContext playerContext = new(playerName, playerId, playerNitroxId, !hasSeenPlayerBefore, playerSettings, false, gameMode, null, introCinematicMode); + string reservationKey = Guid.NewGuid().ToString(); - PlayerWorldEntity playerEntity = new(transform, 0, null, false, player.GameObjectId, NitroxTechType.None, null, null, new List()); - entityRegistry.AddOrUpdate(playerEntity); - world.WorldEntityManager.TrackEntityInTheWorld(playerEntity); - return playerEntity; - } - - PlayerWorldEntity RespawnExistingEntity(Player player) - { - if (entityRegistry.TryGetEntityById(player.PlayerContext.PlayerNitroxId, out PlayerWorldEntity playerWorldEntity)) - { - return playerWorldEntity; - } - Log.Error($"Unable to find player entity for {player.Name}. Re-creating one"); - return SetupPlayerEntity(player); - } + reservations.Add(reservationKey, playerContext); + assetPackage.ReservationKey = reservationKey; - Player player = PlayerConnected(connection, reservationKey, out bool wasBrandNewPlayer); - NitroxId assignedEscapePodId = world.EscapePodManager.AssignPlayerToEscapePod(player.Id, out Optional newlyCreatedEscapePod); + return new MultiplayerSessionReservation(correlationId, playerId, reservationKey); + } - if (newlyCreatedEscapePod.HasValue) - { - SpawnEntities spawnNewEscapePod = new(newlyCreatedEscapePod.Value); - SendPacketToOtherPlayers(spawnNewEscapePod, player); - } + public Player PlayerConnected(INitroxConnection connection, string reservationKey, out bool wasBrandNewPlayer) + { + PlayerContext playerContext = reservations[reservationKey]; + Validate.NotNull(playerContext); + ConnectionAssets assetPackage = assetsByConnection[connection]; + Validate.NotNull(assetPackage); - // Make players on localhost admin by default. - if (connection.Endpoint.Address.IsLocalhost()) - { - Log.Info($"Granted admin to '{player.Name}' because they're playing on the host machine"); - player.Permissions = Perms.ADMIN; - } + wasBrandNewPlayer = playerContext.WasBrandNewPlayer; - List simulations = world.EntitySimulation.AssignGlobalRootEntitiesAndGetData(player); - - player.Entity = wasBrandNewPlayer ? SetupPlayerEntity(player) : RespawnExistingEntity(player); - - List globalRootEntities = world.WorldEntityManager.GetGlobalRootEntities(true); - bool isFirstPlayer = GetConnectedPlayers().Count == 1; - - InitialPlayerSync initialPlayerSync = new(player.GameObjectId, - wasBrandNewPlayer, - assignedEscapePodId, - player.EquippedItems, - player.UsedItems, - player.QuickSlotsBindingIds, - world.GameData.PDAState.GetInitialPDAData(), - world.GameData.StoryGoals.GetInitialStoryGoalData(scheduleKeeper, player), - player.Position, - player.Rotation, - player.SubRootId, - player.Stats, - GetOtherPlayers(player), - globalRootEntities, - simulations, - player.GameMode, - player.Permissions, - wasBrandNewPlayer ? IntroCinematicMode.LOADING : IntroCinematicMode.COMPLETED, - new(new(player.PingInstancePreferences), player.PinnedRecipePreferences.ToList()), - storyManager.GetTimeData(), - isFirstPlayer, - BuildingManager.GetEntitiesOperations(globalRootEntities), - serverConfig.KeepInventoryOnDeath + if (!allPlayersByName.TryGetValue(playerContext.PlayerName, out Player player)) + { + player = new Player(playerContext.PlayerId, + playerContext.PlayerName, + false, + playerContext, + connection, + NitroxVector3.Zero, + NitroxQuaternion.Identity, + playerContext.PlayerNitroxId, + Optional.Empty, + serverConfig.DefaultPlayerPerm, + serverConfig.DefaultPlayerStats, + serverConfig.GameMode, + [], + [], + new Dictionary(), + new Dictionary(), + new Dictionary(), + [] ); - - player.SendPacket(initialPlayerSync); + allPlayersByName[playerContext.PlayerName] = player; } - public Player PlayerConnected(INitroxConnection connection, string reservationKey, out bool wasBrandNewPlayer) - { - PlayerContext playerContext = reservations[reservationKey]; - Validate.NotNull(playerContext); - ConnectionAssets assetPackage = assetsByConnection[connection]; - Validate.NotNull(assetPackage); - - wasBrandNewPlayer = playerContext.WasBrandNewPlayer; - - if (!allPlayersByName.TryGetValue(playerContext.PlayerName, out Player player)) - { - player = new Player(playerContext.PlayerId, - playerContext.PlayerName, - false, - playerContext, - connection, - NitroxVector3.Zero, - NitroxQuaternion.Identity, - playerContext.PlayerNitroxId, - Optional.Empty, - serverConfig.DefaultPlayerPerm, - serverConfig.DefaultPlayerStats, - serverConfig.GameMode, - new List(), - Array.Empty>(), - new Dictionary(), - new Dictionary(), - new Dictionary(), - new List() - ); - allPlayersByName[playerContext.PlayerName] = player; - } - - connectedPlayersById.Add(playerContext.PlayerId, player); + connectedPlayersById.Add(playerContext.PlayerId, player); - // TODO: make a ConnectedPlayer wrapper so this is not stateful - player.PlayerContext = playerContext; - player.Connection = connection; + // TODO: make a ConnectedPlayer wrapper so this is not stateful + player.PlayerContext = playerContext; + player.Connection = connection; - // reconnecting players need to have their cell visibility refreshed - player.ClearVisibleCells(); + // reconnecting players need to have their cell visibility refreshed + player.ClearVisibleCells(); - assetPackage.Player = player; - assetPackage.ReservationKey = null; - reservations.Remove(reservationKey); + assetPackage.Player = player; + assetPackage.ReservationKey = null; + reservations.Remove(reservationKey); - return player; - } + return player; + } - public void PlayerDisconnected(INitroxConnection connection) + public void PlayerDisconnected(INitroxConnection connection) + { + if (!assetsByConnection.TryGetValue(connection, out ConnectionAssets assetPackage)) { - assetsByConnection.TryGetValue(connection, out ConnectionAssets assetPackage); - if (assetPackage == null) - { - return; - } - - if (assetPackage.ReservationKey != null) - { - PlayerContext playerContext = reservations[assetPackage.ReservationKey]; - reservedPlayerNames.Remove(playerContext.PlayerName); - reservations.Remove(assetPackage.ReservationKey); - } - - if (assetPackage.Player != null) - { - Player player = assetPackage.Player; - reservedPlayerNames.Remove(player.Name); - connectedPlayersById.Remove(player.Id); - } - - assetsByConnection.Remove(connection); - - if (!ConnectedPlayers().Any()) - { - Server.Instance.PauseServer(); - Server.Instance.Save(); - } + return; } - public void NonPlayerDisconnected(INitroxConnection connection) + if (assetPackage.ReservationKey != null) { - // They may have been queued, so just erase their entry - JoinQueue = new(JoinQueue.Where(tuple => !Equals(tuple.Item1, connection))); + PlayerContext playerContext = reservations[assetPackage.ReservationKey]; + reservedPlayerNames.Remove(playerContext.PlayerName); + reservations.Remove(assetPackage.ReservationKey); } - public bool TryGetPlayerByName(string playerName, out Player foundPlayer) + if (assetPackage.Player != null) { - foundPlayer = null; - foreach (Player player in ConnectedPlayers()) - { - if (player.Name == playerName) - { - foundPlayer = player; - return true; - } - } - - return false; + Player player = assetPackage.Player; + reservedPlayerNames.Remove(player.Name); + connectedPlayersById.Remove(player.Id); } - public bool TryGetPlayerById(ushort playerId, out Player player) + assetsByConnection.Remove(connection); + + if (!ConnectedPlayers().Any()) { - return connectedPlayersById.TryGetValue(playerId, out player); + Server.Instance.PauseServer(); + Server.Instance.Save(); } + } - public Player GetPlayer(INitroxConnection connection) + public bool TryGetPlayerByName(string playerName, out Player foundPlayer) + { + foundPlayer = null; + foreach (Player player in ConnectedPlayers()) { - if (!assetsByConnection.TryGetValue(connection, out ConnectionAssets assetPackage)) + if (player.Name == playerName) { - return null; + foundPlayer = player; + return true; } - return assetPackage.Player; } - public Optional GetPlayer(string playerName) - { - allPlayersByName.TryGetValue(playerName, out Player player); - return Optional.OfNullable(player); - } + return false; + } + + public bool TryGetPlayerById(ushort playerId, out Player player) + { + return connectedPlayersById.TryGetValue(playerId, out player); + } - public void SendPacketToAllPlayers(Packet packet) + public void SendPacketToAllPlayers(Packet packet) + { + foreach (Player player in ConnectedPlayers()) { - foreach (Player player in ConnectedPlayers()) - { - player.SendPacket(packet); - } + player.SendPacket(packet); } + } - public void SendPacketToOtherPlayers(Packet packet, Player sendingPlayer) + public void SendPacketToOtherPlayers(Packet packet, Player sendingPlayer) + { + foreach (Player player in ConnectedPlayers()) { - foreach (Player player in ConnectedPlayers()) + if (player != sendingPlayer) { - if (player != sendingPlayer) - { - player.SendPacket(packet); - } + player.SendPacket(packet); } } + } - public IEnumerable ConnectedPlayers() - { - return assetsByConnection.Values - .Where(assetPackage => assetPackage.Player != null) - .Select(assetPackage => assetPackage.Player); - } - - public void BroadcastPlayerJoined(Player player) - { - PlayerJoinedMultiplayerSession playerJoinedPacket = new(player.PlayerContext, player.SubRootId, player.Entity); - SendPacketToOtherPlayers(playerJoinedPacket, player); - } + public void BroadcastPlayerJoined(Player player) + { + PlayerJoinedMultiplayerSession playerJoinedPacket = new(player.PlayerContext, player.SubRootId, player.Entity); + SendPacketToOtherPlayers(playerJoinedPacket, player); } } diff --git a/NitroxServer/Serialization/World/WorldPersistence.cs b/NitroxServer/Serialization/World/WorldPersistence.cs index 27254c991e..e8952d3bb4 100644 --- a/NitroxServer/Serialization/World/WorldPersistence.cs +++ b/NitroxServer/Serialization/World/WorldPersistence.cs @@ -210,6 +210,7 @@ public World CreateWorld(PersistedWorldData pWorldData, NitroxGameMode gameMode) EscapePodManager = new EscapePodManager(entityRegistry, randomStart, seed), + PlayerManager = new PlayerManager(pWorldData.PlayerData.GetPlayers(), config), EntityRegistry = entityRegistry, GameData = pWorldData.WorldData.GameData, @@ -217,7 +218,6 @@ public World CreateWorld(PersistedWorldData pWorldData, NitroxGameMode gameMode) Seed = seed }; - world.PlayerManager = new PlayerManager(pWorldData.PlayerData.GetPlayers(), world, config); world.TimeKeeper = new(world.PlayerManager, pWorldData.WorldData.GameData.StoryTiming.ElapsedSeconds, pWorldData.WorldData.GameData.StoryTiming.RealTimeElapsed); world.StoryManager = new(world.PlayerManager, pWorldData.WorldData.GameData.PDAState, pWorldData.WorldData.GameData.StoryGoals, world.TimeKeeper, seed, pWorldData.WorldData.GameData.StoryTiming.AuroraCountdownTime, pWorldData.WorldData.GameData.StoryTiming.AuroraWarningTime, pWorldData.WorldData.GameData.StoryTiming.AuroraRealExplosionTime); world.ScheduleKeeper = new ScheduleKeeper(pWorldData.WorldData.GameData.PDAState, pWorldData.WorldData.GameData.StoryGoals, world.TimeKeeper, world.PlayerManager); From ef9d8d56427feaa32db80eef904295a8bba0b744 Mon Sep 17 00:00:00 2001 From: SpaceMonkeyy86 Date: Tue, 7 Jan 2025 16:01:33 -0600 Subject: [PATCH 19/40] Small tweaks --- .../Packets/Processors/JoinQueueInfoProcessor.cs | 2 +- NitroxModel/Packets/PlayerSyncTimeout.cs | 5 ++++- 2 files changed, 5 insertions(+), 2 deletions(-) diff --git a/NitroxClient/Communication/Packets/Processors/JoinQueueInfoProcessor.cs b/NitroxClient/Communication/Packets/Processors/JoinQueueInfoProcessor.cs index 96cb0a7b08..2824b70599 100644 --- a/NitroxClient/Communication/Packets/Processors/JoinQueueInfoProcessor.cs +++ b/NitroxClient/Communication/Packets/Processors/JoinQueueInfoProcessor.cs @@ -1,6 +1,6 @@ +using System; using NitroxClient.Communication.Packets.Processors.Abstract; using NitroxModel.Packets; -using System; namespace NitroxClient.Communication.Packets.Processors; diff --git a/NitroxModel/Packets/PlayerSyncTimeout.cs b/NitroxModel/Packets/PlayerSyncTimeout.cs index e8e552899f..36a9be6de0 100644 --- a/NitroxModel/Packets/PlayerSyncTimeout.cs +++ b/NitroxModel/Packets/PlayerSyncTimeout.cs @@ -1,3 +1,6 @@ -namespace NitroxModel.Packets; +using System; +namespace NitroxModel.Packets; + +[Serializable] public class PlayerSyncTimeout : Packet { } From a1cf75b16ed1edd89ef28e6cc9d2fc9c142caffc Mon Sep 17 00:00:00 2001 From: SpaceMonkeyy86 Date: Tue, 7 Jan 2025 16:36:37 -0600 Subject: [PATCH 20/40] Fix DI for JoiningManager --- NitroxServer/GameLogic/JoiningManager.cs | 17 +++++------------ NitroxServer/GameLogic/PlayerManager.cs | 4 ---- NitroxServer/Serialization/World/World.cs | 1 + .../Serialization/World/WorldPersistence.cs | 1 + NitroxServer/ServerAutoFacRegistrar.cs | 1 + 5 files changed, 8 insertions(+), 16 deletions(-) diff --git a/NitroxServer/GameLogic/JoiningManager.cs b/NitroxServer/GameLogic/JoiningManager.cs index 15362a9665..87e882903b 100644 --- a/NitroxServer/GameLogic/JoiningManager.cs +++ b/NitroxServer/GameLogic/JoiningManager.cs @@ -14,7 +14,6 @@ using NitroxModel.Packets; using NitroxServer.Communication; using NitroxServer.GameLogic.Bases; -using NitroxServer.GameLogic.Entities; using NitroxServer.Serialization; using NitroxServer.Serialization.World; @@ -25,22 +24,16 @@ public sealed class JoiningManager private readonly PlayerManager playerManager; private readonly ServerConfig serverConfig; private readonly World world; - private readonly EntityRegistry entityRegistry; - private readonly StoryManager storyManager; - private readonly ScheduleKeeper scheduleKeeper; private ThreadSafeQueue<(INitroxConnection, string)> JoinQueue { get; set; } = new(); private bool queueIdle; public Action SyncFinishedCallback { get; private set; } - public JoiningManager(PlayerManager playerManager, ServerConfig serverConfig, World world, EntityRegistry entityRegistry, StoryManager storyManager, ScheduleKeeper scheduleKeeper) + public JoiningManager(PlayerManager playerManager, ServerConfig serverConfig, World world) { this.playerManager = playerManager; this.serverConfig = serverConfig; this.world = world; - this.entityRegistry = entityRegistry; - this.storyManager = storyManager; - this.scheduleKeeper = scheduleKeeper; Task.Run(JoinQueueLoop).ContinueWithHandleError(); } @@ -172,14 +165,14 @@ PlayerWorldEntity SetupPlayerEntity(Player player) NitroxTransform transform = new(player.Position, player.Rotation, NitroxVector3.One); PlayerWorldEntity playerEntity = new(transform, 0, null, false, player.GameObjectId, NitroxTechType.None, null, null, new List()); - entityRegistry.AddOrUpdate(playerEntity); + world.EntityRegistry.AddOrUpdate(playerEntity); world.WorldEntityManager.TrackEntityInTheWorld(playerEntity); return playerEntity; } PlayerWorldEntity RespawnExistingEntity(Player player) { - if (entityRegistry.TryGetEntityById(player.PlayerContext.PlayerNitroxId, out PlayerWorldEntity playerWorldEntity)) + if (world.EntityRegistry.TryGetEntityById(player.PlayerContext.PlayerNitroxId, out PlayerWorldEntity playerWorldEntity)) { return playerWorldEntity; } @@ -217,7 +210,7 @@ PlayerWorldEntity RespawnExistingEntity(Player player) player.UsedItems, player.QuickSlotsBindingIds, world.GameData.PDAState.GetInitialPDAData(), - world.GameData.StoryGoals.GetInitialStoryGoalData(scheduleKeeper, player), + world.GameData.StoryGoals.GetInitialStoryGoalData(world.ScheduleKeeper, player), player.Position, player.Rotation, player.SubRootId, @@ -229,7 +222,7 @@ PlayerWorldEntity RespawnExistingEntity(Player player) player.Permissions, wasBrandNewPlayer ? IntroCinematicMode.LOADING : IntroCinematicMode.COMPLETED, new(new(player.PingInstancePreferences), player.PinnedRecipePreferences.ToList()), - storyManager.GetTimeData(), + world.StoryManager.GetTimeData(), isFirstPlayer, BuildingManager.GetEntitiesOperations(globalRootEntities), serverConfig.KeepInventoryOnDeath diff --git a/NitroxServer/GameLogic/PlayerManager.cs b/NitroxServer/GameLogic/PlayerManager.cs index 08d231ef39..f1e7de7443 100644 --- a/NitroxServer/GameLogic/PlayerManager.cs +++ b/NitroxServer/GameLogic/PlayerManager.cs @@ -26,10 +26,6 @@ public class PlayerManager private readonly ThreadSafeDictionary reservations = new(); private readonly ThreadSafeSet reservedPlayerNames = new("Player"); // "Player" is often used to identify the local player and should not be used by any user - private ThreadSafeQueue<(INitroxConnection, string)> JoinQueue { get; set; } = new(); - private bool queueIdle = false; - public Action SyncFinishedCallback { get; private set; } - private ushort currentPlayerId; public PlayerManager(List players, ServerConfig serverConfig) diff --git a/NitroxServer/Serialization/World/World.cs b/NitroxServer/Serialization/World/World.cs index a48f7629ff..a5ecde5a1b 100644 --- a/NitroxServer/Serialization/World/World.cs +++ b/NitroxServer/Serialization/World/World.cs @@ -9,6 +9,7 @@ namespace NitroxServer.Serialization.World public class World { public PlayerManager PlayerManager { get; set; } + public JoiningManager JoiningManager { get; set; } public ScheduleKeeper ScheduleKeeper { get; set; } public TimeKeeper TimeKeeper { get; set; } public SimulationOwnershipData SimulationOwnershipData { get; set; } diff --git a/NitroxServer/Serialization/World/WorldPersistence.cs b/NitroxServer/Serialization/World/WorldPersistence.cs index e8952d3bb4..729af6ce12 100644 --- a/NitroxServer/Serialization/World/WorldPersistence.cs +++ b/NitroxServer/Serialization/World/WorldPersistence.cs @@ -218,6 +218,7 @@ public World CreateWorld(PersistedWorldData pWorldData, NitroxGameMode gameMode) Seed = seed }; + world.JoiningManager = new(world.PlayerManager, config, world); world.TimeKeeper = new(world.PlayerManager, pWorldData.WorldData.GameData.StoryTiming.ElapsedSeconds, pWorldData.WorldData.GameData.StoryTiming.RealTimeElapsed); world.StoryManager = new(world.PlayerManager, pWorldData.WorldData.GameData.PDAState, pWorldData.WorldData.GameData.StoryGoals, world.TimeKeeper, seed, pWorldData.WorldData.GameData.StoryTiming.AuroraCountdownTime, pWorldData.WorldData.GameData.StoryTiming.AuroraWarningTime, pWorldData.WorldData.GameData.StoryTiming.AuroraRealExplosionTime); world.ScheduleKeeper = new ScheduleKeeper(pWorldData.WorldData.GameData.PDAState, pWorldData.WorldData.GameData.StoryGoals, world.TimeKeeper, world.PlayerManager); diff --git a/NitroxServer/ServerAutoFacRegistrar.cs b/NitroxServer/ServerAutoFacRegistrar.cs index 9e632bdfd7..0366357213 100644 --- a/NitroxServer/ServerAutoFacRegistrar.cs +++ b/NitroxServer/ServerAutoFacRegistrar.cs @@ -45,6 +45,7 @@ private void RegisterWorld(ContainerBuilder containerBuilder) containerBuilder.Register(c => c.Resolve().BuildingManager).SingleInstance(); containerBuilder.Register(c => c.Resolve().TimeKeeper).SingleInstance(); containerBuilder.Register(c => c.Resolve().PlayerManager).SingleInstance(); + containerBuilder.Register(c => c.Resolve().JoiningManager).SingleInstance(); containerBuilder.Register(c => c.Resolve().StoryManager).SingleInstance(); containerBuilder.Register(c => c.Resolve().ScheduleKeeper).SingleInstance(); containerBuilder.Register(c => c.Resolve().SimulationOwnershipData).SingleInstance(); From 4de4fa731d07508108213a59bd64160357025dda Mon Sep 17 00:00:00 2001 From: SpaceMonkeyy86 Date: Tue, 7 Jan 2025 16:58:53 -0600 Subject: [PATCH 21/40] Handle case where client receives a timeout after finishing the sync --- .../MultiplayerSessionManager.cs | 2 +- .../Processors/PlayerSyncTimeoutProcessor.cs | 14 +------------- NitroxClient/MonoBehaviours/Multiplayer.cs | 17 ++++++++++------- 3 files changed, 12 insertions(+), 21 deletions(-) diff --git a/NitroxClient/Communication/MultiplayerSession/MultiplayerSessionManager.cs b/NitroxClient/Communication/MultiplayerSession/MultiplayerSessionManager.cs index 6fd52aaec0..6f4ec24100 100644 --- a/NitroxClient/Communication/MultiplayerSession/MultiplayerSessionManager.cs +++ b/NitroxClient/Communication/MultiplayerSession/MultiplayerSessionManager.cs @@ -108,7 +108,7 @@ public void Disconnect() public bool Send(T packet) where T : Packet { - if (!PacketSuppressor.IsSuppressed) + if (Client.IsConnected && !PacketSuppressor.IsSuppressed) { Client.Send(packet); return true; diff --git a/NitroxClient/Communication/Packets/Processors/PlayerSyncTimeoutProcessor.cs b/NitroxClient/Communication/Packets/Processors/PlayerSyncTimeoutProcessor.cs index 34d6ef207e..8e2395fd51 100644 --- a/NitroxClient/Communication/Packets/Processors/PlayerSyncTimeoutProcessor.cs +++ b/NitroxClient/Communication/Packets/Processors/PlayerSyncTimeoutProcessor.cs @@ -1,4 +1,3 @@ -using NitroxClient.Communication.Abstract; using NitroxClient.Communication.Packets.Processors.Abstract; using NitroxClient.MonoBehaviours; using NitroxModel.Packets; @@ -7,19 +6,8 @@ namespace NitroxClient.Communication.Packets.Processors; public class PlayerSyncTimeoutProcessor : ClientPacketProcessor { - private readonly IMultiplayerSession session; - - public PlayerSyncTimeoutProcessor(IMultiplayerSession session) - { - this.session = session; - } - public override void Process(PlayerSyncTimeout packet) { - // This will advance the coroutine in Multiplayer::LoadAsync() which quits to menu - Multiplayer.Main.InitialSyncCompleted = true; - Multiplayer.Main.TimedOut = true; - - session.Disconnect(); + Multiplayer.Main.TimeOut(); } } diff --git a/NitroxClient/MonoBehaviours/Multiplayer.cs b/NitroxClient/MonoBehaviours/Multiplayer.cs index a927c21022..d06aa64c30 100644 --- a/NitroxClient/MonoBehaviours/Multiplayer.cs +++ b/NitroxClient/MonoBehaviours/Multiplayer.cs @@ -34,7 +34,6 @@ public class Multiplayer : MonoBehaviour private GameLogic.Terrain terrain; public bool InitialSyncCompleted { get; set; } - public bool TimedOut { get; set; } /// /// True if multiplayer is loaded and client is connected to a server. @@ -94,7 +93,6 @@ public static void SubnauticaLoadingCompleted() if (Active) { Main.InitialSyncCompleted = false; - Main.TimedOut = false; Main.StartCoroutine(LoadAsync()); } else @@ -124,7 +122,16 @@ public static IEnumerator LoadAsync() yield return new WaitUntil(() => Main.InitialSyncCompleted); WaitScreen.Remove(waitingItem); - if (Main.TimedOut) + SetLoadingComplete(); + OnLoadingComplete?.Invoke(); + } + + public void TimeOut() + { + multiplayerSession.Disconnect(); + StartCoroutine(TimeOutRoutine()); + + IEnumerator TimeOutRoutine() { int timer = 5; @@ -136,11 +143,7 @@ public static IEnumerator LoadAsync() } IngameMenu.main.QuitGame(false); - yield break; } - - SetLoadingComplete(); - OnLoadingComplete?.Invoke(); } public void ProcessPackets() From 8043c13d346ae1cc485b1803318e60423dc23593 Mon Sep 17 00:00:00 2001 From: SpaceMonkeyy86 Date: Sun, 12 Jan 2025 17:11:44 -0600 Subject: [PATCH 22/40] Remove thread unsafety when a joining player disconnects --- NitroxModel/DataStructures/ThreadSafeQueue.cs | 18 +++++++++++++++++- NitroxServer/GameLogic/JoiningManager.cs | 2 +- 2 files changed, 18 insertions(+), 2 deletions(-) diff --git a/NitroxModel/DataStructures/ThreadSafeQueue.cs b/NitroxModel/DataStructures/ThreadSafeQueue.cs index c0c58b54da..567e460033 100644 --- a/NitroxModel/DataStructures/ThreadSafeQueue.cs +++ b/NitroxModel/DataStructures/ThreadSafeQueue.cs @@ -1,4 +1,4 @@ -using System; +using System; using System.Collections; using System.Collections.Generic; using System.Diagnostics; @@ -121,6 +121,22 @@ public IEnumerable Clone() } } + public void Filter(Func predicate) + { + lock (locker) + { + int count = queue.Count; + for (int i = 0; i < count; i++) + { + T item = queue.Dequeue(); + if (predicate(item)) + { + queue.Enqueue(item); + } + } + } + } + private Queue CreateCopy(IEnumerable data) { return new Queue(data); diff --git a/NitroxServer/GameLogic/JoiningManager.cs b/NitroxServer/GameLogic/JoiningManager.cs index 87e882903b..6a0ce51cfa 100644 --- a/NitroxServer/GameLogic/JoiningManager.cs +++ b/NitroxServer/GameLogic/JoiningManager.cs @@ -234,7 +234,7 @@ PlayerWorldEntity RespawnExistingEntity(Player player) public void JoiningPlayerDisconnected(INitroxConnection connection) { // They may have been queued, so just erase their entry - JoinQueue = new ThreadSafeQueue<(INitroxConnection, string)>(JoinQueue.Where(tuple => !Equals(tuple.Item1, connection))); + JoinQueue.Filter(tuple => !Equals(tuple.Item1, connection)); } public void BroadcastPlayerJoined(Player player) From 95fc884ef83c5041f76f1412d91a019d9db37140 Mon Sep 17 00:00:00 2001 From: SpaceMonkeyy86 Date: Sun, 12 Jan 2025 17:27:42 -0600 Subject: [PATCH 23/40] Some cleanup --- NitroxClient/MonoBehaviours/Multiplayer.cs | 6 ++--- NitroxServer/GameLogic/JoiningManager.cs | 26 ++++++++++------------ 2 files changed, 14 insertions(+), 18 deletions(-) diff --git a/NitroxClient/MonoBehaviours/Multiplayer.cs b/NitroxClient/MonoBehaviours/Multiplayer.cs index d06aa64c30..553d6356dc 100644 --- a/NitroxClient/MonoBehaviours/Multiplayer.cs +++ b/NitroxClient/MonoBehaviours/Multiplayer.cs @@ -133,13 +133,11 @@ public void TimeOut() IEnumerator TimeOutRoutine() { - int timer = 5; - - while (timer > 0) + // TODO: replace with modal + for (int timer = 5; timer > 0; timer--) { Log.InGame($"Initial sync timed out. Quitting game in {timer} second{(timer > 1 ? "s" : "")}…"); yield return new WaitForSecondsRealtime(1); - timer--; } IngameMenu.main.QuitGame(false); diff --git a/NitroxServer/GameLogic/JoiningManager.cs b/NitroxServer/GameLogic/JoiningManager.cs index 6a0ce51cfa..61d9c1c190 100644 --- a/NitroxServer/GameLogic/JoiningManager.cs +++ b/NitroxServer/GameLogic/JoiningManager.cs @@ -25,7 +25,7 @@ public sealed class JoiningManager private readonly ServerConfig serverConfig; private readonly World world; - private ThreadSafeQueue<(INitroxConnection, string)> JoinQueue { get; set; } = new(); + private ThreadSafeQueue<(INitroxConnection, string)> joinQueue { get; } = new(); private bool queueIdle; public Action SyncFinishedCallback { get; private set; } @@ -46,7 +46,7 @@ private async Task JoinQueueLoop() { try { - while (JoinQueue.Count == 0) + while (joinQueue.Count == 0) { queueIdle = true; await Task.Delay(REFRESH_DELAY); @@ -54,17 +54,15 @@ private async Task JoinQueueLoop() queueIdle = false; - (INitroxConnection connection, string reservationKey) = JoinQueue.Dequeue(); + (INitroxConnection connection, string reservationKey) = joinQueue.Dequeue(); string name = playerManager.GetPlayerContext(reservationKey).PlayerName; // Do this after dequeueing because everyone's position shifts forward + (INitroxConnection, string)[] array = [.. joinQueue]; + for (int i = 0; i < array.Length; i++) { - (INitroxConnection, string)[] array = [.. JoinQueue]; - for (int i = 0; i < array.Length; i++) - { - (INitroxConnection c, _) = array[i]; - c.SendPacket(new JoinQueueInfo(i + 1, serverConfig.InitialSyncTimeout, false)); - } + (INitroxConnection c, _) = array[i]; + c.SendPacket(new JoinQueueInfo(i + 1, serverConfig.InitialSyncTimeout, false)); } Log.Info($"Starting sync for player {name}"); @@ -123,7 +121,7 @@ await Task.Run(() => } else { - Log.Info($"Player {name} joined successfully. Remaining requests: {JoinQueue.Count}"); + Log.Info($"Player {name} joined successfully. Remaining requests: {joinQueue.Count}"); BroadcastPlayerJoined(playerManager.GetPlayer(connection)); } }); @@ -139,16 +137,16 @@ public void AddToJoinQueue(INitroxConnection connection, string reservationKey) { if (!queueIdle) { - connection.SendPacket(new JoinQueueInfo(JoinQueue.Count + 1, serverConfig.InitialSyncTimeout, true)); + connection.SendPacket(new JoinQueueInfo(joinQueue.Count + 1, serverConfig.InitialSyncTimeout, true)); } Log.Info($"Added player {playerManager.GetPlayerContext(reservationKey).PlayerName} to queue"); - JoinQueue.Enqueue((connection, reservationKey)); + joinQueue.Enqueue((connection, reservationKey)); } public IEnumerable GetQueuedPlayers() { - return JoinQueue.Select(tuple => tuple.Item1); + return joinQueue.Select(tuple => tuple.Item1); } @@ -234,7 +232,7 @@ PlayerWorldEntity RespawnExistingEntity(Player player) public void JoiningPlayerDisconnected(INitroxConnection connection) { // They may have been queued, so just erase their entry - JoinQueue.Filter(tuple => !Equals(tuple.Item1, connection)); + joinQueue.Filter(tuple => !Equals(tuple.Item1, connection)); } public void BroadcastPlayerJoined(Player player) From 1a771f9e7628667f0257a92226e8002d72c59b38 Mon Sep 17 00:00:00 2001 From: SpaceMonkeyy86 Date: Sat, 8 Feb 2025 23:30:24 -0500 Subject: [PATCH 24/40] Display in-game log correctly when time is paused --- .../ErrorMessage_OnLateUpdate_Patch.cs | 53 +++++++++++++++++++ 1 file changed, 53 insertions(+) create mode 100644 NitroxPatcher/Patches/Dynamic/ErrorMessage_OnLateUpdate_Patch.cs diff --git a/NitroxPatcher/Patches/Dynamic/ErrorMessage_OnLateUpdate_Patch.cs b/NitroxPatcher/Patches/Dynamic/ErrorMessage_OnLateUpdate_Patch.cs new file mode 100644 index 0000000000..c25622e68a --- /dev/null +++ b/NitroxPatcher/Patches/Dynamic/ErrorMessage_OnLateUpdate_Patch.cs @@ -0,0 +1,53 @@ +using System.Collections.Generic; +using System.Reflection; +using System.Reflection.Emit; +using HarmonyLib; +using NitroxModel.Helper; +using NitroxPatcher.Patches; +using UnityEngine; + +/// +/// Ensures the in-game log is animated smoothly regardless of the time scale. +/// +public sealed partial class ErrorMessage_OnLateUpdate_Patch : NitroxPatch, IDynamicPatch +{ + private static readonly MethodInfo TARGET_METHOD_ON_LATE_UPDATE = Reflect.Method((ErrorMessage t) => t.OnLateUpdate()); + private static readonly MethodInfo TARGET_METHOD_ADD_MESSAGE = Reflect.Method((ErrorMessage t) => t._AddMessage(default)); + private static readonly MethodInfo TARGET_OPERAND_TIME = Reflect.Property(() => PDA.time).GetMethod; + private static readonly MethodInfo TARGET_OPERAND_DELTA_TIME = Reflect.Property(() => PDA.deltaTime).GetMethod; + private static readonly MethodInfo INJECTION_OPERAND_UNSCALED_TIME = Reflect.Property(() => Time.unscaledTime).GetMethod; + private static readonly MethodInfo INJECTION_OPERAND_UNSCALED_DELTA_TIME = Reflect.Property(() => Time.unscaledDeltaTime).GetMethod; + + + /* + * Replace all calls to PDA.time with Time.unscaledTime and to + * PDA.deltaTime with Time.unscaledDeltaTime + */ + public static IEnumerable Transpiler(IEnumerable instructions) + { + CodeMatcher matcher = new CodeMatcher(instructions); + + while (matcher.MatchStartForward(new CodeMatch(OpCodes.Call, TARGET_OPERAND_TIME)).IsValid) + { + matcher.SetOperandAndAdvance(INJECTION_OPERAND_UNSCALED_TIME); + } + + matcher.Start(); + + while (matcher.MatchStartForward(new CodeMatch(OpCodes.Call, TARGET_OPERAND_DELTA_TIME)).IsValid) + { + matcher.SetOperandAndAdvance(INJECTION_OPERAND_UNSCALED_DELTA_TIME); + } + + return matcher.InstructionEnumeration(); + } + + public override void Patch(Harmony harmony) + { + MethodInfo transpilerInfo = Reflect.Method(() => Transpiler(default)); + + // TODO: modify sanity checks to support transpilers with multiple target methods + PatchTranspiler(harmony, TARGET_METHOD_ON_LATE_UPDATE, transpilerInfo); + PatchTranspiler(harmony, TARGET_METHOD_ADD_MESSAGE, transpilerInfo); + } +} From 2733ba7ad7100fec4d3538c9dd1b62b90b1ae10c Mon Sep 17 00:00:00 2001 From: SpaceMonkeyy86 Date: Sat, 8 Feb 2025 23:59:02 -0500 Subject: [PATCH 25/40] Add sanity check support for multi-transpilers --- .../Patcher/Patches/PatchesTranspilerTest.cs | 46 ++++++++++++++----- .../ErrorMessage_OnLateUpdate_Patch.cs | 1 - 2 files changed, 35 insertions(+), 12 deletions(-) diff --git a/Nitrox.Test/Patcher/Patches/PatchesTranspilerTest.cs b/Nitrox.Test/Patcher/Patches/PatchesTranspilerTest.cs index 815da3d0f3..e9f1b50bc7 100644 --- a/Nitrox.Test/Patcher/Patches/PatchesTranspilerTest.cs +++ b/Nitrox.Test/Patcher/Patches/PatchesTranspilerTest.cs @@ -43,6 +43,7 @@ public class PatchesTranspilerTest [typeof(EntityCell_AwakeAsync_Patch), 2], [typeof(EntityCell_SleepAsync_Patch), 2], [typeof(Equipment_RemoveItem_Patch), 7], + [typeof(ErrorMessage_OnLateUpdate_Patch), new[] {0, 0}], [typeof(EscapePod_Start_Patch), 43], [typeof(FireExtinguisherHolder_TakeTankAsync_Patch), 2], [typeof(FireExtinguisherHolder_TryStoreTank_Patch), 3], @@ -103,23 +104,46 @@ public void AllTranspilerPatchesHaveSanityTest() [TestMethod] [DynamicData(nameof(TranspilerPatchClasses))] - public void AllPatchesTranspilerSanity(Type patchClassType, int ilDifference, bool logInstructions = false) + public void AllPatchesTranspilerSanity(Type patchClassType, object ilDifference, bool logInstructions = false) { + MethodInfo transpilerMethod = patchClassType.GetMethod("Transpiler"); + if (transpilerMethod == null) + { + Assert.Fail($"Could not find a \"Transpiler\" method inside {patchClassType.Name}"); + } + FieldInfo targetMethodInfo = patchClassType.GetRuntimeFields().FirstOrDefault(x => string.Equals(x.Name.Replace("_", ""), "targetMethod", StringComparison.OrdinalIgnoreCase)); - if (targetMethodInfo == null) + if (targetMethodInfo != null) { - Assert.Fail($"Could not find either \"TARGET_METHOD\" nor \"targetMethod\" inside {patchClassType.Name}"); + MethodInfo targetMethod = targetMethodInfo.GetValue(null) as MethodInfo; + TestTranspilerMethod(patchClassType, targetMethod, transpilerMethod, (int)ilDifference, logInstructions); } + else + { + FieldInfo[] targetMethodInfos = patchClassType.GetRuntimeFields() + .Where(x => x.Name.Replace("_", "").StartsWith("targetMethod", StringComparison.OrdinalIgnoreCase)) + .OrderBy(fieldInfo => fieldInfo.Name) + .ToArray(); + if (targetMethodInfos.Length == 0) + { + Assert.Fail($"Could not find a \"TARGET_METHOD\" or \"targetMethod\" field inside {patchClassType.Name}"); + } - MethodInfo targetMethod = targetMethodInfo.GetValue(null) as MethodInfo; - List originalIl = PatchTestHelper.GetInstructionsFromMethod(targetMethod).ToList(); - List originalIlCopy = PatchTestHelper.GetInstructionsFromMethod(targetMethod).ToList(); // Our custom pattern matching replaces OpCode/Operand in place, therefor we need a copy to compare if changes are present + // Should be put in alphabetical order of the corresponding "TARGET_METHOD" field name + int[] ilDifferences = (int[])ilDifference; - MethodInfo transpilerMethod = patchClassType.GetMethod("Transpiler"); - if (transpilerMethod == null) - { - Assert.Fail($"Could not find \"Transpiler\" inside {patchClassType.Name}"); + for (int i = 0; i < targetMethodInfos.Length; i++) + { + MethodInfo targetMethod = targetMethodInfos[i].GetValue(null) as MethodInfo; + TestTranspilerMethod(patchClassType, targetMethod, transpilerMethod, ilDifferences[i], logInstructions); + } } + } + + private static void TestTranspilerMethod(Type patchClassType, MethodInfo targetMethod, MethodInfo transpilerMethod, int ilDifference, bool logInstructions = false) + { + List originalIl = PatchTestHelper.GetInstructionsFromMethod(targetMethod).ToList(); + List originalIlCopy = PatchTestHelper.GetInstructionsFromMethod(targetMethod).ToList(); // Our custom pattern matching replaces OpCode/Operand in place, therefor we need a copy to compare if changes are present List injectionParameters = []; foreach (ParameterInfo parameterInfo in transpilerMethod.GetParameters()) @@ -143,7 +167,7 @@ public void AllPatchesTranspilerSanity(Type patchClassType, int ilDifference, bo } List transformedIl = (transpilerMethod.Invoke(null, injectionParameters.ToArray()) as IEnumerable)?.ToList(); - + if (logInstructions) { Console.WriteLine(transformedIl.ToPrettyString()); diff --git a/NitroxPatcher/Patches/Dynamic/ErrorMessage_OnLateUpdate_Patch.cs b/NitroxPatcher/Patches/Dynamic/ErrorMessage_OnLateUpdate_Patch.cs index c25622e68a..ca19a2e3cd 100644 --- a/NitroxPatcher/Patches/Dynamic/ErrorMessage_OnLateUpdate_Patch.cs +++ b/NitroxPatcher/Patches/Dynamic/ErrorMessage_OnLateUpdate_Patch.cs @@ -46,7 +46,6 @@ public override void Patch(Harmony harmony) { MethodInfo transpilerInfo = Reflect.Method(() => Transpiler(default)); - // TODO: modify sanity checks to support transpilers with multiple target methods PatchTranspiler(harmony, TARGET_METHOD_ON_LATE_UPDATE, transpilerInfo); PatchTranspiler(harmony, TARGET_METHOD_ADD_MESSAGE, transpilerInfo); } From 29f5305ebdeb01dfcf95a7507d447b2b54b1b261 Mon Sep 17 00:00:00 2001 From: SpaceMonkeyy86 Date: Sun, 9 Feb 2025 16:49:12 -0500 Subject: [PATCH 26/40] Improve join queue messages shown to player --- Nitrox.Assets.Subnautica/LanguageFiles/en.json | 1 + .../Packets/Processors/JoinQueueInfoProcessor.cs | 15 +++------------ NitroxModel/Packets/JoinQueueInfo.cs | 4 +--- NitroxServer/GameLogic/JoiningManager.cs | 4 ++-- 4 files changed, 7 insertions(+), 17 deletions(-) diff --git a/Nitrox.Assets.Subnautica/LanguageFiles/en.json b/Nitrox.Assets.Subnautica/LanguageFiles/en.json index 146565dbea..6a8f8e3821 100644 --- a/Nitrox.Assets.Subnautica/LanguageFiles/en.json +++ b/Nitrox.Assets.Subnautica/LanguageFiles/en.json @@ -64,6 +64,7 @@ "Nitrox_PlayerKicked": "You've been kicked from the server", "Nitrox_PlayerLeft": "{PLAYER} left the game.", "Nitrox_PlayerListTabName": "Player List", + "Nitrox_QueueInfo": "You are at position #{POSITION} in the join queue. Maximum possible wait: {TIME}", "Nitrox_RejectedSessionPolicy": "Reservation rejected…", "Nitrox_RemotePlayerObstacle": "Another player's either inside or too close to the deconstructable target.", "Nitrox_RequestingSessionPolicy": "Requesting session policy info…", diff --git a/NitroxClient/Communication/Packets/Processors/JoinQueueInfoProcessor.cs b/NitroxClient/Communication/Packets/Processors/JoinQueueInfoProcessor.cs index 2824b70599..8b83975397 100644 --- a/NitroxClient/Communication/Packets/Processors/JoinQueueInfoProcessor.cs +++ b/NitroxClient/Communication/Packets/Processors/JoinQueueInfoProcessor.cs @@ -8,17 +8,8 @@ public class JoinQueueInfoProcessor : ClientPacketProcessor { public override void Process(JoinQueueInfo packet) { - Log.InGame($"You are at position #{packet.Position} in the queue."); - - if (packet.ShowMaximumWait) - { - Log.InGame($"The maximum wait time per person is {MillisToMinutes(packet.Timeout)} minutes."); - } - } - - private static string MillisToMinutes(int milliseconds) - { - double minutes = milliseconds / 60000.0; - return Math.Round(minutes, 1).ToString(); + Log.InGame(Language.main.Get("Nitrox_QueueInfo") + .Replace("{POSITION}", packet.Position.ToString()) + .Replace("{TIME}", TimeSpan.FromMilliseconds(packet.Timeout * packet.Position).ToString(@"mm\:ss"))); } } diff --git a/NitroxModel/Packets/JoinQueueInfo.cs b/NitroxModel/Packets/JoinQueueInfo.cs index a7e93bdc91..15ec5d86eb 100644 --- a/NitroxModel/Packets/JoinQueueInfo.cs +++ b/NitroxModel/Packets/JoinQueueInfo.cs @@ -7,12 +7,10 @@ public class JoinQueueInfo : Packet { public int Position { get; } public int Timeout { get; } - public bool ShowMaximumWait { get; } - public JoinQueueInfo(int position, int timeout, bool showMaximumWait) + public JoinQueueInfo(int position, int timeout) { Position = position; Timeout = timeout; - ShowMaximumWait = showMaximumWait; } } diff --git a/NitroxServer/GameLogic/JoiningManager.cs b/NitroxServer/GameLogic/JoiningManager.cs index 4eb8ac9b3d..35bf419c95 100644 --- a/NitroxServer/GameLogic/JoiningManager.cs +++ b/NitroxServer/GameLogic/JoiningManager.cs @@ -60,7 +60,7 @@ private async Task JoinQueueLoop() for (int i = 0; i < array.Length; i++) { (INitroxConnection c, _) = array[i]; - c.SendPacket(new JoinQueueInfo(i + 1, serverConfig.InitialSyncTimeout, false)); + c.SendPacket(new JoinQueueInfo(i + 1, serverConfig.InitialSyncTimeout)); } Log.Info($"Starting sync for player {name}"); @@ -135,7 +135,7 @@ public void AddToJoinQueue(INitroxConnection connection, string reservationKey) { if (!queueIdle) { - connection.SendPacket(new JoinQueueInfo(joinQueue.Count + 1, serverConfig.InitialSyncTimeout, true)); + connection.SendPacket(new JoinQueueInfo(joinQueue.Count + 1, serverConfig.InitialSyncTimeout)); } Log.Info($"Added player {playerManager.GetPlayerContext(reservationKey).PlayerName} to queue"); From f4ad6661997f5eccdf449143d1fc83538fdb0bba Mon Sep 17 00:00:00 2001 From: SpaceMonkeyy86 Date: Sun, 9 Feb 2025 17:09:11 -0500 Subject: [PATCH 27/40] Notify timeout using modal instead of logs --- .../Processors/PlayerSyncTimeoutProcessor.cs | 13 ------------- NitroxClient/MonoBehaviours/Multiplayer.cs | 18 ------------------ NitroxModel/Packets/PlayerSyncTimeout.cs | 6 ------ NitroxServer/GameLogic/JoiningManager.cs | 2 +- 4 files changed, 1 insertion(+), 38 deletions(-) delete mode 100644 NitroxClient/Communication/Packets/Processors/PlayerSyncTimeoutProcessor.cs delete mode 100644 NitroxModel/Packets/PlayerSyncTimeout.cs diff --git a/NitroxClient/Communication/Packets/Processors/PlayerSyncTimeoutProcessor.cs b/NitroxClient/Communication/Packets/Processors/PlayerSyncTimeoutProcessor.cs deleted file mode 100644 index 8e2395fd51..0000000000 --- a/NitroxClient/Communication/Packets/Processors/PlayerSyncTimeoutProcessor.cs +++ /dev/null @@ -1,13 +0,0 @@ -using NitroxClient.Communication.Packets.Processors.Abstract; -using NitroxClient.MonoBehaviours; -using NitroxModel.Packets; - -namespace NitroxClient.Communication.Packets.Processors; - -public class PlayerSyncTimeoutProcessor : ClientPacketProcessor -{ - public override void Process(PlayerSyncTimeout packet) - { - Multiplayer.Main.TimeOut(); - } -} diff --git a/NitroxClient/MonoBehaviours/Multiplayer.cs b/NitroxClient/MonoBehaviours/Multiplayer.cs index 553d6356dc..4a74a5e3ad 100644 --- a/NitroxClient/MonoBehaviours/Multiplayer.cs +++ b/NitroxClient/MonoBehaviours/Multiplayer.cs @@ -126,24 +126,6 @@ public static IEnumerator LoadAsync() OnLoadingComplete?.Invoke(); } - public void TimeOut() - { - multiplayerSession.Disconnect(); - StartCoroutine(TimeOutRoutine()); - - IEnumerator TimeOutRoutine() - { - // TODO: replace with modal - for (int timer = 5; timer > 0; timer--) - { - Log.InGame($"Initial sync timed out. Quitting game in {timer} second{(timer > 1 ? "s" : "")}…"); - yield return new WaitForSecondsRealtime(1); - } - - IngameMenu.main.QuitGame(false); - } - } - public void ProcessPackets() { static PacketProcessor ResolveProcessor(Packet packet, Dictionary processorCache) diff --git a/NitroxModel/Packets/PlayerSyncTimeout.cs b/NitroxModel/Packets/PlayerSyncTimeout.cs deleted file mode 100644 index 36a9be6de0..0000000000 --- a/NitroxModel/Packets/PlayerSyncTimeout.cs +++ /dev/null @@ -1,6 +0,0 @@ -using System; - -namespace NitroxModel.Packets; - -[Serializable] -public class PlayerSyncTimeout : Packet { } diff --git a/NitroxServer/GameLogic/JoiningManager.cs b/NitroxServer/GameLogic/JoiningManager.cs index 35bf419c95..25d3de4919 100644 --- a/NitroxServer/GameLogic/JoiningManager.cs +++ b/NitroxServer/GameLogic/JoiningManager.cs @@ -113,7 +113,7 @@ await Task.Run(() => if (connection.State == NitroxConnectionState.Connected) { - connection.SendPacket(new PlayerSyncTimeout()); + connection.SendPacket(new PlayerKicked("Initial sync took too long and timed out")); } playerManager.PlayerDisconnected(connection); } From c310ff8a821d2afc44350f224484afc71c497ca6 Mon Sep 17 00:00:00 2001 From: SpaceMonkeyy86 Date: Sun, 9 Feb 2025 18:23:48 -0500 Subject: [PATCH 28/40] Only run queue task when there are players in the queue --- NitroxServer/GameLogic/JoiningManager.cs | 32 ++++++++++++++---------- 1 file changed, 19 insertions(+), 13 deletions(-) diff --git a/NitroxServer/GameLogic/JoiningManager.cs b/NitroxServer/GameLogic/JoiningManager.cs index 25d3de4919..60cbb042bd 100644 --- a/NitroxServer/GameLogic/JoiningManager.cs +++ b/NitroxServer/GameLogic/JoiningManager.cs @@ -24,7 +24,7 @@ public sealed class JoiningManager private readonly World world; private ThreadSafeQueue<(INitroxConnection, string)> joinQueue { get; } = new(); - private bool queueIdle; + private bool queueActive; public Action SyncFinishedCallback { get; private set; } public JoiningManager(PlayerManager playerManager, SubnauticaServerConfig serverConfig, World world) @@ -32,26 +32,18 @@ public JoiningManager(PlayerManager playerManager, SubnauticaServerConfig server this.playerManager = playerManager; this.serverConfig = serverConfig; this.world = world; - - Task.Run(JoinQueueLoop).ContinueWithHandleError(); } private async Task JoinQueueLoop() { + queueActive = true; + const int REFRESH_DELAY = 10; - while (true) + while (joinQueue.Count > 0) { try { - while (joinQueue.Count == 0) - { - queueIdle = true; - await Task.Delay(REFRESH_DELAY); - } - - queueIdle = false; - (INitroxConnection connection, string reservationKey) = joinQueue.Dequeue(); string name = playerManager.GetPlayerContext(reservationKey).PlayerName; @@ -129,17 +121,31 @@ await Task.Run(() => Log.Error($"Unexpected error during player connection: {e}"); } } + + queueActive = false; + + // Prevents race condition where someone is enqueued after the loop terminates + // but before queueActive is set to false + if (joinQueue.Count > 0) + { + await JoinQueueLoop(); + } } public void AddToJoinQueue(INitroxConnection connection, string reservationKey) { - if (!queueIdle) + if (queueActive) { connection.SendPacket(new JoinQueueInfo(joinQueue.Count + 1, serverConfig.InitialSyncTimeout)); } Log.Info($"Added player {playerManager.GetPlayerContext(reservationKey).PlayerName} to queue"); joinQueue.Enqueue((connection, reservationKey)); + + if (!queueActive) + { + Task.Run(JoinQueueLoop).ContinueWithHandleError(); + } } public IEnumerable GetQueuedPlayers() From ad181706eacbc058af100d828d197d46b2a8f174 Mon Sep 17 00:00:00 2001 From: SpaceMonkeyy86 Date: Sun, 9 Feb 2025 18:26:41 -0500 Subject: [PATCH 29/40] Use if-else --- NitroxServer/GameLogic/JoiningManager.cs | 9 ++++----- 1 file changed, 4 insertions(+), 5 deletions(-) diff --git a/NitroxServer/GameLogic/JoiningManager.cs b/NitroxServer/GameLogic/JoiningManager.cs index 60cbb042bd..46d38739d4 100644 --- a/NitroxServer/GameLogic/JoiningManager.cs +++ b/NitroxServer/GameLogic/JoiningManager.cs @@ -134,15 +134,14 @@ await Task.Run(() => public void AddToJoinQueue(INitroxConnection connection, string reservationKey) { + Log.Info($"Added player {playerManager.GetPlayerContext(reservationKey).PlayerName} to queue"); + joinQueue.Enqueue((connection, reservationKey)); + if (queueActive) { connection.SendPacket(new JoinQueueInfo(joinQueue.Count + 1, serverConfig.InitialSyncTimeout)); } - - Log.Info($"Added player {playerManager.GetPlayerContext(reservationKey).PlayerName} to queue"); - joinQueue.Enqueue((connection, reservationKey)); - - if (!queueActive) + else { Task.Run(JoinQueueLoop).ContinueWithHandleError(); } From b1f170ec04a1954a1e15ac06f1a474b821b0511f Mon Sep 17 00:00:00 2001 From: SpaceMonkeyy86 Date: Mon, 10 Feb 2025 14:42:27 -0500 Subject: [PATCH 30/40] Fix race conditions with locks --- NitroxServer/GameLogic/JoiningManager.cs | 45 ++++++++++++++---------- 1 file changed, 26 insertions(+), 19 deletions(-) diff --git a/NitroxServer/GameLogic/JoiningManager.cs b/NitroxServer/GameLogic/JoiningManager.cs index 46d38739d4..f2287ad477 100644 --- a/NitroxServer/GameLogic/JoiningManager.cs +++ b/NitroxServer/GameLogic/JoiningManager.cs @@ -24,6 +24,7 @@ public sealed class JoiningManager private readonly World world; private ThreadSafeQueue<(INitroxConnection, string)> joinQueue { get; } = new(); + private readonly Lock queueLocker = new(); private bool queueActive; public Action SyncFinishedCallback { get; private set; } @@ -36,12 +37,23 @@ public JoiningManager(PlayerManager playerManager, SubnauticaServerConfig server private async Task JoinQueueLoop() { + // It may be possible to use the task's status itself for this, + // but the ContinueWithHandleError callback might cause issues queueActive = true; const int REFRESH_DELAY = 10; - while (joinQueue.Count > 0) + while (true) { + lock (queueLocker) + { + if (joinQueue.Count == 0) + { + queueActive = false; + return; + } + } + try { (INitroxConnection connection, string reservationKey) = joinQueue.Dequeue(); @@ -121,29 +133,24 @@ await Task.Run(() => Log.Error($"Unexpected error during player connection: {e}"); } } - - queueActive = false; - - // Prevents race condition where someone is enqueued after the loop terminates - // but before queueActive is set to false - if (joinQueue.Count > 0) - { - await JoinQueueLoop(); - } } public void AddToJoinQueue(INitroxConnection connection, string reservationKey) { - Log.Info($"Added player {playerManager.GetPlayerContext(reservationKey).PlayerName} to queue"); - joinQueue.Enqueue((connection, reservationKey)); - - if (queueActive) - { - connection.SendPacket(new JoinQueueInfo(joinQueue.Count + 1, serverConfig.InitialSyncTimeout)); - } - else + // Necessary to avoid race conditions between this method and the queue count check + lock (queueLocker) { - Task.Run(JoinQueueLoop).ContinueWithHandleError(); + Log.Info($"Added player {playerManager.GetPlayerContext(reservationKey).PlayerName} to queue"); + joinQueue.Enqueue((connection, reservationKey)); + + if (queueActive) + { + connection.SendPacket(new JoinQueueInfo(joinQueue.Count, serverConfig.InitialSyncTimeout)); + } + else + { + Task.Run(JoinQueueLoop).ContinueWithHandleError(); + } } } From 9ff4d189df833cce657d6245b44d004f9907e556 Mon Sep 17 00:00:00 2001 From: SpaceMonkeyy86 Date: Mon, 10 Feb 2025 15:47:19 -0500 Subject: [PATCH 31/40] Minor cleanup --- NitroxServer/GameLogic/JoiningManager.cs | 65 +++++++++--------------- 1 file changed, 23 insertions(+), 42 deletions(-) diff --git a/NitroxServer/GameLogic/JoiningManager.cs b/NitroxServer/GameLogic/JoiningManager.cs index f2287ad477..74e82920bc 100644 --- a/NitroxServer/GameLogic/JoiningManager.cs +++ b/NitroxServer/GameLogic/JoiningManager.cs @@ -41,8 +41,6 @@ private async Task JoinQueueLoop() // but the ContinueWithHandleError callback might cause issues queueActive = true; - const int REFRESH_DELAY = 10; - while (true) { lock (queueLocker) @@ -72,61 +70,44 @@ private async Task JoinQueueLoop() CancellationTokenSource source = new(serverConfig.InitialSyncTimeout); bool syncFinished = false; - bool disconnected = false; SyncFinishedCallback = () => { syncFinished = true; }; await Task.Run(() => { - while (!source.IsCancellationRequested) + while (true) { - if (syncFinished) - { - return true; - } - - if (connection.State == NitroxConnectionState.Disconnected) + if (syncFinished || + connection.State == NitroxConnectionState.Disconnected || + source.IsCancellationRequested) { - disconnected = true; - return false; + return; } - Task.Delay(REFRESH_DELAY).Wait(); + Task.Delay(10).Wait(); } + }); - return false; - }) - // We use ContinueWith to avoid having to try/catch a TaskCanceledException - .ContinueWith((Task task) => + if (connection.State == NitroxConnectionState.Disconnected) { - if (task.IsFaulted) - { - throw task.Exception; - } - - if (disconnected) - { - Log.Info($"Player {name} disconnected while syncing"); - return; - } - - if (task.IsCanceled || !task.Result) - { - Log.Info($"Initial sync timed out for player {name}"); - SyncFinishedCallback = null; + Log.Info($"Player {name} disconnected while syncing"); + } + else if (source.IsCancellationRequested) + { + Log.Info($"Initial sync timed out for player {name}"); + SyncFinishedCallback = null; - if (connection.State == NitroxConnectionState.Connected) - { - connection.SendPacket(new PlayerKicked("Initial sync took too long and timed out")); - } - playerManager.PlayerDisconnected(connection); - } - else + if (connection.State == NitroxConnectionState.Connected) { - Log.Info($"Player {name} joined successfully. Remaining requests: {joinQueue.Count}"); - BroadcastPlayerJoined(playerManager.GetPlayer(connection)); + connection.SendPacket(new PlayerKicked("Initial sync took too long and timed out")); } - }); + playerManager.PlayerDisconnected(connection); + } + else + { + Log.Info($"Player {name} joined successfully. Remaining requests: {joinQueue.Count}"); + BroadcastPlayerJoined(playerManager.GetPlayer(connection)); + } } catch (Exception e) { From 6b759ecc21645341dea2ca7ce01da5e18fdb0fcb Mon Sep 17 00:00:00 2001 From: SpaceMonkeyy86 Date: Mon, 10 Feb 2025 18:10:35 -0500 Subject: [PATCH 32/40] Remove seemingly redundant check when handling packets --- NitroxServer/Communication/Packets/PacketHandler.cs | 9 ++++----- 1 file changed, 4 insertions(+), 5 deletions(-) diff --git a/NitroxServer/Communication/Packets/PacketHandler.cs b/NitroxServer/Communication/Packets/PacketHandler.cs index 449cd893a0..0f9e828de9 100644 --- a/NitroxServer/Communication/Packets/PacketHandler.cs +++ b/NitroxServer/Communication/Packets/PacketHandler.cs @@ -1,6 +1,5 @@ using System; using System.Collections.Generic; -using System.Linq; using NitroxModel.Core; using NitroxModel.Packets; using NitroxModel.Packets.Processors.Abstract; @@ -28,13 +27,13 @@ public PacketHandler(PlayerManager playerManager, JoiningManager joiningManager, public void Process(Packet packet, INitroxConnection connection) { Player player = playerManager.GetPlayer(connection); - if (player != null) + if (player == null) { - ProcessAuthenticated(packet, player); + ProcessUnauthenticated(packet, connection); } - else if (!joiningManager.GetQueuedPlayers().Contains(connection)) + else { - ProcessUnauthenticated(packet, connection); + ProcessAuthenticated(packet, player); } } From 7f4fc41ba446259e417a74e30e17a9ec5549d2c8 Mon Sep 17 00:00:00 2001 From: SpaceMonkeyy86 Date: Mon, 10 Feb 2025 18:19:11 -0500 Subject: [PATCH 33/40] Remove unnecessary DI dependency --- NitroxServer/Communication/Packets/PacketHandler.cs | 4 +--- 1 file changed, 1 insertion(+), 3 deletions(-) diff --git a/NitroxServer/Communication/Packets/PacketHandler.cs b/NitroxServer/Communication/Packets/PacketHandler.cs index 0f9e828de9..35a4436f68 100644 --- a/NitroxServer/Communication/Packets/PacketHandler.cs +++ b/NitroxServer/Communication/Packets/PacketHandler.cs @@ -12,15 +12,13 @@ namespace NitroxServer.Communication.Packets public class PacketHandler { private readonly PlayerManager playerManager; - private readonly JoiningManager joiningManager; private readonly DefaultServerPacketProcessor defaultServerPacketProcessor; private readonly Dictionary packetProcessorAuthCache = new(); private readonly Dictionary packetProcessorUnauthCache = new(); - public PacketHandler(PlayerManager playerManager, JoiningManager joiningManager, DefaultServerPacketProcessor packetProcessor) + public PacketHandler(PlayerManager playerManager, DefaultServerPacketProcessor packetProcessor) { this.playerManager = playerManager; - this.joiningManager = joiningManager; defaultServerPacketProcessor = packetProcessor; } From 228f5dcae2c10263c66ca1d5c2572956bc5b9f75 Mon Sep 17 00:00:00 2001 From: SpaceMonkeyy86 Date: Mon, 10 Feb 2025 18:20:46 -0500 Subject: [PATCH 34/40] Remove out-of-place comma on main menu of launcher (I don't want to create another PR and this deeply annoys me) --- Nitrox.Launcher/Views/LaunchGameView.axaml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Nitrox.Launcher/Views/LaunchGameView.axaml b/Nitrox.Launcher/Views/LaunchGameView.axaml index ec5f4ffc3e..6c1d72f935 100644 --- a/Nitrox.Launcher/Views/LaunchGameView.axaml +++ b/Nitrox.Launcher/Views/LaunchGameView.axaml @@ -35,7 +35,7 @@ Grid.Column="0" Margin="0,0,20,0" FontSize="14" - Text="Start your Subnautica adventure in multiplayer mode together with your friends, powered by the Nitrox mod: An open-source, modification for the game Subnautica. The project is maintained by the community with regular support and updates from its contributors." /> + Text="Start your Subnautica adventure in multiplayer mode together with your friends, powered by the Nitrox mod: An open-source modification for the game Subnautica. The project is maintained by the community with regular support and updates from its contributors." /> From 37d1831c56d0f91e873e1adcc0c3e1a75a8bbec7 Mon Sep 17 00:00:00 2001 From: SpaceMonkeyy86 Date: Thu, 13 Feb 2025 12:02:21 -0500 Subject: [PATCH 35/40] Use a single integer for IL difference in transpiler sanity check --- .../Patcher/Patches/PatchesTranspilerTest.cs | 37 +++++++------------ 1 file changed, 13 insertions(+), 24 deletions(-) diff --git a/Nitrox.Test/Patcher/Patches/PatchesTranspilerTest.cs b/Nitrox.Test/Patcher/Patches/PatchesTranspilerTest.cs index e9f1b50bc7..3b62c6258f 100644 --- a/Nitrox.Test/Patcher/Patches/PatchesTranspilerTest.cs +++ b/Nitrox.Test/Patcher/Patches/PatchesTranspilerTest.cs @@ -43,7 +43,7 @@ public class PatchesTranspilerTest [typeof(EntityCell_AwakeAsync_Patch), 2], [typeof(EntityCell_SleepAsync_Patch), 2], [typeof(Equipment_RemoveItem_Patch), 7], - [typeof(ErrorMessage_OnLateUpdate_Patch), new[] {0, 0}], + [typeof(ErrorMessage_OnLateUpdate_Patch), 0], [typeof(EscapePod_Start_Patch), 43], [typeof(FireExtinguisherHolder_TakeTankAsync_Patch), 2], [typeof(FireExtinguisherHolder_TryStoreTank_Patch), 3], @@ -104,7 +104,7 @@ public void AllTranspilerPatchesHaveSanityTest() [TestMethod] [DynamicData(nameof(TranspilerPatchClasses))] - public void AllPatchesTranspilerSanity(Type patchClassType, object ilDifference, bool logInstructions = false) + public void AllPatchesTranspilerSanity(Type patchClassType, int ilDifference, bool logInstructions = false) { MethodInfo transpilerMethod = patchClassType.GetMethod("Transpiler"); if (transpilerMethod == null) @@ -112,31 +112,20 @@ public void AllPatchesTranspilerSanity(Type patchClassType, object ilDifference, Assert.Fail($"Could not find a \"Transpiler\" method inside {patchClassType.Name}"); } - FieldInfo targetMethodInfo = patchClassType.GetRuntimeFields().FirstOrDefault(x => string.Equals(x.Name.Replace("_", ""), "targetMethod", StringComparison.OrdinalIgnoreCase)); - if (targetMethodInfo != null) + FieldInfo[] targetMethodInfos = patchClassType.GetRuntimeFields() + .Where(x => x.Name.Replace("_", "").StartsWith("targetMethod", StringComparison.OrdinalIgnoreCase)) + .OrderBy(fieldInfo => fieldInfo.Name) + .ToArray(); + + if (targetMethodInfos.Length == 0) { - MethodInfo targetMethod = targetMethodInfo.GetValue(null) as MethodInfo; - TestTranspilerMethod(patchClassType, targetMethod, transpilerMethod, (int)ilDifference, logInstructions); + Assert.Fail($"Could not find a \"TARGET_METHOD\" or \"targetMethod\" field inside {patchClassType.Name}"); } - else - { - FieldInfo[] targetMethodInfos = patchClassType.GetRuntimeFields() - .Where(x => x.Name.Replace("_", "").StartsWith("targetMethod", StringComparison.OrdinalIgnoreCase)) - .OrderBy(fieldInfo => fieldInfo.Name) - .ToArray(); - if (targetMethodInfos.Length == 0) - { - Assert.Fail($"Could not find a \"TARGET_METHOD\" or \"targetMethod\" field inside {patchClassType.Name}"); - } - // Should be put in alphabetical order of the corresponding "TARGET_METHOD" field name - int[] ilDifferences = (int[])ilDifference; - - for (int i = 0; i < targetMethodInfos.Length; i++) - { - MethodInfo targetMethod = targetMethodInfos[i].GetValue(null) as MethodInfo; - TestTranspilerMethod(patchClassType, targetMethod, transpilerMethod, ilDifferences[i], logInstructions); - } + foreach (FieldInfo field in targetMethodInfos) + { + MethodInfo targetMethod = field.GetValue(null) as MethodInfo; + TestTranspilerMethod(patchClassType, targetMethod, transpilerMethod, ilDifference, logInstructions); } } From 663ac27452641d9ad7912d21ae1ccbd05302ce3f Mon Sep 17 00:00:00 2001 From: SpaceMonkeyy86 Date: Thu, 13 Feb 2025 12:03:15 -0500 Subject: [PATCH 36/40] Remove manual translation (will be automatically merged later) --- Nitrox.Assets.Subnautica/LanguageFiles/en.json | 1 - 1 file changed, 1 deletion(-) diff --git a/Nitrox.Assets.Subnautica/LanguageFiles/en.json b/Nitrox.Assets.Subnautica/LanguageFiles/en.json index 6a8f8e3821..146565dbea 100644 --- a/Nitrox.Assets.Subnautica/LanguageFiles/en.json +++ b/Nitrox.Assets.Subnautica/LanguageFiles/en.json @@ -64,7 +64,6 @@ "Nitrox_PlayerKicked": "You've been kicked from the server", "Nitrox_PlayerLeft": "{PLAYER} left the game.", "Nitrox_PlayerListTabName": "Player List", - "Nitrox_QueueInfo": "You are at position #{POSITION} in the join queue. Maximum possible wait: {TIME}", "Nitrox_RejectedSessionPolicy": "Reservation rejected…", "Nitrox_RemotePlayerObstacle": "Another player's either inside or too close to the deconstructable target.", "Nitrox_RequestingSessionPolicy": "Requesting session policy info…", From 220e1a6a688003bb0ec9f2810a4513cb05a0f6c7 Mon Sep 17 00:00:00 2001 From: SpaceMonkeyy86 Date: Thu, 13 Feb 2025 12:09:11 -0500 Subject: [PATCH 37/40] Suggested changes inside JoiningManager --- NitroxServer/GameLogic/JoiningManager.cs | 25 ++++++++++-------------- 1 file changed, 10 insertions(+), 15 deletions(-) diff --git a/NitroxServer/GameLogic/JoiningManager.cs b/NitroxServer/GameLogic/JoiningManager.cs index 74e82920bc..15e0e44107 100644 --- a/NitroxServer/GameLogic/JoiningManager.cs +++ b/NitroxServer/GameLogic/JoiningManager.cs @@ -43,6 +43,7 @@ private async Task JoinQueueLoop() while (true) { + // Necessary to avoid race conditions between JoinQueueLoop and AddToJoinQueue lock (queueLocker) { if (joinQueue.Count == 0) @@ -75,15 +76,10 @@ private async Task JoinQueueLoop() await Task.Run(() => { - while (true) + while (!syncFinished && + connection.State != NitroxConnectionState.Disconnected && + !source.IsCancellationRequested) { - if (syncFinished || - connection.State == NitroxConnectionState.Disconnected || - source.IsCancellationRequested) - { - return; - } - Task.Delay(10).Wait(); } }); @@ -111,14 +107,14 @@ await Task.Run(() => } catch (Exception e) { - Log.Error($"Unexpected error during player connection: {e}"); + Log.Error($"Unexpected error during player connection inside the join queue: {e}"); } } } public void AddToJoinQueue(INitroxConnection connection, string reservationKey) { - // Necessary to avoid race conditions between this method and the queue count check + // Necessary to avoid race conditions between JoinQueueLoop and AddToJoinQueue lock (queueLocker) { Log.Info($"Added player {playerManager.GetPlayerContext(reservationKey).PlayerName} to queue"); @@ -145,11 +141,10 @@ private void SendInitialSync(INitroxConnection connection, string reservationKey { IEnumerable GetOtherPlayers(Player player) { - return playerManager.GetConnectedPlayers().Where(p => p != player) - .Select(p => p.PlayerContext); + return playerManager.GetConnectedPlayers().Where(p => p != player).Select(p => p.PlayerContext); } - PlayerWorldEntity SetupPlayerEntity(Player player) + PlayerWorldEntity SetupNewPlayerEntity(Player player) { NitroxTransform transform = new(player.Position, player.Rotation, NitroxVector3.One); @@ -166,7 +161,7 @@ PlayerWorldEntity RespawnExistingEntity(Player player) return playerWorldEntity; } Log.Error($"Unable to find player entity for {player.Name}. Re-creating one"); - return SetupPlayerEntity(player); + return SetupNewPlayerEntity(player); } Player player = playerManager.PlayerConnected(connection, reservationKey, out bool wasBrandNewPlayer); @@ -187,7 +182,7 @@ PlayerWorldEntity RespawnExistingEntity(Player player) List simulations = world.EntitySimulation.AssignGlobalRootEntitiesAndGetData(player); - player.Entity = wasBrandNewPlayer ? SetupPlayerEntity(player) : RespawnExistingEntity(player); + player.Entity = wasBrandNewPlayer ? SetupNewPlayerEntity(player) : RespawnExistingEntity(player); List globalRootEntities = world.WorldEntityManager.GetGlobalRootEntities(true); bool isFirstPlayer = playerManager.GetConnectedPlayers().Count == 1; From bee00b13ad95fc1aefacb35fde9e7805b6882f4e Mon Sep 17 00:00:00 2001 From: SpaceMonkeyy86 Date: Thu, 13 Feb 2025 12:15:23 -0500 Subject: [PATCH 38/40] Use CodeMatcher::Repeat in transpiler --- .../ErrorMessage_OnLateUpdate_Patch.cs | 22 ++++++------------- 1 file changed, 7 insertions(+), 15 deletions(-) diff --git a/NitroxPatcher/Patches/Dynamic/ErrorMessage_OnLateUpdate_Patch.cs b/NitroxPatcher/Patches/Dynamic/ErrorMessage_OnLateUpdate_Patch.cs index ca19a2e3cd..69efe2183c 100644 --- a/NitroxPatcher/Patches/Dynamic/ErrorMessage_OnLateUpdate_Patch.cs +++ b/NitroxPatcher/Patches/Dynamic/ErrorMessage_OnLateUpdate_Patch.cs @@ -25,21 +25,13 @@ public sealed partial class ErrorMessage_OnLateUpdate_Patch : NitroxPatch, IDyna */ public static IEnumerable Transpiler(IEnumerable instructions) { - CodeMatcher matcher = new CodeMatcher(instructions); - - while (matcher.MatchStartForward(new CodeMatch(OpCodes.Call, TARGET_OPERAND_TIME)).IsValid) - { - matcher.SetOperandAndAdvance(INJECTION_OPERAND_UNSCALED_TIME); - } - - matcher.Start(); - - while (matcher.MatchStartForward(new CodeMatch(OpCodes.Call, TARGET_OPERAND_DELTA_TIME)).IsValid) - { - matcher.SetOperandAndAdvance(INJECTION_OPERAND_UNSCALED_DELTA_TIME); - } - - return matcher.InstructionEnumeration(); + return new CodeMatcher(instructions) + .MatchStartForward(new CodeMatch(OpCodes.Call, TARGET_OPERAND_TIME)) + .Repeat(matcher => matcher.SetOperandAndAdvance(INJECTION_OPERAND_UNSCALED_TIME)) + .Start() + .MatchStartForward(new CodeMatch(OpCodes.Call, TARGET_OPERAND_DELTA_TIME)) + .Repeat(matcher => matcher.SetOperandAndAdvance(INJECTION_OPERAND_UNSCALED_DELTA_TIME)) + .InstructionEnumeration(); } public override void Patch(Harmony harmony) From 7ec398a277a9e68f3c58f1d75ac5a3539279cb3b Mon Sep 17 00:00:00 2001 From: SpaceMonkeyy86 Date: Thu, 13 Feb 2025 12:16:53 -0500 Subject: [PATCH 39/40] Move comment to the correct line --- NitroxServer/GameLogic/JoiningManager.cs | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/NitroxServer/GameLogic/JoiningManager.cs b/NitroxServer/GameLogic/JoiningManager.cs index 15e0e44107..b59e7cac7f 100644 --- a/NitroxServer/GameLogic/JoiningManager.cs +++ b/NitroxServer/GameLogic/JoiningManager.cs @@ -24,7 +24,7 @@ public sealed class JoiningManager private readonly World world; private ThreadSafeQueue<(INitroxConnection, string)> joinQueue { get; } = new(); - private readonly Lock queueLocker = new(); + private readonly Lock queueLocker = new(); // Necessary to avoid race conditions between JoinQueueLoop and AddToJoinQueue private bool queueActive; public Action SyncFinishedCallback { get; private set; } @@ -43,7 +43,6 @@ private async Task JoinQueueLoop() while (true) { - // Necessary to avoid race conditions between JoinQueueLoop and AddToJoinQueue lock (queueLocker) { if (joinQueue.Count == 0) From 004cce47e7d0418dc295cdb93419f7fae339e244 Mon Sep 17 00:00:00 2001 From: SpaceMonkeyy86 Date: Thu, 13 Feb 2025 21:15:01 -0500 Subject: [PATCH 40/40] Remove partial keyword --- .../Patches/Dynamic/ErrorMessage_OnLateUpdate_Patch.cs | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/NitroxPatcher/Patches/Dynamic/ErrorMessage_OnLateUpdate_Patch.cs b/NitroxPatcher/Patches/Dynamic/ErrorMessage_OnLateUpdate_Patch.cs index 69efe2183c..6418a850b1 100644 --- a/NitroxPatcher/Patches/Dynamic/ErrorMessage_OnLateUpdate_Patch.cs +++ b/NitroxPatcher/Patches/Dynamic/ErrorMessage_OnLateUpdate_Patch.cs @@ -3,13 +3,14 @@ using System.Reflection.Emit; using HarmonyLib; using NitroxModel.Helper; -using NitroxPatcher.Patches; using UnityEngine; +namespace NitroxPatcher.Patches.Dynamic; + /// /// Ensures the in-game log is animated smoothly regardless of the time scale. /// -public sealed partial class ErrorMessage_OnLateUpdate_Patch : NitroxPatch, IDynamicPatch +public sealed class ErrorMessage_OnLateUpdate_Patch : NitroxPatch, IDynamicPatch { private static readonly MethodInfo TARGET_METHOD_ON_LATE_UPDATE = Reflect.Method((ErrorMessage t) => t.OnLateUpdate()); private static readonly MethodInfo TARGET_METHOD_ADD_MESSAGE = Reflect.Method((ErrorMessage t) => t._AddMessage(default));