Live-Ergebnisse: Stream-Seite, Anzeige, automatischer Mapwechsel

- Stream-Übersicht unter /stream/<Zuschauercode> (1920x1080, Lobby, Veto, Spielstand)
- Live-Stand, Rundenverlauf und Serienstand auf Match-Seite und Übersicht
- Parser mit echten AMP-Logs geprüft: Game Over mit leerer Map-Gruppe,
  Match_Start vor der Aufwärmphase, veraltete Seitenzuordnung nach der Halbzeit
- Nach Map-Ende automatisch nächste Map bzw. Abschluss der entschiedenen Serie
- LOG_DEBUG zur Fehlersuche, README und .env.example ergänzt

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
SPP UserandClaude Opus 5.5 committed 2026-10-25 19:20:10 +01:00
1 parent 5fade769e7
commit 43dc5b837a
20 files changed
+901 -44

No files matched your search

+37
View File
@@ -26,6 +26,7 @@ export class ServerManager {
Object.assign(this, { db, service, adapter, config, logger });
this.busy = new Set();
this.simulator = new DemoMatchSimulator();
this.autoAdvance = new Map();
this.lastPoll = new Map();
this.timers = [];
}
@@ -34,6 +35,37 @@ export class ServerManager {
this.service.on('created', (id) => this.provision(id).catch((err) => this.logger.error(err)));
this.service.on('vetoFinished', (id) => this.maybeConfigure(id));
this.service.on('closed', (id) => this.stop(id, 'Match beendet').catch((err) => this.logger.error(err)));
this.service.on('mapEnded', (e) => this.onMapEnded(e));
}
/**
* Nach Map-Ende: ist die Serie entschieden, wird das Match abgeschlossen, sonst die nächste Map geladen.
* Die Wartezeit lässt Spielern und Stream Zeit für den Endstand. Lädt der Organisator vorher selbst, entfällt es.
*/
onMapEnded({ matchId, number, decided, hasNext }) {
const seconds = this.config.autoAdvanceSeconds;
if (seconds < 0) return;
clearTimeout(this.autoAdvance.get(matchId));
const finish = Boolean(decided) || !hasNext;
this.service.log(matchId, 'series', finish
? `Serie entschieden – das Match wird in ${seconds} s automatisch abgeschlossen`
: `Map ${number + 1} wird in ${seconds} s automatisch geladen`);
this.service.changed(matchId);
this.autoAdvance.set(matchId, setTimeout(() => {
this.autoAdvance.delete(matchId);
const match = this.service.getMatch(matchId);
if (CLOSED_STATUSES.includes(match.status) || match.current_map_number !== number) return;
try {
if (finish) {
this.service.closeMatch(matchId, 'finished', 'Match automatisch abgeschlossen – Serie entschieden');
} else if (this.row(matchId)?.status === 'configured') {
this.service.advanceSeriesFor(matchId);
this.configure(matchId).catch((err) => this.logger.error(err));
}
} catch (err) {
this.logger.warn(`[serie] Automatischer Wechsel für ${matchId} fehlgeschlagen: ${err.message}`);
}
}, seconds * 1000));
}
/** Nach einem Neustart des Backends: unterbrochene Vorgänge aufräumen und fortsetzen. */
@@ -56,6 +88,8 @@ export class ServerManager {
stopLoops() {
this.simulator.stopAll();
for (const t of this.autoAdvance.values()) clearTimeout(t);
this.autoAdvance.clear();
this.timers.forEach(clearInterval);
this.timers = [];
}
@@ -277,6 +311,8 @@ export class ServerManager {
this.service.require(viewer, 'organizer');
const row = this.row(viewer.matchId);
if (row?.status !== 'configured') throw new HttpError(409, 'Der Server ist nicht bereit für die nächste Map', 'wrong_state');
clearTimeout(this.autoAdvance.get(viewer.matchId));
this.autoAdvance.delete(viewer.matchId);
this.service.advanceSeries(viewer);
this.configure(viewer.matchId).catch((err) => this.logger.error(err));
}
@@ -292,6 +328,7 @@ export class ServerManager {
throw new HttpError(502, `Befehl konnte nicht gesendet werden: ${err.message}`, 'server_error');
}
this.service.log(viewer.matchId, 'server', this.adapter.simulated ? 'Aufwärmphase beendet (simuliert)' : 'Aufwärmphase beendet – Match startet');
this.service.markWarmupEnded(viewer.matchId);
if (this.adapter.simulated) this.startDemoSimulation(viewer.matchId);
this.service.changed(viewer.matchId);
}