egametang/ET · error · Exception
Could not find tile
Error message
Could not find tile
What it means
Thrown by DtNavMesh.AddTile when restoring a previously-removed tile via a non-zero lastRef. The code decodes the tile index from lastRef, looks up m_tiles[tileIndex], then tries to remove that tile object from the availableTiles free list. If the tile is not on the free list (already reallocated, never freed, or lastRef points at a live tile), AddTile aborts. It indicates the lastRef handle is stale or points to a slot that is not actually free.
Source
Thrown at Packages/cn.etetet.recast/Scripts/Core/Share/Detour/DtNavMesh.cs:498
availableTiles.RemoveFirst();
m_tileCount++;
}
else
{
// Try to relocate the tile to specific index with same salt.
int tileIndex = DecodePolyIdTile(lastRef);
if (tileIndex >= m_maxTiles)
{
throw new Exception("Tile index too high");
}
// Try to find the specific tile id from the free list.
DtMeshTile target = m_tiles[tileIndex];
// Remove from freelist
if (!availableTiles.Remove(target))
{
// Could not find the correct location.
throw new Exception("Could not find tile");
}
tile = target;
// Restore salt.
tile.salt = DecodePolyIdSalt(lastRef);
}
tile.data = data;
tile.flags = flags;
tile.links.Clear();
tile.polyLinks = new int[data.polys.Length];
Array.Fill(tile.polyLinks, DtNavMesh.DT_NULL_LINK);
// Insert tile into the position lut.
GetTileListByPos(header.x, header.y).Add(tile);
// Patch header pointers.
View on GitHub (pinned to 5cab01f7a8)
Solutions
- Pass lastRef=0 for a fresh add so the navmesh allocates a new slot from the free list instead of relocating.
- Discard any cached lastRef after a navmesh rebuild or after the corresponding tile has been re-added by another caller.
- Track the live/freed state of refs yourself and only pass a lastRef that was returned by a RemoveTile you performed on the same DtNavMesh instance.
- Verify m_tileCount / free-list capacity before re-adding when streaming tiles concurrently.
Example fix
// before
long oldRef = mesh.AddTile(data, 0, storedLastRef); // storedLastRef may be stale
// after
long oldRef = storedLastRefIsValid
? mesh.AddTile(data, 0, storedLastRef)
: mesh.AddTile(data, 0, 0); // fresh allocation Defensive patterns
Strategy: try-catch
Validate before calling
// Guard a restore-add: only reuse lastRef if the slot is still free-ish. int idx = DtNavMesh.DecodePolyIdTile(lastRef); bool safe = idx < mesh.GetMaxTiles() && lastRef != 0 && refStillTracked; long ref2 = mesh.AddTile(data, flags, safe ? lastRef : 0);
Type guard
static bool IsValidRestoreRef(DtNavMesh mesh, long lastRef)
=> lastRef != 0 && DtNavMesh.DecodePolyIdTile(lastRef) < mesh.GetMaxTiles(); Try / catch
try { mesh.AddTile(data, flags, lastRef); }
catch (Exception e) when (e.Message.Contains("Could not find tile"))
{
// lastRef stale -> fall back to a fresh allocation
mesh.AddTile(data, flags, 0);
} Prevention
- Never cache lastRef across navmesh rebuilds; clear stored refs on rebuild.
- Only pass a lastRef that your own RemoveTile produced on the same DtNavMesh.
- In streaming pipelines, prefer lastRef=0 unless you explicitly relocate a slot.
- Invalidate entity-held refs whenever their tile is updated.
When it happens
Trigger: Calling AddTile(data, flags, lastRef) with a lastRef obtained from a tile that has since been re-added, re-removed, or was never removed; passing a refs belonging to a different DtNavMesh instance; reusing a stored lastRef across navmesh rebuilds that changed m_maxTiles.
Common situations: Streaming tile pipelines that cache a tile's old ref and try to re-add it after the slot was reused; serializing/deserializing navmesh state and feeding a stale lastRef back; calling AddTile with lastRef from RemoveTile but the navmesh was reconstructed in between.
Related errors
- Invalid tile index
- Invalid tile salt
- Invalid number of verts per poly {header.maxVertsPerPoly}
- Empty path
- Invalid magic
AI-assisted analysis of egametang/ET@5cab01f7a8 (2026-08-13).
Data as JSON: /api/errors/aa844e5a08919d3a.
Report an issue: GitHub.