170 lines
5.6 KiB
JavaScript
170 lines
5.6 KiB
JavaScript
|
|
#!/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;
|
||
|
|
});
|