egametang/ET · error · Exception

Invalid tile salt

Error message

Invalid tile salt

What it means

RemoveTile decoded a valid index but the salt stored on that tile no longer matches the salt encoded in the ref. The salt increments each time a slot is recycled, so a mismatch means the ref points to a tile that was removed and its slot has since been reused for a different tile. Removing it would corrupt the live tile, so the call is rejected.

Source

Thrown at Packages/cn.etetet.recast/Scripts/Core/Share/Detour.TileCache/DtTileCache.cs:303

        public void RemoveTile(long refs)
        {
            if (refs == 0)
            {
                throw new Exception("Invalid tile ref");
            }

            int tileIndex = DecodeTileIdTile(refs);
            int tileSalt = DecodeTileIdSalt(refs);
            if (tileIndex >= m_params.maxTiles)
            {
                throw new Exception("Invalid tile index");
            }

            DtCompressedTile tile = m_tiles[tileIndex];
            if (tile.salt != tileSalt)
            {
                throw new Exception("Invalid tile salt");
            }

            // Remove tile from hash lookup.
            int h = DtNavMesh.ComputeTileHash(tile.header.tx, tile.header.ty, m_tileLutMask);
            DtCompressedTile prev = null;
            DtCompressedTile cur = m_posLookup[h];
            while (cur != null)
            {
                if (cur == tile)
                {
                    if (prev != null)
                    {
                        prev.next = cur.next;
                    }
                    else
                    {
                        m_posLookup[h] = cur.next;
                    }

View on GitHub (pinned to 5cab01f7a8)

Solutions

  1. Zero out your stored ref immediately after a successful RemoveTile and guard against double-removal.
  2. Resolve the current ref by position (GetTileRef) right before removing rather than trusting a cached handle.
  3. Treat a salt mismatch as 'already removed' and continue gracefully rather than fatal.

Example fix

// before
cache.RemoveTile(ref);
cache.RemoveTile(ref); // throws: salt changed after first remove

// after
if (ref != 0) { cache.RemoveTile(ref); ref = 0; }
Defensive patterns

Strategy: try-catch

Validate before calling

// Resolve fresh; a recycled slot returns a different (or zero) ref
long ref = cache.GetTileRef(tx, ty, layer);
if (ref == 0) return; // already gone
cache.RemoveTile(ref);

Try / catch

try { cache.RemoveTile(ref); storedRef = 0; }
catch (Exception e) when (e.Message.Contains("Invalid tile salt"))
{ storedRef = 0; /* slot was recycled; treat as already removed */ }

Prevention

When it happens

Trigger: Calling RemoveTile twice with the same ref; removing a ref whose tile was already evicted by an obstacle update or streaming unload; holding a ref across a cache rebuild.

Common situations: Double-unload; an async streaming pipeline where the same tile is removed by two code paths; refs captured before a cache.Init/Reset and reused afterward.

Related errors


AI-assisted analysis of egametang/ET@5cab01f7a8 (2026-08-13). Data as JSON: /api/errors/bf8edb8c88098fe7. Report an issue: GitHub.