Skip to content

Save / load#

Purpose#

Game-state persistence: enumerate save files, read/write the main save blob, sync the castles.dat tile slice, manage lords2.inf install config, and dispatch save/load from the UI (including multiplayer .sva/.svb variants).

Key entry points#

Core file I/O#

Symbol Address Role
lotr2_save_load_from_file 0x004c823e Load save: write field-table blocks; read 16×0x3200 from castles.dat
lotr2_save_write_to_file 0x004c838c Save game: read field-table blocks; write 16×0x3200 to castles.dat
lotr2_save_enum_files 0x004c9520 FindFirstFileA on *.sav/.sva/.svb; fill filename table
lotr2_save_sort_file_list 0x004c965b Sort enumerated save filenames
lotr2_save_read_inf_fields 0x004c89cd Read smaller inf field table (100 entries) from opened file
lotr2_save_sanitize_filename 0x004c8a6c Pad/truncate save filename to 0x20 chars

lords2.inf config#

Symbol Address Role
lotr2_game_load_lords2_inf 0x004c8559 Startup: enum saves, read lords2.inf, apply defaults
lotr2_game_write_lords2_inf 0x004c8500 Write 0x41c-byte config block to lords2.inf
lotr2_save_inf_stub 0x004c864a Empty placeholder in inf load path
lotr2_save_inf_on_load_success 0x004c865c Post-read: increment load counter or reset defaults
lotr2_save_inf_init_defaults 0x004c86af Seed default gameplay/audio globals in config block
lotr2_save_inf_init_audio_tiers 0x004c88c4 Default difficulty/audio tiers; MP vs SP branch

UI flow#

Symbol Address Role
lotr2_save_ui_debounce_dispatch 0x004c7de0 Timer dispatch for save, save-as, load screen modes
lotr2_save_wait_mp_ready 0x004c80a1 Post-save MP player-ready sync loop
lotr2_save_restart_after_load 0x004adc92 Rebuild map/PL8/army state after save or load
lotr2_save_continue_lastturn 0x004b0c1e Continue from lastturn.sav/.sva/.svb

Field tables and globals#

Symbol Address Role
lotr2_save_field_ptrs 0x004fa280 Main save field pointer column (up to 500 entries)
lotr2_save_field_sizes 0x004fa284 Main save field size column; loop ends on size 0
lotr2_save_inf_field_ptrs 0x004fa230 Inf field pointer column (100 entries)
lotr2_save_inf_field_sizes 0x004fa234 Inf field size column
lotr2_save_file_names 0x00504900 100 × 0xd-byte enumerated filename table
lotr2_save_file_count 0x00504544 Count of enumerated save files
lotr2_save_write_ok 0x005d9c78 Set after successful save write
lotr2_save_io_failed 0x005bc724 Non-zero when save/load I/O returns error

Save file layout#

Decompiled name swap — the Ghidra export has the save/load function names reversed. Open-flag evidence (0x8301 = write/create/truncate vs 0x8000 = read-only):

Ghidra name Address Actual role
lotr2_save_load_from_file 0x004c823e Save: opens file write-only; crt_writes field blocks into it
lotr2_save_write_to_file 0x004c838c Load: opens file read-only; __reads blocks into RAM; calls lotr2_save_restart_after_load

The behavioral spec uses the correct roles; the symbols retain the Ghidra names until a rename pass updates src/.

Full on-disk layout is specified in docs/spec/save/save-file-format.md.

flowchart LR
  save["lotr2_save_load_from_file\n(save path — name reversed)"]
  load["lotr2_save_write_to_file\n(load path — name reversed)"]
  fields["lotr2_save_field_ptrs + lotr2_save_field_sizes\n203 blocks, 266 940 bytes total"]
  castles["castles.dat slice\n16 × 0x3200 = 204 800 bytes"]
  save --> fields
  load --> fields
  save --> castles
  load --> castles
  1. 203 sequential field blocks from the static (ptr, size) table at lotr2_save_field_ptrs (0x004fa280); loop ends on size == 0 (entry 203). Total: 266,940 bytes (0x412bc).
  2. 16 × 0x3200 bytes of castle/siege tile data appended, synced with castles.dat. Grand total: 471,740 bytes (fixed; no header/checksum).

Regenerate the field-block table with:

python3 tools/ghidra/dump_save_field_table.py

File extensions#

Extension Role
.sav Single-player saves (*.sav, lastturn.sav)
.sva Multiplayer host saves (*.sva, lastturn.sva, diverged.sva)
.svb Multiplayer client saves (*.svb, lastturn.svb, diverged.svb)

Dependencies#

  • io/MODULE.mdlotr2_io_open, CRT __read/crt_write write
  • map/MODULE.mdlotr2_save_restart_after_load rebuilds province grid
  • game/MODULE.mdlotr2_game_load_lords2_inf at winmain startup
  • net/MODULE.md — MP save/load net messages (0x17–0x1d); .sva/.svb paths
  • ui/MODULE.md — loading messages during save I/O

Data files#

  • *.sav / *.sva / *.svb — full game-state saves
  • lords2.inf — 0x41c-byte install/config block (not a full save)
  • castles.dat — 16×0x3200 tile slice appended to each save
  • lastturn.* — auto-save at end of turn (role-specific extension in MP)
  • diverged.sva / diverged.svb — MP desync recovery loads

Open questions#

  • Full enumeration of main field-table entries at lotr2_save_field_ptrs203 blocks (see docs/spec/save/save-file-format.md)
  • Record sizes and ordering for each field-table block — documented in spec field-block map
  • On-disk header/checksum — none; raw block concatenation only
  • lotr2_combat_reset_message_queue at 0x485570 (was FUN_00485570; renamed pass 28)
  • Relationship between inf field table (100 entries) and lords2.inf layout
  • Semantic content of large unidentified blocks (e.g. block 7 0x549540 34 992 B, block 12 0x580bc0 1 360 B)
  • Whether GoG Lords2.exe uses the same field table (different VAs expected)