import path from "node:path"; export const TREKKER_WATER_FOLIAGE_EXCLUSION_MASK = 0x00a00000; export function normalizeNativeResourcePath(resource) { if (typeof resource !== "string" || !resource.trim()) { throw new Error("Native collision resource path must be a non-empty string."); } const normalized = resource.replaceAll("/", "\\").replace(/^\\+/, ""); const parts = normalized.split("\\"); if (parts.some((part) => !part || part === "." || part === "..")) { throw new Error(`Unsafe native collision resource path: ${resource}`); } return parts.join("\\"); } export function nativeResourceFile(resourceRoot, resource) { const normalized = normalizeNativeResourcePath(resource); const file = path.resolve(resourceRoot, ...normalized.split("\\")); const relative = path.relative(path.resolve(resourceRoot), file); if (path.isAbsolute(relative) || relative === ".." || relative.startsWith(`..${path.sep}`)) { throw new Error(`Native resource escaped the resource root: ${resource}`); } return file; } export function transformNativeCollisionPoint(source, columnMajorMatrix, metersPerSourceUnit) { if (!Array.isArray(source) && !ArrayBuffer.isView(source)) throw new Error("Source point must contain three coordinates."); if (source.length !== 3 || !Array.isArray(columnMajorMatrix) || columnMajorMatrix.length !== 16) { throw new Error("Native collision transform requires a three-value point and 16-value matrix."); } if (!Number.isFinite(metersPerSourceUnit) || metersPerSourceUnit <= 0) { throw new Error("Meters per source unit must be a finite positive number."); } const x = Number(source[0]) * metersPerSourceUnit; const y = Number(source[1]) * metersPerSourceUnit; const z = -Number(source[2]) * metersPerSourceUnit; const point = [ columnMajorMatrix[0] * x + columnMajorMatrix[4] * y + columnMajorMatrix[8] * z + columnMajorMatrix[12], columnMajorMatrix[1] * x + columnMajorMatrix[5] * y + columnMajorMatrix[9] * z + columnMajorMatrix[13], columnMajorMatrix[2] * x + columnMajorMatrix[6] * y + columnMajorMatrix[10] * z + columnMajorMatrix[14], ]; if (point.some((value) => !Number.isFinite(value))) throw new Error("Native collision placement produced a non-finite point."); return point; } export function transformNativeCollisionTriangle(sourcePoints, columnMajorMatrix, metersPerSourceUnit) { if (!Array.isArray(sourcePoints) || sourcePoints.length !== 3) { throw new Error("Native collision triangle must contain exactly three points."); } const [a, b, c] = sourcePoints.map((point) => ( transformNativeCollisionPoint(point, columnMajorMatrix, metersPerSourceUnit) )); // The source-to-Three bridge reflects Z. Reverse winding exactly once here. return [a, c, b]; } export function triangleTileKey(points, tileSizeMeters) { if (!Number.isFinite(tileSizeMeters) || tileSizeMeters <= 0) throw new Error("Tile size must be finite and positive."); const centerX = (points[0][0] + points[1][0] + points[2][0]) / 3; const centerZ = (points[0][2] + points[1][2] + points[2][2]) / 3; return `${Math.floor(centerX / tileSizeMeters)}:${Math.floor(centerZ / tileSizeMeters)}`; } export function isTrekkerPathEligible(triangleFlag) { return ((Number(triangleFlag) >>> 0) & TREKKER_WATER_FOLIAGE_EXCLUSION_MASK) === 0; } export class NativeCollisionPartAccumulator { constructor(maxTriangles) { if (!Number.isSafeInteger(maxTriangles) || maxTriangles < 1 || maxTriangles > 90_000) { throw new Error("Native collision GLB part size must be an integer from 1 to 90,000 triangles."); } this.maxTriangles = maxTriangles; this.positions = new Float32Array(maxTriangles * 9); this.flags = []; this.flagCounts = new Map(); } get triangleCount() { return this.flags.length; } add(points, flag) { if (this.triangleCount >= this.maxTriangles) throw new Error("Flush the full native collision part before adding another triangle."); if (!Array.isArray(points) || points.length !== 3 || points.some((point) => point.length !== 3 || [...point].some((value) => !Number.isFinite(Number(value))))) { throw new Error("Native collision part requires a finite 3D triangle."); } const offset = this.triangleCount * 9; this.positions.set([...points[0], ...points[1], ...points[2]], offset); const normalizedFlag = Number(flag) >>> 0; this.flags.push(normalizedFlag); this.flagCounts.set(normalizedFlag, (this.flagCounts.get(normalizedFlag) ?? 0) + 1); return this.triangleCount === this.maxTriangles ? this.finish() : null; } finish() { if (this.triangleCount === 0) return null; const result = { triangleCount: this.triangleCount, positions: this.positions.slice(0, this.triangleCount * 9), sourceTriangleFlags: this.flags.slice(), flagCounts: Object.fromEntries([...this.flagCounts.entries()].sort((a, b) => a[0] - b[0]).map(([flag, count]) => [String(flag), count])), }; this.positions = new Float32Array(this.maxTriangles * 9); this.flags = []; this.flagCounts = new Map(); return result; } } export function validateNativeCollisionCounts({ triangleCount, flagBytes, positionVertexCount }) { if (!Number.isSafeInteger(triangleCount) || triangleCount < 0) throw new Error("Native collision report has an invalid triangle count."); if (flagBytes !== triangleCount * 4) { throw new Error(`Native triangle flags contain ${flagBytes} bytes, expected ${triangleCount * 4}.`); } if (positionVertexCount !== triangleCount * 3) { throw new Error(`Native collision OBJ contains ${positionVertexCount} vertices, expected ${triangleCount * 3}.`); } } export function renderMeshOmissions(report) { const omitted = []; for (const entity of report.entityNodes ?? []) { for (const [meshIndex, kind] of (entity.meshCollisionTypes ?? []).entries()) { if (kind === "unsupported mesh class") { omitted.push({ entity: entity.name, entityType: entity.type, meshIndex, reason: "unsupported mesh class in native collision traversal", entityCollisionObjectReplacesChildren: entity.hasCollisionObject, }); } else if (String(kind).startsWith("none(serialized)")) { omitted.push({ entity: entity.name, entityType: entity.type, meshIndex, reason: "no serialized mesh collision structure; render-mesh fallback is excluded", entityCollisionObjectReplacesChildren: entity.hasCollisionObject, }); } } } return omitted; }