Files
healer-man/scripts/runewaker-pipeline/native-collision-assembly.test.mjs
2026-10-06 22:25:34 -04:00

157 lines
6.4 KiB
JavaScript

import assert from "node:assert/strict";
import { mkdir, mkdtemp, readFile, rm, writeFile } from "node:fs/promises";
import os from "node:os";
import path from "node:path";
import test from "node:test";
import {
isTrekkerPathEligible,
NativeCollisionPartAccumulator,
nativeResourceFile,
normalizeNativeResourcePath,
transformNativeCollisionTriangle,
} from "./lib/native-collision-assembly.mjs";
import {
readAndValidateNativeCollisionInventory,
sha256File,
writeNativeCollisionInventoryManifest,
} from "./lib/native-collision-inventory.mjs";
test("native collision points apply the Z reflection, placement matrix, and one winding reversal", () => {
const placement = [
2, 0, 0, 0,
0, 3, 0, 0,
0, 0, 4, 0,
7, 11, 13, 1,
];
const triangle = transformNativeCollisionTriangle(
[[1, 0, 0], [0, 1, 0], [0, 0, 1]],
placement,
0.1,
);
assert.deepEqual(triangle, [
[7.2, 11, 13],
[7, 11, 12.6],
[7, 11.3, 13],
]);
});
test("resource paths stay rooted and reject traversal", () => {
assert.equal(normalizeNativeResourcePath("/Model/Dungeon/Test/Room.ROS"), "Model\\Dungeon\\Test\\Room.ROS");
assert.throws(() => normalizeNativeResourcePath("model\\..\\outside.ros"), /Unsafe/);
assert.equal(path.basename(nativeResourceFile("F:\\resource-root", "model/dungeon/test.ros")), "test.ros");
});
test("collision part accumulator preserves triangle order and flags across the hard part boundary", () => {
const writer = new NativeCollisionPartAccumulator(2);
assert.equal(writer.add([[0, 0, 0], [1, 0, 0], [0, 1, 0]], 0x00100000), null);
const first = writer.add([[2, 0, 0], [3, 0, 0], [2, 1, 0]], 0x00200000);
assert.equal(first.triangleCount, 2);
assert.deepEqual(first.sourceTriangleFlags, [0x00100000, 0x00200000]);
assert.deepEqual(first.flagCounts, { "1048576": 1, "2097152": 1 });
assert.deepEqual([...first.positions.slice(0, 9)], [0, 0, 0, 1, 0, 0, 0, 1, 0]);
assert.deepEqual([...first.positions.slice(9, 18)], [2, 0, 0, 3, 0, 0, 2, 1, 0]);
writer.add([[4, 0, 0], [5, 0, 0], [4, 1, 0]], 0x00800000);
const last = writer.finish();
assert.equal(last.triangleCount, 1);
assert.deepEqual(last.sourceTriangleFlags, [0x00800000]);
assert.equal(writer.finish(), null);
assert.throws(() => new NativeCollisionPartAccumulator(90_001), /1 to 90,000/);
});
test("Trekker mask excludes water and foliage bits while preserving other native flags", () => {
assert.equal(isTrekkerPathEligible(0), true);
assert.equal(isTrekkerPathEligible(0x00100000), true);
assert.equal(isTrekkerPathEligible(0x00200000), false);
assert.equal(isTrekkerPathEligible(0x00800000), false);
assert.equal(isTrekkerPathEligible(0x00400000), true);
assert.equal(isTrekkerPathEligible(0x00a00000), false);
});
test("inventory index binds each native report and sidecar to exact placement manifest and source ROS hashes", async (t) => {
const root = await mkdtemp(path.join(os.tmpdir(), "runewaker-native-collision-"));
t.after(() => rm(root, { recursive: true, force: true }));
const sourceRoot = path.join(root, "source");
const inventoryRoot = path.join(root, "inventory");
const sourceFile = path.join(sourceRoot, "model", "test", "room.ros");
const reportFile = path.join(inventoryRoot, "room.json");
const objFile = `${reportFile}.obj`;
const flagsFile = `${reportFile}.flags.bin`;
await mkdir(path.dirname(sourceFile), { recursive: true });
await mkdir(inventoryRoot, { recursive: true });
await writeFile(sourceFile, Buffer.from("native ROS bytes"));
const report = {
schemaVersion: 2,
sourceModel: "model\\test\\room.ros",
collisionMode: "complete",
nativeCollisionTriangleCount: 1,
completeNativeCollisionTriangleCount: 1,
enabledNativeCollisionTriangleCount: 1,
disabledSubtreeNativeTriangles: 0,
disabledCollisionOptionEntityCount: 0,
renderMeshFallbackTriangleCount: 3,
};
await writeFile(reportFile, JSON.stringify(report));
await writeFile(objFile, "v 0 0 0\nv 1 0 0\nv 0 1 0\nf 1 2 3\n");
await writeFile(flagsFile, Buffer.from([0, 0, 16, 0]));
const sourceResourceSha256 = await sha256File(sourceFile);
const nativeExtractor = { executableSha256: "c".repeat(64), sourceSha256: "d".repeat(64), collisionMode: "complete" };
const created = await writeNativeCollisionInventoryManifest({
inventoryRoot,
placementManifestSha256: "a".repeat(64),
sourceWdbSha256: "b".repeat(64),
collisionMode: "complete",
nativeExtractor,
sourceResourceRoot: sourceRoot,
resources: [{
resource: "model\\test\\room.ros",
sourceResourceSha256,
report,
reportFile,
objFile,
flagsFile,
}],
});
assert.match(created.sha256, /^[a-f\d]{64}$/);
const verified = await readAndValidateNativeCollisionInventory({
inventoryRoot,
placementManifestSha256: "a".repeat(64),
sourceWdbSha256: "b".repeat(64),
collisionMode: "complete",
nativeExtractor,
expectedResources: ["model/test/room.ros"],
sourceResourceRoot: sourceRoot,
});
assert.equal(verified.resources.size, 1);
assert.equal(verified.inventory.collisionMode, "complete");
await assert.rejects(readAndValidateNativeCollisionInventory({
inventoryRoot,
placementManifestSha256: "a".repeat(64),
sourceWdbSha256: "b".repeat(64),
collisionMode: "respect-options",
nativeExtractor: { ...nativeExtractor, collisionMode: "respect-options" },
expectedResources: ["model/test/room.ros"],
sourceResourceRoot: sourceRoot,
}), /mode complete does not match requested mode respect-options/);
await assert.rejects(readAndValidateNativeCollisionInventory({
inventoryRoot,
placementManifestSha256: "a".repeat(64),
sourceWdbSha256: "b".repeat(64),
collisionMode: "complete",
nativeExtractor: { ...nativeExtractor, executableSha256: "e".repeat(64) },
expectedResources: ["model/test/room.ros"],
sourceResourceRoot: sourceRoot,
}), /executableSha256 does not match/);
await writeFile(sourceFile, Buffer.from("changed ROS bytes"));
await assert.rejects(readAndValidateNativeCollisionInventory({
inventoryRoot,
placementManifestSha256: "a".repeat(64),
sourceWdbSha256: "b".repeat(64),
collisionMode: "complete",
nativeExtractor,
expectedResources: ["model/test/room.ros"],
sourceResourceRoot: sourceRoot,
}), /Source ROS SHA-256 mismatch/);
const indexed = JSON.parse(await readFile(created.file, "utf8"));
assert.equal(indexed.resources[0].renderMeshFallbackTriangleCount, 3);
});