Beim Konfigurieren aktiviert CS2 Mapveto die Rundensicherung des Servers (mp_backup_round_auto, Präfix mit Match-ID). Im Admin-Modus lassen sich die Sicherungen der laufenden Map auflisten und zurückspielen. Der Live-Stand wird auf die Runden bis zur Sicherung gekürzt. Organisatoren haben keinen Zugang zu den Rollback-Routen. Sicherheit: Dateinamen werden gegen Match-ID und Muster geprüft, damit keine fremden Dateien oder Befehle in die RCON-Zeile gelangen. Co-Authored-By: Claude Haiku 5.5 <noreply@anthropic.com>
63 lines
2.9 KiB
JavaScript
63 lines
2.9 KiB
JavaScript
// 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: _<map>_round<N>_score_<a>_<b>.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,
|
||
};
|
||
}
|