Merge feature/messer-match-server in main

# Conflicts:
#	client/src/styles.css
This commit is contained in:
mregosar committed 2026-11-01 18:26:42 +01:00
commit 148cd99856
10 files changed
+220 -20

No files matched your search

+29 -5
View File
@@ -13,14 +13,15 @@ const MAX_RESTARTS = 5;
* Konsolenbefehle für eine Messerrunde: nur Messer, kein Geld, eine Runde, danach automatisch neues Spiel.
* `pool` bestimmt den Spielmodus (Competitive 5v5 oder Wingman 2v2), `warmupSeconds` die Zeit zum Verbinden.
*/
export function buildKnifeCommands({ map, pool = { gameType: 0, gameMode: 1 }, warmupSeconds = 90 }) {
export function buildKnifeCommands({ map, pool = { gameType: 0, gameMode: 1 }, warmupSeconds = 90, password = '', endlessWarmup = false }) {
return {
map,
beforeMapChange: [
`game_type ${Number(pool.gameType)}`,
`game_mode ${Number(pool.gameMode)}`,
'sv_password ""',
'tv_enable 0',
`sv_password "${password}"`,
// Auf dem Match-Server (mit Passwort) bleibt CSTV, wie es ist: das Match braucht es danach
...(password ? [] : ['tv_enable 0']),
`changelevel ${map}`,
],
afterMapChange: [
@@ -56,15 +57,38 @@ export function buildKnifeCommands({ map, pool = { gameType: 0, gameMode: 1 }, w
'mp_autoteambalance 0',
'mp_limitteams 0',
'mp_autokick 0',
// Aufwärmphase zum Verbinden, danach beginnt die Messerrunde
// Aufwärmphase zum Verbinden, danach beginnt die Messerrunde. Endlos: bis der Organisator sie startet.
'mp_do_warmup_period 1',
`mp_warmuptime ${Math.max(10, Math.min(600, Number(warmupSeconds) || 90))}`,
'mp_warmup_pausetimer 0',
`mp_warmup_pausetimer ${endlessWarmup ? 1 : 0}`,
'mp_warmup_start',
],
};
}
/**
* Setzt die Messer-Einstellungen auf CS2-Standard zurück, bevor auf demselben Server das Match eingerichtet wird.
* Was die Match-Konfiguration selbst setzt (Startgeld, Kaufzeit, Rundenzeit …), steht dort.
*/
export const KNIFE_UNDO_COMMANDS = [
'mp_ct_default_secondary weapon_hkp2000',
'mp_t_default_secondary weapon_glock',
'mp_ct_default_primary ""',
'mp_t_default_primary ""',
'mp_ct_default_grenades ""',
'mp_t_default_grenades ""',
'mp_give_player_c4 1',
'mp_maxmoney 16000',
'mp_afterroundmoney 0',
'mp_free_armor 0',
'mp_weapons_allow_map_placed 1',
'mp_death_drop_gun 1',
'mp_death_drop_grenade 2',
'mp_match_end_restart 0',
'mp_roundtime_defuse 1.92',
'mp_roundtime_hostage 1.92',
];
/** Befehle, mit denen ein laufender Messer-Server für die nächsten Teams zurückgesetzt wird. */
export const KNIFE_RESET_COMMANDS = ['mp_warmup_start'];
+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;