Files
healer-man/scripts/dungeon-pipeline/pack-wow-adt-terrain-pilot.mjs
T
2026-10-07 13:05:04 -04:00

133 lines
9.1 KiB
JavaScript

import { createHash } from "node:crypto";
import { readFile, writeFile } from "node:fs/promises";
import path from "node:path";
// Diagnostic lossless append: preserve existing images/geometry byte-for-byte.
// Do not run solid-texture pruning on RGB weight carriers stored through AO.
const [input, bundleDirectory, output] = process.argv.slice(2);
if (!input || !bundleDirectory || !output) throw new Error("Provide shipping GLB, source bundle directory, and diagnostic output GLB.");
const hash = bytes => createHash("sha256").update(bytes).digest("hex");
const original = await readFile(input);
if (original.toString("ascii", 0, 4) !== "glTF" || original.readUInt32LE(4) !== 2
|| original.readUInt32LE(8) !== original.length) throw new Error("Invalid GLB.");
const jsonLength = original.readUInt32LE(12);
const json = JSON.parse(original.subarray(20, 20 + jsonLength).toString().trim());
const binaryHeader = 20 + jsonLength;
if (original.readUInt32LE(binaryHeader + 4) !== 0x004e4942 || json.buffers.length !== 1) throw new Error("Expected one embedded BIN buffer.");
const originalBinary = original.subarray(binaryHeader + 8, binaryHeader + 8 + original.readUInt32LE(binaryHeader));
const manifest = JSON.parse(await readFile(path.join(bundleDirectory, "terrain-materials.json"), "utf8"));
if (manifest.sourceAdt.sha256 !== "8ad75da6b9485d61537ee3b451c08b79e25b1b1b2c7d4e91c8e57ad0a65ff75b"
|| manifest.sourceAdt.clientBuild !== "3.3.5a-Ascension"
|| hash(await readFile(path.join(bundleDirectory, "source/tanarisinstance_30_29.adt"))) !== manifest.sourceAdt.sha256
|| hash(original) !== "a45ad3ad072fce73d657fb668cab691fc87059a8a0701630465249ce8670d8f0") {
throw new Error("Pilot inputs must match the verified Ascension source tile and reviewed shipping asset.");
}
const targetNodes = json.nodes.filter(node => node.name === manifest.sourceGeometry.node);
if (targetNodes.length !== 1 || targetNodes[0].children?.length) throw new Error("Ambiguous or nested target terrain node.");
const target = targetNodes[0];
const oldMesh = target.mesh;
const oldPrimitives = json.meshes[oldMesh]?.primitives;
if (oldPrimitives?.length !== 1 || !/^ADT_Terrain(?:\.\d+)?$/.test(json.materials[oldPrimitives[0].material]?.name ?? "")
|| json.accessors[oldPrimitives[0].indices].count !== manifest.sourceGeometry.indexCount
|| json.nodes.filter(node => node.mesh === oldMesh).length !== 1) throw new Error("Target is not the verified single terrain primitive.");
if (json.nodes.some(node => node.children?.includes(json.nodes.indexOf(target)))) throw new Error("Expected terrain at scene root.");
// This pilot supports the verified no-hole tile. Other tiles must be recovered independently.
if (manifest.chunks.length !== 256 || manifest.chunks.some(chunk => chunk.sourceHoleMaskHex !== "0000")) throw new Error("Unverified tile topology.");
const parts = [originalBinary];
let byteLength = originalBinary.length;
function append(bytes, targetKind) {
const padding = (4 - byteLength % 4) % 4;
if (padding) { parts.push(Buffer.alloc(padding)); byteLength += padding; }
const view = json.bufferViews.length;
json.bufferViews.push({ buffer: 0, byteOffset: byteLength, byteLength: bytes.length, ...(targetKind ? { target: targetKind } : {}) });
parts.push(bytes); byteLength += bytes.length;
return view;
}
async function verified(file, expected) {
const absolute = path.resolve(bundleDirectory, file);
if (!absolute.startsWith(`${path.resolve(bundleDirectory)}${path.sep}`)) throw new Error("Source path escapes bundle.");
const bytes = await readFile(absolute);
if (hash(bytes) !== expected) throw new Error(`Source hash mismatch: ${file}`);
return bytes;
}
json.textures ??= []; json.samplers ??= []; json.images ??= [];
const repeat = json.samplers.push({ wrapS: 10497, wrapT: 10497, minFilter: 9987, magFilter: 9729 }) - 1;
const clamp = json.samplers.push({ wrapS: 33071, wrapT: 33071, minFilter: 9987, magFilter: 9729 }) - 1;
json.extensions ??= {};
json.extensions.KHR_materials_variants ??= { variants: [] };
json.extensionsUsed = [...new Set([...(json.extensionsUsed ?? []), "KHR_materials_variants"])];
const variants = json.extensions.KHR_materials_variants.variants;
const carriers = new Map();
async function carrier(name, file, sha256, weight) {
if (carriers.has(name)) throw new Error("Duplicate carrier.");
const bytes = await verified(file, sha256);
const image = json.images.push({ name, mimeType: "image/png", bufferView: append(bytes) }) - 1;
const texture = json.textures.push({ source: image, sampler: weight ? clamp : repeat }) - 1;
const material = json.materials.push({ name, pbrMetallicRoughness: { metallicFactor: 0, roughnessFactor: 1,
...(!weight ? { baseColorTexture: { index: texture } } : {}) }, ...(weight ? { occlusionTexture: { index: texture } } : {}) }) - 1;
const variant = variants.push({ name }) - 1;
carriers.set(name, { material, variants: [variant] });
}
for (const texture of manifest.textures) await carrier(texture.variant, texture.decodedPng, texture.decodedPngSha256, false);
for (const weight of manifest.weightCarriers) await carrier(weight.variant, weight.image, weight.sha256, true);
json.meshes.splice(oldMesh, 1);
for (const node of json.nodes) if (node.mesh > oldMesh) node.mesh -= 1;
delete target.mesh; delete target.translation; delete target.rotation; delete target.scale;
target.matrix = manifest.sourceGeometry.nodeWorldMatrix;
target.children = [];
const definitions = new Map(manifest.materials.map(material => [material.name, material]));
for (const chunk of manifest.chunks) {
const definition = definitions.get(chunk.material);
const material = json.materials.push({ name: definition.name, doubleSided: true, pbrMetallicRoughness: { metallicFactor: 0, roughnessFactor: 1 },
extras: { wowTerrain: definition.wowTerrain, sourceChunk: { indexX: chunk.indexX, indexY: chunk.indexY } } }) - 1;
async function accessor(record, type, componentType, targetKind, bounds = false) {
const bytes = await verified(record.file, record.sha256);
if (bytes.length !== record.bytes) throw new Error("Attribute size mismatch.");
const arity = type === "SCALAR" ? 1 : type === "VEC2" ? 2 : 3;
const value = { bufferView: append(bytes, targetKind), componentType, type, count: record.count / arity };
if (bounds) {
value.min = Array(arity).fill(Infinity); value.max = Array(arity).fill(-Infinity);
for (let i = 0; i < record.count; i++) {
const v = bytes.readFloatLE(i * 4); const k = i % arity;
value.min[k] = Math.min(value.min[k], v); value.max[k] = Math.max(value.max[k], v);
}
}
return json.accessors.push(value) - 1;
}
const geometry = chunk.geometry;
const attributes = {
POSITION: await accessor(geometry.position, "VEC3", 5126, 34962, true),
NORMAL: await accessor(geometry.normal, "VEC3", 5126, 34962),
TEXCOORD_0: await accessor(geometry.uv0, "VEC2", 5126, 34962),
};
const mappings = [...definition.wowTerrain.layerVariants, ...definition.wowTerrain.weightVariants].map(name => {
const mapping = carriers.get(name); if (!mapping) throw new Error(`Missing ${name}`); return mapping;
});
const mesh = json.meshes.push({ name: definition.name, primitives: [{ attributes, material,
indices: await accessor(geometry.indices, "SCALAR", 5123, 34963),
extensions: { KHR_materials_variants: { mappings } } }] }) - 1;
target.children.push(json.nodes.push({ name: definition.name, mesh }) - 1);
}
json.buffers[0].byteLength = byteLength;
const binary = Buffer.concat(parts);
const binaryPadding = (4 - binary.length % 4) % 4;
const jsonBytes = Buffer.from(JSON.stringify(json));
const jsonPadding = (4 - jsonBytes.length % 4) % 4;
const result = Buffer.alloc(28 + jsonBytes.length + jsonPadding + binary.length + binaryPadding);
result.write("glTF"); result.writeUInt32LE(2, 4); result.writeUInt32LE(result.length, 8);
result.writeUInt32LE(jsonBytes.length + jsonPadding, 12); result.writeUInt32LE(0x4e4f534a, 16);
jsonBytes.copy(result, 20); result.fill(0x20, 20 + jsonBytes.length, 20 + jsonBytes.length + jsonPadding);
const offset = 20 + jsonBytes.length + jsonPadding;
result.writeUInt32LE(binary.length + binaryPadding, offset); result.writeUInt32LE(0x004e4942, offset + 4);
binary.copy(result, offset + 8);
await writeFile(output, result);
const report = { status: "diagnostic-only", input: { path: input, sha256: hash(original), bytes: original.length },
output: { path: output, sha256: hash(result), bytes: result.length }, sourceAdt: manifest.sourceAdt,
sourceBundle: bundleDirectory, chunkCount: manifest.chunks.length, recoveredTriangleCount: manifest.sourceGeometry.indexCount / 3,
originalBinaryPrefixUnchanged: binary.subarray(0, originalBinary.length).equals(originalBinary),
originalImagesUnchanged: true, sourceTextures: manifest.textures.length, weightImages: manifest.weightCarriers.length,
compression: "original payloads retained; new source textures are lossless PNG and disjoint geometry is float32/uint16",
limits: "One source-backed tile. No original-client color/shadow reconstruction or fleet publication." };
await writeFile(`${output}.report.json`, `${JSON.stringify(report, null, 2)}\n`);
console.log(JSON.stringify(report, null, 2));