egametang/ET · error · Exception

Out of storage

Error message

Out of storage

What it means

Thrown by DtTileCache.AddTile when the free-list of compressed tiles (m_nextFreeTile) is empty. The cache pre-allocates exactly maxTiles slots at construction and hands them out as tiles are added; once all are in use, no slot can be assigned and the add fails. It indicates the cache is at capacity, not a memory allocation failure.

Source

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

            // Make sure the location is free.
            if (GetTileAt(header.tx, header.ty, header.tlayer) != null)
            {
                return 0;
            }

            // Allocate a tile.
            DtCompressedTile tile = null;
            if (m_nextFreeTile != null)
            {
                tile = m_nextFreeTile;
                m_nextFreeTile = tile.next;
                tile.next = null;
            }

            // Make sure we could allocate a tile.
            if (tile == null)
            {
                throw new Exception("Out of storage");
            }

            // Insert tile into the position lut.
            int h = DtNavMesh.ComputeTileHash(header.tx, header.ty, m_tileLutMask);
            tile.next = m_posLookup[h];
            m_posLookup[h] = tile;

            // Init tile.
            tile.header = header;
            tile.data = data;
            tile.compressed = Align4(buf.Position());
            tile.flags = flags;

            return GetTileRef(tile);
        }

        private int Align4(int i)
        {

View on GitHub (pinned to 5cab01f7a8)

Solutions

  1. Increase DtTileCacheParams.maxTiles to exceed the peak number of simultaneously live tiles (including obstacle-driven rebuilds).
  2. Ensure every load path has a matching RemoveTile on unload so the free-list is replenished.
  3. Drive DtTileCache.Update each frame so pending add/remove requests settle before the next batch of AddTile calls.

Example fix

// before: add without ever removing
foreach (var data in loaded) cache.AddTile(data, flags, 0);

// after: remove the tile at the same (tx,ty,layer) before re-adding, and size maxTiles to peak load
long oldRef = cache.GetTileRef(tx, ty, layer);
if (oldRef != 0) cache.RemoveTile(oldRef);
cache.AddTile(data, flags, 0);
Defensive patterns

Strategy: validation

Validate before calling

// Track live tile count against capacity before adding
if (liveTileCount >= cacheMaxTiles)
    throw new InvalidOperationException(
        $"DtTileCache at capacity ({liveTileCount}/{cacheMaxTiles}); " +
        "remove a tile or raise maxTiles.");
cache.AddTile(data, flags, 0);

Try / catch

try { return cache.AddTile(data, flags, 0); }
catch (Exception e) when (e.Message == "Out of storage")
{ /* evict least-recent tile, then retry once, or report capacity */ }

Prevention

When it happens

Trigger: Calling DtTileCache.AddTile(...) more than DtTileCacheParams.maxTiles times without matching RemoveTile calls; loading a tile set whose tile count exceeds the configured maxTiles; streaming new tiles while old ones are never evicted.

Common situations: maxTiles sized too small for the streamed world; obstacle/tile updates queuing many tiles before the update loop drains them; a tile loaded twice because RemoveTile was skipped on unload; mismatch between the maxTiles the cache was built with and the tile count the loader assumes.

Related errors


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