From b1d63533d54ea7aab01c56e893615e5b2dcc5974 Mon Sep 17 00:00:00 2001 From: Mattia Migliorini Date: Sat, 17 Jan 2026 02:09:08 +0100 Subject: [PATCH] Handle confirmation on popstate event if player is active in a game (#2777) Please merge it into v29, since in that version the back navigation out of a game is currently **broken** after switching from hash-based to path-based routing via #2740 ## Description: Protect against players accidentally leaving an active game by pressing the browser back button. Uses the same confirmation dialog as the game exit button. Partially handles issue #1877 (protects against back button, not closing tab or editing the URL directly). image Partial credit to PR #2141 ## 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: deshack_82603 --- src/client/ClientGameRunner.ts | 39 +++++++++++++++++++++-- src/client/Main.ts | 58 ++++++++++++++++++++++++---------- 2 files changed, 78 insertions(+), 19 deletions(-) diff --git a/src/client/ClientGameRunner.ts b/src/client/ClientGameRunner.ts index 995decabe..971a9cd71 100644 --- a/src/client/ClientGameRunner.ts +++ b/src/client/ClientGameRunner.ts @@ -69,7 +69,7 @@ export function joinLobby( lobbyConfig: LobbyConfig, onPrestart: () => void, onJoin: () => void, -): () => void { +): (force?: boolean) => boolean { console.log( `joining lobby: gameID: ${lobbyConfig.gameID}, clientID: ${lobbyConfig.clientID}`, ); @@ -79,6 +79,8 @@ export function joinLobby( const transport = new Transport(lobbyConfig, eventBus); + let currentGameRunner: ClientGameRunner | null = null; + let hasJoined = false; const onconnect = () => { @@ -122,9 +124,15 @@ export function joinLobby( terrainLoad, terrainMapFileLoader, ) - .then((r) => r.start()) + .then((r) => { + currentGameRunner = r; + r.start(); + }) .catch((e) => { console.error("error creating client game", e); + + currentGameRunner = null; + const startingModal = document.querySelector( "game-starting-modal", ) as HTMLElement; @@ -165,9 +173,19 @@ export function joinLobby( } }; transport.connect(onconnect, onmessage); - return () => { + return (force: boolean = false) => { + if (!force && currentGameRunner?.shouldPreventWindowClose()) { + console.log("Player is active, prevent leaving game"); + + return false; + } + console.log("leaving game"); + + currentGameRunner = null; transport.leaveGame(); + + return true; }; } @@ -256,6 +274,21 @@ export class ClientGameRunner { this.lastMessageTime = Date.now(); } + /** + * Determines whether window closing should be prevented. + * + * Used to show a confirmation dialog when the user attempts to close + * the window or navigate away during an active game session. + * + * @returns {boolean} `true` if the window close should be prevented + * (when the player is alive in the game), `false` otherwise + * (when the player is not alive or doesn't exist) + */ + public shouldPreventWindowClose(): boolean { + // Show confirmation dialog if player is alive in the game + return !!this.myPlayer?.isAlive(); + } + private async saveGame(update: WinUpdate) { if (this.myPlayer === null) { return; diff --git a/src/client/Main.ts b/src/client/Main.ts index 88fe223f4..8d1df2004 100644 --- a/src/client/Main.ts +++ b/src/client/Main.ts @@ -208,9 +208,11 @@ export interface JoinLobbyEvent { } class Client { - private gameStop: (() => void) | null = null; + private gameStop: ((force?: boolean) => boolean) | null = null; private eventBus: EventBus = new EventBus(); + private currentUrl: string | null = null; + private usernameInput: UsernameInput | null = null; private flagInput: FlagInput | null = null; @@ -277,7 +279,7 @@ class Client { window.addEventListener("beforeunload", async () => { console.log("Browser is closing"); if (this.gameStop !== null) { - this.gameStop(); + this.gameStop(true); await crazyGamesSDK.gameplayStop(); } }); @@ -583,15 +585,7 @@ class Client { // Attempt to join lobby this.handleUrl(); - let preventHashUpdate = false; - const onHashUpdate = () => { - // Prevent double-handling when both popstate and hashchange fire - if (preventHashUpdate) { - preventHashUpdate = false; - return; - } - // Reset the UI to its initial state this.joinModal?.close(); if (this.gameStop !== null) { @@ -602,11 +596,39 @@ class Client { this.handleUrl(); }; + const onPopState = () => { + if (this.currentUrl !== null && this.gameStop !== null) { + console.info("Game is active"); + + if (!this.gameStop()) { + console.info("Player is active, ask before leaving game"); + + const isConfirmed = confirm( + translateText("help_modal.exit_confirmation"), + ); + + if (!isConfirmed) { + // Rollback navigator history + history.pushState(null, "", this.currentUrl); + return; + } + } + + console.info("Player is not active, leave the game immediately"); + + crazyGamesSDK.gameplayStop().then(() => { + // redirect to the home page + window.location.href = "/"; + }); + } else { + console.info("Game not active, handle hash update"); + + onHashUpdate(); + } + }; + // Handle browser navigation & manual hash edits - window.addEventListener("popstate", () => { - preventHashUpdate = true; - onHashUpdate(); - }); + window.addEventListener("popstate", onPopState); window.addEventListener("hashchange", onHashUpdate); window.addEventListener("join-changed", onHashUpdate); @@ -738,7 +760,7 @@ class Client { console.log(`joining lobby ${lobby.gameID}`); if (this.gameStop !== null) { console.log("joining lobby, stopping existing game"); - this.gameStop(); + this.gameStop(true); document.body.classList.remove("in-game"); } const config = await getServerConfigFromClient(); @@ -844,6 +866,9 @@ class Client { "", `/${config.workerPath(lobby.gameID)}/game/${lobby.gameID}?live`, ); + + // Store current URL for popstate confirmation + this.currentUrl = window.location.href; }, ); } @@ -865,8 +890,9 @@ class Client { return; } console.log("leaving lobby, cancelling game"); - this.gameStop(); + this.gameStop(true); this.gameStop = null; + this.currentUrl = null; document.body.classList.remove("in-game");