⚓ Seaworth Games

lib/mapping

Map Formats

A loaded .h3m, a loaded .vmap, and a freshly generated random map all become the exact same CMap. Only one of the three roads in is a road back out.

Reading a map

CMapService::getMapLoader() doesn't look at the file extension. It looks at the first four bytes.

A .vmap is a zip archive of JSON files, so its magic number is just a zip signature (0x04034b50 and friends) — seeing one hands the stream to CMapLoaderJson. Anything else is checked against a gzip header, or against the raw format-version bytes of the original H3M header (EMapFormat::ROE, SOD, AB, WOG, HOTA, CHR) — either way it goes to CMapLoaderH3M. A map's name could say anything; the loader trusts the bytes.

Three roads, one CMap

A legacy binary map, VCMI's own JSON map, and a map this atlas's Random Map Generator built from scratch all resolve to the identical runtime type — but the road only runs two ways for one of them.

MapFormatH3M CMapLoaderH3M · legacy .h3m read only MapFormatJson CMapLoaderJson / CMapSaverJson read + write CMapGenerator generate() · lib/rmg read only, one-shot CMap the one runtime type — handed to CGameState loadMap() generate() loadMap() saveMap()
CMapService::saveMap() has exactly one implementation path, through CMapSaverJson, regardless of where the map came from. CMapLoaderH3M implements IMapLoader only — there's no matching IMapSaver, so nothing can write a .h3m back out. Import an original campaign map, edit it, save it, and it's a .vmap from that point on; there's no road back.

Decompiling HotA, at load time

Horn of the Abyss maps can embed a compiled event-scripting bytecode VCMI never runs directly. HotaScriptConverter is a small recursive-descent decompiler — it walks that bytecode once, during CMapLoaderH3M's load, and emits equivalent Lua source instead of implementing a second bytecode interpreter alongside the engine's own scripting host. It's the same instinct this atlas keeps running into in Battle and Spells: rather than special-case a foreign format in C++, translate it once into the one scripting language everything else already speaks.

The editor's undo stack

Distinct from load/save entirely — every in-editor edit is an object, not a direct mutation.

Every action in the map editor — drawing terrain, placing an object, laying a road — is a CMapOperation subclass with execute(), undo(), and redo(), pushed onto CMapUndoManager's stack (10 deep by default) through CMapEditManager. CDrawRoadsOperation and CDrawRiversOperation both extend one template, CDrawLinesOperation<T>, that matches a tile's neighbors against a table of edge/corner patterns and picks the right sprite — roads and rivers differ only in the tile type T and which sprite table they draw from, not in how the pattern-matching works. A CComposedOperation bundles several of these into one undo step, the way “clear terrain” is really a fill-with-water-and-rock made of many tile writes underneath.

One class quietly spans both worlds: ObstacleProxy is the shared base for randomly scattering obstacle objects onto blocked terrain, and both the map editor's EditorObstaclePlacer and the Random Map Generator's obstacle-filling modifier build on it — one placement algorithm, two very different callers.