Skip to content

Save file on-disk format#

Summary#

A Lords of the Realm 2 save file (.sav, .sva, .svb) is a flat, header-less, checksum-less binary dump of game-state global memory. There is no magic number, version field, or length prefix. The file is an exact byte-for-byte snapshot of 203 contiguous global-memory regions, written in fixed table order, followed by 16 × 0x3200-byte castle/siege tile records copied from castles.dat.

The layout is fully determined by the static field-pointer table at VA 0x004fa280 in the retail LORDS2.EXE binary (SHA-256 ff33c884… — see docs/METADATA.md). Every save written by that build is exactly 471,740 bytes.


Container layout#

flowchart LR
    A["Region A\nField blocks 0–202\n0x000000–0x0412BB\n266,940 bytes"]
    B["Region B\nCastle/siege tile slice\n0x0412BC–0x0732BB\n16 × 0x3200 = 204,800 bytes"]
    A --> B
Region File offset start File offset end Size (bytes) Description
A 0x000000 0x0412BB 266,940 203 ordered field blocks
B 0x0412BC 0x0732BB 204,800 16 × 0x3200 castle/siege tile records
471,740 Total (fixed)

Read / edit / write algorithm#

Field-block offset computation#

Block file offsets are cumulative sums of the size column. No padding or alignment is applied between blocks.

FIELD_SIZES = [0x8000, 0xc800, 0x108, 0x840, 0x3300, ...]  # 203 entries

def file_offset(block_index):
    return sum(FIELD_SIZES[:block_index])

def read_block(fp, block_index):
    fp.seek(file_offset(block_index))
    return fp.read(FIELD_SIZES[block_index])

Editing a save (no-resize rule)#

Block sizes are compile-time constants embedded in the executable. A valid save must be exactly 471,740 bytes; block boundaries must not shift. To modify a field:

  1. Compute offset = sum(FIELD_SIZES[:i]).
  2. Read FIELD_SIZES[i] bytes starting at offset.
  3. Modify the bytes in place (use struct layout from the field map below and the referenced struct docs).
  4. Write the same number of bytes back at the same offset.

Never truncate, extend, or re-order the file.

Writing a save from scratch#

  1. Allocate a 471,740-byte buffer.
  2. For each block i in 0..202, copy FIELD_SIZES[i] bytes from RAM (or a reconstructed state) into the cumulative file position.
  3. At 0x0412BC, write 16 × 0x3200 bytes of castle/siege tile data (first 0x32000 bytes of the current castles.dat).
  4. Write the buffer atomically.

castles.dat coupling#

The castle/siege tile slice (Region B) is a bidirectional sync:

  • On save: the game reads 16 × 0x3200 bytes from castles.dat starting at offset 0 and appends them to the save file.
  • On load: after restoring all field blocks (Region A), the game writes those same bytes back to castles.dat, overwriting the first 0x32000 bytes.

When editing a save file for use in-game, Region B must be consistent with castles.dat. Editors that only change Region A fields (player stats, economy, diplomacy) can leave Region B untouched.

I/O flags note#

lotr2_io_open flag 0x8000 = _O_RDONLY; flag 0x8301 = _O_WRONLY | _O_CREAT | _O_TRUNC; mode 0x180 = _S_IREAD | _S_IWRITE.

Decompiled-name swap

The Ghidra export has the save/load function names reversed. lotr2_save_load_from_file (0x004c823e) opens the target with 0x8301 (write/create/truncate) and crt_writes field blocks into it — this is the write/save path. lotr2_save_write_to_file (0x004c838c) opens the target with 0x8000 (read-only) and __reads blocks back into RAM, then calls lotr2_save_restart_after_load — this is the read/load path. See architecture/save/MODULE.md.


File variants#

All three extensions share the same binary layout.

Extension Context Notes
.sav Single-player lastturn.sav = auto-save at end of turn
.sva Multiplayer host lastturn.sva, diverged.sva (desync recovery)
.svb Multiplayer client lastturn.svb, diverged.svb (desync recovery)

Field block map#

The table below was generated from the retail binary (see tools/ghidra/dump_save_field_table.py). File offset is the byte position of the block within the save file. Source VA is the runtime global address whose contents are saved. Symbol is the Ghidra-assigned name; all blocks are now named — lotr2_unk_XXXXXXXX entries are provisional placeholders pending further analysis.

Where a symbol is known, the Notes column links to the relevant struct or architecture doc and gives the interpreted layout. Names prefixed lotr2_unk_ are provisional placeholders for scalars with only sparse or reset-only call-site evidence; they should be renamed when additional call sites are identified.

