Live-Ergebnisse aus CS2-Serverlogs (Backend)
Server bekommt beim Konfigurieren logaddress_add_http mit geheimem Match-Schlüssel. Ein Parser wertet Rundenende, Seitenzuordnung, Matchstart und Spielende aus; Stand, Rundenverlauf und Sieger werden pro Map gespeichert und live verteilt. Der Demo-Adapter simuliert nach der Aufwärmphase einen Matchverlauf über dieselben Logzeilen. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
1 parent
5ea5638115
commit
5fade769e7
11 files changed
+515
-11
No files matched your search
@@ -1,6 +1,8 @@
|
||||
import { EventEmitter } from 'node:events';
|
||||
import { BEST_OF, computeVetoState, resolveAction, VetoError } from './veto.js';
|
||||
import { formatCode, hashToken, normalizeCode, randomCode, randomId, randomToken, safeEqual } from './security.js';
|
||||
import { applyLogLines, initialLiveState, seriesScore, splitLogBody } from './liveScore.js';
|
||||
import { sanitizeConsoleText } from './servers/cs2Config.js';
|
||||
|
||||
export class HttpError extends Error {
|
||||
constructor(status, message, code = 'error') {
|
||||
@@ -432,6 +434,68 @@ export class MatchService extends EventEmitter {
|
||||
return nextNumber;
|
||||
}
|
||||
|
||||
// ---------- Live-Ergebnisse ----------
|
||||
|
||||
/** Startet die Zählung einer Map neu (beim Laden der Map auf dem Server). */
|
||||
resetLiveScore(matchId, number) {
|
||||
this.db
|
||||
.prepare('UPDATE series_maps SET score_a = 0, score_b = 0, winner = NULL, live = NULL, live_updated_at = NULL WHERE match_id = ? AND number = ?')
|
||||
.run(matchId, number);
|
||||
}
|
||||
|
||||
/** Logzeilen vom CS2-Server (logaddress_add_http). Der Schlüssel in der URL identifiziert den Server. */
|
||||
ingestLogs(matchId, key, body) {
|
||||
const server = this.serverRow(matchId);
|
||||
if (!server || !safeEqual(String(key ?? ''), server.config_key)) {
|
||||
throw new HttpError(403, 'Unbekannter Log-Schlüssel', 'forbidden');
|
||||
}
|
||||
this.applyLiveLog(matchId, splitLogBody(body));
|
||||
}
|
||||
|
||||
applyLiveLog(matchId, lines) {
|
||||
if (!lines.length) return;
|
||||
let ended = null;
|
||||
this.db.transaction(() => {
|
||||
const match = this.getMatch(matchId);
|
||||
if (CLOSED_STATUSES.includes(match.status)) return;
|
||||
const row = this.seriesMaps(matchId).find((m) => m.number === match.current_map_number);
|
||||
if (!row) return;
|
||||
const before = row.live ? JSON.parse(row.live) : initialLiveState({ map: row.map, ctTeam: row.ct_team });
|
||||
const { state, events } = applyLogLines(before, lines, {
|
||||
teamNames: { A: sanitizeConsoleText(match.team_a_name), B: sanitizeConsoleText(match.team_b_name) },
|
||||
maxRounds: match.mode === 'wingman' ? this.config.match.maxRoundsWingman : this.config.match.maxRoundsCompetitive,
|
||||
overtimeRounds: 6,
|
||||
});
|
||||
if (!events.length && JSON.stringify(before) === JSON.stringify(state)) return;
|
||||
const finished = events.find((e) => e.type === 'map_end');
|
||||
this.db
|
||||
.prepare(
|
||||
`UPDATE series_maps SET score_a = ?, score_b = ?, winner = ?, live = ?, live_updated_at = ?
|
||||
${finished ? ", status = 'done'" : ''} WHERE match_id = ? AND number = ?`,
|
||||
)
|
||||
.run(state.score.A, state.score.B, state.winner, JSON.stringify(state), this.now(), matchId, row.number);
|
||||
if (events.some((e) => e.type === 'match_start')) this.log(matchId, 'score', `Map ${row.number} (${this.mapName(match, row.map)}) live – Zählung beginnt`);
|
||||
if (finished) {
|
||||
ended = { row, state, match };
|
||||
const winner = state.winner ? this.teamName(match, state.winner) : 'Unentschieden';
|
||||
this.log(matchId, 'score',
|
||||
`Map ${row.number} (${this.mapName(match, row.map)}) beendet: ${match.team_a_name} ${state.score.A}:${state.score.B} ${match.team_b_name} – Sieger: ${winner}`);
|
||||
}
|
||||
})();
|
||||
if (ended) {
|
||||
const maps = this.seriesMaps(matchId);
|
||||
const score = seriesScore(maps);
|
||||
const needed = Math.floor(BEST_OF[ended.match.format] / 2) + 1;
|
||||
const decided = score.A >= needed ? 'A' : score.B >= needed ? 'B' : null;
|
||||
if (decided) this.log(matchId, 'score', `Serie entschieden: ${this.teamName(ended.match, decided)} gewinnt ${score[decided]}:${score[decided === 'A' ? 'B' : 'A']}`);
|
||||
}
|
||||
this.changed(matchId);
|
||||
}
|
||||
|
||||
mapName(match, mapId) {
|
||||
return JSON.parse(match.maps).find((m) => m.id === mapId)?.name ?? mapId;
|
||||
}
|
||||
|
||||
markCurrentMapLive(matchId) {
|
||||
const match = this.getMatch(matchId);
|
||||
this.db
|
||||
@@ -479,7 +543,13 @@ export class MatchService extends EventEmitter {
|
||||
const byTeam = Object.fromEntries(counts.all(m.id).map((r) => [r.team, r.n]));
|
||||
const server = this.serverRow(m.id);
|
||||
const pool = this.pools[m.mode];
|
||||
const maps = this.seriesMaps(m.id);
|
||||
const current = maps.find((s) => s.number === m.current_map_number);
|
||||
return {
|
||||
seriesScore: maps.length ? seriesScore(maps) : null,
|
||||
currentMap: current && (current.status === 'live' || current.status === 'done')
|
||||
? { number: current.number, name: this.mapName(m, current.map), score: { A: current.score_a, B: current.score_b }, finished: current.status === 'done' }
|
||||
: null,
|
||||
id: m.id,
|
||||
teamA: m.team_a_name,
|
||||
teamB: m.team_b_name,
|
||||
@@ -528,15 +598,27 @@ export class MatchService extends EventEmitter {
|
||||
|
||||
const veto = this.vetoState(match);
|
||||
const actions = this.vetoActions(matchId);
|
||||
const series = this.seriesMaps(matchId).map((s) => ({
|
||||
number: s.number,
|
||||
map: s.map,
|
||||
mapName: mapNames[s.map] ?? s.map,
|
||||
pickedBy: s.picked_by,
|
||||
sideChosenBy: s.side_chosen_by,
|
||||
ctTeam: s.ct_team,
|
||||
status: s.status,
|
||||
}));
|
||||
const seriesRows = this.seriesMaps(matchId);
|
||||
const series = seriesRows.map((s) => {
|
||||
const live = s.live ? JSON.parse(s.live) : null;
|
||||
return {
|
||||
number: s.number,
|
||||
map: s.map,
|
||||
mapName: mapNames[s.map] ?? s.map,
|
||||
pickedBy: s.picked_by,
|
||||
sideChosenBy: s.side_chosen_by,
|
||||
ctTeam: s.ct_team,
|
||||
status: s.status,
|
||||
score: { A: s.score_a, B: s.score_b },
|
||||
winner: s.winner,
|
||||
live: live && {
|
||||
status: live.status,
|
||||
sides: live.sides,
|
||||
rounds: live.rounds,
|
||||
updatedAt: s.live_updated_at,
|
||||
},
|
||||
};
|
||||
});
|
||||
|
||||
const snapshot = {
|
||||
id: match.id,
|
||||
@@ -578,6 +660,8 @@ export class MatchService extends EventEmitter {
|
||||
})),
|
||||
},
|
||||
series,
|
||||
seriesScore: seriesScore(seriesRows),
|
||||
maxRounds: match.mode === 'wingman' ? this.config.match.maxRoundsWingman : this.config.match.maxRoundsCompetitive,
|
||||
server: this.serverView(match, viewer),
|
||||
events: publicView
|
||||
? []
|
||||
@@ -594,6 +678,8 @@ export class MatchService extends EventEmitter {
|
||||
A: `${base}/join/${formatCode(match.code_a)}`,
|
||||
B: `${base}/join/${formatCode(match.code_b)}`,
|
||||
spectator: `${base}/watch/${formatCode(match.code_spectator)}`,
|
||||
// Vollbild-Übersicht für den Stream (OBS-Browserquelle), nutzt den Zuschauercode
|
||||
stream: `${base}/stream/${formatCode(match.code_spectator)}`,
|
||||
},
|
||||
};
|
||||
}
|
||||
|
||||
Reference in new issue
Block a user