157 lines
6.4 KiB
JavaScript
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);
|
|
});
|