merge lang/nav: ludic.nav over Recast & Detour (17.1)

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
Orkun ÇAKILKAYA 2026-09-27 15:47:14 +03:00
commit 4e2087d6eb
20 changed files with 914 additions and 0 deletions

View file

@ -30,6 +30,7 @@ section. The rules are in [ludic.base](ludic.base/README.md).
| [ludic.needs](ludic.needs/README.md) | a body's warmth, food, water and energy, and the countdown to a collapse |
| [ludic.npc](ludic.npc/README.md) | the other people in a place: a routine by the hour, walking round what is in the way, facing a player who comes near, lines as data, a guest's copy |
| [ludic.physics](ludic.physics/README.md) | rigid bodies, removable still shapes, queries and buoyancy over Jolt Physics (a native library, phase 16) |
| [ludic.nav](ludic.nav/README.md) | a navmesh per kind of walker and the ways across it, over Recast & Detour (a native library, phase 17) |
| [ludic.photo](ludic.photo/README.md) | a camera's photographs: what is in the frame, a grade on size, framing, light and the moment, the roll, its worth, the best of each subject |
| [ludic.save](ludic.save/README.md) | versioned save files: a migration chain the game declares, torn writes told apart, a backup, a newer file refused and read-only |
| [ludic.settings](ludic.settings/README.md) | a game's settings as data: one store, a fact per change, ranges, a safe set |

View file

