Compare commits
2
Commits
123c2f11d7
...
7f7272d09f
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
7f7272d09f | ||
|
|
e8e070dfe6 |
No files matched your search
@@ -33,6 +33,12 @@ export function chatText(text, maxLength = MAX_LENGTH) {
|
|||||||
.slice(0, maxLength);
|
.slice(0, maxLength);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/** Dauer für den Chat: unter zwei Minuten in Sekunden, sonst gerundet in Minuten. */
|
||||||
|
function duration(seconds) {
|
||||||
|
const s = Math.max(0, Math.round(seconds));
|
||||||
|
return s < 120 ? `${s} s` : `etwa ${Math.round(s / 60)} Minuten`;
|
||||||
|
}
|
||||||
|
|
||||||
/** Gefärbter Teil einer Zeile; danach geht es in Weiß weiter. */
|
/** Gefärbter Teil einer Zeile; danach geht es in Weiß weiter. */
|
||||||
const paint = (color, text) => `${color}${text}${COLOR.white}`;
|
const paint = (color, text) => `${color}${text}${COLOR.white}`;
|
||||||
|
|
||||||
@@ -96,8 +102,11 @@ export function matchStartLines({ teams, ctTeam }) {
|
|||||||
* @param {string|null} p.nextMapName nächste Map, wenn es eine gibt
|
* @param {string|null} p.nextMapName nächste Map, wenn es eine gibt
|
||||||
* @param {number} p.autoAdvanceSeconds Wartezeit bis zum automatischen Laden (< 0 = aus)
|
* @param {number} p.autoAdvanceSeconds Wartezeit bis zum automatischen Laden (< 0 = aus)
|
||||||
* @param {number} p.total Maps in der Serie
|
* @param {number} p.total Maps in der Serie
|
||||||
|
* @param {number} [p.tvDelaySeconds] CSTV-Verzögerung (0 = keine). CS2 wechselt die Map erst, wenn CSTV durch ist
|
||||||
|
* (tv_delaymapchange), und am Serienende läuft der Server so lange weiter.
|
||||||
|
* @param {number} [p.shutdownSeconds] am Serienende: so lange läuft der Server noch (null = unbekannt)
|
||||||
*/
|
*/
|
||||||
export function mapEndLines({ teams, map, series, decided, nextMapName, autoAdvanceSeconds, total }) {
|
export function mapEndLines({ teams, map, series, decided, nextMapName, autoAdvanceSeconds, total, tvDelaySeconds = 0, shutdownSeconds = null }) {
|
||||||
const lines = [];
|
const lines = [];
|
||||||
const score = `${map.score.A}:${map.score.B}`;
|
const score = `${map.score.A}:${map.score.B}`;
|
||||||
const mapName = chatText(map.mapName, 32);
|
const mapName = chatText(map.mapName, 32);
|
||||||
@@ -109,11 +118,19 @@ export function mapEndLines({ teams, map, series, decided, nextMapName, autoAdva
|
|||||||
lines.push(total > 1
|
lines.push(total > 1
|
||||||
? `${paint(COLOR.green, `${name(teams, decided)} gewinnt die Serie ${s}`)} - danke fürs Spielen!`
|
? `${paint(COLOR.green, `${name(teams, decided)} gewinnt die Serie ${s}`)} - danke fürs Spielen!`
|
||||||
: 'Danke fürs Spielen!');
|
: 'Danke fürs Spielen!');
|
||||||
|
if (tvDelaySeconds > 0) {
|
||||||
|
lines.push(shutdownSeconds
|
||||||
|
? `Der Server bleibt noch ${duration(shutdownSeconds)} für die CSTV-Übertragung online und schaltet sich dann ab.`
|
||||||
|
: 'Der Server bleibt für die CSTV-Übertragung noch online und schaltet sich dann ab.');
|
||||||
|
}
|
||||||
} else if (nextMapName) {
|
} else if (nextMapName) {
|
||||||
const next = chatText(nextMapName, 32);
|
const next = chatText(nextMapName, 32);
|
||||||
lines.push(`Serienstand: ${name(teams, 'A')} ${series.A}:${series.B} ${name(teams, 'B')}`);
|
lines.push(`Serienstand: ${name(teams, 'A')} ${series.A}:${series.B} ${name(teams, 'B')}`);
|
||||||
|
// Die Map wechselt frühestens nach der Wartezeit der App und erst, wenn CSTV das Ende übertragen hat
|
||||||
|
const wait = Math.max(autoAdvanceSeconds, tvDelaySeconds > 0 ? tvDelaySeconds : 0);
|
||||||
|
const viaTv = tvDelaySeconds > autoAdvanceSeconds ? ' (nach der CSTV-Übertragung)' : '';
|
||||||
lines.push(autoAdvanceSeconds >= 0
|
lines.push(autoAdvanceSeconds >= 0
|
||||||
? `Nächste Map: ${next}, wird in ${autoAdvanceSeconds} s geladen - bleibt auf dem Server`
|
? `Nächste Map: ${next}, wird in ${viaTv && wait < 120 ? 'etwa ' : ''}${duration(wait)} geladen${viaTv} - bleibt auf dem Server`
|
||||||
: `Nächste Map: ${next} - bleibt auf dem Server`);
|
: `Nächste Map: ${next} - bleibt auf dem Server`);
|
||||||
}
|
}
|
||||||
return lines;
|
return lines;
|
||||||
|
|||||||
@@ -63,6 +63,8 @@ export function buildMapCommands({ match, pool, seriesMap, secrets, settings, lo
|
|||||||
'mp_endmatch_votenextmap 0',
|
'mp_endmatch_votenextmap 0',
|
||||||
// Strenge Prüfung der Delta-Ticks (Standard 2) trennt Spieler und CSTV ohne echten Grund
|
// Strenge Prüfung der Delta-Ticks (Standard 2) trennt Spieler und CSTV ohne echten Grund
|
||||||
'sv_deltaticks_enforce 0',
|
'sv_deltaticks_enforce 0',
|
||||||
|
// Nach dem Laden noch einmal: die Spielmodus-Datei der Map setzt tv_delay beim Laden zurück
|
||||||
|
`tv_delay ${Number(settings.tvDelaySeconds)}`,
|
||||||
'mp_autoteambalance 0',
|
'mp_autoteambalance 0',
|
||||||
'mp_limitteams 0',
|
'mp_limitteams 0',
|
||||||
// Aufwärmphase läuft, bis der Organisator das Match startet (mp_warmup_end)
|
// Aufwärmphase läuft, bis der Organisator das Match startet (mp_warmup_end)
|
||||||
|
|||||||
@@ -248,11 +248,11 @@ export class DockerServerAdapter {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
async launch({ matchId, mode, secrets, teamNames, serverName }, blocked = new Set()) {
|
async launch({ matchId, mode, secrets, teamNames, serverName, tvDelaySeconds }, blocked = new Set()) {
|
||||||
const { port, tvPort } = this.allocatePorts(blocked);
|
const { port, tvPort } = this.allocatePorts(blocked);
|
||||||
const lan = (this.options.lanPorts ?? []).includes(port);
|
const lan = (this.options.lanPorts ?? []).includes(port);
|
||||||
if (lan && blocked.size < 10 && !(await this.hostPortsFree([port, tvPort]))) {
|
if (lan && blocked.size < 10 && !(await this.hostPortsFree([port, tvPort]))) {
|
||||||
return this.launch({ matchId, mode, secrets, teamNames, serverName }, new Set([...blocked, port, tvPort]));
|
return this.launch({ matchId, mode, secrets, teamNames, serverName, tvDelaySeconds }, new Set([...blocked, port, tvPort]));
|
||||||
}
|
}
|
||||||
const container = `cs2mv-${matchId.replace(/[^a-zA-Z0-9_-]/g, '')}`.toLowerCase();
|
const container = `cs2mv-${matchId.replace(/[^a-zA-Z0-9_-]/g, '')}`.toLowerCase();
|
||||||
const env = {
|
const env = {
|
||||||
@@ -270,6 +270,9 @@ export class DockerServerAdapter {
|
|||||||
// sv_deltaticks_enforce 0: sonst trennt CS2 Spieler und CSTV mit NETWORK_DISCONNECT_BADDELTATICK
|
// sv_deltaticks_enforce 0: sonst trennt CS2 Spieler und CSTV mit NETWORK_DISCONNECT_BADDELTATICK
|
||||||
CS2_ADDITIONAL_ARGS: '-allow_round_backup_files +sv_deltaticks_enforce 0',
|
CS2_ADDITIONAL_ARGS: '-allow_round_backup_files +sv_deltaticks_enforce 0',
|
||||||
TV_ENABLE: '1',
|
TV_ENABLE: '1',
|
||||||
|
// Das Image schreibt TV_DELAY beim Start in alle gamemode_*.cfg (Standard 0), und die laufen bei jedem
|
||||||
|
// Laden einer Map. Ohne den Wert hier stünde CSTV nach jedem Mapwechsel wieder ohne Verzögerung da.
|
||||||
|
TV_DELAY: String(Math.max(0, Number(tvDelaySeconds) || 0)),
|
||||||
TV_PORT: String(tvPort),
|
TV_PORT: String(tvPort),
|
||||||
TV_PW: secrets.tvPassword,
|
TV_PW: secrets.tvPassword,
|
||||||
TV_RELAY_PW: secrets.tvPassword,
|
TV_RELAY_PW: secrets.tvPassword,
|
||||||
@@ -301,7 +304,7 @@ export class DockerServerAdapter {
|
|||||||
// Port von einem fremden Dienst belegt (die App kennt nur ihre eigenen Server): mit anderen Ports erneut versuchen
|
// Port von einem fremden Dienst belegt (die App kennt nur ihre eigenen Server): mit anderen Ports erneut versuchen
|
||||||
if (!PORT_BUSY.test(`${err.message} ${err.stderr ?? ''}`) || blocked.size >= 10) throw err;
|
if (!PORT_BUSY.test(`${err.message} ${err.stderr ?? ''}`) || blocked.size >= 10) throw err;
|
||||||
await this.docker(['rm', '-f', container]).catch(() => {});
|
await this.docker(['rm', '-f', container]).catch(() => {});
|
||||||
return this.launch({ matchId, mode, secrets, teamNames, serverName }, new Set([...blocked, port, tvPort]));
|
return this.launch({ matchId, mode, secrets, teamNames, serverName, tvDelaySeconds }, new Set([...blocked, port, tvPort]));
|
||||||
}
|
}
|
||||||
|
|
||||||
return {
|
return {
|
||||||
|
|||||||
@@ -209,6 +209,8 @@ export class KnifeServers {
|
|||||||
mode: row.mode,
|
mode: row.mode,
|
||||||
secrets: { rconPassword: row.rcon_password, serverPassword: '', tvPassword: randomPassword(12) },
|
secrets: { rconPassword: row.rcon_password, serverPassword: '', tvPassword: randomPassword(12) },
|
||||||
serverName: `${prefix ? `${prefix} | ` : ''}${row.name} ${size ? `${size}v${size} ` : ''}(Seitenwahl)`,
|
serverName: `${prefix ? `${prefix} | ` : ''}${row.name} ${size ? `${size}v${size} ` : ''}(Seitenwahl)`,
|
||||||
|
// Gleicher Wert wie bei den Match-Servern: das Image schreibt ihn in die gemeinsamen Spielmodus-Dateien
|
||||||
|
tvDelaySeconds: this.config.match?.tvDelaySeconds,
|
||||||
});
|
});
|
||||||
// Während des Starts entfernt: Container gleich wieder abbauen
|
// Während des Starts entfernt: Container gleich wieder abbauen
|
||||||
if (!this.row(id)) {
|
if (!this.row(id)) {
|
||||||
|
|||||||
@@ -26,6 +26,8 @@ const KNIFE_RETRY_DELAY_MS = 10_000;
|
|||||||
// Hinweise im Ingame-Chat während der Aufwärmphase wiederholen (für Spieler, die später verbinden)
|
// Hinweise im Ingame-Chat während der Aufwärmphase wiederholen (für Spieler, die später verbinden)
|
||||||
const WARMUP_REPEAT_MS = 60_000;
|
const WARMUP_REPEAT_MS = 60_000;
|
||||||
const WARMUP_REPEATS = 10;
|
const WARMUP_REPEATS = 10;
|
||||||
|
// Nach der CSTV-Verzögerung noch so lange laufen lassen, damit auch der Endstand übertragen wird
|
||||||
|
const CSTV_GRACE_MS = 15_000;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Steuert den Lebenszyklus der Match-Server. Kennt nur die Adapter-Schnittstelle
|
* Steuert den Lebenszyklus der Match-Server. Kennt nur die Adapter-Schnittstelle
|
||||||
@@ -42,6 +44,9 @@ export class ServerManager {
|
|||||||
this.knifeRetryDelayMs = KNIFE_RETRY_DELAY_MS;
|
this.knifeRetryDelayMs = KNIFE_RETRY_DELAY_MS;
|
||||||
this.warmupRepeatMs = WARMUP_REPEAT_MS;
|
this.warmupRepeatMs = WARMUP_REPEAT_MS;
|
||||||
this.warmupTimers = new Map();
|
this.warmupTimers = new Map();
|
||||||
|
// Nach Match-Ende: Server laufen bis zum Ende der CSTV-Übertragung weiter (matchId -> Timer)
|
||||||
|
this.closeTimers = new Map();
|
||||||
|
this.cstvGraceMs = CSTV_GRACE_MS;
|
||||||
this.lastPoll = new Map();
|
this.lastPoll = new Map();
|
||||||
this.timers = [];
|
this.timers = [];
|
||||||
}
|
}
|
||||||
@@ -49,7 +54,7 @@ export class ServerManager {
|
|||||||
init() {
|
init() {
|
||||||
this.service.on('created', (id) => this.provision(id).catch((err) => this.logger.error(err)));
|
this.service.on('created', (id) => this.provision(id).catch((err) => this.logger.error(err)));
|
||||||
this.service.on('vetoFinished', (id) => this.maybeConfigure(id));
|
this.service.on('vetoFinished', (id) => this.maybeConfigure(id));
|
||||||
this.service.on('closed', (id) => this.stop(id, 'Match beendet').catch((err) => this.logger.error(err)));
|
this.service.on('closed', (id) => this.stopAfterClose(id));
|
||||||
this.service.on('mapEnded', (e) => this.onMapEnded(e));
|
this.service.on('mapEnded', (e) => this.onMapEnded(e));
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -165,6 +170,8 @@ export class ServerManager {
|
|||||||
this.timers = [];
|
this.timers = [];
|
||||||
for (const t of this.warmupTimers.values()) clearInterval(t);
|
for (const t of this.warmupTimers.values()) clearInterval(t);
|
||||||
this.warmupTimers.clear();
|
this.warmupTimers.clear();
|
||||||
|
for (const t of this.closeTimers.values()) clearTimeout(t);
|
||||||
|
this.closeTimers.clear();
|
||||||
}
|
}
|
||||||
|
|
||||||
row(matchId) {
|
row(matchId) {
|
||||||
@@ -360,6 +367,7 @@ export class ServerManager {
|
|||||||
mode: match.mode,
|
mode: match.mode,
|
||||||
secrets,
|
secrets,
|
||||||
teamNames: { A: match.team_a_name, B: match.team_b_name },
|
teamNames: { A: match.team_a_name, B: match.team_b_name },
|
||||||
|
tvDelaySeconds: this.config.match.tvDelaySeconds,
|
||||||
});
|
});
|
||||||
// Während des (oft langen) Starts gestoppt, neu angefordert oder gelöscht: den neuen Container
|
// Während des (oft langen) Starts gestoppt, neu angefordert oder gelöscht: den neuen Container
|
||||||
// gleich wieder abbauen, sonst liefe er ohne Überwachung weiter
|
// gleich wieder abbauen, sonst liefe er ohne Überwachung weiter
|
||||||
@@ -512,6 +520,7 @@ export class ServerManager {
|
|||||||
const map = maps.find((m) => m.number === number);
|
const map = maps.find((m) => m.number === number);
|
||||||
if (!map) return;
|
if (!map) return;
|
||||||
const next = hasNext && !decided ? maps.find((m) => m.number === number + 1) : null;
|
const next = hasNext && !decided ? maps.find((m) => m.number === number + 1) : null;
|
||||||
|
const tvDelay = this.config.match.tvEnabled ? Number(this.config.match.tvDelaySeconds) || 0 : 0;
|
||||||
this.announce(matchId, mapEndLines({
|
this.announce(matchId, mapEndLines({
|
||||||
teams: this.teamNames(matchId),
|
teams: this.teamNames(matchId),
|
||||||
map: { number, mapName: this.service.mapName(match, map.map), winner: map.winner, score: { A: map.score_a, B: map.score_b } },
|
map: { number, mapName: this.service.mapName(match, map.map), winner: map.winner, score: { A: map.score_a, B: map.score_b } },
|
||||||
@@ -520,6 +529,11 @@ export class ServerManager {
|
|||||||
nextMapName: next ? this.service.mapName(match, next.map) : null,
|
nextMapName: next ? this.service.mapName(match, next.map) : null,
|
||||||
autoAdvanceSeconds: this.config.autoAdvanceSeconds,
|
autoAdvanceSeconds: this.config.autoAdvanceSeconds,
|
||||||
total: maps.length,
|
total: maps.length,
|
||||||
|
tvDelaySeconds: tvDelay,
|
||||||
|
// Serienende: Match schließt nach der Wartezeit, der Server läuft dann noch CSTV-Verzögerung + Puffer
|
||||||
|
shutdownSeconds: decided && this.config.autoAdvanceSeconds >= 0 && tvDelay > 0
|
||||||
|
? this.config.autoAdvanceSeconds + tvDelay + this.cstvGraceMs / 1000
|
||||||
|
: null,
|
||||||
}));
|
}));
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -772,9 +786,32 @@ export class ServerManager {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Match beendet: Lief es bis zum Ende und ist CSTV mit Verzögerung an, bleibt der Server so lange stehen,
|
||||||
|
* bis die Zuschauer das Ende gesehen haben. CSTV läuft im selben Container und endet mit ihm.
|
||||||
|
*/
|
||||||
|
stopAfterClose(matchId) {
|
||||||
|
const match = this.service.getMatch(matchId);
|
||||||
|
const row = this.row(matchId);
|
||||||
|
const delay = this.config.match.tvEnabled ? Number(this.config.match.tvDelaySeconds) || 0 : 0;
|
||||||
|
if (match.status !== 'finished' || delay <= 0 || row?.status !== 'configured') {
|
||||||
|
this.stop(matchId, 'Match beendet').catch((err) => this.logger.error(err));
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
const waitMs = delay * 1000 + this.cstvGraceMs;
|
||||||
|
this.service.log(matchId, 'server', `Server läuft noch ${Math.round(waitMs / 1000)} s weiter, damit CSTV das Ende überträgt`);
|
||||||
|
clearTimeout(this.closeTimers.get(matchId));
|
||||||
|
this.closeTimers.set(matchId, setTimeout(() => {
|
||||||
|
this.closeTimers.delete(matchId);
|
||||||
|
this.stop(matchId, 'Match beendet').catch((err) => this.logger.error(err));
|
||||||
|
}, waitMs));
|
||||||
|
}
|
||||||
|
|
||||||
async stop(matchId, reason) {
|
async stop(matchId, reason) {
|
||||||
this.simulator.stop(matchId);
|
this.simulator.stop(matchId);
|
||||||
this.stopWarmupRepeat(matchId);
|
this.stopWarmupRepeat(matchId);
|
||||||
|
clearTimeout(this.closeTimers.get(matchId));
|
||||||
|
this.closeTimers.delete(matchId);
|
||||||
const row = this.row(matchId);
|
const row = this.row(matchId);
|
||||||
if (!row || row.status === 'stopped') return;
|
if (!row || row.status === 'stopped') return;
|
||||||
if (row.handle) await this.safeStop(row);
|
if (row.handle) await this.safeStop(row);
|
||||||
@@ -806,7 +843,7 @@ export class ServerManager {
|
|||||||
if (this.busy.has(row.match_id)) continue;
|
if (this.busy.has(row.match_id)) continue;
|
||||||
const match = this.service.getMatch(row.match_id);
|
const match = this.service.getMatch(row.match_id);
|
||||||
if (CLOSED_STATUSES.includes(match.status)) {
|
if (CLOSED_STATUSES.includes(match.status)) {
|
||||||
await this.stop(row.match_id, 'Match beendet');
|
if (!this.closeTimers.has(row.match_id)) await this.stop(row.match_id, 'Match beendet');
|
||||||
continue;
|
continue;
|
||||||
}
|
}
|
||||||
if (row.started_at && now - row.started_at > this.config.maxLifetimeHours * 3_600_000) {
|
if (row.started_at && now - row.started_at > this.config.maxLifetimeHours * 3_600_000) {
|
||||||
|
|||||||
@@ -177,7 +177,11 @@ test('kompletter Ablauf: Lobby, Berechtigungen, Veto, Server-Konfiguration', asy
|
|||||||
return s.status === 'live' && s.currentMapNumber === 2;
|
return s.status === 'live' && s.currentMapNumber === 2;
|
||||||
}, { label: 'Map 2' });
|
}, { label: 'Map 2' });
|
||||||
|
|
||||||
|
// Abschließen von Hand: der Server wartet auf das Ende der CSTV-Übertragung (hier kurz gehalten)
|
||||||
|
app.manager.config.match.tvDelaySeconds = 1;
|
||||||
|
app.manager.cstvGraceMs = 200;
|
||||||
assert.equal((await call('POST', `/matches/${id}/finish`, { token: org })).status, 200);
|
assert.equal((await call('POST', `/matches/${id}/finish`, { token: org })).status, 200);
|
||||||
|
assert.notEqual(app.service.serverRow(id).status, 'stopped', 'läuft für CSTV noch');
|
||||||
await waitFor(() => app.service.serverRow(id).status === 'stopped', { label: 'Server gestoppt' });
|
await waitFor(() => app.service.serverRow(id).status === 'stopped', { label: 'Server gestoppt' });
|
||||||
const list = (await call('GET', '/matches')).body.matches;
|
const list = (await call('GET', '/matches')).body.matches;
|
||||||
assert.equal(list.find((m) => m.id === id).group, 'closed');
|
assert.equal(list.find((m) => m.id === id).group, 'closed');
|
||||||
@@ -339,9 +343,15 @@ test('Nach Map-Ende lädt die nächste Map automatisch, entschiedene Serie wird
|
|||||||
await waitFor(() => app.service.getMatch(id).current_map_number === 2 && app.service.serverRow(id).status === 'configured', { label: 'Map 2 automatisch' });
|
await waitFor(() => app.service.getMatch(id).current_map_number === 2 && app.service.serverRow(id).status === 'configured', { label: 'Map 2 automatisch' });
|
||||||
assert.equal(app.service.getMatch(id).status, 'live');
|
assert.equal(app.service.getMatch(id).status, 'live');
|
||||||
|
|
||||||
|
// CSTV-Verzögerung kurz halten: der Server soll erst danach gestoppt werden
|
||||||
|
app.manager.config.match.tvDelaySeconds = 1;
|
||||||
|
app.manager.cstvGraceMs = 300;
|
||||||
await winMapForAlpha(app.service.seriesMaps(id)[1].map);
|
await winMapForAlpha(app.service.seriesMaps(id)[1].map);
|
||||||
// 2:0 im Best of 3 -> Match wird automatisch abgeschlossen, Map 3 nicht gespielt
|
// 2:0 im Best of 3 -> Match wird automatisch abgeschlossen, Map 3 nicht gespielt
|
||||||
await waitFor(() => app.service.getMatch(id).status === 'finished', { label: 'Serie abgeschlossen' });
|
await waitFor(() => app.service.getMatch(id).status === 'finished', { label: 'Serie abgeschlossen' });
|
||||||
|
assert.equal(app.service.serverRow(id).status, 'configured', 'Server läuft für CSTV noch weiter');
|
||||||
|
await app.manager.reap();
|
||||||
|
assert.equal(app.service.serverRow(id).status, 'configured', 'auch der Aufräum-Lauf wartet auf CSTV');
|
||||||
await waitFor(() => app.service.serverRow(id).status === 'stopped', { label: 'Server freigegeben' });
|
await waitFor(() => app.service.serverRow(id).status === 'stopped', { label: 'Server freigegeben' });
|
||||||
const snap = (await call('GET', `/matches/${id}`, { token: org })).body;
|
const snap = (await call('GET', `/matches/${id}`, { token: org })).body;
|
||||||
assert.deepEqual(snap.seriesScore, { A: 2, B: 0 });
|
assert.deepEqual(snap.seriesScore, { A: 2, B: 0 });
|
||||||
|
|||||||
@@ -62,3 +62,15 @@ test('Farben: CT blau, T gold, Sieger grün, Aufwärmphase gelb; Eingaben bringe
|
|||||||
const [cmd] = sayCommands([`${COLOR.yellow}gelb ${COLOR.blue}blau"; quit`]);
|
const [cmd] = sayCommands([`${COLOR.yellow}gelb ${COLOR.blue}blau"; quit`]);
|
||||||
assert.equal(cmd, `say ${COLOR.yellow}gelb ${COLOR.blue}blau quit`, 'nur erlaubte Codes, kein Befehl anhängbar, Farben am Anfang bleiben');
|
assert.equal(cmd, `say ${COLOR.yellow}gelb ${COLOR.blue}blau quit`, 'nur erlaubte Codes, kein Befehl anhängbar, Farben am Anfang bleiben');
|
||||||
});
|
});
|
||||||
|
|
||||||
|
test('Mit CSTV-Verzögerung: Mapwechsel nach der Übertragung, Hinweis zum Abschalten am Serienende', () => {
|
||||||
|
const p = (lines) => lines.map((l) => l.replace(/[\x01-\x10]/g, ''));
|
||||||
|
const base = { teams, total: 3, autoAdvanceSeconds: 30, tvDelaySeconds: 90, map: { number: 1, mapName: 'Mirage', winner: 'A', score: { A: 13, B: 9 } } };
|
||||||
|
const next = p(mapEndLines({ ...base, decided: null, nextMapName: 'Inferno', series: { A: 1, B: 0 } }));
|
||||||
|
assert.equal(next.at(-1), 'Nächste Map: Inferno, wird in etwa 90 s geladen (nach der CSTV-Übertragung) - bleibt auf dem Server');
|
||||||
|
const end = p(mapEndLines({ ...base, decided: 'A', nextMapName: null, series: { A: 2, B: 0 }, shutdownSeconds: 135 }));
|
||||||
|
assert.equal(end.at(-1), 'Der Server bleibt noch etwa 2 Minuten für die CSTV-Übertragung online und schaltet sich dann ab.');
|
||||||
|
// Ohne CSTV-Verzögerung kein Hinweis zum Abschalten
|
||||||
|
const noTv = p(mapEndLines({ ...base, tvDelaySeconds: 0, decided: 'A', nextMapName: null, series: { A: 2, B: 0 } }));
|
||||||
|
assert.ok(!noTv.some((l) => l.includes('CSTV')));
|
||||||
|
});
|
||||||
@@ -97,3 +97,13 @@ test('Warteschlange gibt frei, sobald der Server per RCON antwortet – auch ohn
|
|||||||
assert.equal(d.calls.filter((c) => c[0] === 'run').length, 2);
|
assert.equal(d.calls.filter((c) => c[0] === 'run').length, 2);
|
||||||
reachable.add(r2.handle.container);
|
reachable.add(r2.handle.container);
|
||||||
});
|
});
|
||||||
|
|
||||||
|
test('TV_DELAY geht an das Image, sonst setzt es CSTV nach jedem Mapwechsel auf 0 Sekunden', async () => {
|
||||||
|
const d = fakeDocker();
|
||||||
|
const a = new DockerServerAdapter(options, { getUsedPorts: () => new Set(), runDocker: d.run });
|
||||||
|
a.isReachable = async () => false;
|
||||||
|
await a.launch({ matchId: 'tv1', mode: 'competitive', secrets, tvDelaySeconds: 90 });
|
||||||
|
const run = d.calls.find((c) => c.args?.[0] === 'run' || c[0] === 'run');
|
||||||
|
const env = envOf(run.args ?? run);
|
||||||
|
assert.equal(env.TV_DELAY, '90');
|
||||||
|
});
|
||||||
@@ -80,6 +80,7 @@ test('Log-Weiterleitung wird vor dem Mapwechsel gesetzt, unsichere URLs nicht',
|
|||||||
assert.ok(ok.includes('logaddress_add_http "http://192.168.178.31:3000/api/logs/abc_D-1/KEY123"'));
|
assert.ok(ok.includes('logaddress_add_http "http://192.168.178.31:3000/api/logs/abc_D-1/KEY123"'));
|
||||||
assert.ok(buildMapCommands({ ...base, logUrl: 'http://x/api/logs/a/b' }).afterMapChange.includes('mp_endmatch_votenextmap 0'), 'keine Map-Abstimmung nach dem Match');
|
assert.ok(buildMapCommands({ ...base, logUrl: 'http://x/api/logs/a/b' }).afterMapChange.includes('mp_endmatch_votenextmap 0'), 'keine Map-Abstimmung nach dem Match');
|
||||||
assert.ok(buildMapCommands({ ...base, logUrl: 'http://x/api/logs/a/b' }).afterMapChange.includes('sv_deltaticks_enforce 0'), 'keine Trennung wegen Delta-Ticks');
|
assert.ok(buildMapCommands({ ...base, logUrl: 'http://x/api/logs/a/b' }).afterMapChange.includes('sv_deltaticks_enforce 0'), 'keine Trennung wegen Delta-Ticks');
|
||||||
|
assert.ok(buildMapCommands({ ...base, logUrl: 'http://x/api/logs/a/b' }).afterMapChange.some((c) => /^tv_delay \d+$/.test(c)), 'tv_delay nach dem Laden der Map');
|
||||||
assert.ok(ok.indexOf('log on') < ok.indexOf('changelevel de_nuke'));
|
assert.ok(ok.indexOf('log on') < ok.indexOf('changelevel de_nuke'));
|
||||||
const bad = buildMapCommands({ ...base, logUrl: 'http://x"; quit' }).beforeMapChange;
|
const bad = buildMapCommands({ ...base, logUrl: 'http://x"; quit' }).beforeMapChange;
|
||||||
assert.ok(!bad.some((c) => c.startsWith('logaddress')));
|
assert.ok(!bad.some((c) => c.startsWith('logaddress')));
|
||||||
|
|||||||
Reference in new issue
Block a user