Refresh Configs with browser editing and source inspection

This commit is contained in:
2026-09-18 11:02:22 -04:00
parent ec51e38312
commit ef8ff69d17
13 changed files with 944 additions and 395 deletions
+62
View File
@@ -0,0 +1,62 @@
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<string, TracedLine>();
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<string, { key: string; history: ConfigAssignment[] }>();
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))
};
}