189 lines
6.9 KiB
JavaScript
189 lines
6.9 KiB
JavaScript
import assert from "node:assert/strict";
|
|
import { deflateSync } from "node:zlib";
|
|
import test from "node:test";
|
|
import {
|
|
dxt1ToRgba,
|
|
encodeChunkKey,
|
|
intersectingChunkIndices,
|
|
parseTerrainChunk,
|
|
readDataStore,
|
|
wrapDxt1AsDds,
|
|
} from "./extract-native-terrain.mjs";
|
|
|
|
function i32(value) {
|
|
const buffer = Buffer.alloc(4);
|
|
buffer.writeInt32LE(value);
|
|
return buffer;
|
|
}
|
|
|
|
function u32(value) {
|
|
const buffer = Buffer.alloc(4);
|
|
buffer.writeUInt32LE(value);
|
|
return buffer;
|
|
}
|
|
|
|
function f32(value) {
|
|
const buffer = Buffer.alloc(4);
|
|
buffer.writeFloatLE(value);
|
|
return buffer;
|
|
}
|
|
|
|
function makeChunk({ dimension = 2, visibleVertices = null, waterLayerCount = 0, waterPayload = Buffer.alloc(0) } = {}) {
|
|
const count = (dimension + 1) ** 2 + (dimension + 1) * 4;
|
|
const classGuid = Buffer.alloc(16);
|
|
const heights = Buffer.concat(Array.from({ length: count }, (_, index) => f32(index / 10)));
|
|
const vertexData = Buffer.concat(Array.from({ length: count }, (_, index) => {
|
|
const visible = visibleVertices === null || visibleVertices.has(index);
|
|
const alpha = visible ? 255 : 0;
|
|
return u32(((alpha << 24) | (191 << 16) | (128 << 8) | 255) >>> 0);
|
|
}));
|
|
const bounds = Buffer.concat([
|
|
f32(-960), f32(-2), f32(-960),
|
|
f32(-945), f32(2), f32(-945),
|
|
]);
|
|
const mapDxt1 = Buffer.alloc(8);
|
|
mapDxt1.writeUInt16LE(0xffff, 0);
|
|
mapDxt1.writeUInt16LE(0x0000, 2);
|
|
const layerIds = Buffer.concat([u32(1).subarray(0, 2), u32(3).subarray(0, 2), u32(9).subarray(0, 2)]);
|
|
const bytes = Buffer.concat([
|
|
classGuid,
|
|
i32(4), // chunk iteration
|
|
i32(dimension),
|
|
f32(7.5),
|
|
i32(4),
|
|
bounds,
|
|
f32(-960), f32(-960),
|
|
heights,
|
|
vertexData,
|
|
i32(128), // splat resolution
|
|
classGuid,
|
|
i32(1), // compressed-alpha map type
|
|
i32(4), // splat map dimension
|
|
i32(3),
|
|
layerIds,
|
|
i32(1), // alpha maps
|
|
i32(1), // mip count
|
|
u32(4), u32(4),
|
|
mapDxt1,
|
|
i32(0), // doodad clusters
|
|
i32(waterLayerCount), // water layers
|
|
waterPayload,
|
|
]);
|
|
return deflateSync(bytes);
|
|
}
|
|
|
|
function makeDataStoreFixture() {
|
|
const tablePosition = 28;
|
|
const recordHeaderPosition = tablePosition + 272;
|
|
const recordPayloadPosition = recordHeaderPosition + 16;
|
|
const payload = Buffer.from([1, 2, 3, 4]);
|
|
const buffer = Buffer.alloc(recordPayloadPosition + payload.length);
|
|
buffer.writeInt32LE(tablePosition, 16);
|
|
buffer.writeInt32LE(1, 20);
|
|
buffer.writeInt32LE(32, 24);
|
|
buffer.write("header", tablePosition, "ascii");
|
|
buffer.writeInt32LE(4, tablePosition + 256);
|
|
buffer.writeInt32LE(recordHeaderPosition, tablePosition + 260);
|
|
buffer.writeInt32LE(1, tablePosition + 264);
|
|
buffer.writeInt32LE(64, tablePosition + 268);
|
|
buffer.writeUInt32LE(0, recordHeaderPosition);
|
|
buffer.writeInt32LE(recordPayloadPosition, recordHeaderPosition + 4);
|
|
buffer.writeInt32LE(payload.length, recordHeaderPosition + 8);
|
|
buffer.writeInt32LE(64, recordHeaderPosition + 12);
|
|
payload.copy(buffer, recordPayloadPosition);
|
|
return buffer;
|
|
}
|
|
|
|
test("terrain chunk parser preserves source geometry, visibility, normals, and splat layer IDs", () => {
|
|
const parsed = parseTerrainChunk(makeChunk({ visibleVertices: new Set([0]) }), "test");
|
|
|
|
assert.equal(parsed.dimension, 2);
|
|
assert.equal(parsed.localPositions.length, 27);
|
|
assert.deepEqual([...parsed.normalizedUvs.slice(0, 6)], [0, 0, 0.5, 0, 1, 0]);
|
|
assert.deepEqual([...parsed.layerIds], [1, 3, 9]);
|
|
assert.deepEqual([...parsed.triangleVisibility], [1, 1, 0, 0, 0, 0, 0, 0]);
|
|
assert.deepEqual([...parsed.indices], [0, 3, 4, 0, 4, 1, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0]);
|
|
assert.deepEqual([...parsed.cellVisibility], [1, 0, 0, 0]);
|
|
const expectedNormal = [
|
|
(191 / 255 - 0.5) * 2,
|
|
(128 / 255 - 0.5) * 2,
|
|
1,
|
|
];
|
|
for (let axis = 0; axis < 3; axis += 1) {
|
|
assert.ok(Math.abs(parsed.nativeNormals[axis] - expectedNormal[axis]) < 1e-6);
|
|
}
|
|
assert.equal(parsed.splatMaps[0].mipmaps[0].bytes.length, 8);
|
|
assert.equal(parsed.doodadClusterCount, 0);
|
|
assert.equal(parsed.waterLayerCount, 0);
|
|
assert.equal(parsed.unconsumedBytes, 0);
|
|
});
|
|
|
|
test("terrain topology retains every face slot and degenerates fully masked triangles", () => {
|
|
const parsed = parseTerrainChunk(makeChunk({ visibleVertices: new Set() }), "masked-test");
|
|
|
|
assert.equal(parsed.indices.length / 3, 8);
|
|
assert.equal(parsed.triangleVisibility.reduce((sum, value) => sum + value, 0), 0);
|
|
assert.ok(parsed.indices.every((value) => value === 0));
|
|
assert.ok(parsed.cellVisibility.every((value) => value === 0));
|
|
});
|
|
|
|
test("water-bearing WDB chunks can export their unchanged ground heightfield when explicitly enabled", () => {
|
|
const source = makeChunk({ waterLayerCount: 1, waterPayload: Buffer.from([0x51, 0x52, 0x53, 0x54]) });
|
|
assert.throws(() => parseTerrainChunk(source, "wet-fixture"), /allowUnparsedWaterPayload/);
|
|
|
|
const parsed = parseTerrainChunk(source, "wet-fixture", { allowUnparsedWaterPayload: true });
|
|
assert.equal(parsed.waterLayerCount, 1);
|
|
assert.equal(parsed.heights[0], 0);
|
|
assert.deepEqual([...parsed.triangleVisibility], [1, 1, 1, 1, 1, 1, 1, 1]);
|
|
assert.equal(parsed.unconsumedBytes, 4);
|
|
});
|
|
|
|
test("Rune disk datastore table and record headers are read using native offsets", () => {
|
|
const datastore = readDataStore(makeDataStoreFixture());
|
|
const header = datastore.tables.get("header");
|
|
const record = header.records.get("0");
|
|
|
|
assert.equal(datastore.tableCount, 1);
|
|
assert.equal(header.keyLength, 4);
|
|
assert.equal(header.count, 1);
|
|
assert.equal(record.position, 316);
|
|
assert.deepEqual([...record.key], [0, 0, 0, 0]);
|
|
});
|
|
|
|
test("native terrain chunk selection and DXT1 standalone DDS wrapper are deterministic", () => {
|
|
const selected = intersectingChunkIndices({
|
|
origin: [-7680, 0, -7680],
|
|
chunkDimension: 64,
|
|
gridSize: 7.5,
|
|
numXChunks: 32,
|
|
numZChunks: 32,
|
|
}, { min: [-664.98, 0, -716.26], max: [180.76, 0, 117.06] });
|
|
const dxt1 = Buffer.alloc(8);
|
|
dxt1.writeUInt16LE(0xffff, 0);
|
|
const rgba = dxt1ToRgba(dxt1, 4, 4);
|
|
const dds = wrapDxt1AsDds(4, 4, dxt1);
|
|
|
|
assert.equal(selected.length, 9);
|
|
assert.equal(selected[0].x, 14);
|
|
assert.equal(selected[0].z, 14);
|
|
assert.equal(encodeChunkKey(14, 14), 917518);
|
|
assert.deepEqual([...rgba.subarray(0, 4)], [255, 255, 255, 255]);
|
|
assert.equal(dds.toString("ascii", 0, 4), "DDS ");
|
|
assert.equal(dds.readUInt32LE(12), 4);
|
|
assert.equal(dds.readUInt32LE(16), 4);
|
|
assert.equal(dds.toString("ascii", 84, 88), "DXT1");
|
|
assert.deepEqual(dds.subarray(128), dxt1);
|
|
|
|
const rowPattern = Buffer.alloc(8);
|
|
rowPattern.writeUInt16LE(0xf800, 0); // red endpoint
|
|
rowPattern.writeUInt16LE(0x001f, 2); // blue endpoint
|
|
let packed = 0;
|
|
for (let pixel = 0; pixel < 16; pixel += 1) {
|
|
packed = (packed | ((pixel >>> 2) << (pixel * 2))) >>> 0;
|
|
}
|
|
rowPattern.writeUInt32LE(packed, 4);
|
|
const rowPixels = dxt1ToRgba(rowPattern, 4, 4);
|
|
assert.deepEqual([...rowPixels.subarray(0, 4)], [255, 0, 0, 255]);
|
|
assert.deepEqual([...rowPixels.subarray(4 * 4, 4 * 4 + 4)], [0, 0, 255, 255]);
|
|
});
|