diff --git a/src/client/ClientGameRunner.ts b/src/client/ClientGameRunner.ts index f782ee0b9..d0c4de33c 100644 --- a/src/client/ClientGameRunner.ts +++ b/src/client/ClientGameRunner.ts @@ -112,6 +112,7 @@ export async function createClientGame( const config = await getConfig( lobbyConfig.gameStartInfo.config, userSettings, + lobbyConfig.gameRecord != null, ); let gameMap: TerrainMapData | null = null; @@ -243,6 +244,7 @@ export class ClientGameRunner { this.stop(true); return; } + this.transport.turnComplete(); gu.updates[GameUpdateType.Hash].forEach((hu: HashUpdate) => { this.eventBus.emit(new SendHashEvent(hu.tick, hu.hash)); }); diff --git a/src/client/LocalServer.ts b/src/client/LocalServer.ts index 42a777129..852d6fb64 100644 --- a/src/client/LocalServer.ts +++ b/src/client/LocalServer.ts @@ -16,42 +16,58 @@ import { LobbyConfig } from "./ClientGameRunner"; import { getPersistentIDFromCookie } from "./Main"; export class LocalServer { + // All turns from the game record on replay. + private replayTurns: Turn[] = []; + private turns: Turn[] = []; + private intents: Intent[] = []; private startedAt: number; - private endTurnIntervalID; - private paused = false; private winner: ClientSendWinnerMessage = null; private allPlayersStats: AllPlayersStats = {}; + private turnsExecuted = 0; + private lastTurnCompletedTime = 0; + + private turnCheckInterval: NodeJS.Timeout; + constructor( private lobbyConfig: LobbyConfig, private clientConnect: () => void, private clientMessage: (message: ServerMessage) => void, + private isReplay: boolean, ) {} start() { + this.turnCheckInterval = setInterval(() => { + if (this.turnsExecuted == this.turns.length) { + if ( + this.isReplay || + Date.now() > + this.lastTurnCompletedTime + + this.lobbyConfig.serverConfig.turnIntervalMs() + ) { + this.endTurn(); + } + } + }, 5); + this.startedAt = Date.now(); - if (!this.lobbyConfig.gameRecord) { - this.endTurnIntervalID = setInterval( - () => this.endTurn(), - this.lobbyConfig.serverConfig.turnIntervalMs(), - ); - } this.clientConnect(); if (this.lobbyConfig.gameRecord) { - this.turns = decompressGameRecord(this.lobbyConfig.gameRecord).turns; - console.log(`loaded turns: ${JSON.stringify(this.turns)}`); + this.replayTurns = decompressGameRecord( + this.lobbyConfig.gameRecord, + ).turns; } this.clientMessage( ServerStartGameMessageSchema.parse({ type: "start", gameID: this.lobbyConfig.gameStartInfo.gameID, gameStartInfo: this.lobbyConfig.gameStartInfo, - turns: this.turns, + turns: [], }), ); } @@ -90,7 +106,7 @@ export class LocalServer { return; } // If we are replaying a game then verify hash. - const archivedHash = this.turns[clientMsg.turnNumber].hash; + const archivedHash = this.replayTurns[clientMsg.turnNumber].hash; if (!archivedHash) { console.warn( `no archived hash found for turn ${clientMsg.turnNumber}, client hash: ${clientMsg.hash}`, @@ -121,10 +137,18 @@ export class LocalServer { } } + public turnComplete() { + this.turnsExecuted++; + this.lastTurnCompletedTime = Date.now(); + } + private endTurn() { if (this.paused) { return; } + if (this.replayTurns.length > 0) { + this.intents = this.replayTurns[this.turns.length].intents; + } const pastTurn: Turn = { turnNumber: this.turns.length, intents: this.intents, @@ -139,7 +163,7 @@ export class LocalServer { public endGame(saveFullGame: boolean = false) { consolex.log("local server ending game"); - clearInterval(this.endTurnIntervalID); + clearInterval(this.turnCheckInterval); const players: PlayerRecord[] = [ { ip: null, diff --git a/src/client/Transport.ts b/src/client/Transport.ts index 73c9fee97..6c52e4e17 100644 --- a/src/client/Transport.ts +++ b/src/client/Transport.ts @@ -263,7 +263,12 @@ export class Transport { onconnect: () => void, onmessage: (message: ServerMessage) => void, ) { - this.localServer = new LocalServer(this.lobbyConfig, onconnect, onmessage); + this.localServer = new LocalServer( + this.lobbyConfig, + onconnect, + onmessage, + this.lobbyConfig.gameRecord != null, + ); this.localServer.start(); } @@ -318,6 +323,12 @@ export class Transport { this.connect(this.onconnect, this.onmessage); } + public turnComplete() { + if (this.isLocal) { + this.localServer.turnComplete(); + } + } + private onSendLogEvent(event: SendLogEvent) { this.sendMsg( JSON.stringify({ diff --git a/src/client/graphics/layers/OptionsMenu.ts b/src/client/graphics/layers/OptionsMenu.ts index cad75e929..5d69da364 100644 --- a/src/client/graphics/layers/OptionsMenu.ts +++ b/src/client/graphics/layers/OptionsMenu.ts @@ -122,7 +122,8 @@ export class OptionsMenu extends LitElement implements Layer { init() { console.log("init called from OptionsMenu"); this.showPauseButton = - this.game.config().gameConfig().gameType == GameType.Singleplayer; + this.game.config().gameConfig().gameType == GameType.Singleplayer || + this.game.config().isReplay(); this.isVisible = true; this.requestUpdate(); } diff --git a/src/core/configuration/Config.ts b/src/core/configuration/Config.ts index 93e2f54c9..26564eaf5 100644 --- a/src/core/configuration/Config.ts +++ b/src/core/configuration/Config.ts @@ -136,6 +136,7 @@ export interface Config { defaultNukeSpeed(): number; nukeDeathFactor(humans: number, tilesOwned: number): number; structureMinDist(): number; + isReplay(): boolean; } export interface Theme { diff --git a/src/core/configuration/ConfigLoader.ts b/src/core/configuration/ConfigLoader.ts index 3954e6a4c..342b425fc 100644 --- a/src/core/configuration/ConfigLoader.ts +++ b/src/core/configuration/ConfigLoader.ts @@ -12,15 +12,16 @@ export let cachedSC: ServerConfig = null; export async function getConfig( gameConfig: GameConfig, userSettings: UserSettings | null = null, + isReplay: boolean = false, ): Promise { const sc = await getServerConfigFromClient(); switch (sc.env()) { case GameEnv.Dev: - return new DevConfig(sc, gameConfig, userSettings); + return new DevConfig(sc, gameConfig, userSettings, isReplay); case GameEnv.Preprod: case GameEnv.Prod: consolex.log("using prod config"); - return new DefaultConfig(sc, gameConfig, userSettings); + return new DefaultConfig(sc, gameConfig, userSettings, isReplay); default: throw Error(`unsupported server configuration: ${process.env.GAME_ENV}`); } diff --git a/src/core/configuration/DefaultConfig.ts b/src/core/configuration/DefaultConfig.ts index c9a4d09e0..12c1b63d8 100644 --- a/src/core/configuration/DefaultConfig.ts +++ b/src/core/configuration/DefaultConfig.ts @@ -158,7 +158,11 @@ export class DefaultConfig implements Config { private _serverConfig: ServerConfig, private _gameConfig: GameConfig, private _userSettings: UserSettings, + private _isReplay: boolean, ) {} + isReplay(): boolean { + return this._isReplay; + } samHittingChance(): number { return 0.8; diff --git a/src/core/configuration/DevConfig.ts b/src/core/configuration/DevConfig.ts index 909e3a156..0f6f0f019 100644 --- a/src/core/configuration/DevConfig.ts +++ b/src/core/configuration/DevConfig.ts @@ -41,8 +41,13 @@ export class DevServerConfig extends DefaultServerConfig { } export class DevConfig extends DefaultConfig { - constructor(sc: ServerConfig, gc: GameConfig, us: UserSettings) { - super(sc, gc, us); + constructor( + sc: ServerConfig, + gc: GameConfig, + us: UserSettings, + isReplay: boolean, + ) { + super(sc, gc, us, isReplay); } // numSpawnPhaseTurns(): number { diff --git a/tests/util/Setup.ts b/tests/util/Setup.ts index b77f74112..8168870e4 100644 --- a/tests/util/Setup.ts +++ b/tests/util/Setup.ts @@ -42,7 +42,12 @@ export async function setup( instantBuild: false, ..._gameConfig, }; - const config = new TestConfig(serverConfig, gameConfig, new UserSettings()); + const config = new TestConfig( + serverConfig, + gameConfig, + new UserSettings(), + false, + ); // Create and return the game return createGame(humans, [], gameMap, miniGameMap, config);