Cherry pick PR 1836 "reloading the page during a game should reload with the same ClientID"

Co-authored-by: Danny Asmussen <woodydrn@gmail.com>
This commit is contained in:
Danny Asmussen
2025-11-09 18:58:06 +01:00
committed by variablevince
parent 6a78494404
commit 14d10307a9
10 changed files with 97 additions and 15 deletions
+3 -3
View File
@@ -2,7 +2,7 @@ import { LitElement, html } from "lit";
import { customElement, query, state } from "lit/decorators.js";
import { translateText } from "../client/Utils";
import { GameInfo, GameRecordSchema } from "../core/Schemas";
import { generateID } from "../core/Util";
import { getClientID } from "../core/Util";
import { getServerConfigFromClient } from "../core/configuration/ConfigLoader";
import { JoinLobbyEvent } from "./Main";
import "./components/baseComponents/Button";
@@ -218,7 +218,7 @@ export class JoinPrivateLobbyModal extends LitElement {
new CustomEvent("join-lobby", {
detail: {
gameID: lobbyId,
clientID: generateID(),
clientID: getClientID(lobbyId),
} as JoinLobbyEvent,
bubbles: true,
composed: true,
@@ -290,7 +290,7 @@ export class JoinPrivateLobbyModal extends LitElement {
detail: {
gameID: lobbyId,
gameRecord: parsed.data,
clientID: generateID(),
clientID: getClientID(lobbyId),
} as JoinLobbyEvent,
bubbles: true,
composed: true,
+4 -2
View File
@@ -1,7 +1,9 @@
import version from "../../resources/version.txt";
import { UserMeResponse } from "../core/ApiSchemas";
import { ID } from "../core/BaseSchemas";
import { EventBus } from "../core/EventBus";
import { GameRecord, GameStartInfo, ID } from "../core/Schemas";
import { GameRecord, GameStartInfo } from "../core/Schemas";
import { getClientID } from "../core/Util";
import { getServerConfigFromClient } from "../core/configuration/ConfigLoader";
import { UserSettings } from "../core/game/UserSettings";
import "./AccountModal";
@@ -501,7 +503,7 @@ class Client {
},
playerName: this.usernameInput?.getCurrentUsername() ?? "",
token: getPlayToken(),
clientID: lobby.clientID,
clientID: getClientID(lobby.gameID),
gameStartInfo: lobby.gameStartInfo ?? lobby.gameRecord?.info,
gameRecord: lobby.gameRecord,
},
+2 -2
View File
@@ -3,7 +3,7 @@ import { customElement, state } from "lit/decorators.js";
import { renderDuration, translateText } from "../client/Utils";
import { GameMapType, GameMode } from "../core/game/Game";
import { GameID, GameInfo } from "../core/Schemas";
import { generateID } from "../core/Util";
import { getClientID } from "../core/Util";
import { JoinLobbyEvent } from "./Main";
import { terrainMapFileLoader } from "./TerrainMapFileLoader";
@@ -211,7 +211,7 @@ export class PublicLobby extends LitElement {
new CustomEvent("join-lobby", {
detail: {
gameID: lobby.gameID,
clientID: generateID(),
clientID: getClientID(lobby.gameID),
} as JoinLobbyEvent,
bubbles: true,
composed: true,
+2 -2
View File
@@ -17,7 +17,7 @@ import {
} from "../core/game/Game";
import { UserSettings } from "../core/game/UserSettings";
import { TeamCountConfig } from "../core/Schemas";
import { generateID } from "../core/Util";
import { generateID, getClientID } from "../core/Util";
import "./components/baseComponents/Button";
import "./components/baseComponents/Modal";
import "./components/Difficulties";
@@ -525,8 +525,8 @@ export class SinglePlayerModal extends LitElement {
console.log(
`Starting single player game with map: ${GameMapType[this.selectedMap as keyof typeof GameMapType]}${this.useRandomMap ? " (Randomly selected)" : ""}`,
);
const clientID = generateID();
const gameID = generateID();
const clientID = getClientID(gameID);
const usernameInput = document.querySelector(
"username-input",
+7
View File
@@ -0,0 +1,7 @@
// This file contains shared schemas
import { z } from "zod";
export const ID = z
.string()
.regex(/^[a-zA-Z0-9]+$/)
.length(8);
+1 -4
View File
@@ -1,6 +1,7 @@
import { z } from "zod";
import quickChatData from "../../resources/QuickChat.json" with { type: "json" };
import countries from "../client/data/countries.json" with { type: "json" };
import { ID } from "./BaseSchemas";
import {
ColorPaletteSchema,
PatternDataSchema,
@@ -200,10 +201,6 @@ const EmojiSchema = z
.number()
.nonnegative()
.max(flattenedEmojiTable.length - 1);
export const ID = z
.string()
.regex(/^[a-zA-Z0-9]+$/)
.length(8);
export const AllPlayersStatsSchema = z.record(ID, PlayerStatsSchema);
+20
View File
@@ -1,8 +1,10 @@
import DOMPurify from "dompurify";
import { customAlphabet } from "nanoid";
import { ID } from "./BaseSchemas";
import { Cell, Unit } from "./game/Game";
import { GameMap, TileRef } from "./game/GameMap";
import {
ClientID,
GameConfig,
GameID,
GameRecord,
@@ -254,6 +256,24 @@ export function generateID(): GameID {
return nanoid();
}
export function getClientID(gameID: GameID): ClientID {
const cachedGame = localStorage.getItem("game_id");
const cachedClient = localStorage.getItem("client_id");
if (
gameID === cachedGame &&
cachedClient &&
ID.safeParse(cachedClient).success
)
return cachedClient;
const clientId = generateID();
localStorage.setItem("game_id", gameID);
localStorage.setItem("client_id", clientId);
return clientId;
}
export function toInt(num: number): bigint {
if (num === Infinity) {
return BigInt(Number.MAX_SAFE_INTEGER);
+2 -1
View File
@@ -4,8 +4,9 @@ import rateLimit from "express-rate-limit";
import http from "http";
import path from "path";
import { fileURLToPath } from "url";
import { ID } from "../core/BaseSchemas";
import { getServerConfigFromServer } from "../core/configuration/ConfigLoader";
import { GameInfo, ID } from "../core/Schemas";
import { GameInfo } from "../core/Schemas";
import { generateID } from "../core/Util";
import { logger } from "./Logger";
import { MapPlaylist } from "./MapPlaylist";
+1 -1
View File
@@ -7,12 +7,12 @@ import path from "path";
import { fileURLToPath } from "url";
import { WebSocket, WebSocketServer } from "ws";
import { z } from "zod";
import { ID } from "../core/BaseSchemas";
import { getServerConfigFromServer } from "../core/configuration/ConfigLoader";
import { GameType } from "../core/game/Game";
import {
ClientMessageSchema,
GameID,
ID,
PartialGameRecordSchema,
ServerErrorMessage,
} from "../core/Schemas";
+55
View File
@@ -0,0 +1,55 @@
import { getClientID } from "../../src/core/Util";
describe("Util", () => {
class InMemoryLocalStorage {
private readonly store = new Map<string, string>();
getItem(key: string): string | null {
return this.store.has(key) ? this.store.get(key)! : null;
}
setItem(key: string, value: string): void {
this.store.set(key, String(value));
}
removeItem(key: string): void {
this.store.delete(key);
}
clear(): void {
this.store.clear();
}
}
beforeEach(() => {
(globalThis as any).localStorage = new InMemoryLocalStorage();
});
test("creates and persists a new client", () => {
expect((globalThis as any).localStorage.getItem("client_id")).toBeNull();
const id = getClientID("testGameID");
expect(typeof id).toBe("string");
expect(id).toMatch(/^[0-9a-zA-Z]{8}$/);
const stored = (globalThis as any).localStorage.getItem("client_id");
expect(stored).toBe(id);
});
test("creates two games and make sure only last one is updated", () => {
const id1 = getClientID("testGameID1");
const id2 = getClientID("testGameID2");
expect(id1).not.toBe(id2);
const stored = (globalThis as any).localStorage.getItem("client_id");
expect(stored).toBe(id2);
});
test("creates two games with same game id, make sure the id stays the same", () => {
const id1 = getClientID("testGameID1");
const id2 = getClientID("testGameID1");
expect(id1).toBe(id2);
const stored = (globalThis as any).localStorage.getItem("client_id");
expect(stored).toBe(id1);
});
});