mirror of
https://github.com/openfrontio/OpenFrontIO.git
synced 2026-06-21 07:40:43 +00:00
dynamic lobby sizes
This commit is contained in:
@@ -88,9 +88,12 @@ export class GameModeSelector extends LitElement {
|
||||
);
|
||||
this.requestUpdate();
|
||||
|
||||
const allGames = Object.values(lobbies.games ?? {}).flat();
|
||||
const allGames =
|
||||
lobbies.mode === "multi"
|
||||
? Object.values(lobbies.games).flat()
|
||||
: [lobbies.lobby];
|
||||
for (const game of allGames) {
|
||||
const mapType = game.gameConfig?.gameMap as GameMapType;
|
||||
const mapType = game?.gameConfig?.gameMap as GameMapType;
|
||||
if (mapType && !this.mapAspectRatios.has(mapType)) {
|
||||
// New Map reference triggers Lit reactivity; placeholder ratio 1 lets
|
||||
// has() guard against duplicate in-flight fetches.
|
||||
@@ -114,9 +117,15 @@ export class GameModeSelector extends LitElement {
|
||||
}
|
||||
|
||||
render() {
|
||||
const ffa = this.lobbies?.games?.["ffa"]?.[0];
|
||||
const teams = this.lobbies?.games?.["team"]?.[0];
|
||||
const special = this.lobbies?.games?.["special"]?.[0];
|
||||
const lobbies = this.lobbies;
|
||||
const isSingle = lobbies?.mode === "single";
|
||||
const singleLobby = lobbies?.mode === "single" ? lobbies.lobby : undefined;
|
||||
const ffa =
|
||||
lobbies?.mode === "multi" ? lobbies.games?.["ffa"]?.[0] : undefined;
|
||||
const teams =
|
||||
lobbies?.mode === "multi" ? lobbies.games?.["team"]?.[0] : undefined;
|
||||
const special =
|
||||
lobbies?.mode === "multi" ? lobbies.games?.["special"]?.[0] : undefined;
|
||||
|
||||
return html`
|
||||
<div class="flex flex-col gap-4 w-full px-4 sm:px-0 mx-auto pb-4 sm:pb-0">
|
||||
@@ -148,47 +157,74 @@ export class GameModeSelector extends LitElement {
|
||||
</div>
|
||||
<!-- Game cards grid -->
|
||||
<div
|
||||
class="grid grid-cols-1 sm:grid-cols-[2fr_1fr] gap-4 sm:h-[min(24rem,40vh)]"
|
||||
class="grid grid-cols-1 gap-4 sm:h-[min(24rem,40vh)] ${isSingle
|
||||
? ""
|
||||
: "sm:grid-cols-[2fr_1fr]"}"
|
||||
>
|
||||
<!-- Left col: main card (desktop only) -->
|
||||
${special
|
||||
? html`<div class="hidden sm:block">
|
||||
${this.renderSpecialLobbyCard(special)}
|
||||
</div>`
|
||||
: ffa
|
||||
? html`<div class="hidden sm:block">
|
||||
${this.renderLobbyCard(ffa, this.getLobbyTitle(ffa))}
|
||||
</div>`
|
||||
: nothing}
|
||||
${isSingle && singleLobby
|
||||
? html`
|
||||
<!-- Single lobby mode: one full-width card -->
|
||||
<div class="hidden sm:block">
|
||||
${singleLobby.publicGameType === "special"
|
||||
? this.renderSpecialLobbyCard(singleLobby)
|
||||
: this.renderLobbyCard(
|
||||
singleLobby,
|
||||
this.getLobbyTitle(singleLobby),
|
||||
)}
|
||||
</div>
|
||||
<div class="sm:hidden">
|
||||
${singleLobby.publicGameType === "special"
|
||||
? this.renderSpecialLobbyCard(singleLobby)
|
||||
: this.renderLobbyCard(
|
||||
singleLobby,
|
||||
this.getLobbyTitle(singleLobby),
|
||||
)}
|
||||
</div>
|
||||
`
|
||||
: html`
|
||||
<!-- Left col: main card (desktop only) -->
|
||||
${special
|
||||
? html`<div class="hidden sm:block">
|
||||
${this.renderSpecialLobbyCard(special)}
|
||||
</div>`
|
||||
: ffa
|
||||
? html`<div class="hidden sm:block">
|
||||
${this.renderLobbyCard(ffa, this.getLobbyTitle(ffa))}
|
||||
</div>`
|
||||
: nothing}
|
||||
|
||||
<!-- Right col: FFA + teams (desktop only) -->
|
||||
<div class="hidden sm:flex sm:flex-col sm:gap-4">
|
||||
${special && ffa
|
||||
? html`<div class="flex-1 min-h-0">
|
||||
${this.renderLobbyCard(ffa, this.getLobbyTitle(ffa))}
|
||||
</div>`
|
||||
: nothing}
|
||||
${teams
|
||||
? html`<div class="flex-1 min-h-0">
|
||||
${this.renderLobbyCard(teams, this.getLobbyTitle(teams))}
|
||||
</div>`
|
||||
: nothing}
|
||||
</div>
|
||||
<!-- Right col: FFA + teams (desktop only) -->
|
||||
<div class="hidden sm:flex sm:flex-col sm:gap-4">
|
||||
${special && ffa
|
||||
? html`<div class="flex-1 min-h-0">
|
||||
${this.renderLobbyCard(ffa, this.getLobbyTitle(ffa))}
|
||||
</div>`
|
||||
: nothing}
|
||||
${teams
|
||||
? html`<div class="flex-1 min-h-0">
|
||||
${this.renderLobbyCard(
|
||||
teams,
|
||||
this.getLobbyTitle(teams),
|
||||
)}
|
||||
</div>`
|
||||
: nothing}
|
||||
</div>
|
||||
|
||||
<!-- Mobile: special, ffa, teams inline -->
|
||||
<div class="sm:hidden">
|
||||
${special ? this.renderSpecialLobbyCard(special) : nothing}
|
||||
</div>
|
||||
<div class="sm:hidden">
|
||||
${ffa
|
||||
? this.renderLobbyCard(ffa, this.getLobbyTitle(ffa))
|
||||
: nothing}
|
||||
</div>
|
||||
<div class="sm:hidden">
|
||||
${teams
|
||||
? this.renderLobbyCard(teams, this.getLobbyTitle(teams))
|
||||
: nothing}
|
||||
</div>
|
||||
<!-- Mobile: special, ffa, teams inline -->
|
||||
<div class="sm:hidden">
|
||||
${special ? this.renderSpecialLobbyCard(special) : nothing}
|
||||
</div>
|
||||
<div class="sm:hidden">
|
||||
${ffa
|
||||
? this.renderLobbyCard(ffa, this.getLobbyTitle(ffa))
|
||||
: nothing}
|
||||
</div>
|
||||
<div class="sm:hidden">
|
||||
${teams
|
||||
? this.renderLobbyCard(teams, this.getLobbyTitle(teams))
|
||||
: nothing}
|
||||
</div>
|
||||
`}
|
||||
</div>
|
||||
|
||||
<!-- Solo: full width, desktop only -->
|
||||
|
||||
+13
-1
@@ -164,11 +164,23 @@ export const PublicGameInfoSchema = z.object({
|
||||
publicGameType: PublicGameTypeSchema,
|
||||
});
|
||||
|
||||
export const PublicGamesSchema = z.object({
|
||||
const SingleLobbyGamesSchema = z.object({
|
||||
mode: z.literal("single"),
|
||||
serverTime: z.number(),
|
||||
lobby: PublicGameInfoSchema,
|
||||
});
|
||||
|
||||
const MultiLobbyGamesSchema = z.object({
|
||||
mode: z.literal("multi"),
|
||||
serverTime: z.number(),
|
||||
games: z.record(PublicGameTypeSchema, z.array(PublicGameInfoSchema)),
|
||||
});
|
||||
|
||||
export const PublicGamesSchema = z.discriminatedUnion("mode", [
|
||||
SingleLobbyGamesSchema,
|
||||
MultiLobbyGamesSchema,
|
||||
]);
|
||||
|
||||
export class LobbyInfoEvent implements GameEvent {
|
||||
constructor(
|
||||
public lobby: GameInfo,
|
||||
|
||||
@@ -23,6 +23,7 @@ export type MasterMessage = z.infer<typeof MasterMessageSchema>;
|
||||
const WorkerLobbyListSchema = z.object({
|
||||
type: z.literal("lobbyList"),
|
||||
lobbies: z.array(PublicGameInfoSchema),
|
||||
clientCount: z.number(),
|
||||
});
|
||||
|
||||
const WorkerReadySchema = z.object({
|
||||
|
||||
@@ -19,12 +19,17 @@ export interface MasterLobbyServiceOptions {
|
||||
log: typeof logger;
|
||||
}
|
||||
|
||||
const CCU_SINGLE_LOBBY_THRESHOLD = 1500;
|
||||
const SINGLE_LOBBY_ROTATION: PublicGameType[] = ["special", "team", "ffa"];
|
||||
|
||||
export class MasterLobbyService {
|
||||
private readonly workers = new Map<number, Worker>();
|
||||
// Worker id => the lobbies it owns.
|
||||
private readonly workerLobbies = new Map<number, PublicGameInfo[]>();
|
||||
private readonly workerClientCounts = new Map<number, number>();
|
||||
private readonly readyWorkers = new Set<number>();
|
||||
private started = false;
|
||||
private singleLobbyRotationIndex = 0;
|
||||
|
||||
constructor(
|
||||
private config: ServerConfig,
|
||||
@@ -49,6 +54,7 @@ export class MasterLobbyService {
|
||||
break;
|
||||
case "lobbyList":
|
||||
this.workerLobbies.set(workerId, msg.lobbies);
|
||||
this.workerClientCounts.set(workerId, msg.clientCount);
|
||||
break;
|
||||
}
|
||||
});
|
||||
@@ -109,13 +115,27 @@ export class MasterLobbyService {
|
||||
return result;
|
||||
}
|
||||
|
||||
private getTotalCCU(): number {
|
||||
let total = 0;
|
||||
for (const count of this.workerClientCounts.values()) {
|
||||
total += count;
|
||||
}
|
||||
return total;
|
||||
}
|
||||
|
||||
private broadcastLobbies() {
|
||||
const singleLobbyMode = this.getTotalCCU() < CCU_SINGLE_LOBBY_THRESHOLD;
|
||||
const publicGames = singleLobbyMode
|
||||
? this.buildSingleLobbyBroadcast()
|
||||
: {
|
||||
mode: "multi" as const,
|
||||
serverTime: Date.now(),
|
||||
games: this.getAllLobbies(),
|
||||
};
|
||||
|
||||
const msg = {
|
||||
type: "lobbiesBroadcast",
|
||||
publicGames: {
|
||||
serverTime: Date.now(),
|
||||
games: this.getAllLobbies(),
|
||||
},
|
||||
publicGames,
|
||||
} satisfies MasterLobbiesBroadcast;
|
||||
for (const worker of this.workers.values()) {
|
||||
worker.send(msg, (e) => {
|
||||
@@ -126,7 +146,63 @@ export class MasterLobbyService {
|
||||
}
|
||||
}
|
||||
|
||||
private buildSingleLobbyBroadcast() {
|
||||
const allLobbies = this.getAllLobbies();
|
||||
const type = SINGLE_LOBBY_ROTATION[this.singleLobbyRotationIndex];
|
||||
const lobby = allLobbies[type][0];
|
||||
// If the active rotation type has no lobby yet, fall back to any available lobby.
|
||||
const activeLobby =
|
||||
lobby ?? SINGLE_LOBBY_ROTATION.map((t) => allLobbies[t][0]).find(Boolean);
|
||||
return {
|
||||
mode: "single" as const,
|
||||
serverTime: Date.now(),
|
||||
lobby: activeLobby,
|
||||
};
|
||||
}
|
||||
|
||||
private async maybeScheduleLobby() {
|
||||
const singleLobbyMode = this.getTotalCCU() < CCU_SINGLE_LOBBY_THRESHOLD;
|
||||
|
||||
if (singleLobbyMode) {
|
||||
await this.maybeScheduleSingleLobby();
|
||||
} else {
|
||||
await this.maybeScheduleAllLobbies();
|
||||
}
|
||||
}
|
||||
|
||||
private async maybeScheduleSingleLobby() {
|
||||
const allLobbies = this.getAllLobbies();
|
||||
const type = SINGLE_LOBBY_ROTATION[this.singleLobbyRotationIndex];
|
||||
const lobbies = allLobbies[type];
|
||||
|
||||
// Advance rotation when the current lobby has started.
|
||||
if (lobbies[0]?.startsAt !== undefined) {
|
||||
this.singleLobbyRotationIndex =
|
||||
(this.singleLobbyRotationIndex + 1) % SINGLE_LOBBY_ROTATION.length;
|
||||
}
|
||||
|
||||
// Clean up any stale lobbies from the other two types (don't schedule more of them).
|
||||
// We only need to manage the active type.
|
||||
if (lobbies.length >= 2) return;
|
||||
|
||||
const nextLobby = lobbies[0];
|
||||
if (nextLobby && nextLobby.startsAt === undefined) {
|
||||
this.sendMessageToWorker({
|
||||
type: "updateLobby",
|
||||
gameID: nextLobby.gameID,
|
||||
startsAt: Date.now() + this.config.gameCreationRate(),
|
||||
});
|
||||
}
|
||||
|
||||
this.sendMessageToWorker({
|
||||
type: "createGame",
|
||||
gameID: generateID(),
|
||||
gameConfig: await this.playlist.gameConfig(type),
|
||||
publicGameType: type,
|
||||
} satisfies MasterCreateGame);
|
||||
}
|
||||
|
||||
private async maybeScheduleAllLobbies() {
|
||||
const lobbiesByType = this.getAllLobbies();
|
||||
|
||||
for (const type of Object.keys(lobbiesByType) as PublicGameType[]) {
|
||||
|
||||
@@ -88,7 +88,11 @@ export class WorkerLobbyService {
|
||||
publicGameType: gi.publicGameType!,
|
||||
} satisfies PublicGameInfo;
|
||||
});
|
||||
process.send?.({ type: "lobbyList", lobbies } satisfies WorkerLobbyList);
|
||||
process.send?.({
|
||||
type: "lobbyList",
|
||||
lobbies,
|
||||
clientCount: this.gm.activeClients(),
|
||||
} satisfies WorkerLobbyList);
|
||||
}
|
||||
|
||||
private setupUpgradeHandler() {
|
||||
|
||||
Reference in New Issue
Block a user