// Rundensicherung und Zurückspielen (Rollback). // // CS2 speichert nach jeder Runde eine Sicherungsdatei des Spielzustands auf dem Server (mp_backup_round_auto). // Diese Datei lässt sich später wieder laden (mp_backup_restore_load_file). Dieses Modul kennt die Namen, // wertet die Liste aus und kürzt den gespeicherten Live-Stand passend zur Rückkehr. // Präfix und Muster wie bei TMT2: der Präfix enthält die Match-ID, das Muster Map, Runde und Stand. export const backupPrefix = (matchId) => `round_backup_${matchId}`; export const BACKUP_PATTERN = '%prefix%_%date%_%time%_%map%_round%round%_score_%score1%_%score2%.txt'; // Endung: __round_score__.txt – die Map beginnt mit zwei Kleinbuchstaben und Unterstrich (de_, cs_, ar_ …) const ROUND_RE = /_([a-z]{2}_[a-z0-9_]+?)_round(\d+)_score_(\d+)_(\d+)\.txt$/; /** Ist der Dateiname eine Sicherung genau dieses Matches? Schützt vor Pfaden wie "../" und fremden Dateien. */ export function isValidBackupFile(file, matchId) { if (typeof file !== 'string' || file.length > 200) return false; if (!file.startsWith(backupPrefix(matchId))) return false; return /^[A-Za-z0-9_.-]+\.txt$/.test(file) && !file.includes('..') && ROUND_RE.test(file); } /** Findet alle Sicherungsdateien dieses Matches in einer RCON-Antwort (Format der Antwort ist nicht festgelegt). */ export function parseBackupFiles(text, matchId) { const prefix = backupPrefix(matchId); const found = new Set(); const re = new RegExp(`${prefix}_[A-Za-z0-9_.-]+\\.txt`, 'g'); for (const m of String(text ?? '').matchAll(re)) found.add(m[0]); return [...found].filter((f) => isValidBackupFile(f, matchId)).map((file) => describeBackup(file, matchId)); } /** * Zerlegt einen Dateinamen in Map, Rundennummer und Stand. Mit matchId wird vorher der Präfix entfernt, * damit Teile der Match-ID nicht mit einem Map-Namen verwechselt werden. */ export function describeBackup(file, matchId = null) { const rest = matchId && file.startsWith(backupPrefix(matchId)) ? file.slice(backupPrefix(matchId).length) : file; const m = ROUND_RE.exec(rest); if (!m) return { file, map: null, round: null, score: null }; return { file, map: m[1], round: Number(m[2]), score: { s1: Number(m[3]), s2: Number(m[4]) } }; } /** Nur die Sicherungen der gerade gespielten Map, neueste Runde zuerst. */ export function pointsForMap(points, map) { return points.filter((p) => p.map === map).sort((a, b) => b.round - a.round); } /** * Kürzt den Live-Stand auf die Runden bis einschließlich roundNumber und rechnet den Stand neu. * Danach zählt die Anzeige weiter, sobald CS2 wieder Rundenmeldungen schickt. */ export function trimLiveState(state, roundNumber) { const rounds = (state.rounds ?? []).filter((r) => r.number <= roundNumber); const score = { A: 0, B: 0 }; for (const r of rounds) if (r.winner) score[r.winner] += 1; return { ...state, status: 'live', rounds, score, winner: null, pendingRound: null, }; }