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); });