diff --git a/src/client/GameModeSelector.ts b/src/client/GameModeSelector.ts index 0c98d2635..5698df9ce 100644 --- a/src/client/GameModeSelector.ts +++ b/src/client/GameModeSelector.ts @@ -178,11 +178,24 @@ export class GameModeSelector extends LitElement { ) { const mapType = lobby.gameConfig!.gameMap as GameMapType; const mapImageSrc = terrainMapFileLoader.getMapData(mapType).webpPath; - const timeRemaining = Math.max( - 0, - Math.floor((lobby.startsAt - this.serverTimeOffset - Date.now()) / 1000), - ); - const timeDisplay = renderDuration(timeRemaining); + const timeRemaining = lobby.startsAt + ? Math.max( + 0, + Math.floor( + (lobby.startsAt - this.serverTimeOffset - Date.now()) / 1000, + ), + ) + : undefined; + + let timeDisplay: string = ""; + if (timeRemaining === undefined) { + timeDisplay = "-s"; + } else if (timeRemaining > 0) { + timeDisplay = renderDuration(timeRemaining); + } else { + timeDisplay = translateText("public_lobby.starting_game"); + } + const mapName = getMapName(lobby.gameConfig?.gameMap); const modifierLabels = this.getModifierLabels( @@ -222,15 +235,10 @@ export class GameModeSelector extends LitElement { ` : html`
`}
- ${timeRemaining > 0 - ? html`${timeDisplay}` - : html`${translateText("public_lobby.starting_game")}`} + ${timeDisplay}
diff --git a/src/core/Schemas.ts b/src/core/Schemas.ts index 5647eb744..86c74292d 100644 --- a/src/core/Schemas.ts +++ b/src/core/Schemas.ts @@ -159,7 +159,7 @@ export const GameInfoSchema = z.object({ export const PublicGameInfoSchema = z.object({ gameID: z.string(), numClients: z.number(), - startsAt: z.number(), + startsAt: z.number().optional(), gameConfig: z.lazy(() => GameConfigSchema).optional(), publicGameType: PublicGameTypeSchema, }); diff --git a/src/server/GameServer.ts b/src/server/GameServer.ts index fa4ebfb6c..42b46a68d 100644 --- a/src/server/GameServer.ts +++ b/src/server/GameServer.ts @@ -514,6 +514,10 @@ export class GameServer { } } + public setStartsAt(startsAt: number) { + this.startsAt = startsAt; + } + public numClients(): number { return this.activeClients.length; } @@ -795,7 +799,7 @@ export class GameServer { // Public Games - const lessThanLifetime = Date.now() < this.startsAt!; + const lessThanLifetime = this.startsAt ? Date.now() < this.startsAt : true; const notEnoughPlayers = this.gameConfig.gameType === GameType.Public && this.gameConfig.maxPlayers && diff --git a/src/server/IPCBridgeSchema.ts b/src/server/IPCBridgeSchema.ts index 100b0cf52..48293614d 100644 --- a/src/server/IPCBridgeSchema.ts +++ b/src/server/IPCBridgeSchema.ts @@ -11,6 +11,8 @@ export type WorkerReady = z.infer; export type MasterLobbiesBroadcast = z.infer< typeof MasterLobbiesBroadcastSchema >; + +export type MasterUpdateGame = z.infer; export type MasterCreateGame = z.infer; export type WorkerMessage = z.infer; export type MasterMessage = z.infer; @@ -35,6 +37,12 @@ export const WorkerMessageSchema = z.discriminatedUnion("type", [ // --- Master Messages --- +const MasterUpdateGameSchema = z.object({ + type: z.literal("updateLobby"), + gameID: z.string(), + startsAt: z.number(), +}); + // Broadcasts all public game info to all workers. // Workers need information on all public lobbies so // it can send it to the client. @@ -48,11 +56,11 @@ const MasterCreateGameSchema = z.object({ type: z.literal("createGame"), gameID: z.string(), gameConfig: GameConfigSchema, - startsAt: z.number(), publicGameType: PublicGameTypeSchema, }); export const MasterMessageSchema = z.discriminatedUnion("type", [ MasterLobbiesBroadcastSchema, MasterCreateGameSchema, + MasterUpdateGameSchema, ]); diff --git a/src/server/MasterLobbyService.ts b/src/server/MasterLobbyService.ts index 31e58b55d..17a41a285 100644 --- a/src/server/MasterLobbyService.ts +++ b/src/server/MasterLobbyService.ts @@ -6,6 +6,7 @@ import { generateID } from "../core/Util"; import { MasterCreateGame, MasterLobbiesBroadcast, + MasterUpdateGame, WorkerMessageSchema, } from "./IPCBridgeSchema"; import { logger } from "./Logger"; @@ -93,7 +94,16 @@ export class MasterLobbyService { } for (const type of Object.keys(result) as PublicGameType[]) { - result[type].sort((a, b) => a.startsAt - b.startsAt); + result[type].sort((a, b) => { + if (a.startsAt === undefined && b.startsAt === undefined) { + // Sort by game id for stability. + return a.gameID > b.gameID ? 1 : -1; + } + // If a lobby has startsAt set, we assume it's the active one. + if (a.startsAt === undefined) return 1; + if (b.startsAt === undefined) return -1; + return a.startsAt - b.startsAt; + }); } return result; @@ -124,39 +134,36 @@ export class MasterLobbyService { if (lobbies.length >= 2) { continue; } - - const lastStart = lobbies.reduce( - (max, pb) => Math.max(max, pb.startsAt), - Date.now(), - ); - - const gameID = generateID(); - const workerId = this.config.workerIndex(gameID); - - const gameConfig = await this.playlist.gameConfig(type); - const worker = this.workers.get(workerId); - if (!worker) { - this.log.error(`Worker ${workerId} not found`); - continue; + const nextLobby = lobbies[0]; + if (nextLobby && nextLobby.startsAt === undefined) { + // The previous game has started, so we need to set the timer on the next game. + this.sendMessageToWorker({ + type: "updateLobby", + gameID: nextLobby.gameID, + startsAt: Date.now() + this.config.gameCreationRate(), + }); } - worker.send( - { - type: "createGame", - gameID, - gameConfig, - startsAt: lastStart + this.config.gameCreationRate(), - publicGameType: type, - } satisfies MasterCreateGame, - (e) => { - if (e) { - this.log.error("Failed to schedule lobby on worker:", e); - } - }, - ); - this.log.info( - `Scheduled public game ${gameID} (${type}) on worker ${workerId}`, - ); + this.sendMessageToWorker({ + type: "createGame", + gameID: generateID(), + gameConfig: await this.playlist.gameConfig(type), + publicGameType: type, + } satisfies MasterCreateGame); } } + + private sendMessageToWorker(msg: MasterCreateGame | MasterUpdateGame): void { + const workerId = this.config.workerIndex(msg.gameID); + const worker = this.workers.get(workerId); + if (!worker) { + this.log.error(`Worker ${workerId} not found`); + return; + } + worker.send(msg, (e) => { + if (e) { + this.log.error("Failed to send message to worker:", e); + } + }); + } } diff --git a/src/server/WorkerLobbyService.ts b/src/server/WorkerLobbyService.ts index e9d7b7709..2bbd50e08 100644 --- a/src/server/WorkerLobbyService.ts +++ b/src/server/WorkerLobbyService.ts @@ -51,10 +51,21 @@ export class WorkerLobbyService { msg.gameID, msg.gameConfig, undefined, - msg.startsAt, + undefined, msg.publicGameType, ); break; + case "updateLobby": { + const game = this.gm.game(msg.gameID); + if (!game) { + this.log.warn("cannot update game, not found", { + gameID: msg.gameID, + }); + return; + } + game.setStartsAt(msg.startsAt); + break; + } } }); } @@ -72,7 +83,7 @@ export class WorkerLobbyService { return { gameID: gi.gameID, numClients: gi.clients?.length ?? 0, - startsAt: gi.startsAt!, + startsAt: gi.startsAt, gameConfig: gi.gameConfig, publicGameType: gi.publicGameType!, } satisfies PublicGameInfo;