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