# File offset Source VA Size Symbol / Notes
0 0x000000 0x00552180 0x8000 (32 768) lotr2_map_province_cell — province cell table, 64×64 cells × 8 bytes each; see lotr2_game_state.md
1 0x008000 0x005841C0 0xC800 (51 200) lotr2_battle_tile_damage_ctr — battle/siege tile plane A, 0x50×0x50 tiles × 8 bytes; see lotr2_battle_tile.md
2 0x014800 0x0055A900 0x108 (264) lotr2_game_player_display_id — 6 × 0x2C player display-name blocks; see lotr2_player_record.md
3 0x014908 0x0055ADA0 0x840 (2 112) lotr2_game_ai_player_step — 6 × 0x160 player records (base of player-record array); see lotr2_player_record.md
4 0x015148 0x0055C3B0 0x3300 (13 056) lotr2_province_economy_table — province economy/stock table, stride 0x300 × up to 17 entries (gold, food, garrison, happiness etc.); evidence: (&DAT_0055c3b0)[id * 0x300] in lotr2_game_economy_tick_player, lotr2_net_partial_checksum; see castle-economy.md
5 0x018448 0x005643D0 0xF7BC (63 420) lotr2_game_army_record_table — army record table, stride 0x1A4; see lotr2_game_state.md
6 0x027C04 0x00582FF0 0x104 (260) lotr2_recruit_flag_slots — recruit/battle-flag slot table, stride 0x14 × 0x13 entries; fields: province id, flag type; evidence: lotr2_combat_zero_flag_slot_bytes, lotr2_game_recruit_apply_flag_bonus
7 0x027D08 0x00549540 0x88B0 (34 992) lotr2_combat_figure_slots — battle figure array, stride 0x1B0 × 0x50 slots; evidence: lotr2_combat_clear_all_figures, lotr2_net_partial_checksum; see lotr2_battle_figure.md
8 0x030558 0x005476C0 0x1DFC (7 676) lotr2_projectile_slots — projectile slot array; see lotr2_projectile.md
9 0x0323B4 0x00583140 0x1074 (4 212) lotr2_combat_group_table — combat group slots 1..0x50, 0x34 bytes each; see lotr2_battle_group.md
10 0x033428 0x00573C50 0xC800 (51 200) (no symbol) — battle/siege tile plane B, 0x50×0x50 × 8 bytes
11 0x03FC28 0x00563E70 0x80 (128) lotr2_combat_siege_sprite_slots — siege sprite/state table, stride 0x18; evidence: lotr2_game_ui_memcpy_n(0x549520, &DAT_00563e70 + DAT_005818ec * 0x18, 0x18)
12 0x03FCA8 0x00580BC0 0x550 (1 360) lotr2_map_army_province_slots — per-player army-on-map cell index table; evidence: *(int *)(&DAT_00580bc0 + slot * 4 + player * 0x50) in lotr2_map_build_army_slot_table
13 0x0401F8 0x0059B85C 4 lotr2_game_campaign_difficulty — campaign difficulty level (0–3); used as economy/crown multiplier; evidence: lotr2_game_economy_tick_player adds DAT_0059b85c * 0x14 to player crown total; loaded from difficulty config table
14 0x0401FC 0x0059C220 4 lotr2_game_active_view — top-level view: 0=campaign, 2=battle/siege; see lotr2_game_state.md
15 0x040200 0x0055A898 4 lotr2_map_iso_rotation — map isometric rotation mode (0, 1, or 2); evidence: if (DAT_0055a898 == 2) in lotr2_map_draw_cell_tile dispatch
16 0x040204 0x0055AC6C 4 lotr2_map_viewport_row — map viewport origin row (Y), clamped 0..0x40; evidence: lotr2_map_clamp_viewport_origin
17 0x040208 0x0055AC70 4 lotr2_map_viewport_col — map viewport origin column (X), clamped 0..0x80; evidence: same
18 0x04020C 0x005DA4B8 4 lotr2_rng_lfsr_hi — 31-bit LFSR RNG state high dword; init 0x54657687; evidence: lotr2_game_rng_lfsr_shift_wide_pair
19 0x040210 0x005DA4BC 4 lotr2_rng_lfsr_lo — 31-bit LFSR RNG state low dword; init 0x72641663; evidence: same
20 0x040214 0x0055AA24 4 lotr2_combat_ui_y_baseline — battle UI Y baseline coord; init 0x4F3 at new campaign; evidence: lotr2_game_begin_new_campaign, lotr2_game_ui_draw_signed_value_captioned
21 0x040218 0x0055BFE8 4 lotr2_combat_ui_y_pos — battle UI Y position (companion to lotr2_combat_ui_y_baseline); init 0x4F2 at new campaign
22 0x04021C 0x00573BA0 4 lotr2_combat_msg_panel_y — battle message panel Y coordinate; init 0x4F4 at new campaign
23 0x040220 0x0055BFCC 4 lotr2_econ_tick_phase — economy tick season/phase parameter; value 4 triggers special AI paths; evidence: lotr2_game_economy_tick_player(uVar2, DAT_0055bfcc)
24 0x040224 0x005636E4 4 lotr2_econ_tick_mode — economy tick mode snapshot (copy of lotr2_econ_tick_phase taken at end-turn); evidence: lotr2_game_end_turn_tick, lotr2_game_snapshot_economy_before_tick
25 0x040228 0x0059B7E4 4 lotr2_diplomacy_tick_phase — diplomacy tick phase parameter; evidence: lotr2_game_diplomacy_tick_player(prov, DAT_0059b7e4)
26 0x04022C 0x00582D3C 4 lotr2_unk_00582d3c — (provisional) unknown scalar; zeroed at new-game
27 0x040230 0x00580B80 4 lotr2_unk_00580b80 — (provisional) unknown scalar; zeroed at new-game
28 0x040234 0x0059BC90 4 lotr2_unk_0059bc90 — (provisional) unknown scalar; init 0x4F3; in turn-phase cluster
29 0x040238 0x00582F24 4 lotr2_unk_00582f24 — (provisional) unknown scalar; init 3 at new-game
30 0x04023C 0x0059312C 4 lotr2_unk_0059312c — (provisional) unknown scalar
31 0x040240 0x0055AC48 4 lotr2_game_turn_number — turn counter; incremented each end-turn; evidence: DAT_0055ac48 = DAT_0055ac48 + 1 in lotr2_game_end_turn_tick
32 0x040244 0x0059BFD4 4 lotr2_combat_figure_dirty_ctr — (provisional) figure dirty-pass counter; zeroed in lotr2_combat_mark_figure_tiles_dirty; saved twice (also block 48)
33 0x040248 0x0055AC18 4 lotr2_combat_attacker_army_slot — attacker army record slot index; evidence: lotr2_game_army_record_table[DAT_0055ac18 * 0x1a4] in battle prep and resolution
34 0x04024C 0x0055AC1C 4 lotr2_combat_defender_army_slot — defender army record slot index; evidence: same pattern
35 0x040250 0x00582DE4 4 lotr2_unk_00582de4 — (provisional) unknown scalar
36 0x040254 0x005814F4 4 lotr2_combat_map_size_row — map-size row index for formation threshold lookup; evidence: lotr2_combat_init_battle_state
37 0x040258 0x00552154 4 lotr2_combat_attacker_troop_count — live attacker total figure head-count; accumulated per battle tick; evidence: lotr2_game_ui_draw_number_centered(DAT_00552154, …, 0x1fa, …)
38 0x04025C 0x0055B7B4 4 lotr2_combat_attacker_troop_snapshot — snapshot of attacker troop count taken at battle start; evidence: _DAT_0055b7b4 = DAT_00552154 in lotr2_combat_init_battle_state
39 0x040260 0x0055C37C 4 lotr2_combat_defender_troop_count — live defender total figure head-count; evidence: lotr2_game_ui_draw_number_centered(DAT_0055c37c, …, 0x24a, …)
40 0x040264 0x0055C370 4 lotr2_combat_defender_troop_snapshot — snapshot of defender troop count at battle start; evidence: _DAT_0055c370 = DAT_0055c37c in lotr2_combat_init_battle_state
41 0x040268 0x00582DEC 4 lotr2_unk_00582dec — (provisional) unknown scalar
42 0x04026C 0x00583110 4 lotr2_unk_00583110 — (provisional) unknown scalar; zeroed in battle init
43 0x040270 0x0055F6C8 4 lotr2_combat_attacker_underdog — 1 when attacker has ≤75 % of defender strength; gates attacker favorable spawn; evidence: DAT_0055f6c8 = (att + att/3 < def)
44 0x040274 0x00563E60 4 lotr2_combat_defender_underdog — 1 when defender has ≤75 % of attacker strength; gates defender favorable spawn; evidence: DAT_00563e60 = (def + def/3 < att)
45 0x040278 0x0055AC80 4 lotr2_unk_0055ac80 — (provisional) unknown scalar; zeroed in battle init
46 0x04027C 0x0054355C 4 lotr2_unk_0054355c — (provisional) unknown scalar; zeroed in battle init
47 0x040280 0x0055BFC0 4 lotr2_unk_0055bfc0 — (provisional) unknown scalar; zeroed in battle init
48 0x040284 0x0059BFD4 4 lotr2_combat_figure_dirty_ctrduplicate of block 32 (same VA 0x0059BFD4; both saved and loaded)
49 0x040288 0x005436AC 4 lotr2_unk_005436ac — (provisional) unknown scalar
50 0x04028C 0x0059310C 4 lotr2_unk_0059310c — (provisional) unknown scalar
51 0x040290 0x0059C348 4 lotr2_unk_0059c348 — (provisional) unknown scalar
52 0x040294 0x005818F4 4 lotr2_unk_005818f4 — (provisional) unknown scalar
53 0x040298 0x00593454 4 lotr2_unk_00593454 — (provisional) unknown scalar
54 0x04029C 0x0059C324 4 lotr2_combat_battle_province — province id being contested in the active battle; evidence: lotr2_game_conquest_capture_province(…, DAT_0059c324), [DAT_0059c324 * 0x300] economy access
55 0x0402A0 0x00573BDC 4 lotr2_unk_00573bdc — (provisional) unknown scalar
56 0x0402A4 0x0055AA14 4 lotr2_combat_battlefield_variant — battlefield PL8 asset variant index (0..0x2E, wraps); evidence: lotr2_combat_load_open_field_map
57 0x0402A8 0x0055AB70 4 lotr2_combat_attacker_strength — attacker army weighted strength score; evidence: DAT_00563e60 = (def + def/3 < DAT_0055ab70) in lotr2_combat_init_battle_state
58 0x0402AC 0x0055ABE8 4 lotr2_combat_defender_strength — defender army weighted strength score; evidence: DAT_0055f6c8 = (att + att/3 < DAT_0055abe8)
59 0x0402B0 0x00563814 4 lotr2_combat_formation_shift_x — formation blit X-shift from map-size lookup table; evidence: lotr2_combat_init_battle_state loads from DAT_004f49a8[DAT_005814f4]
60 0x0402B4 0x00563820 4 lotr2_combat_formation_shift_y — formation blit Y-shift from map-size lookup table; evidence: same
61 0x0402B8 0x0055AB04 4 lotr2_unk_0055ab04 — (provisional) unknown scalar
62 0x0402BC 0x0055AB08 4 lotr2_unk_0055ab08 — (provisional) unknown scalar
63 0x0402C0 0x0056432C 4 lotr2_combat_focus_group_index — active focus group for move-to-tile; see lotr2_game_state.md
64 0x0402C4 0x0059344C 4 lotr2_map_minimap_mode — minimap overlay render mode; evidence: lotr2_game_set_minimap_render_mode
65 0x0402C8 0x0055B748 4 lotr2_combat_prep_ui_mode — battle prep UI mode (1 = garrison empty, 2 = otherwise); evidence: battle army-pair validation
66 0x0402CC 0x0059BC3C 4 lotr2_history_write_slot — history ring write cursor (0–399, wraps); evidence: DAT_0059bc3c = DAT_0059bc3c + 1; if (399 < DAT_0059bc3c) DAT_0059bc3c = 0
67 0x0402D0 0x0056433C 4 lotr2_history_ring_gen — history ring wrap generation counter (increments each time ring wraps at 400); evidence: same loop
68 0x0402D4 0x0059BCBC 4 lotr2_history_entry_count — total history entries recorded, capped at 400; evidence: if (400 < DAT_0059bcbc) DAT_0059bcbc = 400
69 0x0402D8 0x0059C388 4 lotr2_unk_0059c388 — (provisional) unknown scalar
70 0x0402DC 0x005637A8 4 lotr2_unk_005637a8 — (provisional) unknown scalar
71 0x0402E0 0x0055AA08 4 lotr2_map_selected_province — currently selected province id; evidence: [DAT_0055aa08 * 0x300] in province economy UI draw
72 0x0402E4 0x00580BAC 4 lotr2_game_local_player_slot — local player index (0-based); see lotr2_game_state.md
73 0x0402E8 0x0055ABD0 4 lotr2_game_ai_turn_phase — AI outer sim phase 1–7; see lotr2_game_state.md
74 0x0402EC 0x00573BB4 4 lotr2_unk_00573bb4 — (provisional) unknown scalar; zeroed on AI phase advance
75 0x0402F0 0x0059BC28 4 lotr2_game_ai_turn_phase_prev — previous AI turn phase (1–7); evidence: _DAT_0059bc28 = lotr2_game_ai_turn_phase in lotr2_game_ai_advance_turn_phase
76 0x0402F4 0x0055ABD8 4 lotr2_game_ai_turn_phase_next — next AI turn phase (wraps 1–7); evidence: DAT_0055abd8 = lotr2_game_ai_turn_phase + 1; if (7 < DAT_0055abd8) DAT_0055abd8 = 1
77 0x0402F8 0x0055AA20 4 lotr2_map_max_province_id — highest valid province id loaded from map; loop upper bound; evidence: for (province_idx = 1; province_idx <= DAT_0055aa20; …)
78 0x0402FC 0x0059C34C 4 lotr2_map_start_armies_spawned — count of starting armies spawned at campaign start; evidence: _DAT_0059c34c = _DAT_0059c34c + 1 in lotr2_map_spawn_starting_armies_from_list
79 0x040300 0x005637CC 4 lotr2_game_event_province_cursor — random province picker seed/cursor for event assignment; evidence: lotr2_game_init_new_campaign_players sets = 1
80 0x040304 0x0059C044 4 lotr2_combat_prep_entered — flag set on entering battle prep mode, cleared on exit; evidence: lotr2_game_action_enter_battle_prep
81 0x040308 0x0059E620 4 lotr2_econ_selected_resource — selected resource type for build-cost / trade UI; evidence: lotr2_game_calc_build_turn_cost; values 0xD/0xE = trade resource types
82 0x04030C 0x00563D50 0x100 (256) lotr2_map_province_build_costs — province build-cost defaults (256 B); initialized from ROM table DAT_004f39f0; evidence: lotr2_map_init_province_cost_table
83 0x04040C 0x0055AC44 4 lotr2_unk_0055ac44 — (provisional) unknown scalar
84 0x040410 0x0055A84C 4 lotr2_game_ai_player_step_cursor — AI player-step rotation cursor; evidence: lotr2_game_ai_player_step_loop
85 0x040414 0x0059BFDC 4 lotr2_game_active_player_count — count of active (non-eliminated) players; evidence: DAT_0059bfdc = DAT_0059bfdc + 1 in lotr2_game_ai_reset_step_counters; compared against 3 for minimum-player checks
86 0x040418 0x00580B98 4 lotr2_net_roster_player_count — MP player count from roster; see lotr2_game_state.md
87 0x04041C 0x00507734 4 lotr2_game_screen_mode_saved — screen mode snapshot saved/restored across load; evidence: lotr2_game_screen_mode = DAT_00507734 on load
88 0x040420 0x00580BA4 4 lotr2_unk_00580ba4 — (provisional) unknown scalar
89 0x040424 0x00581910 0xC0 (192) lotr2_combat_player_ui_layout — per-player battle UI layout constants, stride 0x60; evidence: *(…&DAT_00581910 + player * 8 + sub * 0x60)
90 0x0404E4 0x00552110 0x40 (64) lotr2_map_player_scalar_table — per-player map scalar table, stride 8; evidence: (short)*(…&DAT_00552110 + param_1 * 8)
91 0x040524 0x0055A7A0 0x60 (96) lotr2_unk_0055a7a0 — (provisional) unknown table (96 bytes)
92 0x040584 0x0059C228 4 lotr2_game_ai_diplomacy_cursor — diplomacy queue slot cursor / siege-entry coord index; evidence: (&DAT_0055acd0)[DAT_0059c228 * 8] in lotr2_combat_group_order_goto_siege_entry
93 0x040588 0x00593108 4 lotr2_unk_00593108 — (provisional) unknown scalar
94 0x04058C 0x0056437C 4 lotr2_combat_attacker_player_slot — attacker player slot index (default 6 = none); evidence: set from attacker army record in battle prep validation
95 0x040590 0x005643A8 4 lotr2_combat_defender_player_slot — defender player slot index (default 6 = none); evidence: same
96 0x040594 0x0055C390 4 lotr2_combat_attacker_owner — player id owning the attacker army; evidence: set from lotr2_game_army_record_table[slot * 0x1a4] in battle prep
97 0x040598 0x0055C398 4 lotr2_combat_defender_owner — player id owning the defender army; evidence: same
98 0x04059C 0x0055ACD0 0xC0 (192) lotr2_combat_siege_entry_coords — siege entry tile coordinate pairs, stride 8; evidence: lotr2_combat_group_order_goto_siege_entry reads +DAT_0059c228 * 8
99 0x04065C 0x00581230 0x280 (640) lotr2_combat_siege_spawn_markers — siege spawn-marker tables (16 groups × 8 B each pair); evidence: lotr2_combat_parse_siege_spawn_markers
100 0x0408DC 0x00580B74 4 lotr2_unk_00580b74 — (provisional) unknown scalar; near formation-threshold cluster
101 0x0408E0 0x0055C030 4 lotr2_unk_0055c030 — (provisional) unknown scalar; incremented by 4 in build-queue logic
102 0x0408E4 0x005814EC 4 lotr2_unk_005814ec — (provisional) unknown scalar; near formation threshold
103 0x0408E8 0x005643BC 4 lotr2_unk_005643bc — (provisional) unknown scalar; incremented in combat group-order paths
104 0x0408EC 0x0059C370 4 lotr2_combat_is_siege — battle type flag: 0 = open-field, non-zero = siege; evidence: if (DAT_0059c370 == 0) gates open-field vs siege spawn and order dispatch
105 0x0408F0 0x0055AC14 4 lotr2_unk_0055ac14 — (provisional) unknown scalar
106 0x0408F4 0x0059BC30 4 lotr2_unk_0059bc30 — (provisional) unknown scalar
107 0x0408F8 0x005930D4 4 lotr2_unk_005930d4 — (provisional) unknown scalar
108 0x0408FC 0x00573C04 4 lotr2_unk_00573c04 — (provisional) unknown scalar
109 0x040900 0x0055F6B8 4 lotr2_unk_0055f6b8 — (provisional) unknown scalar
110 0x040904 0x0055A6E0 0xA0 (160) lotr2_combat_lane_coord_buf — combat lane-coord ring buffer, 19 × 8 bytes; evidence: lotr2_combat_store_lane_coord first-fit append
111 0x0409A4 0x00552150 4 lotr2_combat_gate_figure_count — count of figures currently on gate tiles (tile type 5, state 4); evidence: lotr2_combat_tick_all_figures
112 0x0409A8 0x00581A00 4 lotr2_combat_siege_engine_count — live siege-engine figure count (unit types 7–9); evidence: lotr2_combat_tick_all_figures
113 0x0409AC 0x00551E00 4 lotr2_game_build_slot_index — build-queue slot scan cursor; see lotr2_game_state.md
114 0x0409B0 0x0059BC78 4 lotr2_combat_catapult_count — catapult figure count during group-order assignment; evidence: lotr2_combat_assign_group_orders
115 0x0409B4 0x0059C3C8 4 lotr2_combat_ram_count — battering-ram figure count; evidence: same
116 0x0409B8 0x00593460 4 lotr2_combat_siege_tower_count — siege-tower figure count; evidence: same
117 0x0409BC 0x005814F0 4 lotr2_combat_moat_figure_count — moat-digger / ladder figure count; evidence: same
118 0x0409C0 0x00582D1C 4 lotr2_combat_infantry_figure_count — non-siege infantry figure count; evidence: same
119 0x0409C4 0x00580B94 4 lotr2_combat_form_tier_thresh_lg — large formation size threshold; see lotr2_game_state.md
120 0x0409C8 0x00580B90 4 lotr2_combat_form_tier_thresh_md — medium formation size threshold
121 0x0409CC 0x00580B84 4 lotr2_combat_form_tier_thresh_sm — small formation size threshold
122 0x0409D0 0x005819D4 4 lotr2_ranking_max_troop_val — (provisional) ranking: highest troop count value across players; evidence: lotr2_game_ai_compute_player_rankings
123 0x0409D4 0x0055A364 4 lotr2_combat_ui_config — battle UI config dword from load-count table; evidence: DAT_0055a364 = *(uint *)(&DAT_004f3ef8 + DAT_0059b878 * 0x20)
124 0x0409D8 0x0059E664 4 lotr2_game_other_player_count — count of other active players (excluding local); evidence: lotr2_game_ai_compute_player_rankings
125 0x0409DC 0x0055C018 4 lotr2_econ_province_dev_tier — province development tier derived from economy record; values 0–4; evidence: DAT_0055c018 = (byte)(&DAT_0055c5a9)[DAT_0059c324 * 0x300]
126 0x0409E0 0x005436A4 4 lotr2_unk_005436a4 — (provisional) unknown scalar
127 0x0409E4 0x0055AC10 4 lotr2_unk_0055ac10 — (provisional) unknown scalar
128 0x0409E8 0x0055ABF0 4 lotr2_unk_0055abf0 — (provisional) unknown scalar
129 0x0409EC 0x00563D38 4 lotr2_unk_00563d38 — (provisional) unknown scalar; near lotr2_map_province_build_costs
130 0x0409F0 0x0055AC24 4 lotr2_unk_0055ac24 — (provisional) unknown scalar
131 0x0409F4 0x00543648 4 lotr2_unk_00543648 — (provisional) unknown scalar
132 0x0409F8 0x00573BF4 4 lotr2_combat_moat_tile_type — tile type index of current moat-digger figure; evidence: lotr2_combat_assign_group_orders sets from lotr2_battle_tile_type
133 0x0409FC 0x005476B4 4 lotr2_unk_005476b4 — (provisional) unknown scalar
134 0x040A00 0x00582DD0 4 lotr2_ranking_max_treasury_val — ranking: highest treasury value across players; evidence: lotr2_game_ai_compute_player_rankings
135 0x040A04 0x00573BA8 4 lotr2_unk_00573ba8 — (provisional) unknown scalar
136 0x040A08 0x0059BCA8 4 lotr2_unk_0059bca8 — (provisional) unknown scalar
137 0x040A0C 0x0055AC9C 4 lotr2_unk_0055ac9c — (provisional) unknown scalar
138 0x040A10 0x0055B750 0x60 (96) lotr2_map_layer_province_lists — map layer → province-id lists, 6 layers × 16 entries (byte); evidence: lotr2_map_append_province_to_layer writes [province + (layer-1) * 0x10]
139 0x040A70 0x00573BC0 6 lotr2_map_spawn_army_list — starting-army spawn descriptor (6 bytes); evidence: lotr2_map_spawn_starting_armies_from_list, lotr2_map_build_layer_province_chains
140 0x040A76 0x0055AC00 6 lotr2_map_province_slot_pairs — per-player starting province assignments, stride 2 (province id + slot byte); evidence: (&DAT_0055ac00)[player_id * 2] in lotr2_game_select_province_by_tile_index
141 0x040A7C 0x00582D34 4 lotr2_map_province_slot_pair_count — count of province-slot pairs written; evidence: incremented in lotr2_map_set_province_slot_pair
142 0x040A80 0x0055C39C 4 lotr2_econ_ui_flag — province economy UI dirty/active flag; set to 1 in economy paths
143 0x040A84 0x0055AB84 4 lotr2_unk_0055ab84 — (provisional) unknown scalar
144 0x040A88 0x00582DAC 4 lotr2_diplomacy_gold_offer — diplomacy gold offer amount (adjusted ±10, clamped to treasury); evidence: DAT_00582dac = DAT_00582dac + 10, clamped draw + net transmit
145 0x040A8C 0x005930E4 4 lotr2_unk_005930e4 — (provisional) unknown scalar; cleared in diplomacy text mode
146 0x040A90 0x0059C230 0xF0 (240) lotr2_game_ai_diplomacy_queue — AI diplomacy action queue, 6 players × 5 slots × 8 bytes; evidence: (&DAT_0059c230)[player_id * 0x28 + slot * 8] in lotr2_game_ai_process_diplomacy_queue
147 0x040B80 0x00563D44 4 lotr2_unk_00563d44 — (provisional) unknown scalar; near lotr2_map_province_build_costs
148 0x040B84 0x005818E8 4 lotr2_ranking_max_castle_val — ranking: highest castle count value across players; evidence: lotr2_game_ai_compute_player_rankings
149 0x040B88 0x00563850 0x4BC (1 212) lotr2_game_event_msg_table — province event message text table, stride 0xCA per entry; evidence: lotr2_game_ui_draw_bytes_wrapped(&DAT_00563850 + i * 0xca, …)
150 0x041044 0x00564340 0x30 (48) lotr2_game_player_rankings — player score ranking table, 6 slots × 8 bytes (id + score), bubble-sorted; evidence: lotr2_game_ai_compute_player_rankings
151 0x041074 0x0055BFE4 4 lotr2_ranking_top_player_id — id of leading player from ranking sort; evidence: DAT_0055bfe4 = *(int *)(&DAT_00564340 + rank_slot * 8)
152 0x041078 0x00593114 4 lotr2_ranking_top_player_id_b — (provisional) secondary reference to leading player id from same ranking sort
153 0x04107C 0x00573B8C 4 lotr2_ranking_max_gold_val — ranking: highest province gold value across players; evidence: lotr2_game_ai_compute_player_rankings
154 0x041080 0x00573BA4 4 lotr2_ranking_max_gold_player — player id with highest province gold; evidence: same
155 0x041084 0x0059C340 4 lotr2_ranking_max_happiness_val — ranking: highest average happiness value; evidence: same
156 0x041088 0x0059C360 4 lotr2_ranking_max_happiness_player — player id with highest average happiness; evidence: same
157 0x04108C 0x0055AC60 4 lotr2_ranking_max_pop_val — ranking: highest average province population value; evidence: same
158 0x041090 0x0055AC90 4 lotr2_ranking_max_pop_player — player id with highest average province population; evidence: same
159 0x041094 0x00582F20 4 lotr2_unk_00582f20 — (provisional) unknown scalar
160 0x041098 0x00582D64 4 lotr2_ranking_max_treasury_player — player id with highest treasury; evidence: lotr2_game_ai_compute_player_rankings
161 0x04109C 0x005476BC 4 lotr2_ranking_max_county_player — player id with most provinces (counties); evidence: same
162 0x0410A0 0x0059E658 4 lotr2_ranking_max_county_count — highest province count across players; evidence: same
163 0x0410A4 0x0059B7F0 4 lotr2_ranking_max_castle_player — player id with most castles; evidence: same
164 0x0410A8 0x00593118 4 lotr2_ranking_max_castle_count — highest castle count across players; evidence: same
165 0x0410AC 0x005819F0 4 lotr2_ranking_max_troop_player — player id with most troops; evidence: same
166 0x0410B0 0x00581224 4 lotr2_ranking_max_troop_total — highest total troop count across players; evidence: same
167 0x0410B4 0x0055A780 4 lotr2_ranking_max_army_val — ranking: highest army count value; evidence: same
168 0x0410B8 0x0055A82C 4 lotr2_ranking_max_army_player — player id with most armies; evidence: same
169 0x0410BC 0x0059B88C 4 lotr2_game_difficulty_param — difficulty-derived config dword from DAT_004f7148[DAT_0059b8ac]; cleared on battle UI init
170 0x0410C0 0x0059B87C 4 lotr2_game_turn_phase1_ack — turn phase 1 toggle; see lotr2_game_state.md
171 0x0410C4 0x0059B880 4 lotr2_game_turn_phase2_ack — turn phase 2 toggle
172 0x0410C8 0x0059B884 4 lotr2_game_turn_phase3_ack — turn phase 3 toggle
173 0x0410CC 0x0059B888 4 lotr2_game_culture_count(difficulty+2) − player_count; see lotr2_game_state.md
174 0x0410D0 0x0055AC0C 4 lotr2_unk_0055ac0c — (provisional) unknown scalar
175 0x0410D4 0x0055AC38 4 lotr2_unk_0055ac38 — (provisional) unknown scalar
176 0x0410D8 0x005476B8 4 lotr2_unk_005476b8 — (provisional) unknown scalar
177 0x0410DC 0x00543680 4 lotr2_unk_00543680 — (provisional) unknown scalar; zeroed at new-game
178 0x0410E0 0x005934A0 0x30 (48) lotr2_combat_battlefield_variant_table — battlefield asset variant permutation table (48 B); shuffled at battle reset; evidence: lotr2_combat_reset_campaign_battle_state, lotr2_combat_load_open_field_map reads DAT_005934a0[DAT_0055aa14]
179 0x041110 0x0059BC44 4 lotr2_combat_battlefield_asset_pick — current battlefield asset index selected from variant table; evidence: DAT_0059bc44 = DAT_005934a0[DAT_0055aa14]
180 0x041114 0x00580B6C 4 lotr2_unk_00580b6c — (provisional) unknown scalar; near formation-threshold cluster
181 0x041118 0x00580B70 4 lotr2_unk_00580b70 — (provisional) unknown scalar; near formation-threshold cluster
182 0x04111C 0x00581130 0xF0 (240) lotr2_combat_focus_group_table — per-player combat focus-group index table, stride 0x28 per player; evidence: (&DAT_00581130 + (group-0x31)*4 + player*0x28) = lotr2_combat_focus_group_index
183 0x04120C 0x0055ABC0 4 lotr2_combat_peasant_count — peasant (unit type 0) figure head-count tally during battle tick; evidence: _DAT_0055abc0 = _DAT_0055abc0 + figure_head_count for type 0
184 0x041210 0x0055AC34 4 lotr2_combat_archer_count — archer/crossbow (unit types 1, 5) figure head-count tally; evidence: same accumulation loop
185 0x041214 0x0054363C 4 lotr2_combat_idle_infantry_count — idle infantry (unit type with state = 0) figure head-count tally; evidence: same
186 0x041218 0x005930DC 4 lotr2_unk_005930dc — (provisional) unknown scalar
187 0x04121C 0x0055BFA8 4 lotr2_unk_0055bfa8 — (provisional) unknown scalar; zeroed in battle init
188 0x041220 0x0055BFB4 4 lotr2_unk_0055bfb4 — (provisional) unknown scalar; zeroed in battle init
189 0x041224 0x0059E660 4 lotr2_unk_0059e660 — (provisional) unknown scalar; zeroed on combat retreat
190 0x041228 0x00583108 4 lotr2_game_great_noble_stat_column — Great Noble stat column 0–5; see lotr2_game_state.md
191 0x04122C 0x00543650 0x30 (48) lotr2_unk_00543650 — (provisional) unknown table (48 bytes); zeroed alongside lotr2_combat_battlefield_variant_table during battle reset
192 0x04125C 0x0059BC70 4 lotr2_unk_0059bc70 — (provisional) unknown scalar
193 0x041260 0x0059C390 0x38 (56) lotr2_combat_troop_compare_ui — per-type troop count array for battle compare UI panel, 14 × 4 bytes; evidence: lotr2_game_ui_draw_battle_player_compare_rows reads *(int *)(&DAT_0059c390 + type * 4)
194 0x041298 0x0055215C 4 lotr2_unk_0055215c — (provisional) unknown scalar
195 0x04129C 0x005930F4 4 lotr2_unk_005930f4 — (provisional) unknown scalar
196 0x0412A0 0x0055AC30 4 lotr2_unk_0055ac30 — (provisional) unknown scalar
197 0x0412A4 0x0059E65C 4 lotr2_unk_0059e65c — (provisional) unknown scalar
198 0x0412A8 0x0059B878 4 lotr2_game_inf_load_count — INF/load count index; incremented on each in-engine game load; indexes into a config table at stride 0x20; evidence: DAT_0055a364 = *(uint *)(&DAT_004f3ef8 + DAT_0059b878 * 0x20)
199 0x0412AC 0x0059C35C 4 lotr2_combat_knight_count — knight (unit type 6) figure head-count tally; evidence: lotr2_combat_assign_group_orders
200 0x0412B0 0x0055B7B8 4 lotr2_unk_0055b7b8 — (provisional) unknown scalar
201 0x0412B4 0x0050BB88 4 lotr2_unk_0050bb88 — (provisional) no export symbol in Ghidra C output; saved twice (also block 202); see open questions
202 0x0412B8 0x0050BB88 4 lotr2_unk_0050bb88duplicate of block 201 (same VA 0x0050BB88; both saved and loaded)

