149 lines
5.0 KiB
JavaScript
149 lines
5.0 KiB
JavaScript
#!/usr/bin/env node
|
|
import { open } from "node:fs/promises";
|
|
import path from "node:path";
|
|
import {
|
|
exists,
|
|
projectRoot,
|
|
readJson,
|
|
sha256,
|
|
workRoot,
|
|
writeJson,
|
|
} from "./lib.mjs";
|
|
|
|
const slug = process.argv[2];
|
|
if (!slug || !/^[a-z0-9]+(?:-[a-z0-9]+)*$/.test(slug)) {
|
|
throw new Error("Usage: node reconcile-conversion-report.mjs <dungeon-slug>");
|
|
}
|
|
|
|
async function glbTriangleCount(file) {
|
|
const handle = await open(file, "r");
|
|
try {
|
|
const header = Buffer.alloc(20);
|
|
await handle.read(header, 0, header.length, 0);
|
|
if (header.toString("ascii", 0, 4) !== "glTF" || header.readUInt32LE(4) !== 2) {
|
|
throw new Error(`${file}: expected a glTF 2 GLB.`);
|
|
}
|
|
const jsonLength = header.readUInt32LE(12);
|
|
if (header.readUInt32LE(16) !== 0x4E4F534A) {
|
|
throw new Error(`${file}: leading GLB chunk is not JSON.`);
|
|
}
|
|
const json = Buffer.alloc(jsonLength);
|
|
await handle.read(json, 0, jsonLength, 20);
|
|
const document = JSON.parse(json.toString("utf8").replace(/\0+$/u, "").trimEnd());
|
|
const accessors = document.accessors ?? [];
|
|
return (document.meshes ?? []).reduce((total, mesh) => (
|
|
total + (mesh.primitives ?? []).reduce((meshTotal, primitive) => {
|
|
const mode = primitive.mode ?? 4;
|
|
if (mode !== 4) throw new Error(`${file}: non-triangle primitive mode ${mode}.`);
|
|
const accessorIndex = primitive.indices ?? primitive.attributes?.POSITION;
|
|
if (accessorIndex === undefined) {
|
|
throw new Error(`${file}: primitive has neither indices nor POSITION.`);
|
|
}
|
|
return meshTotal + Math.floor(accessors[accessorIndex].count / 3);
|
|
}, 0)
|
|
), 0);
|
|
} finally {
|
|
await handle.close();
|
|
}
|
|
}
|
|
|
|
const conversionFile = path.join(
|
|
workRoot,
|
|
slug,
|
|
"staging",
|
|
"environment",
|
|
"conversion-report.json",
|
|
);
|
|
if (!await exists(conversionFile)) throw new Error(`${slug}: conversion report is missing.`);
|
|
const conversion = await readJson(conversionFile);
|
|
const collisionEntries = Array.isArray(conversion.collision)
|
|
? conversion.collision
|
|
: conversion.collision
|
|
? [conversion.collision]
|
|
: [];
|
|
if (!collisionEntries.length) throw new Error(`${slug}: conversion report has no collision entries.`);
|
|
|
|
const corrections = [];
|
|
for (const entry of collisionEntries) {
|
|
const file = path.join(path.dirname(conversionFile), entry.file);
|
|
const exportedTriangles = await glbTriangleCount(file);
|
|
if (entry.triangles !== exportedTriangles) {
|
|
corrections.push({
|
|
file: entry.file,
|
|
reportedTriangles: entry.triangles,
|
|
exportedTriangles,
|
|
});
|
|
entry.triangles = exportedTriangles;
|
|
}
|
|
}
|
|
|
|
const method = "sum floor(index-or-position accessor count / 3) for TRIANGLES primitives";
|
|
const visualCorrections = [];
|
|
if (conversion.visual?.file) {
|
|
const file = path.join(path.dirname(conversionFile), conversion.visual.file);
|
|
const exportedTriangles = await glbTriangleCount(file);
|
|
if (conversion.visual.triangles !== exportedTriangles) {
|
|
visualCorrections.push({
|
|
file: conversion.visual.file,
|
|
reportedTriangles: conversion.visual.triangles,
|
|
exportedTriangles,
|
|
});
|
|
conversion.visual.triangles = exportedTriangles;
|
|
}
|
|
}
|
|
conversion.collisionTriangleReconciliation = {
|
|
count: corrections.length,
|
|
provenance: "post-export GLB accessor recount",
|
|
method,
|
|
corrections,
|
|
};
|
|
conversion.visualTriangleReconciliation = {
|
|
count: visualCorrections.length,
|
|
provenance: "post-export GLB accessor recount",
|
|
method,
|
|
corrections: visualCorrections,
|
|
};
|
|
if (corrections.length) {
|
|
const warning = `Reconciled ${corrections.length} collision triangle count(s) to authoritative exported GLB accessors.`;
|
|
conversion.warnings = [...new Set([...(conversion.warnings ?? []), warning])];
|
|
}
|
|
if (visualCorrections.length) {
|
|
const warning = "Reconciled the visual triangle count to authoritative exported GLB accessors.";
|
|
conversion.warnings = [...new Set([...(conversion.warnings ?? []), warning])];
|
|
}
|
|
await writeJson(conversionFile, conversion);
|
|
const conversionReportChecksum = await sha256(conversionFile);
|
|
|
|
const recastFile = path.join(workRoot, slug, "recast-report.json");
|
|
let recastUpdated = false;
|
|
if (await exists(recastFile)) {
|
|
const recast = await readJson(recastFile);
|
|
const expectedSource = path.relative(projectRoot, conversionFile).replace(/\\/g, "/");
|
|
if (recast.source?.path !== expectedSource) {
|
|
throw new Error(`${slug}: Recast source is ${recast.source?.path}, expected ${expectedSource}.`);
|
|
}
|
|
recast.source.checksum = conversionReportChecksum;
|
|
recast.source.collisionTriangleReconciliation = {
|
|
count: corrections.length,
|
|
conversionReportChecksum,
|
|
geometryChanged: false,
|
|
};
|
|
recast.source.visualTriangleReconciliation = {
|
|
count: visualCorrections.length,
|
|
conversionReportChecksum,
|
|
geometryChanged: false,
|
|
};
|
|
await writeJson(recastFile, recast);
|
|
recastUpdated = true;
|
|
}
|
|
|
|
console.log(JSON.stringify({
|
|
status: "green",
|
|
slug,
|
|
conversionReport: path.relative(projectRoot, conversionFile).replace(/\\/g, "/"),
|
|
conversionReportChecksum,
|
|
corrections,
|
|
visualCorrections,
|
|
recastUpdated,
|
|
}, null, 2));
|