egametang/ET · error · Exception
Invalid tile salt
Error message
Invalid tile salt
What it means
Thrown by DtNavMesh.RemoveTile when the tile slot at the decoded index exists but its current salt does not match the salt encoded in refs. The salt is a generation counter bumped each time a slot is reused, so a mismatch means the handle refers to an older (or future) generation of that slot. The tile being pointed at is not the one the caller thinks.
Source
Thrown at Packages/cn.etetet.recast/Scripts/Core/Share/Detour/DtNavMesh.cs:587
/// @see #addTile
public long RemoveTile(long refs)
{
if (refs == 0)
{
return 0;
}
int tileIndex = DecodePolyIdTile(refs);
int tileSalt = DecodePolyIdSalt(refs);
if (tileIndex >= m_maxTiles)
{
throw new Exception("Invalid tile index");
}
DtMeshTile tile = m_tiles[tileIndex];
if (tile.salt != tileSalt)
{
throw new Exception("Invalid tile salt");
}
// Remove tile from hash lookup.
GetTileListByPos(tile.data.header.x, tile.data.header.y).Remove(tile);
// Remove connections to neighbour tiles.
// Create connections with neighbour tiles.
// Disconnect from other layers in current tile.
List<DtMeshTile> nneis = GetTilesAt(tile.data.header.x, tile.data.header.y);
foreach (DtMeshTile j in nneis)
{
if (j == tile)
{
continue;
}
UnconnectLinks(j, tile);View on GitHub (pinned to 5cab01f7a8)
Solutions
- Re-acquire the tile ref immediately before removal instead of caching it long-term.
- Invalidate stored refs whenever their tile is removed or updated.
- Treat a salt mismatch as 'already gone' and drop the ref rather than retrying.
- Reduce tile recycling churn if refs must be held longer (fewer add/remove cycles).
Example fix
// before
mesh.RemoveTile(entity.CachedTileRef); // ref may be stale
// after
long currentRef = mesh.GetTileRefAt(x, y, layer);
if (currentRef != 0)
mesh.RemoveTile(currentRef); Defensive patterns
Strategy: validation
Validate before calling
// Re-fetch the current ref at the tile position before removing. long current = mesh.GetTileRefAt(x, y, layer); if (current != 0) mesh.RemoveTile(current); // don't trust a long-cached ref
Type guard
// Salt cannot be read externally in all ports; treat any cached ref older than the latest add/remove cycle as invalid. static bool RefIsCurrent(long cached, long current) => cached == current;
Try / catch
try { mesh.RemoveTile(refs); }
catch (Exception e) when (e.Message.Contains("Invalid tile salt"))
{ // tile already recycled; treat as already removed } Prevention
- Re-query the tile ref right before removal rather than caching indefinitely.
- Invalidate cached refs on every add/remove cycle for that slot.
- Treat salt mismatch as benign 'already gone' and skip.
- Minimize tile recycling churn when refs must be held.
When it happens
Trigger: Calling RemoveTile with a refs whose tile was already removed and re-added (salt bumped); using a refs cached before a remove/add cycle; holding a refs across a navmesh rebuild.
Common situations: Caching tile refs in game objects for long-lived entities while the streaming system recycles tile slots; navigation queries returning refs that are then held and later used for removal after a tile update.
Related errors
- Could not find tile
- Invalid tile index
- Invalid number of verts per poly {header.maxVertsPerPoly}
- Empty path
- Invalid tile salt
AI-assisted analysis of egametang/ET@5cab01f7a8 (2026-08-13).
Data as JSON: /api/errors/c4f9f9a9dc10a8ca.
Report an issue: GitHub.