Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Join queue despaghettification #2006

Open
wants to merge 36 commits into
base: master
Choose a base branch
from
Open
Show file tree
Hide file tree
Changes from 32 commits
Commits
Show all changes
36 commits
Select commit Hold shift + click to select a range
b4e462a
Draft 1
SpaceMonkeyy86 Dec 6, 2022
2521bac
Finished join queue loop
SpaceMonkeyy86 Dec 8, 2022
d6404e6
Enter join queue after base game finishes loading
SpaceMonkeyy86 Dec 8, 2022
02ee6c6
Changed OnLateUpdate to UpdatePings (#1913)
Coding-Hen Dec 10, 2022
e43c176
Merge master
SpaceMonkeyy86 Jan 10, 2023
f161aac
Merge master (again)
SpaceMonkeyy86 Mar 18, 2023
4cb25d9
Move initial sync code to the new spot after the merge reset it
SpaceMonkeyy86 Mar 18, 2023
eddd834
Fix broken in-game log for join queue stage
SpaceMonkeyy86 Mar 18, 2023
5dc17a9
Use WaitScreen to stay on loading screen
SpaceMonkeyy86 Mar 18, 2023
1717850
Reduce default initial sync timeout
SpaceMonkeyy86 Mar 18, 2023
ef14264
Merge branch 'master' into join-queue-fix
SpaceMonkeyy86 Mar 22, 2023
86010a6
Remove unnecessary using
SpaceMonkeyy86 Apr 15, 2023
d613ec9
Merge master (this is so not going to work)
SpaceMonkeyy86 Jan 2, 2024
17e9c09
It almost worked (it works now)
SpaceMonkeyy86 Jan 2, 2024
5c9ffef
Show modal when timed out
SpaceMonkeyy86 Feb 7, 2024
5fd6aa5
Merge master
SpaceMonkeyy86 Feb 7, 2024
932edb9
Tweaks and fixes
SpaceMonkeyy86 Feb 8, 2024
708c8cf
Instantly continue the queue if a client disconnects while syncing
SpaceMonkeyy86 Feb 9, 2024
2eb654a
Send info about the queue to the client
SpaceMonkeyy86 Feb 9, 2024
1b5920f
Remove join queue "lobby"
SpaceMonkeyy86 Feb 12, 2024
708f753
Some cleanups
SpaceMonkeyy86 Feb 12, 2024
5d618c7
Merge branch 'master' into join-queue-fix
SpaceMonkeyy86 Feb 12, 2024
34b2c18
Revert a file that wasn't actually modified
SpaceMonkeyy86 Feb 12, 2024
906d1c6
I love merge conflicts
SpaceMonkeyy86 Mar 20, 2024
131bac7
Merge branch 'master' into join-queue-fix
SpaceMonkeyy86 Aug 14, 2024
eb1ce78
Fix another conflict (did not test but it's probably fine)
SpaceMonkeyy86 Sep 2, 2024
099f8f2
Fix merge conflicts
SpaceMonkeyy86 Nov 12, 2024
201d221
Merge branch 'join-queue-fix' of https://github.com/TwinBuilderOne/Ni…
SpaceMonkeyy86 Nov 12, 2024
a8f742f
Merge branch 'master' into join-queue-fix
SpaceMonkeyy86 Nov 16, 2024
76e5a9d
Merge branch 'master' into join-queue-fix
SpaceMonkeyy86 Jan 7, 2025
efba962
Apply Jannify's patch
SpaceMonkeyy86 Jan 7, 2025
ef9d8d5
Small tweaks
SpaceMonkeyy86 Jan 7, 2025
a1cf75b
Fix DI for JoiningManager
SpaceMonkeyy86 Jan 7, 2025
4de4fa7
Handle case where client receives a timeout after finishing the sync
SpaceMonkeyy86 Jan 7, 2025
8043c13
Remove thread unsafety when a joining player disconnects
SpaceMonkeyy86 Jan 12, 2025
95fc884
Some cleanup
SpaceMonkeyy86 Jan 12, 2025
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
using System;
using System;
using System.Threading.Tasks;
using NitroxClient.Communication.Abstract;
using NitroxClient.Communication.MultiplayerSession.ConnectionState;
Expand Down Expand Up @@ -82,28 +82,13 @@ 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.Info("Waiting in join queue…");
Log.InGame(Language.main.Get("Nitrox_Waiting"));
return;
}

PlayerSettings = playerSettings;
AuthenticationContext = authenticationContext;
CurrentState.NegotiateReservationAsync(this);
}

