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 <evanpelle@gmail.com>
Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com>
This commit is contained in:
Ryan
2026-02-03 23:07:12 +00:00
committed by GitHub
parent 144442a99b
commit c9d8ed767c
4 changed files with 33 additions and 11 deletions
+20
View File
@@ -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
+9 -5
View File
@@ -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<GameInfo> {
async function createLobby(
creatorClientID: string,
gameID: string,
): Promise<GameInfo> {
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: {
+2 -3
View File
@@ -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;
+2 -3
View File
@@ -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,