Compare commits
2
Commits
7e17a33c3f
...
69fcf6d15a
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
69fcf6d15a | ||
|
|
ece21db78a |
No files matched your search
@@ -0,0 +1,81 @@
|
|||||||
|
// Hinweise für den Ingame-Chat (RCON "say"). Absender ist im Spiel "Console".
|
||||||
|
// Reine Textbausteine ohne Server-Zugriff, damit sie sich mit Beispielen testen lassen.
|
||||||
|
|
||||||
|
const MAX_LENGTH = 120;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Text für "say", ohne Zeichen, mit denen sich weitere Konsolenbefehle anhängen ließen
|
||||||
|
* (Anführungszeichen, Semikolon, Zeilenumbrüche). Doppelpunkt und Komma bleiben für Spielstände erlaubt.
|
||||||
|
*/
|
||||||
|
export function chatText(text) {
|
||||||
|
return String(text ?? '')
|
||||||
|
.replace(/["';\\\r\n\t]/g, '')
|
||||||
|
.replace(/[^\p{L}\p{N} .,:_\-+#!?()[\]|/&]/gu, '')
|
||||||
|
.replace(/\s+/g, ' ')
|
||||||
|
.trim()
|
||||||
|
.slice(0, MAX_LENGTH);
|
||||||
|
}
|
||||||
|
|
||||||
|
/** Konsolenbefehle für eine Liste von Hinweisen. Leere Zeilen fallen weg. */
|
||||||
|
export function sayCommands(lines) {
|
||||||
|
return lines.map(chatText).filter(Boolean).map((line) => `say ${line}`);
|
||||||
|
}
|
||||||
|
|
||||||
|
const other = (team) => (team === 'A' ? 'B' : 'A');
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Hinweise nach dem Laden einer Map: welche Map, wer sie gewählt hat, wer welche Seite startet,
|
||||||
|
* Serienstand und dass die Aufwärmphase bis zur Freigabe läuft.
|
||||||
|
* @param {object} p
|
||||||
|
* @param {{A: string, B: string}} p.teams Teamnamen
|
||||||
|
* @param {{number: number, mapName: string, pickedBy: 'A'|'B'|null, sideChosenBy: 'A'|'B'|null, ctTeam: 'A'|'B'|null}} p.map
|
||||||
|
* @param {number} p.total Maps in der Serie
|
||||||
|
* @param {{A: number, B: number}} p.series Serienstand vor dieser Map
|
||||||
|
*/
|
||||||
|
export function mapLoadedLines({ teams, map, total, series }) {
|
||||||
|
const lines = [];
|
||||||
|
const head = total > 1 ? `Map ${map.number} von ${total}: ${map.mapName}` : `Map: ${map.mapName}`;
|
||||||
|
lines.push(map.pickedBy ? `${head} - gewählt von ${teams[map.pickedBy]}` : `${head} - Entscheidungsmap`);
|
||||||
|
if (map.ctTeam) {
|
||||||
|
const t = other(map.ctTeam);
|
||||||
|
const chooser = map.sideChosenBy ? `${teams[map.sideChosenBy]} hat die Seite gewählt: ` : '';
|
||||||
|
lines.push(`${chooser}${teams[map.ctTeam]} startet als CT, ${teams[t]} als T`);
|
||||||
|
}
|
||||||
|
if (total > 1) lines.push(`Serienstand: ${teams.A} ${series.A}:${series.B} ${teams.B}`);
|
||||||
|
lines.push('Aufwärmphase - das Match startet, sobald der Organisator es freigibt');
|
||||||
|
return lines;
|
||||||
|
}
|
||||||
|
|
||||||
|
/** Hinweis beim Start des Matches (Aufwärmphase beendet). */
|
||||||
|
export function matchStartLines({ teams, ctTeam }) {
|
||||||
|
if (!ctTeam) return [`Match live! ${teams.A} gegen ${teams.B} - viel Glück, have fun`];
|
||||||
|
return [`Match live! ${teams[ctTeam]} (CT) gegen ${teams[other(ctTeam)]} (T) - viel Glück, have fun`];
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Hinweise am Ende einer Map: Sieger mit Stand, Serienstand und was als Nächstes passiert.
|
||||||
|
* @param {object} p
|
||||||
|
* @param {{mapName: string, number: number, winner: 'A'|'B'|null, score: {A: number, B: number}}} p.map
|
||||||
|
* @param {{A: number, B: number}} p.series Serienstand nach dieser Map
|
||||||
|
* @param {'A'|'B'|null} p.decided Sieger der Serie, wenn entschieden
|
||||||
|
* @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.total Maps in der Serie
|
||||||
|
*/
|
||||||
|
export function mapEndLines({ teams, map, series, decided, nextMapName, autoAdvanceSeconds, total }) {
|
||||||
|
const lines = [];
|
||||||
|
const score = `${map.score.A}:${map.score.B}`;
|
||||||
|
lines.push(map.winner
|
||||||
|
? `Map ${map.number} (${map.mapName}) geht an ${teams[map.winner]} mit ${score}`
|
||||||
|
: `Map ${map.number} (${map.mapName}) endet ${score}`);
|
||||||
|
if (decided) {
|
||||||
|
const s = decided === 'A' ? `${series.A}:${series.B}` : `${series.B}:${series.A}`;
|
||||||
|
lines.push(total > 1 ? `${teams[decided]} gewinnt die Serie ${s} - danke fürs Spielen!` : 'Danke fürs Spielen!');
|
||||||
|
} else if (nextMapName) {
|
||||||
|
lines.push(`Serienstand: ${teams.A} ${series.A}:${series.B} ${teams.B}`);
|
||||||
|
lines.push(autoAdvanceSeconds >= 0
|
||||||
|
? `Nächste Map: ${nextMapName}, wird in ${autoAdvanceSeconds} s geladen - bleibt auf dem Server`
|
||||||
|
: `Nächste Map: ${nextMapName} - bleibt auf dem Server`);
|
||||||
|
}
|
||||||
|
return lines;
|
||||||
|
}
|
||||||
@@ -1,5 +1,7 @@
|
|||||||
import { buildMapCommands, sanitizeConsoleText } from './cs2Config.js';
|
import { buildMapCommands, sanitizeConsoleText } from './cs2Config.js';
|
||||||
import { buildKnifeCommands, KNIFE_UNDO_COMMANDS } from './knifeServers.js';
|
import { buildKnifeCommands, KNIFE_UNDO_COMMANDS } from './knifeServers.js';
|
||||||
|
import { mapEndLines, mapLoadedLines, matchStartLines, sayCommands } from './chatAnnouncements.js';
|
||||||
|
import { seriesScore } from '../liveScore.js';
|
||||||
import { DemoMatchSimulator } from './demoMatchSimulator.js';
|
import { DemoMatchSimulator } from './demoMatchSimulator.js';
|
||||||
import { backupPrefix, describeBackup, isValidBackupFile, pointsForMap } from '../rounds.js';
|
import { backupPrefix, describeBackup, isValidBackupFile, pointsForMap } from '../rounds.js';
|
||||||
import { HttpError, CLOSED_STATUSES } from '../matchService.js';
|
import { HttpError, CLOSED_STATUSES } from '../matchService.js';
|
||||||
@@ -21,6 +23,9 @@ const SLOW_POLL_MS = 30_000;
|
|||||||
// Messerrunde: so oft wird das Einrichten versucht, bevor der Organisator selbst neu starten muss
|
// Messerrunde: so oft wird das Einrichten versucht, bevor der Organisator selbst neu starten muss
|
||||||
const KNIFE_RETRIES = 3;
|
const KNIFE_RETRIES = 3;
|
||||||
const KNIFE_RETRY_DELAY_MS = 10_000;
|
const KNIFE_RETRY_DELAY_MS = 10_000;
|
||||||
|
// 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_REPEATS = 10;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Steuert den Lebenszyklus der Match-Server. Kennt nur die Adapter-Schnittstelle
|
* Steuert den Lebenszyklus der Match-Server. Kennt nur die Adapter-Schnittstelle
|
||||||
@@ -35,6 +40,8 @@ export class ServerManager {
|
|||||||
// Fehlversuche beim Einrichten der Messerrunde je Match (für die automatische Wiederholung)
|
// Fehlversuche beim Einrichten der Messerrunde je Match (für die automatische Wiederholung)
|
||||||
this.knifeAttempts = new Map();
|
this.knifeAttempts = new Map();
|
||||||
this.knifeRetryDelayMs = KNIFE_RETRY_DELAY_MS;
|
this.knifeRetryDelayMs = KNIFE_RETRY_DELAY_MS;
|
||||||
|
this.warmupRepeatMs = WARMUP_REPEAT_MS;
|
||||||
|
this.warmupTimers = new Map();
|
||||||
this.lastPoll = new Map();
|
this.lastPoll = new Map();
|
||||||
this.timers = [];
|
this.timers = [];
|
||||||
}
|
}
|
||||||
@@ -52,6 +59,7 @@ export class ServerManager {
|
|||||||
*/
|
*/
|
||||||
onMapEnded({ matchId, number, decided, hasNext }) {
|
onMapEnded({ matchId, number, decided, hasNext }) {
|
||||||
const seconds = this.config.autoAdvanceSeconds;
|
const seconds = this.config.autoAdvanceSeconds;
|
||||||
|
this.announceMapEnd(matchId, number, decided, hasNext);
|
||||||
if (seconds < 0) return;
|
if (seconds < 0) return;
|
||||||
clearTimeout(this.autoAdvance.get(matchId));
|
clearTimeout(this.autoAdvance.get(matchId));
|
||||||
const finish = Boolean(decided) || !hasNext;
|
const finish = Boolean(decided) || !hasNext;
|
||||||
@@ -155,6 +163,8 @@ export class ServerManager {
|
|||||||
this.autoAdvance.clear();
|
this.autoAdvance.clear();
|
||||||
this.timers.forEach(clearInterval);
|
this.timers.forEach(clearInterval);
|
||||||
this.timers = [];
|
this.timers = [];
|
||||||
|
for (const t of this.warmupTimers.values()) clearInterval(t);
|
||||||
|
this.warmupTimers.clear();
|
||||||
}
|
}
|
||||||
|
|
||||||
row(matchId) {
|
row(matchId) {
|
||||||
@@ -443,6 +453,76 @@ export class ServerManager {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// ---------- Hinweise im Ingame-Chat ----------
|
||||||
|
|
||||||
|
teamNames(matchId) {
|
||||||
|
const match = this.service.getMatch(matchId);
|
||||||
|
return { A: match.team_a_name, B: match.team_b_name };
|
||||||
|
}
|
||||||
|
|
||||||
|
currentSeriesMap(matchId) {
|
||||||
|
const match = this.service.getMatch(matchId);
|
||||||
|
return this.service.seriesMaps(matchId).find((m) => m.number === match.current_map_number) ?? null;
|
||||||
|
}
|
||||||
|
|
||||||
|
/** Schickt Hinweise per "say" an den eingerichteten Server. Fehler stören den Ablauf nie. */
|
||||||
|
announce(matchId, lines) {
|
||||||
|
const row = this.row(matchId);
|
||||||
|
if (!row?.handle || row.status !== 'configured' || !this.adapter.sendCommands || !lines.length) return;
|
||||||
|
this.adapter.sendCommands(JSON.parse(row.handle), sayCommands(lines))
|
||||||
|
.catch((err) => this.logger.warn?.(`[chat] Hinweis an ${matchId} nicht gesendet: ${err.message}`));
|
||||||
|
}
|
||||||
|
|
||||||
|
/** Nach dem Laden einer Map: Map, Wahl, Seiten, Serienstand. Wiederholt sich, solange die Aufwärmphase läuft. */
|
||||||
|
announceMapLoaded(matchId, number) {
|
||||||
|
const match = this.service.getMatch(matchId);
|
||||||
|
const maps = this.service.seriesMaps(matchId);
|
||||||
|
const map = maps.find((m) => m.number === number);
|
||||||
|
if (!map) return;
|
||||||
|
const lines = mapLoadedLines({
|
||||||
|
teams: this.teamNames(matchId),
|
||||||
|
map: { number, mapName: this.service.mapName(match, map.map), pickedBy: map.picked_by, sideChosenBy: map.side_chosen_by, ctTeam: map.ct_team },
|
||||||
|
total: maps.length,
|
||||||
|
series: seriesScore(maps),
|
||||||
|
});
|
||||||
|
this.announce(matchId, lines);
|
||||||
|
this.stopWarmupRepeat(matchId);
|
||||||
|
let left = WARMUP_REPEATS;
|
||||||
|
const timer = setInterval(() => {
|
||||||
|
const current = this.currentSeriesMap(matchId);
|
||||||
|
const live = current?.live ? JSON.parse(current.live) : null;
|
||||||
|
if (left-- <= 0 || current?.number !== number || live?.warmupEnded || this.row(matchId)?.status !== 'configured') {
|
||||||
|
this.stopWarmupRepeat(matchId);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
this.announce(matchId, lines);
|
||||||
|
}, this.warmupRepeatMs);
|
||||||
|
this.warmupTimers.set(matchId, timer);
|
||||||
|
}
|
||||||
|
|
||||||
|
stopWarmupRepeat(matchId) {
|
||||||
|
clearInterval(this.warmupTimers.get(matchId));
|
||||||
|
this.warmupTimers.delete(matchId);
|
||||||
|
}
|
||||||
|
|
||||||
|
/** Am Map-Ende: Sieger, Serienstand, nächste Map oder Seriensieger. */
|
||||||
|
announceMapEnd(matchId, number, decided, hasNext) {
|
||||||
|
const match = this.service.getMatch(matchId);
|
||||||
|
const maps = this.service.seriesMaps(matchId);
|
||||||
|
const map = maps.find((m) => m.number === number);
|
||||||
|
if (!map) return;
|
||||||
|
const next = hasNext && !decided ? maps.find((m) => m.number === number + 1) : null;
|
||||||
|
this.announce(matchId, mapEndLines({
|
||||||
|
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 } },
|
||||||
|
series: seriesScore(maps),
|
||||||
|
decided,
|
||||||
|
nextMapName: next ? this.service.mapName(match, next.map) : null,
|
||||||
|
autoAdvanceSeconds: this.config.autoAdvanceSeconds,
|
||||||
|
total: maps.length,
|
||||||
|
}));
|
||||||
|
}
|
||||||
|
|
||||||
/** Match in der Lobby mit Messerrunde und hochgefahrenem Server. */
|
/** Match in der Lobby mit Messerrunde und hochgefahrenem Server. */
|
||||||
wantsKnife(match, row) {
|
wantsKnife(match, row) {
|
||||||
return match?.status === 'lobby' && match.veto_start === 'knife' && row?.status === 'ready';
|
return match?.status === 'lobby' && match.veto_start === 'knife' && row?.status === 'ready';
|
||||||
@@ -563,6 +643,7 @@ export class ServerManager {
|
|||||||
this.service.markCurrentMapLive(matchId);
|
this.service.markCurrentMapLive(matchId);
|
||||||
if (this.service.getMatch(matchId).status === 'server_setup') this.service.setStatus(matchId, 'live');
|
if (this.service.getMatch(matchId).status === 'server_setup') this.service.setStatus(matchId, 'live');
|
||||||
this.service.log(matchId, 'server', `Map ${seriesMap.number} (${mapName}) ist eingerichtet`);
|
this.service.log(matchId, 'server', `Map ${seriesMap.number} (${mapName}) ist eingerichtet`);
|
||||||
|
this.announceMapLoaded(matchId, seriesMap.number);
|
||||||
} finally {
|
} finally {
|
||||||
this.busy.delete(matchId);
|
this.busy.delete(matchId);
|
||||||
}
|
}
|
||||||
@@ -622,6 +703,9 @@ export class ServerManager {
|
|||||||
throw new HttpError(502, `Befehl konnte nicht gesendet werden: ${err.message}`, 'server_error');
|
throw new HttpError(502, `Befehl konnte nicht gesendet werden: ${err.message}`, 'server_error');
|
||||||
}
|
}
|
||||||
this.service.log(viewer.matchId, 'server', this.adapter.simulated ? 'Aufwärmphase beendet (simuliert)' : 'Aufwärmphase beendet – Match startet');
|
this.service.log(viewer.matchId, 'server', this.adapter.simulated ? 'Aufwärmphase beendet (simuliert)' : 'Aufwärmphase beendet – Match startet');
|
||||||
|
this.stopWarmupRepeat(viewer.matchId);
|
||||||
|
const current = this.currentSeriesMap(viewer.matchId);
|
||||||
|
this.announce(viewer.matchId, matchStartLines({ teams: this.teamNames(viewer.matchId), ctTeam: current?.ct_team ?? null }));
|
||||||
// CSTV-Zugang für Zuschauer, gleiche Form wie im Zuschauerbereich der Webseite
|
// CSTV-Zugang für Zuschauer, gleiche Form wie im Zuschauerbereich der Webseite
|
||||||
const fields = [];
|
const fields = [];
|
||||||
if (this.config.match.tvEnabled && row.tv_port) {
|
if (this.config.match.tvEnabled && row.tv_port) {
|
||||||
@@ -690,6 +774,7 @@ export class ServerManager {
|
|||||||
|
|
||||||
async stop(matchId, reason) {
|
async stop(matchId, reason) {
|
||||||
this.simulator.stop(matchId);
|
this.simulator.stop(matchId);
|
||||||
|
this.stopWarmupRepeat(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);
|
||||||
|
|||||||
@@ -831,3 +831,39 @@ test('Offene Lobbys werden nach LOBBY_MAX_HOURS samt Server gelöscht, laufende
|
|||||||
await close();
|
await close();
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
|
test('Ingame-Chat: Hinweise nach dem Laden, beim Start und am Map-Ende', async () => {
|
||||||
|
const { app, call, close } = await setup();
|
||||||
|
try {
|
||||||
|
const said = [];
|
||||||
|
const send = app.adapter.sendCommands.bind(app.adapter);
|
||||||
|
app.adapter.sendCommands = async (h, commands) => {
|
||||||
|
said.push(...commands.filter((c) => c.startsWith('say ')).map((c) => c.slice(4)));
|
||||||
|
return send(h, commands);
|
||||||
|
};
|
||||||
|
const { id, org, tokens } = await readyWingmanMatch(call, 'bo3');
|
||||||
|
await call('POST', `/matches/${id}/veto/start`, { token: org });
|
||||||
|
const snap = (await call('GET', `/matches/${id}`, { token: org })).body;
|
||||||
|
const cap = (t) => tokens[snap.teams[t].players.find((p) => p.isCaptain).nickname];
|
||||||
|
await playVeto(call, id, { A: cap('A'), B: cap('B') });
|
||||||
|
await app.manager.poll();
|
||||||
|
await waitFor(() => app.manager.row(id).status === 'configured', { label: 'Map 1 eingerichtet' });
|
||||||
|
await waitFor(() => said.some((l) => l.startsWith('Map 1 von 3')), { label: 'Hinweis nach dem Laden' });
|
||||||
|
assert.ok(said.some((l) => / startet als CT, .* als T$/.test(l)), 'Seiten werden genannt');
|
||||||
|
assert.ok(said.includes('Serienstand: Alpha 0:0 Bravo'));
|
||||||
|
|
||||||
|
assert.equal((await call('POST', `/matches/${id}/server/warmup-end`, { token: org })).status, 200);
|
||||||
|
await waitFor(() => said.some((l) => l.startsWith('Match live!')), { label: 'Hinweis beim Start' });
|
||||||
|
|
||||||
|
const map = app.service.seriesMaps(id)[0].map;
|
||||||
|
const key = app.manager.row(id).config_key;
|
||||||
|
const body = ['Team playing "CT": Alpha', 'Team playing "TERRORIST": Bravo', 'World triggered "Match_Start" on "' + map + '"',
|
||||||
|
'Team "CT" triggered "SFUI_Notice_CTs_Win" (CT "1") (T "0")', 'World triggered "Round_End"',
|
||||||
|
`Game Over: scrimcomp2v2 ${map} score 1:0 after 1 min`].join('\n');
|
||||||
|
await fetch(`http://127.0.0.1:${app.server.address().port}/api/logs/${id}/${key}`, { method: 'POST', body });
|
||||||
|
await waitFor(() => said.some((l) => /^Map 1 \(.+\) geht an Alpha mit 1:0$/.test(l)), { label: 'Hinweis am Map-Ende' });
|
||||||
|
assert.ok(said.some((l) => l.startsWith('Nächste Map: ')));
|
||||||
|
} finally {
|
||||||
|
await close();
|
||||||
|
}
|
||||||
|
});
|
||||||
@@ -0,0 +1,45 @@
|
|||||||
|
import { test } from 'node:test';
|
||||||
|
import assert from 'node:assert/strict';
|
||||||
|
import { chatText, mapEndLines, mapLoadedLines, matchStartLines, sayCommands } from '../src/servers/chatAnnouncements.js';
|
||||||
|
|
||||||
|
const teams = { A: 'Nordlicht', B: 'Hafenkante' };
|
||||||
|
|
||||||
|
test('Hinweise nach dem Laden: Map, Wahl, Seiten, Serienstand, Aufwärmphase', () => {
|
||||||
|
const lines = mapLoadedLines({
|
||||||
|
teams, total: 3, series: { A: 1, B: 0 },
|
||||||
|
map: { number: 2, mapName: 'Inferno', pickedBy: 'B', sideChosenBy: 'A', ctTeam: 'A' },
|
||||||
|
});
|
||||||
|
assert.deepEqual(lines, [
|
||||||
|
'Map 2 von 3: Inferno - gewählt von Hafenkante',
|
||||||
|
'Nordlicht hat die Seite gewählt: Nordlicht startet als CT, Hafenkante als T',
|
||||||
|
'Serienstand: Nordlicht 1:0 Hafenkante',
|
||||||
|
'Aufwärmphase - das Match startet, sobald der Organisator es freigibt',
|
||||||
|
]);
|
||||||
|
const bo1 = mapLoadedLines({ teams, total: 1, series: { A: 0, B: 0 }, map: { number: 1, mapName: 'Nuke', pickedBy: null, sideChosenBy: 'B', ctTeam: 'B' } });
|
||||||
|
assert.equal(bo1[0], 'Map: Nuke - Entscheidungsmap');
|
||||||
|
assert.ok(!bo1.some((l) => l.startsWith('Serienstand')), 'kein Serienstand im BO1');
|
||||||
|
});
|
||||||
|
|
||||||
|
test('Hinweise bei Match-Start und Map-Ende', () => {
|
||||||
|
assert.deepEqual(matchStartLines({ teams, ctTeam: 'B' }), ['Match live! Hafenkante (CT) gegen Nordlicht (T) - viel Glück, have fun']);
|
||||||
|
const next = mapEndLines({
|
||||||
|
teams, total: 3, decided: null, nextMapName: 'Inferno', autoAdvanceSeconds: 30, series: { A: 1, B: 0 },
|
||||||
|
map: { number: 1, mapName: 'Mirage', winner: 'A', score: { A: 13, B: 9 } },
|
||||||
|
});
|
||||||
|
assert.deepEqual(next, [
|
||||||
|
'Map 1 (Mirage) geht an Nordlicht mit 13:9',
|
||||||
|
'Serienstand: Nordlicht 1:0 Hafenkante',
|
||||||
|
'Nächste Map: Inferno, wird in 30 s geladen - bleibt auf dem Server',
|
||||||
|
]);
|
||||||
|
const won = mapEndLines({
|
||||||
|
teams, total: 3, decided: 'B', nextMapName: null, autoAdvanceSeconds: 30, series: { A: 0, B: 2 },
|
||||||
|
map: { number: 2, mapName: 'Nuke', winner: 'B', score: { A: 5, B: 13 } },
|
||||||
|
});
|
||||||
|
assert.equal(won[1], 'Hafenkante gewinnt die Serie 2:0 - danke fürs Spielen!');
|
||||||
|
});
|
||||||
|
|
||||||
|
test('Chattexte können keine weiteren Befehle anhängen', () => {
|
||||||
|
assert.equal(chatText('Team"; rcon_password x; say "'), 'Team rcon_password x say');
|
||||||
|
assert.deepEqual(sayCommands(['Hallo; quit', '', 'Stand 13:9']), ['say Hallo quit', 'say Stand 13:9']);
|
||||||
|
assert.ok(chatText('x'.repeat(300)).length <= 120);
|
||||||
|
});
|
||||||
Reference in new issue
Block a user