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:
- Compute
offset = sum(FIELD_SIZES[:i]). - Read
FIELD_SIZES[i]bytes starting atoffset. - Modify the bytes in place (use struct layout from the field map below and the referenced struct docs).
- Write the same number of bytes back at the same offset.
Never truncate, extend, or re-order the file.
Writing a save from scratch#
- Allocate a 471,740-byte buffer.
- For each block
iin0..202, copyFIELD_SIZES[i]bytes from RAM (or a reconstructed state) into the cumulative file position. - At
0x0412BC, write 16 × 0x3200 bytes of castle/siege tile data (first0x32000bytes of the currentcastles.dat). - 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.datstarting 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 first0x32000bytes.
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_ctr — duplicate 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_0050bb88 — duplicate of block 201 (same VA 0x0050BB88; both saved and loaded) |
Region B — castle/siege tile slice (0x0412BC–0x0732BB)
| 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_failedset). - Block boundaries are fixed at compile time; do not insert, remove, or resize blocks.
castles.datmust be present alongside the save for an in-game load to succeed; the loader overwrites the first0x32000bytes ofcastles.datwith 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 againstlotr2_battle_figurestruct (lotr2_battle_figure.md). - Whether GoG
Lords2.exeuses an identical, extended, or entirely different field table. - Meaning of the two duplicate VA entries (blocks 32/48 =
lotr2_combat_figure_dirty_ctrat0x59BFD4; blocks 201/202 =lotr2_unk_0050bb88at0x50BB88): intentional double-save artifact or bug?0x50BB88has no symbol in the Ghidra C export; the nearby symbolDAT_0050bb8cis 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 oflotr2_game_player_rankingsinlotr2_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).