import { spawn } from "node:child_process"; import { mkdir, stat, writeFile } from "node:fs/promises"; import path from "node:path"; import { hasFlag, optionValue, pipelineDirectory, projectPath, projectRoot, readJson, resolveProject, } from "./lib/recipe.mjs"; const argv = process.argv.slice(2); const preparationReport = resolveProject(optionValue(argv, "--catalog", "runewaker-export-work/all-instances-preparation-report.json")); const outputReport = resolveProject(optionValue(argv, "--report", "runewaker-export-work/all-instances-build-report.json")); const resume = hasFlag(argv, "--resume"); const force = hasFlag(argv, "--force"); const requested = new Set(String(optionValue(argv, "--only", "")).split(",").map((value) => value.trim()).filter(Boolean)); const phaseValue = optionValue(argv, "--phase", "all"); const phases = phaseValue === "all" ? ["environment", "actors", "population", "mechanics", "validate"] : phaseValue.split(",").map((value) => value.trim()).filter(Boolean); const validPhases = new Set(["environment", "actors", "population", "mechanics", "validate"]); for (const phase of phases) if (!validPhases.has(phase)) throw new Error("Unsupported phase: " + phase); const catalog = await readJson(preparationReport); const dungeons = catalog.dungeons.filter((entry) => ( entry.slug && entry.status !== "preserved-existing-pilot" && (!requested.size || requested.has(entry.slug) || requested.has(entry.id)) )); if (!dungeons.length) throw new Error("No RuneWaker dungeons matched the requested batch."); function run(script, argumentsList, label) { return new Promise((resolve, reject) => { console.log("\n[runewaker-all] " + label); const child = spawn(process.execPath, [path.join(pipelineDirectory, script), ...argumentsList], { cwd: projectRoot, stdio: "inherit", windowsHide: true, }); child.once("error", reject); child.once("exit", (code) => code === 0 ? resolve() : reject(new Error(label + " exited with code " + code + "."))); }); } async function exists(file) { try { return (await stat(file)).isFile(); } catch { return false; } } function phaseOutput(slug, recipe, phase) { if (phase === "environment") return resolveProject(recipe.files.environmentMetadata); if (phase === "actors") return resolveProject(recipe.files.actorManifest); if (phase === "population") return resolveProject(recipe.files.report); if (phase === "mechanics") return resolveProject(recipe.files.mechanicsReport); return path.join(projectRoot, "src", "assets", "game", "dungeons", slug, "instance-validation-report.json"); } async function executePhase(slug, recipeFile, recipe, phase) { const output = phaseOutput(slug, recipe, phase); if (resume && !force && await exists(output)) { return { phase, status: "skipped-existing", output: projectPath(output) }; } if (phase === "environment") { await run("build-environment.mjs", ["--recipe", recipe.files.environmentRecipe], slug + " environment"); } else if (phase === "actors") { await run("build-population.mjs", ["--recipe", projectPath(recipeFile), "--actors-only"], slug + " actors"); } else if (phase === "population") { await run("build-population.mjs", ["--recipe", projectPath(recipeFile)], slug + " population"); } else if (phase === "mechanics") { await run("extract-mechanics.mjs", ["--recipe", projectPath(recipeFile)], slug + " mechanics audit"); } else { await run("validate-instance.mjs", ["--recipe", projectPath(recipeFile)], slug + " validation"); } return { phase, status: "completed", output: projectPath(output) }; } const startedAt = new Date().toISOString(); const results = []; await mkdir(path.dirname(outputReport), { recursive: true }); async function saveReport() { await writeFile(outputReport, JSON.stringify({ schemaVersion: 1, startedAt, updatedAt: new Date().toISOString(), requestedPhases: phases, resume, results, }, null, 2) + "\n", "utf8"); } for (const dungeon of dungeons) { const recipeFile = path.join(pipelineDirectory, "recipes", dungeon.slug + "-population.json"); const recipe = await readJson(recipeFile); const result = { dungeonId: dungeon.slug, status: "in-progress", phases: [] }; results.push(result); try { for (const phase of phases) result.phases.push(await executePhase(dungeon.slug, recipeFile, recipe, phase)); result.status = "green"; } catch (error) { result.status = "failed"; result.error = error instanceof Error ? error.message : String(error); } await saveReport(); } const failed = results.filter((result) => result.status === "failed"); console.log("\n[runewaker-all] " + (results.length - failed.length) + "/" + results.length + " dungeons completed."); console.log("[runewaker-all] report: " + projectPath(outputReport)); if (failed.length) { console.error("[runewaker-all] failed: " + failed.map((result) => result.dungeonId).join(", ")); process.exitCode = 1; }