Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
37 changes: 37 additions & 0 deletions .agents/docs/2026-11-15-add-recastnavigation-mwrap-plan.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,37 @@
# Add mcpplibs.mwrap.recastnavigation (recastnavigation-m 1.6.0)

## Form

- Third-party upstream [recastnavigation/recastnavigation](https://github.com/recastnavigation/recastnavigation), release
v1.6.0, zlib.
- Form A external mwrap module repository [mcpplibs/recastnavigation-m](https://github.com/mcpplibs/recastnavigation-m),
package `mcpplibs.mwrap.recastnavigation`, version 1.6.0 (the first indexed release of the identity), tag `mwrap-1.6.0`;
release asset `recastnavigation-m-1.6.0.tar.gz` (`13382f6c…daa05`), CN mirror `gitcode.com/mcpp-res/mwrap-recastnavigation`
with identical bytes.
- Implementation ownership: the library has no compat package in this index, so the wrapper vendors the four libraries
(Recast, Detour, DetourCrowd, DetourTileCache: `Include/` and `Source/`) from the release archive (checksums of every
file in `upstream/documents.json`) and builds them with `-ffp-contract=off` and no fast math, with `NDEBUG` in every
profile (upstream declares its assert hooks only without it). DebugUtils, the demo and upstream's tests are not
vendored. It deploys upstream's README and License.txt with its own LICENSE/NOTICE (`runtime.deploy`).

## Module

- `mcpplibs.mwrap.recastnavigation`: every name declared at global scope by the four libraries' public headers, by
`export using ::name;` (99 types, 75 enumerators of unscoped enumerations, 170 functions).
- Internal-linkage adaptation: 52 `static const` objects (`DT_VERTS_PER_POLYGON`, `DT_SUCCESS`, `DT_FAILURE`,
`RC_WALKABLE_AREA`, `RC_PI` …) are declared again under their names, with their types and values, in
`mcpplibs::mwrap::recastnavigation` (an integer from the header's constant, a float from its literal).
- No `.macros` module: upstream's macros are its asserts, branch hints and `RC_SIZE_MAX`, used by its own sources.
- The export list is derived by `tools/surface.py` from the headers as clang reads them (its JSON AST), recorded in
`tools/surface.json`, and audited in CI: the module is the record on every row, and on the Linux LLVM row the record
is derived again and compared.

## Verification

- Wrapper CI (Linux x86_64 and arm64 with GCC 16.1 and LLVM 23.1.3, macOS LLVM, Windows LLVM, each in the dev and
release profiles; wasm32-emscripten under node): a navigation mesh built from a floor and a wall, the same bytes
twice; a path around the wall, its straight path, a ray along the mesh stopped by the wall; a crowd of four walked
through the doorway; the mesh's bytes and the crowd's places pinned and the same on every row; the adapted
constants; the export audit; the default template from an installed copy with a strict build.
- This index: consumer `tests/mwrap/recastnavigation-path` (the same pinned hashes); the installed-template check
with `--deploy` (`STATIC_NOTICES`).
5 changes: 5 additions & 0 deletions .github/workflows/validate.yml
Original file line number Diff line number Diff line change
Expand Up @@ -1738,6 +1738,11 @@ jobs:
shell: bash
run: python3 tests/mwrap/check_installed_template.py --mcpp "$MCPP" --package mcpplibs.mwrap.jolt@5.6.0 --deploy

- name: Installed recastnavigation wrapper archive, actual default path template and its relocated notices
if: contains(format(' {0} ', env.MEMBERS), ' recastnavigation-path ') && matrix.toolchain == 'default'
shell: bash
run: python3 tests/mwrap/check_installed_template.py --mcpp "$MCPP" --package mcpplibs.mwrap.recastnavigation@1.6.0 --deploy

- name: Installed libjpeg-turbo wrapper archive, actual default decoding template and its relocated notices
if: contains(format(' {0} ', env.MEMBERS), ' libjpeg-turbo-decode ') && matrix.toolchain == 'default'
shell: bash
Expand Down
8 changes: 8 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -41,6 +41,14 @@

### Added

- **mcpplibs.mwrap.recastnavigation 1.6.0(新包)。** recastnavigation-m:Recast & Detour 1.6.0(由三角形构建导航网格、寻路、群体、
带障碍物的分块网格)的 C++23 模块,以 `export using` 在全局命名空间导出 Recast、Detour、DetourCrowd、DetourTileCache 头文件的
全部公开名字(99 个类型、75 个无作用域枚举的枚举项、170 个函数);52 个内部链接的 `static const` 常量(`DT_VERTS_PER_POLYGON`、
`DT_SUCCESS`、`RC_WALKABLE_AREA`、`RC_PI` 等)以原名、原类型和原值位于 `mcpplibs::mwrap::recastnavigation`。导出清单由 clang 读头文件
推出并在 CI 中审计。封装仓库从上游 1.6.0 发布包取源码,以 `-ffp-contract=off`、不用快速数学构建,`NDEBUG` 使各配置相同:同一
组三角形与群体在 Linux x86-64 与 arm64、macOS、Windows、wasm32 上,dev 与 release,网格字节与群体位置的哈希相同(钉住)。不含
DebugUtils 与演示程序。消费者测试 `tests/mwrap/recastnavigation-path`,安装后模板检查带 `--deploy`。GLOBAL 为 GitHub 发布资产,
CN 为 GitCode `mcpp-res/mwrap-recastnavigation` 镜像,sha256 相同。
- **mcpplibs.mwrap.jolt 5.6.0(新包)。** jolt-m:Jolt Physics 5.6.0(刚体与软体物理:形状、约束、查询、角色、载具)的 C++23
模块,以 `export using` 在上游的命名空间里导出 `<Jolt/…>` 下各头文件的全部公开名字(`JPH` 450 个类型、127 个函数与运算符、
6 个对象,及嵌套命名空间 `ClosestPoint`、`HalfFloatConversion`、`ScaleHelpers`、`StreamUtils`、`literals` 的名字);50 个内部链接的
Expand Down
1 change: 1 addition & 0 deletions mcpp.toml
Original file line number Diff line number Diff line change
Expand Up @@ -39,6 +39,7 @@ members = [
"tests/mwrap/cgltf-inspect",
"tests/mwrap/manifold-boolean",
"tests/mwrap/jolt-world",
"tests/mwrap/recastnavigation-path",
"tests/mwrap/basis-universal-transcode",
"tests/mwrap/libjpeg-turbo-decode",
"tests/mwrap/libarchive-bundle",
Expand Down
50 changes: 50 additions & 0 deletions pkgs/mwrap/recastnavigation.lua
Original file line number Diff line number Diff line change
@@ -0,0 +1,50 @@
-- Immutable upstream-aligned mwrap source release (recastnavigation-m mwrap-1.6.0, the first release of upstream
-- 1.6.0); GLOBAL and CN bytes are identical.
--
-- The wrapper is Apache-2.0; the libraries it vendors and links statically,
-- Recast & Detour 1.6.0, are zlib: the package deploys their README and
-- License.txt beside programs (runtime.deploy).
package = {
spec = "1",
namespace = "mcpplibs.mwrap",
name = "recastnavigation",
description = "recastnavigation-m: Recast & Detour 1.6.0 (navigation meshes built from triangles, path finding, crowds, tiled meshes with obstacles) as a C++23 module",
licenses = { "Apache-2.0" },
repo = "https://github.com/mcpplibs/recastnavigation-m",
type = "package",
extensions = {
autoupdate = {
tag = "mwrap-${version}",
url = "https://github.com/mcpplibs/recastnavigation-m/releases/download/${tag}/recastnavigation-m-${version}.tar.gz",
},
},
xpm = {
linux = {
["1.6.0"] = {
url = {
GLOBAL = "https://github.com/mcpplibs/recastnavigation-m/releases/download/mwrap-1.6.0/recastnavigation-m-1.6.0.tar.gz",
CN = "https://gitcode.com/mcpp-res/mwrap-recastnavigation/releases/download/1.6.0/recastnavigation-m-1.6.0.tar.gz",
},
sha256 = "13382f6c751ee5d000987c8e45e3721a4106f928753e004856dc339fbf5daa05",
},
},
macosx = {
["1.6.0"] = {
url = {
GLOBAL = "https://github.com/mcpplibs/recastnavigation-m/releases/download/mwrap-1.6.0/recastnavigation-m-1.6.0.tar.gz",
CN = "https://gitcode.com/mcpp-res/mwrap-recastnavigation/releases/download/1.6.0/recastnavigation-m-1.6.0.tar.gz",
},
sha256 = "13382f6c751ee5d000987c8e45e3721a4106f928753e004856dc339fbf5daa05",
},
},
windows = {
["1.6.0"] = {
url = {
GLOBAL = "https://github.com/mcpplibs/recastnavigation-m/releases/download/mwrap-1.6.0/recastnavigation-m-1.6.0.tar.gz",
CN = "https://gitcode.com/mcpp-res/mwrap-recastnavigation/releases/download/1.6.0/recastnavigation-m-1.6.0.tar.gz",
},
sha256 = "13382f6c751ee5d000987c8e45e3721a4106f928753e004856dc339fbf5daa05",
},
},
},
}
1 change: 1 addition & 0 deletions tests/mwrap/check_installed_template.py
Original file line number Diff line number Diff line change
Expand Up @@ -23,6 +23,7 @@
"mcpplibs.mwrap.meshoptimizer": ("meshoptimizer.cppm", "upstream/meshoptimizer/LICENSE.md", b"MIT License", b"Arseny Kapoulkine"),
"mcpplibs.mwrap.cgltf": ("cgltf.cppm", "upstream/cgltf/LICENSE", b"Permission is hereby granted", b"Kuhlmann"),
"mcpplibs.mwrap.manifold": ("manifold.cppm", "upstream/manifold/LICENSE", b"Apache License", b"The Manifold Authors"),
"mcpplibs.mwrap.recastnavigation": ("recastnavigation.cppm", "upstream/recastnavigation/License.txt", b"This software is provided 'as-is'", b"Recast & Detour is Copyright (c) 2009 Mikko"),
"mcpplibs.mwrap.jolt": ("jolt.cppm", "upstream/jolt/LICENSE", b"Permission is hereby granted", b"Jolt Physics is Copyright (c) 2021 Jorrit"),
"mcpplibs.mwrap.libjpeg_turbo": ("libjpeg_turbo.cppm", "upstream/libjpeg-turbo/README.ijg", b"Independent JPEG Group", b"libjpeg-turbo Project"),
"mcpplibs.mwrap.basis_universal": ("basis_universal.cppm", "upstream/basis_universal/NOTICE", b"Binomial LLC", b"Basis Universal"),
Expand Down
15 changes: 15 additions & 0 deletions tests/mwrap/recastnavigation-path/mcpp.toml
Original file line number Diff line number Diff line change
@@ -0,0 +1,15 @@
[package]
name = "recastnavigation-path"
version = "1.6.0"
description = "Consume the immutable Recast & Detour wrapper: a navigation mesh built from triangles, a path around a wall, a crowd walked through a doorway; the mesh's bytes and the crowd's places pinned"

[indices]
# Use the complete candidate built-in index prepared by tests/run_members.sh.
# An explicit empty table overrides the legacy workspace redirect.

[dependencies]
mcpplibs.mwrap.recastnavigation = "1.6.0"

# Detour's inline math compiled here too, as the library is built.
[build]
cxxflags = ["-ffp-contract=off"]
193 changes: 193 additions & 0 deletions tests/mwrap/recastnavigation-path/tests/behavior.cpp
Original file line number Diff line number Diff line change
@@ -0,0 +1,193 @@
// The installed Recast & Detour wrapper: a navigation mesh built from a floor with a wall across it, the same
// bytes twice; a path around the wall and a ray along the mesh stopped by it; a crowd of four walked through the
// doorway to the far side. The mesh's bytes and the crowd's places are pinned, as the wrapper's CI pins them on
// every platform; the adapted constants.
import std;
import mcpplibs.mwrap.recastnavigation;

namespace nav = mcpplibs::mwrap::recastnavigation;

namespace {

int failures = 0;
void check(bool held) { failures += held ? 0 : 1; }

// A 20 m square floor at y 0, and a wall 0.4 m thick and 2 m high across x = 0 from z = -10 to z = 6 (a
// doorway from z = 6 to 10), as triangles.
struct Soup {
std::vector<float> vertices;
std::vector<int> triangles;
void quad(std::array<float, 3> a, std::array<float, 3> b, std::array<float, 3> c, std::array<float, 3> d) {
const int base = static_cast<int>(vertices.size() / 3);
for (const auto& corner : {a, b, c, d}) vertices.insert(vertices.end(), corner.begin(), corner.end());
triangles.insert(triangles.end(), {base, base + 1, base + 2, base, base + 2, base + 3});
}
// A box from `low` to `high`, its faces wound counter-clockwise seen from outside.
void box(std::array<float, 3> low, std::array<float, 3> high) {
const auto [x0, y0, z0] = low;
const auto [x1, y1, z1] = high;
quad({x0, y1, z0}, {x0, y1, z1}, {x1, y1, z1}, {x1, y1, z0}); // top
quad({x0, y0, z0}, {x1, y0, z0}, {x1, y0, z1}, {x0, y0, z1}); // bottom
quad({x0, y0, z0}, {x0, y1, z0}, {x1, y1, z0}, {x1, y0, z0}); // -z
quad({x0, y0, z1}, {x1, y0, z1}, {x1, y1, z1}, {x0, y1, z1}); // +z
quad({x0, y0, z0}, {x0, y0, z1}, {x0, y1, z1}, {x0, y1, z0}); // -x
quad({x1, y0, z0}, {x1, y1, z0}, {x1, y1, z1}, {x1, y0, z1}); // +x
}
};
Soup scene() {
Soup soup;
soup.quad({-10, 0, -10}, {-10, 0, 10}, {10, 0, 10}, {10, 0, -10});
soup.box({-0.2f, 0, -10}, {0.2f, 2, 6});
return soup;
}

// The fingerprint of bytes: FNV-1a, 64 bits.
std::uint64_t fnv(std::span<const unsigned char> bytes, std::uint64_t hash = 0xcbf29ce484222325ull) {
for (const unsigned char byte : bytes) hash = (hash ^ byte) * 0x100000001b3ull;
return hash;
}
std::uint64_t fnv(std::span<const float> values, std::uint64_t hash = 0xcbf29ce484222325ull) {
return fnv(std::as_bytes(values).size() ? std::span(reinterpret_cast<const unsigned char*>(values.data()), values.size_bytes()) : std::span<const unsigned char>{}, hash);
}

// Recast's pipeline for one tile, as upstream's solo-mesh sample runs it: the triangles rasterised into a
// heightfield, what an agent cannot stand on or pass under filtered out, regions, contours, polygons and the
// detail mesh; then Detour's data of it.
std::vector<unsigned char> build(const Soup& soup) {
rcConfig config{};
config.cs = 0.2f;
config.ch = 0.1f;
config.walkableSlopeAngle = 45;
config.walkableHeight = 20; // 2 m in cells of 0.1
config.walkableClimb = 4; // 0.4 m
config.walkableRadius = 2; // 0.4 m in cells of 0.2
config.maxEdgeLen = 60;
config.maxSimplificationError = 1.3f;
config.minRegionArea = 8 * 8;
config.mergeRegionArea = 20 * 20;
config.maxVertsPerPoly = 6;
config.detailSampleDist = 1.2f;
config.detailSampleMaxError = 0.1f;
const int vertex_count = static_cast<int>(soup.vertices.size() / 3), triangle_count = static_cast<int>(soup.triangles.size() / 3);
rcCalcBounds(soup.vertices.data(), vertex_count, config.bmin, config.bmax);
rcCalcGridSize(config.bmin, config.bmax, config.cs, &config.width, &config.height);

rcContext context(false);
rcHeightfield* solid = rcAllocHeightfield();
check(rcCreateHeightfield(&context, *solid, config.width, config.height, config.bmin, config.bmax, config.cs, config.ch));
std::vector<unsigned char> areas(static_cast<std::size_t>(triangle_count), 0);
rcMarkWalkableTriangles(&context, config.walkableSlopeAngle, soup.vertices.data(), vertex_count, soup.triangles.data(), triangle_count, areas.data());
check(rcRasterizeTriangles(&context, soup.vertices.data(), vertex_count, soup.triangles.data(), areas.data(), triangle_count, *solid, config.walkableClimb));
rcFilterLowHangingWalkableObstacles(&context, config.walkableClimb, *solid);
rcFilterLedgeSpans(&context, config.walkableHeight, config.walkableClimb, *solid);
rcFilterWalkableLowHeightSpans(&context, config.walkableHeight, *solid);
rcCompactHeightfield* compact = rcAllocCompactHeightfield();
check(rcBuildCompactHeightfield(&context, config.walkableHeight, config.walkableClimb, *solid, *compact));
rcFreeHeightField(solid);
check(rcErodeWalkableArea(&context, config.walkableRadius, *compact));
check(rcBuildDistanceField(&context, *compact));
check(rcBuildRegions(&context, *compact, 0, config.minRegionArea, config.mergeRegionArea));
rcContourSet* contours = rcAllocContourSet();
check(rcBuildContours(&context, *compact, config.maxSimplificationError, config.maxEdgeLen, *contours));
rcPolyMesh* mesh = rcAllocPolyMesh();
check(rcBuildPolyMesh(&context, *contours, config.maxVertsPerPoly, *mesh));
rcPolyMeshDetail* detail = rcAllocPolyMeshDetail();
check(rcBuildPolyMeshDetail(&context, *mesh, *compact, config.detailSampleDist, config.detailSampleMaxError, *detail));
rcFreeCompactHeightfield(compact);
rcFreeContourSet(contours);
for (int poly = 0; poly < mesh->npolys; ++poly)
if (mesh->areas[poly] == nav::RC_WALKABLE_AREA) mesh->flags[poly] = 1;

dtNavMeshCreateParams params{};
params.verts = mesh->verts;
params.vertCount = mesh->nverts;
params.polys = mesh->polys;
params.polyAreas = mesh->areas;
params.polyFlags = mesh->flags;
params.polyCount = mesh->npolys;
params.nvp = mesh->nvp;
params.detailMeshes = detail->meshes;
params.detailVerts = detail->verts;
params.detailVertsCount = detail->nverts;
params.detailTris = detail->tris;
params.detailTriCount = detail->ntris;
params.walkableHeight = 2.0f;
params.walkableRadius = 0.4f;
params.walkableClimb = 0.4f;
std::copy_n(mesh->bmin, 3, params.bmin);
std::copy_n(mesh->bmax, 3, params.bmax);
params.cs = config.cs;
params.ch = config.ch;
params.buildBvTree = true;
unsigned char* data = nullptr;
int size = 0;
check(dtCreateNavMeshData(&params, &data, &size));
rcFreePolyMesh(mesh);
rcFreePolyMeshDetail(detail);
std::vector<unsigned char> out(data, data + size);
dtFree(data);
return out;
}

// The mesh loaded: Detour takes a copy it frees.
dtNavMesh* load(const std::vector<unsigned char>& bytes) {
dtNavMesh* mesh = dtAllocNavMesh();
auto* data = static_cast<unsigned char*>(dtAlloc(static_cast<int>(bytes.size()), DT_ALLOC_PERM));
std::ranges::copy(bytes, data);
check(dtStatusSucceed(mesh->init(data, static_cast<int>(bytes.size()), DT_TILE_FREE_DATA)));
return mesh;
}

} // namespace

int main() {
const std::vector<unsigned char> bytes = build(scene());
check(!bytes.empty() && bytes == build(scene()));
dtNavMesh* mesh = load(bytes);
dtNavMeshQuery* query = dtAllocNavMeshQuery();
check(dtStatusSucceed(query->init(mesh, 2048)));
dtQueryFilter filter;
const float extents[3] = {1, 2, 1}, start[3] = {-5, 0, -5}, end[3] = {5, 0, -5};
dtPolyRef start_ref = 0, end_ref = 0;
float start_at[3], end_at[3];
query->findNearestPoly(start, extents, &filter, &start_ref, start_at);
query->findNearestPoly(end, extents, &filter, &end_ref, end_at);
dtPolyRef polys[256];
int poly_count = 0, corner_count = 0;
float corners[3 * 32];
check(dtStatusSucceed(query->findPath(start_ref, end_ref, start_at, end_at, &filter, polys, &poly_count, 256)));
check(dtStatusSucceed(query->findStraightPath(start_at, end_at, polys, poly_count, corners, nullptr, nullptr, &corner_count, 32)));
float through = -100;
for (int corner = 0; corner < corner_count; ++corner) through = std::max(through, corners[3 * corner + 2]);
check(corner_count >= 3 && through > 6.0f);
float hit = 0, normal[3];
dtPolyRef ray_polys[64];
int ray_count = 0;
check(dtStatusSucceed(query->raycast(start_ref, start_at, end_at, &filter, &hit, normal, ray_polys, &ray_count, 64)) && hit > 0.4f && hit < 0.5f);

dtCrowd* crowd = dtAllocCrowd();
check(crowd->init(8, 0.4f, mesh));
dtCrowdAgentParams agent{};
agent.radius = 0.4f, agent.height = 2, agent.maxAcceleration = 8, agent.maxSpeed = 3.5f;
agent.collisionQueryRange = agent.radius * 12, agent.pathOptimizationRange = agent.radius * 30, agent.separationWeight = 2;
agent.updateFlags = DT_CROWD_ANTICIPATE_TURNS | DT_CROWD_OPTIMIZE_VIS | DT_CROWD_OPTIMIZE_TOPO | DT_CROWD_OBSTACLE_AVOIDANCE | DT_CROWD_SEPARATION;
agent.obstacleAvoidanceType = 3;
for (int index = 0; index < 4; ++index) {
const float at[3] = {-5, 0, -6.0f + static_cast<float>(index)};
check(crowd->addAgent(at, &agent) == index);
check(crowd->requestMoveTarget(index, end_ref, end_at));
}
for (int step = 0; step < 600; ++step) crowd->update(1.0f / 60, nullptr);
std::vector<float> places;
for (int index = 0; index < 4; ++index) {
const dtCrowdAgent* walker = crowd->getAgent(index);
places.insert(places.end(), walker->npos, walker->npos + 3);
check(dtVdist2D(walker->npos, end_at) < 2.5f);
}
check(fnv(bytes) == 0xd50409b3475ce9afull && fnv(places) == 0x040f1c330fce843bull);
check(nav::DT_VERTS_PER_POLYGON == 6 && nav::RC_WALKABLE_AREA == 63 && dtStatusFailed(nav::DT_FAILURE));
std::println("Recast & Detour 1.6.0 through import mcpplibs.mwrap.recastnavigation: {} polygon refs to the far side, {} corners; the mesh {:#018x}, the crowd {:#018x}{}",
poly_count, corner_count, fnv(bytes), fnv(places), failures ? std::format(" — {} checks failed", failures) : "");
dtFreeCrowd(crowd), dtFreeNavMeshQuery(query), dtFreeNavMesh(mesh);
return failures;
}
Loading