#31964·TrinityCore

vmap4assembler produces child-map tiles that StaticMapTree::LoadMapTile always rejects

Author: RomanKern89Created Aug 8, 2026Updated Aug 8, 2026
LabelsBranch-master

vmap4assembler produces child-map tiles that StaticMapTree::LoadMapTile always rejects

Summary

When a child map inherits only part of a parent map's tile geometry, vmap4assembler writes a standalone .vmtileidx whose spawn count is the number of inherited spawns, while StaticMapTree::LoadMapTile compares that count against the parent's .vmtile count and rejects the tile when they differ.

The tile is then never loaded — no VMAP collision, line-of-sight or height data for that tile on that map — and the server logs an error on every load attempt.

Affected

master @ f1d9bbe9ca3d (2026-08-07), Linux x86_64, RelWithDebInfo. Data extracted from retail 12.0.7.68974.

Measured across a full vmaps tree (117,418 .vmtileidx, 28,320 .vmtile):

tiles inheriting parent geometry 89,098
counts equal — load fine 88,890
indices fewer than parent spawns — always rejected 208
indices more than parent spawns 0

208 tiles across 17 maps, including real instances: Tomb of Sargeras (1676, 22 tiles), Battle of Dazar'alor (2070, 19), Siege of Boralus (1822, 8), Theater of Pain (2293, 4), Plaguefall (2289, 2), Shrine of the Storm (1864, 2).

The mismatch is one-directional — indices are never more than spawns, which is what partial inheritance predicts and data corruption would not.

Why it is not stale or corrupt extraction data

Re-running vmap4assembler from the original Buildings/dir_bin inputs reproduces the same counts byte for byte:

tile     | shipped vmaps idx/tile | freshly assembled idx/tile
27_25    |      7/660             |      7/660
19_30    |      3/587             |      3/587
20_29    |      2/304             |      2/304
21_29    |      2/69              |      2/69
23_30    |     11/154             |     11/154
21_30    |   1415/2318            |   1415/2318
23_26    |     15/213             |     15/213
16_26    |     40/199             |     40/199

(map 1950 "8.0 Boost Experience - Horde", parent 571 Draenor; on freshly assembled data 411 of its tiles inherit geometry and 32 of those are rejected.)

Mechanism

TileAssembler.cpp — for a tile that has only parent-flagged spawns (MOD_PARENT_SPAWN, set in adtfile.cpp:216 when map_num != originalMapId), the assembler writes a standalone index file with the count of those spawns:

cpp
for (auto const& [tileId, spawns] : data.ParentTileEntries)
{
    if (data.TileEntries.contains(tileId))
        continue;
    ...
    uint32 nSpawns = spawns.size();          // only the inherited spawns
    fwrite(&nSpawns, sizeof(uint32), 1, tileSpawnIndicesFile.get());
    for (uint32 spawnId : spawns)
        fwrite(&modelNodeIdx[spawnId], sizeof(uint32), 1, tileSpawnIndicesFile.get());
}

MapTree.cppOpenMapTileFile falls back to the parent's .vmtile but keeps the child's .vmtileidx, then LoadMapTile requires the two counts to be equal:

cpp
if (numSpawns != numSpawnIndices)
    result = LoadResult::ReadFromFileFailed;

numSpawns comes from the parent tile (all of the parent's spawns there), numSpawnIndices from the child index (only the inherited subset). Whenever the child inherits a subset, the tile is rejected.

Note this is checked before the read loop, so nothing is loaded at all — it is not a partial load.

Log output

Could not load VMAP name:Battle of Dazar'alor, id:2070, x:34, y:33 (vmap rep.: x:34, y:33)

35,398 such lines in one server lifetime on our realm.

Why a naive relaxation would be wrong

Simply allowing numSpawnIndices < numSpawns and reading only the available indices is not a fix: the index file is positional — the i-th index belongs to the i-th spawn in the tile file. With a subset there is no way to tell which parent spawns the indices refer to, so the first N would be paired with the wrong models and produce wrong collision, which is worse than not loading the tile.

A correct fix has to make the two sides agree, e.g. the assembler emitting an index entry for every parent spawn in the tile (with a sentinel for spawns the child does not inherit), or the format carrying spawn IDs rather than relying on position. Reporting rather than patching, since that is a format decision.

Reproduction

  1. Extract vmaps for a build containing map 1950 (or any of the 17 above).
  2. vmap4assembler <Buildings> <out>
  3. Compare out/1950/1950_27_25.vmtileidx (7) with out/0571/0571_27_25.vmtile (660) — the spawn counts in the uint32 after the 8-byte VMAP_4.E magic.
  4. Start worldserver; every load of that tile logs Could not load VMAP.