Anmeldung mit ADMIN_PASSWORD (12 h, Sitzung nur im Speicher, Sperre nach 5 Fehlversuchen pro Minute). Alle Matches mit Codes, Servern, Spielern, Veto und Verlauf. Aktionen: Veto starten bzw. Schritte für beide Teams, Warmup beenden, nächste Map, Server stoppen/neu starten, abschließen, abbrechen, Spieler entfernen, Codes erneuern, Match löschen. Ohne ADMIN_PASSWORD ist der Admin-Modus komplett aus. Co-Authored-By: Claude Haiku 5.5 <noreply@anthropic.com>
121 lines
4.7 KiB
JavaScript
121 lines
4.7 KiB
JavaScript
import path from 'node:path';
|
|
import { fileURLToPath } from 'node:url';
|
|
import dotenv from 'dotenv';
|
|
|
|
const here = path.dirname(fileURLToPath(import.meta.url));
|
|
export const SERVER_ROOT = path.resolve(here, '..');
|
|
export const REPO_ROOT = path.resolve(SERVER_ROOT, '..');
|
|
|
|
dotenv.config({ path: path.join(REPO_ROOT, '.env') });
|
|
|
|
function int(name, fallback) {
|
|
const raw = process.env[name];
|
|
if (raw === undefined || raw === '') return fallback;
|
|
const value = Number.parseInt(raw, 10);
|
|
if (Number.isNaN(value)) throw new Error(`Umgebungsvariable ${name} muss eine Zahl sein (ist: "${raw}")`);
|
|
return value;
|
|
}
|
|
|
|
function bool(name, fallback) {
|
|
const raw = process.env[name];
|
|
if (raw === undefined || raw === '') return fallback;
|
|
return ['1', 'true', 'yes', 'ja', 'on'].includes(raw.toLowerCase());
|
|
}
|
|
|
|
function str(name, fallback = '') {
|
|
const raw = process.env[name];
|
|
return raw === undefined || raw === '' ? fallback : raw;
|
|
}
|
|
|
|
export function loadConfig(overrides = {}) {
|
|
const config = {
|
|
port: int('PORT', 3000),
|
|
publicUrl: str('PUBLIC_URL', 'http://localhost:5173').replace(/\/$/, ''),
|
|
// Adresse, unter der der CS2-Server dieses Backend für Live-Logs erreicht (Standard: PUBLIC_URL)
|
|
logReceiverUrl: str('LOG_RECEIVER_URL', str('PUBLIC_URL', 'http://localhost:3000')).replace(/\/$/, ''),
|
|
logDebug: bool('LOG_DEBUG', false),
|
|
// Sekunden nach Map-Ende bis zur nächsten Map bzw. zum Abschluss der Serie; -1 = nur manuell
|
|
autoAdvanceSeconds: int('SERIES_AUTO_ADVANCE_SECONDS', 30),
|
|
databaseFile: path.resolve(REPO_ROOT, str('DATABASE_FILE', 'data/cs2-mapveto.db')),
|
|
mapPoolFile: path.resolve(REPO_ROOT, str('MAP_POOL_FILE', 'server/config/map-pools.json')),
|
|
createMatchPassword: str('CREATE_MATCH_PASSWORD'),
|
|
// Optionaler Schlüssel für die Stream-Übersicht aller Matches (/stream?key=…); leer = offen
|
|
streamKey: str('STREAM_KEY'),
|
|
// Passwort für den Admin-Modus (/admin). Leer = Admin-Modus ist aus.
|
|
adminPassword: str('ADMIN_PASSWORD'),
|
|
requireFullTeams: bool('REQUIRE_FULL_TEAMS', false),
|
|
trustProxy: bool('TRUST_PROXY', false),
|
|
|
|
serverAdapter: str('SERVER_ADAPTER', 'demo'),
|
|
idleTimeoutMinutes: int('SERVER_IDLE_TIMEOUT_MINUTES', 30),
|
|
maxLifetimeHours: int('SERVER_MAX_LIFETIME_HOURS', 8),
|
|
statusPollSeconds: int('SERVER_STATUS_POLL_SECONDS', 5),
|
|
reaperIntervalSeconds: int('SERVER_REAPER_INTERVAL_SECONDS', 60),
|
|
|
|
match: {
|
|
tvDelaySeconds: int('CS2_TV_DELAY', 90),
|
|
tvEnabled: bool('CS2_TV_ENABLE', true),
|
|
maxRoundsCompetitive: int('CS2_MAXROUNDS_COMPETITIVE', 24),
|
|
maxRoundsWingman: int('CS2_MAXROUNDS_WINGMAN', 16),
|
|
overtime: bool('CS2_OVERTIME', true),
|
|
extraCommands: str('CS2_EXTRA_COMMANDS')
|
|
.split(';')
|
|
.map((c) => c.trim())
|
|
.filter(Boolean),
|
|
},
|
|
|
|
demo: {
|
|
startSeconds: int('DEMO_START_SECONDS', 20),
|
|
failFirstConfigure: bool('DEMO_FAIL_FIRST_CONFIGURE', false),
|
|
// Sekunden pro simulierter Runde nach "Aufwärmphase beenden"
|
|
roundSeconds: int('DEMO_ROUND_SECONDS', 4),
|
|
},
|
|
|
|
docker: {
|
|
bin: str('DOCKER_BIN', 'docker'),
|
|
image: str('CS2_DOCKER_IMAGE', 'joedwards32/cs2:latest'),
|
|
host: str('CS2_PUBLIC_HOST'),
|
|
rconHost: str('CS2_RCON_HOST', '127.0.0.1'),
|
|
portRangeStart: int('CS2_PORT_RANGE_START', 27015),
|
|
portRangeEnd: int('CS2_PORT_RANGE_END', 27115),
|
|
gslt: str('CS2_GSLT'),
|
|
volume: str('CS2_DOCKER_VOLUME', 'cs2-mapveto-gamefiles'),
|
|
extraArgs: str('CS2_DOCKER_EXTRA_ARGS'),
|
|
startTimeoutMinutes: int('CS2_START_TIMEOUT_MINUTES', 20),
|
|
},
|
|
|
|
staticPool: {
|
|
// Format: host:port:rconPasswort[:tvPort], mehrere durch Komma getrennt
|
|
servers: str('CS2_STATIC_SERVERS')
|
|
.split(',')
|
|
.map((s) => s.trim())
|
|
.filter(Boolean)
|
|
.map(parseStaticServer),
|
|
},
|
|
};
|
|
return deepMerge(config, overrides);
|
|
}
|
|
|
|
function parseStaticServer(entry) {
|
|
const parts = entry.split(':');
|
|
if (parts.length < 3) {
|
|
throw new Error(`CS2_STATIC_SERVERS-Eintrag "${entry}" hat nicht das Format host:port:rconPasswort[:tvPort]`);
|
|
}
|
|
const [host, port, ...rest] = parts;
|
|
let tvPort = null;
|
|
// Das Passwort darf Doppelpunkte enthalten; ein abschließender rein numerischer Teil ist der TV-Port.
|
|
if (rest.length > 1 && /^\d+$/.test(rest[rest.length - 1])) tvPort = Number(rest.pop());
|
|
return { host, port: Number(port), rconPassword: rest.join(':'), tvPort: tvPort ?? Number(port) + 5 };
|
|
}
|
|
|
|
function deepMerge(target, source) {
|
|
for (const [key, value] of Object.entries(source)) {
|
|
if (value && typeof value === 'object' && !Array.isArray(value) && typeof target[key] === 'object') {
|
|
deepMerge(target[key], value);
|
|
} else {
|
|
target[key] = value;
|
|
}
|
|
}
|
|
return target;
|
|
}
|