From f23c50784e51f34e90e1b1e77be7f0862052abf0 Mon Sep 17 00:00:00 2001 From: Mattia Migliorini Date: Sat, 3 Jan 2026 10:56:34 +0100 Subject: [PATCH 1/5] Handle confirmation on popstate event if player is active in a game --- src/client/ClientGameRunner.ts | 36 +++++++++++++++++++++++--- src/client/Main.ts | 46 ++++++++++++++++++++++++++++++---- 2 files changed, 74 insertions(+), 8 deletions(-) diff --git a/src/client/ClientGameRunner.ts b/src/client/ClientGameRunner.ts index 27c2f01cbd..3568f4edb3 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 = () => { @@ -121,7 +123,10 @@ export function joinLobby( userSettings, terrainLoad, terrainMapFileLoader, - ).then((r) => r.start()); + ).then((r) => { + currentGameRunner = r; + r.start(); + }); } if (message.type === "error") { if (message.error === "full-lobby") { @@ -146,9 +151,19 @@ export function joinLobby( } }; transport.connect(onconnect, onmessage); - return () => { + return (force: boolean = false) => { + if (!force && currentGameRunner && currentGameRunner.shouldPreventWindowClose()) { + console.log('Player is active, prevent leaving game'); + + return false; + } + console.log("leaving game"); + + currentGameRunner = null; transport.leaveGame(); + + return true; }; } @@ -237,6 +252,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 && 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 58dffff725..6ebe1a620d 100644 --- a/src/client/Main.ts +++ b/src/client/Main.ts @@ -43,7 +43,7 @@ import { import { UserSettingModal } from "./UserSettingModal"; import "./UsernameInput"; import { UsernameInput } from "./UsernameInput"; -import { incrementGamesPlayed, isInIframe } from "./Utils"; +import { incrementGamesPlayed, isInIframe, translateText } from "./Utils"; import "./components/baseComponents/Button"; import "./components/baseComponents/Modal"; import "./styles.css"; @@ -103,9 +103,12 @@ 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 preventHashUpdate: boolean = false; + private usernameInput: UsernameInput | null = null; private flagInput: FlagInput | null = null; private darkModeButton: DarkModeButton | null = null; @@ -177,7 +180,7 @@ class Client { window.addEventListener("beforeunload", async () => { console.log("Browser is closing"); if (this.gameStop !== null) { - this.gameStop(); + this.gameStop(true); await crazyGamesSDK.gameplayStop(); } }); @@ -368,6 +371,12 @@ class Client { this.handleUrl(); const onHashUpdate = () => { + if (this.preventHashUpdate) { + this.preventHashUpdate = false; + + return; + } + // Reset the UI to its initial state this.joinModal.close(); if (this.gameStop !== null) { @@ -378,8 +387,31 @@ class Client { this.handleUrl(); }; + const onPopState = () => { + if (this.currentUrl !== null && this.gameStop && !this.gameStop()) { + console.info("Player is active, ask before leaving game"); + + const isConfirmed = confirm( + translateText("help_modal.exit_confirmation"), + ); + + if (!isConfirmed) { + console.debug("Player denied leaving game, restore navigator history"); + + // Rollback navigator history + this.preventHashUpdate = true; + history.pushState(null, "", this.currentUrl); + return; + } + } + + console.info("Player not active, handle hash update"); + + onHashUpdate(); + }; + // Handle browser navigation & manual hash edits - window.addEventListener("popstate", onHashUpdate); + window.addEventListener("popstate", onPopState); window.addEventListener("hashchange", onHashUpdate); function updateSliderProgress(slider: HTMLInputElement) { @@ -604,6 +636,9 @@ class Client { history.replaceState(null, "", window.location.origin + "#refresh"); } history.pushState(null, "", `#join=${lobby.gameID}`); + + // Store current URL for popstate confirmation + this.currentUrl = window.location.href; }, ); } @@ -613,8 +648,9 @@ class Client { return; } console.log("leaving lobby, cancelling game"); - this.gameStop(); + this.gameStop(true); this.gameStop = null; + this.currentUrl = null; crazyGamesSDK.gameplayStop(); From 2370e549c720c7aee90997f26681d8922dac93d2 Mon Sep 17 00:00:00 2001 From: Mattia Migliorini Date: Sat, 3 Jan 2026 11:15:12 +0100 Subject: [PATCH 2/5] Apply prettier reformat --- src/client/ClientGameRunner.ts | 8 ++++++-- src/client/Main.ts | 4 +++- 2 files changed, 9 insertions(+), 3 deletions(-) diff --git a/src/client/ClientGameRunner.ts b/src/client/ClientGameRunner.ts index 3568f4edb3..97f62530de 100644 --- a/src/client/ClientGameRunner.ts +++ b/src/client/ClientGameRunner.ts @@ -152,8 +152,12 @@ export function joinLobby( }; transport.connect(onconnect, onmessage); return (force: boolean = false) => { - if (!force && currentGameRunner && currentGameRunner.shouldPreventWindowClose()) { - console.log('Player is active, prevent leaving game'); + if ( + !force && + currentGameRunner && + currentGameRunner.shouldPreventWindowClose() + ) { + console.log("Player is active, prevent leaving game"); return false; } diff --git a/src/client/Main.ts b/src/client/Main.ts index 6ebe1a620d..6427c5d8f0 100644 --- a/src/client/Main.ts +++ b/src/client/Main.ts @@ -396,7 +396,9 @@ class Client { ); if (!isConfirmed) { - console.debug("Player denied leaving game, restore navigator history"); + console.debug( + "Player denied leaving game, restore navigator history", + ); // Rollback navigator history this.preventHashUpdate = true; From 13b25656d4139ccbb1577b45bb1d45c0e55aafa0 Mon Sep 17 00:00:00 2001 From: Mattia Migliorini Date: Mon, 5 Jan 2026 08:55:34 +0100 Subject: [PATCH 3/5] Fix review comments --- src/client/ClientGameRunner.ts | 8 ++------ 1 file changed, 2 insertions(+), 6 deletions(-) diff --git a/src/client/ClientGameRunner.ts b/src/client/ClientGameRunner.ts index 97f62530de..b9b11578d9 100644 --- a/src/client/ClientGameRunner.ts +++ b/src/client/ClientGameRunner.ts @@ -152,11 +152,7 @@ export function joinLobby( }; transport.connect(onconnect, onmessage); return (force: boolean = false) => { - if ( - !force && - currentGameRunner && - currentGameRunner.shouldPreventWindowClose() - ) { + if (!force && currentGameRunner?.shouldPreventWindowClose()) { console.log("Player is active, prevent leaving game"); return false; @@ -268,7 +264,7 @@ export class ClientGameRunner { */ public shouldPreventWindowClose(): boolean { // Show confirmation dialog if player is alive in the game - return !!(this.myPlayer && this.myPlayer.isAlive()); + return !!this.myPlayer?.isAlive(); } private async saveGame(update: WinUpdate) { From 9a1e61e2d5eb65ebdc782d056dd615292c2ad50b Mon Sep 17 00:00:00 2001 From: Mattia Migliorini Date: Thu, 8 Jan 2026 14:33:14 +0100 Subject: [PATCH 4/5] Force stop previous game when joining a new one --- src/client/Main.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/client/Main.ts b/src/client/Main.ts index d6b82cae5c..9450872f7d 100644 --- a/src/client/Main.ts +++ b/src/client/Main.ts @@ -540,7 +540,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); } const config = await getServerConfigFromClient(); From fef4bcae7bd6a918aedb3d3123d46b8abf27223a Mon Sep 17 00:00:00 2001 From: Mattia Migliorini Date: Sat, 10 Jan 2026 11:03:43 +0100 Subject: [PATCH 5/5] Reformat code --- src/client/Main.ts | 1 - 1 file changed, 1 deletion(-) diff --git a/src/client/Main.ts b/src/client/Main.ts index 5856ff1728..ad8339132d 100644 --- a/src/client/Main.ts +++ b/src/client/Main.ts @@ -511,7 +511,6 @@ class Client { // Attempt to join lobby this.handleUrl(); - const onHashUpdate = () => { // Prevent double-handling when both popstate and hashchange fire if (this.preventHashUpdate) {