Region B — castle/siege tile slice (0x0412BC0x0732BB)

Slot File offset Size Notes
0 0x0412BC 0x3200 First castle tile record
1 0x0445BC 0x3200
0x3200
15 0x0700BC 0x3200 Last castle tile record

Data is transferred via a 0x3200-byte intermediate buffer (lotr2_castles_dat_io_buf at VA 0x00507AD0), not a direct bulk copy.


Data model#

Block Struct doc
Block 0 — province cell table lotr2_game_state.md — Province cell table
Block 1, 10 — battle tile planes lotr2_battle_tile.md
Block 2, 3 — player display-name + record array lotr2_player_record.md
Block 5 — army record table lotr2_game_state.md — Army/build tables
Block 8 — projectile slots lotr2_projectile.md
Block 9 — combat group table lotr2_battle_group.md

Evidence#

Symbol Address Role
lotr2_save_load_from_file 0x004C823E Save path (names swapped): opens write-only, crt_writes each field block
lotr2_save_write_to_file 0x004C838C Load path (names swapped): opens read-only, __reads each field block into RAM
lotr2_save_field_ptrs 0x004FA280 Static table of RAM pointers (stride 8, interleaved with sizes)
lotr2_save_field_sizes 0x004FA284 Static table of block sizes; loop terminates on first size == 0
lotr2_save_restart_after_load 0x004ADC92 Post-load: rebuilds map/PL8/army derived state

