vmap4assembler produces child-map tiles that StaticMapTree::LoadMapTile always rejects
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:
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.cpp — OpenMapTileFile falls back to the parent's .vmtile but keeps
the child's .vmtileidx, then LoadMapTile requires the two counts to be equal:
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
- Extract vmaps for a build containing map 1950 (or any of the 17 above).
vmap4assembler <Buildings> <out>- Compare
out/1950/1950_27_25.vmtileidx(7) without/0571/0571_27_25.vmtile(660) — the spawn counts in the uint32 after the 8-byteVMAP_4.Emagic. - Start worldserver; every load of that tile logs
Could not load VMAP.
Source: TrinityCore/TrinityCore