Merge feature/messer-match-server in main
# Conflicts: # client/src/styles.css
This commit is contained in:
commit
148cd99856
10 files changed
+220
-20
No files matched your search
@@ -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'];
|
||||
|
||||
|
||||
@@ -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;
|
||||
|
||||
Reference in new issue
Block a user