Compare commits
4
Commits
69fcf6d15a
...
123c2f11d7
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
123c2f11d7 | ||
|
|
4bce16d084 | ||
|
|
1fddf4d20c | ||
|
|
52c07c88b8 |
No files matched your search
@@ -4,24 +4,57 @@
|
||||
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.
|
||||
* Farbcodes für den CS2-Chat (am Server geprüft). Ein Code färbt alles bis zum nächsten Code.
|
||||
* Sie kommen ausschließlich aus den Textbausteinen hier, nie aus Eingaben.
|
||||
*/
|
||||
export function chatText(text) {
|
||||
export const COLOR = {
|
||||
white: '\x01',
|
||||
red: '\x02',
|
||||
green: '\x04',
|
||||
yellow: '\x09',
|
||||
blue: '\x0B',
|
||||
gold: '\x10',
|
||||
};
|
||||
const ALLOWED_CODES = new Set(Object.values(COLOR));
|
||||
// CS2 färbt wie im Spiel: CT blau, T gold
|
||||
const SIDE_COLOR = { CT: COLOR.blue, T: COLOR.gold };
|
||||
|
||||
/**
|
||||
* Eingaben (Teamnamen, Map-Namen) für den Chat: ohne Anführungszeichen, Semikolon, Zeilenumbrüche und ohne
|
||||
* Steuerzeichen, sodass sich weder Befehle anhängen noch eigene Farben einbauen lassen.
|
||||
* Doppelpunkt und Komma bleiben für Spielstände erlaubt.
|
||||
*/
|
||||
export function chatText(text, maxLength = MAX_LENGTH) {
|
||||
return String(text ?? '')
|
||||
.replace(/["';\\\r\n\t]/g, '')
|
||||
.replace(/[^\p{L}\p{N} .,:_\-+#!?()[\]|/&]/gu, '')
|
||||
.replace(/\s+/g, ' ')
|
||||
.trim()
|
||||
.slice(0, MAX_LENGTH);
|
||||
.slice(0, maxLength);
|
||||
}
|
||||
|
||||
/** Konsolenbefehle für eine Liste von Hinweisen. Leere Zeilen fallen weg. */
|
||||
/** Gefärbter Teil einer Zeile; danach geht es in Weiß weiter. */
|
||||
const paint = (color, text) => `${color}${text}${COLOR.white}`;
|
||||
|
||||
/**
|
||||
* Konsolenbefehle für eine Liste von Hinweisen. Erlaubt sind nur die Farbcodes aus COLOR; alles, was einen
|
||||
* weiteren Befehl einleiten könnte, fällt weg. Das Leerzeichen am Anfang braucht CS2, damit der erste Farbcode wirkt.
|
||||
*/
|
||||
export function sayCommands(lines) {
|
||||
return lines.map(chatText).filter(Boolean).map((line) => `say ${line}`);
|
||||
return lines
|
||||
// Kein trim(): Gelb (\x09) und Blau (\x0B) zählen für JavaScript als Leerraum und gingen sonst verloren
|
||||
.map((line) => [...String(line ?? '')]
|
||||
.filter((ch) => ALLOWED_CODES.has(ch) || !/[\x00-\x1F\x7F"';\\]/.test(ch))
|
||||
.join(''))
|
||||
.filter((line) => line.replace(/[\x00-\x1F]/g, '').trim())
|
||||
.map((line) => `say ${line}`);
|
||||
}
|
||||
|
||||
const other = (team) => (team === 'A' ? 'B' : 'A');
|
||||
const name = (teams, team) => chatText(teams[team], 32) || 'Team';
|
||||
|
||||
/** Teamname in der Farbe seiner Startseite. */
|
||||
const sided = (teams, team, ctTeam) => (ctTeam ? paint(SIDE_COLOR[team === ctTeam ? 'CT' : 'T'], name(teams, team)) : name(teams, team));
|
||||
|
||||
/**
|
||||
* Hinweise nach dem Laden einer Map: welche Map, wer sie gewählt hat, wer welche Seite startet,
|
||||
@@ -34,22 +67,24 @@ const other = (team) => (team === 'A' ? 'B' : 'A');
|
||||
*/
|
||||
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`);
|
||||
const mapName = chatText(map.mapName, 32);
|
||||
const head = total > 1 ? `Map ${map.number} von ${total}: ${mapName}` : `Map: ${mapName}`;
|
||||
lines.push(map.pickedBy ? `${head} - gewählt von ${name(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`);
|
||||
const chooser = map.sideChosenBy ? `${name(teams, map.sideChosenBy)} hat die Seite gewählt: ` : '';
|
||||
lines.push(`${chooser}${sided(teams, map.ctTeam, map.ctTeam)} startet als ${paint(COLOR.blue, 'CT')}, ${sided(teams, t, map.ctTeam)} als ${paint(COLOR.gold, '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');
|
||||
if (total > 1) lines.push(`Serienstand: ${name(teams, 'A')} ${series.A}:${series.B} ${name(teams, 'B')}`);
|
||||
lines.push(paint(COLOR.yellow, '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`];
|
||||
const live = paint(COLOR.green, 'Match live!');
|
||||
if (!ctTeam) return [`${live} ${name(teams, 'A')} gegen ${name(teams, 'B')} - viel Glück, have fun`];
|
||||
return [`${live} ${sided(teams, ctTeam, ctTeam)} (CT) gegen ${sided(teams, other(ctTeam), ctTeam)} (T) - viel Glück, have fun`];
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -65,17 +100,21 @@ export function matchStartLines({ teams, ctTeam }) {
|
||||
export function mapEndLines({ teams, map, series, decided, nextMapName, autoAdvanceSeconds, total }) {
|
||||
const lines = [];
|
||||
const score = `${map.score.A}:${map.score.B}`;
|
||||
const mapName = chatText(map.mapName, 32);
|
||||
lines.push(map.winner
|
||||
? `Map ${map.number} (${map.mapName}) geht an ${teams[map.winner]} mit ${score}`
|
||||
: `Map ${map.number} (${map.mapName}) endet ${score}`);
|
||||
? `Map ${map.number} (${mapName}) geht an ${paint(COLOR.green, name(teams, map.winner))} mit ${score}`
|
||||
: `Map ${map.number} (${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!');
|
||||
lines.push(total > 1
|
||||
? `${paint(COLOR.green, `${name(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}`);
|
||||
const next = chatText(nextMapName, 32);
|
||||
lines.push(`Serienstand: ${name(teams, 'A')} ${series.A}:${series.B} ${name(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`);
|
||||
? `Nächste Map: ${next}, wird in ${autoAdvanceSeconds} s geladen - bleibt auf dem Server`
|
||||
: `Nächste Map: ${next} - bleibt auf dem Server`);
|
||||
}
|
||||
return lines;
|
||||
}
|
||||
@@ -61,6 +61,8 @@ export function buildMapCommands({ match, pool, seriesMap, secrets, settings, lo
|
||||
'mp_match_can_clinch 1',
|
||||
// Keine Map-Abstimmung nach Spielende: die nächste Map legt das Veto fest
|
||||
'mp_endmatch_votenextmap 0',
|
||||
// Strenge Prüfung der Delta-Ticks (Standard 2) trennt Spieler und CSTV ohne echten Grund
|
||||
'sv_deltaticks_enforce 0',
|
||||
'mp_autoteambalance 0',
|
||||
'mp_limitteams 0',
|
||||
// Aufwärmphase läuft, bis der Organisator das Match startet (mp_warmup_end)
|
||||
|
||||
@@ -266,8 +266,9 @@ export class DockerServerAdapter {
|
||||
CS2_GAMETYPE: '0',
|
||||
CS2_GAMEMODE: mode === 'wingman' ? '2' : '1',
|
||||
CS2_SERVER_HIBERNATE: '0',
|
||||
// Ohne diesen Startparameter legt CS2 keine Rundensicherungen an (Rollback)
|
||||
CS2_ADDITIONAL_ARGS: '-allow_round_backup_files',
|
||||
// -allow_round_backup_files: ohne ihn legt CS2 keine Rundensicherungen an (Rollback).
|
||||
// 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',
|
||||
TV_ENABLE: '1',
|
||||
TV_PORT: String(tvPort),
|
||||
TV_PW: secrets.tvPassword,
|
||||
|
||||
@@ -32,6 +32,8 @@ export function buildKnifeCommands({ map, pool = { gameType: 0, gameMode: 1 }, w
|
||||
'mp_match_can_clinch 1',
|
||||
// Keine Map-Abstimmung nach Spielende: die nächste Map legt das Veto fest
|
||||
'mp_endmatch_votenextmap 0',
|
||||
// Strenge Prüfung der Delta-Ticks (Standard 2) trennt Spieler und CSTV ohne echten Grund
|
||||
'sv_deltaticks_enforce 0',
|
||||
'mp_match_end_restart 1',
|
||||
'mp_match_restart_delay 10',
|
||||
'mp_halftime 0',
|
||||
|
||||
@@ -838,7 +838,7 @@ test('Ingame-Chat: Hinweise nach dem Laden, beim Start und am Map-Ende', async (
|
||||
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)));
|
||||
said.push(...commands.filter((c) => c.startsWith('say ')).map((c) => c.slice(4).replace(/[-]/g, '').trim()));
|
||||
return send(h, commands);
|
||||
};
|
||||
const { id, org, tokens } = await readyWingmanMatch(call, 'bo3');
|
||||
|
||||
@@ -1,32 +1,34 @@
|
||||
import { test } from 'node:test';
|
||||
import assert from 'node:assert/strict';
|
||||
import { chatText, mapEndLines, mapLoadedLines, matchStartLines, sayCommands } from '../src/servers/chatAnnouncements.js';
|
||||
import { chatText, COLOR, mapEndLines, mapLoadedLines, matchStartLines, sayCommands } from '../src/servers/chatAnnouncements.js';
|
||||
|
||||
const teams = { A: 'Nordlicht', B: 'Hafenkante' };
|
||||
// Text ohne Farbcodes, zum Vergleichen des Inhalts
|
||||
const plain = (lines) => lines.map((l) => l.replace(/[-]/g, ''));
|
||||
|
||||
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, [
|
||||
assert.deepEqual(plain(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.equal(plain(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']);
|
||||
assert.deepEqual(plain(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, [
|
||||
assert.deepEqual(plain(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',
|
||||
@@ -35,7 +37,7 @@ test('Hinweise bei Match-Start und Map-Ende', () => {
|
||||
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!');
|
||||
assert.equal(plain(won)[1], 'Hafenkante gewinnt die Serie 2:0 - danke fürs Spielen!');
|
||||
});
|
||||
|
||||
test('Chattexte können keine weiteren Befehle anhängen', () => {
|
||||
@@ -43,3 +45,20 @@ test('Chattexte können keine weiteren Befehle anhängen', () => {
|
||||
assert.deepEqual(sayCommands(['Hallo; quit', '', 'Stand 13:9']), ['say Hallo quit', 'say Stand 13:9']);
|
||||
assert.ok(chatText('x'.repeat(300)).length <= 120);
|
||||
});
|
||||
|
||||
test('Farben: CT blau, T gold, Sieger grün, Aufwärmphase gelb; Eingaben bringen keine eigenen Farben mit', () => {
|
||||
const loaded = mapLoadedLines({ teams, total: 1, series: { A: 0, B: 0 }, map: { number: 1, mapName: 'Nuke', pickedBy: null, sideChosenBy: 'B', ctTeam: 'B' } });
|
||||
const sides = loaded.find((l) => l.includes('startet als'));
|
||||
assert.ok(sides.includes(`${COLOR.blue}Hafenkante${COLOR.white}`), 'CT-Team blau');
|
||||
assert.ok(sides.includes(`${COLOR.gold}Nordlicht${COLOR.white}`), 'T-Team gold');
|
||||
assert.ok(loaded.at(-1).startsWith(COLOR.yellow), 'Aufwärmphase gelb');
|
||||
assert.ok(matchStartLines({ teams, ctTeam: 'A' })[0].startsWith(`${COLOR.green}Match live!`));
|
||||
const end = mapEndLines({ teams, total: 1, decided: null, nextMapName: null, autoAdvanceSeconds: 30, series: { A: 1, B: 0 }, map: { number: 1, mapName: 'Nuke', winner: 'A', score: { A: 13, B: 2 } } });
|
||||
assert.ok(end[0].includes(`${COLOR.green}Nordlicht${COLOR.white}`), 'Sieger grün');
|
||||
|
||||
// Farbcodes im Teamnamen werden entfernt, die eigenen Farben bleiben
|
||||
const evil = matchStartLines({ teams: { A: `BöseRot`, B: 'Gut' }, ctTeam: 'A' })[0];
|
||||
assert.ok(evil.includes(`${COLOR.blue}BöseRot${COLOR.white}`));
|
||||
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');
|
||||
});
|
||||
@@ -37,7 +37,7 @@ test('Ohne GSLT startet der Container im LAN-Modus, mit GSLT mit Token', async (
|
||||
assert.equal(env.SRCDS_TOKEN, undefined);
|
||||
assert.equal(env.CS2_GAMEMODE, '2');
|
||||
assert.equal(env.CS2_MAXPLAYERS, '6', '4 Spieler + CSTV + Reserve');
|
||||
assert.equal(env.CS2_ADDITIONAL_ARGS, '-allow_round_backup_files', 'Rundensicherung für Rollback');
|
||||
assert.equal(env.CS2_ADDITIONAL_ARGS, '-allow_round_backup_files +sv_deltaticks_enforce 0', 'Rundensicherung für Rollback, keine Trennung wegen Delta-Ticks');
|
||||
assert.equal(env.CS2_PORT, '27100');
|
||||
assert.equal(env.TV_PORT, '27200');
|
||||
assert.ok(runArgs.includes('cs2-mapveto-gamefiles:/home/steam/cs2-dedicated'));
|
||||
|
||||
@@ -79,6 +79,7 @@ test('Log-Weiterleitung wird vor dem Mapwechsel gesetzt, unsichere URLs nicht',
|
||||
const ok = buildMapCommands({ ...base, logUrl: 'http://192.168.178.31:3000/api/logs/abc_D-1/KEY123' }).beforeMapChange;
|
||||
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('sv_deltaticks_enforce 0'), 'keine Trennung wegen Delta-Ticks');
|
||||
assert.ok(ok.indexOf('log on') < ok.indexOf('changelevel de_nuke'));
|
||||
const bad = buildMapCommands({ ...base, logUrl: 'http://x"; quit' }).beforeMapChange;
|
||||
assert.ok(!bad.some((c) => c.startsWith('logaddress')));
|
||||
|
||||
Reference in new issue
Block a user