Files
healer-man/scripts/runewaker-pipeline/verify-native-waterfall-source-layout.mjs
2026-10-06 22:25:34 -04:00

70 lines
4.5 KiB
JavaScript

import assert from "node:assert/strict";
import { createHash } from "node:crypto";
import { readFile, writeFile } from "node:fs/promises";
import { NodeIO } from "@gltf-transform/core";
import { ALL_EXTENSIONS } from "@gltf-transform/extensions";
const option = name => process.argv.find(value => value.startsWith(`--${name}=`))?.slice(name.length + 3);
const stage = option("stage-root") ?? "artifacts/playtest/rom-room-coverage/native-alpha-stage";
const slugs = (option("slugs") ?? "cave-water-dragon,kulech-bones-nest,mystic-altar,raksha-temple,sardo-castle,tomb-seven-heroes").split(",");
const reportName = option("report-name") ?? "native-waterfall-alpha-stage-report.json";
const sha = bytes => createHash("sha256").update(bytes).digest("hex");
const io = new NodeIO().registerExtensions(ALL_EXTENSIONS);
function accessor(value) {
if (!value) return null;
const array = value.getArray();
assert.ok(array);
return { type: value.getType(), componentType: value.getComponentType(), normalized: value.getNormalized(),
count: value.getCount(), sha256: sha(new Uint8Array(array.buffer, array.byteOffset, array.byteLength)) };
}
function layout(document) {
const root = document.getRoot();
const meshes = root.listMeshes();
const nodes = root.listNodes();
return {
meshes: meshes.map(mesh => ({ name: mesh.getName(), primitives: mesh.listPrimitives().map(primitive => ({
mode: primitive.getMode(), indices: accessor(primitive.getIndices()),
attributes: Object.fromEntries(primitive.listSemantics().sort().map(name => [name, accessor(primitive.getAttribute(name))])),
targets: primitive.listTargets().map(target => Object.fromEntries(target.listSemantics().sort().map(name => [name, accessor(target.getAttribute(name))]))),
})) })),
nodes: nodes.map(node => ({ name: node.getName(), translation: node.getTranslation(), rotation: node.getRotation(),
scale: node.getScale(), weights: node.getWeights(), mesh: meshes.indexOf(node.getMesh()),
children: node.listChildren().map(child => nodes.indexOf(child)) })),
scenes: root.listScenes().map(scene => ({ name: scene.getName(), children: scene.listChildren().map(node => nodes.indexOf(node)) })),
};
}
function images(document) {
return document.getRoot().listTextures().map(texture => ({ name: texture.getName(), mimeType: texture.getMimeType(),
sha256: sha(texture.getImage()) })).sort((a, b) => JSON.stringify(a).localeCompare(JSON.stringify(b)));
}
function binHash(bytes) {
for (let offset = 12; offset < bytes.length;) {
const length = bytes.readUInt32LE(offset);
if (bytes.readUInt32LE(offset + 4) === 0x004e4942) return sha(bytes.subarray(offset + 8, offset + 8 + length));
offset += length + 8;
}
throw new Error("Missing BIN chunk");
}
for (const slug of slugs) {
const directory = process.argv.includes("--flat") ? stage : `${stage}/${slug}`;
const report = JSON.parse(await readFile(`${directory}/${reportName}`, "utf8"));
const sourceBytes = await readFile(report.sourceVisual.file);
const stagedBytes = await readFile(report.stagedSource.file);
assert.equal(sha(sourceBytes), report.sourceVisual.sha256);
assert.equal(sha(stagedBytes), report.stagedSource.sha256);
const source = await io.readBinary(sourceBytes);
const staged = await io.readBinary(stagedBytes);
const sourceLayout = layout(source), stagedLayout = layout(staged);
const sourceImages = images(source), stagedImages = images(staged);
assert.deepEqual(stagedLayout, sourceLayout, `${slug}: final staged geometry/transforms changed`);
assert.deepEqual(stagedImages, sourceImages, `${slug}: final staged image bytes changed`);
const proof = { schemaVersion: 1, dungeonId: slug, sourceSha256: sha(sourceBytes), stagedSha256: sha(stagedBytes),
geometryAndHierarchyUnchanged: true, sourceImageBytesUnchanged: true,
geometryLayoutSha256: sha(JSON.stringify(sourceLayout)), imageInventorySha256: sha(JSON.stringify(sourceImages)),
meshCount: sourceLayout.meshes.length, nodeCount: sourceLayout.nodes.length, textureCount: sourceImages.length,
sourceBinSha256: binHash(sourceBytes), stagedBinSha256: binHash(stagedBytes),
note: "The JSON-only alpha patch retains BIN bytes. Subsequent native unlit semantics use NodeIO and may repack BIN layout; final decoded streams, hierarchy/TRS and image bytes are verified separately here." };
await writeFile(`${directory}/final-source-layout-proof.json`, `${JSON.stringify(proof, null, 2)}\n`);
console.log(`${slug}: final geometry, UVs, hierarchy and ${sourceImages.length} exact source images preserved`);
}