Files
healer-man/scripts/runewaker-pipeline/promote-native-waterfall-alpha.mjs
T
2026-10-06 22:25:34 -04:00

129 lines
7.3 KiB
JavaScript

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