forked from phenom/healer-man
Update Healer Man server
This commit is contained in:
1 parent
6114a2295f
commit
69809f7228
593 files changed
+903643
-40774
No files matched your search
@@ -8,6 +8,7 @@ import {
|
||||
freePolyMesh,
|
||||
freePolyMeshDetail,
|
||||
init,
|
||||
NavMeshQuery,
|
||||
Recast,
|
||||
} from "@recast-navigation/core";
|
||||
import { threeToSoloNavMesh, threeToTiledNavMesh, NavMeshHelper } from "@recast-navigation/three";
|
||||
@@ -33,6 +34,11 @@ import {
|
||||
validateTiledBuild,
|
||||
} from "./recast-policy.mjs";
|
||||
import { threeToMonotoneTiledNavMesh } from "./recast-monotone-fallback.mjs";
|
||||
import {
|
||||
buildDetourGeometryRanges,
|
||||
extractDetourTopology,
|
||||
stableSha256,
|
||||
} from "./detour-topology.mjs";
|
||||
|
||||
globalThis.self = globalThis;
|
||||
globalThis.ProgressEvent ??= class ProgressEvent {
|
||||
@@ -156,14 +162,122 @@ function populatedNavMeshTileCount(navMesh) {
|
||||
return populated;
|
||||
}
|
||||
|
||||
async function writeGeometry(file, geometry, inputBounds, boundsTolerance) {
|
||||
function queryDetourRouteProbes(navMesh, probes) {
|
||||
if (!probes.length) return [];
|
||||
const query = new NavMeshQuery(navMesh);
|
||||
try {
|
||||
return probes.map((probe) => {
|
||||
const startRadius = probe.maxStartSnapDistance ?? 3;
|
||||
const endRadius = probe.maxEndSnapDistance ?? 6;
|
||||
const start = query.findNearestPoly(
|
||||
{ x: probe.start[0], y: probe.start[1], z: probe.start[2] },
|
||||
{ halfExtents: { x: startRadius, y: startRadius, z: startRadius } },
|
||||
);
|
||||
const end = query.findNearestPoly(
|
||||
{ x: probe.end[0], y: probe.end[1], z: probe.end[2] },
|
||||
{ halfExtents: { x: endRadius, y: endRadius, z: endRadius } },
|
||||
);
|
||||
const pointDistance = (source, target) => Math.hypot(
|
||||
source[0] - target.x,
|
||||
source[1] - target.y,
|
||||
source[2] - target.z,
|
||||
);
|
||||
const startFound = start.success && start.nearestRef !== 0;
|
||||
const endFound = end.success && end.nearestRef !== 0;
|
||||
const startDistance = startFound ? pointDistance(probe.start, start.nearestPoint) : null;
|
||||
const endDistance = endFound ? pointDistance(probe.end, end.nearestPoint) : null;
|
||||
if (!startFound || !endFound
|
||||
|| startDistance > startRadius
|
||||
|| endDistance > endRadius) {
|
||||
return {
|
||||
id: probe.id,
|
||||
from: probe.from ?? null,
|
||||
to: probe.to ?? null,
|
||||
start: {
|
||||
found: startFound,
|
||||
polygonRef: startFound ? start.nearestRef : null,
|
||||
distance: startDistance,
|
||||
maximumDistance: startRadius,
|
||||
status: start.status,
|
||||
},
|
||||
end: {
|
||||
found: endFound,
|
||||
polygonRef: endFound ? end.nearestRef : null,
|
||||
distance: endDistance,
|
||||
maximumDistance: endRadius,
|
||||
status: end.status,
|
||||
},
|
||||
reachable: false,
|
||||
reason: "endpoint-not-found-within-query-snap-limit",
|
||||
};
|
||||
}
|
||||
|
||||
const path = query.findPath(
|
||||
start.nearestRef,
|
||||
end.nearestRef,
|
||||
start.nearestPoint,
|
||||
end.nearestPoint,
|
||||
{ maxPathPolys: 100_000 },
|
||||
);
|
||||
try {
|
||||
const refs = path.polys.toTypedArray();
|
||||
const usedRefs = [];
|
||||
for (const ref of refs) {
|
||||
if (ref === 0) break;
|
||||
usedRefs.push(ref);
|
||||
}
|
||||
const complete = path.success
|
||||
&& usedRefs.length > 0
|
||||
&& usedRefs[usedRefs.length - 1] === end.nearestRef;
|
||||
return {
|
||||
id: probe.id,
|
||||
from: probe.from ?? null,
|
||||
to: probe.to ?? null,
|
||||
start: {
|
||||
found: true,
|
||||
polygonRef: start.nearestRef,
|
||||
point: [start.nearestPoint.x, start.nearestPoint.y, start.nearestPoint.z],
|
||||
distance: startDistance,
|
||||
maximumDistance: startRadius,
|
||||
},
|
||||
end: {
|
||||
found: true,
|
||||
polygonRef: end.nearestRef,
|
||||
point: [end.nearestPoint.x, end.nearestPoint.y, end.nearestPoint.z],
|
||||
distance: endDistance,
|
||||
maximumDistance: endRadius,
|
||||
},
|
||||
reachable: complete,
|
||||
partial: path.success && usedRefs.length > 0 && !complete,
|
||||
status: path.status,
|
||||
polygonCount: usedRefs.length,
|
||||
// Raw Detour refs are useful for tracing this exact bake, but are not stable asset IDs.
|
||||
polygonRefs: usedRefs,
|
||||
};
|
||||
} finally {
|
||||
path.polys.destroy();
|
||||
}
|
||||
});
|
||||
} finally {
|
||||
query.destroy();
|
||||
}
|
||||
}
|
||||
|
||||
async function writeGeometry(file, geometry, inputBounds, boundsTolerance, detourTopology = 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) {
|
||||
throw new Error(
|
||||
`Detour topology describes ${detourTopology.detailTriangleCount} detail triangles, `
|
||||
+ `but the helper emitted ${sourceElementCount / 3}.`,
|
||||
);
|
||||
}
|
||||
const [minimumInput, maximumInput] = inputBounds;
|
||||
const compactPositions = [];
|
||||
const compactIndices = [];
|
||||
const exportedTrianglePolygonIds = detourTopology ? [] : null;
|
||||
const remappedVertices = new Map();
|
||||
let rejectedOutOfBoundsTriangles = 0;
|
||||
|
||||
@@ -181,6 +295,29 @@ async function writeGeometry(file, geometry, inputBounds, boundsTolerance) {
|
||||
return true;
|
||||
};
|
||||
|
||||
const sourceTrianglePolygonIds = detourTopology
|
||||
? new Uint32Array(sourceElementCount / 3)
|
||||
: null;
|
||||
if (detourTopology) {
|
||||
for (const [polygonId, , , , , , firstTriangle, triangleCount] of detourTopology.polygons) {
|
||||
if (firstTriangle < 0 || triangleCount < 0 || firstTriangle + triangleCount > sourceTrianglePolygonIds.length) {
|
||||
throw new Error(`Detour topology polygon ${polygonId} has an invalid detail-triangle range.`);
|
||||
}
|
||||
for (let triangle = firstTriangle; triangle < firstTriangle + triangleCount; triangle += 1) {
|
||||
if (sourceTrianglePolygonIds[triangle] !== 0) {
|
||||
throw new Error(`Detour topology polygons overlap at detail triangle ${triangle}.`);
|
||||
}
|
||||
sourceTrianglePolygonIds[triangle] = polygonId + 1;
|
||||
}
|
||||
}
|
||||
for (let triangle = 0; triangle < sourceTrianglePolygonIds.length; triangle += 1) {
|
||||
if (sourceTrianglePolygonIds[triangle] === 0) {
|
||||
throw new Error(`Detour topology is missing source detail triangle ${triangle}.`);
|
||||
}
|
||||
sourceTrianglePolygonIds[triangle] -= 1;
|
||||
}
|
||||
}
|
||||
|
||||
for (let offset = 0; offset < sourceElementCount; offset += 3) {
|
||||
const triangle = [
|
||||
sourceIndices?.[offset] ?? offset,
|
||||
@@ -191,6 +328,10 @@ async function writeGeometry(file, geometry, inputBounds, boundsTolerance) {
|
||||
rejectedOutOfBoundsTriangles += 1;
|
||||
continue;
|
||||
}
|
||||
if (detourTopology) {
|
||||
const polygonId = sourceTrianglePolygonIds[offset / 3];
|
||||
exportedTrianglePolygonIds.push(polygonId);
|
||||
}
|
||||
for (const sourceIndex of triangle) {
|
||||
if (!sourceIndices) {
|
||||
const nextIndex = compactPositions.length / 3;
|
||||
@@ -219,6 +360,16 @@ async function writeGeometry(file, geometry, inputBounds, boundsTolerance) {
|
||||
if (!compactIndices.length) {
|
||||
throw new Error("Recast helper produced no triangles within the authoritative collision bounds.");
|
||||
}
|
||||
if (detourTopology) {
|
||||
const geometryRanges = buildDetourGeometryRanges(
|
||||
detourTopology.polygonCount,
|
||||
exportedTrianglePolygonIds,
|
||||
);
|
||||
for (const [polygonId, polygonRecord] of detourTopology.polygons.entries()) {
|
||||
[polygonRecord[6], polygonRecord[7]] = geometryRanges[polygonId];
|
||||
}
|
||||
detourTopology.geometryTriangleCount = compactIndices.length / 3;
|
||||
}
|
||||
|
||||
const positions = new Float32Array(compactPositions);
|
||||
const indices = new Uint32Array(compactIndices);
|
||||
@@ -238,6 +389,11 @@ async function writeGeometry(file, geometry, inputBounds, boundsTolerance) {
|
||||
document.createMesh("navigation").addPrimitive(primitive);
|
||||
document.createNode("navigation").setMesh(document.getRoot().listMeshes()[0]);
|
||||
document.createScene("navigation").addChild(document.getRoot().listNodes()[0]);
|
||||
if (detourTopology) {
|
||||
document.getRoot().setExtras({
|
||||
runewakerNavigationTopology: detourTopology,
|
||||
});
|
||||
}
|
||||
await new NodeIO().write(file, document);
|
||||
return {
|
||||
vertices: positions.length / 3,
|
||||
@@ -258,6 +414,8 @@ export async function bakeNavigationFromCollisionAssets({
|
||||
output = path.join(workRoot, slug, "staging", `${slug}-navigation.glb`),
|
||||
reportFile = path.join(workRoot, slug, "recast-report.json"),
|
||||
source = undefined,
|
||||
includeDetourTopology = false,
|
||||
detourRouteProbes = [],
|
||||
}) {
|
||||
if (!collisionAssets.length) throw new Error(`${slug}: no collision chunks are available for Recast.`);
|
||||
const normalizedPaths = collisionAssets.map((asset) => asset.path.replace(/\\/g, "/"));
|
||||
@@ -310,6 +468,8 @@ export async function bakeNavigationFromCollisionAssets({
|
||||
let monotoneProvenance = null;
|
||||
let helper = null;
|
||||
let geometry;
|
||||
let detourTopology = null;
|
||||
let detourRoutes = [];
|
||||
try {
|
||||
if (build.mode === "tiled") {
|
||||
while (true) {
|
||||
@@ -401,12 +561,20 @@ export async function bakeNavigationFromCollisionAssets({
|
||||
if (!helper.navMeshGeometry.getAttribute("position")?.count) {
|
||||
throw new Error(`${slug}: Recast produced no navigation geometry.`);
|
||||
}
|
||||
if (includeDetourTopology) {
|
||||
detourTopology = extractDetourTopology(generated.navMesh, {
|
||||
sourceCollisionSha256: stableSha256(inputChunkChecksums),
|
||||
settingsSha256: stableSha256({ settings, generatorConfig }),
|
||||
});
|
||||
}
|
||||
detourRoutes = queryDetourRouteProbes(generated.navMesh, detourRouteProbes);
|
||||
await mkdir(path.dirname(output), { recursive: true });
|
||||
geometry = await writeGeometry(
|
||||
output,
|
||||
helper.navMeshGeometry,
|
||||
input.bounds,
|
||||
Math.max(settings.cellSize, settings.cellHeight) * 2,
|
||||
detourTopology,
|
||||
);
|
||||
} finally {
|
||||
helper?.navMeshGeometry.dispose();
|
||||
@@ -434,6 +602,26 @@ export async function bakeNavigationFromCollisionAssets({
|
||||
inputChunkChecksums,
|
||||
output: relativeProjectPath(output),
|
||||
geometry,
|
||||
...(detourRouteProbes.length ? { detourRouteProbes: detourRoutes } : {}),
|
||||
...(detourTopology ? {
|
||||
detourTopology: {
|
||||
schemaVersion: detourTopology.schemaVersion,
|
||||
tileCount: detourTopology.tileCount,
|
||||
polygonCount: detourTopology.polygonCount,
|
||||
detailTriangleCount: detourTopology.detailTriangleCount,
|
||||
polygonFanTriangleCount: detourTopology.polygonFanTriangleCount,
|
||||
geometryTriangleCount: detourTopology.geometryTriangleCount,
|
||||
directedLinkCount: detourTopology.directedLinkCount,
|
||||
unrepresentedLinkCount: detourTopology.unrepresentedLinkCount,
|
||||
offMeshPolygonCount: detourTopology.offMeshPolygonCount,
|
||||
weakComponents: {
|
||||
count: detourTopology.weakComponents.count,
|
||||
largestPolygonCount: detourTopology.weakComponents.largestPolygonCount,
|
||||
},
|
||||
sourceCollisionSha256: detourTopology.sourceCollisionSha256,
|
||||
settingsSha256: detourTopology.settingsSha256,
|
||||
},
|
||||
} : {}),
|
||||
...(source ? { source } : {}),
|
||||
};
|
||||
await writeJson(reportFile, report);
|
||||
|
||||
Reference in new issue
Block a user