mirror of
https://github.com/openfrontio/OpenFrontIO.git
synced 2026-07-01 23:23:29 +00:00
bugfix: set lobby start time only when it's the next lobby in rotation (#3261)
## Description: The master set lobby start times on creation, which caused an issue if the previous lobby filled up and started before its timer ran out, the next lobby would have its timer set too far back. For example, if lobby time is 60 seconds, and the first lobby fills up after 10s, the subsequent lobby would have its timer set for 110 seconds (60+50). Instead we have the master set the lobby start time only when it is next up in rotation. So all lobbies behind it don't have a start time, because we don't actually know what it should be. ## Please complete the following: - [x] I have added screenshots for all UI updates - [x] I process any text displayed to the user through translateText() and I've added it to the en.json file - [x] I have added relevant tests to the test directory - [x] I confirm I have thoroughly tested these changes and take full responsibility for any bugs introduced ## Please put your Discord username so you can be contacted if a bug or regression is found: evan
This commit is contained in:
@@ -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 {
|
||||
</div>`
|
||||
: html`<div></div>`}
|
||||
<div class="shrink-0">
|
||||
${timeRemaining > 0
|
||||
? html`<span
|
||||
class="text-[10px] font-bold uppercase tracking-widest bg-blue-600 px-2 py-0.5 rounded"
|
||||
>${timeDisplay}</span
|
||||
>`
|
||||
: html`<span
|
||||
class="text-[10px] font-bold uppercase tracking-widest bg-green-600 px-2 py-0.5 rounded"
|
||||
>${translateText("public_lobby.starting_game")}</span
|
||||
>`}
|
||||
<span
|
||||
class="text-[10px] font-bold uppercase tracking-widest bg-blue-600 px-2 py-0.5 rounded"
|
||||
>${timeDisplay}</span
|
||||
>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
+1
-1
@@ -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,
|
||||
});
|
||||
|
||||
@@ -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 &&
|
||||
|
||||
@@ -11,6 +11,8 @@ export type WorkerReady = z.infer<typeof WorkerReadySchema>;
|
||||
export type MasterLobbiesBroadcast = z.infer<
|
||||
typeof MasterLobbiesBroadcastSchema
|
||||
>;
|
||||
|
||||
export type MasterUpdateGame = z.infer<typeof MasterUpdateGameSchema>;
|
||||
export type MasterCreateGame = z.infer<typeof MasterCreateGameSchema>;
|
||||
export type WorkerMessage = z.infer<typeof WorkerMessageSchema>;
|
||||
export type MasterMessage = z.infer<typeof MasterMessageSchema>;
|
||||
@@ -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,
|
||||
]);
|
||||
|
||||
@@ -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);
|
||||
}
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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;
|
||||
|
||||
Reference in New Issue
Block a user