public void ProcessReservationResponsePacket(MultiplayerSessionReservation reservation)
{
if (reservation.ReservationState == MultiplayerSessionReservationState.ENQUEUED_IN_JOIN_QUEUE)
{
Log.Info("Waiting in join queue…");
Log.InGame(Language.main.Get("Nitrox_Waiting"));
return;
}

Reservation = reservation;
CurrentState.NegotiateReservationAsync(this);
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -13,7 +13,7 @@ public class InitialPlayerSyncProcessor : ClientPacketProcessor<InitialPlayerSyn
{
private readonly IPacketSender packetSender;
private readonly HashSet<IInitialSyncProcessor> processors;
private readonly HashSet<Type> alreadyRan = new();
private readonly HashSet<Type> alreadyRan = [];
private InitialPlayerSync packet;

private WaitScreen.ManualWaitItem loadingMultiplayerWaitItem;
Expand All @@ -31,7 +31,10 @@ public InitialPlayerSyncProcessor(IPacketSender packetSender, IEnumerable<IIniti
public override void Process(InitialPlayerSync packet)
{
this.packet = packet;

loadingMultiplayerWaitItem = WaitScreen.Add(Language.main.Get("Nitrox_SyncingWorld"));
Log.InGame(Language.main.Get("Nitrox_SyncingWorld"));

cumulativeProcessorsRan = 0;
Multiplayer.Main.StartCoroutine(ProcessInitialSyncPacket(this, null));
}
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,24 @@
using System;
using NitroxClient.Communication.Packets.Processors.Abstract;
using NitroxModel.Packets;

namespace NitroxClient.Communication.Packets.Processors;

public class JoinQueueInfoProcessor : ClientPacketProcessor<JoinQueueInfo>
{
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.");
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Would use something like this:

Suggested change
Log.InGame($"The maximum wait time per person is {MillisToMinutes(packet.Timeout)} minutes.");
TimeSpan time = TimeSpan.FromMilliseconds(packet.Timeout);
Log.InGame($"The maximum wait time per person is {time.ToString(@"mm\:ss")}.");

}
}

private static string MillisToMinutes(int milliseconds)
{
double minutes = milliseconds / 60000.0;
return Math.Round(minutes, 1).ToString();
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I find that this could be misleading, why would we not just have it in seconds or a combination of both. If the milliseconds came out to 20 seconds we would round it to 0.3 which would require the user to calculate what this actually means

}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,25 @@
using NitroxClient.Communication.Abstract;
using NitroxClient.Communication.Packets.Processors.Abstract;
using NitroxClient.MonoBehaviours;
using NitroxModel.Packets;

namespace NitroxClient.Communication.Packets.Processors;

public class PlayerSyncTimeoutProcessor : ClientPacketProcessor<PlayerSyncTimeout>
{
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();
SpaceMonkeyy86 marked this conversation as resolved.
Show resolved Hide resolved
}
}
24 changes: 22 additions & 2 deletions NitroxClient/MonoBehaviours/Multiplayer.cs
Original file line number Diff line number Diff line change
Expand Up @@ -34,6 +34,7 @@ public class Multiplayer : MonoBehaviour
private GameLogic.Terrain terrain;

public bool InitialSyncCompleted { get; set; }
public bool TimedOut { get; set; }

/// <summary>
/// True if multiplayer is loaded and client is connected to a server.
Expand Down Expand Up @@ -93,6 +94,7 @@ public static void SubnauticaLoadingCompleted()
if (Active)
{
Main.InitialSyncCompleted = false;
Main.TimedOut = false;
Main.StartCoroutine(LoadAsync());
}
else
Expand All @@ -113,11 +115,29 @@ 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)
{
int timer = 5;

while (timer > 0)
SpaceMonkeyy86 marked this conversation as resolved.
Show resolved Hide resolved
{
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;
}
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I'd replace that with simply a modal notifying the user of the time out (with option freeze so that the game doesn't play behind) with a QuitGame on callback


