149 lines
5.3 KiB
JavaScript
149 lines
5.3 KiB
JavaScript
export const DEFAULT_TILED_NAVMESH_SIZE = 512;
|
|
export const MIN_TILED_NAVMESH_SIZE = 128;
|
|
export const MAX_SOLO_HEIGHTFIELD_AXIS = 4096;
|
|
export const MAX_SOLO_HEIGHTFIELD_CELLS = MAX_SOLO_HEIGHTFIELD_AXIS ** 2;
|
|
export const MAX_DETOUR_TILES = 16384;
|
|
|
|
function finiteBounds(bounds) {
|
|
return Array.isArray(bounds)
|
|
&& bounds.length === 2
|
|
&& bounds.every((corner) => Array.isArray(corner)
|
|
&& corner.length === 3
|
|
&& corner.every(Number.isFinite));
|
|
}
|
|
|
|
export function planNavigationBuild(bounds, cellSize, {
|
|
tileSize = DEFAULT_TILED_NAVMESH_SIZE,
|
|
maxSoloAxis = MAX_SOLO_HEIGHTFIELD_AXIS,
|
|
maxSoloCells = MAX_SOLO_HEIGHTFIELD_CELLS,
|
|
maxTiles = MAX_DETOUR_TILES,
|
|
} = {}) {
|
|
if (!finiteBounds(bounds)) throw new Error("Navigation bounds must contain two finite XYZ coordinates.");
|
|
if (!Number.isFinite(cellSize) || cellSize <= 0) throw new Error("Navigation cell size must be positive and finite.");
|
|
if (!Number.isInteger(tileSize) || tileSize <= 0) throw new Error("Navigation tile size must be a positive integer.");
|
|
|
|
const [minimum, maximum] = bounds;
|
|
const gridWidth = Math.max(1, Math.ceil((maximum[0] - minimum[0]) / cellSize));
|
|
const gridHeight = Math.max(1, Math.ceil((maximum[2] - minimum[2]) / cellSize));
|
|
if (maximum.some((value, index) => value < minimum[index])) {
|
|
throw new Error("Navigation bounds maximum must not be below its minimum.");
|
|
}
|
|
|
|
const gridCells = gridWidth * gridHeight;
|
|
const mode = gridWidth > maxSoloAxis || gridHeight > maxSoloAxis || gridCells > maxSoloCells
|
|
? "tiled"
|
|
: "solo";
|
|
const tileColumns = mode === "tiled" ? Math.ceil(gridWidth / tileSize) : 1;
|
|
const tileRows = mode === "tiled" ? Math.ceil(gridHeight / tileSize) : 1;
|
|
const tileCount = tileColumns * tileRows;
|
|
if (mode === "tiled" && tileCount > maxTiles) {
|
|
throw new Error(
|
|
`Navigation bounds require ${tileCount} tiles, above Detour's ${maxTiles}-tile pipeline limit.`,
|
|
);
|
|
}
|
|
|
|
return {
|
|
mode,
|
|
gridWidth,
|
|
gridHeight,
|
|
gridCells,
|
|
tileSize: mode === "tiled" ? tileSize : null,
|
|
tileColumns,
|
|
tileRows,
|
|
tileCount,
|
|
};
|
|
}
|
|
|
|
function tileKey(x, y) {
|
|
return `${x},${y}`;
|
|
}
|
|
|
|
function tileCoordinatesFromLog(message) {
|
|
const match = /^Building tile at x:\s*(-?\d+), y:\s*(-?\d+)$/.exec(message);
|
|
return match ? { x: Number(match[1]), y: Number(match[2]) } : null;
|
|
}
|
|
|
|
function count(intermediate, property, method) {
|
|
return intermediate?.[property]?.[method]?.() ?? null;
|
|
}
|
|
|
|
export function validateTiledBuild(intermediates, {
|
|
errorCategory = 3,
|
|
warningCategory = 2,
|
|
intermediateRetention = "retained",
|
|
populatedTileCount = null,
|
|
} = {}) {
|
|
if (!["retained", "released"].includes(intermediateRetention)) {
|
|
throw new Error(`Unsupported tiled intermediate retention mode: ${intermediateRetention}.`);
|
|
}
|
|
const tiles = new Map(
|
|
(intermediates?.tileIntermediates ?? []).map((tile) => [tileKey(tile.x, tile.y), tile]),
|
|
);
|
|
const failures = [];
|
|
const ignoredEmptyTilePackingFailures = [];
|
|
let currentTile = null;
|
|
|
|
for (const log of intermediates?.buildContext?.logs ?? []) {
|
|
currentTile = tileCoordinatesFromLog(log.msg) ?? currentTile;
|
|
const failedToAddTile = log.category === warningCategory
|
|
&& log.msg.startsWith("Failed to add tile to nav mesh");
|
|
if (log.category !== errorCategory && !failedToAddTile) continue;
|
|
|
|
const tile = currentTile ? tiles.get(tileKey(currentTile.x, currentTile.y)) : null;
|
|
const polygonCount = count(tile, "polyMesh", "npolys");
|
|
const contourCount = count(tile, "contourSet", "nconts");
|
|
const confirmedEmptyPackingFailure = log.msg === "Failed to create Detour navmesh data"
|
|
&& (
|
|
(polygonCount === 0 && contourCount === 0)
|
|
|| (
|
|
intermediateRetention === "released"
|
|
&& polygonCount === null
|
|
&& contourCount === null
|
|
)
|
|
);
|
|
|
|
const issue = {
|
|
x: currentTile?.x ?? null,
|
|
y: currentTile?.y ?? null,
|
|
message: log.msg,
|
|
polygonCount,
|
|
contourCount,
|
|
verification: intermediateRetention,
|
|
};
|
|
if (confirmedEmptyPackingFailure) ignoredEmptyTilePackingFailures.push(issue);
|
|
else failures.push(issue);
|
|
}
|
|
|
|
const populatedTiles = [...tiles.values()].filter(
|
|
(tile) => (count(tile, "polyMesh", "npolys") ?? 0) > 0,
|
|
);
|
|
const effectivePopulatedTileCount = populatedTileCount ?? populatedTiles.length;
|
|
return {
|
|
valid: failures.length === 0 && effectivePopulatedTileCount > 0,
|
|
failures,
|
|
ignoredEmptyTilePackingFailures,
|
|
populatedTileCount: effectivePopulatedTileCount,
|
|
tileCount: tiles.size,
|
|
intermediateRetention,
|
|
};
|
|
}
|
|
|
|
export function nextTiledNavMeshSize(validation, currentTileSize, {
|
|
minimumTileSize = MIN_TILED_NAVMESH_SIZE,
|
|
} = {}) {
|
|
const retryable = validation?.failures?.length > 0
|
|
&& validation.failures.every((failure) => (
|
|
/^rcBuildContours: Bad outline for region \d+,/.test(failure.message)
|
|
|| /^rcBuildRegions: \d+ overlapping regions\.$/.test(failure.message)
|
|
));
|
|
if (!retryable || currentTileSize <= minimumTileSize) return null;
|
|
return Math.max(minimumTileSize, Math.floor(currentTileSize / 2));
|
|
}
|
|
|
|
export function shouldUseMonotonePartition(validation) {
|
|
return validation?.failures?.length > 0
|
|
&& validation.failures.every(
|
|
(failure) => /^rcBuildRegions: \d+ overlapping regions\.$/.test(failure.message),
|
|
);
|
|
}
|