From 0dc522413efb1a028d41d4d3664578a5e7b7c7c4 Mon Sep 17 00:00:00 2001 From: FloPinguin <25036848+FloPinguin@users.noreply.github.com> Date: Tue, 24 Mar 2026 21:00:20 +0100 Subject: [PATCH] =?UTF-8?q?Close=20private=20lobby=20when=20host=20leaves?= =?UTF-8?q?=20=F0=9F=9A=AA=20(#3503)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit ## Description: When the host of a private lobby disconnects before the game starts, the lobby is now closed: - **Server:** Detects host disconnection via `creatorPersistentID` match, kicks all remaining clients with a `host_left` reason, and marks the game as ended so it's cleaned up by the game manager and can no longer be joined. - **Client:** Participants receive an `alert()` saying "The host has left the lobby." and their JoinLobbyModal is closed automatically via the `leave-lobby` event. - **Phase logic:** `phase()` now returns `Finished` for ended private lobbies that haven't started, preventing the game from lingering in `Lobby` state. ## Please complete the following: - [X] I have added screenshots for all UI updates - [X] I process any text displayed to the user through translateText() and I've added it to the en.json file - [X] I have added relevant tests to the test directory - [X] I confirm I have thoroughly tested these changes and take full responsibility for any bugs introduced ## Please put your Discord username so you can be contacted if a bug or regression is found: FloPinguin --- resources/lang/en.json | 3 ++- src/client/ClientGameRunner.ts | 9 +++++++++ src/server/GameServer.ts | 17 +++++++++++++++++ 3 files changed, 28 insertions(+), 1 deletion(-) diff --git a/resources/lang/en.json b/resources/lang/en.json index 2fbb59b57..8e7bf662d 100644 --- a/resources/lang/en.json +++ b/resources/lang/en.json @@ -863,7 +863,8 @@ }, "kick_reason": { "duplicate_session": "Kicked from game (you may have been playing on another tab)", - "lobby_creator": "Kicked by lobby creator" + "lobby_creator": "Kicked by lobby creator", + "host_left": "The host has left the lobby." }, "send_troops_modal": { "title_with_name": "Send Troops to {name}", diff --git a/src/client/ClientGameRunner.ts b/src/client/ClientGameRunner.ts index 55946dce0..aefbac8ed 100644 --- a/src/client/ClientGameRunner.ts +++ b/src/client/ClientGameRunner.ts @@ -172,6 +172,15 @@ export function joinLobby( composed: true, }), ); + } else if (message.error === "kick_reason.host_left") { + alert(translateText("kick_reason.host_left")); + document.dispatchEvent( + new CustomEvent("leave-lobby", { + detail: { lobby: lobbyConfig.gameID, cause: "host-left" }, + bubbles: true, + composed: true, + }), + ); } else { showErrorModal( message.error, diff --git a/src/server/GameServer.ts b/src/server/GameServer.ts index da724214c..f6ac05e76 100644 --- a/src/server/GameServer.ts +++ b/src/server/GameServer.ts @@ -35,6 +35,7 @@ export enum GamePhase { const KICK_REASON_DUPLICATE_SESSION = "kick_reason.duplicate_session"; const KICK_REASON_LOBBY_CREATOR = "kick_reason.lobby_creator"; +const KICK_REASON_HOST_LEFT = "kick_reason.host_left"; const KICK_REASON_TOO_MUCH_DATA = "kick_reason.too_much_data"; const KICK_REASON_INVALID_MESSAGE = "kick_reason.invalid_message"; @@ -542,6 +543,20 @@ export class GameServer { this.activeClients = this.activeClients.filter( (c) => c.clientID !== client.clientID, ); + // Close lobby when host leaves before game starts + if ( + !this._hasStarted && + !this.isPublic() && + client.persistentID === this.creatorPersistentID + ) { + this.log.info("Host left, closing lobby", { + gameID: this.id, + }); + for (const c of [...this.activeClients]) { + this.kickClient(c.clientID, KICK_REASON_HOST_LEFT); + } + this._hasEnded = true; + } }); client.ws.on("error", (error: Error) => { if ((error as any).code === "WS_ERR_UNEXPECTED_RSV_1") { @@ -847,6 +862,8 @@ export class GameServer { } else { return GamePhase.Active; } + } else if (this._hasEnded) { + return GamePhase.Finished; } else { return GamePhase.Lobby; }