diff --git a/src/client/ClientGameRunner.ts b/src/client/ClientGameRunner.ts index 1ac489863..55755c186 100644 --- a/src/client/ClientGameRunner.ts +++ b/src/client/ClientGameRunner.ts @@ -1,22 +1,14 @@ -import { - PlayerID, - GameMapType, - Difficulty, - GameType, - Unit, - UnitType, - TeamName, -} from "../core/game/Game"; +import { Unit, UnitType, TeamName } from "../core/game/Game"; import { EventBus } from "../core/EventBus"; import { createRenderer, GameRenderer } from "./graphics/GameRenderer"; import { InputHandler, MouseUpEvent, MouseMoveEvent } from "./InputHandler"; import { ClientID, - GameConfig, GameID, ServerMessage, PlayerRecord, GameRecord, + GameStartInfo, } from "../core/Schemas"; import { loadTerrainMap } from "../core/game/TerrainMapLoader"; import { @@ -54,14 +46,13 @@ function distSortUnitWorld(tile: TileRef, game: GameView) { export interface LobbyConfig { serverConfig: ServerConfig; - flag: () => string; - playerName: () => string; + flag: string; + playerName: string; clientID: ClientID; - playerID: PlayerID; - persistentID: string; gameID: GameID; - // GameConfig only exists when playing a singleplayer game. - gameConfig?: GameConfig; + persistentID: string; + // GameStartInfo only exists when playing a singleplayer game. + gameStartInfo?: GameStartInfo; // GameRecord exists when replaying an archived game. gameRecord?: GameRecord; } @@ -74,14 +65,13 @@ export function joinLobby( initRemoteSender(eventBus); consolex.log( - `joinging lobby: gameID: ${lobbyConfig.gameID}, clientID: ${lobbyConfig.clientID}, persistentID: ${lobbyConfig.persistentID}`, + `joinging lobby: gameID: ${lobbyConfig.gameID}, clientID: ${lobbyConfig.clientID}, persistentID: ${lobbyConfig.persistentID.slice(0, 5)}`, ); const userSettings: UserSettings = new UserSettings(); LocalPersistantStats.startGame( lobbyConfig.gameID, - lobbyConfig.playerID, - lobbyConfig.gameConfig, + lobbyConfig.gameStartInfo?.config, ); const transport = new Transport(lobbyConfig, eventBus); @@ -92,10 +82,10 @@ export function joinLobby( }; const onmessage = (message: ServerMessage) => { if (message.type == "start") { - consolex.log("lobby: game started"); + consolex.log(`lobby: game started: ${JSON.stringify(message)}`); onjoin(); - // For multiplayer games, GameConfig is not known until game starts. - lobbyConfig.gameConfig = message.config; + // For multiplayer games, GameStartInfo is not known until game starts. + lobbyConfig.gameStartInfo = message.gameStartInfo; createClientGame(lobbyConfig, eventBus, transport, userSettings).then( (r) => r.start(), ); @@ -114,12 +104,16 @@ export async function createClientGame( transport: Transport, userSettings: UserSettings, ): Promise { - const config = await getConfig(lobbyConfig.gameConfig, userSettings); + const config = await getConfig( + lobbyConfig.gameStartInfo.config, + userSettings, + ); - const gameMap = await loadTerrainMap(lobbyConfig.gameConfig.gameMap); + const gameMap = await loadTerrainMap( + lobbyConfig.gameStartInfo.config.gameMap, + ); const worker = new WorkerClient( - lobbyConfig.gameID, - lobbyConfig.gameConfig, + lobbyConfig.gameStartInfo, lobbyConfig.clientID, ); await worker.initialize(); @@ -128,7 +122,7 @@ export async function createClientGame( config, gameMap.gameMap, lobbyConfig.clientID, - lobbyConfig.gameID, + lobbyConfig.gameStartInfo.gameID, ); consolex.log("going to init path finder"); @@ -142,7 +136,7 @@ export async function createClientGame( ); consolex.log( - `creating private game got difficulty: ${lobbyConfig.gameConfig.difficulty}`, + `creating private game got difficulty: ${lobbyConfig.gameStartInfo.config.difficulty}`, ); return new ClientGameRunner( @@ -182,7 +176,7 @@ export class ClientGameRunner { { ip: null, persistentID: getPersistentIDFromCookie(), - username: this.lobby.playerName(), + username: this.lobby.playerName, clientID: this.lobby.clientID, }, ]; @@ -196,8 +190,8 @@ export class ClientGameRunner { } const record = createGameRecord( - this.lobby.gameID, - this.lobby.gameConfig, + this.lobby.gameStartInfo.gameID, + this.lobby.gameStartInfo, players, // Not saving turns locally [], @@ -223,7 +217,7 @@ export class ClientGameRunner { showErrorModal( gu.errMsg, gu.stack, - this.lobby.gameID, + this.lobby.gameStartInfo.gameID, this.lobby.clientID, ); this.stop(true); @@ -274,7 +268,7 @@ export class ClientGameRunner { showErrorModal( `desync from server: ${JSON.stringify(message)}`, "", - this.lobby.gameID, + this.lobby.gameStartInfo.gameID, this.lobby.clientID, true, "You are desynced from other players. What you see might differ from other players.", diff --git a/src/client/HostLobbyModal.ts b/src/client/HostLobbyModal.ts index cf8e7ddb0..9b19a69c6 100644 --- a/src/client/HostLobbyModal.ts +++ b/src/client/HostLobbyModal.ts @@ -317,6 +317,7 @@ export class HostLobbyModal extends LitElement { new CustomEvent("join-lobby", { detail: { gameID: this.lobbyId, + clientID: generateID(), } as JoinLobbyEvent, bubbles: true, composed: true, diff --git a/src/client/JoinPrivateLobbyModal.ts b/src/client/JoinPrivateLobbyModal.ts index 23a32ecbf..5515eefdf 100644 --- a/src/client/JoinPrivateLobbyModal.ts +++ b/src/client/JoinPrivateLobbyModal.ts @@ -7,7 +7,7 @@ import { JoinLobbyEvent } from "./Main"; import { translateText } from "../client/Utils"; import "./components/baseComponents/Modal"; import "./components/baseComponents/Button"; - +import { generateID } from "../core/Util"; @customElement("join-private-lobby-modal") export class JoinPrivateLobbyModal extends LitElement { @query("o-modal") private modalEl!: HTMLElement & { @@ -187,7 +187,10 @@ export class JoinPrivateLobbyModal extends LitElement { this.dispatchEvent( new CustomEvent("join-lobby", { - detail: { gameID: lobbyId } as JoinLobbyEvent, + detail: { + gameID: lobbyId, + clientID: generateID(), + } as JoinLobbyEvent, bubbles: true, composed: true, }), @@ -232,6 +235,7 @@ export class JoinPrivateLobbyModal extends LitElement { detail: { gameID: lobbyId, gameRecord: gameRecord, + clientID: generateID(), } as JoinLobbyEvent, bubbles: true, composed: true, diff --git a/src/client/LocalPersistantStats.ts b/src/client/LocalPersistantStats.ts index ee3f49f89..a879fbe5f 100644 --- a/src/client/LocalPersistantStats.ts +++ b/src/client/LocalPersistantStats.ts @@ -4,7 +4,6 @@ import { GameConfig, GameID, GameRecord } from "../core/Schemas"; export interface LocalStatsData { [key: GameID]: { - playerId: PlayerID; lobby: GameConfig; // Only once the game is over gameRecord?: GameRecord; @@ -29,14 +28,14 @@ export namespace LocalPersistantStats { // The user can quit the game anytime so better save the lobby as soon as the // game starts. - export function startGame(id: GameID, playerId: PlayerID, lobby: GameConfig) { + export function startGame(id: GameID, lobby: GameConfig) { if (typeof localStorage === "undefined") { return; } _startTime = Date.now(); const stats = getStats(); - stats[id] = { playerId, lobby }; + stats[id] = { lobby }; save(stats); } diff --git a/src/client/LocalServer.ts b/src/client/LocalServer.ts index e2ad73e6b..717ec7825 100644 --- a/src/client/LocalServer.ts +++ b/src/client/LocalServer.ts @@ -3,18 +3,14 @@ import { consolex } from "../core/Consolex"; import { GameEvent } from "../core/EventBus"; import { AllPlayersStats, - ClientID, ClientMessage, ClientMessageSchema, ClientSendWinnerMessage, - GameConfig, - GameID, GameRecordSchema, Intent, PlayerRecord, ServerMessage, ServerStartGameMessageSchema, - ServerTurnMessageSchema, Turn, } from "../core/Schemas"; import { @@ -59,8 +55,17 @@ export class LocalServer { this.clientMessage( ServerStartGameMessageSchema.parse({ type: "start", - config: this.lobbyConfig.gameConfig, + gameID: this.lobbyConfig.gameStartInfo.gameID, + gameStartInfo: this.lobbyConfig.gameStartInfo, turns: this.turns, + players: [ + { + flag: this.lobbyConfig.flag, + playerID: generateID(), + clientID: this.lobbyConfig.clientID, + username: this.lobbyConfig.playerName, + }, + ], }), ); } @@ -136,7 +141,7 @@ export class LocalServer { } const pastTurn: Turn = { turnNumber: this.turns.length, - gameID: this.lobbyConfig.gameID, + gameID: this.lobbyConfig.gameStartInfo.gameID, intents: this.intents, }; this.turns.push(pastTurn); @@ -154,13 +159,13 @@ export class LocalServer { { ip: null, persistentID: getPersistentIDFromCookie(), - username: this.lobbyConfig.playerName(), + username: this.lobbyConfig.playerName, clientID: this.lobbyConfig.clientID, }, ]; const record = createGameRecord( - this.lobbyConfig.gameID, - this.lobbyConfig.gameConfig, + this.lobbyConfig.gameStartInfo.gameID, + this.lobbyConfig.gameStartInfo, players, this.turns, this.startedAt, @@ -178,7 +183,7 @@ export class LocalServer { type: "application/json", }); const workerPath = this.lobbyConfig.serverConfig.workerPath( - this.lobbyConfig.gameID, + this.lobbyConfig.gameStartInfo.gameID, ); navigator.sendBeacon(`/${workerPath}/api/archive_singleplayer_game`, blob); } diff --git a/src/client/Main.ts b/src/client/Main.ts index 866d20ecf..06f4d0e21 100644 --- a/src/client/Main.ts +++ b/src/client/Main.ts @@ -25,15 +25,21 @@ import { HelpModal } from "./HelpModal"; import { GameType } from "../core/game/Game"; import { getServerConfigFromClient } from "../core/configuration/ConfigLoader"; import GoogleAdElement from "./GoogleAdElement"; -import { GameConfig, GameInfo, GameRecord } from "../core/Schemas"; +import { + GameConfig, + GameInfo, + GameRecord, + GameStartInfo, +} from "../core/Schemas"; import "./LangSelector"; import { LangSelector } from "./LangSelector"; 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. - gameConfig?: GameConfig; + gameStartInfo?: GameStartInfo; // GameRecord exists when replaying an archived game. gameRecord?: GameRecord; } @@ -179,17 +185,16 @@ class Client { const config = await getServerConfigFromClient(); this.gameStop = joinLobby( { + gameID: lobby.gameID, serverConfig: config, - flag: (): string => + flag: this.flagInput.getCurrentFlag() == "xx" ? "" : this.flagInput.getCurrentFlag(), - playerName: (): string => this.usernameInput.getCurrentUsername(), - gameID: lobby.gameID, + playerName: this.usernameInput.getCurrentUsername(), persistentID: getPersistentIDFromCookie(), - playerID: generateID(), - clientID: generateID(), - gameConfig: lobby.gameConfig ?? lobby.gameRecord?.gameConfig, + clientID: lobby.clientID, + gameStartInfo: lobby.gameStartInfo ?? lobby.gameRecord?.gameStartInfo, gameRecord: lobby.gameRecord, }, () => { diff --git a/src/client/PublicLobby.ts b/src/client/PublicLobby.ts index 2cfc37148..a0dae6799 100644 --- a/src/client/PublicLobby.ts +++ b/src/client/PublicLobby.ts @@ -5,6 +5,8 @@ import { consolex } from "../core/Consolex"; import { getMapsImage } from "./utilities/Maps"; import { GameID, GameInfo } from "../core/Schemas"; import { translateText } from "../client/Utils"; +import { JoinLobbyEvent } from "./Main"; +import { generateID } from "../core/Util"; @customElement("public-lobby") export class PublicLobby extends LitElement { @@ -166,7 +168,10 @@ export class PublicLobby extends LitElement { this.currLobby = lobby; this.dispatchEvent( new CustomEvent("join-lobby", { - detail: lobby, + detail: { + gameID: lobby.gameID, + clientID: generateID(), + } as JoinLobbyEvent, bubbles: true, composed: true, }), diff --git a/src/client/SinglePlayerModal.ts b/src/client/SinglePlayerModal.ts index e8fd90549..bda1d8cf1 100644 --- a/src/client/SinglePlayerModal.ts +++ b/src/client/SinglePlayerModal.ts @@ -324,22 +324,35 @@ export class SinglePlayerModal extends LitElement { consolex.log( `Starting single player game with map: ${GameMapType[this.selectedMap]}${this.useRandomMap ? " (Randomly selected)" : ""}`, ); + const clientID = generateID(); + const gameID = generateID(); this.dispatchEvent( new CustomEvent("join-lobby", { detail: { - gameID: generateID(), - gameConfig: { - gameMap: this.selectedMap, - gameType: GameType.Singleplayer, - gameMode: this.gameMode, - difficulty: this.selectedDifficulty, - disableNPCs: this.disableNPCs, - disableNukes: this.disableNukes, - bots: this.bots, - infiniteGold: this.infiniteGold, - infiniteTroops: this.infiniteTroops, - instantBuild: this.instantBuild, + clientID: clientID, + gameID: gameID, + gameStartInfo: { + gameID: gameID, + players: [ + { + playerID: generateID(), + clientID, + username: "PLACEHOLDER", + }, + ], + config: { + gameMap: this.selectedMap, + gameType: GameType.Singleplayer, + gameMode: this.gameMode, + difficulty: this.selectedDifficulty, + disableNPCs: this.disableNPCs, + disableNukes: this.disableNukes, + bots: this.bots, + infiniteGold: this.infiniteGold, + infiniteTroops: this.infiniteTroops, + instantBuild: this.instantBuild, + }, }, } as JoinLobbyEvent, bubbles: true, diff --git a/src/client/Transport.ts b/src/client/Transport.ts index e66b9d08a..a4f0db599 100644 --- a/src/client/Transport.ts +++ b/src/client/Transport.ts @@ -165,7 +165,7 @@ export class Transport { // For multiplayer games, GameConfig is not known until game starts. this.isLocal = lobbyConfig.gameRecord != null || - lobbyConfig.gameConfig?.gameType == GameType.Singleplayer; + lobbyConfig.gameStartInfo?.config.gameType == GameType.Singleplayer; this.eventBus.on(SendAllianceRequestIntentEvent, (e) => this.onSendAllianceRequest(e), @@ -325,7 +325,7 @@ export class Transport { clientID: this.lobbyConfig.clientID, lastTurn: numTurns, persistentID: this.lobbyConfig.persistentID, - username: this.lobbyConfig.playerName(), + username: this.lobbyConfig.playerName, }), ), ); @@ -354,7 +354,6 @@ export class Transport { this.sendIntent({ type: "allianceRequest", clientID: this.lobbyConfig.clientID, - playerID: event.requestor.id(), recipient: event.recipient.id(), }); } @@ -364,7 +363,6 @@ export class Transport { type: "allianceRequestReply", clientID: this.lobbyConfig.clientID, requestor: event.requestor.id(), - playerID: event.recipient.id(), accept: event.accepted, }); } @@ -373,7 +371,6 @@ export class Transport { this.sendIntent({ type: "breakAlliance", clientID: this.lobbyConfig.clientID, - playerID: event.requestor.id(), recipient: event.recipient.id(), }); } @@ -382,9 +379,8 @@ export class Transport { this.sendIntent({ type: "spawn", clientID: this.lobbyConfig.clientID, - playerID: this.lobbyConfig.playerID, - flag: this.lobbyConfig.flag(), - name: this.lobbyConfig.playerName(), + flag: this.lobbyConfig.flag, + name: this.lobbyConfig.playerName, playerType: PlayerType.Human, x: event.cell.x, y: event.cell.y, @@ -395,7 +391,6 @@ export class Transport { this.sendIntent({ type: "attack", clientID: this.lobbyConfig.clientID, - playerID: this.lobbyConfig.playerID, targetID: event.targetID, troops: event.troops, }); @@ -405,7 +400,6 @@ export class Transport { this.sendIntent({ type: "boat", clientID: this.lobbyConfig.clientID, - playerID: this.lobbyConfig.playerID, targetID: event.targetID, troops: event.troops, x: event.cell.x, @@ -417,7 +411,6 @@ export class Transport { this.sendIntent({ type: "targetPlayer", clientID: this.lobbyConfig.clientID, - playerID: this.lobbyConfig.playerID, target: event.targetID, }); } @@ -426,7 +419,6 @@ export class Transport { this.sendIntent({ type: "emoji", clientID: this.lobbyConfig.clientID, - playerID: this.lobbyConfig.playerID, recipient: event.recipient == AllPlayers ? AllPlayers : event.recipient.id(), emoji: event.emoji, @@ -437,7 +429,6 @@ export class Transport { this.sendIntent({ type: "donate", clientID: this.lobbyConfig.clientID, - playerID: event.sender.id(), recipient: event.recipient.id(), troops: event.troops, }); @@ -447,7 +438,6 @@ export class Transport { this.sendIntent({ type: "embargo", clientID: this.lobbyConfig.clientID, - playerID: this.lobbyConfig.playerID, targetID: event.target.id(), action: event.action, }); @@ -457,7 +447,6 @@ export class Transport { this.sendIntent({ type: "troop_ratio", clientID: this.lobbyConfig.clientID, - playerID: this.lobbyConfig.playerID, ratio: event.ratio, }); } @@ -466,7 +455,6 @@ export class Transport { this.sendIntent({ type: "build_unit", clientID: this.lobbyConfig.clientID, - playerID: this.lobbyConfig.playerID, unit: event.unit, x: event.cell.x, y: event.cell.y, @@ -530,7 +518,6 @@ export class Transport { this.sendIntent({ type: "cancel_attack", clientID: this.lobbyConfig.clientID, - playerID: event.playerID, attackID: event.attackID, }); } @@ -539,7 +526,6 @@ export class Transport { this.sendIntent({ type: "move_warship", clientID: this.lobbyConfig.clientID, - playerID: this.lobbyConfig.playerID, unitId: event.unitId, tile: event.tile, }); diff --git a/src/client/graphics/layers/WinModal.ts b/src/client/graphics/layers/WinModal.ts index 067b18983..b7c2c8b3e 100644 --- a/src/client/graphics/layers/WinModal.ts +++ b/src/client/graphics/layers/WinModal.ts @@ -211,7 +211,12 @@ export class WinModal extends LitElement implements Layer { tick() { const myPlayer = this.game.myPlayer(); - if (!this.hasShownDeathModal && myPlayer && !myPlayer.isAlive()) { + if ( + !this.hasShownDeathModal && + myPlayer && + !myPlayer.isAlive() && + !this.game.inSpawnPhase() + ) { this.hasShownDeathModal = true; this._title = "You died"; this.won = false; diff --git a/src/core/GameRunner.ts b/src/core/GameRunner.ts index d02077157..c1562063d 100644 --- a/src/core/GameRunner.ts +++ b/src/core/GameRunner.ts @@ -7,10 +7,8 @@ import { WinCheckExecution } from "./execution/WinCheckExecution"; import { AllPlayers, BuildableUnit, - Cell, Game, GameUpdates, - MessageType, Player, PlayerActions, PlayerID, @@ -18,24 +16,34 @@ import { PlayerBorderTiles, PlayerType, UnitType, + PlayerInfo, } from "./game/Game"; -import { DisplayMessageUpdate, ErrorUpdate } from "./game/GameUpdates"; +import { ErrorUpdate } from "./game/GameUpdates"; import { NameViewData } from "./game/Game"; import { GameUpdateType } from "./game/GameUpdates"; import { createGame } from "./game/GameImpl"; import { loadTerrainMap as loadGameMap } from "./game/TerrainMapLoader"; -import { ClientID, GameConfig, Turn } from "./Schemas"; +import { ClientID, GameStartInfo, Turn } from "./Schemas"; import { GameUpdateViewData } from "./game/GameUpdates"; export async function createGameRunner( - gameID: string, - gameConfig: GameConfig, + gameStart: GameStartInfo, clientID: ClientID, callBack: (gu: GameUpdateViewData) => void, ): Promise { - const config = await getConfig(gameConfig, null); - const gameMap = await loadGameMap(gameConfig.gameMap); + const config = await getConfig(gameStart.config, null); + const gameMap = await loadGameMap(gameStart.config.gameMap); const game = createGame( + gameStart.players.map( + (p) => + new PlayerInfo( + p.flag, + p.username, + PlayerType.Human, + p.clientID, + p.playerID, + ), + ), gameMap.gameMap, gameMap.miniGameMap, gameMap.nationMap, @@ -43,7 +51,7 @@ export async function createGameRunner( ); const gr = new GameRunner( game as Game, - new Executor(game, gameID, clientID), + new Executor(game, gameStart.gameID, clientID), callBack, ); gr.init(); diff --git a/src/core/Schemas.ts b/src/core/Schemas.ts index fc5421d61..8c15d083e 100644 --- a/src/core/Schemas.ts +++ b/src/core/Schemas.ts @@ -83,7 +83,8 @@ export type GameRecord = z.infer; export type AllPlayersStats = z.infer; export type PlayerStats = z.infer; - +export type Player = z.infer; +export type GameStartInfo = z.infer; const PlayerTypeSchema = z.nativeEnum(PlayerType); export interface GameInfo { @@ -173,12 +174,10 @@ const BaseIntentSchema = z.object({ "move_warship", ]), clientID: ID, - playerID: ID, }); export const AttackIntentSchema = BaseIntentSchema.extend({ type: z.literal("attack"), - playerID: ID, targetID: ID.nullable(), troops: z.number().nullable(), }); @@ -186,7 +185,6 @@ export const AttackIntentSchema = BaseIntentSchema.extend({ export const SpawnIntentSchema = BaseIntentSchema.extend({ flag: z.string().nullable(), type: z.literal("spawn"), - playerID: ID, name: SafeString, playerType: PlayerTypeSchema, x: z.number(), @@ -195,7 +193,6 @@ export const SpawnIntentSchema = BaseIntentSchema.extend({ export const BoatAttackIntentSchema = BaseIntentSchema.extend({ type: z.literal("boat"), - playerID: ID, targetID: ID.nullable(), troops: z.number().nullable(), x: z.number(), @@ -204,59 +201,50 @@ export const BoatAttackIntentSchema = BaseIntentSchema.extend({ export const AllianceRequestIntentSchema = BaseIntentSchema.extend({ type: z.literal("allianceRequest"), - playerID: ID, recipient: ID, }); export const AllianceRequestReplyIntentSchema = BaseIntentSchema.extend({ type: z.literal("allianceRequestReply"), requestor: ID, // The one who made the original alliance request - playerID: ID, accept: z.boolean(), }); export const BreakAllianceIntentSchema = BaseIntentSchema.extend({ type: z.literal("breakAlliance"), - playerID: ID, recipient: ID, }); export const TargetPlayerIntentSchema = BaseIntentSchema.extend({ type: z.literal("targetPlayer"), - playerID: ID, target: ID, }); export const EmojiIntentSchema = BaseIntentSchema.extend({ type: z.literal("emoji"), - playerID: ID, recipient: z.union([ID, z.literal(AllPlayers)]), emoji: EmojiSchema, }); export const EmbargoIntentSchema = BaseIntentSchema.extend({ type: z.literal("embargo"), - playerID: ID, targetID: ID, action: z.union([z.literal("start"), z.literal("stop")]), }); export const DonateIntentSchema = BaseIntentSchema.extend({ type: z.literal("donate"), - playerID: ID, recipient: ID, troops: z.number().nullable(), }); export const TargetTroopRatioIntentSchema = BaseIntentSchema.extend({ type: z.literal("troop_ratio"), - playerID: ID, ratio: z.number().min(0).max(1), }); export const BuildUnitIntentSchema = BaseIntentSchema.extend({ type: z.literal("build_unit"), - playerID: ID, unit: z.nativeEnum(UnitType), x: z.number(), y: z.number(), @@ -264,7 +252,6 @@ export const BuildUnitIntentSchema = BaseIntentSchema.extend({ export const CancelAttackIntentSchema = BaseIntentSchema.extend({ type: z.literal("cancel_attack"), - playerID: ID, attackID: z.string(), }); @@ -314,11 +301,24 @@ export const ServerPingMessageSchema = ServerBaseMessageSchema.extend({ type: z.literal("ping"), }); +export const PlayerSchema = z.object({ + playerID: ID, + clientID: ID, + username: SafeString, + flag: SafeString.optional(), +}); + +export const GameStartInfoSchema = z.object({ + gameID: ID, + config: GameConfigSchema, + players: z.array(PlayerSchema), +}); + export const ServerStartGameMessageSchema = ServerBaseMessageSchema.extend({ type: z.literal("start"), // Turns the client missed if they are late to the game. turns: z.array(TurnSchema), - config: GameConfigSchema, + gameStartInfo: GameStartInfoSchema, }); export const ServerDesyncSchema = ServerBaseMessageSchema.extend({ @@ -400,7 +400,7 @@ export const PlayerRecordSchema = z.object({ export const GameRecordSchema = z.object({ id: ID, - gameConfig: GameConfigSchema, + gameStartInfo: GameStartInfoSchema, players: z.array(PlayerRecordSchema), startTimestampMS: z.number(), endTimestampMS: z.number(), diff --git a/src/core/Util.ts b/src/core/Util.ts index c86c52439..103061644 100644 --- a/src/core/Util.ts +++ b/src/core/Util.ts @@ -8,6 +8,7 @@ import { GameConfig, GameID, GameRecord, + GameStartInfo, PlayerRecord, PlayerStats, Turn, @@ -249,7 +250,7 @@ export function onlyImages(html: string) { export function createGameRecord( id: GameID, - gameConfig: GameConfig, + gameStart: GameStartInfo, // username does not need to be set. players: PlayerRecord[], turns: Turn[], @@ -261,7 +262,7 @@ export function createGameRecord( ): GameRecord { const record: GameRecord = { id: id, - gameConfig: gameConfig, + gameStartInfo: gameStart, startTimestampMS: start, endTimestampMS: end, date: new Date().toISOString().split("T")[0], diff --git a/src/core/execution/BotSpawner.ts b/src/core/execution/BotSpawner.ts index 05369a627..e54346ad7 100644 --- a/src/core/execution/BotSpawner.ts +++ b/src/core/execution/BotSpawner.ts @@ -1,14 +1,15 @@ import { consolex } from "../Consolex"; -import { Cell, Game, PlayerType } from "../game/Game"; +import { Cell, Game, PlayerInfo, PlayerType } from "../game/Game"; import { TileRef } from "../game/GameMap"; import { PseudoRandom } from "../PseudoRandom"; import { GameID, SpawnIntent } from "../Schemas"; import { simpleHash } from "../Util"; +import { SpawnExecution } from "./SpawnExecution"; import { BOT_NAME_PREFIXES, BOT_NAME_SUFFIXES } from "./utils/BotNames"; export class BotSpawner { private random: PseudoRandom; - private bots: SpawnIntent[] = []; + private bots: SpawnExecution[] = []; constructor( private gs: Game, @@ -17,7 +18,7 @@ export class BotSpawner { this.random = new PseudoRandom(simpleHash(gameID)); } - spawnBots(numBots: number): SpawnIntent[] { + spawnBots(numBots: number): SpawnExecution[] { let tries = 0; while (this.bots.length < numBots) { if (tries > 10000) { @@ -35,24 +36,20 @@ export class BotSpawner { return this.bots; } - spawnBot(botName: string): SpawnIntent | null { + spawnBot(botName: string): SpawnExecution | null { const tile = this.randTile(); if (!this.gs.isLand(tile)) { return null; } for (const spawn of this.bots) { - if (this.gs.manhattanDist(this.gs.ref(spawn.x, spawn.y), tile) < 30) { + if (this.gs.manhattanDist(spawn.tile, tile) < 30) { return null; } } - return { - type: "spawn", - playerID: this.random.nextID(), - name: botName, - playerType: PlayerType.Bot, - x: this.gs.x(tile), - y: this.gs.y(tile), - }; + return new SpawnExecution( + new PlayerInfo("", botName, PlayerType.Bot, null, this.random.nextID()), + tile, + ); } private randomBotName(): string { diff --git a/src/core/execution/ExecutionManager.ts b/src/core/execution/ExecutionManager.ts index 90ed18044..d5ccc4c56 100644 --- a/src/core/execution/ExecutionManager.ts +++ b/src/core/execution/ExecutionManager.ts @@ -56,34 +56,24 @@ export class Executor { } createExec(intent: Intent): Execution { - let player: Player; - if (intent.type != "spawn") { - if (!this.mg.hasPlayer(intent.playerID)) { - console.warn( - `player ${intent.playerID} not found on intent ${intent.type}`, - ); - return new NoOpExecution(); - } - player = this.mg.player(intent.playerID); - if (player.clientID() != intent.clientID) { - console.warn( - `intent ${intent.type} has incorrect clientID ${intent.clientID} for player ${player.name()} with clientID ${player.clientID()}`, - ); - return new NoOpExecution(); - } + const player = this.mg.playerByClientID(intent.clientID); + if (!player) { + console.warn(`player with clientID ${intent.clientID} not found`); + return new NoOpExecution(); } + const playerID = player.id(); switch (intent.type) { case "attack": { return new AttackExecution( intent.troops, - intent.playerID, + playerID, intent.targetID, null, ); } case "cancel_attack": - return new RetreatExecution(intent.playerID, intent.attackID); + return new RetreatExecution(playerID, intent.attackID); case "move_warship": return new MoveWarshipExecution(intent.unitId, intent.tile); case "spawn": @@ -94,50 +84,42 @@ export class Executor { intent.clientID == this.clientID ? sanitize(intent.name) : fixProfaneUsername(sanitize(intent.name)), - intent.playerType, + PlayerType.Human, intent.clientID, - intent.playerID, + playerID, ), this.mg.ref(intent.x, intent.y), ); case "boat": return new TransportShipExecution( - intent.playerID, + playerID, intent.targetID, this.mg.ref(intent.x, intent.y), intent.troops, ); case "allianceRequest": - return new AllianceRequestExecution(intent.playerID, intent.recipient); + return new AllianceRequestExecution(playerID, intent.recipient); case "allianceRequestReply": return new AllianceRequestReplyExecution( intent.requestor, - intent.playerID, + playerID, intent.accept, ); case "breakAlliance": - return new BreakAllianceExecution(intent.playerID, intent.recipient); + return new BreakAllianceExecution(playerID, intent.recipient); case "targetPlayer": - return new TargetPlayerExecution(intent.playerID, intent.target); + return new TargetPlayerExecution(playerID, intent.target); case "emoji": - return new EmojiExecution( - intent.playerID, - intent.recipient, - intent.emoji, - ); + return new EmojiExecution(playerID, intent.recipient, intent.emoji); case "donate": - return new DonateExecution( - intent.playerID, - intent.recipient, - intent.troops, - ); + return new DonateExecution(playerID, intent.recipient, intent.troops); case "troop_ratio": - return new SetTargetTroopRatioExecution(intent.playerID, intent.ratio); + return new SetTargetTroopRatioExecution(playerID, intent.ratio); case "embargo": return new EmbargoExecution(player, intent.targetID, intent.action); case "build_unit": return new ConstructionExecution( - intent.playerID, + playerID, this.mg.ref(intent.x, intent.y), intent.unit, ); @@ -147,9 +129,7 @@ export class Executor { } spawnBots(numBots: number): Execution[] { - return new BotSpawner(this.mg, this.gameID) - .spawnBots(numBots) - .map((i) => this.createExec(i)); + return new BotSpawner(this.mg, this.gameID).spawnBots(numBots); } fakeHumanExecutions(): Execution[] { diff --git a/src/core/execution/SpawnExecution.ts b/src/core/execution/SpawnExecution.ts index 0c8cc5687..6acfdbd3e 100644 --- a/src/core/execution/SpawnExecution.ts +++ b/src/core/execution/SpawnExecution.ts @@ -17,7 +17,7 @@ export class SpawnExecution implements Execution { constructor( private playerInfo: PlayerInfo, - private tile: TileRef, + public readonly tile: TileRef, ) {} init(mg: Game, ticks: number) { diff --git a/src/core/game/GameImpl.ts b/src/core/game/GameImpl.ts index 7d9e4b9be..21bb752d6 100644 --- a/src/core/game/GameImpl.ts +++ b/src/core/game/GameImpl.ts @@ -39,12 +39,13 @@ import { Stats } from "./Stats"; import { simpleHash } from "../Util"; export function createGame( + humans: PlayerInfo[], gameMap: GameMap, miniGameMap: GameMap, nationMap: NationMap, config: Config, ): Game { - return new GameImpl(gameMap, miniGameMap, nationMap, config); + return new GameImpl(humans, gameMap, miniGameMap, nationMap, config); } export type CellString = string; @@ -82,11 +83,13 @@ export class GameImpl implements Game { private botTeam: Team = { name: TeamName.Bot }; constructor( + private _humans: PlayerInfo[], private _map: GameMap, private miniGameMap: GameMap, nationMap: NationMap, private _config: Config, ) { + this._humans.forEach((p) => this.addPlayer(p, 100)); this._terraNullius = new TerraNulliusImpl(); this._width = _map.width(); this._height = _map.height(); diff --git a/src/core/worker/Worker.worker.ts b/src/core/worker/Worker.worker.ts index 232f5100f..7394e2849 100644 --- a/src/core/worker/Worker.worker.ts +++ b/src/core/worker/Worker.worker.ts @@ -33,8 +33,7 @@ ctx.addEventListener("message", async (e: MessageEvent) => { case "init": try { gameRunner = createGameRunner( - message.gameID, - message.gameConfig, + message.gameStartInfo, message.clientID, gameUpdate, ).then((gr) => { diff --git a/src/core/worker/WorkerClient.ts b/src/core/worker/WorkerClient.ts index 256ef071d..33efd1abd 100644 --- a/src/core/worker/WorkerClient.ts +++ b/src/core/worker/WorkerClient.ts @@ -1,12 +1,11 @@ import { PlayerActions, PlayerID, - PlayerInfo, PlayerProfile, PlayerBorderTiles, } from "../game/Game"; import { ErrorUpdate, GameUpdateViewData } from "../game/GameUpdates"; -import { ClientID, GameConfig, GameID, Turn } from "../Schemas"; +import { ClientID, GameStartInfo, Turn } from "../Schemas"; import { generateID } from "../Util"; import { WorkerMessage } from "./WorkerMessages"; @@ -19,8 +18,7 @@ export class WorkerClient { ) => void; constructor( - private gameID: GameID, - private gameConfig: GameConfig, + private gameStartInfo: GameStartInfo, private clientID: ClientID, ) { this.worker = new Worker(new URL("./Worker.worker.ts", import.meta.url)); @@ -68,8 +66,7 @@ export class WorkerClient { this.worker.postMessage({ type: "init", id: messageId, - gameID: this.gameID, - gameConfig: this.gameConfig, + gameStartInfo: this.gameStartInfo, clientID: this.clientID, }); diff --git a/src/core/worker/WorkerMessages.ts b/src/core/worker/WorkerMessages.ts index 206bbd265..d682b352a 100644 --- a/src/core/worker/WorkerMessages.ts +++ b/src/core/worker/WorkerMessages.ts @@ -1,5 +1,10 @@ import { GameUpdateViewData } from "../game/GameUpdates"; -import { ClientID, GameConfig, GameID, Turn } from "../Schemas"; +import { + ClientID, + Turn, + ServerStartGameMessage, + GameStartInfo, +} from "../Schemas"; import { PlayerActions, PlayerID, @@ -33,8 +38,7 @@ export interface HeartbeatMessage extends BaseWorkerMessage { // Messages from main thread to worker export interface InitMessage extends BaseWorkerMessage { type: "init"; - gameID: GameID; - gameConfig: GameConfig; + gameStartInfo: GameStartInfo; clientID: ClientID; } diff --git a/src/server/Archive.ts b/src/server/Archive.ts index b182d2829..9609b1983 100644 --- a/src/server/Archive.ts +++ b/src/server/Archive.ts @@ -54,10 +54,10 @@ async function archiveAnalyticsToR2(gameRecord: GameRecord) { end_time: new Date(gameRecord.endTimestampMS).toISOString(), duration_seconds: gameRecord.durationSeconds, number_turns: gameRecord.num_turns, - game_mode: gameRecord.gameConfig.gameType, + game_mode: gameRecord.gameStartInfo.config.gameType, winner: gameRecord.winner, - difficulty: gameRecord.gameConfig.difficulty, - mapType: gameRecord.gameConfig.gameMap, + difficulty: gameRecord.gameStartInfo.config.difficulty, + mapType: gameRecord.gameStartInfo.config.gameMap, players: gameRecord.players.map((p) => ({ username: p.username, ip: p.ip, diff --git a/src/server/Client.ts b/src/server/Client.ts index df00ebb0c..6417730ce 100644 --- a/src/server/Client.ts +++ b/src/server/Client.ts @@ -1,12 +1,15 @@ import WebSocket from "ws"; import { ClientID } from "../core/Schemas"; -import { Tick } from "../core/game/Game"; +import { PlayerID, Tick } from "../core/game/Game"; +import { generateID } from "../core/Util"; export class Client { public lastPing: number; public hashes: Map = new Map(); + public readonly playerID: PlayerID = generateID(); + constructor( public readonly clientID: ClientID, public readonly persistentID: string, diff --git a/src/server/GameServer.ts b/src/server/GameServer.ts index b133191e1..3035e24d5 100644 --- a/src/server/GameServer.ts +++ b/src/server/GameServer.ts @@ -8,6 +8,8 @@ import { ClientSendWinnerMessage, GameConfig, GameInfo, + GameStartInfo, + GameStartInfoSchema, Intent, PlayerRecord, ServerDesyncSchema, @@ -15,7 +17,7 @@ import { ServerTurnMessageSchema, Turn, } from "../core/Schemas"; -import { createGameRecord } from "../core/Util"; +import { createGameRecord, generateID } from "../core/Util"; import { ServerConfig } from "../core/configuration/Config"; import { GameType } from "../core/game/Game"; import { archive } from "./Archive"; @@ -50,6 +52,8 @@ export class GameServer { // This field is currently only filled at victory private allPlayersStats: AllPlayersStats = {}; + private gameStartInfo: GameStartInfo; + private log: Logger; constructor( @@ -223,6 +227,16 @@ export class GameServer { // if no client connects/pings. this.lastPingUpdate = Date.now(); + this.gameStartInfo = GameStartInfoSchema.parse({ + gameID: this.id, + config: this.gameConfig, + players: this.activeClients.map((c) => ({ + playerID: c.playerID, + username: c.username, + clientID: c.clientID, + })), + }); + this.endTurnIntervalID = setInterval( () => this.endTurn(), this.config.turnIntervalMs(), @@ -244,7 +258,7 @@ export class GameServer { ServerStartGameMessageSchema.parse({ type: "start", turns: this.turns.slice(lastTurn), - config: this.gameConfig, + gameStartInfo: this.gameStartInfo, }), ), ); @@ -317,7 +331,7 @@ export class GameServer { archive( createGameRecord( this.id, - this.gameConfig, + this.gameStartInfo, playerRecords, this.turns, this._startTime, diff --git a/tests/util/Setup.ts b/tests/util/Setup.ts index 89724843c..c6612ea67 100644 --- a/tests/util/Setup.ts +++ b/tests/util/Setup.ts @@ -36,5 +36,5 @@ export async function setup(mapName: string, _gameConfig: GameConfig = {}) { const config = new TestConfig(serverConfig, gameConfig, new UserSettings()); // Create and return the game - return createGame(gameMap, miniGameMap, nationMap, config); + return createGame([], gameMap, miniGameMap, nationMap, config); // TODO: !!! }