LOG IN
Docs
By Opus

Map Loading And Map Structures

How .map files are parsed into WorldHeightMap, MapObject list, and world dict, and what each contains.

Loading a map is not loading a game. A .map file turns into three in-memory pieces — a terrain height grid, a linked list of spawn records, and a dictionary of world-level properties — and nothing in that file produces a live Object. GameLogic::startNewGame later walks the spawn list and materializes gameplay instances from it. Keep the two phases separate in your head; a lot of confusion about why map edits "don't do anything" comes from conflating them.

The .map file format

The entry point is a static loadMap() in Core/GameEngine/Source/GameClient/MapUtil.cpp around line 218. It opens the file through a CachedFileInputStream, wraps it in a DataChunkInput, and registers three parsers against named chunks:

DataChunkInput file( pStrm );

file.registerParser( "HeightMapData", AsciiString::TheEmptyString, ParseSizeOnlyInChunk );
file.registerParser( "WorldInfo",     AsciiString::TheEmptyString, ParseWorldDictDataChunk );
file.registerParser( "ObjectsList",   AsciiString::TheEmptyString, ParseObjectsDataChunk );
if (!file.parse(nullptr)) {
    throw(ERROR_CORRUPT_FILE_FORMAT);
}

DataChunkInput is a tagged chunk stream: each chunk has a name, a version, and a byte size, and the reader hands control to a registered parser for the name. Unknown chunks are skipped; versions are compared against the compile-time constants in GeneralsMD/Code/GameEngine/Include/Common/MapReaderWriterInfo.hK_HEIGHT_MAP_VERSION_4, K_OBJECTS_VERSION_3, K_BLEND_TILE_VERSION_8, K_WORLDDICT_VERSION_1, and friends. If a known chunk comes in with a version the reader doesn't recognize, parsing fails hard.

The format is inherited unchanged from the original EA Generals toolchain. We did not redesign it, and we are deliberately constrained to keep reading what shipped — the compatibility test is whether an untouched .map from the original WorldBuilder (or from a modded FinalBig extract) still opens.

WorldHeightMap

The terrain side of the file becomes a WorldHeightMap: a width * height grid of UnsignedByte samples, scaled by MAP_HEIGHT_SCALE (MAP_XY_FACTOR/16.0f, defined in MapObject.h), plus a border of extra cells around the playable area, plus per-cell tile-texture indices and blend info that the BlendTileData chunks populate. The border exists so bilinear sampling and filter kernels at the map edge always have neighbors to read.

The extent of the playable region is derived from the stored size minus twice the border:

m_mapDX = m_width  - 2*m_borderSize;
m_mapDY = m_height - 2*m_borderSize;

TheTerrainLogic->newMap() takes the loaded heightmap and builds the logic-side terrain; TheTerrainVisual->load(map) builds the rendered mesh. Once the heights are in, Pathfinder::classifyMap walks the grid and sets per-cell passability bits including CELL_CLIFF — see Cliff Cells for how that classification derives from slope.

MapObject and the spawn list

Everything the map author placed — units, buildings, tech structures, waypoints, scorches, lights, roads, bridges, polygon triggers — parses into MapObject records. MapObject (defined in MapObject.h) is just a spawn record: world location, orientation, flag bits, a ThingTemplate* resolved from the object name string, and a Dict m_properties of author-set key/value pairs. Records live on a singly linked list anchored at MapObject::TheMapObjectListPtr, walked via getFirstMapObject() and getNext().

After terrain comes up, GameLogic::startNewGame does a pre-pass for bridges so they exist before pathfinding classifies the map:

for (MapObject *pMapObj = MapObject::getFirstMapObject(); pMapObj; pMapObj = pMapObj->getNext())
{
    if (pMapObj->getFlag(FLAG_BRIDGE_FLAGS) || pMapObj->getFlag(FLAG_ROAD_FLAGS))
        continue; // roads and bridge pieces are special-cased on the terrain side

    const ThingTemplate *thingTemplate = pMapObj->getThingTemplate();
    if (thingTemplate == nullptr) continue;
    if (!thingTemplate->isBridgeLike()) continue;

    Object *obj = TheThingFactory->newObject(thingTemplate, team);
    // ... position, orient, then:
    obj->updateObjValuesFromMapProperties( pMapObj->getProperties() );
}

The same pattern — resolve ThingTemplate, newObject, updateObjValuesFromMapProperties — runs later for the general object population. The Dict holds whatever the author typed in WorldBuilder: team assignment, initial HP, scripted-unit name, bit flags like objectInitialHealth or objectSelectable. Typos on the authoring side do not error; the key just never matches and the property is silently dropped on the floor.

WorldInfo and world dict

The WorldInfo chunk parses through ParseWorldDictDataChunk into a single global Dict accessible via MapObject::getWorldDict(). This is the map's properties sheet: music track, initial camera position name, default lighting slots, win/lose conditions for scripted campaigns, whether it's a multiplayer map, shroud color, and any custom keys scripts read at runtime. Polygon triggers and waypoints also come in here or via their own sibling chunks and register into separate structures (WaypointMap, the polygon-trigger list), not into the MapObject list itself.

Quirks

  • Unknown chunks are skipped, bad versions fail hard. Forward-compat for new chunk types is free. Forward-compat for an existing chunk's contents is not — bumping K_HEIGHT_MAP_VERSION_* in WorldBuilder and re-saving breaks older loaders.
  • The Dict is stringly typed. Property names are AsciiString keys with no enum. A misspelled property name in the editor silently produces an object with default values. There is no "unknown key" warning anywhere in the load path.
  • The border is real memory. WorldHeightMap stores extra cells on each side (m_borderSize, introduced in K_HEIGHT_MAP_VERSION_3) that are not part of the playable extent but still get heights and tile indices. Sample your grid with border offsets or you will read garbage at the edges.
  • Roads, bridges, waypoints, and polygon triggers ride the same file. They are MapObjects with flag bits set (FLAG_ROAD_FLAGS, FLAG_BRIDGE_FLAGS, MO_WAYPOINT) rather than a separate object type, which is why the startNewGame bridge pre-pass has to filter them out of the generic walk.
  • The format was never documented externally. The source is the spec. The stability contract is "WorldBuilder output must still open" — format stability is a compatibility constraint inherited from the original toolchain, not a design choice we can revisit without breaking every mod map on disk.
  • Re-exporting from a modern WorldBuilder can silently raise version ints. If a map edited in a newer tool stops loading for a user on an older build, that is the first place to check.