Files
2026-08-14 15:56:39 -04:00

113 lines
4.9 KiB
JavaScript

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;
}