From c7ea1befe2090dfcb03354476b9b995e3e0c231d Mon Sep 17 00:00:00 2001 From: VariableVince <24507472+VariableVince@users.noreply.github.com> Date: Fri, 3 Apr 2026 02:04:42 +0200 Subject: [PATCH] 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. --- src/server/MapPlaylist.ts | 47 ++++++++++++++++++++++++----- src/server/MasterLobbyService.ts | 52 +++++++++++++++++++++++++++----- 2 files changed, 84 insertions(+), 15 deletions(-) diff --git a/src/server/MapPlaylist.ts b/src/server/MapPlaylist.ts index 0449c4c21..2987d2e42 100644 --- a/src/server/MapPlaylist.ts +++ b/src/server/MapPlaylist.ts @@ -149,14 +149,36 @@ export class MapPlaylist { team: [], }; - public async gameConfig(type: PublicGameType): Promise { + public async gameConfigNotInUse( + type: PublicGameType, + isInUse: (config: GameConfig) => boolean, + ): Promise { + 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 { 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 { + private async buildSpecialConfig(map: GameMapType): Promise { 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[] { diff --git a/src/server/MasterLobbyService.ts b/src/server/MasterLobbyService.ts index 9285b8a91..80c3da0c6 100644 --- a/src/server/MasterLobbyService.ts +++ b/src/server/MasterLobbyService.ts @@ -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(); + const usedTeamTypes = new Set(); + const usedMaxPlayers = new Set(); + + 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); }