#!/usr/bin/env node // Batch-export every LSET*/L*.WDL found under the PSX disc root into a // permanent .output-render/// 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; });