Crusader_Decomp/psx-map-exporter/scripts/export-all.mjs

170 lines
5.6 KiB
JavaScript
Raw Permalink Normal View History

2026-04-18 16:34:35 +02:00
#!/usr/bin/env node
// Batch-export every LSET*/L*.WDL found under the PSX disc root into a
// permanent .output-render/<mapStem>/<variant>/ directory tree. Each export
// runs as a separate child process so an OOM or crash on one map cannot kill
// the batch.
import { spawn } from 'node:child_process';
import fs from 'node:fs/promises';
import path from 'node:path';
import process from 'node:process';
import { fileURLToPath } from 'node:url';
const SCRIPT_DIR = path.dirname(fileURLToPath(import.meta.url));
const PROJECT_ROOT = path.resolve(SCRIPT_DIR, '..');
const CLI_PATH = path.join(PROJECT_ROOT, 'src', 'cli.js');
const VARIANTS = [
{ label: 'auto', mapSource: 'auto' },
{ label: 'region01', mapSource: 'region01' },
];
function parseArgs(argv) {
const options = {
discRoot: 'E:/emu/psx/Crusader - No Remorse',
outputRoot: path.join(PROJECT_ROOT, '.output-render'),
debugLabels: true,
only: null,
timeoutMs: 300000,
};
for (let index = 2; index < argv.length; index += 1) {
const arg = argv[index];
const next = argv[index + 1];
if (arg === '--disc-root') { options.discRoot = path.resolve(next); index += 1; }
else if (arg === '--output-root') { options.outputRoot = path.resolve(next); index += 1; }
else if (arg === '--no-debug-labels') { options.debugLabels = false; }
else if (arg === '--only') {
options.only = String(next).split(',').map((v) => v.trim()).filter(Boolean);
index += 1;
} else if (arg === '--timeout-ms') {
options.timeoutMs = Number(next); index += 1;
}
}
return options;
}
async function discoverMaps(discRoot) {
const entries = await fs.readdir(discRoot, { withFileTypes: true });
const maps = [];
for (const entry of entries) {
if (!entry.isDirectory()) continue;
if (!/^LSET\d+$/i.test(entry.name)) continue;
const dir = path.join(discRoot, entry.name);
const files = await fs.readdir(dir);
for (const file of files) {
if (!/^L\d+\.WDL$/i.test(file)) continue;
maps.push({
set: entry.name,
name: path.parse(file).name,
wdlPath: path.join(dir, file),
sourceRel: `${entry.name}/${file}`,
});
}
}
maps.sort((a, b) => {
const an = Number.parseInt(a.name.replace(/^L/i, ''), 10);
const bn = Number.parseInt(b.name.replace(/^L/i, ''), 10);
return an - bn;
});
return maps;
}
function runExport(mapEntry, variant, options) {
return new Promise((resolve) => {
const outDir = path.join(options.outputRoot, mapEntry.name, variant.label);
const args = [
'--max-old-space-size=4096',
CLI_PATH,
'--disc-root', options.discRoot,
'--source', mapEntry.sourceRel,
'--map-source', variant.mapSource,
'--output-root', outDir,
];
if (options.debugLabels) args.push('--debug-labels');
const started = Date.now();
const child = spawn(process.execPath, args, { stdio: ['ignore', 'pipe', 'pipe'] });
let stdout = '';
let stderr = '';
let killed = false;
const timer = setTimeout(() => {
killed = true;
child.kill('SIGKILL');
}, options.timeoutMs);
child.stdout.on('data', (chunk) => { stdout += chunk; });
child.stderr.on('data', (chunk) => { stderr += chunk; });
child.on('close', (code, signal) => {
clearTimeout(timer);
const ms = Date.now() - started;
resolve({ outDir, code, signal, killed, ms, stdout, stderr });
});
});
}
async function main() {
const options = parseArgs(process.argv);
const maps = await discoverMaps(options.discRoot);
const filtered = options.only ? maps.filter((m) => options.only.includes(m.name)) : maps;
console.log(`Found ${filtered.length} maps under ${options.discRoot}`);
await fs.mkdir(options.outputRoot, { recursive: true });
const summary = [];
let okCount = 0;
let failCount = 0;
for (const mapEntry of filtered) {
for (const variant of VARIANTS) {
const tag = `[${mapEntry.set}/${mapEntry.name}] variant=${variant.label}`;
process.stdout.write(`${tag} ... `);
const result = await runExport(mapEntry, variant, options);
if (result.code === 0) {
okCount += 1;
process.stdout.write(`OK (${result.ms}ms)\n`);
summary.push({
set: mapEntry.set,
map: mapEntry.name,
variant: variant.label,
ms: result.ms,
ok: true,
outDir: path.relative(options.outputRoot, result.outDir),
});
} else {
failCount += 1;
const reason = result.killed
? `TIMEOUT after ${result.ms}ms`
: `exit ${result.code}${result.signal ? ' signal ' + result.signal : ''}`;
process.stdout.write(`FAIL (${reason})\n`);
if (result.stderr) {
process.stdout.write(` stderr: ${result.stderr.trim().split(/\r?\n/).slice(-5).join('\n stderr: ')}\n`);
}
summary.push({
set: mapEntry.set,
map: mapEntry.name,
variant: variant.label,
ms: result.ms,
ok: false,
reason,
stderrTail: result.stderr.trim().split(/\r?\n/).slice(-10).join('\n'),
outDir: path.relative(options.outputRoot, result.outDir),
});
}
}
}
const indexPath = path.join(options.outputRoot, 'index.json');
await fs.writeFile(indexPath, JSON.stringify({
discRoot: options.discRoot,
generatedAt: new Date().toISOString(),
variants: VARIANTS.map((v) => v.label),
okCount,
failCount,
maps: summary,
}, null, 2));
console.log(`\nWrote index ${indexPath} (ok=${okCount} fail=${failCount})`);
}
main().catch((error) => {
console.error(error);
process.exitCode = 1;
});