Messerrunde auf dem eigenen Match-Server, danach derselbe Server fürs Match

Bei Seitenwahl per Messerrunde richtet die App den Match-Server in der Lobby als
Messer-Server ein (Passwort des Matches, Aufwärmphase ohne Ende). Spieler sehen die
Verbindungsdaten in der Lobby, der Organisator startet bzw. wiederholt die Runde.
Nach dem Veto wird derselbe Server fürs Match eingerichtet, die Messer-Einstellungen
werden zurückgenommen.
This commit is contained in:
mregosar committed 2026-11-01 18:25:07 +01:00
1 parent 2c57b8c170
commit 13320313ec
10 files changed
+220 -20

No files matched your search

+67 -1
View File
@@ -1,4 +1,5 @@
import { buildMapCommands, sanitizeConsoleText } from './cs2Config.js';
import { buildKnifeCommands, KNIFE_UNDO_COMMANDS } from './knifeServers.js';
import { DemoMatchSimulator } from './demoMatchSimulator.js';
import { backupPrefix, describeBackup, isValidBackupFile, pointsForMap } from '../rounds.js';
import { HttpError, CLOSED_STATUSES } from '../matchService.js';
@@ -305,7 +306,7 @@ export class ServerManager {
VALUES (@match_id, @adapter, @simulated, 'starting', @message, NULL, NULL, NULL, NULL,
@rcon, @server, @tv, @key, @attempts, @now, @now)
ON CONFLICT(match_id) DO UPDATE SET adapter = excluded.adapter, simulated = excluded.simulated,
status = 'starting', message = excluded.message, handle = NULL, host = NULL, port = NULL, tv_port = NULL,
status = 'starting', message = excluded.message, handle = NULL, knife_at = NULL, knife_phase = NULL, host = NULL, port = NULL, tv_port = NULL,
rcon_password = excluded.rcon_password, server_password = excluded.server_password,
tv_password = excluded.tv_password, config_key = excluded.config_key, attempts = excluded.attempts,
started_at = excluded.started_at, ready_at = NULL, configured_at = NULL, stopped_at = NULL,
@@ -406,12 +407,66 @@ export class ServerManager {
maybeConfigure(matchId) {
const match = this.service.getMatch(matchId);
const row = this.row(matchId);
// Seitenwahl per Messerrunde: derselbe Server dient vor dem Veto als Messer-Server
if (this.wantsKnife(match, row) && !row.knife_at) {
this.configureKnife(matchId).catch((err) => this.logger.error(err));
return;
}
// Auch im laufenden Match: nach einem Neustart des Servers die aktuelle Map samt Passwort wieder laden
if (['server_setup', 'live'].includes(match.status) && row?.status === 'ready') {
this.configure(matchId).catch((err) => this.logger.error(err));
}
}
/** Match in der Lobby mit Messerrunde und hochgefahrenem Server. */
wantsKnife(match, row) {
return match?.status === 'lobby' && match.veto_start === 'knife' && row?.status === 'ready';
}
/** Messerrunde auf dem Match-Server einrichten: Aufwärmphase ohne Ende, Passwort des Matches. */
async configureKnife(matchId) {
if (this.busy.has(matchId)) throw new HttpError(409, 'Für diesen Server läuft bereits eine Aktion', 'busy');
this.busy.add(matchId);
try {
const row = this.row(matchId);
const match = this.service.getMatch(matchId);
const pool = this.service.poolFor(match);
const map = pool.maps.some((m) => m.id === this.config.knife?.map) ? this.config.knife.map : pool.maps[0].id;
this.update(matchId, { message: 'Messerrunde wird eingerichtet' });
try {
await this.adapter.configure(JSON.parse(row.handle), buildKnifeCommands({ map, pool, password: row.server_password, endlessWarmup: true }));
} catch (err) {
this.update(matchId, { knife_at: null, knife_phase: null, message: `Messerrunde konnte nicht eingerichtet werden: ${err.message}` });
this.service.log(matchId, 'server', `Messerrunde nicht eingerichtet: ${err.message}`);
return;
}
this.update(matchId, { knife_at: Date.now(), knife_phase: 'warmup', message: 'Messerrunde: Aufwärmphase, bis der Organisator startet' });
this.service.log(matchId, 'server', 'Server für die Messerrunde bereit (Aufwärmphase)');
} finally {
this.busy.delete(matchId);
}
// Ist das Veto inzwischen durch, gleich das Match einrichten (der Aufruf dafür kam, während wir beschäftigt waren)
if (this.service.getMatch(matchId).status !== 'lobby') this.maybeConfigure(matchId);
}
/** Organisator: Messerrunde starten (Aufwärmphase beenden) oder wiederholen (zurück in die Aufwärmphase). */
async knifeAction(viewer, what) {
this.service.require(viewer, 'organizer');
const match = this.service.getMatch(viewer.matchId);
const row = this.row(viewer.matchId);
if (match.status !== 'lobby' || !row?.knife_at) throw new HttpError(409, 'Der Server ist noch nicht für die Messerrunde bereit', 'wrong_state');
const start = what === 'start';
try {
await this.adapter.sendCommands(JSON.parse(row.handle), start ? ['mp_warmup_pausetimer 0', 'mp_warmup_end'] : ['mp_warmup_pausetimer 1', 'mp_warmup_start']);
} catch (err) {
throw new HttpError(502, `Befehl konnte nicht gesendet werden: ${err.message}`, 'server_error');
}
this.update(viewer.matchId, start
? { knife_phase: 'live', message: 'Messerrunde läuft' }
: { knife_phase: 'warmup', message: 'Messerrunde: Aufwärmphase, bis der Organisator startet' });
this.service.log(viewer.matchId, 'server', start ? 'Messerrunde gestartet' : 'Messerrunde zurück in die Aufwärmphase');
}
async configure(matchId) {
const row = this.row(matchId);
if (!row?.handle) throw new HttpError(409, 'Es gibt noch keinen gestarteten Server', 'no_server');
@@ -444,6 +499,8 @@ export class ServerManager {
});
this.simulator.stop(matchId);
this.service.resetLiveScore(matchId, seriesMap.number);
// Lief vorher die Messerrunde auf diesem Server, deren Einstellungen zurücknehmen
if (row.knife_at) plan.afterMapChange = [...KNIFE_UNDO_COMMANDS, ...plan.afterMapChange];
try {
await this.adapter.configure(JSON.parse(row.handle), { map: seriesMap.map, ...plan });
} catch (err) {
@@ -454,6 +511,8 @@ export class ServerManager {
this.update(matchId, {
status: 'configured',
configured_at: Date.now(),
knife_at: null,
knife_phase: null,
message: this.adapter.simulated
? `Simuliert: ${mapName} wäre jetzt geladen – kein echter Server`
: `${mapName} geladen – Aufwärmphase läuft`,
@@ -482,6 +541,11 @@ export class ServerManager {
await this.provision(matchId);
return;
}
if (this.wantsKnife(match, row)) {
this.service.log(matchId, 'server', 'Organisator richtet die Messerrunde neu ein');
this.configureKnife(matchId).catch((err) => this.logger.error(err));
return;
}
if (row.status === 'config_failed' || (row.status === 'ready' && ['server_setup', 'live'].includes(match.status))) {
this.service.log(matchId, 'server', 'Organisator wiederholt die Konfiguration');
this.configure(matchId).catch((err) => this.logger.error(err));
@@ -609,6 +673,8 @@ export class ServerManager {
const webActive = this.service.presence(row.match_id).size > 0;
// Ein (neu) gestarteter Server zählt als Nutzung, sonst stoppt ein Neustart nach langer Pause sofort wieder
let lastUse = Math.max(match.last_activity_at, row.started_at ?? 0);
// Spieler auf dem Server während der Messerrunde: in Benutzung
if (row.knife_at && row.player_count > 0) continue;
if (row.status === 'configured' && row.player_count !== null) {
// Echter Server mit bekannter Spielerzahl: entscheidend ist, ob jemand auf dem Server ist.
if (row.player_count > 0) continue;