Backend: Match-Lobbys, Map-Veto, Echtzeit und CS2-Serveradapter
Express + Socket.IO + SQLite. Veto-Engine mit konfigurierbaren Map-Pools und validierten Abläufen, rollenbasierte Berechtigungen, Server-Lebenszyklus mit Demo-, Docker- und Static-Pool-Adapter (RCON), automatisches Herunterfahren. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
commit
104aa1a67b
27 files changed
+6355
No files matched your search
@@ -0,0 +1,110 @@
|
||||
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', true),
|
||||
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),
|
||||
matchzy: bool('MATCHZY_ENABLED', false),
|
||||
},
|
||||
|
||||
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;
|
||||
}
|
||||
Reference in new issue
Block a user