Files
Cs2-Mapveto/server/src/config.js
T
mregosar eda15bc585 Mehrere Docker-Hosts: Matches verteilt auf die Hosts starten
CS2_DOCKER_HOSTS nimmt eine Liste von Docker-Hosts. Jedes Match läuft auf
dem Host mit den meisten freien Ports und merkt sich seinen Host, damit
Status, Konfiguration und Stopp dort landen. Ohne die Variable bleibt alles
beim bisherigen einzelnen Host. deploy/docker-host-einrichten.sh richtet
einen Host per SSH ein.
2026-10-29 10:00:19 +01:00

153 lines
6.1 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 parseDockerHosts(raw) {
if (!raw) return null;
let list;
try {
list = JSON.parse(raw);
} catch {
throw new Error('CS2_DOCKER_HOSTS ist kein gültiges JSON');
}
if (!Array.isArray(list) || list.length === 0) throw new Error('CS2_DOCKER_HOSTS braucht mindestens einen Eintrag');
const names = new Set();
return list.map((h) => {
if (!h.name || !h.publicHost) throw new Error('Jeder Docker-Host braucht name und publicHost');
if (names.has(h.name)) throw new Error(`Docker-Host-Name "${h.name}" kommt doppelt vor`);
names.add(h.name);
return {
name: String(h.name),
dockerHost: h.dockerHost ?? null,
publicHost: String(h.publicHost),
rconHost: h.rconHost ?? h.publicHost,
portRangeStart: h.portStart == null ? undefined : Number(h.portStart),
portRangeEnd: h.portEnd == null ? undefined : Number(h.portEnd),
volume: h.volume ?? undefined,
};
});
}
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'),
discordWebhookUrl: str('DISCORD_WEBHOOK_URL'),
// 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),
// Frist pro Veto-Schritt in Sekunden, danach führt das Backend den Schritt automatisch aus. 0 = keine Frist
vetoStepSeconds: int('VETO_STEP_SECONDS', 30),
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),
// Optional: mehrere Docker-Hosts als JSON-Liste, z. B.
// [{"name":"host1","dockerHost":"ssh://cs2@192.168.178.111","publicHost":"192.168.178.111","portStart":27100,"portEnd":27139}]
hosts: parseDockerHosts(str('CS2_DOCKER_HOSTS')),
},
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;
}