247 lines
9.3 KiB
JavaScript
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);
|
|
});
|