import assert from "node:assert/strict"; import { createHash } from "node:crypto"; import { constants } from "node:fs"; import { copyFile, mkdir, readFile, writeFile } from "node:fs/promises"; import path from "node:path"; const magma = process.argv.includes("--magma"); const stage = magma ? "artifacts/playtest/rom-room-coverage/lair-magma-alpha-stage" : "artifacts/playtest/rom-room-coverage/native-alpha-stage"; const slugs = magma ? ["lair-demon-dragon"] : ["cave-water-dragon", "kulech-bones-nest", "mystic-altar", "raksha-temple", "sardo-castle", "tomb-seven-heroes"]; const proofKey = magma ? "nativeMagmaAlpha" : "nativeWaterfallAlpha"; const backup = `${stage}/pre-promotion-backup`; const sha = bytes => createHash("sha256").update(bytes).digest("hex"); const json = async file => JSON.parse(await readFile(file, "utf8")); const copies = []; const edits = []; const proofs = []; const visuals = await json(`${stage}/${magma ? "lair-magma-alpha-ab.json" : "gate-waterfall-triangle-pairs-report.json"}`); for (const slug of slugs) { const directory = magma ? stage : `${stage}/${slug}`; const reportFile = `${directory}/${magma ? "lair-magma-alpha-stage-report.json" : "native-waterfall-alpha-stage-report.json"}`; const report = await json(reportFile); assert.equal(report.dungeonId, slug); const visual = magma ? visuals : visuals.dungeons.find(row => row.slug === slug); if (magma) { for (const value of Object.values(visual.checks)) assert.equal(value, true); assert.equal(visual.candidateSha256, report.optimized.sha256); assert.equal(report.optimized.targetAlphaMode, "MASK"); assert.equal(report.optimized.targetAlphaCutoff, 192 / 255); assert.equal(report.optimized.targetUnlit, true); } else { assert.equal(visual?.status, "direct-hit-paired", `${slug}: require a same-camera visible waterfall pair`); assert.equal(visual.errors.length, 0); assert.equal(visual.afterFirstHit.transparent, true, `${slug}: candidate must actually supply native blending`); assert.ok(visual.afterFirstHit.nativeSourceModel, `${slug}: candidate provenance missing from runtime hit`); assert.equal(visual.stagedResponseSha256, report.optimized.sha256); } assert.ok(report.alphaPatch.bindingCount > 0); assert.equal(report.alphaPatch.binBytesUnchanged, true); assert.equal(report.alphaPatch.beforeBinSha256, report.alphaPatch.afterBinSha256); const layout = await json(`${directory}/final-source-layout-proof.json`); assert.equal(layout.geometryAndHierarchyUnchanged, true); assert.equal(layout.sourceImageBytesUnchanged, true); assert.equal(layout.sourceSha256, report.sourceVisual.sha256); assert.equal(layout.stagedSha256, report.stagedSource.sha256); const alphaImages = magma ? [report.optimized.decodedKtxAlpha] : report.optimized.gateWaterfallKtxAlpha; assert.ok(alphaImages?.length > 0, `${slug}: decode alpha before promotion`); for (const image of alphaImages) { if (!magma) assert.equal(image.hasAlpha, true); assert.ok(image.decodedAlphaRange[0] < image.decodedAlphaRange[1] && image.decodedAlphaRange[1] <= 255); if (!magma) assert.ok(image.decodedAlphaRange[1] < 255); } for (const source of report.sourceTextureProvenance) { assert.equal(sha(await readFile(source.sourceTexturePath)), source.sourceTextureSha256); assert.equal(sha(await readFile(source.sourcePngPath)), source.sourcePngSha256); } assert.equal(sha(await readFile(report.sourceVisual.file)), report.sourceVisual.sha256); const canonicalBytes = await readFile(report.stagedSource.file); assert.equal(sha(canonicalBytes), report.stagedSource.sha256); assert.equal(canonicalBytes.length, report.stagedSource.bytes); assert.equal(sha(await readFile(report.optimized.file)), report.optimized.sha256); const ship = `src/assets/game/dungeons/${slug}`; const converted = `runewaker-export-work/${slug}/converted`; const oldShipping = sha(await readFile(`${ship}/${slug}-visual.glb`)); assert.equal(sha(await readFile(`${converted}/${slug}-visual.optimized.glb`)), oldShipping); copies.push( { file: report.sourceVisual.file, staged: report.stagedSource.file, previousSha256: report.sourceVisual.sha256, sha256: report.stagedSource.sha256 }, ...[`${ship}/${slug}-visual.glb`, `${converted}/${slug}-visual.optimized.glb`].map(file => ({ file, staged: report.optimized.file, previousSha256: oldShipping, sha256: report.optimized.sha256 })), ); const proof = { stageReport: reportFile, canonicalBefore: report.sourceVisual.sha256, canonicalAfter: report.stagedSource.sha256, shippingBefore: oldShipping, shippingAfter: report.optimized.sha256, bindingCount: report.alphaPatch.bindingCount, sourceTextureProvenance: report.sourceTextureProvenance, alphaPatch: report.alphaPatch, finalSourceLayout: layout, directVisualProof: visual, decodedKtxAlpha: alphaImages, }; proofs.push({ dungeonId: slug, ...proof }); for (const directory of [ship, converted]) { for (const name of ["import-metadata.json", "optimization-report.json", "conversion-report.json"]) { const file = `${directory}/${name}`; const bytes = await readFile(file).catch(error => { if (directory === converted && name === "import-metadata.json" && error.code === "ENOENT") return null; throw error; }); if (!bytes) continue; const data = JSON.parse(bytes); if (name === "import-metadata.json") { const visual = data.assets.find(row => row.role === "visual"); if (directory === ship) assert.equal(visual.sha256, oldShipping); visual.sha256 = report.optimized.sha256; visual.size = report.optimized.bytes; data.continuationReview ??= {}; data.continuationReview[proofKey] = proof; if (data.nativeMaterialSemantics?.assembledSourceSha256) data.nativeMaterialSemantics.assembledSourceSha256 = report.stagedSource.sha256; } else if (name === "optimization-report.json") { if (directory === ship) assert.equal(data.optimized.checksum, oldShipping); data.source.checksum = report.stagedSource.sha256; data.source.size = report.stagedSource.bytes; data.optimized.checksum = report.optimized.sha256; data.optimized.size = report.optimized.bytes; data.compression = "meshopt+ktx2"; data.textureQuality = await json(report.optimized.textureQualityReport); data.nativeMaterialSemantics = report.nativeMaterialSemantics; data[proofKey] = proof; } else { data.continuationIntegrityProof ??= {}; data.continuationIntegrityProof[proofKey] = proof; } edits.push({ file, bytes, data }); } } } // All old and candidate identities have been checked before the first write. // Exclusive backups make an accidental repeat fail without losing the baseline. for (const row of [...copies, ...edits]) { const destination = path.join(backup, row.file); await mkdir(path.dirname(destination), { recursive: true }); await copyFile(row.file, destination, constants.COPYFILE_EXCL); } for (const row of copies) await copyFile(row.staged, row.file); for (const row of edits) await writeFile(row.file, `${JSON.stringify(row.data, null, 2)}\n`); await writeFile(`${stage}/promotion-proof.json`, `${JSON.stringify({ schemaVersion: 1, backup, files: copies, dungeons: proofs }, null, 2)}\n`); console.log(JSON.stringify({ promoted: slugs, files: copies.length, backup }));