{"record":{"id":"bf8edb8c88098fe7","repo":"egametang/ET","slug":"invalid-tile-salt","errorCode":null,"errorMessage":"Invalid tile salt","messagePattern":"Invalid tile salt","errorType":"exception","errorClass":"Exception","httpStatus":null,"severity":"error","filePath":"Packages/cn.etetet.recast/Scripts/Core/Share/Detour.TileCache/DtTileCache.cs","lineNumber":303,"sourceCode":"\n        public void RemoveTile(long refs)\n        {\n            if (refs == 0)\n            {\n                throw new Exception(\"Invalid tile ref\");\n            }\n\n            int tileIndex = DecodeTileIdTile(refs);\n            int tileSalt = DecodeTileIdSalt(refs);\n            if (tileIndex >= m_params.maxTiles)\n            {\n                throw new Exception(\"Invalid tile index\");\n            }\n\n            DtCompressedTile tile = m_tiles[tileIndex];\n            if (tile.salt != tileSalt)\n            {\n                throw new Exception(\"Invalid tile salt\");\n            }\n\n            // Remove tile from hash lookup.\n            int h = DtNavMesh.ComputeTileHash(tile.header.tx, tile.header.ty, m_tileLutMask);\n            DtCompressedTile prev = null;\n            DtCompressedTile cur = m_posLookup[h];\n            while (cur != null)\n            {\n                if (cur == tile)\n                {\n                    if (prev != null)\n                    {\n                        prev.next = cur.next;\n                    }\n                    else\n                    {\n                        m_posLookup[h] = cur.next;\n                    }","sourceCodeStart":285,"sourceCodeEnd":321,"githubUrl":"https://github.com/egametang/ET/blob/5cab01f7a8bee5f49f4781eebe9e2b1c6d7ebe0f/Packages/cn.etetet.recast/Scripts/Core/Share/Detour.TileCache/DtTileCache.cs#L285-L321","documentation":"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.","triggerScenarios":"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.","commonSituations":"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.","solutions":["Zero out your stored ref immediately after a successful RemoveTile and guard against double-removal.","Resolve the current ref by position (GetTileRef) right before removing rather than trusting a cached handle.","Treat a salt mismatch as 'already removed' and continue gracefully rather than fatal."],"exampleFix":"// before\ncache.RemoveTile(ref);\ncache.RemoveTile(ref); // throws: salt changed after first remove\n\n// after\nif (ref != 0) { cache.RemoveTile(ref); ref = 0; }","handlingStrategy":"try-catch","validationCode":"// Resolve fresh; a recycled slot returns a different (or zero) ref\nlong ref = cache.GetTileRef(tx, ty, layer);\nif (ref == 0) return; // already gone\ncache.RemoveTile(ref);","typeGuard":null,"tryCatchPattern":"try { cache.RemoveTile(ref); storedRef = 0; }\ncatch (Exception e) when (e.Message.Contains(\"Invalid tile salt\"))\n{ storedRef = 0; /* slot was recycled; treat as already removed */ }","preventionTips":["Clear stored refs immediately after removal.","Resolve the current ref by position instead of trusting a cached handle.","Treat salt mismatch as a benign 'already removed' condition."],"tags":["validation","detour-tilecache","stale-ref","salt"],"backgroundTag":null,"analyzedSha":"5cab01f7a8bee5f49f4781eebe9e2b1c6d7ebe0f","analyzedAt":"2026-08-13T21:10:40.377Z","schemaVersion":2},"datasetVersion":"2026-08-14T00:17:13.853Z"}