See also: architecture/save/MODULE.md.


Confidence#

Confirmed — field-block count, per-block file offsets, and total file size are extracted directly from the retail binary field-pointer table and independently corroborated by the decompiled load/save loops.

Likely — semantic interpretation of named blocks; field-interior layouts reference existing struct docs which carry their own confidence labels.

Partial — all 203 blocks now carry a human-readable symbol name; ~60 retain lotr2_unk_ provisional names pending further call-site analysis. Internal field layouts are documented only for the named structs listed in the data model table above.


Interop notes#

  • File size is exactly 471,740 bytes for every retail (LORDS2.EXE) save. Mismatched file size will cause a silent I/O error on load (lotr2_save_io_failed set).
  • Block boundaries are fixed at compile time; do not insert, remove, or resize blocks.
  • castles.dat must be present alongside the save for an in-game load to succeed; the loader overwrites the first 0x32000 bytes of castles.dat with Region B from the save.
  • The GoG build (gog/Lords2.exe) is expected to use a different field table (all VAs differ); do not assume offset compatibility between retail and GoG saves.

Open questions#

  • Confirm field layouts of newly-named large blocks: lotr2_recruit_flag_slots (block 6, 260 B), lotr2_combat_siege_sprite_slots (block 11, 128 B), lotr2_map_army_province_slots (block 12, 1 360 B).
  • Block 7 (lotr2_combat_figure_slots, 34 992 B): confirm stride 0x1B0 × 80 slots against lotr2_battle_figure struct (lotr2_battle_figure.md).
  • Whether GoG Lords2.exe uses an identical, extended, or entirely different field table.
  • Meaning of the two duplicate VA entries (blocks 32/48 = lotr2_combat_figure_dirty_ctr at 0x59BFD4; blocks 201/202 = lotr2_unk_0050bb88 at 0x50BB88): intentional double-save artifact or bug? 0x50BB88 has no symbol in the Ghidra C export; the nearby symbol DAT_0050bb8c is used in last-turn / phase-4 gate logic.
  • Blocks 151/152 (lotr2_ranking_top_player_id / lotr2_ranking_top_player_id_b): both are set to the same field of lotr2_game_player_rankings in lotr2_game_ai_compute_player_rankings; clarify whether they serve distinct consumers or one is stale.
  • Resolve ~60 lotr2_unk_* provisional scalars by expanding call-site analysis; priority candidates include blocks 26–30 (new-game initialisation cluster) and blocks 174–177 (adjacent to turn-phase acks).