Merge feature/veto-timer in main

This commit is contained in:
mregosar committed 2026-10-26 11:26:34 +01:00
commit 656bfbaadf
5 files changed
+73

No files matched your search

+2
View File
@@ -23,6 +23,7 @@ export function createApp(config, { adapter: adapterOverride, logger = console }
const adapter = adapterOverride ?? createServerAdapter(config, { getUsedPorts: () => manager.usedPorts() });
manager = new ServerManager({ db, service, adapter, config, logger });
manager.init();
service.startVetoTimer();
const app = express();
if (config.trustProxy) app.set('trust proxy', 1);
@@ -54,6 +55,7 @@ export function createApp(config, { adapter: adapterOverride, logger = console }
},
async close() {
manager.stopLoops();
service.stopVetoTimer();
// Laufende Server-Aktionen (Start, Konfiguration) kurz abwarten, bevor die Datenbank schließt
await manager.idle(5000);
io.disposeRealtime();
+2
View File
@@ -52,6 +52,8 @@ export function loadConfig(overrides = {}) {
maxLifetimeHours: int('SERVER_MAX_LIFETIME_HOURS', 8),
statusPollSeconds: int('SERVER_STATUS_POLL_SECONDS', 5),
reaperIntervalSeconds: int('SERVER_REAPER_INTERVAL_SECONDS', 60),
// Frist pro Veto-Schritt in Sekunden, danach führt das Backend den Schritt automatisch aus. 0 = keine Frist
vetoStepSeconds: int('VETO_STEP_SECONDS', 30),
match: {
tvDelaySeconds: int('CS2_TV_DELAY', 90),
+53
View File
@@ -351,6 +351,58 @@ export class MatchService extends EventEmitter {
this.changed(viewer.matchId);
}
/** Ablaufzeitpunkt des aktuellen Veto-Schritts (ms), null wenn keine Frist gilt. */
vetoDeadline(match) {
if (!this.config.vetoStepSeconds || match.status !== 'veto') return null;
const actions = this.vetoActions(match.id);
const since = actions.length ? actions[actions.length - 1].created_at : match.veto_started_at;
return since == null ? null : since + this.config.vetoStepSeconds * 1000;
}
vetoRemainingMs(match) {
const deadline = this.vetoDeadline(match);
return deadline === null ? null : Math.max(0, deadline - this.now());
}
/** Führt den aktuellen Veto-Schritt für das Team am Zug aus: zufällige Map bzw. zufällige Seite. */
autoVetoStep(match) {
const state = this.vetoState(match);
if (state.done || !state.current) return;
const { type, team, index } = state.current;
const pick = (list) => list[Math.floor(Math.random() * list.length)];
const viewer = {
matchId: match.id, role: 'captain', isOrganizer: false, isCaptain: true, isSpectator: false, playerId: null, team,
};
const action = type === 'side'
? { stepIndex: index, side: pick(['CT', 'T']) }
: { stepIndex: index, map: pick(state.maps.filter((m) => m.status === 'available')).id };
this.log(match.id, 'veto', `Zeit abgelaufen – ${this.teamName(match, team)} wird automatisch vertreten`);
this.vetoAction(viewer, action);
}
/** Läuft die Frist eines Veto-Schritts ab, führt das Backend den Schritt aus. */
autoVetoTimeouts() {
for (const match of this.db.prepare("SELECT * FROM matches WHERE status = 'veto'").all()) {
const deadline = this.vetoDeadline(match);
if (deadline === null || this.now() < deadline) continue;
try {
this.autoVetoStep(match);
} catch (err) {
this.log(match.id, 'veto', `Automatischer Schritt fehlgeschlagen: ${err.message}`);
}
}
}
startVetoTimer() {
if (!this.config.vetoStepSeconds || this.vetoTimer) return;
this.vetoTimer = setInterval(() => this.autoVetoTimeouts(), 1000);
}
stopVetoTimer() {
clearInterval(this.vetoTimer);
this.vetoTimer = null;
}
vetoAction(viewer, { stepIndex, map, side }) {
this.require(viewer, 'captain');
let finished = false;
@@ -841,6 +893,7 @@ export class MatchService extends EventEmitter {
steps: veto.steps,
maps: veto.maps,
current: match.status === 'veto' ? veto.current : null,
remainingMs: this.vetoRemainingMs(match),
done: veto.done,
history: actions.map((a) => ({
index: a.idx,