From 950344c0bd62a0941882a5e3ccb6f930cf49a299 Mon Sep 17 00:00:00 2001 From: Evan Date: Thu, 3 Apr 2025 15:42:25 -0700 Subject: [PATCH] created prestart message: add 2 second delay before starting public games to allow all players to connect --- src/client/ClientGameRunner.ts | 36 ++++++++++++++++++++------- src/client/Main.ts | 17 +++++++------ src/client/graphics/GameRenderer.ts | 2 +- src/core/Schemas.ts | 13 +++++++--- src/core/game/TerrainMapLoader.ts | 13 ++++++---- src/server/GameManager.ts | 15 ++++++++---- src/server/GameServer.ts | 38 +++++++++++++++++++++++++++-- src/server/Worker.ts | 4 +++ 8 files changed, 105 insertions(+), 33 deletions(-) diff --git a/src/client/ClientGameRunner.ts b/src/client/ClientGameRunner.ts index 534b693a5..555272e8e 100644 --- a/src/client/ClientGameRunner.ts +++ b/src/client/ClientGameRunner.ts @@ -21,7 +21,7 @@ import { WinUpdate, } from "../core/game/GameUpdates"; import { GameView, PlayerView, UnitView } from "../core/game/GameView"; -import { loadTerrainMap } from "../core/game/TerrainMapLoader"; +import { loadTerrainMap, TerrainMapData } from "../core/game/TerrainMapLoader"; import { UserSettings } from "../core/game/UserSettings"; import { WorkerClient } from "../core/worker/WorkerClient"; import { InputHandler, MouseMoveEvent, MouseUpEvent } from "./InputHandler"; @@ -60,7 +60,8 @@ export interface LobbyConfig { export function joinLobby( lobbyConfig: LobbyConfig, - onjoin: () => void, + onPrestart: () => void, + onJoin: () => void, ): () => void { const eventBus = new EventBus(); initRemoteSender(eventBus); @@ -78,15 +79,28 @@ export function joinLobby( consolex.log(`Joined game lobby ${lobbyConfig.gameID}`); transport.joinGame(0); }; + let terrainLoad: Promise | null = null; + const onmessage = (message: ServerMessage) => { + if (message.type == "prestart") { + consolex.log(`lobby: game prestarting: ${JSON.stringify(message)}`); + terrainLoad = loadTerrainMap(message.gameMap); + onPrestart(); + } if (message.type == "start") { + // Trigger prestart for singleplayer games + onPrestart(); consolex.log(`lobby: game started: ${JSON.stringify(message)}`); - onjoin(); + onJoin(); // For multiplayer games, GameStartInfo is not known until game starts. lobbyConfig.gameStartInfo = message.gameStartInfo; - createClientGame(lobbyConfig, eventBus, transport, userSettings).then( - (r) => r.start(), - ); + createClientGame( + lobbyConfig, + eventBus, + transport, + userSettings, + terrainLoad, + ).then((r) => r.start()); } }; transport.connect(onconnect, onmessage); @@ -101,15 +115,19 @@ export async function createClientGame( eventBus: EventBus, transport: Transport, userSettings: UserSettings, + terrainLoad: Promise | null, ): Promise { const config = await getConfig( lobbyConfig.gameStartInfo.config, userSettings, ); + let gameMap: TerrainMapData | null = null; - const gameMap = await loadTerrainMap( - lobbyConfig.gameStartInfo.config.gameMap, - ); + if (terrainLoad) { + gameMap = await terrainLoad; + } else { + gameMap = await loadTerrainMap(lobbyConfig.gameStartInfo.config.gameMap); + } const worker = new WorkerClient( lobbyConfig.gameStartInfo, lobbyConfig.clientID, diff --git a/src/client/Main.ts b/src/client/Main.ts index a301232ce..e7436e820 100644 --- a/src/client/Main.ts +++ b/src/client/Main.ts @@ -10,6 +10,7 @@ import "./DarkModeButton"; import { DarkModeButton } from "./DarkModeButton"; import "./FlagInput"; import { FlagInput } from "./FlagInput"; +import { GameStartingModal } from "./GameStartingModal"; import "./GoogleAdElement"; import GoogleAdElement from "./GoogleAdElement"; import { HelpModal } from "./HelpModal"; @@ -25,7 +26,6 @@ import { UsernameInput } from "./UsernameInput"; import { generateCryptoRandomUUID } from "./Utils"; import "./components/baseComponents/Button"; import "./components/baseComponents/Modal"; -import { GameStartingModal } from "./gameStartingModal"; import "./styles.css"; export interface JoinLobbyEvent { @@ -177,6 +177,7 @@ class Client { this.gameStop(); } const config = await getServerConfigFromClient(); + this.gameStop = joinLobby( { gameID: lobby.gameID, @@ -192,18 +193,18 @@ class Client { gameRecord: lobby.gameRecord, }, () => { - this.joinModal.close(); - this.publicLobby.stop(); - document.querySelectorAll(".ad").forEach((ad) => { - (ad as HTMLElement).style.display = "none"; - }); - - // show when the game loads const startingModal = document.querySelector( "game-starting-modal", ) as GameStartingModal; startingModal instanceof GameStartingModal; startingModal.show(); + }, + () => { + this.joinModal.close(); + this.publicLobby.stop(); + document.querySelectorAll(".ad").forEach((ad) => { + (ad as HTMLElement).style.display = "none"; + }); if (event.detail.gameConfig?.gameType != GameType.Singleplayer) { window.history.pushState({}, "", `/join/${lobby.gameID}`); diff --git a/src/client/graphics/GameRenderer.ts b/src/client/graphics/GameRenderer.ts index 6af009d13..5f76b79e7 100644 --- a/src/client/graphics/GameRenderer.ts +++ b/src/client/graphics/GameRenderer.ts @@ -2,8 +2,8 @@ import { consolex } from "../../core/Consolex"; import { EventBus } from "../../core/EventBus"; import { ClientID } from "../../core/Schemas"; import { GameView } from "../../core/game/GameView"; +import { GameStartingModal } from "../GameStartingModal"; import { RefreshGraphicsEvent as RedrawGraphicsEvent } from "../InputHandler"; -import { GameStartingModal } from "../gameStartingModal"; import { TransformHandler } from "./TransformHandler"; import { UIState } from "./UIState"; import { BuildMenu } from "./layers/BuildMenu"; diff --git a/src/core/Schemas.ts b/src/core/Schemas.ts index 002273531..71c28ac99 100644 --- a/src/core/Schemas.ts +++ b/src/core/Schemas.ts @@ -64,7 +64,8 @@ export type ServerMessage = | ServerSyncMessage | ServerStartGameMessage | ServerPingMessage - | ServerDesyncMessage; + | ServerDesyncMessage + | ServerPrestartMessage; export type ServerSyncMessage = z.infer; export type ServerStartGameMessage = z.infer< @@ -72,7 +73,7 @@ export type ServerStartGameMessage = z.infer< >; export type ServerPingMessage = z.infer; export type ServerDesyncMessage = z.infer; - +export type ServerPrestartMessage = z.infer; export type ClientSendWinnerMessage = z.infer; export type ClientPingMessage = z.infer; export type ClientIntentMessage = z.infer; @@ -298,7 +299,7 @@ export const TurnSchema = z.object({ // Server const ServerBaseMessageSchema = z.object({ - type: z.enum(["turn", "ping", "start", "desync"]), + type: z.enum(["turn", "ping", "prestart", "start", "desync"]), }); export const ServerTurnMessageSchema = ServerBaseMessageSchema.extend({ @@ -310,6 +311,11 @@ export const ServerPingMessageSchema = ServerBaseMessageSchema.extend({ type: z.literal("ping"), }); +export const ServerPrestartMessageSchema = ServerBaseMessageSchema.extend({ + type: z.literal("prestart"), + gameMap: z.nativeEnum(GameMapType), +}); + export const PlayerSchema = z.object({ playerID: ID, clientID: ID, @@ -341,6 +347,7 @@ export const ServerDesyncSchema = ServerBaseMessageSchema.extend({ export const ServerMessageSchema = z.union([ ServerTurnMessageSchema, + ServerPrestartMessageSchema, ServerStartGameMessageSchema, ServerPingMessageSchema, ServerDesyncSchema, diff --git a/src/core/game/TerrainMapLoader.ts b/src/core/game/TerrainMapLoader.ts index 710f3d929..98216d8a1 100644 --- a/src/core/game/TerrainMapLoader.ts +++ b/src/core/game/TerrainMapLoader.ts @@ -3,10 +3,13 @@ import { GameMapType } from "./Game"; import { GameMap, GameMapImpl } from "./GameMap"; import { terrainMapFileLoader } from "./TerrainMapFileLoader"; -const loadedMaps = new Map< - GameMapType, - { nationMap: NationMap; gameMap: GameMap; miniGameMap: GameMap } ->(); +export type TerrainMapData = { + nationMap: NationMap; + gameMap: GameMap; + miniGameMap: GameMap; +}; + +const loadedMaps = new Map(); export interface NationMap { nations: Nation[]; @@ -21,7 +24,7 @@ export interface Nation { export async function loadTerrainMap( map: GameMapType, -): Promise<{ nationMap: NationMap; gameMap: GameMap; miniGameMap: GameMap }> { +): Promise { if (loadedMaps.has(map)) { return loadedMaps.get(map); } diff --git a/src/server/GameManager.ts b/src/server/GameManager.ts index d73811532..2a4e196c5 100644 --- a/src/server/GameManager.ts +++ b/src/server/GameManager.ts @@ -64,11 +64,16 @@ export class GameManager { const phase = game.phase(); if (phase == GamePhase.Active) { if (!game.hasStarted()) { - try { - game.start(); - } catch (error) { - this.log.error(`error starting game ${id}: ${error}`); - } + // Prestart tells clients to start loading the game. + game.prestart(); + // Start game on delay to allow time for clients to connect. + setTimeout(() => { + try { + game.start(); + } catch (error) { + this.log.error(`error starting game ${id}: ${error}`); + } + }, 2000); } } diff --git a/src/server/GameServer.ts b/src/server/GameServer.ts index 13bb64460..bf3db8566 100644 --- a/src/server/GameServer.ts +++ b/src/server/GameServer.ts @@ -13,6 +13,7 @@ import { Intent, PlayerRecord, ServerDesyncSchema, + ServerPrestartMessageSchema, ServerStartGameMessageSchema, ServerTurnMessageSchema, Turn, @@ -55,6 +56,8 @@ export class GameServer { private log: Logger; + private _hasPrestarted = false; + constructor( public readonly id: string, readonly log_: Logger, @@ -219,7 +222,38 @@ export class GameServer { } } + public prestart() { + if (this.hasStarted()) { + return; + } + this._hasPrestarted = true; + + const prestartMsg = ServerPrestartMessageSchema.safeParse({ + type: "prestart", + gameMap: this.gameConfig.gameMap, + }); + + if (!prestartMsg.success) { + console.error( + `error creating prestart message for game ${this.id}, ${prestartMsg.error}`.substring( + 0, + 250, + ), + ); + return; + } + + const msg = JSON.stringify(prestartMsg.data); + this.activeClients.forEach((c) => { + this.log.info(`${this.id}: sending prestart message to ${c.clientID}`); + c.ws.send(msg); + }); + } + public start() { + if (this._hasStarted) { + return; + } this._hasStarted = true; this._startTime = Date.now(); // Set last ping to start so we don't immediately stop the game @@ -424,7 +458,7 @@ export class GameServer { } hasStarted(): boolean { - return this._hasStarted; + return this._hasStarted || this._hasPrestarted; } public gameInfo(): GameInfo { @@ -446,7 +480,7 @@ export class GameServer { } private handleSynchronization() { - if (this.activeClients.length < 1) { + if (this.activeClients.length <= 1) { return; } if (this.turns.length % 10 != 0 || this.turns.length < 10) { diff --git a/src/server/Worker.ts b/src/server/Worker.ts index 25d178c8a..32ece2100 100644 --- a/src/server/Worker.ts +++ b/src/server/Worker.ts @@ -154,6 +154,10 @@ export function startWorker() { log.warn(`cannot update public game ${game.id}, ip: ${clientIP}`); return res.status(400); } + if (game.hasStarted()) { + log.warn(`cannot update game ${game.id} after it has started`); + return res.status(400); + } game.updateGameConfig({ gameMap: req.body.gameMap, difficulty: req.body.difficulty,