mirror of
https://github.com/openfrontio/OpenFrontIO.git
synced 2026-06-21 09:30:45 +00:00
Fix client reconnection after page refresh (#3117)
## Description: - Removed all code related to generating a client ID on the client. The server now assigns the client ID and sends it to the client in lobby messages. ## 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
This commit is contained in:
@@ -2,16 +2,12 @@ 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;
|
||||
|
||||
@@ -213,22 +209,6 @@ 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
|
||||
|
||||
@@ -56,7 +56,6 @@ export interface LobbyConfig {
|
||||
serverConfig: ServerConfig;
|
||||
cosmetics: PlayerCosmeticRefs;
|
||||
playerName: string;
|
||||
clientID: ClientID;
|
||||
gameID: GameID;
|
||||
turnstileToken: string | null;
|
||||
// GameStartInfo only exists when playing a singleplayer game.
|
||||
@@ -71,9 +70,10 @@ export function joinLobby(
|
||||
onPrestart: () => void,
|
||||
onJoin: () => void,
|
||||
): (force?: boolean) => boolean {
|
||||
console.log(
|
||||
`joining lobby: gameID: ${lobbyConfig.gameID}, clientID: ${lobbyConfig.clientID}`,
|
||||
);
|
||||
// Mutable clientID state — assigned by server (multiplayer) or derived from gameStartInfo (singleplayer)
|
||||
let clientID: ClientID | undefined;
|
||||
|
||||
console.log(`joining lobby: gameID: ${lobbyConfig.gameID}`);
|
||||
|
||||
const userSettings: UserSettings = new UserSettings();
|
||||
startGame(lobbyConfig.gameID, lobbyConfig.gameStartInfo?.config ?? {});
|
||||
@@ -82,23 +82,18 @@ export function joinLobby(
|
||||
|
||||
let currentGameRunner: ClientGameRunner | null = null;
|
||||
|
||||
let hasJoined = false;
|
||||
|
||||
const onconnect = () => {
|
||||
if (hasJoined) {
|
||||
console.log("rejoining game");
|
||||
transport.rejoinGame(0);
|
||||
} else {
|
||||
hasJoined = true;
|
||||
console.log(`Joining game lobby ${lobbyConfig.gameID}`);
|
||||
transport.joinGame();
|
||||
}
|
||||
// Always send join - server will detect reconnection via persistentID
|
||||
console.log(`Joining game lobby ${lobbyConfig.gameID}`);
|
||||
transport.joinGame();
|
||||
};
|
||||
let terrainLoad: Promise<TerrainMapData> | null = null;
|
||||
|
||||
const onmessage = (message: ServerMessage) => {
|
||||
if (message.type === "lobby_info") {
|
||||
eventBus.emit(new LobbyInfoEvent(message.lobby));
|
||||
// Server tells us our assigned clientID
|
||||
clientID = message.myClientID;
|
||||
eventBus.emit(new LobbyInfoEvent(message.lobby, message.myClientID));
|
||||
return;
|
||||
}
|
||||
if (message.type === "prestart") {
|
||||
@@ -118,11 +113,14 @@ export function joinLobby(
|
||||
console.log(
|
||||
`lobby: game started: ${JSON.stringify(message, replacer, 2)}`,
|
||||
);
|
||||
// Server tells us our assigned clientID (also sent on start for late joins)
|
||||
clientID = message.myClientID;
|
||||
onJoin();
|
||||
// For multiplayer games, GameStartInfo is not known until game starts.
|
||||
lobbyConfig.gameStartInfo = message.gameStartInfo;
|
||||
createClientGame(
|
||||
lobbyConfig,
|
||||
clientID,
|
||||
eventBus,
|
||||
transport,
|
||||
userSettings,
|
||||
@@ -148,7 +146,7 @@ export function joinLobby(
|
||||
e.message,
|
||||
e.stack,
|
||||
lobbyConfig.gameID,
|
||||
lobbyConfig.clientID,
|
||||
clientID,
|
||||
true,
|
||||
false,
|
||||
"error_modal.connection_error",
|
||||
@@ -169,7 +167,7 @@ export function joinLobby(
|
||||
message.error,
|
||||
message.message,
|
||||
lobbyConfig.gameID,
|
||||
lobbyConfig.clientID,
|
||||
clientID,
|
||||
true,
|
||||
false,
|
||||
"error_modal.connection_error",
|
||||
@@ -196,6 +194,7 @@ export function joinLobby(
|
||||
|
||||
async function createClientGame(
|
||||
lobbyConfig: LobbyConfig,
|
||||
clientID: ClientID,
|
||||
eventBus: EventBus,
|
||||
transport: Transport,
|
||||
userSettings: UserSettings,
|
||||
@@ -221,16 +220,13 @@ async function createClientGame(
|
||||
mapLoader,
|
||||
);
|
||||
}
|
||||
const worker = new WorkerClient(
|
||||
lobbyConfig.gameStartInfo,
|
||||
lobbyConfig.clientID,
|
||||
);
|
||||
const worker = new WorkerClient(lobbyConfig.gameStartInfo, clientID);
|
||||
await worker.initialize();
|
||||
const gameView = new GameView(
|
||||
worker,
|
||||
config,
|
||||
gameMap,
|
||||
lobbyConfig.clientID,
|
||||
clientID,
|
||||
lobbyConfig.gameStartInfo.gameID,
|
||||
lobbyConfig.gameStartInfo.players,
|
||||
);
|
||||
@@ -244,6 +240,7 @@ async function createClientGame(
|
||||
|
||||
return new ClientGameRunner(
|
||||
lobbyConfig,
|
||||
clientID,
|
||||
eventBus,
|
||||
gameRenderer,
|
||||
new InputHandler(gameRenderer.uiState, canvas, eventBus),
|
||||
@@ -269,6 +266,7 @@ export class ClientGameRunner {
|
||||
|
||||
constructor(
|
||||
private lobby: LobbyConfig,
|
||||
private clientID: ClientID,
|
||||
private eventBus: EventBus,
|
||||
private renderer: GameRenderer,
|
||||
private input: InputHandler,
|
||||
@@ -302,8 +300,8 @@ export class ClientGameRunner {
|
||||
{
|
||||
persistentID: getPersistentID(),
|
||||
username: this.lobby.playerName,
|
||||
clientID: this.lobby.clientID,
|
||||
stats: update.allPlayersStats[this.lobby.clientID],
|
||||
clientID: this.clientID,
|
||||
stats: update.allPlayersStats[this.clientID],
|
||||
},
|
||||
];
|
||||
|
||||
@@ -360,7 +358,7 @@ export class ClientGameRunner {
|
||||
gu.errMsg,
|
||||
gu.stack ?? "missing",
|
||||
this.lobby.gameStartInfo.gameID,
|
||||
this.lobby.clientID,
|
||||
this.clientID,
|
||||
);
|
||||
console.error(gu.stack);
|
||||
this.stop();
|
||||
@@ -422,7 +420,7 @@ export class ClientGameRunner {
|
||||
"spawn_failed",
|
||||
translateText("error_modal.spawn_failed.description"),
|
||||
this.lobby.gameID,
|
||||
this.lobby.clientID,
|
||||
this.clientID,
|
||||
true,
|
||||
false,
|
||||
translateText("error_modal.spawn_failed.title"),
|
||||
@@ -459,7 +457,7 @@ export class ClientGameRunner {
|
||||
`desync from server: ${JSON.stringify(message)}`,
|
||||
"",
|
||||
this.lobby.gameStartInfo.gameID,
|
||||
this.lobby.clientID,
|
||||
this.clientID,
|
||||
true,
|
||||
false,
|
||||
"error_modal.desync_notice",
|
||||
@@ -470,7 +468,7 @@ export class ClientGameRunner {
|
||||
message.error,
|
||||
message.message,
|
||||
this.lobby.gameID,
|
||||
this.lobby.clientID,
|
||||
this.clientID,
|
||||
true,
|
||||
false,
|
||||
"error_modal.connection_error",
|
||||
@@ -554,7 +552,7 @@ export class ClientGameRunner {
|
||||
return;
|
||||
}
|
||||
if (this.myPlayer === null) {
|
||||
const myPlayer = this.gameView.playerByClientID(this.lobby.clientID);
|
||||
const myPlayer = this.gameView.playerByClientID(this.clientID);
|
||||
if (myPlayer === null) return;
|
||||
this.myPlayer = myPlayer;
|
||||
}
|
||||
@@ -589,7 +587,7 @@ export class ClientGameRunner {
|
||||
const tile = this.gameView.ref(cell.x, cell.y);
|
||||
|
||||
if (this.myPlayer === null) {
|
||||
const myPlayer = this.gameView.playerByClientID(this.lobby.clientID);
|
||||
const myPlayer = this.gameView.playerByClientID(this.clientID);
|
||||
if (myPlayer === null) return;
|
||||
this.myPlayer = myPlayer;
|
||||
}
|
||||
@@ -650,7 +648,7 @@ export class ClientGameRunner {
|
||||
}
|
||||
|
||||
if (this.myPlayer === null) {
|
||||
const myPlayer = this.gameView.playerByClientID(this.lobby.clientID);
|
||||
const myPlayer = this.gameView.playerByClientID(this.clientID);
|
||||
if (myPlayer === null) return;
|
||||
this.myPlayer = myPlayer;
|
||||
}
|
||||
@@ -669,7 +667,7 @@ export class ClientGameRunner {
|
||||
}
|
||||
|
||||
if (this.myPlayer === null) {
|
||||
const myPlayer = this.gameView.playerByClientID(this.lobby.clientID);
|
||||
const myPlayer = this.gameView.playerByClientID(this.clientID);
|
||||
if (myPlayer === null) return;
|
||||
this.myPlayer = myPlayer;
|
||||
}
|
||||
@@ -766,7 +764,7 @@ function showErrorModal(
|
||||
error: string,
|
||||
message: string | undefined,
|
||||
gameID: GameID,
|
||||
clientID: ClientID,
|
||||
clientID: ClientID | undefined,
|
||||
closable = false,
|
||||
showDiscord = true,
|
||||
heading = "error_modal.crashed",
|
||||
|
||||
@@ -1,7 +1,8 @@
|
||||
import { TemplateResult, html } from "lit";
|
||||
import { customElement, state } from "lit/decorators.js";
|
||||
import { customElement, property, state } from "lit/decorators.js";
|
||||
import { translateText } from "../client/Utils";
|
||||
import { getServerConfigFromClient } from "../core/configuration/ConfigLoader";
|
||||
import { EventBus } from "../core/EventBus";
|
||||
import {
|
||||
Difficulty,
|
||||
Duos,
|
||||
@@ -17,11 +18,12 @@ import {
|
||||
ClientInfo,
|
||||
GameConfig,
|
||||
GameInfo,
|
||||
LobbyInfoEvent,
|
||||
TeamCountConfig,
|
||||
isValidGameID,
|
||||
} from "../core/Schemas";
|
||||
import { generateID } from "../core/Util";
|
||||
import { getClientIDForGame } from "./Auth";
|
||||
import { getPlayToken } from "./Auth";
|
||||
import "./components/baseComponents/Modal";
|
||||
import { BaseModal } from "./components/BaseModal";
|
||||
import "./components/CopyButton";
|
||||
@@ -74,6 +76,8 @@ export class HostLobbyModal extends BaseModal {
|
||||
@state() private lobbyCreatorClientID: string = "";
|
||||
@state() private nationCount: number = 0;
|
||||
|
||||
@property({ attribute: false }) eventBus: EventBus | null = null;
|
||||
|
||||
private playersInterval: NodeJS.Timeout | null = null;
|
||||
// Add a new timer for debouncing bot changes
|
||||
private botsUpdateTimer: number | null = null;
|
||||
@@ -81,6 +85,14 @@ export class HostLobbyModal extends BaseModal {
|
||||
|
||||
private leaveLobbyOnClose = true;
|
||||
|
||||
private readonly handleLobbyInfo = (event: LobbyInfoEvent) => {
|
||||
const lobby = event.lobby;
|
||||
this.lobbyCreatorClientID = lobby.lobbyCreatorClientID ?? "";
|
||||
if (lobby.clients) {
|
||||
this.clients = lobby.clients;
|
||||
}
|
||||
};
|
||||
|
||||
private renderOptionToggle(
|
||||
labelKey: string,
|
||||
checked: boolean,
|
||||
@@ -137,6 +149,21 @@ export class HostLobbyModal extends BaseModal {
|
||||
}
|
||||
}
|
||||
|
||||
private startLobbyUpdates() {
|
||||
this.stopLobbyUpdates();
|
||||
if (!this.eventBus) {
|
||||
console.warn(
|
||||
"HostLobbyModal: eventBus not set, cannot subscribe to lobby updates",
|
||||
);
|
||||
return;
|
||||
}
|
||||
this.eventBus.on(LobbyInfoEvent, this.handleLobbyInfo);
|
||||
}
|
||||
|
||||
private stopLobbyUpdates() {
|
||||
this.eventBus?.off(LobbyInfoEvent, this.handleLobbyInfo);
|
||||
}
|
||||
|
||||
render() {
|
||||
const maxTimerHandlers = this.createToggleHandlers(
|
||||
() => this.maxTimer,
|
||||
@@ -636,10 +663,13 @@ export class HostLobbyModal extends BaseModal {
|
||||
}
|
||||
|
||||
protected onOpen(): void {
|
||||
this.startLobbyUpdates();
|
||||
this.lobbyId = generateID();
|
||||
this.lobbyCreatorClientID = getClientIDForGame(this.lobbyId);
|
||||
// Note: clientID will be assigned by server when we join the lobby
|
||||
// lobbyCreatorClientID stays empty until then
|
||||
|
||||
createLobby(this.lobbyCreatorClientID, this.lobbyId)
|
||||
// Pass auth token for creator identification (server extracts persistentID from it)
|
||||
createLobby(this.lobbyId)
|
||||
.then(async (lobby) => {
|
||||
this.lobbyId = lobby.gameID;
|
||||
if (!isValidGameID(this.lobbyId)) {
|
||||
@@ -654,7 +684,6 @@ export class HostLobbyModal extends BaseModal {
|
||||
new CustomEvent("join-lobby", {
|
||||
detail: {
|
||||
gameID: this.lobbyId,
|
||||
clientID: this.lobbyCreatorClientID,
|
||||
source: "host",
|
||||
} as JoinLobbyEvent,
|
||||
bubbles: true,
|
||||
@@ -720,6 +749,7 @@ export class HostLobbyModal extends BaseModal {
|
||||
|
||||
protected onClose(): void {
|
||||
console.log("Closing host lobby modal");
|
||||
this.stopLobbyUpdates();
|
||||
if (this.leaveLobbyOnClose) {
|
||||
this.leaveLobby();
|
||||
this.updateHistory("/"); // Reset URL to base
|
||||
@@ -1083,20 +1113,20 @@ export class HostLobbyModal extends BaseModal {
|
||||
}
|
||||
}
|
||||
|
||||
async function createLobby(
|
||||
creatorClientID: string,
|
||||
gameID: string,
|
||||
): Promise<GameInfo> {
|
||||
async function createLobby(gameID: string): Promise<GameInfo> {
|
||||
const config = await getServerConfigFromClient();
|
||||
// Send JWT token for creator identification - server extracts persistentID from it
|
||||
// persistentID should never be exposed to other clients
|
||||
const token = await getPlayToken();
|
||||
try {
|
||||
const response = await fetch(
|
||||
`/${config.workerPath(gameID)}/api/create_game/${gameID}?creatorClientID=${encodeURIComponent(creatorClientID)}`,
|
||||
`/${config.workerPath(gameID)}/api/create_game/${gameID}`,
|
||||
{
|
||||
method: "POST",
|
||||
headers: {
|
||||
"Content-Type": "application/json",
|
||||
Authorization: `Bearer ${token}`,
|
||||
},
|
||||
// body: JSON.stringify(data), // Include this if you need to send data
|
||||
},
|
||||
);
|
||||
|
||||
|
||||
@@ -25,7 +25,6 @@ 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";
|
||||
@@ -61,9 +60,7 @@ export class JoinLobbyModal extends BaseModal {
|
||||
|
||||
private readonly handleLobbyInfo = (event: LobbyInfoEvent) => {
|
||||
const lobby = event.lobby;
|
||||
if (!this.currentLobbyId || lobby.gameID !== this.currentLobbyId) {
|
||||
return;
|
||||
}
|
||||
this.currentClientID = event.myClientID;
|
||||
// Only stop showing spinner when we have player info
|
||||
if (this.isConnecting && lobby.clients) {
|
||||
this.isConnecting = false;
|
||||
@@ -335,7 +332,6 @@ export class JoinLobbyModal extends BaseModal {
|
||||
new CustomEvent("join-lobby", {
|
||||
detail: {
|
||||
gameID: lobbyId,
|
||||
clientID: this.currentClientID,
|
||||
source: "public",
|
||||
} as JoinLobbyEvent,
|
||||
bubbles: true,
|
||||
@@ -346,7 +342,8 @@ export class JoinLobbyModal extends BaseModal {
|
||||
|
||||
private startTrackingLobby(lobbyId: string, lobbyInfo?: GameInfo) {
|
||||
this.currentLobbyId = lobbyId;
|
||||
this.currentClientID = getClientIDForGame(lobbyId);
|
||||
// clientID will be assigned by server via lobby_info message
|
||||
this.currentClientID = "";
|
||||
this.gameConfig = null;
|
||||
this.players = [];
|
||||
this.nationCount = 0;
|
||||
@@ -545,9 +542,7 @@ export class JoinLobbyModal extends BaseModal {
|
||||
}
|
||||
}
|
||||
|
||||
this.lobbyCreatorClientID = this.isPrivateLobby()
|
||||
? (lobby.clients?.[0]?.clientID ?? null)
|
||||
: null;
|
||||
this.lobbyCreatorClientID = lobby.lobbyCreatorClientID ?? null;
|
||||
}
|
||||
|
||||
private startLobbyUpdates() {
|
||||
@@ -776,7 +771,6 @@ export class JoinLobbyModal extends BaseModal {
|
||||
new CustomEvent("join-lobby", {
|
||||
detail: {
|
||||
gameID: lobbyId,
|
||||
clientID: this.currentClientID,
|
||||
source: "private",
|
||||
} as JoinLobbyEvent,
|
||||
bubbles: true,
|
||||
@@ -835,7 +829,6 @@ export class JoinLobbyModal extends BaseModal {
|
||||
detail: {
|
||||
gameID: lobbyId,
|
||||
gameRecord: parsed.data,
|
||||
clientID: this.currentClientID,
|
||||
source: "private",
|
||||
} as JoinLobbyEvent,
|
||||
bubbles: true,
|
||||
|
||||
@@ -2,13 +2,14 @@ import { z } from "zod";
|
||||
import { EventBus } from "../core/EventBus";
|
||||
import {
|
||||
AllPlayersStats,
|
||||
ClientID,
|
||||
ClientMessage,
|
||||
ClientSendWinnerMessage,
|
||||
Intent,
|
||||
PartialGameRecordSchema,
|
||||
PlayerRecord,
|
||||
ServerMessage,
|
||||
ServerStartGameMessage,
|
||||
StampedIntent,
|
||||
Turn,
|
||||
} from "../core/Schemas";
|
||||
import {
|
||||
@@ -34,12 +35,13 @@ export class LocalServer {
|
||||
|
||||
private turns: Turn[] = [];
|
||||
|
||||
private intents: Intent[] = [];
|
||||
private intents: StampedIntent[] = [];
|
||||
private startedAt: number;
|
||||
|
||||
private paused = false;
|
||||
private replaySpeedMultiplier = defaultReplaySpeedMultiplier;
|
||||
|
||||
private clientID: ClientID | undefined;
|
||||
private winner: ClientSendWinnerMessage | null = null;
|
||||
private allPlayersStats: AllPlayersStats = {};
|
||||
|
||||
@@ -102,34 +104,48 @@ export class LocalServer {
|
||||
if (this.lobbyConfig.gameStartInfo === undefined) {
|
||||
throw new Error("missing gameStartInfo");
|
||||
}
|
||||
this.clientID = this.lobbyConfig.gameStartInfo.players[0]?.clientID;
|
||||
if (!this.clientID) {
|
||||
throw new Error("missing clientID");
|
||||
}
|
||||
this.clientMessage({
|
||||
type: "start",
|
||||
gameStartInfo: this.lobbyConfig.gameStartInfo,
|
||||
turns: [],
|
||||
lobbyCreatedAt: this.lobbyConfig.gameStartInfo.lobbyCreatedAt,
|
||||
myClientID: this.clientID,
|
||||
} satisfies ServerStartGameMessage);
|
||||
}
|
||||
|
||||
onMessage(clientMsg: ClientMessage) {
|
||||
if (clientMsg.type === "rejoin") {
|
||||
if (!this.clientID) {
|
||||
throw new Error("missing clientID");
|
||||
}
|
||||
this.clientMessage({
|
||||
type: "start",
|
||||
gameStartInfo: this.lobbyConfig.gameStartInfo!,
|
||||
turns: this.turns,
|
||||
lobbyCreatedAt: this.lobbyConfig.gameStartInfo!.lobbyCreatedAt,
|
||||
myClientID: this.clientID,
|
||||
} satisfies ServerStartGameMessage);
|
||||
}
|
||||
if (clientMsg.type === "intent") {
|
||||
if (clientMsg.intent.type === "toggle_pause") {
|
||||
if (clientMsg.intent.paused) {
|
||||
// Server stamps clientID - client doesn't send it
|
||||
const stampedIntent = {
|
||||
...clientMsg.intent,
|
||||
clientID: this.clientID!,
|
||||
};
|
||||
if (stampedIntent.type === "toggle_pause") {
|
||||
if (stampedIntent.paused) {
|
||||
// Pausing: add intent and end turn before pause takes effect
|
||||
this.intents.push(clientMsg.intent);
|
||||
this.intents.push(stampedIntent);
|
||||
this.endTurn();
|
||||
this.paused = true;
|
||||
} else {
|
||||
// Unpausing: clear pause flag before adding intent so next turn can execute
|
||||
this.paused = false;
|
||||
this.intents.push(clientMsg.intent);
|
||||
this.intents.push(stampedIntent);
|
||||
this.endTurn();
|
||||
}
|
||||
return;
|
||||
@@ -139,7 +155,7 @@ export class LocalServer {
|
||||
return;
|
||||
}
|
||||
|
||||
this.intents.push(clientMsg.intent);
|
||||
this.intents.push(stampedIntent);
|
||||
}
|
||||
if (clientMsg.type === "hash") {
|
||||
if (!this.lobbyConfig.gameRecord) {
|
||||
@@ -224,8 +240,8 @@ export class LocalServer {
|
||||
{
|
||||
persistentID: getPersistentID(),
|
||||
username: this.lobbyConfig.playerName,
|
||||
clientID: this.lobbyConfig.clientID,
|
||||
stats: this.allPlayersStats[this.lobbyConfig.clientID],
|
||||
clientID: this.clientID!,
|
||||
stats: this.allPlayersStats[this.clientID!],
|
||||
cosmetics: this.lobbyConfig.gameStartInfo?.players[0].cosmetics,
|
||||
clanTag: getClanTag(this.lobbyConfig.playerName) ?? undefined,
|
||||
},
|
||||
|
||||
+2
-2
@@ -210,7 +210,6 @@ declare global {
|
||||
}
|
||||
|
||||
export interface JoinLobbyEvent {
|
||||
clientID: string;
|
||||
// Multiplayer games only have gameID, gameConfig is not known until game starts.
|
||||
gameID: string;
|
||||
// GameConfig only exists when playing a singleplayer game.
|
||||
@@ -504,6 +503,8 @@ class Client {
|
||||
) as HostPrivateLobbyModal;
|
||||
if (!this.hostModal || !(this.hostModal instanceof HostPrivateLobbyModal)) {
|
||||
console.warn("Host private lobby modal element not found");
|
||||
} else {
|
||||
this.hostModal.eventBus = this.eventBus;
|
||||
}
|
||||
const hostLobbyButton = document.getElementById("host-lobby-button");
|
||||
if (hostLobbyButton === null) throw new Error("Missing host-lobby-button");
|
||||
@@ -818,7 +819,6 @@ class Client {
|
||||
turnstileToken: await this.getTurnstileToken(lobby),
|
||||
playerName:
|
||||
this.usernameInput?.getCurrentUsername() ?? genAnonUsername(),
|
||||
clientID: lobby.clientID,
|
||||
gameStartInfo: lobby.gameStartInfo ?? lobby.gameRecord?.info,
|
||||
gameRecord: lobby.gameRecord,
|
||||
},
|
||||
|
||||
@@ -3,7 +3,7 @@ import { customElement, query, state } from "lit/decorators.js";
|
||||
import { UserMeResponse } from "../core/ApiSchemas";
|
||||
import { getServerConfigFromClient } from "../core/configuration/ConfigLoader";
|
||||
import { getUserMe, hasLinkedAccount } from "./Api";
|
||||
import { getClientIDForGame, getPlayToken } from "./Auth";
|
||||
import { getPlayToken } from "./Auth";
|
||||
import { BaseModal } from "./components/BaseModal";
|
||||
import "./components/Difficulties";
|
||||
import "./components/PatternButton";
|
||||
@@ -230,7 +230,6 @@ export class MatchmakingModal extends BaseModal {
|
||||
new CustomEvent("join-lobby", {
|
||||
detail: {
|
||||
gameID: this.gameID,
|
||||
clientID: getClientIDForGame(this.gameID),
|
||||
source: "matchmaking",
|
||||
} as JoinLobbyEvent,
|
||||
bubbles: true,
|
||||
|
||||
@@ -912,7 +912,6 @@ export class SinglePlayerModal extends BaseModal {
|
||||
this.dispatchEvent(
|
||||
new CustomEvent("join-lobby", {
|
||||
detail: {
|
||||
clientID: clientID,
|
||||
gameID: gameID,
|
||||
gameStartInfo: {
|
||||
gameID: gameID,
|
||||
|
||||
+2
-25
@@ -402,7 +402,7 @@ export class Transport {
|
||||
this.sendMsg({
|
||||
type: "join",
|
||||
gameID: this.lobbyConfig.gameID,
|
||||
clientID: this.lobbyConfig.clientID,
|
||||
// Note: clientID is not sent - server assigns it based on persistentID
|
||||
username: this.lobbyConfig.playerName,
|
||||
cosmetics: this.lobbyConfig.cosmetics,
|
||||
turnstileToken: this.lobbyConfig.turnstileToken,
|
||||
@@ -414,7 +414,7 @@ export class Transport {
|
||||
this.sendMsg({
|
||||
type: "rejoin",
|
||||
gameID: this.lobbyConfig.gameID,
|
||||
clientID: this.lobbyConfig.clientID,
|
||||
// Note: clientID is not sent - server looks it up from persistentID in token
|
||||
lastTurn: lastTurn,
|
||||
token: await getPlayToken(),
|
||||
} satisfies ClientRejoinMessage);
|
||||
@@ -443,7 +443,6 @@ export class Transport {
|
||||
private onSendAllianceRequest(event: SendAllianceRequestIntentEvent) {
|
||||
this.sendIntent({
|
||||
type: "allianceRequest",
|
||||
clientID: this.lobbyConfig.clientID,
|
||||
recipient: event.recipient.id(),
|
||||
});
|
||||
}
|
||||
@@ -451,7 +450,6 @@ export class Transport {
|
||||
private onAllianceRequestReplyUIEvent(event: SendAllianceReplyIntentEvent) {
|
||||
this.sendIntent({
|
||||
type: "allianceRequestReply",
|
||||
clientID: this.lobbyConfig.clientID,
|
||||
requestor: event.requestor.id(),
|
||||
accept: event.accepted,
|
||||
});
|
||||
@@ -460,7 +458,6 @@ export class Transport {
|
||||
private onBreakAllianceRequestUIEvent(event: SendBreakAllianceIntentEvent) {
|
||||
this.sendIntent({
|
||||
type: "breakAlliance",
|
||||
clientID: this.lobbyConfig.clientID,
|
||||
recipient: event.recipient.id(),
|
||||
});
|
||||
}
|
||||
@@ -470,7 +467,6 @@ export class Transport {
|
||||
) {
|
||||
this.sendIntent({
|
||||
type: "allianceExtension",
|
||||
clientID: this.lobbyConfig.clientID,
|
||||
recipient: event.recipient.id(),
|
||||
});
|
||||
}
|
||||
@@ -478,7 +474,6 @@ export class Transport {
|
||||
private onSendSpawnIntentEvent(event: SendSpawnIntentEvent) {
|
||||
this.sendIntent({
|
||||
type: "spawn",
|
||||
clientID: this.lobbyConfig.clientID,
|
||||
tile: event.tile,
|
||||
});
|
||||
}
|
||||
@@ -486,7 +481,6 @@ export class Transport {
|
||||
private onSendAttackIntent(event: SendAttackIntentEvent) {
|
||||
this.sendIntent({
|
||||
type: "attack",
|
||||
clientID: this.lobbyConfig.clientID,
|
||||
targetID: event.targetID,
|
||||
troops: event.troops,
|
||||
});
|
||||
@@ -495,7 +489,6 @@ export class Transport {
|
||||
private onSendBoatAttackIntent(event: SendBoatAttackIntentEvent) {
|
||||
this.sendIntent({
|
||||
type: "boat",
|
||||
clientID: this.lobbyConfig.clientID,
|
||||
troops: event.troops,
|
||||
dst: event.dst,
|
||||
});
|
||||
@@ -505,7 +498,6 @@ export class Transport {
|
||||
this.sendIntent({
|
||||
type: "upgrade_structure",
|
||||
unit: event.unitType,
|
||||
clientID: this.lobbyConfig.clientID,
|
||||
unitId: event.unitId,
|
||||
});
|
||||
}
|
||||
@@ -513,7 +505,6 @@ export class Transport {
|
||||
private onSendTargetPlayerIntent(event: SendTargetPlayerIntentEvent) {
|
||||
this.sendIntent({
|
||||
type: "targetPlayer",
|
||||
clientID: this.lobbyConfig.clientID,
|
||||
target: event.targetID,
|
||||
});
|
||||
}
|
||||
@@ -521,7 +512,6 @@ export class Transport {
|
||||
private onSendEmojiIntent(event: SendEmojiIntentEvent) {
|
||||
this.sendIntent({
|
||||
type: "emoji",
|
||||
clientID: this.lobbyConfig.clientID,
|
||||
recipient:
|
||||
event.recipient === AllPlayers ? AllPlayers : event.recipient.id(),
|
||||
emoji: event.emoji,
|
||||
@@ -531,7 +521,6 @@ export class Transport {
|
||||
private onSendDonateGoldIntent(event: SendDonateGoldIntentEvent) {
|
||||
this.sendIntent({
|
||||
type: "donate_gold",
|
||||
clientID: this.lobbyConfig.clientID,
|
||||
recipient: event.recipient.id(),
|
||||
gold: event.gold ? Number(event.gold) : null,
|
||||
});
|
||||
@@ -540,7 +529,6 @@ export class Transport {
|
||||
private onSendDonateTroopIntent(event: SendDonateTroopsIntentEvent) {
|
||||
this.sendIntent({
|
||||
type: "donate_troops",
|
||||
clientID: this.lobbyConfig.clientID,
|
||||
recipient: event.recipient.id(),
|
||||
troops: event.troops,
|
||||
});
|
||||
@@ -549,7 +537,6 @@ export class Transport {
|
||||
private onSendQuickChatIntent(event: SendQuickChatEvent) {
|
||||
this.sendIntent({
|
||||
type: "quick_chat",
|
||||
clientID: this.lobbyConfig.clientID,
|
||||
recipient: event.recipient.id(),
|
||||
quickChatKey: event.quickChatKey,
|
||||
target: event.target,
|
||||
@@ -559,7 +546,6 @@ export class Transport {
|
||||
private onSendEmbargoIntent(event: SendEmbargoIntentEvent) {
|
||||
this.sendIntent({
|
||||
type: "embargo",
|
||||
clientID: this.lobbyConfig.clientID,
|
||||
targetID: event.target.id(),
|
||||
action: event.action,
|
||||
});
|
||||
@@ -568,7 +554,6 @@ export class Transport {
|
||||
private onSendEmbargoAllIntent(event: SendEmbargoAllIntentEvent) {
|
||||
this.sendIntent({
|
||||
type: "embargo_all",
|
||||
clientID: this.lobbyConfig.clientID,
|
||||
action: event.action,
|
||||
});
|
||||
}
|
||||
@@ -576,7 +561,6 @@ export class Transport {
|
||||
private onBuildUnitIntent(event: BuildUnitIntentEvent) {
|
||||
this.sendIntent({
|
||||
type: "build_unit",
|
||||
clientID: this.lobbyConfig.clientID,
|
||||
unit: event.unit,
|
||||
tile: event.tile,
|
||||
rocketDirectionUp: event.rocketDirectionUp,
|
||||
@@ -586,7 +570,6 @@ export class Transport {
|
||||
private onPauseGameIntent(event: PauseGameIntentEvent) {
|
||||
this.sendIntent({
|
||||
type: "toggle_pause",
|
||||
clientID: this.lobbyConfig.clientID,
|
||||
paused: event.paused,
|
||||
});
|
||||
}
|
||||
@@ -626,7 +609,6 @@ export class Transport {
|
||||
private onCancelAttackIntentEvent(event: CancelAttackIntentEvent) {
|
||||
this.sendIntent({
|
||||
type: "cancel_attack",
|
||||
clientID: this.lobbyConfig.clientID,
|
||||
attackID: event.attackID,
|
||||
});
|
||||
}
|
||||
@@ -634,7 +616,6 @@ export class Transport {
|
||||
private onCancelBoatIntentEvent(event: CancelBoatIntentEvent) {
|
||||
this.sendIntent({
|
||||
type: "cancel_boat",
|
||||
clientID: this.lobbyConfig.clientID,
|
||||
unitID: event.unitID,
|
||||
});
|
||||
}
|
||||
@@ -642,7 +623,6 @@ export class Transport {
|
||||
private onMoveWarshipEvent(event: MoveWarshipIntentEvent) {
|
||||
this.sendIntent({
|
||||
type: "move_warship",
|
||||
clientID: this.lobbyConfig.clientID,
|
||||
unitId: event.unitId,
|
||||
tile: event.tile,
|
||||
});
|
||||
@@ -651,7 +631,6 @@ export class Transport {
|
||||
private onSendDeleteUnitIntent(event: SendDeleteUnitIntentEvent) {
|
||||
this.sendIntent({
|
||||
type: "delete_unit",
|
||||
clientID: this.lobbyConfig.clientID,
|
||||
unitId: event.unitId,
|
||||
});
|
||||
}
|
||||
@@ -659,7 +638,6 @@ export class Transport {
|
||||
private onSendKickPlayerIntent(event: SendKickPlayerIntentEvent) {
|
||||
this.sendIntent({
|
||||
type: "kick_player",
|
||||
clientID: this.lobbyConfig.clientID,
|
||||
target: event.target,
|
||||
});
|
||||
}
|
||||
@@ -667,7 +645,6 @@ export class Transport {
|
||||
private onSendUpdateGameConfigIntent(event: SendUpdateGameConfigIntentEvent) {
|
||||
this.sendIntent({
|
||||
type: "update_game_config",
|
||||
clientID: this.lobbyConfig.clientID,
|
||||
config: event.config,
|
||||
});
|
||||
}
|
||||
|
||||
+42
-33
@@ -148,6 +148,7 @@ const ClientInfoSchema = z.object({
|
||||
export const GameInfoSchema = z.object({
|
||||
gameID: z.string(),
|
||||
clients: z.array(ClientInfoSchema).optional(),
|
||||
lobbyCreatorClientID: z.string().optional(),
|
||||
startsAt: z.number().optional(),
|
||||
serverTime: z.number(),
|
||||
gameConfig: z.lazy(() => GameConfigSchema).optional(),
|
||||
@@ -166,7 +167,10 @@ export const PublicGamesSchema = z.object({
|
||||
});
|
||||
|
||||
export class LobbyInfoEvent implements GameEvent {
|
||||
constructor(public lobby: GameInfo) {}
|
||||
constructor(
|
||||
public lobby: GameInfo,
|
||||
public myClientID: ClientID,
|
||||
) {}
|
||||
}
|
||||
|
||||
export interface ClientInfo {
|
||||
@@ -280,139 +284,136 @@ export const QuickChatKeySchema = z.enum(
|
||||
// Intents
|
||||
//
|
||||
|
||||
const BaseIntentSchema = z.object({
|
||||
clientID: ID,
|
||||
});
|
||||
|
||||
export const AllianceExtensionIntentSchema = BaseIntentSchema.extend({
|
||||
export const AllianceExtensionIntentSchema = z.object({
|
||||
type: z.literal("allianceExtension"),
|
||||
recipient: ID,
|
||||
});
|
||||
|
||||
export const AttackIntentSchema = BaseIntentSchema.extend({
|
||||
export const AttackIntentSchema = z.object({
|
||||
type: z.literal("attack"),
|
||||
targetID: ID.nullable(),
|
||||
troops: z.number().nonnegative().nullable(),
|
||||
});
|
||||
|
||||
export const SpawnIntentSchema = BaseIntentSchema.extend({
|
||||
export const SpawnIntentSchema = z.object({
|
||||
type: z.literal("spawn"),
|
||||
tile: z.number(),
|
||||
});
|
||||
|
||||
export const BoatAttackIntentSchema = BaseIntentSchema.extend({
|
||||
export const BoatAttackIntentSchema = z.object({
|
||||
type: z.literal("boat"),
|
||||
troops: z.number().nonnegative(),
|
||||
dst: z.number(),
|
||||
});
|
||||
|
||||
export const AllianceRequestIntentSchema = BaseIntentSchema.extend({
|
||||
export const AllianceRequestIntentSchema = z.object({
|
||||
type: z.literal("allianceRequest"),
|
||||
recipient: ID,
|
||||
});
|
||||
|
||||
export const AllianceRequestReplyIntentSchema = BaseIntentSchema.extend({
|
||||
export const AllianceRequestReplyIntentSchema = z.object({
|
||||
type: z.literal("allianceRequestReply"),
|
||||
requestor: ID, // The one who made the original alliance request
|
||||
accept: z.boolean(),
|
||||
});
|
||||
|
||||
export const BreakAllianceIntentSchema = BaseIntentSchema.extend({
|
||||
export const BreakAllianceIntentSchema = z.object({
|
||||
type: z.literal("breakAlliance"),
|
||||
recipient: ID,
|
||||
});
|
||||
|
||||
export const TargetPlayerIntentSchema = BaseIntentSchema.extend({
|
||||
export const TargetPlayerIntentSchema = z.object({
|
||||
type: z.literal("targetPlayer"),
|
||||
target: ID,
|
||||
});
|
||||
|
||||
export const EmojiIntentSchema = BaseIntentSchema.extend({
|
||||
export const EmojiIntentSchema = z.object({
|
||||
type: z.literal("emoji"),
|
||||
recipient: z.union([ID, z.literal(AllPlayers)]),
|
||||
emoji: EmojiSchema,
|
||||
});
|
||||
|
||||
export const EmbargoIntentSchema = BaseIntentSchema.extend({
|
||||
export const EmbargoIntentSchema = z.object({
|
||||
type: z.literal("embargo"),
|
||||
targetID: ID,
|
||||
action: z.union([z.literal("start"), z.literal("stop")]),
|
||||
});
|
||||
|
||||
export const EmbargoAllIntentSchema = BaseIntentSchema.extend({
|
||||
export const EmbargoAllIntentSchema = z.object({
|
||||
type: z.literal("embargo_all"),
|
||||
action: z.union([z.literal("start"), z.literal("stop")]),
|
||||
});
|
||||
|
||||
export const DonateGoldIntentSchema = BaseIntentSchema.extend({
|
||||
export const DonateGoldIntentSchema = z.object({
|
||||
type: z.literal("donate_gold"),
|
||||
recipient: ID,
|
||||
gold: z.number().nonnegative().nullable(),
|
||||
});
|
||||
|
||||
export const DonateTroopIntentSchema = BaseIntentSchema.extend({
|
||||
export const DonateTroopIntentSchema = z.object({
|
||||
type: z.literal("donate_troops"),
|
||||
recipient: ID,
|
||||
troops: z.number().nonnegative().nullable(),
|
||||
});
|
||||
|
||||
export const BuildUnitIntentSchema = BaseIntentSchema.extend({
|
||||
export const BuildUnitIntentSchema = z.object({
|
||||
type: z.literal("build_unit"),
|
||||
unit: z.enum(UnitType),
|
||||
tile: z.number(),
|
||||
rocketDirectionUp: z.boolean().optional(),
|
||||
});
|
||||
|
||||
export const UpgradeStructureIntentSchema = BaseIntentSchema.extend({
|
||||
export const UpgradeStructureIntentSchema = z.object({
|
||||
type: z.literal("upgrade_structure"),
|
||||
unit: z.enum(UnitType),
|
||||
unitId: z.number(),
|
||||
});
|
||||
|
||||
export const CancelAttackIntentSchema = BaseIntentSchema.extend({
|
||||
export const CancelAttackIntentSchema = z.object({
|
||||
type: z.literal("cancel_attack"),
|
||||
attackID: z.string(),
|
||||
});
|
||||
|
||||
export const CancelBoatIntentSchema = BaseIntentSchema.extend({
|
||||
export const CancelBoatIntentSchema = z.object({
|
||||
type: z.literal("cancel_boat"),
|
||||
unitID: z.number(),
|
||||
});
|
||||
|
||||
export const MoveWarshipIntentSchema = BaseIntentSchema.extend({
|
||||
export const MoveWarshipIntentSchema = z.object({
|
||||
type: z.literal("move_warship"),
|
||||
unitId: z.number(),
|
||||
tile: z.number(),
|
||||
});
|
||||
|
||||
export const DeleteUnitIntentSchema = BaseIntentSchema.extend({
|
||||
export const DeleteUnitIntentSchema = z.object({
|
||||
type: z.literal("delete_unit"),
|
||||
unitId: z.number(),
|
||||
});
|
||||
|
||||
export const QuickChatIntentSchema = BaseIntentSchema.extend({
|
||||
export const QuickChatIntentSchema = z.object({
|
||||
type: z.literal("quick_chat"),
|
||||
recipient: ID,
|
||||
quickChatKey: QuickChatKeySchema,
|
||||
target: ID.optional(),
|
||||
});
|
||||
|
||||
export const MarkDisconnectedIntentSchema = BaseIntentSchema.extend({
|
||||
export const MarkDisconnectedIntentSchema = z.object({
|
||||
type: z.literal("mark_disconnected"),
|
||||
clientID: ID,
|
||||
isDisconnected: z.boolean(),
|
||||
});
|
||||
|
||||
export const KickPlayerIntentSchema = BaseIntentSchema.extend({
|
||||
export const KickPlayerIntentSchema = z.object({
|
||||
type: z.literal("kick_player"),
|
||||
target: ID,
|
||||
});
|
||||
|
||||
export const TogglePauseIntentSchema = BaseIntentSchema.extend({
|
||||
export const TogglePauseIntentSchema = z.object({
|
||||
type: z.literal("toggle_pause"),
|
||||
paused: z.boolean().default(false),
|
||||
});
|
||||
|
||||
export const UpdateGameConfigIntentSchema = BaseIntentSchema.extend({
|
||||
export const UpdateGameConfigIntentSchema = z.object({
|
||||
type: z.literal("update_game_config"),
|
||||
config: GameConfigSchema.partial(),
|
||||
});
|
||||
@@ -444,13 +445,17 @@ const IntentSchema = z.discriminatedUnion("type", [
|
||||
UpdateGameConfigIntentSchema,
|
||||
]);
|
||||
|
||||
// StampedIntent = Intent with server-stamped clientID (used in turns and execution)
|
||||
export const StampedIntentSchema = IntentSchema.and(z.object({ clientID: ID }));
|
||||
export type StampedIntent = Intent & { clientID: ClientID };
|
||||
|
||||
//
|
||||
// Server utility types
|
||||
//
|
||||
|
||||
export const TurnSchema = z.object({
|
||||
turnNumber: z.number(),
|
||||
intents: IntentSchema.array(),
|
||||
intents: StampedIntentSchema.array(),
|
||||
// The hash of the game state at the end of the turn.
|
||||
hash: z.number().nullable().optional(),
|
||||
});
|
||||
@@ -539,6 +544,8 @@ export const ServerStartGameMessageSchema = z.object({
|
||||
turns: TurnSchema.array(),
|
||||
gameStartInfo: GameStartInfoSchema,
|
||||
lobbyCreatedAt: z.number(),
|
||||
// The clientID assigned to this connection by the server
|
||||
myClientID: ID,
|
||||
});
|
||||
|
||||
export const ServerDesyncSchema = z.object({
|
||||
@@ -559,6 +566,8 @@ export const ServerErrorSchema = z.object({
|
||||
export const ServerLobbyInfoMessageSchema = z.object({
|
||||
type: z.literal("lobby_info"),
|
||||
lobby: GameInfoSchema,
|
||||
// The clientID assigned to this connection by the server
|
||||
myClientID: ID,
|
||||
});
|
||||
|
||||
export const ServerMessageSchema = z.discriminatedUnion("type", [
|
||||
@@ -603,10 +612,10 @@ export const ClientIntentMessageSchema = z.object({
|
||||
});
|
||||
|
||||
// WARNING: never send this message to clients.
|
||||
// Note: clientID is NOT included - server assigns it based on persistentID from token
|
||||
export const ClientJoinMessageSchema = z.object({
|
||||
type: z.literal("join"),
|
||||
clientID: ID,
|
||||
token: TokenSchema, // WARNING: PII
|
||||
token: TokenSchema, // WARNING: PII - server extracts persistentID from this
|
||||
gameID: ID,
|
||||
username: UsernameSchema,
|
||||
// Server replaces the refs with the actual cosmetic data.
|
||||
@@ -617,7 +626,7 @@ export const ClientJoinMessageSchema = z.object({
|
||||
export const ClientRejoinMessageSchema = z.object({
|
||||
type: z.literal("rejoin"),
|
||||
gameID: ID,
|
||||
clientID: ID,
|
||||
// Note: clientID is NOT sent - server looks it up from persistentID in token
|
||||
lastTurn: z.number(),
|
||||
token: TokenSchema,
|
||||
});
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
import { Execution, Game } from "../game/Game";
|
||||
import { PseudoRandom } from "../PseudoRandom";
|
||||
import { ClientID, GameID, Intent, Turn } from "../Schemas";
|
||||
import { ClientID, GameID, StampedIntent, Turn } from "../Schemas";
|
||||
import { simpleHash } from "../Util";
|
||||
import { AllianceExtensionExecution } from "./alliance/AllianceExtensionExecution";
|
||||
import { AllianceRequestExecution } from "./alliance/AllianceRequestExecution";
|
||||
@@ -46,7 +46,7 @@ export class Executor {
|
||||
return turn.intents.map((i) => this.createExec(i));
|
||||
}
|
||||
|
||||
createExec(intent: Intent): Execution {
|
||||
createExec(intent: StampedIntent): Execution {
|
||||
const player = this.mg.playerByClientID(intent.clientID);
|
||||
if (!player) {
|
||||
console.warn(`player with clientID ${intent.clientID} not found`);
|
||||
|
||||
@@ -20,6 +20,5 @@ export class Client {
|
||||
public readonly username: string,
|
||||
public ws: WebSocket,
|
||||
public readonly cosmetics: PlayerCosmetics | undefined,
|
||||
public readonly isRejoin: boolean = false,
|
||||
) {}
|
||||
}
|
||||
|
||||
+14
-16
@@ -8,7 +8,7 @@ import {
|
||||
GameMode,
|
||||
GameType,
|
||||
} from "../core/game/Game";
|
||||
import { ClientRejoinMessage, GameConfig, GameID } from "../core/Schemas";
|
||||
import { GameConfig, GameID } from "../core/Schemas";
|
||||
import { Client } from "./Client";
|
||||
import { GamePhase, GameServer } from "./GameServer";
|
||||
|
||||
@@ -32,32 +32,30 @@ export class GameManager {
|
||||
);
|
||||
}
|
||||
|
||||
joinClient(client: Client, gameID: GameID): boolean {
|
||||
joinClient(
|
||||
client: Client,
|
||||
gameID: GameID,
|
||||
): "joined" | "kicked" | "rejected" | "not_found" {
|
||||
const game = this.games.get(gameID);
|
||||
if (game) {
|
||||
game.joinClient(client);
|
||||
return true;
|
||||
}
|
||||
return false;
|
||||
if (!game) return "not_found";
|
||||
return game.joinClient(client);
|
||||
}
|
||||
|
||||
rejoinClient(
|
||||
ws: WebSocket,
|
||||
persistentID: string,
|
||||
msg: ClientRejoinMessage,
|
||||
gameID: GameID,
|
||||
lastTurn: number = 0,
|
||||
): boolean {
|
||||
const game = this.games.get(msg.gameID);
|
||||
if (game) {
|
||||
game.rejoinClient(ws, persistentID, msg);
|
||||
return true;
|
||||
}
|
||||
return false;
|
||||
const game = this.games.get(gameID);
|
||||
if (!game) return false;
|
||||
return game.rejoinClient(ws, persistentID, lastTurn);
|
||||
}
|
||||
|
||||
createGame(
|
||||
id: GameID,
|
||||
gameConfig: GameConfig | undefined,
|
||||
creatorClientID?: string,
|
||||
creatorPersistentID?: string,
|
||||
startsAt?: number,
|
||||
) {
|
||||
const game = new GameServer(
|
||||
@@ -83,7 +81,7 @@ export class GameManager {
|
||||
disabledUnits: [],
|
||||
...gameConfig,
|
||||
},
|
||||
creatorClientID,
|
||||
creatorPersistentID,
|
||||
startsAt,
|
||||
);
|
||||
this.games.set(id, game);
|
||||
|
||||
+95
-81
@@ -7,13 +7,11 @@ import { GameType } from "../core/game/Game";
|
||||
import {
|
||||
ClientID,
|
||||
ClientMessageSchema,
|
||||
ClientRejoinMessage,
|
||||
ClientSendWinnerMessage,
|
||||
GameConfig,
|
||||
GameInfo,
|
||||
GameStartInfo,
|
||||
GameStartInfoSchema,
|
||||
Intent,
|
||||
PlayerRecord,
|
||||
ServerDesyncSchema,
|
||||
ServerErrorMessage,
|
||||
@@ -21,6 +19,7 @@ import {
|
||||
ServerPrestartMessageSchema,
|
||||
ServerStartGameMessage,
|
||||
ServerTurnMessage,
|
||||
StampedIntent,
|
||||
Turn,
|
||||
} from "../core/Schemas";
|
||||
import { createPartialGameRecord, getClanTag } from "../core/Util";
|
||||
@@ -43,9 +42,11 @@ export class GameServer {
|
||||
private disconnectedTimeout = 1 * 30 * 1000; // 30 seconds
|
||||
|
||||
private turns: Turn[] = [];
|
||||
private intents: Intent[] = [];
|
||||
private intents: StampedIntent[] = [];
|
||||
public activeClients: Client[] = [];
|
||||
private allClients: Map<ClientID, Client> = new Map();
|
||||
// Map persistentID to clientID for reconnection lookup
|
||||
private persistentIdToClientId: Map<string, ClientID> = new Map();
|
||||
private clientsDisconnectedStatus: Map<ClientID, boolean> = new Map();
|
||||
private _hasStarted = false;
|
||||
private _startTime: number | null = null;
|
||||
@@ -63,7 +64,7 @@ export class GameServer {
|
||||
|
||||
private _hasPrestarted = false;
|
||||
|
||||
private kickedClients: Set<ClientID> = new Set();
|
||||
private kickedPersistentIds: Set<string> = new Set();
|
||||
private outOfSyncClients: Set<ClientID> = new Set();
|
||||
|
||||
private isPaused = false;
|
||||
@@ -87,12 +88,18 @@ export class GameServer {
|
||||
public readonly createdAt: number,
|
||||
private config: ServerConfig,
|
||||
public gameConfig: GameConfig,
|
||||
private lobbyCreatorID?: string,
|
||||
private creatorPersistentID?: string,
|
||||
private startsAt?: number,
|
||||
) {
|
||||
this.log = log_.child({ gameID: id });
|
||||
}
|
||||
|
||||
private get lobbyCreatorID(): ClientID | undefined {
|
||||
return this.creatorPersistentID
|
||||
? this.persistentIdToClientId.get(this.creatorPersistentID)
|
||||
: undefined;
|
||||
}
|
||||
|
||||
public updateGameConfig(gameConfig: Partial<GameConfig>): void {
|
||||
if (gameConfig.gameMap !== undefined) {
|
||||
this.gameConfig.gameMap = gameConfig.gameMap;
|
||||
@@ -150,20 +157,24 @@ export class GameServer {
|
||||
}
|
||||
}
|
||||
|
||||
public joinClient(client: Client) {
|
||||
this.websockets.add(client.ws);
|
||||
if (this.kickedClients.has(client.clientID)) {
|
||||
this.log.warn(`cannot add client, already kicked`, {
|
||||
clientID: client.clientID,
|
||||
});
|
||||
return;
|
||||
}
|
||||
private isKicked(clientID: ClientID): boolean {
|
||||
const persistentID = this.allClients.get(clientID)?.persistentID;
|
||||
return (
|
||||
persistentID !== undefined && this.kickedPersistentIds.has(persistentID)
|
||||
);
|
||||
}
|
||||
|
||||
if (this.allClients.has(client.clientID)) {
|
||||
this.log.warn("cannot add client, already in game", {
|
||||
clientID: client.clientID,
|
||||
});
|
||||
return;
|
||||
// Get existing clientID for this persistentID, or null if new player
|
||||
public getClientIdForPersistentId(persistentID: string): ClientID | null {
|
||||
const clientID = this.persistentIdToClientId.get(persistentID);
|
||||
if (!clientID) return null;
|
||||
if (this.kickedPersistentIds.has(persistentID)) return null;
|
||||
return clientID;
|
||||
}
|
||||
|
||||
public joinClient(client: Client): "joined" | "kicked" | "rejected" {
|
||||
if (this.kickedPersistentIds.has(client.persistentID)) {
|
||||
return "kicked";
|
||||
}
|
||||
|
||||
if (
|
||||
@@ -180,16 +191,9 @@ export class GameServer {
|
||||
error: "full-lobby",
|
||||
} satisfies ServerErrorMessage),
|
||||
);
|
||||
return;
|
||||
return "rejected";
|
||||
}
|
||||
|
||||
// Log when lobby creator joins private game
|
||||
if (client.clientID === this.lobbyCreatorID) {
|
||||
this.log.info("Lobby creator joined", {
|
||||
gameID: this.id,
|
||||
creatorID: this.lobbyCreatorID,
|
||||
});
|
||||
}
|
||||
this.log.info("client joining game", {
|
||||
clientID: client.clientID,
|
||||
persistentID: client.persistentID,
|
||||
@@ -206,7 +210,7 @@ export class GameServer {
|
||||
clientID: client.clientID,
|
||||
clientIP: ipAnonymize(client.ip),
|
||||
});
|
||||
return;
|
||||
return "rejected";
|
||||
}
|
||||
|
||||
if (this.config.env() === GameEnv.Prod) {
|
||||
@@ -231,6 +235,8 @@ export class GameServer {
|
||||
}
|
||||
|
||||
// Client connection accepted
|
||||
this.websockets.add(client.ws);
|
||||
this.persistentIdToClientId.set(client.persistentID, client.clientID);
|
||||
this.activeClients.push(client);
|
||||
client.lastPing = Date.now();
|
||||
this.markClientDisconnected(client.clientID, false);
|
||||
@@ -242,54 +248,47 @@ export class GameServer {
|
||||
if (this._hasStarted) {
|
||||
this.sendStartGameMsg(client.ws, 0);
|
||||
}
|
||||
|
||||
return "joined";
|
||||
}
|
||||
|
||||
// Attempt to reconnect a client by persistentID. Returns true if successful.
|
||||
// Only the WebSocket is updated — username, cosmetics, etc. are preserved
|
||||
// from the original join to maintain consistency throughout the game session.
|
||||
public rejoinClient(
|
||||
ws: WebSocket,
|
||||
persistentID: string,
|
||||
msg: ClientRejoinMessage,
|
||||
): void {
|
||||
lastTurn: number = 0,
|
||||
): boolean {
|
||||
const clientID = this.getClientIdForPersistentId(persistentID);
|
||||
if (!clientID) return false;
|
||||
const client = this.allClients.get(clientID);
|
||||
if (!client) return false;
|
||||
|
||||
this.websockets.add(ws);
|
||||
this.log.info("client rejoining", { clientID, lastTurn });
|
||||
|
||||
if (this.kickedClients.has(msg.clientID)) {
|
||||
this.log.warn("cannot rejoin client, client has been kicked", {
|
||||
clientID: msg.clientID,
|
||||
});
|
||||
return;
|
||||
}
|
||||
|
||||
const client = this.allClients.get(msg.clientID);
|
||||
if (!client) {
|
||||
this.log.warn("cannot rejoin client, existing client not found", {
|
||||
clientID: msg.clientID,
|
||||
});
|
||||
return;
|
||||
}
|
||||
|
||||
if (client.persistentID !== persistentID) {
|
||||
this.log.error("persistent ids do not match", {
|
||||
clientID: msg.clientID,
|
||||
clientPersistentID: persistentID,
|
||||
existingIP: ipAnonymize(client.ip),
|
||||
existingPersistentID: client.persistentID,
|
||||
});
|
||||
return;
|
||||
// Close old WebSocket to prevent resource leaks
|
||||
if (client.ws !== ws) {
|
||||
client.ws.removeAllListeners();
|
||||
client.ws.close();
|
||||
}
|
||||
|
||||
this.activeClients = this.activeClients.filter(
|
||||
(c) => c.clientID !== msg.clientID,
|
||||
(c) => c.clientID !== client.clientID,
|
||||
);
|
||||
this.activeClients.push(client);
|
||||
client.lastPing = Date.now();
|
||||
this.markClientDisconnected(msg.clientID, false);
|
||||
this.markClientDisconnected(client.clientID, false);
|
||||
|
||||
client.ws = ws;
|
||||
this.addListeners(client);
|
||||
this.startLobbyInfoBroadcast();
|
||||
|
||||
if (this._hasStarted) {
|
||||
this.sendStartGameMsg(client.ws, msg.lastTurn);
|
||||
this.sendStartGameMsg(client.ws, lastTurn);
|
||||
}
|
||||
return true;
|
||||
}
|
||||
|
||||
private addListeners(client: Client) {
|
||||
@@ -321,13 +320,12 @@ export class GameServer {
|
||||
break;
|
||||
}
|
||||
case "intent": {
|
||||
if (clientMsg.intent.clientID !== client.clientID) {
|
||||
this.log.warn(
|
||||
`client id mismatch, client: ${client.clientID}, intent: ${clientMsg.intent.clientID}`,
|
||||
);
|
||||
return;
|
||||
}
|
||||
switch (clientMsg.intent.type) {
|
||||
// Server stamps clientID from the authenticated connection
|
||||
const stampedIntent = {
|
||||
...clientMsg.intent,
|
||||
clientID: client.clientID,
|
||||
};
|
||||
switch (stampedIntent.type) {
|
||||
case "mark_disconnected": {
|
||||
this.log.warn(
|
||||
`Should not receive mark_disconnected intent from client`,
|
||||
@@ -342,14 +340,14 @@ export class GameServer {
|
||||
this.log.warn(`Only lobby creator can kick players`, {
|
||||
clientID: client.clientID,
|
||||
creatorID: this.lobbyCreatorID,
|
||||
target: clientMsg.intent.target,
|
||||
target: stampedIntent.target,
|
||||
gameID: this.id,
|
||||
});
|
||||
return;
|
||||
}
|
||||
|
||||
// Don't allow lobby creator to kick themselves
|
||||
if (client.clientID === clientMsg.intent.target) {
|
||||
if (client.clientID === stampedIntent.target) {
|
||||
this.log.warn(`Cannot kick yourself`, {
|
||||
clientID: client.clientID,
|
||||
});
|
||||
@@ -359,13 +357,13 @@ export class GameServer {
|
||||
// Log and execute the kick
|
||||
this.log.info(`Lobby creator initiated kick of player`, {
|
||||
creatorID: client.clientID,
|
||||
target: clientMsg.intent.target,
|
||||
target: stampedIntent.target,
|
||||
gameID: this.id,
|
||||
kickMethod: "websocket",
|
||||
});
|
||||
|
||||
this.kickClient(
|
||||
clientMsg.intent.target,
|
||||
stampedIntent.target,
|
||||
KICK_REASON_LOBBY_CREATOR,
|
||||
);
|
||||
return;
|
||||
@@ -400,7 +398,7 @@ export class GameServer {
|
||||
return;
|
||||
}
|
||||
|
||||
if (clientMsg.intent.config.gameType === GameType.Public) {
|
||||
if (stampedIntent.config.gameType === GameType.Public) {
|
||||
this.log.warn(`Cannot update game to public via WebSocket`, {
|
||||
gameID: this.id,
|
||||
clientID: client.clientID,
|
||||
@@ -416,7 +414,7 @@ export class GameServer {
|
||||
},
|
||||
);
|
||||
|
||||
this.updateGameConfig(clientMsg.intent.config);
|
||||
this.updateGameConfig(stampedIntent.config);
|
||||
return;
|
||||
}
|
||||
case "toggle_pause": {
|
||||
@@ -430,15 +428,15 @@ export class GameServer {
|
||||
return;
|
||||
}
|
||||
|
||||
if (clientMsg.intent.paused) {
|
||||
if (stampedIntent.paused) {
|
||||
// Pausing: send intent and complete current turn before pause takes effect
|
||||
this.addIntent(clientMsg.intent);
|
||||
this.addIntent(stampedIntent);
|
||||
this.endTurn();
|
||||
this.isPaused = true;
|
||||
} else {
|
||||
// Unpausing: clear pause flag before sending intent so next turn can execute
|
||||
this.isPaused = false;
|
||||
this.addIntent(clientMsg.intent);
|
||||
this.addIntent(stampedIntent);
|
||||
this.endTurn();
|
||||
}
|
||||
|
||||
@@ -451,7 +449,7 @@ export class GameServer {
|
||||
default: {
|
||||
// Don't process intents while game is paused
|
||||
if (!this.isPaused) {
|
||||
this.addIntent(clientMsg.intent);
|
||||
this.addIntent(stampedIntent);
|
||||
}
|
||||
break;
|
||||
}
|
||||
@@ -501,6 +499,17 @@ export class GameServer {
|
||||
client.ws.close(1002, "WS_ERR_UNEXPECTED_RSV_1");
|
||||
}
|
||||
});
|
||||
|
||||
// Check if WebSocket already closed before we added the listener (race condition)
|
||||
if (client.ws.readyState >= 2) {
|
||||
this.log.info("client WebSocket already closing/closed, removing", {
|
||||
clientID: client.clientID,
|
||||
readyState: client.ws.readyState,
|
||||
});
|
||||
this.activeClients = this.activeClients.filter(
|
||||
(c) => c.clientID !== client.clientID,
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
public numClients(): number {
|
||||
@@ -569,12 +578,14 @@ export class GameServer {
|
||||
}
|
||||
|
||||
private broadcastLobbyInfo() {
|
||||
const msg = JSON.stringify({
|
||||
type: "lobby_info",
|
||||
lobby: this.gameInfo(),
|
||||
} satisfies ServerLobbyInfoMessage);
|
||||
const lobbyInfo = this.gameInfo();
|
||||
this.activeClients.forEach((c) => {
|
||||
if (c.ws.readyState === WebSocket.OPEN) {
|
||||
const msg = JSON.stringify({
|
||||
type: "lobby_info",
|
||||
lobby: lobbyInfo,
|
||||
myClientID: c.clientID,
|
||||
} satisfies ServerLobbyInfoMessage);
|
||||
c.ws.send(msg);
|
||||
}
|
||||
});
|
||||
@@ -621,7 +632,7 @@ export class GameServer {
|
||||
});
|
||||
}
|
||||
|
||||
private addIntent(intent: Intent) {
|
||||
private addIntent(intent: StampedIntent) {
|
||||
this.intents.push(intent);
|
||||
}
|
||||
|
||||
@@ -646,6 +657,7 @@ export class GameServer {
|
||||
turns: this.turns.slice(lastTurn),
|
||||
gameStartInfo: this.gameStartInfo,
|
||||
lobbyCreatedAt: this.createdAt,
|
||||
myClientID: client.clientID,
|
||||
} satisfies ServerStartGameMessage),
|
||||
);
|
||||
} catch (error) {
|
||||
@@ -808,6 +820,7 @@ export class GameServer {
|
||||
username: c.username,
|
||||
clientID: c.clientID,
|
||||
})),
|
||||
lobbyCreatorClientID: this.lobbyCreatorID,
|
||||
gameConfig: this.gameConfig,
|
||||
startsAt: this.startsAt,
|
||||
serverTime: Date.now(),
|
||||
@@ -822,7 +835,7 @@ export class GameServer {
|
||||
clientID: ClientID,
|
||||
reasonKey: string = KICK_REASON_DUPLICATE_SESSION,
|
||||
): void {
|
||||
if (this.kickedClients.has(clientID)) {
|
||||
if (this.isKicked(clientID)) {
|
||||
this.log.warn(`cannot kick client, already kicked`, {
|
||||
clientID,
|
||||
reasonKey,
|
||||
@@ -830,7 +843,8 @@ export class GameServer {
|
||||
return;
|
||||
}
|
||||
|
||||
if (!this.allClients.has(clientID)) {
|
||||
const clientToKick = this.allClients.get(clientID);
|
||||
if (!clientToKick) {
|
||||
this.log.warn(`cannot kick client, not found in game`, {
|
||||
clientID,
|
||||
reasonKey,
|
||||
@@ -838,7 +852,7 @@ export class GameServer {
|
||||
return;
|
||||
}
|
||||
|
||||
this.kickedClients.add(clientID);
|
||||
this.kickedPersistentIds.add(clientToKick.persistentID);
|
||||
|
||||
const client = this.activeClients.find((c) => c.clientID === clientID);
|
||||
if (client) {
|
||||
@@ -1041,7 +1055,7 @@ export class GameServer {
|
||||
private handleWinner(client: Client, clientMsg: ClientSendWinnerMessage) {
|
||||
if (
|
||||
this.outOfSyncClients.has(client.clientID) ||
|
||||
this.kickedClients.has(client.clientID) ||
|
||||
this.isKicked(client.clientID) ||
|
||||
this.winner !== null ||
|
||||
client.reportedWinner !== null
|
||||
) {
|
||||
|
||||
+62
-29
@@ -12,7 +12,6 @@ import { GameType } from "../core/game/Game";
|
||||
import {
|
||||
ClientMessageSchema,
|
||||
GameID,
|
||||
ID,
|
||||
PartialGameRecordSchema,
|
||||
ServerErrorMessage,
|
||||
} from "../core/Schemas";
|
||||
@@ -125,12 +124,27 @@ export async function startWorker() {
|
||||
|
||||
app.post("/api/create_game/:id", async (req, res) => {
|
||||
const id = req.params.id;
|
||||
const creatorClientID = (() => {
|
||||
if (typeof req.query.creatorClientID !== "string") return undefined;
|
||||
|
||||
const trimmed = req.query.creatorClientID.trim();
|
||||
return ID.safeParse(trimmed).success ? trimmed : undefined;
|
||||
})();
|
||||
// Extract persistentID from Authorization header token
|
||||
// Never accept persistentID directly from client
|
||||
let creatorPersistentID: string | undefined;
|
||||
const authHeader = req.headers.authorization;
|
||||
if (authHeader?.startsWith("Bearer ")) {
|
||||
const token = authHeader.substring("Bearer ".length);
|
||||
const result = await verifyClientToken(token, config);
|
||||
if (result.type === "success") {
|
||||
creatorPersistentID = result.persistentId;
|
||||
} else {
|
||||
log.warn(`Invalid creator token: ${result.message}`);
|
||||
return res.status(401).json({ error: "Invalid creator token" });
|
||||
}
|
||||
} else if (
|
||||
!req.headers[config.adminHeader()] // Public games use admin token instead
|
||||
) {
|
||||
return res
|
||||
.status(400)
|
||||
.json({ error: "Authorization header required to create a game" });
|
||||
}
|
||||
|
||||
if (!id) {
|
||||
log.warn(`cannot create game, id not found`);
|
||||
@@ -164,11 +178,11 @@ export async function startWorker() {
|
||||
return res.status(400).json({ error: "Worker, game id mismatch" });
|
||||
}
|
||||
|
||||
// Pass creatorClientID to createGame
|
||||
const game = gm.createGame(id, gc, creatorClientID);
|
||||
// Pass creatorPersistentID to createGame
|
||||
const game = gm.createGame(id, gc, creatorPersistentID);
|
||||
|
||||
log.info(
|
||||
`Worker ${workerId}: IP ${ipAnonymize(clientIP)} creating ${game.isPublic() ? "Public" : "Private"}${gc?.gameMode ? ` ${gc.gameMode}` : ""} game with id ${id}${creatorClientID ? `, creator: ${creatorClientID}` : ""}`,
|
||||
`Worker ${workerId}: IP ${ipAnonymize(clientIP)} creating ${game.isPublic() ? "Public" : "Private"}${gc?.gameMode ? ` ${gc.gameMode}` : ""} game with id ${id}${creatorPersistentID ? `, creator: ${creatorPersistentID.substring(0, 8)}...` : ""}`,
|
||||
);
|
||||
res.json(game.gameInfo());
|
||||
});
|
||||
@@ -311,12 +325,9 @@ export async function startWorker() {
|
||||
const result = await verifyClientToken(clientMsg.token, config);
|
||||
if (result.type === "error") {
|
||||
log.warn(`Invalid token: ${result.message}`, {
|
||||
clientID: clientMsg.clientID,
|
||||
gameID: clientMsg.gameID,
|
||||
});
|
||||
ws.close(
|
||||
1002,
|
||||
`Unauthorized: invalid token for client ${clientMsg.clientID}`,
|
||||
);
|
||||
ws.close(1002, `Unauthorized: invalid token`);
|
||||
return;
|
||||
}
|
||||
const { persistentId, claims } = result;
|
||||
@@ -324,11 +335,14 @@ export async function startWorker() {
|
||||
if (clientMsg.type === "rejoin") {
|
||||
log.info("rejoining game", {
|
||||
gameID: clientMsg.gameID,
|
||||
clientID: clientMsg.clientID,
|
||||
persistentID: persistentId,
|
||||
});
|
||||
const wasFound = gm.rejoinClient(ws, persistentId, clientMsg);
|
||||
|
||||
const wasFound = gm.rejoinClient(
|
||||
ws,
|
||||
persistentId,
|
||||
clientMsg.gameID,
|
||||
clientMsg.lastTurn,
|
||||
);
|
||||
if (!wasFound) {
|
||||
log.warn(
|
||||
`game ${clientMsg.gameID} not found on worker ${workerId}`,
|
||||
@@ -338,6 +352,12 @@ export async function startWorker() {
|
||||
return;
|
||||
}
|
||||
|
||||
// Try to reconnect an existing client (e.g., page refresh)
|
||||
// If successful, skip all authorization
|
||||
if (gm.rejoinClient(ws, persistentId, clientMsg.gameID)) {
|
||||
return;
|
||||
}
|
||||
|
||||
let roles: string[] | undefined;
|
||||
let flares: string[] | undefined;
|
||||
|
||||
@@ -353,12 +373,10 @@ export async function startWorker() {
|
||||
const result = await getUserMe(clientMsg.token, config);
|
||||
if (result.type === "error") {
|
||||
log.warn(`Unauthorized: ${result.message}`, {
|
||||
clientID: clientMsg.clientID,
|
||||
persistentID: persistentId,
|
||||
gameID: clientMsg.gameID,
|
||||
});
|
||||
ws.close(
|
||||
1002,
|
||||
`Unauthorized: user me fetch failed for client ${clientMsg.clientID}`,
|
||||
);
|
||||
ws.close(1002, "Unauthorized: user me fetch failed");
|
||||
return;
|
||||
}
|
||||
roles = result.response.player.roles;
|
||||
@@ -384,7 +402,8 @@ export async function startWorker() {
|
||||
|
||||
if (cosmeticResult.type === "forbidden") {
|
||||
log.warn(`Forbidden: ${cosmeticResult.reason}`, {
|
||||
clientID: clientMsg.clientID,
|
||||
persistentID: persistentId,
|
||||
gameID: clientMsg.gameID,
|
||||
});
|
||||
ws.close(1002, cosmeticResult.reason);
|
||||
return;
|
||||
@@ -401,7 +420,8 @@ export async function startWorker() {
|
||||
break;
|
||||
case "rejected":
|
||||
log.warn("Unauthorized: Turnstile token rejected", {
|
||||
clientID: clientMsg.clientID,
|
||||
persistentID: persistentId,
|
||||
gameID: clientMsg.gameID,
|
||||
reason: turnstileResult.reason,
|
||||
});
|
||||
ws.close(1002, "Unauthorized: Turnstile token rejected");
|
||||
@@ -409,7 +429,8 @@ export async function startWorker() {
|
||||
case "error":
|
||||
// Fail open, allow the client to join.
|
||||
log.error("Turnstile token error", {
|
||||
clientID: clientMsg.clientID,
|
||||
persistentID: persistentId,
|
||||
gameID: clientMsg.gameID,
|
||||
reason: turnstileResult.reason,
|
||||
});
|
||||
}
|
||||
@@ -417,7 +438,7 @@ export async function startWorker() {
|
||||
|
||||
// Create client and add to game
|
||||
const client = new Client(
|
||||
clientMsg.clientID,
|
||||
generateID(),
|
||||
persistentId,
|
||||
claims,
|
||||
roles,
|
||||
@@ -428,11 +449,23 @@ export async function startWorker() {
|
||||
cosmeticResult.cosmetics,
|
||||
);
|
||||
|
||||
const wasFound = gm.joinClient(client, clientMsg.gameID);
|
||||
const joinResult = gm.joinClient(client, clientMsg.gameID);
|
||||
|
||||
if (!wasFound) {
|
||||
if (joinResult === "not_found") {
|
||||
log.info(`game ${clientMsg.gameID} not found on worker ${workerId}`);
|
||||
// Handle game not found case
|
||||
ws.close(1002, "Game not found");
|
||||
} else if (joinResult === "kicked") {
|
||||
log.warn(`kicked client tried to join game ${clientMsg.gameID}`, {
|
||||
gameID: clientMsg.gameID,
|
||||
workerId,
|
||||
});
|
||||
ws.close(1002, "Cannot join game");
|
||||
} else if (joinResult === "rejected") {
|
||||
log.info(`client rejected from game ${clientMsg.gameID}`, {
|
||||
gameID: clientMsg.gameID,
|
||||
workerId,
|
||||
});
|
||||
ws.close(1002, "Lobby full");
|
||||
}
|
||||
|
||||
// Handle other message types
|
||||
|
||||
Reference in New Issue
Block a user