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); }