export interface ConfigSource { id: number | null; name: string; version: number | null; kind: 'baseline' | 'config' | 'map'; draft?: boolean; } export interface ConfigAssignment { value: string; source: ConfigSource; line: number; } export interface ConfigSetting { key: string; value: string; source: ConfigSource; history: ConfigAssignment[]; } export interface ConfigInspection { text: string; settings: ConfigSetting[]; sources: ConfigSource[]; warnings: string[]; reason?: string; } export interface ConfigServer { name: string; game: string | null; map: string | null; } /** The same lowercase `set` grammar that the overlay composer accepts. */ export function configLines(text: string) { return text.split(/\r?\n/).flatMap((line, index) => { const match = /^(\s*set\s+)(\S+)(\s+)(.*)$/.exec(line); return match ? [{ index, key: match[2], value: match[4], prefix: match[1] + match[2] + match[3] }] : []; }); } export function changeConfigValue(text: string, index: number, value: string): string { // Keep every separator, comment and unrelated line byte-for-byte intact. const parts = text.split(/(\r?\n)/); const entry = configLines(text).find(line => line.index === index); if (entry) parts[index * 2] = entry.prefix + value.replace(/[\r\n]/g, ''); return parts.join(''); } /** Add one assignment without changing existing commands or line endings. */ export function addConfigSetting(text: string, key: string, value: string): string { key = key.trim(); value = value.trim(); if (!/^[a-zA-Z_][a-zA-Z0-9_]*$/.test(key)) throw new Error('Use a setting name containing letters, numbers, and underscores.'); if (!value || /[\r\n]/.test(value) || value.startsWith('//')) throw new Error('Enter a value on one line.'); if (configLines(text).some(entry => entry.key.toLowerCase() === key.toLowerCase())) throw new Error('This setting already exists. Find it above to edit its value.'); const newline = text.includes('\r\n') ? '\r\n' : '\n'; const formatted = /\s/.test(value) && !value.startsWith('"') ? JSON.stringify(value) : value; return text + (text && !text.endsWith('\n') ? newline : '') + `set ${key} ${formatted}` + newline; } export function configWarnings(text: string, overlay: boolean): string[] { const warnings: string[] = []; const seen = new Map(); for (const entry of configLines(text)) { const key = entry.key.toLowerCase(); if (seen.has(key)) warnings.push(`Line ${entry.index + 1}: ${entry.key} is also assigned on line ${seen.get(key)}.`); seen.set(key, entry.index + 1); if (!entry.value.trim() || entry.value.trim().startsWith('//')) warnings.push(`Line ${entry.index + 1}: ${entry.key} has no value.`); } if (overlay) text.split(/\r?\n/).forEach((line, index) => { if (line.trim() && !/^\s*\/\//.test(line) && !/^\s*set\s+\S+\s+/.test(line)) { warnings.push(`Line ${index + 1} is ignored by overlays. Use set KEY VALUE.`); } }); if (/^\s*(?:(?:set|seta)\s+)?sv_maprotation\s+/im.test(text)) { warnings.push('For recognized maps, the server’s selected map overrides sv_maprotation.'); } return warnings; }