From a8b7df48a6fc76f732590b78f04ad2272aabdec9 Mon Sep 17 00:00:00 2001 From: cottongin Date: Fri, 20 Mar 2026 17:55:07 -0400 Subject: [PATCH] fix: restore lobby state on refresh, handle game.status heartbeat - Extract maxPlayers from game object in #applyGameAdded so the meter works immediately when a game is added - Read playerName field in lobby.player-joined (matches API payload) - Handle game.status 20s heartbeat to keep overlay in sync - Restore in-progress game on page refresh using status-live endpoint for full shard state including player names Made-with: Cursor --- js/state-manager.js | 26 ++++++++- js/websocket-client.js | 124 ++++++++++++++++++++++++++++++++++++----- 2 files changed, 133 insertions(+), 17 deletions(-) diff --git a/js/state-manager.js b/js/state-manager.js index cc65702..35248cc 100644 --- a/js/state-manager.js +++ b/js/state-manager.js @@ -217,6 +217,15 @@ export class OverlayManager { this.#transitionTo('idle'); break; + case 'game.status': + this.#applyGameStatus(d); + if (this.#state === 'idle' && d.gameState === 'Lobby') { + this.#transitionTo('lobby'); + } else { + this.#broadcastUpdate(); + } + break; + case 'player-count.updated': this.#applyPlayerCountUpdated(d); this.#broadcastUpdate(); @@ -286,6 +295,8 @@ export class OverlayManager { const code = game.room_code ?? game.roomCode ?? game.code; if (code != null) this.#context.roomCode = String(code); + const mp = game.max_players ?? game.maxPlayers; + if (mp != null) this.#context.maxPlayers = Number(mp); this.#context.game = { ...game }; } @@ -306,8 +317,8 @@ export class OverlayManager { if (d.maxPlayers != null) this.#context.maxPlayers = Number(d.maxPlayers); if (d.playerCount != null) this.#context.playerCount = Number(d.playerCount); if (Array.isArray(d.players)) this.#context.players = [...d.players]; - if (d.player !== undefined) this.#context.lastJoinedPlayer = d.player; - if (d.lastJoinedPlayer !== undefined) this.#context.lastJoinedPlayer = d.lastJoinedPlayer; + const joined = d.playerName ?? d.player ?? d.lastJoinedPlayer; + if (joined !== undefined) this.#context.lastJoinedPlayer = joined; } /** @@ -318,6 +329,17 @@ export class OverlayManager { if (d.playerCount != null) this.#context.playerCount = Number(d.playerCount); } + /** + * @param {Record} d + */ + #applyGameStatus(d) { + if (d.roomCode != null) this.#context.roomCode = String(d.roomCode); + if (d.maxPlayers != null) this.#context.maxPlayers = Number(d.maxPlayers); + if (d.playerCount != null) this.#context.playerCount = Number(d.playerCount); + if (Array.isArray(d.players)) this.#context.players = [...d.players]; + if (d.lobbyState !== undefined) this.#context.lobbyState = d.lobbyState; + } + /** * @param {Record} d */ diff --git a/js/websocket-client.js b/js/websocket-client.js index eee36ff..c9e2175 100644 --- a/js/websocket-client.js +++ b/js/websocket-client.js @@ -307,31 +307,125 @@ export class WebSocketClient { headers: { Authorization: `Bearer ${token}` }, }); - if (response.ok) { - const session = await response.json(); - if (session && session.id !== undefined && session.id !== null) { - console.log( - '[WebSocketClient] Found active session:', - session.id, - '— subscribing', - ); - this.subscribeToSession(session.id); - } else { - console.log( - '[WebSocketClient] No active session found; waiting for session.started', - ); - } - } else { + if (!response.ok) { console.log( '[WebSocketClient] Could not fetch active session:', response.status, ); + return; } + + const session = await response.json(); + if (!session || session.id === undefined || session.id === null) { + console.log( + '[WebSocketClient] No active session found; waiting for session.started', + ); + return; + } + + const sessionId = session.id; + console.log( + '[WebSocketClient] Found active session:', + sessionId, + '— subscribing', + ); + this.subscribeToSession(sessionId); + + await this._restoreCurrentGame(sessionId); } catch (err) { console.error('[WebSocketClient] Error fetching active session:', err); } } + /** + * After subscribing, fetch the session's games to find the currently playing + * game, then hit status-live for full shard state (players, lobby, etc.). + * @param {string | number} sessionId + */ + async _restoreCurrentGame(sessionId) { + const apiUrl = this._apiUrl; + const token = this._jwtToken; + if (!apiUrl || !token) return; + + try { + const gamesRes = await fetch(`${apiUrl}/api/sessions/${sessionId}/games`, { + headers: { Authorization: `Bearer ${token}` }, + }); + + if (!gamesRes.ok) { + console.log('[WebSocketClient] Could not fetch session games:', gamesRes.status); + return; + } + + const games = await gamesRes.json(); + if (!Array.isArray(games) || games.length === 0) return; + + const playing = games.find((g) => g.status === 'playing'); + if (!playing) return; + + console.log( + '[WebSocketClient] Restoring in-progress game:', + playing.title, + '(room:', playing.room_code, ')', + ); + + this._onEvent('game.added', { + session: { id: sessionId }, + game: { + id: playing.game_id, + title: playing.title, + pack_name: playing.pack_name, + min_players: playing.min_players, + max_players: playing.max_players, + room_code: playing.room_code, + manually_added: playing.manually_added, + }, + }); + + const sessionGameId = playing.id; + const statusRes = await fetch( + `${apiUrl}/api/sessions/${sessionId}/games/${sessionGameId}/status-live`, + ); + + if (statusRes.ok) { + const live = await statusRes.json(); + console.log( + '[WebSocketClient] Restored live status — players:', + live.playerCount, + 'state:', live.gameState, + ); + + this._onEvent('game.status', { + sessionId, + gameId: live.gameId ?? playing.game_id, + roomCode: live.roomCode ?? playing.room_code, + appTag: live.appTag, + maxPlayers: live.maxPlayers ?? playing.max_players, + playerCount: live.playerCount ?? playing.player_count, + players: Array.isArray(live.players) ? live.players : [], + lobbyState: live.lobbyState, + gameState: live.gameState, + gameStarted: live.gameStarted, + gameFinished: live.gameFinished, + monitoring: live.monitoring, + }); + } else if (playing.player_count != null && playing.max_players != null) { + this._onEvent('room.connected', { + sessionId, + gameId: playing.game_id, + roomCode: playing.room_code, + maxPlayers: playing.max_players, + playerCount: playing.player_count, + players: [], + lobbyState: 'Unknown', + gameState: 'Unknown', + }); + } + } catch (err) { + console.error('[WebSocketClient] Error restoring current game:', err); + } + } + _startHeartbeat() { this._stopHeartbeat(); this._heartbeatInterval = setInterval(() => {