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
This commit is contained in:
1 parent
306b3aa7e5
commit
263c7e7b7c
20 files changed
+901
-44
No files matched your search
@@ -39,8 +39,11 @@ export class DemoMatchSimulator {
|
||||
run.score[winner] += 1;
|
||||
run.played += 1;
|
||||
const notice = REASONS[side][Math.floor(this.random() * REASONS[side].length)];
|
||||
// Reihenfolge wie bei einem echten CS2-Server: Rundenende, Seitenzuordnung der gespielten Runde, Round_End
|
||||
const lines = [
|
||||
`${stamp()}Team "${side === 'CT' ? 'CT' : 'TERRORIST'}" triggered "SFUI_Notice_${notice}" (CT "${run.score[run.sides.CT]}") (T "${run.score[run.sides.T]}")`,
|
||||
...teamLines(),
|
||||
`${stamp()}World triggered "Round_End"`,
|
||||
];
|
||||
if (run.score[winner] >= toWin) {
|
||||
lines.push(`${stamp()}Game Over: competitive mg_active ${map} score ${run.score[run.sides.CT]}:${run.score[run.sides.T]} after 1 min`);
|
||||
@@ -48,10 +51,8 @@ export class DemoMatchSimulator {
|
||||
this.stop(matchId);
|
||||
return;
|
||||
}
|
||||
if (run.played === maxRounds / 2) {
|
||||
run.sides = { CT: run.sides.T, T: run.sides.CT };
|
||||
lines.push(...teamLines());
|
||||
}
|
||||
// Halbzeit: Seiten wechseln, die nächsten Zeilen nennen die neue Zuordnung
|
||||
if (run.played === maxRounds / 2) run.sides = { CT: run.sides.T, T: run.sides.CT };
|
||||
onLines(lines);
|
||||
}, roundMs);
|
||||
this.runs.set(matchId, run);
|
||||
|
||||
@@ -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);
|
||||
}
|
||||
|
||||
Reference in new issue
Block a user