diff --git a/src/client/ClientGameRunner.ts b/src/client/ClientGameRunner.ts index 7c34dcb5d..49f2fc5a0 100644 --- a/src/client/ClientGameRunner.ts +++ b/src/client/ClientGameRunner.ts @@ -1,46 +1,16 @@ -import { Executor } from "../core/execution/ExecutionManager"; -import { - Cell, - Game, - PlayerID, - GameMapType, - Difficulty, - GameType, -} from "../core/game/Game"; +import { PlayerID, GameMapType, Difficulty, GameType } from "../core/game/Game"; import { EventBus } from "../core/EventBus"; import { createRenderer, GameRenderer } from "./graphics/GameRenderer"; -import { - InputHandler, - MouseUpEvent, - ZoomEvent, - DragEvent, - MouseDownEvent, -} from "./InputHandler"; -import { - ClientID, - ClientIntentMessageSchema, - ClientJoinMessageSchema, - ClientMessageSchema, - GameConfig, - GameID, - Intent, - ServerMessage, - ServerMessageSchema, - ServerSyncMessage, - Turn, -} from "../core/Schemas"; -import { - loadTerrainFromFile, - loadTerrainMap, -} from "../core/game/TerrainMapLoader"; +import { InputHandler, MouseUpEvent } from "./InputHandler"; +import { ClientID, GameConfig, GameID, ServerMessage } from "../core/Schemas"; +import { loadTerrainMap } from "../core/game/TerrainMapLoader"; import { SendAttackIntentEvent, SendSpawnIntentEvent, Transport, } from "./Transport"; import { createCanvas } from "./Utils"; -import { MessageType } from "../core/game/Game"; -import { DisplayMessageUpdate, ErrorUpdate } from "../core/game/GameUpdates"; +import { ErrorUpdate } from "../core/game/GameUpdates"; import { WorkerClient } from "../core/worker/WorkerClient"; import { consolex, initRemoteSender } from "../core/Consolex"; import { getConfig, getServerConfig } from "../core/configuration/Config"; @@ -219,6 +189,14 @@ export class ClientGameRunner { if (turn.turnNumber < this.turnsSeen) { continue; } + while (turn.turnNumber - 1 > this.turnsSeen) { + this.worker.sendTurn({ + turnNumber: this.turnsSeen, + gameID: turn.gameID, + intents: [], + }); + this.turnsSeen++; + } this.worker.sendTurn(turn); this.turnsSeen++; } diff --git a/src/core/execution/PlayerExecution.ts b/src/core/execution/PlayerExecution.ts index 3ee17aad8..11e191632 100644 --- a/src/core/execution/PlayerExecution.ts +++ b/src/core/execution/PlayerExecution.ts @@ -121,15 +121,15 @@ export class PlayerExecution implements Execution { private surroundedBySamePlayer(cluster: Set): false | Player { const enemies = new Set(); - for (const ref of cluster) { + for (const tile of cluster) { if ( - this.mg.isOceanShore(ref) || - this.mg.neighbors(ref).some((n) => !this.mg.hasOwner(n)) + this.mg.isOceanShore(tile) || + this.mg.neighbors(tile).some((n) => !this.mg.hasOwner(n)) ) { return false; } this.mg - .neighbors(ref) + .neighbors(tile) .filter((n) => this.mg.ownerID(n) != this.player.smallID()) .forEach((p) => enemies.add(this.mg.ownerID(p))); if (enemies.size != 1) { diff --git a/src/server/Archive.ts b/src/server/Archive.ts index 61d57d7f8..1c08b33d2 100644 --- a/src/server/Archive.ts +++ b/src/server/Archive.ts @@ -128,6 +128,52 @@ async function archiveToGCS(gameRecord: GameRecord) { console.log(`${gameRecord.id}: game record successfully written to GCS`); } +export async function readGameRecord(gameId: GameID): Promise { + try { + const file = bucket.file(gameId); + + // Check if file exists + const [exists] = await file.exists(); + if (!exists) { + throw new Error(`Game record ${gameId} not found in GCS`); + } + + // Download and parse file content + const [content] = await file.download(); + const gameRecord = JSON.parse(content.toString()); + + // Validate the parsed content against the schema + const validatedRecord = GameRecordSchema.parse(gameRecord); + + console.log(`${gameId}: Successfully read game record from GCS`); + return validatedRecord; + } catch (error) { + console.error(`${gameId}: Error reading game record from GCS: ${error}`, { + message: error?.message || error, + stack: error?.stack, + name: error?.name, + ...(error && typeof error === "object" ? error : {}), + }); + throw error; + } +} + +export async function gameRecordExists(gameId: GameID): Promise { + try { + const file = bucket.file(gameId); + const [exists] = await file.exists(); + return exists; + } catch (error) { + console.error(`${gameId}: Error checking archive existence: ${error}`, { + message: error?.message || error, + stack: error?.stack, + name: error?.name, + ...(error && typeof error === "object" ? error : {}), + }); + return false; + } +} + function anonymizeIPv4(ipv4: string): string | null { const ipv4Regex = /^(\d{1,3}\.){3}\d{1,3}$/; diff --git a/src/server/GameManager.ts b/src/server/GameManager.ts index 49de68163..f2d212932 100644 --- a/src/server/GameManager.ts +++ b/src/server/GameManager.ts @@ -24,13 +24,13 @@ export class GameManager { return this.games.filter((g) => g.phase() == phase); } - addClient(client: Client, gameID: GameID, lastTurn: number) { + addClient(client: Client, gameID: GameID, lastTurn: number): boolean { const game = this.games.find((g) => g.id == gameID); - if (!game) { - console.log(`game id ${gameID} not found`); - return; + if (game) { + game.addClient(client, lastTurn); + return true; } - game.addClient(client, lastTurn); + return false; } updateGameConfig(gameID: GameID, gameConfig: GameConfig) { diff --git a/src/server/Server.ts b/src/server/Server.ts index 51c983253..b24495b21 100644 --- a/src/server/Server.ts +++ b/src/server/Server.ts @@ -10,6 +10,7 @@ import { GameRecord, GameRecordSchema, LogSeverity, + ServerStartGameMessageSchema, } from "../core/Schemas"; import { GameEnv, @@ -19,7 +20,7 @@ import { import { slog } from "./StructuredLog"; import { Client } from "./Client"; import { GamePhase, GameServer } from "./GameServer"; -import { archive } from "./Archive"; +import { archive, gameRecordExists, readGameRecord } from "./Archive"; import { DiscordBot } from "./DiscordBot"; import { sanitizeUsername, @@ -176,13 +177,14 @@ app.put("/private_lobby/:id", (req, res) => { }); }); -app.get("/lobby/:id/exists", (req, res) => { +app.get("/lobby/:id/exists", async (req, res) => { const lobbyId = req.params.id; - console.log(`checking lobby ${lobbyId} exists`); - const lobbyExists = gm.hasActiveGame(lobbyId); - + let gameExists = gm.hasActiveGame(lobbyId); + if (!gameExists) { + gameExists = await gameRecordExists(lobbyId); + } res.json({ - exists: lobbyExists, + exists: gameExists, }); }); @@ -212,7 +214,7 @@ app.get("*", function (req, res) { }); wss.on("connection", (ws, req) => { - ws.on("message", (message: string) => { + ws.on("message", async (message: string) => { try { const clientMsg: ClientMessage = ClientMessageSchema.parse( JSON.parse(message), @@ -233,7 +235,7 @@ wss.on("connection", (ws, req) => { return; } clientMsg.username = sanitizeUsername(clientMsg.username); - gm.addClient( + const wasFound = gm.addClient( new Client( clientMsg.clientID, clientMsg.persistentID, @@ -244,6 +246,19 @@ wss.on("connection", (ws, req) => { clientMsg.gameID, clientMsg.lastTurn, ); + if (!wasFound) { + console.log(`game ${clientMsg.gameID} not found, loading from gcs`); + const record = await readGameRecord(clientMsg.gameID); + ws.send( + JSON.stringify( + ServerStartGameMessageSchema.parse({ + type: "start", + turns: record.turns, + config: record.gameConfig, + }), + ), + ); + } } if (clientMsg.type == "log") { slog({