182 lines
8.1 KiB
JavaScript
182 lines
8.1 KiB
JavaScript
#!/usr/bin/env node
|
|
import { readdir, readFile, stat, writeFile } from "node:fs/promises";
|
|
import path from "node:path";
|
|
import { fileURLToPath } from "node:url";
|
|
import { buildWdbPlacementPlan } from "./lib/environment-assembly.mjs";
|
|
import { sourceRootFor } from "./lib/recipe.mjs";
|
|
|
|
const root = path.resolve(path.dirname(fileURLToPath(import.meta.url)), "../..");
|
|
const recipeDirectory = path.join(root, "scripts/runewaker-pipeline/recipes");
|
|
const reportJsonFile = path.join(
|
|
root,
|
|
"scripts/runewaker-pipeline/RUNEWAKER_ENVIRONMENT_ASSEMBLY_REPORT.json",
|
|
);
|
|
const reportMarkdownFile = path.join(
|
|
root,
|
|
"scripts/runewaker-pipeline/RUNEWAKER_ENVIRONMENT_ASSEMBLY_REPORT.md",
|
|
);
|
|
const projectPath = (file) => path.relative(root, file).split(path.sep).join("/");
|
|
|
|
async function exists(file) {
|
|
try {
|
|
await stat(file);
|
|
return true;
|
|
} catch {
|
|
return false;
|
|
}
|
|
}
|
|
|
|
function renderMarkdown(report) {
|
|
const rows = report.dungeons.map((dungeon) => [
|
|
"|", dungeon.id, "|", dungeon.mode, "|", dungeon.sourceDescriptors, "|",
|
|
dungeon.uniqueResources, "|", dungeon.staticPlacements, "|",
|
|
dungeon.dynamicPlacements, "|", dungeon.dynamicReviewCandidates, "|",
|
|
dungeon.unsupportedPlacements, "|", dungeon.missingSourceResources, "|",
|
|
].join(" "));
|
|
return [
|
|
"# RuneWaker WDB environment assembly audit",
|
|
"",
|
|
"Structural validation: **" + report.validationStatus + "**",
|
|
"Source-behavior review: **" + report.reviewStatus + "**",
|
|
"",
|
|
"This deterministic audit reads the checked-in WDB reports and environment recipes. It does",
|
|
"not convert or replace shipping GLBs. The builder now supports every valid ROS placement",
|
|
"resource with normalized-path model caching validated by source hashes and the complete translation/quaternion/scale",
|
|
"transform relative to the reviewed primary placement.",
|
|
"",
|
|
"## Coverage",
|
|
"",
|
|
"- " + report.summary.dungeons + " dungeon WDB packages and "
|
|
+ report.summary.sourceDescriptors + " source descriptors.",
|
|
"- " + report.summary.uniqueDungeonResources
|
|
+ " per-dungeon unique ROS resources (resources reused across dungeons share cache keys).",
|
|
"- " + report.summary.staticPlacements + " static placements and "
|
|
+ report.summary.dynamicPlacements + " explicitly reviewed dynamic placements.",
|
|
"- " + report.summary.dynamicReviewCandidates
|
|
+ " filename-based dynamic candidates remain static and review-required; names never prove behavior.",
|
|
"- " + report.summary.unsupportedPlacements + " unsupported placement records and "
|
|
+ report.summary.missingSourceResources + " missing source ROS resources.",
|
|
"",
|
|
"| Dungeon | Mode | Descriptors | Resources | Static | Dynamic | Dynamic review | Unsupported | Missing ROS |",
|
|
"| --- | --- | ---: | ---: | ---: | ---: | ---: | ---: | ---: |",
|
|
...rows,
|
|
"",
|
|
"## Runtime-facing output",
|
|
"",
|
|
"A full build writes `wdb-placement-manifest.json` beside the environment package. Each",
|
|
"placement has a stable WDB GUID ID, normalized resource/cache key, source TRS, primary-local",
|
|
"Three.js matrix, bounds, flags, asset status, and optional reviewed dynamic definition.",
|
|
"Static placements are assembled into the optimized visual and navigation collision. Reviewed",
|
|
"dynamic placements are omitted from static collision and packaged as separately addressable GLBs.",
|
|
"",
|
|
"## Unsupported or review-required behavior",
|
|
"",
|
|
...report.unsupportedResourceTypes.map((item) => "- " + item),
|
|
"- The primary-only top-down preview and editable `.blend` do not include the final merged WDB",
|
|
" assembly; the generated GLB and placement manifest are authoritative.",
|
|
"- Dynamic-candidate filenames are not enough to remove geometry from the static package. Add a",
|
|
" reviewed `source.wdbAssembly.dynamicPlacements` rule with evidence first.",
|
|
"",
|
|
"## Validation issues",
|
|
"",
|
|
...(report.issues.length ? report.issues.map((item) => "- " + item) : ["- None."]),
|
|
"",
|
|
].join("\n");
|
|
}
|
|
|
|
export async function buildEnvironmentAssemblyAudit() {
|
|
const recipeNames = (await readdir(recipeDirectory))
|
|
.filter((name) => name.endsWith(".json")
|
|
&& !name.endsWith("-population.json")
|
|
&& name !== "runewaker-player-avatars.json")
|
|
.sort();
|
|
const dungeons = [];
|
|
const issues = [];
|
|
for (const recipeName of recipeNames) {
|
|
const recipe = JSON.parse(await readFile(path.join(recipeDirectory, recipeName), "utf8"));
|
|
const wdbReportFile = path.join(
|
|
root,
|
|
"src/assets/game/dungeons",
|
|
recipe.slug,
|
|
"wdb-report.json",
|
|
);
|
|
if (!await exists(wdbReportFile)) {
|
|
issues.push(recipe.slug + ": checked-in WDB report is missing.");
|
|
continue;
|
|
}
|
|
try {
|
|
const wdbReport = JSON.parse(await readFile(wdbReportFile, "utf8"));
|
|
const plan = buildWdbPlacementPlan(wdbReport, recipe);
|
|
const resourceRoot = path.join(sourceRootFor(recipe), recipe.source.resourceRoot);
|
|
const missingResources = [];
|
|
for (const resource of plan.resources) {
|
|
const file = path.join(resourceRoot, ...resource.normalizedResource.split("\\"));
|
|
if (!await exists(file)) missingResources.push(resource.resource);
|
|
}
|
|
dungeons.push({
|
|
id: recipe.slug,
|
|
mode: recipe.source.proceduralTerrainFallback
|
|
? "primary-only-procedural-fallback"
|
|
: recipe.source.wdbAssembly?.mode ?? "all-ros",
|
|
recipe: projectPath(path.join(recipeDirectory, recipeName)),
|
|
wdbReport: projectPath(wdbReportFile),
|
|
sourceDescriptors: plan.sourceDescriptorCount,
|
|
uniqueResources: plan.resources.length,
|
|
staticPlacements: plan.staticPlacements.length,
|
|
dynamicPlacements: plan.dynamicPlacements.length,
|
|
dynamicReviewCandidates: plan.reviewRequiredPlacements.length,
|
|
unsupportedPlacements: plan.unsupportedPlacements.length,
|
|
missingSourceResources: missingResources.length,
|
|
missingResources,
|
|
unsupported: plan.unsupportedPlacements,
|
|
});
|
|
} catch (error) {
|
|
issues.push(recipe.slug + ": " + error.message);
|
|
}
|
|
}
|
|
dungeons.sort((left, right) => left.id.localeCompare(right.id));
|
|
const sum = (field) => dungeons.reduce((total, dungeon) => total + dungeon[field], 0);
|
|
const unsupportedResourceTypes = [
|
|
"WDB placement resources other than ROS are recorded but not converted.",
|
|
"Particle, light, sound, camera, and script-only components absent from ROS mesh descriptors remain unsupported.",
|
|
"Animated/stateful behavior remains unsupported until an exact reviewed dynamic-placement rule supplies its kind, initial state, collision policy, and evidence.",
|
|
"A ROS that the preserved exporter cannot decode is skipped, recorded with its resource path and failure, and never replaced with guessed geometry.",
|
|
];
|
|
return {
|
|
schemaVersion: 1,
|
|
generatedBy: "scripts/runewaker-pipeline/audit-environment-assembly.mjs",
|
|
deterministic: true,
|
|
validationStatus: issues.length ? "blocked" : "green",
|
|
reviewStatus: sum("dynamicReviewCandidates") || sum("unsupportedPlacements")
|
|
|| sum("missingSourceResources") ? "review-required" : "green",
|
|
summary: {
|
|
dungeons: dungeons.length,
|
|
sourceDescriptors: sum("sourceDescriptors"),
|
|
uniqueDungeonResources: sum("uniqueResources"),
|
|
staticPlacements: sum("staticPlacements"),
|
|
dynamicPlacements: sum("dynamicPlacements"),
|
|
dynamicReviewCandidates: sum("dynamicReviewCandidates"),
|
|
unsupportedPlacements: sum("unsupportedPlacements"),
|
|
missingSourceResources: sum("missingSourceResources"),
|
|
},
|
|
unsupportedResourceTypes,
|
|
issues,
|
|
dungeons,
|
|
};
|
|
}
|
|
|
|
const report = await buildEnvironmentAssemblyAudit();
|
|
await Promise.all([
|
|
writeFile(reportJsonFile, JSON.stringify(report, null, 2) + "\n", "utf8"),
|
|
writeFile(reportMarkdownFile, renderMarkdown(report), "utf8"),
|
|
]);
|
|
console.log(JSON.stringify({
|
|
validationStatus: report.validationStatus,
|
|
reviewStatus: report.reviewStatus,
|
|
reportJson: projectPath(reportJsonFile),
|
|
reportMarkdown: projectPath(reportMarkdownFile),
|
|
summary: report.summary,
|
|
issues: report.issues,
|
|
}, null, 2));
|
|
if (report.issues.length) process.exitCode = 1;
|