Files
Cs2-Mapveto/server/src/config.js
T
SPP UserandClaude Opus 5.5 3a790ce0b1 Ein bereiter Spieler pro Team reicht standardmäßig für den Veto-Start
REQUIRE_FULL_TEAMS ist jetzt standardmäßig false; vollständige Teams
lassen sich weiterhin per Umgebungsvariable erzwingen.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-25 17:11:46 +01:00

110 lines
3.9 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(/\/$/, ''),
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'),
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),
},
docker: {
bin: str('DOCKER_BIN', 'docker'),
image: str('CS2_DOCKER_IMAGE', 'cm2network/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;
}