feat(nav): 17.3 - a map in square tiles (nav_tiled, nav_tile_build; a tile must be a whole number of cells or its seams never join, and the shim refuses one that is not; 64-bit polygon refs for 16384 tiles), ground as a NavGround with boulders as convex footprints, one save format for one tile or many, and nav_random_near - a reachable point from the caller's seed, bit-identical on the Mac and the PC. 10 tests on both
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
parent
9b8606483d
commit
e6893f58be
16 changed files with 436 additions and 109 deletions
|
|
@ -1,7 +1,8 @@
|
|||
#!/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. Objects go to build/native; a few seconds on eight cores.
|
||||
# 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"
|
||||
|
|
@ -14,7 +15,7 @@ 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 $INC"
|
||||
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)"
|
||||
|
||||
|
|
|
|||
|
|
@ -1,6 +1,6 @@
|
|||
// nav_build.inl - a navmesh from triangles: Recast's solo pipeline (rasterize, filter, erode,
|
||||
// regions, contours, polygons, detail) and one Detour tile. c[] is the configuration:
|
||||
// cell size, cell height, agent height, agent radius, max climb, max slope (degrees).
|
||||
// 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) {
|
||||
|
|
@ -10,12 +10,21 @@ static void nav_mark_cylinders(rcContext &ctx, rcCompactHeightfield &chf, const
|
|||
}
|
||||
}
|
||||
|
||||
// v: nv points (x, y, z); t: nt triangles; area: one per triangle, 0 not walkable, 1..62 a kind of
|
||||
// ground (its cost is the filter's); 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 *c) {
|
||||
rcContext ctx(false);
|
||||
rcConfig cfg;
|
||||
// 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];
|
||||
|
|
@ -30,11 +39,18 @@ NAV_SHIM void *nav_build(const float *v, int nv, const int *t, int nt, const uns
|
|||
cfg.maxVertsPerPoly = 6;
|
||||
cfg.detailSampleDist = cfg.cs * 6.0f;
|
||||
cfg.detailSampleMaxError = cfg.ch;
|
||||
rcCalcBounds(v, nv, cfg.bmin, cfg.bmax);
|
||||
rcCalcGridSize(cfg.bmin, cfg.bmax, cfg.cs, &cfg.width, &cfg.height);
|
||||
}
|
||||
|
||||
std::vector<unsigned char> areas(area, area + nt);
|
||||
rcClearUnwalkableTriangles(&ctx, cfg.walkableSlopeAngle, v, nv, t, nt, areas.data());
|
||||
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();
|
||||
|
|
@ -42,7 +58,7 @@ NAV_SHIM void *nav_build(const float *v, int nv, const int *t, int nt, const uns
|
|||
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, v, nv, t, areas.data(), nt, *hf, cfg.walkableClimb);
|
||||
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);
|
||||
|
|
@ -50,16 +66,16 @@ NAV_SHIM void *nav_build(const float *v, int nv, const int *t, int nt, const uns
|
|||
ok = rcBuildCompactHeightfield(&ctx, cfg.walkableHeight, cfg.walkableClimb, *hf, *chf);
|
||||
}
|
||||
if (ok) {
|
||||
nav_mark_cylinders(ctx, *chf, cyl, nc);
|
||||
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, 0, cfg.minRegionArea, cfg.mergeRegionArea) &&
|
||||
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);
|
||||
}
|
||||
unsigned char *data = nullptr;
|
||||
int size = 0;
|
||||
if (ok) {
|
||||
*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));
|
||||
|
|
@ -67,30 +83,28 @@ NAV_SHIM void *nav_build(const float *v, int nv, const int *t, int nt, const uns
|
|||
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 = c[2]; p.walkableRadius = c[3]; p.walkableClimb = c[4];
|
||||
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);
|
||||
ok = dtCreateNavMeshData(&p, data, size);
|
||||
}
|
||||
rcFreeHeightField(hf); rcFreeCompactHeightfield(chf); rcFreeContourSet(cs); rcFreePolyMesh(pm); rcFreePolyMeshDetail(dm);
|
||||
if (!ok) return nullptr;
|
||||
return nav_from(data, size);
|
||||
return ok;
|
||||
}
|
||||
|
||||
// the tile's bytes, to write to a file: first the size (buf null), then the copy
|
||||
NAV_SHIM int nav_save(void *h, unsigned char *buf, int cap) {
|
||||
Nav *n = static_cast<Nav *>(h);
|
||||
if (!buf) return n->size;
|
||||
if (cap < n->size) return -1;
|
||||
memcpy(buf, n->data, n->size);
|
||||
return n->size;
|
||||
}
|
||||
|
||||
// a navmesh from bytes nav_save wrote
|
||||
NAV_SHIM void *nav_load(const unsigned char *buf, int size) {
|
||||
unsigned char *data = static_cast<unsigned char *>(dtAlloc(size, DT_ALLOC_PERM));
|
||||
if (!data) return nullptr;
|
||||
memcpy(data, buf, size);
|
||||
// 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);
|
||||
}
|
||||
|
||||
|
|
@ -102,7 +116,13 @@ NAV_SHIM void nav_free(void *h) {
|
|||
delete n;
|
||||
}
|
||||
|
||||
// every polygon in every tile
|
||||
NAV_SHIM int nav_polys(void *h) {
|
||||
const dtMeshTile *t = static_cast<const dtNavMesh *>(static_cast<Nav *>(h)->mesh)->getTile(0);
|
||||
return t && t->header ? t->header->polyCount : 0;
|
||||
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;
|
||||
}
|
||||
|
|
|
|||
|
|
@ -51,3 +51,22 @@ NAV_SHIM int nav_raycast(void *h, float sx, float sy, float sz, float ex, float
|
|||
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;
|
||||
}
|
||||
|
|
|
|||
|
|
@ -27,24 +27,26 @@ struct Nav {
|
|||
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];
|
||||
unsigned char *data = nullptr; // the tile as built or loaded (the mesh frees it)
|
||||
int size = 0;
|
||||
int partial = 0; // the last path stopped short of its end
|
||||
};
|
||||
|
||||
Nav *nav_from(unsigned char *data, int size) {
|
||||
// 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 = dtAllocNavMesh();
|
||||
if (!n->mesh || dtStatusFailed(n->mesh->init(data, size, DT_TILE_FREE_DATA))) { dtFree(data); dtFreeNavMesh(n->mesh); delete n; return nullptr; }
|
||||
n->data = data;
|
||||
n->size = size;
|
||||
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"
|
||||
|
|
|
|||
102
packages/ludic.nav/native/shim/nav_tiles.inl
Normal file
102
packages/ludic.nav/native/shim/nav_tiles.inl
Normal 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);
|
||||
}
|
||||
Loading…
Add table
Add a link
Reference in a new issue