SetLoadingComplete();
OnLoadingComplete?.Invoke();
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
18 changes: 18 additions & 0 deletions NitroxModel/Packets/JoinQueueInfo.cs
Original file line number Diff line number Diff line change
@@ -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; }
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Why would we sometimes not show the maximum wait time ?

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

We show the maximum wait time per person when the player enters the queue. This information never changes so there is no need to show it again.


public JoinQueueInfo(int position, int timeout, bool showMaximumWait)
{
Position = position;
Timeout = timeout;
ShowMaximumWait = showMaximumWait;
}
}
6 changes: 6 additions & 0 deletions NitroxModel/Packets/PlayerSyncTimeout.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,6 @@
using System;

namespace NitroxModel.Packets;

[Serializable]
public class PlayerSyncTimeout : Packet { }
2 changes: 1 addition & 1 deletion NitroxServer/Communication/LiteNetLib/LiteNetLibServer.cs
Original file line number Diff line number Diff line change
Expand Up @@ -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);
Expand Down
31 changes: 16 additions & 15 deletions NitroxServer/Communication/NitroxServer.cs
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
using System;
using System;
using System.Collections.Generic;
using NitroxModel.DataStructures;
using NitroxModel.Packets;
Expand All @@ -25,11 +25,13 @@ static NitroxServer()
protected readonly EntitySimulation entitySimulation;
protected readonly Dictionary<int, INitroxConnection> 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;
Expand All @@ -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<SimulatedEntity> 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<SimulatedEntity> ownershipChanges = entitySimulation.CalculateSimulationChangesFromPlayerDisconnect(player);

if (ownershipChanges.Count > 0)
{
playerManager.NonPlayerDisconnected(connection);
SimulationOwnershipChange ownershipChange = new(ownershipChanges);
playerManager.SendPacketToAllPlayers(ownershipChange);
}
}

Expand Down
15 changes: 9 additions & 6 deletions NitroxServer/Communication/Packets/PacketHandler.cs
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
using System;
using System;
using System.Collections.Generic;
using System.Linq;
using NitroxModel.Core;
using NitroxModel.Packets;
using NitroxModel.Packets.Processors.Abstract;
Expand All @@ -12,26 +13,28 @@ namespace NitroxServer.Communication.Packets
public class PacketHandler
{
private readonly PlayerManager playerManager;
private readonly JoiningManager joiningManager;
private readonly DefaultServerPacketProcessor defaultServerPacketProcessor;
private readonly Dictionary<Type, PacketProcessor> packetProcessorAuthCache = new();
private readonly Dictionary<Type, PacketProcessor> packetProcessorUnauthCache = new();

public PacketHandler(PlayerManager playerManager, DefaultServerPacketProcessor packetProcessor)
public PacketHandler(PlayerManager playerManager, JoiningManager joiningManager, DefaultServerPacketProcessor packetProcessor)
{
this.playerManager = playerManager;
this.joiningManager = joiningManager;
defaultServerPacketProcessor = packetProcessor;
}

public void Process(Packet packet, INitroxConnection connection)
{
Player player = playerManager.GetPlayer(connection);
if (player == null)
if (player != null)
{
ProcessUnauthenticated(packet, connection);
ProcessAuthenticated(packet, player);
}
else
else if (!joiningManager.GetQueuedPlayers().Contains(connection))
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Isn't it too harsh on the connection ? o(n) on packet receiving (a lot per second) might be a load too bad for the server

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I highly doubt this could cause performance problems since it only runs for unauthenticated packets (sent very infrequently compared to normal packets) and there will realistically never be more than a few people in the queue at once. It's best to use a profiler to confirm that a piece of code is actually causing a bottleneck before trying to optimize it.

Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

When a player is disconnected from the server POV but not on their end, they just send all their packet as unauthenticated packets which will just spam this else condition. This else conditions uses GetQueuedPlayers which creates a new list instance each time (o(n) + memory impact), and uses Contains which is also o(n)

{
ProcessAuthenticated(packet, player);
ProcessUnauthenticated(packet, connection);
}
}

Expand Down
Loading