Prevent the 3 public lobbies showing the same map or maxPlayers or (for Team games) number of teams.

It was relatively quite frequently happening that one map showed up in two or even three lobbies at the same time. Same for teams games, the Special Mix lobby would have 42 Humans vs 42 Nations and the Teams lobby would show the exact same configuration. Or there would be two lobbies with 125 players.
This commit is contained in:
VariableVince
2026-04-03 02:04:42 +02:00
parent 21c286189e
commit c7ea1befe2
2 changed files with 84 additions and 15 deletions
+39 -8
View File
@@ -149,14 +149,36 @@ export class MapPlaylist {
team: [],
};
public async gameConfig(type: PublicGameType): Promise<GameConfig> {
public async gameConfigNotInUse(
type: PublicGameType,
isInUse: (config: GameConfig) => boolean,
): Promise<GameConfig> {
const maxAttempts = 6;
let attempts = 0;
do {
const map = this.tryFirstMap(type);
const config = await this.buildConfig(type, map);
attempts++;
if (!isInUse(config) || attempts >= maxAttempts) {
this.useFirstMap(type);
return config;
}
this.firstMapToLast(type);
} while (true);
}
private async buildConfig(
type: PublicGameType,
map: GameMapType,
): Promise<GameConfig> {
if (type === "special") {
return this.getSpecialConfig();
return this.buildSpecialConfig(map);
}
const mode = type === "ffa" ? GameMode.FFA : GameMode.Team;
const map = this.getNextMap(type);
const playerTeams =
mode === GameMode.Team ? this.getTeamCount(map) : undefined;
@@ -199,9 +221,8 @@ export class MapPlaylist {
} satisfies GameConfig;
}
private async getSpecialConfig(): Promise<GameConfig> {
private async buildSpecialConfig(map: GameMapType): Promise<GameConfig> {
const mode = Math.random() < 0.5 ? GameMode.FFA : GameMode.Team;
const map = this.getNextMap("special");
const playerTeams =
mode === GameMode.Team ? this.getTeamCount(map) : undefined;
@@ -401,12 +422,22 @@ export class MapPlaylist {
} satisfies GameConfig;
}
private getNextMap(type: PublicGameType): GameMapType {
private tryFirstMap(type: PublicGameType): GameMapType {
const playlist = this.playlists[type];
if (playlist.length === 0) {
playlist.push(...this.generateNewPlaylist(type));
}
return playlist.shift()!;
return playlist[0];
}
private useFirstMap(type: PublicGameType): GameMapType {
this.tryFirstMap(type); // Ensure this.playlists[type] is populated
return this.playlists[type].shift()!;
}
private firstMapToLast(type: PublicGameType): void {
this.tryFirstMap(type);
this.playlists[type].push(this.playlists[type].shift()!);
}
private generateNewPlaylist(type: PublicGameType): GameMapType[] {
+45 -7
View File
@@ -1,7 +1,7 @@
import { Worker } from "cluster";
import winston from "winston";
import { ServerConfig } from "../core/configuration/Config";
import { PublicGameInfo, PublicGameType } from "../core/Schemas";
import { GameConfig, PublicGameInfo, PublicGameType } from "../core/Schemas";
import { generateID } from "../core/Util";
import {
MasterCreateGame,
@@ -132,14 +132,33 @@ export class MasterLobbyService {
private async maybeScheduleLobby() {
const lobbiesByType = this.getAllLobbies();
const lobbyTypes = Object.keys(lobbiesByType) as PublicGameType[];
for (const type of Object.keys(lobbiesByType) as PublicGameType[]) {
const usedMaps = new Set<string>();
const usedTeamTypes = new Set<string>();
const usedMaxPlayers = new Set<number>();
const recordInUse = (config: GameConfig) => {
usedMaps.add(config.gameMap);
if (config.playerTeams !== undefined) {
usedTeamTypes.add(String(config.playerTeams));
}
if (config.maxPlayers !== undefined) {
usedMaxPlayers.add(config.maxPlayers);
}
};
for (const type of lobbyTypes) {
const lobbies = lobbiesByType[type];
const nextLobby = lobbies[0];
if (nextLobby && nextLobby.gameConfig) {
recordInUse(nextLobby.gameConfig);
}
}
for (const type of lobbyTypes) {
const lobbies = lobbiesByType[type];
// Always ensure the next lobby has a timer, even if we already have 2+
// lobbies. This prevents a race where two lobbies are created before
// either receives a startsAt (IPC round-trip delay), leaving both stuck
// without a countdown.
const nextLobby = lobbies[0];
if (nextLobby && nextLobby.startsAt === undefined) {
this.sendMessageToWorker({
@@ -153,10 +172,29 @@ export class MasterLobbyService {
continue;
}
const gameConfig = await this.playlist.gameConfigNotInUse(type, (c) => {
if (usedMaps.has(c.gameMap)) return true;
if (
c.playerTeams !== undefined &&
usedTeamTypes.has(String(c.playerTeams))
) {
return true;
}
if (c.maxPlayers !== undefined && usedMaxPlayers.has(c.maxPlayers)) {
return true;
}
return false;
});
recordInUse(gameConfig);
this.sendMessageToWorker({
type: "createGame",
gameID: generateID(),
gameConfig: await this.playlist.gameConfig(type),
gameConfig,
publicGameType: type,
} satisfies MasterCreateGame);
}