forked from phenom/healer-man
Update Healer Man server
This commit is contained in:
1 parent
69809f7228
commit
ff20ab8779
101 files changed
+16713
-945
No files matched your search
@@ -15,6 +15,7 @@ import { threeToSoloNavMesh, threeToTiledNavMesh, NavMeshHelper } from "@recast-
|
||||
import { Vector3 } from "three";
|
||||
import { GLTFLoader } from "three/examples/jsm/loaders/GLTFLoader.js";
|
||||
import { MeshoptDecoder } from "meshoptimizer";
|
||||
import { unpackMeshesForRecast } from "./recast-mesh-adapter.mjs";
|
||||
import {
|
||||
exists,
|
||||
fixturesRoot,
|
||||
@@ -39,6 +40,11 @@ import {
|
||||
extractDetourTopology,
|
||||
stableSha256,
|
||||
} from "./detour-topology.mjs";
|
||||
import {
|
||||
countSurfaceBucketReferences,
|
||||
DETOUR_COMPONENT_SELECTION_LIMITS,
|
||||
selectDetourAnchorComponents,
|
||||
} from "./detour-component-selection.mjs";
|
||||
|
||||
globalThis.self = globalThis;
|
||||
globalThis.ProgressEvent ??= class ProgressEvent {
|
||||
@@ -263,21 +269,95 @@ function queryDetourRouteProbes(navMesh, probes) {
|
||||
}
|
||||
}
|
||||
|
||||
async function writeGeometry(file, geometry, inputBounds, boundsTolerance, detourTopology = null) {
|
||||
function mapDetourAnchorsToPolygons(navMesh, topology, anchors) {
|
||||
if (!Array.isArray(anchors) || anchors.length === 0) return [];
|
||||
const polygonIdByReference = new Map();
|
||||
for (const [polygonId, tileIndex, , , , polygonIndex] of topology.polygons) {
|
||||
const tile = navMesh.getTile(tileIndex);
|
||||
const reference = navMesh.getPolyRefBase(tile) + polygonIndex;
|
||||
polygonIdByReference.set(String(reference), polygonId);
|
||||
}
|
||||
const query = new NavMeshQuery(navMesh);
|
||||
try {
|
||||
return anchors.map((anchor) => {
|
||||
if (!anchor || typeof anchor.id !== "string" || !anchor.id.trim()
|
||||
|| !Array.isArray(anchor.position) || anchor.position.length !== 3
|
||||
|| !anchor.position.every(Number.isFinite)
|
||||
|| !Number.isFinite(anchor.maximumDistance) || anchor.maximumDistance <= 0) {
|
||||
throw new Error("Detour source anchors require an ID, finite XYZ position, and positive maximumDistance.");
|
||||
}
|
||||
const radius = anchor.maximumDistance;
|
||||
const nearest = query.findNearestPoly(
|
||||
{ x: anchor.position[0], y: anchor.position[1], z: anchor.position[2] },
|
||||
{ halfExtents: { x: radius, y: radius, z: radius } },
|
||||
);
|
||||
const found = nearest.success && String(nearest.nearestRef) !== "0";
|
||||
if (!found) {
|
||||
return {
|
||||
...anchor,
|
||||
id: anchor.id,
|
||||
kind: anchor.kind ?? null,
|
||||
position: [...anchor.position],
|
||||
maximumDistance: radius,
|
||||
polygonId: null,
|
||||
distance: null,
|
||||
nearestPoint: null,
|
||||
reason: "no-detour-surface-within-source-projection-search-box",
|
||||
};
|
||||
}
|
||||
const nearestPoint = [nearest.nearestPoint.x, nearest.nearestPoint.y, nearest.nearestPoint.z];
|
||||
const distance = Math.hypot(
|
||||
anchor.position[0] - nearestPoint[0],
|
||||
anchor.position[1] - nearestPoint[1],
|
||||
anchor.position[2] - nearestPoint[2],
|
||||
);
|
||||
const polygonId = polygonIdByReference.get(String(nearest.nearestRef));
|
||||
if (polygonId === undefined) {
|
||||
return {
|
||||
...anchor,
|
||||
id: anchor.id,
|
||||
kind: anchor.kind ?? null,
|
||||
position: [...anchor.position],
|
||||
maximumDistance: radius,
|
||||
polygonId: null,
|
||||
distance,
|
||||
nearestPoint,
|
||||
reason: "nearest-detour-reference-is-not-a-topology-surface-polygon",
|
||||
};
|
||||
}
|
||||
return {
|
||||
...anchor,
|
||||
id: anchor.id,
|
||||
kind: anchor.kind ?? null,
|
||||
position: [...anchor.position],
|
||||
maximumDistance: radius,
|
||||
polygonId,
|
||||
distance,
|
||||
nearestPoint,
|
||||
};
|
||||
});
|
||||
} finally {
|
||||
query.destroy();
|
||||
}
|
||||
}
|
||||
|
||||
async function writeGeometry(file, geometry, inputBounds, boundsTolerance, detourTopology = null, detourSelection = null) {
|
||||
const sourcePositions = geometry.getAttribute("position")?.array;
|
||||
const sourceIndices = geometry.getIndex()?.array;
|
||||
if (!sourcePositions) throw new Error("Recast helper produced no position buffer.");
|
||||
const sourceElementCount = sourceIndices?.length ?? sourcePositions.length / 3;
|
||||
if (detourTopology && detourTopology.detailTriangleCount !== sourceElementCount / 3) {
|
||||
const sourceTopology = detourSelection?.sourceTopology ?? detourTopology;
|
||||
const outputTopology = detourSelection?.topology ?? detourTopology;
|
||||
if (sourceTopology && sourceTopology.detailTriangleCount !== sourceElementCount / 3) {
|
||||
throw new Error(
|
||||
`Detour topology describes ${detourTopology.detailTriangleCount} detail triangles, `
|
||||
`Detour topology describes ${sourceTopology.detailTriangleCount} detail triangles, `
|
||||
+ `but the helper emitted ${sourceElementCount / 3}.`,
|
||||
);
|
||||
}
|
||||
const [minimumInput, maximumInput] = inputBounds;
|
||||
const compactPositions = [];
|
||||
const compactIndices = [];
|
||||
const exportedTrianglePolygonIds = detourTopology ? [] : null;
|
||||
const exportedTrianglePolygonIds = outputTopology ? [] : null;
|
||||
const remappedVertices = new Map();
|
||||
let rejectedOutOfBoundsTriangles = 0;
|
||||
|
||||
@@ -295,11 +375,11 @@ async function writeGeometry(file, geometry, inputBounds, boundsTolerance, detou
|
||||
return true;
|
||||
};
|
||||
|
||||
const sourceTrianglePolygonIds = detourTopology
|
||||
const sourceTrianglePolygonIds = sourceTopology
|
||||
? new Uint32Array(sourceElementCount / 3)
|
||||
: null;
|
||||
if (detourTopology) {
|
||||
for (const [polygonId, , , , , , firstTriangle, triangleCount] of detourTopology.polygons) {
|
||||
if (sourceTopology) {
|
||||
for (const [polygonId, , , , , , firstTriangle, triangleCount] of sourceTopology.polygons) {
|
||||
if (firstTriangle < 0 || triangleCount < 0 || firstTriangle + triangleCount > sourceTrianglePolygonIds.length) {
|
||||
throw new Error(`Detour topology polygon ${polygonId} has an invalid detail-triangle range.`);
|
||||
}
|
||||
@@ -319,6 +399,9 @@ async function writeGeometry(file, geometry, inputBounds, boundsTolerance, detou
|
||||
}
|
||||
|
||||
for (let offset = 0; offset < sourceElementCount; offset += 3) {
|
||||
const sourceTriangleIndex = offset / 3;
|
||||
const sourcePolygonId = sourceTrianglePolygonIds?.[sourceTriangleIndex];
|
||||
if (detourSelection && !detourSelection.selectedSourcePolygonIds.has(sourcePolygonId)) continue;
|
||||
const triangle = [
|
||||
sourceIndices?.[offset] ?? offset,
|
||||
sourceIndices?.[offset + 1] ?? offset + 1,
|
||||
@@ -328,8 +411,11 @@ async function writeGeometry(file, geometry, inputBounds, boundsTolerance, detou
|
||||
rejectedOutOfBoundsTriangles += 1;
|
||||
continue;
|
||||
}
|
||||
if (detourTopology) {
|
||||
const polygonId = sourceTrianglePolygonIds[offset / 3];
|
||||
if (outputTopology) {
|
||||
const polygonId = detourSelection
|
||||
? detourSelection.sourceToSelectedPolygonId.get(sourcePolygonId)
|
||||
: sourcePolygonId;
|
||||
if (!Number.isInteger(polygonId)) throw new Error(`Selected detail triangle ${sourceTriangleIndex} has no remapped polygon ID.`);
|
||||
exportedTrianglePolygonIds.push(polygonId);
|
||||
}
|
||||
for (const sourceIndex of triangle) {
|
||||
@@ -360,19 +446,32 @@ async function writeGeometry(file, geometry, inputBounds, boundsTolerance, detou
|
||||
if (!compactIndices.length) {
|
||||
throw new Error("Recast helper produced no triangles within the authoritative collision bounds.");
|
||||
}
|
||||
if (detourTopology) {
|
||||
if (outputTopology) {
|
||||
const geometryRanges = buildDetourGeometryRanges(
|
||||
detourTopology.polygonCount,
|
||||
outputTopology.polygonCount,
|
||||
exportedTrianglePolygonIds,
|
||||
);
|
||||
for (const [polygonId, polygonRecord] of detourTopology.polygons.entries()) {
|
||||
for (const [polygonId, polygonRecord] of outputTopology.polygons.entries()) {
|
||||
[polygonRecord[6], polygonRecord[7]] = geometryRanges[polygonId];
|
||||
}
|
||||
detourTopology.geometryTriangleCount = compactIndices.length / 3;
|
||||
outputTopology.geometryTriangleCount = compactIndices.length / 3;
|
||||
}
|
||||
|
||||
const positions = new Float32Array(compactPositions);
|
||||
const indices = new Uint32Array(compactIndices);
|
||||
const surfaceBucketReferenceCount = outputTopology
|
||||
? countSurfaceBucketReferences(positions, indices, 8)
|
||||
: null;
|
||||
if (detourSelection) {
|
||||
if (surfaceBucketReferenceCount > DETOUR_COMPONENT_SELECTION_LIMITS.surfaceBucketReferences) {
|
||||
throw new Error(
|
||||
`Source-anchored Detour subset exceeds the surfaceBucketReferences budget `
|
||||
+ `(${surfaceBucketReferenceCount} > ${DETOUR_COMPONENT_SELECTION_LIMITS.surfaceBucketReferences}).`,
|
||||
);
|
||||
}
|
||||
detourSelection.summary.selected.surfaceBucketReferenceCount = surfaceBucketReferenceCount;
|
||||
outputTopology.sourceAnchorSelection.selected.surfaceBucketReferenceCount = surfaceBucketReferenceCount;
|
||||
}
|
||||
const minimum = [Infinity, Infinity, Infinity];
|
||||
const maximum = [-Infinity, -Infinity, -Infinity];
|
||||
for (let offset = 0; offset < positions.length; offset += 3) {
|
||||
@@ -389,9 +488,9 @@ async function writeGeometry(file, geometry, inputBounds, boundsTolerance, detou
|
||||
document.createMesh("navigation").addPrimitive(primitive);
|
||||
document.createNode("navigation").setMesh(document.getRoot().listMeshes()[0]);
|
||||
document.createScene("navigation").addChild(document.getRoot().listNodes()[0]);
|
||||
if (detourTopology) {
|
||||
if (outputTopology) {
|
||||
document.getRoot().setExtras({
|
||||
runewakerNavigationTopology: detourTopology,
|
||||
runewakerNavigationTopology: outputTopology,
|
||||
});
|
||||
}
|
||||
await new NodeIO().write(file, document);
|
||||
@@ -401,6 +500,7 @@ async function writeGeometry(file, geometry, inputBounds, boundsTolerance, detou
|
||||
sourceVertices: sourcePositions.length / 3,
|
||||
sourceTriangles: sourceElementCount / 3,
|
||||
rejectedOutOfBoundsTriangles,
|
||||
...(surfaceBucketReferenceCount !== null ? { surfaceBucketReferenceCount } : {}),
|
||||
bounds: [minimum, maximum],
|
||||
boundsTolerance,
|
||||
};
|
||||
@@ -415,9 +515,13 @@ export async function bakeNavigationFromCollisionAssets({
|
||||
reportFile = path.join(workRoot, slug, "recast-report.json"),
|
||||
source = undefined,
|
||||
includeDetourTopology = false,
|
||||
detourAnchorPositions = [],
|
||||
detourRouteProbes = [],
|
||||
}) {
|
||||
if (!collisionAssets.length) throw new Error(`${slug}: no collision chunks are available for Recast.`);
|
||||
if (detourAnchorPositions.length && !includeDetourTopology) {
|
||||
throw new Error(`${slug}: source-anchor Detour selection requires includeDetourTopology.`);
|
||||
}
|
||||
const normalizedPaths = collisionAssets.map((asset) => asset.path.replace(/\\/g, "/"));
|
||||
if (new Set(normalizedPaths.map((value) => value.toLowerCase())).size !== normalizedPaths.length) {
|
||||
throw new Error(`${slug}: collision chunks contain duplicate paths.`);
|
||||
@@ -431,8 +535,10 @@ export async function bakeNavigationFromCollisionAssets({
|
||||
await sha256(resolvedInputs[index]),
|
||||
])));
|
||||
const scenes = await Promise.all(resolvedInputs.map(loadScene));
|
||||
const meshes = scenes.flatMap(meshesIn);
|
||||
if (!meshes.length) throw new Error(`${slug}: collision chunks contain no triangle meshes.`);
|
||||
const sourceMeshes = scenes.flatMap(meshesIn);
|
||||
if (!sourceMeshes.length) throw new Error(`${slug}: collision chunks contain no triangle meshes.`);
|
||||
const adapted = unpackMeshesForRecast(sourceMeshes);
|
||||
const meshes = adapted.meshes;
|
||||
const input = inspectNavigationInput(meshes);
|
||||
const navigationPlan = (tileSize) => planNavigationBuild(input.bounds, settings.cellSize, {
|
||||
tileSize,
|
||||
@@ -469,6 +575,7 @@ export async function bakeNavigationFromCollisionAssets({
|
||||
let helper = null;
|
||||
let geometry;
|
||||
let detourTopology = null;
|
||||
let detourSelection = null;
|
||||
let detourRoutes = [];
|
||||
try {
|
||||
if (build.mode === "tiled") {
|
||||
@@ -566,6 +673,22 @@ export async function bakeNavigationFromCollisionAssets({
|
||||
sourceCollisionSha256: stableSha256(inputChunkChecksums),
|
||||
settingsSha256: stableSha256({ settings, generatorConfig }),
|
||||
});
|
||||
if (detourAnchorPositions.length) {
|
||||
const anchorMappings = mapDetourAnchorsToPolygons(
|
||||
generated.navMesh,
|
||||
detourTopology,
|
||||
detourAnchorPositions,
|
||||
);
|
||||
for (const anchor of detourAnchorPositions.filter(anchor => anchor.required === true)) {
|
||||
const mapping = anchorMappings.find(mapping => mapping.id === anchor.id);
|
||||
if (mapping?.polygonId === null || mapping?.polygonId === undefined
|
||||
|| !Number.isFinite(mapping.distance) || mapping.distance > anchor.maximumDistance) {
|
||||
throw new Error(`${slug}: required source navigation anchor ${anchor.id} is outside its projection limit.`);
|
||||
}
|
||||
}
|
||||
detourSelection = selectDetourAnchorComponents(detourTopology, anchorMappings);
|
||||
detourSelection.sourceTopology = detourTopology;
|
||||
}
|
||||
}
|
||||
detourRoutes = queryDetourRouteProbes(generated.navMesh, detourRouteProbes);
|
||||
await mkdir(path.dirname(output), { recursive: true });
|
||||
@@ -575,6 +698,7 @@ export async function bakeNavigationFromCollisionAssets({
|
||||
input.bounds,
|
||||
Math.max(settings.cellSize, settings.cellHeight) * 2,
|
||||
detourTopology,
|
||||
detourSelection,
|
||||
);
|
||||
} finally {
|
||||
helper?.navMeshGeometry.dispose();
|
||||
@@ -588,6 +712,7 @@ export async function bakeNavigationFromCollisionAssets({
|
||||
generator: "@recast-navigation/three@0.43.1",
|
||||
settings,
|
||||
input,
|
||||
navigationGeometryAdapter: adapted.report,
|
||||
build: {
|
||||
...build,
|
||||
partition: build.mode === "tiled" ? tiledPartition : "watershed",
|
||||
@@ -606,22 +731,23 @@ export async function bakeNavigationFromCollisionAssets({
|
||||
...(detourTopology ? {
|
||||
detourTopology: {
|
||||
schemaVersion: detourTopology.schemaVersion,
|
||||
tileCount: detourTopology.tileCount,
|
||||
polygonCount: detourTopology.polygonCount,
|
||||
detailTriangleCount: detourTopology.detailTriangleCount,
|
||||
polygonFanTriangleCount: detourTopology.polygonFanTriangleCount,
|
||||
geometryTriangleCount: detourTopology.geometryTriangleCount,
|
||||
directedLinkCount: detourTopology.directedLinkCount,
|
||||
tileCount: detourSelection?.topology.tileCount ?? detourTopology.tileCount,
|
||||
polygonCount: detourSelection?.topology.polygonCount ?? detourTopology.polygonCount,
|
||||
detailTriangleCount: detourSelection?.topology.detailTriangleCount ?? detourTopology.detailTriangleCount,
|
||||
polygonFanTriangleCount: detourSelection?.topology.polygonFanTriangleCount ?? detourTopology.polygonFanTriangleCount,
|
||||
geometryTriangleCount: detourSelection?.topology.geometryTriangleCount ?? detourTopology.geometryTriangleCount,
|
||||
directedLinkCount: detourSelection?.topology.directedLinkCount ?? detourTopology.directedLinkCount,
|
||||
unrepresentedLinkCount: detourTopology.unrepresentedLinkCount,
|
||||
offMeshPolygonCount: detourTopology.offMeshPolygonCount,
|
||||
offMeshPolygonCount: detourSelection?.topology.offMeshPolygonCount ?? detourTopology.offMeshPolygonCount,
|
||||
weakComponents: {
|
||||
count: detourTopology.weakComponents.count,
|
||||
largestPolygonCount: detourTopology.weakComponents.largestPolygonCount,
|
||||
count: detourSelection?.topology.weakComponents.count ?? detourTopology.weakComponents.count,
|
||||
largestPolygonCount: detourSelection?.topology.weakComponents.largestPolygonCount ?? detourTopology.weakComponents.largestPolygonCount,
|
||||
},
|
||||
sourceCollisionSha256: detourTopology.sourceCollisionSha256,
|
||||
settingsSha256: detourTopology.settingsSha256,
|
||||
},
|
||||
} : {}),
|
||||
...(detourSelection ? { detourComponentSelection: detourSelection.summary } : {}),
|
||||
...(source ? { source } : {}),
|
||||
};
|
||||
await writeJson(reportFile, report);
|
||||
|
||||
Reference in new issue
Block a user