Apply selected server maps automatically without config overlays
This commit is contained in:
@@ -14,12 +14,11 @@ import {
|
||||
import {
|
||||
getBaselineForScope,
|
||||
getVersion,
|
||||
listPromotedOverlaysForScope,
|
||||
listScripts,
|
||||
type ScriptRow,
|
||||
type ScriptVersionRow
|
||||
} from './scripts.js';
|
||||
import { composeCfg } from './config-compose.js';
|
||||
import { getPromotedServerConfig, getServerMapConfig, MAP_CONFIG_FILE } from './server-config.js';
|
||||
import { T5_ZOMBIE_MAPS } from './map-aliases.js';
|
||||
import { allServers, getServer, listServers } from './servers.js';
|
||||
|
||||
// Destinations drift scans, in a stable order so the UI shows scripts first.
|
||||
@@ -92,6 +91,12 @@ async function walkFiles(root: string): Promise<string[]> {
|
||||
export async function scanServerDrift(server: string): Promise<DriftEntry[]> {
|
||||
const tracked = new Set(listServerScripts(server).map((r) => r.rel_path));
|
||||
const drift: DriftEntry[] = [];
|
||||
const srv = getServer(server);
|
||||
// Without a promoted baseline, Plutainer owns the seeded config. Keep it
|
||||
// until a baseline is promoted; startup arguments handle map selection.
|
||||
const keepDefaultConfig = srv?.game === 't5'
|
||||
&& T5_ZOMBIE_MAPS.some((entry) => entry.canonical === srv.map)
|
||||
&& !getBaselineForScope('game', srv.game)?.current_version_id;
|
||||
|
||||
// Pass 1 — untracked files on disk in each managed destination.
|
||||
for (const kind of DRIFT_DESTS) {
|
||||
@@ -101,6 +106,9 @@ export async function scanServerDrift(server: string): Promise<DriftEntry[]> {
|
||||
|
||||
for (const relPath of onDisk) {
|
||||
if (tracked.has(relPath)) continue;
|
||||
if (keepDefaultConfig && kind === 'config' && relPath === config.configTargetFile) continue;
|
||||
if (kind === 'config' && relPath === MAP_CONFIG_FILE && srv?.created_by
|
||||
&& srv.game_mode === 'zombie' && getServerMapConfig(srv.game, srv.map)) continue;
|
||||
const abs = join(dest, relPath);
|
||||
try {
|
||||
const st = await stat(abs);
|
||||
@@ -133,6 +141,19 @@ async function scanMissingForServer(server: string): Promise<DriftEntry[]> {
|
||||
if (!srv) return [];
|
||||
const out: DriftEntry[] = [];
|
||||
|
||||
// Provisioned servers load this generated file after their main cfg. It
|
||||
// remains managed even when there is no promoted baseline yet.
|
||||
const mapConfig = srv.created_by && srv.game_mode === 'zombie'
|
||||
? getServerMapConfig(srv.game, srv.map) : null;
|
||||
if (mapConfig) {
|
||||
const abs = join(destOfKind(server, 'config'), MAP_CONFIG_FILE);
|
||||
const content = await readFile(abs).catch(() => null);
|
||||
if (!content?.equals(mapConfig)) {
|
||||
out.push({ server, relPath: MAP_CONFIG_FILE, kind: 'config', driftKind: 'missing',
|
||||
size: mapConfig.byteLength, sha256: sha256Buffer(mapConfig) });
|
||||
}
|
||||
}
|
||||
|
||||
const scopeApplies = (r: { scope_type: string | null; scope_value: string | null }): boolean => {
|
||||
if (!r.scope_type || !r.scope_value) return false;
|
||||
const field = r.scope_type === 'game' ? srv.game : srv.map;
|
||||
@@ -170,7 +191,7 @@ async function scanMissingForServer(server: string): Promise<DriftEntry[]> {
|
||||
if (srv.game) {
|
||||
const baseline = getBaselineForScope('game', srv.game);
|
||||
if (baseline?.current_version_id) {
|
||||
const composed = composeExpectedCfg(server, srv.game, srv.map);
|
||||
const composed = composeExpectedCfg(srv.game, srv.map);
|
||||
if (composed) {
|
||||
const dest = destOfKind(server, 'config');
|
||||
const targetRel = config.configTargetFile;
|
||||
@@ -205,7 +226,8 @@ async function writeMissingEntry(server: string, entry: DriftEntry): Promise<Buf
|
||||
if (entry.kind === 'config') {
|
||||
const srv = getServer(server);
|
||||
if (!srv?.game) return null;
|
||||
const composed = composeExpectedCfg(server, srv.game, srv.map);
|
||||
if (entry.relPath === MAP_CONFIG_FILE) return getServerMapConfig(srv.game, srv.map);
|
||||
const composed = composeExpectedCfg(srv.game, srv.map);
|
||||
return composed?.content ?? null;
|
||||
}
|
||||
if (!entry.scriptVersionId) return null;
|
||||
@@ -214,38 +236,11 @@ async function writeMissingEntry(server: string, entry: DriftEntry): Promise<Buf
|
||||
}
|
||||
|
||||
function composeExpectedCfg(
|
||||
server: string,
|
||||
game: string | null,
|
||||
map: string | null
|
||||
): { content: Buffer; sha256: string } | null {
|
||||
if (!game) return null;
|
||||
const baseline = getBaselineForScope('game', game);
|
||||
if (!baseline?.current_version_id) return null;
|
||||
const baselineVer = getVersion(baseline.current_version_id);
|
||||
if (!baselineVer) return null;
|
||||
|
||||
const scopeApplies = (r: ScriptRow): boolean => {
|
||||
if (!r.scope_type || !r.scope_value) return false;
|
||||
const field = r.scope_type === 'game' ? game : map;
|
||||
return field === r.scope_value;
|
||||
};
|
||||
|
||||
const overlayInputs: Array<{ id: number; text: string }> = [];
|
||||
const seen = new Set<number>();
|
||||
const collect = (rows: ScriptRow[]): void => {
|
||||
for (const r of rows) {
|
||||
if (seen.has(r.id) || !scopeApplies(r) || !r.current_version_id) continue;
|
||||
const v = getVersion(r.current_version_id);
|
||||
if (!v) continue;
|
||||
overlayInputs.push({ id: r.id, text: v.content.toString('utf8') });
|
||||
seen.add(r.id);
|
||||
}
|
||||
};
|
||||
collect(listPromotedOverlaysForScope('game', game));
|
||||
if (map) collect(listPromotedOverlaysForScope('map', map));
|
||||
|
||||
const text = composeCfg(baselineVer.content.toString('utf8'), overlayInputs);
|
||||
const content = Buffer.from(text, 'utf8');
|
||||
const content = getPromotedServerConfig(game, map);
|
||||
if (!content) return null;
|
||||
return { content, sha256: sha256Buffer(content) };
|
||||
}
|
||||
|
||||
|
||||
Reference in New Issue
Block a user