import fs from 'node:fs'; import { BEST_OF, generateFlow, validateFlow } from './veto.js'; /** * Lädt die Map-Pools und markiert jede Kombination aus Modus und Format als unterstützt oder nicht. * Nur gültige Kombinationen können beim Erstellen eines Matches gewählt werden. */ export function loadMapPools(file) { const raw = JSON.parse(fs.readFileSync(file, 'utf8')); return buildMapPools(raw); } export function buildMapPools(raw) { const pools = {}; for (const [mode, pool] of Object.entries(raw.pools ?? {})) { const ids = new Set(); for (const map of pool.maps ?? []) { if (!/^[a-z0-9_]+$/.test(map.id)) throw new Error(`Ungültige Map-ID "${map.id}" im Pool "${mode}"`); if (ids.has(map.id)) throw new Error(`Map "${map.id}" ist im Pool "${mode}" doppelt`); if (map.image !== undefined && !/^(https?:\/\/|\/)[^\s"'()<>]+$/.test(map.image)) { throw new Error(`Bild für "${map.id}" im Pool "${mode}" muss eine http(s)-URL oder ein Pfad mit / sein`); } ids.add(map.id); } // "active": false = Map ist im Katalog, wird aber erst nach Auswahl im Admin-Bereich gespielt const active = pool.maps.filter((m) => m.active !== false).map((m) => m.id); pools[mode] = { mode, label: pool.label ?? mode, teamSize: pool.teamSize, gameType: pool.gameType, gameMode: pool.gameMode, allMaps: pool.maps.map(({ active: _ignored, ...m }) => m), flows: pool.flows ?? {}, maps: [], formats: {}, }; setActiveMaps(pools[mode], active, { warn: true }); } return pools; } /** * Veto-Abläufe für die aktiven Maps: Ein in der Konfiguration hinterlegter Ablauf gilt, wenn er zur Map-Anzahl passt, * sonst erzeugt die Anwendung einen passenden Ablauf. */ export function formatsFor(pool, mapCount, { warn = false } = {}) { const formats = {}; for (const format of Object.keys(BEST_OF)) { const configured = pool.flows?.[format]; if (configured && validateFlow(configured, mapCount, format).length === 0) { formats[format] = { supported: true, flow: configured, generated: false }; continue; } const flow = generateFlow(mapCount, format); if (!flow) { formats[format] = { supported: false, reason: `${format.toUpperCase()} braucht mindestens ${Math.max(BEST_OF[format], 2)} Maps` }; continue; } const errors = validateFlow(flow, mapCount, format); formats[format] = errors.length ? { supported: false, reason: errors.join('; ') } : { supported: true, flow, generated: true }; if (errors.length && warn) console.warn(`[map-pools] ${pool.mode}/${format} deaktiviert: ${errors.join('; ')}`); } return formats; } /** Aktive Maps eines Pools setzen (Reihenfolge wie im Katalog) und die Formate neu berechnen. */ export function setActiveMaps(pool, ids, options) { const wanted = new Set(ids); pool.maps = pool.allMaps.filter((m) => wanted.has(m.id)); pool.formats = formatsFor(pool, pool.maps.length, options); return pool; } export function publicPools(pools) { return Object.values(pools).map((p) => ({ mode: p.mode, label: p.label, teamSize: p.teamSize, maps: p.maps, formats: Object.fromEntries( Object.entries(p.formats).map(([f, v]) => [f, { supported: v.supported, reason: v.reason ?? null, flow: v.flow ?? null }]), ), })); }