129 lines
7.3 KiB
JavaScript
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 }));
|