From c9d8ed767ce9b72cf42460201986d6d9238dafc1 Mon Sep 17 00:00:00 2001 From: Ryan <7389646+ryanbarlow97@users.noreply.github.com> Date: Tue, 3 Feb 2026 23:07:12 +0000 Subject: [PATCH] Add getClientIDForGame for consistent client IDs per game session (#3108) ## Description: - Add getClientIDForGame function to Auth.ts that generates and stores a consistent clientID per gameID using sessionStorage - Update HostLobbyModal to use getClientIDForGame for lobby creation - Update Matchmaking to use getClientIDForGame when joining games - Update PublicLobby to use getClientIDForGame when joining lobbies This enables reconnection support by ensuring the same clientID is used when rejoining a game session. ## 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: w.o.n --------- Co-authored-by: Evan Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com> --- src/client/Auth.ts | 20 ++++++++++++++++++++ src/client/HostLobbyModal.ts | 14 +++++++++----- src/client/JoinLobbyModal.ts | 5 ++--- src/client/Matchmaking.ts | 5 ++--- 4 files changed, 33 insertions(+), 11 deletions(-) diff --git a/src/client/Auth.ts b/src/client/Auth.ts index c5fdef981..6dd5f4992 100644 --- a/src/client/Auth.ts +++ b/src/client/Auth.ts @@ -2,12 +2,16 @@ import { decodeJwt } from "jose"; import { z } from "zod"; import { TokenPayload, TokenPayloadSchema } from "../core/ApiSchemas"; import { base64urlToUuid } from "../core/Base64"; +import { ID } from "../core/Schemas"; +import { generateID } from "../core/Util"; import { getApiBase, getAudience } from "./Api"; import { generateCryptoRandomUUID } from "./Utils"; export type UserAuth = { jwt: string; claims: TokenPayload } | false; const PERSISTENT_ID_KEY = "player_persistent_id"; +const CLIENT_ID_KEY = "client_join_id"; +const CLIENT_GAME_ID_KEY = "client_join_game_id"; let __jwt: string | null = null; @@ -209,6 +213,22 @@ export function getPersistentID(): string { return base64urlToUuid(sub); } +export function getClientIDForGame(gameID: string): string { + const storedGameID = sessionStorage.getItem(CLIENT_GAME_ID_KEY); + const storedClientID = sessionStorage.getItem(CLIENT_ID_KEY); + if ( + storedGameID === gameID && + storedClientID && + ID.safeParse(storedClientID).success + ) { + return storedClientID; + } + const newID = generateID(); + sessionStorage.setItem(CLIENT_GAME_ID_KEY, gameID); + sessionStorage.setItem(CLIENT_ID_KEY, newID); + return newID; +} + // WARNING: DO NOT EXPOSE THIS ID function getPersistentIDFromLocalStorage(): string { // Try to get existing localStorage diff --git a/src/client/HostLobbyModal.ts b/src/client/HostLobbyModal.ts index 8b3e314f4..c086354e6 100644 --- a/src/client/HostLobbyModal.ts +++ b/src/client/HostLobbyModal.ts @@ -21,6 +21,7 @@ import { isValidGameID, } from "../core/Schemas"; import { generateID } from "../core/Util"; +import { getClientIDForGame } from "./Auth"; import "./components/baseComponents/Modal"; import { BaseModal } from "./components/BaseModal"; import "./components/CopyButton"; @@ -635,9 +636,10 @@ export class HostLobbyModal extends BaseModal { } protected onOpen(): void { - this.lobbyCreatorClientID = generateID(); + this.lobbyId = generateID(); + this.lobbyCreatorClientID = getClientIDForGame(this.lobbyId); - createLobby(this.lobbyCreatorClientID) + createLobby(this.lobbyCreatorClientID, this.lobbyId) .then(async (lobby) => { this.lobbyId = lobby.gameID; if (!isValidGameID(this.lobbyId)) { @@ -1080,12 +1082,14 @@ export class HostLobbyModal extends BaseModal { } } -async function createLobby(creatorClientID: string): Promise { +async function createLobby( + creatorClientID: string, + gameID: string, +): Promise { const config = await getServerConfigFromClient(); try { - const id = generateID(); const response = await fetch( - `/${config.workerPath(id)}/api/create_game/${id}?creatorClientID=${encodeURIComponent(creatorClientID)}`, + `/${config.workerPath(gameID)}/api/create_game/${gameID}?creatorClientID=${encodeURIComponent(creatorClientID)}`, { method: "POST", headers: { diff --git a/src/client/JoinLobbyModal.ts b/src/client/JoinLobbyModal.ts index 59ee5b48a..d2653c0e6 100644 --- a/src/client/JoinLobbyModal.ts +++ b/src/client/JoinLobbyModal.ts @@ -17,7 +17,6 @@ import { GameRecordSchema, LobbyInfoEvent, } from "../core/Schemas"; -import { generateID } from "../core/Util"; import { getServerConfigFromClient } from "../core/configuration/ConfigLoader"; import { GameMapSize, @@ -26,8 +25,8 @@ import { HumansVsNations, } from "../core/game/Game"; import { getApiBase } from "./Api"; +import { getClientIDForGame } from "./Auth"; import { crazyGamesSDK } from "./CrazyGamesSDK"; -import { JoinLobbyEvent } from "./Main"; import { terrainMapFileLoader } from "./TerrainMapFileLoader"; import { BaseModal } from "./components/BaseModal"; import "./components/CopyButton"; @@ -346,7 +345,7 @@ export class JoinLobbyModal extends BaseModal { private startTrackingLobby(lobbyId: string, lobbyInfo?: GameInfo) { this.currentLobbyId = lobbyId; - this.currentClientID = generateID(); + this.currentClientID = getClientIDForGame(lobbyId); this.gameConfig = null; this.players = []; this.playerCount = 0; diff --git a/src/client/Matchmaking.ts b/src/client/Matchmaking.ts index c29931896..4e53ea6ab 100644 --- a/src/client/Matchmaking.ts +++ b/src/client/Matchmaking.ts @@ -2,9 +2,8 @@ import { html, LitElement } from "lit"; import { customElement, query, state } from "lit/decorators.js"; import { UserMeResponse } from "../core/ApiSchemas"; import { getServerConfigFromClient } from "../core/configuration/ConfigLoader"; -import { generateID } from "../core/Util"; import { getUserMe, hasLinkedAccount } from "./Api"; -import { getPlayToken } from "./Auth"; +import { getClientIDForGame, getPlayToken } from "./Auth"; import { BaseModal } from "./components/BaseModal"; import "./components/Difficulties"; import "./components/PatternButton"; @@ -231,7 +230,7 @@ export class MatchmakingModal extends BaseModal { new CustomEvent("join-lobby", { detail: { gameID: this.gameID, - clientID: generateID(), + clientID: getClientIDForGame(this.gameID), } as JoinLobbyEvent, bubbles: true, composed: true,