@ -0,0 +1,67 @@
# ludic.nav
A walkable mesh of the world and the ways across it, over
[Recast & Detour](https://github.com/recastnavigation/recastnavigation) (zlib) built here from a
pinned tag (phase 17). Uses `ludic.base` and nothing else.
```ludic
import "ludic.nav"
```
## The rules it keeps
- **One mesh per kind of walker.** `NAV_PERSON` (0.35 m wide, the hiker's 0.55 m step),
`NAV_LARGE` (an elk, a bear, a horse) and `NAV_SMALL` (a hare, a marmot) each have their own,
because what a hare slips between a bear walks round. A `NavConfig` says what a mesh is built
for: its voxels and its walker's height, radius, step and steepest slope.
- **Built from ground, or loaded from a bake.** A `NavGround` is triangles - each with an area
byte, 0 not walkable and 1..62 a kind of ground - and what nothing stands in: cylinders (a trunk,
a post) and convex footprints (a boulder). Ground steeper than the slope is cleared whatever its
area. `nav_build` makes one mesh of it; a map is square tiles (`nav_tiled`, then `nav_tile_build`
for each tile from the ground under it and a few metres round), and a tile must be a whole number
of cells across or its seams never join - the shim refuses one that is not. A game bakes its maps
once and loads the bytes (`nav_save_file` / `nav_load_file`, one format for one tile or many): a
map is never built at start-up. Polygon refs are 64-bit, so a map may have 16384 tiles.
- **A path is corners.** `nav_path` answers the way from one point to another as the corners
where it turns, the first the start and the last the end. When the end cannot be reached the
path stops as near it as the mesh allows and `nav_partial` says so - a walker goes there and
gives up, never through a river. Either end off the mesh is `-1`.
- **A wander's goal is somewhere it can go.** `nav_random_near` answers a point about r away that a
walker standing at the start can reach - never across a river without a ford - from a seed the
caller draws from its own `Rng`, so the same seed is the same point on every machine.
- **A kind of ground has a cost** (`nav_area_cost`): a trail cheaper than a meadow, scree dearer.
- **The same question gets the same answer.** Detour is deterministic for the same mesh and the
same points, so co-op's order of dice is untouched by asking it.
## API
| | |
| --- | --- |
| `NAV_PERSON`, `NAV_LARGE`, `NAV_SMALL`, `NAV_CORNERS` | the kinds of walker, and a path's most corners (256) |
| `NavConfig { cell, cell_h, height, radius, climb, slope }` | what a mesh is built for (a person by default, 0.25 m cells) |
| `NavGround { v, nv, t, nt, area, cyl, nc, foot, nf }` | what it is built from: triangles, an area byte each, cylinders (`x, y, z, r, h`) and footprints (`n`, n points `x z`, `ymin`, `ymax`) |
| `nav_build(kind, ground, config) -> bool`, `nav_polygons(kind)` | one mesh over all of it |
| `nav_tiled(kind, ox, oz, tile, max_tiles, max_polys) -> bool`, `nav_tile_build(kind, tx, tz, ground, config) -> int` | a map of square tiles, a tile at a time (its polygons, 0 none, -1 failed) |
| `nav_save_file(kind, path)`, `nav_load_file(kind, path)`, `nav_reset()` | a baked mesh written and read; every mesh let go |
| `nav_nearest(kind, x, y, z) -> bool`, `nav_near_x/y/z()` | the nearest walkable point within a couple of metres |
| `nav_path(kind, sx, sy, sz, ex, ey, ez) -> int`, `nav_corners()`, `nav_corner_x/y/z(i)`, `nav_partial(kind)` | a way as corners |
| `nav_straight(kind, sx, sy, sz, ex, ez) -> bool` | does the straight line stay walkable |
| `nav_random_near(kind, x, y, z, r, seed) -> bool`, `nav_near_x/y/z()` | a reachable point about r away, from the caller's seed |
| `nav_area_cost(kind, area, cost)` | how dear a kind of ground is to cross |
## Tests
```bash
ludic test packages/ludic.nav
```
A 40 m meadow built by hand: a path round a post and a boulder's footprint, over a river's ford,
stopping at the bank of a river with none, the nearest point, random points that never cross the
river, and a saved mesh answering as the built one did. And a 128 m meadow as four tiles: a path
across the seams, a tile under water with no polygons, and the set saved and loaded.
## The native library
`native/build.sh` fetches Recast & Detour v1.6.0, checks its SHA-256, and builds Recast, Detour
and the shim (`native/shim/nav_shim.cpp`) into `lib/<target>/` - the same script on the Mac and on
the PC (Git Bash, the LLVM installer's clang). `native/LICENSE-recastnavigation` ships with it.

View file

@ -0,0 +1,62 @@
# build.ludic - a kind's mesh: whole from its ground, or tile by tile (a map), or from a bake's file
function nv_ground_ok(g: NavGround) -> bool {
if g.v == null or g.t == null or g.area == null { return false }
if len(g.v) < g.nv * 3 or len(g.t) < g.nt * 3 or len(g.area) < g.nt { return false }
if g.nc > 0 and (g.cyl == null or len(g.cyl) < g.nc * 5) { return false }
return g.nf == 0 or (g.foot != null and len(g.foot) >= g.nf)
}
function nv_cfg(c: NavConfig) -> []float {
let cfg = floats(6)
cfg[0] = c.cell
cfg[1] = c.cell_h
cfg[2] = c.height
cfg[3] = c.radius
cfg[4] = c.climb
cfg[5] = c.slope
return cfg
}
function nv_or_none(xs: []float) -> []float {
if xs == null { return floats(1) }
return xs
}
# one mesh over the whole of g
export function nav_build(nav_st: mut NavState, kind: int, g: NavGround, c: NavConfig) -> bool {
if not nv_ground_ok(g) { return false }
return nv_set(nav_st, kind, nvc_build(g.v, g.nv, g.t, g.nt, g.area, nv_or_none(g.cyl), g.nc, nv_or_none(g.foot), g.nf, nv_cfg(c)))
}
# an empty mesh of square tiles `tile` metres across from (ox, oz), for a map built a tile at a time
export function nav_tiled(nav_st: mut NavState, kind: int, ox: float, oz: float, tile: float, max_tiles: int, max_polys: int) -> bool {
return nv_set(nav_st, kind, nvc_tiled(ox, oz, tile, max_tiles, max_polys))
}
# tile (tx, tz) from ground g covering it and a few metres round it: its polygons, 0 none, -1 failed
export function nav_tile_build(nav_st: NavState, kind: int, tx: int, tz: int, g: NavGround, c: NavConfig) -> int {
let h = nv_mesh(nav_st, kind)
if h == null or not nv_ground_ok(g) { return -1 }
return nvc_tile_build(h, tx, tz, g.v, g.nv, g.t, g.nt, g.area, nv_or_none(g.cyl), g.nc, nv_or_none(g.foot), g.nf, nv_cfg(c))
}
# the polygons a kind's mesh has (0: none)
export function nav_polygons(nav_st: NavState, kind: int) -> int {
let h = nv_mesh(nav_st, kind)
if h == null { return 0 }
return nvc_polys(h)
}
# a kind's mesh written to a file, and read back
export function nav_save_file(nav_st: NavState, kind: int, path: string) -> bool {
let h = nv_mesh(nav_st, kind)
if h == null { return false }
let n = nvc_save(h, null, 0)
if n <= 0 { return false }
let buf = buffer(n)
if nvc_save(h, buf, n) != n { return false }
return Fs.write_bytes(path, buf, n)
}
export function nav_load_file(nav_st: mut NavState, kind: int, path: string) -> bool {
if not Fs.exists(path) { return false }
let buf = Fs.read_bytes(path)
if buf == null or len(buf) == 0 { return false }
return nv_set(nav_st, kind, nvc_load(buf, len(buf)))
}

View file

@ -0,0 +1,10 @@
# ludic.nav - a navmesh per kind of walker (a person, a large animal, a small one): built from
# triangles and the cylinders nothing stands in, or loaded from the bytes a bake saved; asked for
# the nearest walkable point, a path as corners, and whether a straight line stays walkable.
module ludic_nav uses ludic_base
numbers float
import "ludic.base"
import "native.ludic"
import "state.ludic"
import "build.ludic"
import "query.ludic"

BIN
packages/ludic.nav/lib/macos-arm64/libludicnav.dylib (Stored with Git LFS) Executable file

Binary file not shown.

BIN
packages/ludic.nav/lib/windows-x64/ludicnav.dll (Stored with Git LFS) Normal file

Binary file not shown.

BIN
packages/ludic.nav/lib/windows-x64/ludicnav.lib (Stored with Git LFS) Normal file

Binary file not shown.

View file

@ -0,0 +1,15 @@
# native.ludic - the shim's symbols (native/shim/nav_shim.cpp). A navmesh is a handle the package
# keeps; points come back through a float buffer it owns. None of this is exported.
extern function nvc_build(v: pointer, nv: int, t: pointer, nt: int, area: pointer, cyl: pointer, nc: int, foot: pointer, nf: int, c: pointer) -> pointer = "nav_build"
extern function nvc_tiled(ox: float, oz: float, tile: float, max_tiles: int, max_polys: int) -> pointer = "nav_tiled"
extern function nvc_tile_build(h: pointer, tx: int, tz: int, v: pointer, nv: int, t: pointer, nt: int, area: pointer, cyl: pointer, nc: int, foot: pointer, nf: int, c: pointer) -> int = "nav_tile_build"
extern function nvc_save(h: pointer, buf: pointer, cap: int) -> int = "nav_save"
extern function nvc_load(buf: pointer, size: int) -> pointer = "nav_load"
extern function nvc_free(h: pointer) -> void = "nav_free"
extern function nvc_polys(h: pointer) -> int = "nav_polys"
extern function nvc_nearest(h: pointer, x: float, y: float, z: float, out: pointer) -> int = "nav_nearest"
extern function nvc_area_cost(h: pointer, area: int, cost: float) -> void = "nav_area_cost"
extern function nvc_path(h: pointer, sx: float, sy: float, sz: float, ex: float, ey: float, ez: float, out: pointer, max: int) -> int = "nav_path"
extern function nvc_partial(h: pointer) -> int = "nav_partial"
extern function nvc_raycast(h: pointer, sx: float, sy: float, sz: float, ex: float, ez: float) -> int = "nav_raycast"
extern function nvc_random_near(h: pointer, x: float, y: float, z: float, r: float, seed: int, out: pointer) -> int = "nav_random_near"

View file

@ -0,0 +1,18 @@
Copyright (c) 2009 Mikko Mononen memon@inside.org
This software is provided 'as-is', without any express or implied
warranty. In no event will the authors be held liable for any damages
arising from the use of this software.
Permission is granted to anyone to use this software for any purpose,
including commercial applications, and to alter it and redistribute it
freely, subject to the following restrictions:
1. The origin of this software must not be misrepresented; you must not
claim that you wrote the original software. If you use this software
in a product, an acknowledgment in the product documentation would be
appreciated but is not required.
2. Altered source versions must be plainly marked as such, and must not be
misrepresented as being the original software.
3. This notice may not be removed or altered from any source distribution.

View file

@ -0,0 +1,28 @@
#!/bin/sh
# builds lib/<target>/ for ludic.nav: Recast & Detour at a pinned tag, and the shim over them.
# Recast builds the mesh, Detour answers it; DetourTileCache and DetourCrowd come with phases 17.8
# and 18. Polygon refs are 64-bit (DT_POLYREF64): an 8 km map in 64 m tiles is 16384 tiles, which
# 32-bit refs would leave 256 polygons each. Objects go to build/native; a few seconds on eight cores.
set -eu
PKG="$(cd "$(dirname "$0")/.." && pwd)"
. "$PKG/../../tools/native/lib.sh"
RC_TAG=v1.6.0
RC_SHA=d48ca0121962fa0639502c0f56c4e3ae72f98e55d88727225444f500775c0074
SRC="$PKG/build/src/recast-$RC_TAG"
native_fetch "$SRC" "https://github.com/recastnavigation/recastnavigation/archive/refs/tags/$RC_TAG.tar.gz" "$RC_SHA"
OBJ="$PKG/build/native/$(native_target)"
mkdir -p "$OBJ"
CXX="$(native_cxx)"
INC="-I$SRC/Recast/Include -I$SRC/Detour/Include"
FLAGS="$(native_cflags) -std=c++17 -ffp-contract=off -fno-exceptions -fno-rtti -DNDEBUG -DDT_POLYREF64 $INC"
case "$(native_target)" in windows-*) FLAGS="$FLAGS -D_CRT_SECURE_NO_WARNINGS" ;; esac
JOBS="$(getconf _NPROCESSORS_ONLN 2>/dev/null || echo 4)"
( cd "$SRC" && ls Recast/Source/*.cpp Detour/Source/*.cpp ) | while read -r f; do
o="$OBJ/$(basename "$f" .cpp).o"
[ "$o" -nt "$SRC/$f" ] || echo "$f $o"
done | xargs -P "$JOBS" -n 2 sh -c '"$0" '"$FLAGS"' -c "'"$SRC"'/$1" -o "$2"' "$CXX"
"$CXX" $FLAGS -I"$PKG/native/shim" -c "$PKG/native/shim/nav_shim.cpp" -o "$OBJ/nav_shim.o"
native_link "$PKG" ludicnav "$OBJ"/*.o

View file

@ -0,0 +1,128 @@
// nav_build.inl - Recast's pipeline, shared by a whole mesh and a tile: rasterize the triangles,
// filter, mark what nothing stands in, erode, regions, contours, polygons, detail, Detour's data.
// c[] is the configuration: cell size, cell height, agent height, radius, max climb, max slope.
// cylinders (x, y, z, r, h each) no agent may stand in: trunks, posts, tents
static void nav_mark_cylinders(rcContext &ctx, rcCompactHeightfield &chf, const float *cyl, int nc) {
for (int i = 0; i < nc; ++i) {
float pos[3] = {cyl[i * 5], cyl[i * 5 + 1], cyl[i * 5 + 2]};
rcMarkCylinderArea(&ctx, pos, cyl[i * 5 + 3], cyl[i * 5 + 4], RC_NULL_AREA, chf);
}
}
// convex footprints no agent may stand in (a boulder): each is n, n points (x, z), ymin, ymax
static void nav_mark_footprints(rcContext &ctx, rcCompactHeightfield &chf, const float *f, int nf) {
float pts[3 * 64];
int i = 0;
while (i < nf) {
int n = (int)f[i];
if (n < 3 || n > 64 || i + 1 + n * 2 + 2 > nf) return;
float y0 = f[i + 1 + n * 2], y1 = f[i + 2 + n * 2];
for (int k = 0; k < n; ++k) { pts[k * 3] = f[i + 1 + k * 2]; pts[k * 3 + 1] = y0; pts[k * 3 + 2] = f[i + 2 + k * 2]; }
rcMarkConvexPolyArea(&ctx, pts, n, y0, y1, RC_NULL_AREA, chf);
i += 3 + n * 2;
}
}
static void nav_config(rcConfig &cfg, const float *c) {
memset(&cfg, 0, sizeof(cfg));
cfg.cs = c[0];
cfg.ch = c[1];
cfg.walkableSlopeAngle = c[5];
cfg.walkableHeight = (int)ceilf(c[2] / cfg.ch);
cfg.walkableClimb = (int)floorf(c[4] / cfg.ch);
cfg.walkableRadius = (int)ceilf(c[3] / cfg.cs);
cfg.maxEdgeLen = (int)(12.0f / cfg.cs);
cfg.maxSimplificationError = 1.3f;
cfg.minRegionArea = 8 * 8;
cfg.mergeRegionArea = 20 * 20;
cfg.maxVertsPerPoly = 6;
cfg.detailSampleDist = cfg.cs * 6.0f;
cfg.detailSampleMaxError = cfg.ch;
}
struct NavIn {
const float *v; int nv; const int *t; int nt; const unsigned char *area;
const float *cyl; int nc; const float *foot; int nf; const float *c;
};
// the pipeline over cfg's box; the tile's Detour data into data / size. 0 polygons is not an error
static bool nav_pipeline(rcConfig &cfg, const NavIn &in, int tx, int tz, unsigned char **data, int *size, int *polys) {
rcContext ctx(false);
std::vector<unsigned char> areas(in.area, in.area + in.nt);
rcClearUnwalkableTriangles(&ctx, cfg.walkableSlopeAngle, in.v, in.nv, in.t, in.nt, areas.data());
rcHeightfield *hf = rcAllocHeightfield();
rcCompactHeightfield *chf = rcAllocCompactHeightfield();
rcContourSet *cs = rcAllocContourSet();
rcPolyMesh *pm = rcAllocPolyMesh();
rcPolyMeshDetail *dm = rcAllocPolyMeshDetail();
bool ok = hf && chf && cs && pm && dm &&
rcCreateHeightfield(&ctx, *hf, cfg.width, cfg.height, cfg.bmin, cfg.bmax, cfg.cs, cfg.ch) &&
rcRasterizeTriangles(&ctx, in.v, in.nv, in.t, areas.data(), in.nt, *hf, cfg.walkableClimb);
if (ok) {
rcFilterLowHangingWalkableObstacles(&ctx, cfg.walkableClimb, *hf);
rcFilterLedgeSpans(&ctx, cfg.walkableHeight, cfg.walkableClimb, *hf);
rcFilterWalkableLowHeightSpans(&ctx, cfg.walkableHeight, *hf);
ok = rcBuildCompactHeightfield(&ctx, cfg.walkableHeight, cfg.walkableClimb, *hf, *chf);
}
if (ok) {
nav_mark_cylinders(ctx, *chf, in.cyl, in.nc);
nav_mark_footprints(ctx, *chf, in.foot, in.nf);
ok = rcErodeWalkableArea(&ctx, cfg.walkableRadius, *chf) && rcBuildDistanceField(&ctx, *chf) &&
rcBuildRegions(&ctx, *chf, cfg.borderSize, cfg.minRegionArea, cfg.mergeRegionArea) &&
rcBuildContours(&ctx, *chf, cfg.maxSimplificationError, cfg.maxEdgeLen, *cs) &&
rcBuildPolyMesh(&ctx, *cs, cfg.maxVertsPerPoly, *pm) &&
rcBuildPolyMeshDetail(&ctx, *pm, *chf, cfg.detailSampleDist, cfg.detailSampleMaxError, *dm);
}
*polys = ok ? pm->npolys : 0;
if (ok && pm->npolys > 0) {
for (int i = 0; i < pm->npolys; ++i) pm->flags[i] = pm->areas[i] ? 1 : 0;
dtNavMeshCreateParams p;
memset(&p, 0, sizeof(p));
p.verts = pm->verts; p.vertCount = pm->nverts; p.polys = pm->polys; p.polyAreas = pm->areas;
p.polyFlags = pm->flags; p.polyCount = pm->npolys; p.nvp = pm->nvp;
p.detailMeshes = dm->meshes; p.detailVerts = dm->verts; p.detailVertsCount = dm->nverts;
p.detailTris = dm->tris; p.detailTriCount = dm->ntris;
p.walkableHeight = in.c[2]; p.walkableRadius = in.c[3]; p.walkableClimb = in.c[4];
p.tileX = tx; p.tileY = tz;
rcVcopy(p.bmin, pm->bmin); rcVcopy(p.bmax, pm->bmax);
p.cs = cfg.cs; p.ch = cfg.ch; p.buildBvTree = true;
ok = dtCreateNavMeshData(&p, data, size);
}
rcFreeHeightField(hf); rcFreeCompactHeightfield(chf); rcFreeContourSet(cs); rcFreePolyMesh(pm); rcFreePolyMeshDetail(dm);
return ok;
}
// one mesh over all the triangles given: v nv points (x, y, z); t nt triangles; area a byte each,
// 0 not walkable and 1..62 a kind of ground; the slope clears what is too steep whatever its area
NAV_SHIM void *nav_build(const float *v, int nv, const int *t, int nt, const unsigned char *area,
const float *cyl, int nc, const float *foot, int nf, const float *c) {
rcConfig cfg;
nav_config(cfg, c);
rcCalcBounds(v, nv, cfg.bmin, cfg.bmax);
rcCalcGridSize(cfg.bmin, cfg.bmax, cfg.cs, &cfg.width, &cfg.height);
NavIn in = {v, nv, t, nt, area, cyl, nc, foot, nf, c};
unsigned char *data = nullptr;
int size = 0, polys = 0;
if (!nav_pipeline(cfg, in, 0, 0, &data, &size, &polys) || polys == 0) return nullptr;
return nav_from(data, size);
}
NAV_SHIM void nav_free(void *h) {
Nav *n = static_cast<Nav *>(h);
if (!n) return;
dtFreeNavMeshQuery(n->query);
dtFreeNavMesh(n->mesh);
delete n;
}
// every polygon in every tile
NAV_SHIM int nav_polys(void *h) {
const dtNavMesh *m = static_cast<Nav *>(h)->mesh;
int total = 0;
for (int i = 0; i < m->getMaxTiles(); ++i) {
const dtMeshTile *t = m->getTile(i);
if (t && t->header) total += t->header->polyCount;
}
return total;
}

View file

@ -0,0 +1,72 @@
// nav_query.inl - what a navmesh answers: the nearest point on it, a path as corner points, and
// whether a straight line stays on it. Points go out through out[] (x, y, z each).
static dtPolyRef nav_poly_at(Nav *n, const float *p, float *on) {
dtPolyRef r = 0;
if (dtStatusFailed(n->query->findNearestPoly(p, n->ext, &n->filter, &r, on))) return 0;
return r;
}
// the nearest point on the mesh to (x, y, z) into out; 0 when none is within reach
NAV_SHIM int nav_nearest(void *h, float x, float y, float z, float *out) {
float p[3] = {x, y, z};
return nav_poly_at(static_cast<Nav *>(h), p, out) ? 1 : 0;
}
// a cost per kind of ground (area 1..62): the filter every query uses
NAV_SHIM void nav_area_cost(void *h, int area, float cost) {
Nav *n = static_cast<Nav *>(h);
if (area > 0 && area < DT_MAX_AREAS) n->filter.setAreaCost(area, cost);
}
// the corners of a path from one point to another, at most max of them, into out: the count, or
// -1 when either end is off the mesh. One that cannot reach the end stops as near it as the mesh
// allows, and nav_partial says so
NAV_SHIM int nav_path(void *h, float sx, float sy, float sz, float ex, float ey, float ez, float *out, int max) {
Nav *n = static_cast<Nav *>(h);
float s[3] = {sx, sy, sz}, e[3] = {ex, ey, ez}, so[3], eo[3];
dtPolyRef a = nav_poly_at(n, s, so), b = nav_poly_at(n, e, eo);
n->partial = 0;
if (!a || !b) return -1;
int np = 0;
dtStatus st = n->query->findPath(a, b, so, eo, &n->filter, n->polys, &np, NAV_MAX_POLYS);
if (dtStatusFailed(st) || np == 0) return -1;
if (dtStatusDetail(st, DT_PARTIAL_RESULT) || n->polys[np - 1] != b) n->partial = 1;
float end[3];
dtVcopy(end, eo);
if (n->polys[np - 1] != b) n->query->closestPointOnPoly(n->polys[np - 1], eo, end, nullptr);
int count = 0;
if (dtStatusFailed(n->query->findStraightPath(so, end, n->polys, np, out, nullptr, nullptr, &count, max, 0))) return -1;
return count;
}
NAV_SHIM int nav_partial(void *h) { return static_cast<Nav *>(h)->partial; }
// does the straight line from one point to another stay on the mesh? 1 yes, 0 no, -1 off it
NAV_SHIM int nav_raycast(void *h, float sx, float sy, float sz, float ex, float ez) {
Nav *n = static_cast<Nav *>(h);
float s[3] = {sx, sy, sz}, so[3], e[3] = {ex, sy, ez}, t = 0, nrm[3];
dtPolyRef a = nav_poly_at(n, s, so);
if (!a) return -1;
int np = 0;
if (dtStatusFailed(n->query->raycast(a, so, e, &n->filter, &t, nrm, n->polys, &np, NAV_MAX_POLYS))) return -1;
return t >= 1.0f ? 1 : 0;
}
// a point about r from (x, y, z) that a walker there can reach, into out; 0 when there is none.
// Detour asks a function for its dice: this one is seeded by the caller, so the same seed from the
// package's Rng gives the same point on every machine
static unsigned int nav_rs = 1;
static float nav_frand() {
nav_rs = nav_rs * 1664525u + 1013904223u;
return (float)(nav_rs >> 8) * (1.0f / 16777216.0f);
}
NAV_SHIM int nav_random_near(void *h, float x, float y, float z, float r, int seed, float *out) {
Nav *n = static_cast<Nav *>(h);
float p[3] = {x, y, z}, on[3];
dtPolyRef start = nav_poly_at(n, p, on);
if (!start) return 0;
nav_rs = (unsigned int)seed * 2654435761u + 1u;
dtPolyRef ref = 0;
if (dtStatusFailed(n->query->findRandomPointAroundCircle(start, on, r, &n->filter, nav_frand, &ref, out))) return 0;
return ref ? 1 : 0;
}

View file

@ -0,0 +1,52 @@
// nav_shim.cpp - ludic.nav's door into Recast & Detour (phase 17). The shim rules of
// packages/README.md: int, float and opaque handles cross; no struct by value, no callback into
// Ludic; what a query finds is written into a buffer the package owns; errors are return codes.
#include <Recast.h>
#include <DetourNavMesh.h>
#include <DetourNavMeshBuilder.h>
#include <DetourNavMeshQuery.h>
#include <DetourCommon.h>
#include <cstring>
#include <cmath>
#include <vector>
#if defined(_WIN32)
#define NAV_SHIM extern "C" __declspec(dllexport)
#else
#define NAV_SHIM extern "C" __attribute__((visibility("default")))
#endif
namespace {
const int NAV_MAX_POLYS = 512; // polygons a path may cross; a longer one is cut there
const int NAV_MAX_NODES = 4096; // the search's open list
// a navmesh and the one query that walks it, with the scratch a path needs
struct Nav {
dtNavMesh *mesh = nullptr;
dtNavMeshQuery *query = nullptr;
dtQueryFilter filter;
float ext[3] = {2.0f, 4.0f, 2.0f}; // how far to look for the mesh around a point
dtPolyRef polys[NAV_MAX_POLYS];
int partial = 0; // the last path stopped short of its end
};
// a navmesh with its query, from params (a set of tiles) or from one tile's data
Nav *nav_open(dtNavMesh *mesh) {
Nav *n = new Nav();
n->mesh = mesh;
n->query = dtAllocNavMeshQuery();
if (!n->query || dtStatusFailed(n->query->init(n->mesh, NAV_MAX_NODES))) { dtFreeNavMeshQuery(n->query); dtFreeNavMesh(n->mesh); delete n; return nullptr; }
n->filter.setIncludeFlags(0xffff);
n->filter.setExcludeFlags(0);
return n;
}
Nav *nav_from(unsigned char *data, int size) {
dtNavMesh *mesh = dtAllocNavMesh();
if (!mesh || dtStatusFailed(mesh->init(data, size, DT_TILE_FREE_DATA))) { dtFree(data); dtFreeNavMesh(mesh); return nullptr; }
return nav_open(mesh);
}
} // namespace
#include "nav_build.inl"
#include "nav_tiles.inl"
#include "nav_query.inl"

View file

@ -0,0 +1,102 @@
// nav_tiles.inl - a map as square tiles: a tiled mesh, one tile built from the triangles under it
// and a border round it, and every tile written and read back as one set of bytes
// an empty mesh of tiles `tile` metres across, the first at (ox, oz)
NAV_SHIM void *nav_tiled(float ox, float oz, float tile, int max_tiles, int max_polys) {
dtNavMeshParams p;
memset(&p, 0, sizeof(p));
p.orig[0] = ox; p.orig[1] = 0.0f; p.orig[2] = oz;
p.tileWidth = tile; p.tileHeight = tile;
p.maxTiles = max_tiles; p.maxPolys = max_polys;
dtNavMesh *mesh = dtAllocNavMesh();
if (!mesh || dtStatusFailed(mesh->init(&p))) { dtFreeNavMesh(mesh); return nullptr; }
return nav_open(mesh);
}
// tile (tx, tz) built from triangles that cover it and a border of the walker's radius and three
// cells round it, replacing whatever was there; its polygons, 0 for none, -1 when it failed
NAV_SHIM int nav_tile_build(void *h, int tx, int tz, const float *v, int nv, const int *t, int nt,
const unsigned char *area, const float *cyl, int nc, const float *foot, int nf, const float *c) {
Nav *n = static_cast<Nav *>(h);
const dtNavMeshParams *mp = n->mesh->getParams();
rcConfig cfg;
nav_config(cfg, c);
cfg.tileSize = (int)(mp->tileWidth / cfg.cs + 0.5f);
// a tile a cell short of its width leaves a seam its neighbour's polygons never meet
if (fabsf(cfg.tileSize * cfg.cs - mp->tileWidth) > 0.001f) return -1;
cfg.borderSize = cfg.walkableRadius + 3;
cfg.width = cfg.height = cfg.tileSize + cfg.borderSize * 2;
float lo[3], hi[3];
rcCalcBounds(v, nv, lo, hi);
cfg.bmin[0] = mp->orig[0] + tx * mp->tileWidth - cfg.borderSize * cfg.cs;
cfg.bmin[2] = mp->orig[2] + tz * mp->tileHeight - cfg.borderSize * cfg.cs;
cfg.bmax[0] = mp->orig[0] + (tx + 1) * mp->tileWidth + cfg.borderSize * cfg.cs;
cfg.bmax[2] = mp->orig[2] + (tz + 1) * mp->tileHeight + cfg.borderSize * cfg.cs;
cfg.bmin[1] = lo[1] - 1.0f;
cfg.bmax[1] = hi[1] + c[2] + 1.0f;
NavIn in = {v, nv, t, nt, area, cyl, nc, foot, nf, c};
unsigned char *data = nullptr;
int size = 0, polys = 0;
n->mesh->removeTile(n->mesh->getTileRefAt(tx, tz, 0), nullptr, nullptr);
if (!nav_pipeline(cfg, in, tx, tz, &data, &size, &polys)) return -1;
if (polys == 0) return 0;
if (dtStatusFailed(n->mesh->addTile(data, size, DT_TILE_FREE_DATA, 0, nullptr))) { dtFree(data); return -1; }
return polys;
}
// the set's bytes: "NAVT", a version, the mesh's params, then each tile's ref, size and data.
// First call with buf null for the size, then with a buffer that big
static const int NAV_MAGIC = 'N' << 24 | 'A' << 16 | 'V' << 8 | 'T';
static const int NAV_VERSION = 1;
static void nav_put(unsigned char *buf, int &at, const void *p, int n) {
if (buf) memcpy(buf + at, p, n);
at += n;
}
NAV_SHIM int nav_save(void *h, unsigned char *buf, int cap) {
const dtNavMesh *m = static_cast<Nav *>(h)->mesh;
const dtNavMeshParams *p = m->getParams();
int at = 0, count = 0;
for (int i = 0; i < m->getMaxTiles(); ++i) {
const dtMeshTile *t = m->getTile(i);
if (t && t->header && t->dataSize) count++;
}
nav_put(buf, at, &NAV_MAGIC, 4); nav_put(buf, at, &NAV_VERSION, 4); nav_put(buf, at, &count, 4);
nav_put(buf, at, p->orig, 12); nav_put(buf, at, &p->tileWidth, 4); nav_put(buf, at, &p->tileHeight, 4);
nav_put(buf, at, &p->maxTiles, 4); nav_put(buf, at, &p->maxPolys, 4);
for (int i = 0; i < m->getMaxTiles(); ++i) {
const dtMeshTile *t = m->getTile(i);
if (!t || !t->header || !t->dataSize) continue;
unsigned long long ref = (unsigned long long)m->getTileRef(t);
if (buf && at + 12 + t->dataSize > cap) return -1;
nav_put(buf, at, &ref, 8); nav_put(buf, at, &t->dataSize, 4); nav_put(buf, at, t->data, t->dataSize);
}
return at;
}
// a mesh from bytes nav_save wrote; null for anything else
NAV_SHIM void *nav_load(const unsigned char *buf, int size) {
int at = 0, magic = 0, version = 0, count = 0;
if (size < 40) return nullptr;
memcpy(&magic, buf, 4); memcpy(&version, buf + 4, 4); memcpy(&count, buf + 8, 4);
if (magic != NAV_MAGIC || version != NAV_VERSION) return nullptr;
dtNavMeshParams p;
memset(&p, 0, sizeof(p));
memcpy(p.orig, buf + 12, 12); memcpy(&p.tileWidth, buf + 24, 4); memcpy(&p.tileHeight, buf + 28, 4);
memcpy(&p.maxTiles, buf + 32, 4); memcpy(&p.maxPolys, buf + 36, 4);
at = 40;
dtNavMesh *mesh = dtAllocNavMesh();
if (!mesh || dtStatusFailed(mesh->init(&p))) { dtFreeNavMesh(mesh); return nullptr; }
for (int i = 0; i < count; ++i) {
unsigned long long ref = 0;
int n = 0;
if (at + 12 > size) break;
memcpy(&ref, buf + at, 8); memcpy(&n, buf + at + 8, 4);
at += 12;
if (n <= 0 || at + n > size) break;
unsigned char *data = static_cast<unsigned char *>(dtAlloc(n, DT_ALLOC_PERM));
memcpy(data, buf + at, n);
at += n;
if (dtStatusFailed(mesh->addTile(data, n, DT_TILE_FREE_DATA, (dtTileRef)ref, nullptr))) dtFree(data);
}
return nav_open(mesh);
}

View file

@ -0,0 +1,7 @@
# ludic.nav - a walkable mesh of the world and the ways across it, over Recast & Detour (phase 17),
# built here from a pinned tag (native/build.sh). Uses ludic.base and nothing else.
package "ludic.nav"
version "0.1.0"
kind source
native "macos-arm64" "lib/macos-arm64/libludicnav.dylib"
native "windows-x64" "lib/windows-x64/ludicnav.dll"

View file

@ -0,0 +1,51 @@
# query.ludic - what a kind's mesh answers. A path is its corners, read back one at a time
# (nav_corner_x / _y / _z); the nearest point the same (nav_near_x / _y / _z)
# the nearest walkable point to (x, y, z) within a couple of metres; false when there is none
export function nav_nearest(nav_st: NavState, kind: int, x: float, y: float, z: float) -> bool {
let h = nv_mesh(nav_st, kind)
if h == null { return false }
return nvc_nearest(h, x, y, z, nav_st.nv_near) == 1
}
export function nav_near_x(nav_st: NavState) -> float { return nav_st.nv_near[0] }
export function nav_near_y(nav_st: NavState) -> float { return nav_st.nv_near[1] }
export function nav_near_z(nav_st: NavState) -> float { return nav_st.nv_near[2] }
# a point about r from (x, y, z) that a walker standing there can reach (a wander's goal), read back
# as the nearest is; the seed is the caller's dice, so the same seed is the same point everywhere
export function nav_random_near(nav_st: NavState, kind: int, x: float, y: float, z: float, r: float, seed: int) -> bool {
let h = nv_mesh(nav_st, kind)
if h == null { return false }
return nvc_random_near(h, x, y, z, r, seed, nav_st.nv_near) == 1
}
# a way from one point to another: its corners, first the start and last the end (or as near the end
# as the mesh reaches - nav_partial says which); -1 when either end is off the mesh
export function nav_path(nav_st: mut NavState, kind: int, sx: float, sy: float, sz: float, ex: float, ey: float, ez: float) -> int {
let h = nv_mesh(nav_st, kind)
nav_st.nv_count = 0
if h == null { return -1 }
let n = nvc_path(h, sx, sy, sz, ex, ey, ez, nav_st.nv_out, NAV_CORNERS)
if n > 0 { nav_st.nv_count = n }
return n
}
export function nav_partial(nav_st: NavState, kind: int) -> bool {
let h = nv_mesh(nav_st, kind)
return h != null and nvc_partial(h) == 1
}
export function nav_corners(nav_st: NavState) -> int { return nav_st.nv_count }
export function nav_corner_x(nav_st: NavState, i: int) -> float { return nav_st.nv_out[i * 3] }
export function nav_corner_y(nav_st: NavState, i: int) -> float { return nav_st.nv_out[i * 3 + 1] }
export function nav_corner_z(nav_st: NavState, i: int) -> float { return nav_st.nv_out[i * 3 + 2] }
# does the straight line from (sx, sy, sz) to (ex, ez) stay walkable?
export function nav_straight(nav_st: NavState, kind: int, sx: float, sy: float, sz: float, ex: float, ez: float) -> bool {
let h = nv_mesh(nav_st, kind)
return h != null and nvc_raycast(h, sx, sy, sz, ex, ez) == 1
}
# how dear a kind of ground is to cross (1 is plain; a trail cheaper, scree dearer)
export function nav_area_cost(nav_st: NavState, kind: int, area: int, cost: float) -> void {
let h = nv_mesh(nav_st, kind)
if h != null { nvc_area_cost(h, area, cost) }
}

View file

@ -0,0 +1,62 @@
# state.ludic - the kinds of walker, what a mesh is built for, and what the package holds: a mesh
# per kind, and the last path's corners and nearest point, read back through the verbs
export const NAV_PERSON: int = 0 # 0.35 m wide, steps what a hiker steps (CHAR_STEP)
export const NAV_LARGE: int = 1 # an elk, a bear, a horse
export const NAV_SMALL: int = 2 # a hare, a marmot
export const NAV_KINDS: int = 3
export const NAV_CORNERS: int = 256 # a path's corners at most; a longer one is cut there
# what a mesh is built for: its voxels (cell across, cell high) and its walker (height, radius,
# the step it takes whatever its gradient, the steepest ground it walks, degrees). A tile must be a
# whole number of cells across, or its seams never join: 0.25 m is 256 to a 64 m tile
export property NavConfig {
cell: float = 0.25
cell_h: float = 0.2
height: float = 1.8
radius: float = 0.35
climb: float = 0.55
slope: float = 45.0
}
# what a mesh is built from: v holds nv points (x, y, z) and t nt triangles (three indices each);
# area a byte per triangle, 0 not walkable and 1..62 a kind of ground; cyl nc cylinders (x, y, z,
# r, h) and foot nf floats of convex footprints (n, n points x z, ymin, ymax) nothing stands in
export property NavGround {
v: []float = null
nv: int = 0
t: []int = null
nt: int = 0
area: []byte = null
cyl: []float = null
nc: int = 0
foot: []float = null
nf: int = 0
}
export state NavState {
nv_meshes: []pointer = nv_none()
nv_out: []float = floats(NAV_CORNERS * 3)
nv_count: int = 0
nv_near: []float = floats(3)
}
function nv_none() -> []pointer {
let m = new []pointer
for k in 0 .. NAV_KINDS { push(m, null) }
return m
}
function nv_mesh(nav_st: NavState, kind: int) -> pointer {
if kind < 0 or kind >= NAV_KINDS { return null }
return nav_st.nv_meshes[kind]
}
function nv_set(nav_st: mut NavState, kind: int, h: pointer) -> bool {
if kind < 0 or kind >= NAV_KINDS { return false }
if nav_st.nv_meshes[kind] != null { nvc_free(nav_st.nv_meshes[kind]) }
nav_st.nv_meshes[kind] = h
return h != null
}
# every mesh let go: a new map, or the world closed
export function nav_reset(nav_st: mut NavState) -> void {
for k in 0 .. NAV_KINDS { nv_set(nav_st, k, null) }
nav_st.nv_count = 0
}

View file

@ -0,0 +1,61 @@
# fakes/meadow.ludic - ground built by hand for ludic.nav's tests: n metre squares a side, flat at
# y = 0; river 1 crosses x 18..22 with a ford at z 30..34, river 2 without one, river 0 is none
function ground(n: int, river: int) -> NavGround {
let g = new NavGround
g.nv = (n + 1) * (n + 1)
g.v = floats(g.nv * 3)
for j in 0 .. n + 1 {
for i in 0 .. n + 1 {
let o = (j * (n + 1) + i) * 3
g.v[o] = float(i)
g.v[o + 2] = float(j)
}
}
g.nt = n * n * 2
g.t = new []int
g.area = buffer(g.nt)
for j in 0 .. n {
for i in 0 .. n {
let a = j * (n + 1) + i
push(g.t, a)
push(g.t, a + n + 1)
push(g.t, a + 1)
push(g.t, a + 1)
push(g.t, a + n + 1)
push(g.t, a + n + 2)
var k = 1
if river > 0 and i >= 18 and i < 22 and not (river == 1 and j >= 30 and j < 34) { k = 0 }
g.area[(j * n + i) * 2] = k
g.area[(j * n + i) * 2 + 1] = k
}
}
return g
}
# a post r wide at (x, z), from under the ground to over a walker's head
function post_at(x: float, z: float, r: float) -> []float {
let c = floats(5)
c[0] = x
c[1] = -1.0
c[2] = z
c[3] = r
c[4] = 4.0
return c
}
# a square footprint 2h across at (x, z), as a boulder's is written: n, the corners, ymin, ymax
function square_at(x: float, z: float, h: float) -> []float {
let f = floats(11)
f[0] = 4.0
f[1] = x - h
f[2] = z - h
f[3] = x + h
f[4] = z - h
f[5] = x + h
f[6] = z + h
f[7] = x - h
f[8] = z + h
f[9] = -1.0
f[10] = 3.0
return f
}

View file

@ -0,0 +1,103 @@
# nav_test.ludic - ludic.nav on ground built by hand: a 40 m meadow of metre squares, a post in its
# middle, a river across it (area 0) with a ford or without one. A path goes round the post and over
# the ford, stops at the bank when there is none, and a saved mesh answers as the built one did.
import "ludic.nav"
import "ludic.base"
import "fakes/meadow.ludic"
program NavTest {
numbers float
function meadow(nav_st: mut NavState, kind: int, river: int, post: bool) -> bool {
let g = ground(40, river)
if post { g.cyl = post_at(20.0, 20.0, 2.0) }
if post { g.nc = 1 }
return nav_build(nav_st, kind, g, new NavConfig)
}
function last_x(nav_st: NavState) -> float { return nav_corner_x(nav_st, nav_corners(nav_st) - 1) }
function last_z(nav_st: NavState) -> float { return nav_corner_z(nav_st, nav_corners(nav_st) - 1) }
test "a path goes round a post, and the straight line through it is not walkable" (nav_st: mut NavState) {
expect(meadow(nav_st, NAV_PERSON, 0, true))
expect(nav_polygons(nav_st, NAV_PERSON) > 0)
let n = nav_path(nav_st, NAV_PERSON, 5.0, 0.0, 20.0, 35.0, 0.0, 20.0)
expect(n >= 3)
expect(not nav_partial(nav_st, NAV_PERSON))
expect(Math.abs(last_x(nav_st) - 35.0) < 0.1)
for i in 0 .. n {
let dx = nav_corner_x(nav_st, i) - 20.0
let dz = nav_corner_z(nav_st, i) - 20.0
expect(dx * dx + dz * dz > 2.0 * 2.0)
}
expect(not nav_straight(nav_st, NAV_PERSON, 5.0, 0.0, 20.0, 35.0, 20.0))
expect(nav_straight(nav_st, NAV_PERSON, 5.0, 0.0, 5.0, 35.0, 5.0))
}
test "a river is crossed at its ford" (nav_st: mut NavState) {
expect(meadow(nav_st, NAV_PERSON, 1, false))
let n = nav_path(nav_st, NAV_PERSON, 5.0, 0.0, 10.0, 35.0, 0.0, 10.0)
expect(n >= 3)
expect(not nav_partial(nav_st, NAV_PERSON))
var ford = false
for i in 0 .. n { if nav_corner_z(nav_st, i) > 29.0 { ford = true } }
expect(ford)
}
test "without a ford the path stops at the near bank and says so" (nav_st: mut NavState) {
expect(meadow(nav_st, NAV_PERSON, 2, false))
let n = nav_path(nav_st, NAV_PERSON, 5.0, 0.0, 10.0, 35.0, 0.0, 10.0)
expect(n >= 1)
expect(nav_partial(nav_st, NAV_PERSON))
expect(last_x(nav_st) < 18.0)
}
test "the nearest walkable point, and nothing off the mesh" (nav_st: mut NavState) {
expect(meadow(nav_st, NAV_PERSON, 0, false))
expect(nav_nearest(nav_st, NAV_PERSON, 10.0, 1.0, 10.0))
expect(Math.abs(nav_near_y(nav_st)) < 0.3)
expect(Math.abs(nav_near_x(nav_st) - 10.0) < 0.1)
expect(not nav_nearest(nav_st, NAV_PERSON, -50.0, 0.0, -50.0))
expect_eq(nav_path(nav_st, NAV_PERSON, -50.0, 0.0, -50.0, 10.0, 0.0, 10.0), -1)
expect_eq(nav_path(nav_st, NAV_LARGE, 5.0, 0.0, 5.0, 10.0, 0.0, 10.0), -1)
}
test "a random point near is always one a walker there can reach, and a seed is always the same point" (nav_st: mut NavState) {
expect(meadow(nav_st, NAV_PERSON, 2, false))
for seed in 0 .. 50 {
expect(nav_random_near(nav_st, NAV_PERSON, 5.0, 0.0, 10.0, 30.0, seed))
expect(nav_near_x(nav_st) < 18.0)
}
nav_random_near(nav_st, NAV_PERSON, 5.0, 0.0, 10.0, 30.0, 7)
let x7 = nav_near_x(nav_st)
let z7 = nav_near_z(nav_st)
nav_random_near(nav_st, NAV_PERSON, 5.0, 0.0, 10.0, 30.0, 8)
expect(Math.abs(nav_near_x(nav_st) - x7) + Math.abs(nav_near_z(nav_st) - z7) > 0.01)
nav_random_near(nav_st, NAV_PERSON, 5.0, 0.0, 10.0, 30.0, 7)
expect_eq(nav_near_x(nav_st), x7)
expect_eq(nav_near_z(nav_st), z7)
}
test "a boulder's footprint is walked round" (nav_st: mut NavState) {
let g = ground(40, 0)
g.foot = square_at(20.0, 20.0, 3.0)
g.nf = len(g.foot)
expect(nav_build(nav_st, NAV_PERSON, g, new NavConfig))
expect(not nav_straight(nav_st, NAV_PERSON, 5.0, 0.0, 20.0, 35.0, 20.0))
expect(nav_path(nav_st, NAV_PERSON, 5.0, 0.0, 20.0, 35.0, 0.0, 20.0) >= 3)
}
test "a mesh saved and loaded answers as the built one did" (nav_st: mut NavState) {
expect(meadow(nav_st, NAV_PERSON, 1, true))
let path = Os.temp_dir() + "/ludic_nav_test.navmesh"
expect(nav_save_file(nav_st, NAV_PERSON, path))
expect(nav_load_file(nav_st, NAV_SMALL, path))
let a = nav_path(nav_st, NAV_PERSON, 5.0, 0.0, 10.0, 35.0, 0.0, 10.0)
let ax = last_x(nav_st)
let az = last_z(nav_st)
let b = nav_path(nav_st, NAV_SMALL, 5.0, 0.0, 10.0, 35.0, 0.0, 10.0)
expect_eq(a, b)
expect_near(last_x(nav_st), ax, 0.0001)
expect_near(last_z(nav_st), az, 0.0001)
nav_reset(nav_st)
expect_eq(nav_polygons(nav_st, NAV_SMALL), 0)
}
}

View file

@ -0,0 +1,66 @@
# tiles_test.ludic - ludic.nav's map in tiles: a 128 m meadow as four 64 m tiles, one of them under
# water. A path crosses the tiles' seams, the water's tile has no polygons and nothing inside it is
# a way's end, and the set saved and loaded answers as the built one did.
import "ludic.nav"
import "ludic.base"
import "fakes/meadow.ludic"
program TilesTest {
numbers float
# four tiles over ground(128), the far corner's squares (x and z past 64) under water when wet
function map(nav_st: mut NavState, wet: bool) -> int {
let g = ground(128, 0)
if wet {
for j in 64 .. 128 { for i in 64 .. 128 { g.area[(j * 128 + i) * 2] = 0 } }
for j in 64 .. 128 { for i in 64 .. 128 { g.area[(j * 128 + i) * 2 + 1] = 0 } }
}
g.cyl = post_at(96.0, 20.0, 2.0)
g.nc = 1
expect(nav_tiled(nav_st, NAV_PERSON, 0.0, 0.0, 64.0, 16, 4096))
var empty = 0
for tz in 0 .. 2 {
for tx in 0 .. 2 {
let p = nav_tile_build(nav_st, NAV_PERSON, tx, tz, g, new NavConfig)
expect(p >= 0)
if p == 0 { empty += 1 }
}
}
return empty
}
function last_x(nav_st: NavState) -> float { return nav_corner_x(nav_st, nav_corners(nav_st) - 1) }
function last_z(nav_st: NavState) -> float { return nav_corner_z(nav_st, nav_corners(nav_st) - 1) }
test "a path crosses the tiles' seams and goes round the post in the far tile" (nav_st: mut NavState) {
expect_eq(map(nav_st, false), 0)
expect(nav_polygons(nav_st, NAV_PERSON) > 0)
let n = nav_path(nav_st, NAV_PERSON, 10.0, 0.0, 20.0, 120.0, 0.0, 20.0)
expect(n >= 3)
expect(not nav_partial(nav_st, NAV_PERSON))
expect(Math.abs(last_x(nav_st) - 120.0) < 0.1)
let d = nav_path(nav_st, NAV_PERSON, 10.0, 0.0, 10.0, 120.0, 0.0, 120.0)
expect(d >= 2)
expect(not nav_partial(nav_st, NAV_PERSON))
}
test "a tile under water has no polygons: a way into it is none, the shore beside it is reached" (nav_st: mut NavState) {
expect_eq(map(nav_st, true), 1)
expect(not nav_nearest(nav_st, NAV_PERSON, 100.0, 0.0, 100.0))
expect_eq(nav_path(nav_st, NAV_PERSON, 10.0, 0.0, 10.0, 70.0, 0.0, 70.0), -1)
expect(nav_path(nav_st, NAV_PERSON, 10.0, 0.0, 10.0, 62.0, 0.0, 70.0) >= 2)
expect(not nav_partial(nav_st, NAV_PERSON))
expect(Math.abs(last_z(nav_st) - 70.0) < 0.1)
}
test "the tiles saved and loaded answer as the built ones did" (nav_st: mut NavState) {
map(nav_st, true)
let path = Os.temp_dir() + "/ludic_nav_tiles.navmesh"
expect(nav_save_file(nav_st, NAV_PERSON, path))
expect(nav_load_file(nav_st, NAV_LARGE, path))
expect_eq(nav_polygons(nav_st, NAV_LARGE), nav_polygons(nav_st, NAV_PERSON))
let a = nav_path(nav_st, NAV_PERSON, 10.0, 0.0, 20.0, 120.0, 0.0, 20.0)
let ax = last_x(nav_st)
let b = nav_path(nav_st, NAV_LARGE, 10.0, 0.0, 20.0, 120.0, 0.0, 20.0)
expect_eq(a, b)
expect_near(last_x(nav_st), ax, 0.0001)
}
}