113 lines
4.9 KiB
JavaScript
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;
|
|
}
|