Files
healer-man/scripts/dungeon-pipeline/wow-native-wmo-audit.test.mjs
2026-10-07 13:05:04 -04:00

247 lines
9.3 KiB
JavaScript

import test from "node:test";
import assert from "node:assert/strict";
import { createHash } from "node:crypto";
import {
assertMatchingRootDigests,
auditWmoAsset,
documentedFixColorVertexAlpha,
mapExactObjMocv,
parseGroupWmo,
parseRootWmo,
sourceToObjPoint,
} from "./wow-native-wmo-audit.mjs";
function reversedChunk(id, payload) {
const header = Buffer.alloc(8);
header.write(id.split("").reverse().join(""), 0, "ascii");
header.writeUInt32LE(payload.length, 4);
return Buffer.concat([header, payload]);
}
function floats(values) {
const result = Buffer.alloc(values.length * 4);
values.forEach((value, index) => result.writeFloatLE(value, index * 4));
return result;
}
function makeLightRow() {
const row = Buffer.alloc(48);
row.set([0, 1, 1, 1, 230, 160, 90, 255], 0);
[2, 3, 4].forEach((value, index) => row.writeFloatLE(value, 8 + index * 4));
row.writeFloatLE(0.65, 20);
row.writeFloatLE(0.5, 24);
row.writeFloatLE(8, 28);
return row;
}
function makeRoot({ flags = 0, lightPayload = makeLightRow(), ambient = [20, 30, 40, 255] } = {}) {
const mver = Buffer.alloc(4);
mver.writeUInt32LE(17);
const mohd = Buffer.alloc(64);
mohd.writeUInt32LE(1, 4); // one group
mohd.writeUInt32LE(lightPayload.length / 48, 12);
mohd.set(ambient, 28);
mohd.writeUInt32LE(723, 32);
floats([-10, -20, -30, 10, 20, 30]).copy(mohd, 36);
mohd.writeUInt16LE(flags, 60);
const mogi = Buffer.alloc(32);
mogi.writeUInt32LE(0x2000, 0);
floats([-1, -1, -1, 1, 1, 1]).copy(mogi, 4);
mogi.writeInt32LE(1, 28);
return Buffer.concat([
reversedChunk("MVER", mver),
reversedChunk("MOHD", mohd),
reversedChunk("MOGN", Buffer.from("\0JailRoom\0Interior\0", "latin1")),
reversedChunk("MOGI", mogi),
reversedChunk("MOLT", lightPayload),
]);
}
function makeGroup({
flags = 0x2204,
colors = [[100, 90, 80, 0], [110, 100, 90, 0], [120, 110, 100, 0]],
refs = [0],
batchLastVertex = 0,
} = {}) {
const positions = [[1, 2, 3], [4, 5, 6], [7, 8, 9]];
const normals = [[0, 1, 0], [0, 0, 1], [1, 0, 0]];
const uvs = [[0.25, 0.75], [0.5, 0.25], [0.125, 0.875]];
const header = Buffer.alloc(68);
header.writeUInt32LE(1, 0);
header.writeUInt32LE(10, 4);
header.writeUInt32LE(flags, 8);
floats([-10, -20, -30, 10, 20, 30]).copy(header, 12);
header.writeUInt16LE(1, 40); // batch A
header.writeUInt16LE(1, 42); // interior batch
header.writeUInt16LE(0, 44); // exterior batch
const nested = [
reversedChunk("MOVT", floats(positions.flat())),
reversedChunk("MONR", floats(normals.flat())),
reversedChunk("MOTV", floats(uvs.flat())),
reversedChunk("MOLR", Buffer.from(refs.flatMap((value) => {
const row = Buffer.alloc(2);
row.writeInt16LE(value);
return [...row];
}))),
reversedChunk("MOCV", Buffer.from(colors.flat())),
reversedChunk("MOBA", (() => {
const batch = Buffer.alloc(24);
batch.writeUInt16LE(batchLastVertex, 20);
return batch;
})()),
];
const version = Buffer.alloc(4);
version.writeUInt32LE(17);
return {
bytes: Buffer.concat([reversedChunk("MVER", version), reversedChunk("MOGP", Buffer.concat([header, ...nested]))]),
positions,
normals,
uvs,
colors,
};
}
function makeObj({ positions, normals, uvs }) {
const lines = [];
for (const [x, y, z] of positions) lines.push(`v ${x} ${z} ${-y}`);
for (const [x, y, z] of normals) lines.push(`vn ${x} ${z} ${-y}`);
for (const [u, v] of uvs) lines.push(`vt ${u} ${1 - v}`);
return `${lines.join("\n")}\n`;
}
function digest(bytes) {
return createHash("sha256").update(bytes).digest("hex");
}
test("WMO v17 parsers decode MOLT rows, MOGP flags, batches, MOLR, and raw MOCV bytes", () => {
const rootBytes = makeRoot();
const group = makeGroup();
const root = parseRootWmo(rootBytes);
const parsedGroup = parseGroupWmo(group.bytes, 0);
assert.equal(root.header.groupCount, 1);
assert.equal(root.header.lightCount, 1);
assert.deepEqual(root.header.ambientColorBytes, [20, 30, 40, 255]);
assert.deepEqual(root.lights[0], {
index: 0,
lightType: 0,
secondaryType: 1,
useAttenuation: 1,
padding: 1,
colorBytes: [230, 160, 90, 255],
position: [2, 3, 4],
intensity: Math.fround(0.65),
attenuationStart: 0.5,
attenuationEnd: 8,
unknownFloats: [0, 0, 0, 0],
});
assert.equal(parsedGroup.header.flags, 0x2204);
assert.deepEqual(parsedGroup.header.batchCounts, { unknownA: 1, interior: 1, exterior: 0, unknownD: 0 });
assert.equal(parsedGroup.hasLightFlag, true);
assert.equal(parsedGroup.indoorFlag, true);
assert.deepEqual(parsedGroup.lightRefs, [0]);
assert.deepEqual(parsedGroup.vertices[0].mocvBytes, [100, 90, 80, 0]);
});
test("source coordinate basis is exact WMO (x,y,z) to OBJ (x,z,-y)", () => {
assert.deepEqual(sourceToObjPoint([2, 3, 4]), [2, 4, -3]);
});
test("exact MOCV mapping uses full position, normal, and UV signatures and preserves raw BGRA", () => {
const group = makeGroup();
const obj = makeObj(group);
const mapping = mapExactObjMocv(parseRootWmo(makeRoot()), [parseGroupWmo(group.bytes)], obj);
assert.equal(mapping.exactCompleteMapping, true);
assert.equal(mapping.mappedVertexCount, 3);
assert.equal(mapping.ambiguousVertexCount, 0);
assert.equal(mapping.unmatchedVertexCount, 0);
assert.equal(mapping.rawMocvByteOrder, "rawColorBytesByObjVertex preserves BGRA storage bytes exactly; alpha semantics are deliberately not applied");
assert.deepEqual([...mapping.rawColorBytesByObjVertex], group.colors.flat());
assert.equal(mapping.documentedFixColorAlphaMapping.exactCompleteMapping, true);
assert.deepEqual([...mapping.documentedFixColorAlphaMapping.colorsByObjVertex].slice(3, 4), [0]);
assert.deepEqual(mapping.rawAlphaHistogram, { 0: 3 });
});
test("documented MOCV fixup treats alpha as lighting data and rewrites indoor/outdoor alpha, never opacity", () => {
const root = parseRootWmo(makeRoot());
const colors = [[110, 90, 70, 128], [100, 80, 60, 64], [40, 50, 60, 0]];
const indoor = parseGroupWmo(makeGroup({ flags: 0x2204, colors }).bytes);
const outdoor = parseGroupWmo(makeGroup({ flags: 0x220c, colors }).bytes);
const indoorResult = documentedFixColorVertexAlpha(root, indoor);
const outdoorResult = documentedFixColorVertexAlpha(root, outdoor);
assert.equal(indoorResult.secondBatchVertexStart, 1);
assert.deepEqual([...indoorResult.rawColorBytes], colors.flat());
assert.deepEqual([...indoorResult.adaptedColorBytes], [22, 14, 7, 128, 90, 65, 40, 0, 10, 10, 10, 0]);
assert.deepEqual([...outdoorResult.adaptedColorBytes], [22, 14, 7, 128, 90, 65, 40, 255, 10, 10, 10, 255]);
assert.match(indoorResult.alphaSemantics, /not opacity/u);
assert.equal(indoorResult.productionStatus.includes("diagnostic-only"), true);
});
test("documented MOHD 0x0008 path is marked disputed and does not claim production colors", () => {
const root = parseRootWmo(makeRoot({ flags: 0x8 }));
const group = parseGroupWmo(makeGroup({ flags: 0x2204, colors: [[100, 80, 60, 64], [50, 40, 30, 32], [20, 10, 5, 7]] }).bytes);
const result = documentedFixColorVertexAlpha(root, group);
assert.equal(result.fixupPath, "documented-special-mohd-0x0008-disputed");
assert.deepEqual([...result.adaptedColorBytes], [100, 80, 60, 64, 50, 40, 30, 0, 20, 10, 5, 0]);
assert.match(result.productionStatus, /diagnostic-only/u);
});
test("exact mapping fails closed on changed geometry instead of nearest-vertex guessing", () => {
const group = makeGroup();
const obj = makeObj(group).replace("v 1 3 -2", "v 1.0001 3 -2");
const mapping = mapExactObjMocv(parseRootWmo(makeRoot()), [parseGroupWmo(group.bytes)], obj);
assert.equal(mapping.exactCompleteMapping, false);
assert.equal(mapping.unmatchedVertexCount, 1);
});
test("group data is rejected unless it is paired with the byte-identical expected root", () => {
const rootBytes = makeRoot();
const expected = digest(rootBytes);
assert.deepEqual(assertMatchingRootDigests({
rootBytes,
groupCacheRootBytes: Buffer.from(rootBytes),
expectedRootSha256: expected,
}), { rootSha256: expected, groupCacheRootSha256: expected });
const differentRoot = Buffer.from(rootBytes);
differentRoot[differentRoot.length - 1] ^= 1;
assert.throws(() => assertMatchingRootDigests({
rootBytes,
groupCacheRootBytes: differentRoot,
expectedRootSha256: expected,
}), /not byte-identical/u);
assert.throws(() => assertMatchingRootDigests({
rootBytes,
groupCacheRootBytes: rootBytes,
expectedRootSha256: "",
}), /expected 64-character/u);
});
test("MOLT references are eligible only with HasLight and without DoNotUseLocalLighting", () => {
const rootBytes = makeRoot();
const group = makeGroup({ flags: 0x2244 });
const result = auditWmoAsset({
rootBytes,
groupCacheRootBytes: rootBytes,
expectedRootSha256: digest(rootBytes),
groupBuffers: [group.bytes],
objText: makeObj(group),
});
assert.equal(result.lighting.totalMoltRows, 1);
assert.equal(result.lighting.groupCountWithHasLightAndMolr, 0);
assert.equal(result.lighting.eligibleMolrReferenceCount, 0);
assert.deepEqual(result.lighting.eligibleUniqueMoltRows, []);
assert.match(result.lighting.runtimeLightClaim, /no Three\.js dynamic lights/u);
assert.match(result.sourceGeometry.mocv.alphaSemantics, /not glTF opacity/u);
});
test("malformed MOLT rows are rejected", () => {
assert.throws(() => parseRootWmo(makeRoot({ lightPayload: Buffer.alloc(47) })), /not divisible/u);
});