created prestart message: add 2 second delay before starting public games to allow all players to connect

This commit is contained in:
Evan
2025-04-03 15:42:25 -07:00
parent f09801fe3e
commit 950344c0bd
8 changed files with 105 additions and 33 deletions
+27 -9
View File
@@ -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<TerrainMapData> | 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<TerrainMapData> | null,
): Promise<ClientGameRunner> {
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,
+9 -8
View File
@@ -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}`);
+1 -1
View File
@@ -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";
+10 -3
View File
@@ -64,7 +64,8 @@ export type ServerMessage =
| ServerSyncMessage
| ServerStartGameMessage
| ServerPingMessage
| ServerDesyncMessage;
| ServerDesyncMessage
| ServerPrestartMessage;
export type ServerSyncMessage = z.infer<typeof ServerTurnMessageSchema>;
export type ServerStartGameMessage = z.infer<
@@ -72,7 +73,7 @@ export type ServerStartGameMessage = z.infer<
>;
export type ServerPingMessage = z.infer<typeof ServerPingMessageSchema>;
export type ServerDesyncMessage = z.infer<typeof ServerDesyncSchema>;
export type ServerPrestartMessage = z.infer<typeof ServerPrestartMessageSchema>;
export type ClientSendWinnerMessage = z.infer<typeof ClientSendWinnerSchema>;
export type ClientPingMessage = z.infer<typeof ClientPingMessageSchema>;
export type ClientIntentMessage = z.infer<typeof ClientIntentMessageSchema>;
@@ -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,
+8 -5
View File
@@ -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<GameMapType, TerrainMapData>();
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<TerrainMapData> {
if (loadedMaps.has(map)) {
return loadedMaps.get(map);
}
+10 -5
View File
@@ -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);
}
}
+36 -2
View File
@@ -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) {
+4
View File
@@ -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,