Skip to content

l2_maps.dat — campaign map records#

Summary#

l2_maps.dat is an install asset that stores fixed-size campaign map records. Each record describes one strategic-map variant: a 64×64 province cell layout (six byte planes plus derived layer/slot tables) and a separate 0x81×0x41 visible tile-index grid. The client reads records with plain seek + read; there is no decompression step in the map loaders.

Record count is not stored in the file. The client probes map01.pl8 variants (lotr2_map_load_pl8_tiles, up to 40) and uses the same index for both PL8 tile sheets and l2_maps.dat records.

File layout#

Property Value Evidence
Record size 0x80c1 (32961 bytes) lotr2_asset_read_file(..., 0x80c1, ...)
Record offset record_index × 0x80c1 Same call sites
Header / magic None observed Loaders seek directly to index × size
Record count file_size / 0x80c1 Implied; validated only by PL8 probe at runtime

Total file size must be an exact multiple of 0x80c1.

Record layout#

Within one record, cell (x, y) on the 64×64 grid uses linear index i = y × 0x40 + x (both coordinates 0..63).

Record offset Size Name Runtime destination
0x0000 + i 1 flags Province cell +1 (DAT_00552181)
0x1000 + i 1 layer_a Province cell +2
0x2000 + i 1 layer_b Province cell +3
0x3000 + i 1 layer_c Province cell +4
0x4000 + i 1 layer_index Not stored in cell; drives layer/slot side tables
0x5000 + i 1 province_id Province cell +7
0x6000 + j 1 visible_tile Visible grid dword 0xfff0000 + byte

Check: 6 × 0x1000 + 0x20c1 = 0x80c1.

The visible grid has 0x81 rows × 0x41 columns (j runs 0..0x20c0).

Struct reference: ../../structs/lotr2_maps_dat_record.md.

Decode rules#

Province cell load (lotr2_map_load_from_dat)#

For each of the 4096 cells:

  1. Copy planes flags, layer_a, layer_b, layer_c, and province_id into the live 8-byte province cell table (lotr2_map_province_cell stride).
  2. Track max_province_id as the maximum province_id seen where province_id < 0x11 (17).
  3. When layer_index != 0:
  4. If flags & 0x40: append province_id to layer list layer_index (lotr2_map_append_province_to_layer).
  5. Else if flags & 0x80: write province slot pair (lotr2_map_set_province_slot_pair).

Before parsing, the loader clears six 16-entry layer lists and six slot pairs.

Visible grid seed (lotr2_map_seed_grid_from_dat)#

For each visible-grid cell, set:

lotr2_map_tile_index_grid[row, col] = 0xfff0000 | visible_tile_byte

This runs independently of the province-cell parse and uses the same record read at the same index.

Flag bits (plane 0 / cell +1)#

Bit Use in code Confidence
0x20 Starting-unit placement cell (lotr2_map_place_starting_units) likely
0x40 Layer-list membership when layer_index != 0 confirmed
0x80 Province slot pair when layer_index != 0 confirmed
0x10 Special-tile collection (lotr2_map_collect_special_tiles) likely
& 0xfd == 0 Passability probe (lotr2_map_rect_has_passable_cell) likely

Other bits are used elsewhere but are not written directly from l2_maps.dat during load.

Load pipeline#

New campaign (lotr2_map_load_new_game) uses map index DAT_0055a364:

  1. Clear province layers.
  2. lotr2_map_load_from_dat(index) — province cells + layer/slot tables.
  3. Clear highlights; paint visible grid; paint sea tiles.
  4. lotr2_map_load_pl8_province_tiles(index) — companion map01.pl8 art.
  5. Build neighbors, province setup, starting units, army slots, layer chains.

Wizard and battle UI reload dat + grid when the map index changes.

Data model#

Evidence#

Symbol Address Role
lotr2_map_load_from_dat 0x00477610 Parse 64×64 planes into province table
lotr2_map_seed_grid_from_dat 0x0047781d Seed 0x81×0x41 tile index grid from +0x6000
lotr2_map_load_new_game 0x00477580 New-game orchestrator
lotr2_asset_read_file 0x004ca01c Raw seek/read I/O
lotr2_map_load_pl8_tiles 0x0047a007 Count map variants via map01.pl8 probe
lotr2_map_append_province_to_layer 0x00433984 Layer list side effect
lotr2_map_set_province_slot_pair 0x004b1e40 Slot-pair side effect

Evidence layer: ../../architecture/map/MODULE.md.

Confidence#

likely — record size, plane offsets, and copy rules are direct from decompilation. Upgrade to confirmed by dumping a retail l2_maps.dat and matching decoder output to in-game province layout for several indices.

Interop notes#

  • Install asset only — not appended to saves (contrast castles.dat siege slice in save files).
  • Multiplayer — wizard province-pack sync reloads dat at the negotiated map index (see networking wizard opcodes).
  • Skirmish — single-battle mode uses embedded scenario tables, not l2_maps.dat (../../skirmish/army-and-scenarios.md).

Tooling#

Decode an external copy of the asset:

python3 tools/dump_l2_maps_dat.py /path/to/l2_maps.dat --record 0
python3 tools/dump_l2_maps_dat.py /path/to/l2_maps.dat --json

Open questions#

  • Exact semantic names for layer_a..layer_c planes beyond copy targets
  • Meaning of every flag bit in plane 0 not yet tied to a named behavior
  • Whether retail installs always ship exactly as many records as PL8 variants
  • Full runtime use of province cell bytes +0, +5, +6 not written by dat load