import { createHash } from "node:crypto"; import { mkdir, readFile, writeFile } from "node:fs/promises"; import path from "node:path"; import { fileURLToPath, pathToFileURL } from "node:url"; import { getPositionsAndIndices } from "@recast-navigation/three"; const EXPECTED_GENERATOR_VERSION = "0.43.1"; const TILED_WATERSHED_CALL = "buildRegions(buildContext, compactHeightfield, tileConfig.borderSize, tileConfig.minRegionArea, tileConfig.mergeRegionArea)"; const TILED_MONOTONE_CALL = "buildRegionsMonotone(buildContext, compactHeightfield, tileConfig.borderSize, tileConfig.minRegionArea, tileConfig.mergeRegionArea)"; let generatorPromise; function sha256(value) { return createHash("sha256").update(value).digest("hex"); } async function loadPatchedGenerator() { const packageFile = fileURLToPath(import.meta.resolve("@recast-navigation/generators/package.json")); const packageJson = JSON.parse(await readFile(packageFile, "utf8")); if (packageJson.version !== EXPECTED_GENERATOR_VERSION) { throw new Error( `Monotone Recast fallback expects @recast-navigation/generators ${EXPECTED_GENERATOR_VERSION}, ` + `but found ${packageJson.version}.`, ); } const entryFile = fileURLToPath(import.meta.resolve("@recast-navigation/generators")); const upstream = await readFile(entryFile, "utf8"); if (!upstream.includes("buildRegions, allocContourSet")) { throw new Error("Monotone Recast fallback could not locate the pinned generator import."); } const watershedCallCount = upstream.split(TILED_WATERSHED_CALL).length - 1; if (watershedCallCount !== 1) { throw new Error( `Monotone Recast fallback expected one tiled watershed call, found ${watershedCallCount}.`, ); } const patched = upstream .replace("buildRegions, allocContourSet", "buildRegions, buildRegionsMonotone, allocContourSet") .replace(TILED_WATERSHED_CALL, TILED_MONOTONE_CALL) .replace(/\n\/\/# sourceMappingURL=.*$/u, ""); const sourceChecksum = sha256(upstream); const generatedDirectory = path.resolve( "dungeon-pipeline/work/.generated-recast", ); const generatedFile = path.join( generatedDirectory, `generators-${EXPECTED_GENERATOR_VERSION}-monotone-${sourceChecksum.slice(0, 12)}.mjs`, ); await mkdir(generatedDirectory, { recursive: true }); await writeFile(generatedFile, patched, "utf8"); const module = await import(`${pathToFileURL(generatedFile).href}?source=${sourceChecksum}`); return { generateTiledNavMesh: module.generateTiledNavMesh, provenance: { package: `@recast-navigation/generators@${EXPECTED_GENERATOR_VERSION}`, upstreamEntry: path.relative(process.cwd(), entryFile).replace(/\\/g, "/"), upstreamChecksum: sourceChecksum, operation: "replace tiled buildRegions watershed call with buildRegionsMonotone", }, }; } async function monotoneGenerator() { generatorPromise ??= loadPatchedGenerator(); return generatorPromise; } export async function threeToMonotoneTiledNavMesh( meshes, navMeshGeneratorConfig = {}, keepIntermediates = false, ) { const [positions, indices] = getPositionsAndIndices(meshes); const generator = await monotoneGenerator(); return { result: generator.generateTiledNavMesh( positions, indices, navMeshGeneratorConfig, keepIntermediates, ), provenance: generator.provenance, }; }