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;