Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
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
6 changes: 6 additions & 0 deletions Source/Common/PlayerManager.cs
Original file line number Diff line number Diff line change
Expand Up @@ -71,6 +71,12 @@ public void SetDisconnected(ConnectionBase conn, MpDisconnectReason reason)
ServerPlayer player = conn.serverPlayer;
Players.Remove(player);

if (player.IsHost && server.worldData.CreatingJoinPoint)
{
server.worldData.AbortJoinPointCreation();
ServerLog.Log("Aborted join point creation because the host disconnected.");
Comment on lines 72 to +77
Copy link

Copilot AI Apr 7, 2026

Choose a reason for hiding this comment

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

SetDisconnected aborts join point creation only when the disconnected player is IsHost. In arbiter mode, join point creation uploads are accepted only from the arbiter (Server.ArbiterPlaying ? Player.IsArbiter : Player.IsHost in ServerPlayingState.HandleWorldDataUpload), so if the arbiter disconnects during an in-progress join point the server can still remain stuck in CreatingJoinPoint and keep ServerLoadingState waiters blocked. Consider aborting when the disconnecting player is the current join point upload source (host vs arbiter), and note that you may need to capture the arbiter/host mode before Players.Remove(player) so the decision isn't affected by the removal.

Suggested change
Players.Remove(player);
if (player.IsHost && server.worldData.CreatingJoinPoint)
{
server.worldData.AbortJoinPointCreation();
ServerLog.Log("Aborted join point creation because the host disconnected.");
bool arbiterPlaying = server.ArbiterPlaying;
bool isJoinPointUploadSource = arbiterPlaying ? player.IsArbiter : player.IsHost;
Players.Remove(player);
if (isJoinPointUploadSource && server.worldData.CreatingJoinPoint)
{
server.worldData.AbortJoinPointCreation();
ServerLog.Log($"Aborted join point creation because the {(arbiterPlaying ? "arbiter" : "host")} disconnected.");

Copilot uses AI. Check for mistakes.
}

if (player.hasJoined)
{
// Send PlayerCount command to remove the player from their last known map
Expand Down
11 changes: 11 additions & 0 deletions Source/Common/WorldData.cs
Original file line number Diff line number Diff line change
Expand Up @@ -52,6 +52,17 @@ public void EndJoinPointCreation()
tmpMapCmds = null;
lastJoinPointAtWorkTicks = Server.workTicks;
dataSource!.SetResult(this);
dataSource = null;
}

public void AbortJoinPointCreation()
{
if (!CreatingJoinPoint)
return;

tmpMapCmds = null;
dataSource?.SetResult(this);
dataSource = null;
}

public Task<WorldData> WaitJoinPoint()
Expand Down
25 changes: 25 additions & 0 deletions Source/Tests/ServerTest.cs
Original file line number Diff line number Diff line change
Expand Up @@ -45,6 +45,31 @@ public void Test()
}
}

[Test]
public async Task JoinPointAbortUnblocksWaiters()
{
var server = new MultiplayerServer(new ServerSettings
{
gameName = "Test",
direct = false,
lan = false
});

Assert.That(server.worldData.TryStartJoinPointCreation(true), Is.True);
Assert.That(server.worldData.CreatingJoinPoint, Is.True);

var waitTask = server.worldData.WaitJoinPoint();
Assert.That(waitTask.IsCompleted, Is.False);

server.worldData.AbortJoinPointCreation();

var completed = await waitTask.WaitAsync(TimeSpan.FromSeconds(1));

Assert.That(completed, Is.SameAs(server.worldData));
Assert.That(server.worldData.CreatingJoinPoint, Is.False);
Assert.That(server.worldData.WaitJoinPoint().IsCompleted, Is.True);
}

private void ConnectClient(int port)
{
var clientListener = new TestNetListener();
Expand Down
Loading