import { composeServerCfg, parseCfg } from './config-compose.js'; import { selectedZombieMap, mapRotation } from './map-aliases.js'; import { configWarnings, type ConfigAssignment, type ConfigInspection, type ConfigSource } from '../../config-workbench.js'; export interface ConfigLayer { source: ConfigSource; text: string; } /** Trace file composition, including assignments left in place by the composer. * This is an intended-file preview, not a query of live runtime dvars. */ export function inspectConfig(baseline: ConfigLayer, overlays: ConfigLayer[], game: string | null, map: string | null, mapSlug?: string | null): ConfigInspection { const sorted = [...overlays].sort((a, b) => a.source.id! - b.source.id!); const parsed = parseCfg(baseline.text); type TracedLine = { line: string; history: ConfigAssignment[] }; function assignment(line: string, source: ConfigSource, index: number): ConfigAssignment { const match = /^\s*(?:(?:set|seta)\s+)(\S+)\s+(.*)$/i.exec(line) ?? /^\s*(sv_maprotation)\s+(.*)$/i.exec(line); return { value: match?.[2] ?? '', source, line: index + 1 }; } const lines: TracedLine[] = parsed.order.map((entry, index) => ({ line: entry.line, history: [assignment(entry.line, baseline.source, index)] })); const replacements = new Map(); const warnings = configWarnings(baseline.text, false).map(w => `${baseline.source.name}: ${w}`); for (const overlay of sorted) { warnings.push(...configWarnings(overlay.text, true).map(w => `${overlay.source.name}: ${w}`)); overlay.text.split(/\r?\n/).forEach((line, index) => { const match = /^\s*set\s+(\S+)\s+/.exec(line); if (!match) return; const previous = replacements.get(match[1]); replacements.set(match[1], { line, history: [...(previous?.history ?? []), assignment(line, overlay.source, index)] }); }); } for (const [key, replacement] of replacements) { const index = parsed.entries.get(key); if (index === undefined) lines.push(replacement); else lines[index] = { line: replacement.line, history: [...lines[index].history, ...replacement.history] }; } const sources = [baseline.source, ...sorted.map(o => o.source)]; const selected = selectedZombieMap(game, map, mapSlug); if (selected) { const source: ConfigSource = { id: null, name: 'Server map selection', version: null, kind: 'map' }; sources.push(source); const rotation = /^\s*(?:(?:set|seta)\s+)?sv_maprotation\s+/i; const history = lines.filter(entry => rotation.test(entry.line)).flatMap(entry => entry.history); for (let i = lines.length - 1; i >= 0; i--) if (rotation.test(lines[i].line)) lines.splice(i, 1); const line = `set sv_maprotation "${mapRotation(selected)}"`; lines.push({ line, history: [...history, assignment(line, source, 0)] }); } const byKey = new Map(); for (const entry of lines) { const match = /^\s*(?:set|seta)\s+(\S+)\s+/i.exec(entry.line) ?? /^\s*(sv_maprotation)\s+/i.exec(entry.line); if (!match) continue; const normalized = match[1].toLowerCase(); const existing = byKey.get(normalized); byKey.set(normalized, { key: match[1], history: [...(existing?.history ?? []), ...entry.history] }); } return { text: composeServerCfg(baseline.text, sorted.map(o => ({ id: o.source.id!, text: o.text })), game, map, mapSlug), sources, warnings, settings: [...byKey.values()].map(({ key, history }) => ({ key, history, value: history.at(-1)!.value, source: history.at(-1)!.source })) .sort((a, b) => a.key.localeCompare(b.key)) }; }