# Characters and difficulties (one file per character) ## Request (2026-10-19) Get rid of exporting characters. One character = one save file; inside it one world state per difficulty. The hero (gear, level, attributes, skills, chest, everything) is shared between the difficulties; only the unlocked portals are per difficulty (they have to be found again on each level). - Unlocking: Bronze and Silver are open from the start; each following difficulty opens by completing the story on the one before it. More difficulties shall be addable later. - The export / import screen goes away. The load window lists characters; loading asks for the difficulty. - "Spiel speichern" saves the current character into its current difficulty. No "switch difficulty" button: save, end the game, load on another difficulty. No save on quit. - A silent autosave every few minutes into a separate slot, so the player still decides when the real save is updated. - Existing saves are migrated. - Delete a character (everything of it); reset a single difficulty. - Multiplayer: later (see `todo.md`). Decided (2026-10-19, follow-up): - Unlocking by story completion only; Bronze and Silver always open (maybe only Bronze after a later rebalance). No level ranges: enemies always scale with the player and the difficulty. - Autosave every 10 minutes for now (a game setting later); the load window offers it per character when it is newer than the real save. - The game's own autosaves on story steps also go into the autosave slot, silently. - Migration as proposed (group by hero name + class), a backup of all saves first. Done by hand once: `Documents\Sacred\backup\2026-10-19_before_characters` (Documents save folder and game save folder); the migration code makes its own backup too. ## Measured / read so far ### Files - Save games `GAMEnn.PAK` ("AMS", version 0x1b): a full world dump; the hero is one object in it (`* Objects`, ref 1, family 3) and its items are in `* Inventory`. Chunks are listed by the loader's log (`* Engine`, `* world`, `* calendar`, `* Blood`, `* RegionInfo`, `* Team/AllianceInfo`, `* Objects`, `* Inventory`, `* Particles`, `* Views`, `* Weather`, `* Scripts`). - Hero files `Heronn.pax` ("AMH"): written by `ItemDataMgr::saveHero` `0x6046f0`; header 0x22c bytes + chunks (items, inventories, chest, stats block). The hero's **portals** and world position are part of it (log `saveHero ... wpos[..] portals[%x]`; portal bits on the hero at +0x578). ### Functions | Address | What | |---|---| | 0x60ac70 | `cEngine::cEngine`: new game or save-game load | | 0x61b790 | `cEngine::Load(path, quiet)`: reads a `GAME.PAK` (version check 0x15..0x1b), chunk loop, logs `---LOADING SAVEGAME.PAK` | | 0x619c90 | `cEngine::Save(path)`: called from the render thread when engine +0x54 has 0x40/0x80; row = engine +0x68 | | 0x60dc00 | `cEngine::InitGame`: new game (campaign start, start equipment) | | 0x61cda0 | `cEngine::SaveHero(path)`: hero export (`saveHero` into a RAM file, then to disk) | | 0x7d7c40 | session singleton (0x3ad0 bytes, `g_session` 0x182cb68) | | 0x7efd90 | `Session::SelectHeroSlot(slot 1..8)`, ret 4: loads `Save\Hero%02d.pax` into the session's RAM file +0x3a3c, sets +0x49 = 1 (hero from file), +0x37b8 = slot, reads the hero header into +0x16a (0x22c bytes) | | 0x755ac0 | `cCommand_armaPlay::execute`: posts 0x8002 (mode 1 load row, 4 load multi, else init) to the main window | | 0x706b90 | difficulty list for a hero level (Bronze .. Niob) | - With session +0x49 set, the engine constructor loads the hero from the session's file into the **loaded save game** (`loadHero` at 0x60ba65) and destroys the old hero's items afterwards. That is the vanilla mechanism for "this world, that hero" and the base of the shared hero. - In-game save / load: engine +0x54 flags 0xc0 (save row +0x68 via `cEngine::Save`), 0x200 (load row via `cEngine::Load`). ### Difficulty - `g_gameLevel` 0x182cdc4: current difficulty, from the save header (log `GameLevel[n]`). Index mapping still to confirm (0 = Bronze ..). - Story completion: `cStatsManager` (`cStatsManager::Instance(player)` 0x425160) +0x5704, one byte counter per difficulty (6 entries). `0x424bc0` (k) increments, `0x424be0` (k) reads, `0x424c00` (k, value) sets. The script opcode at 0x4aed26 increments counter[`g_gameLevel`]: the story end. The counters travel in the save header (loader 0x61bb19..) and in the hero file (loadHero 0x606632.., saveHero 0x60444c..). The console cheats `bronze` / `silber` / ... (0x615d00..) increment them too. - Vanilla offers a difficulty by **hero level**, not by completion: thresholds `0xad2dc4..0xad2dd4` (5 dwords, parsed from the balance data key `StartLevMulti` at 0x5ef12c). Silver for levels [dc4, dcc], Gold [dc8, dd0], Platinum [dcc, dd4], Niob >= dd0, Bronze <= dc8. The same thresholds reduce experience when the hero is far above the difficulty (0x57dd60). Values: to be logged at runtime. ### Runtime log (2026-10-19, vanilla flows) | Flow | What happened | |---|---| | load row 2 | `armaPlay load [2]` -> `cEngine(0x00000002)` -> `cEngine::Load("./SAVE/GAME02.PAK", 1)`; engine +0x68 = row | | in-game save | `cEngine::Save("SAVE/GAME02.PAK")`, engine +0x54 = 0x444 | | new game, exported hero, Silver | `cUI_Character::executeAction(IMPORT_DO) difficulty[1]`, `cEngine(0x80080000)`: bit 31 new game, bits 16..23 = hero slot + 1; `InitGame` creates a start hero of class session +0x44 (9) with start equipment, then `LoadHeroFile("Save\Hero07.pax", 1, 0)` (`0x61d190`, cdecl) loads the exported hero onto it | | new game, new hero, Bronze | `armaPlay init`, `cEngine(0x80000000)`, class +0x44 = 6, no hero file | - `g_gameLevel`: 0 Bronze, 1 Silver (2 Gold, 3 Platinum, 4 Niob). The battlemage row is Silver. - Level thresholds at runtime: 20, 60, 100, 140, 180. - `Session::SelectHeroSlot` and session +0x49 are not used in single player (multiplayer). The hero swap block after it (0x60ba1c..) is not on the single-player load path. - `LoadHeroFile(path, 1, 0)` puts a hero file onto the existing player hero, start items and all: the same operation the shared hero needs after a world load. - Confirmed in-game (2026-10-19/20): `LoadHeroFile(hero, 1, 0)` called right after `cEngine::Load` returns puts the hero onto the loaded world's hero. An exported battlemage loaded onto another character's world came out exact: level, attributes, skills, gear, backpack, chest; nothing duplicated or left behind. It even replaced the class (dwarf hero onto a battlemage world). The result survives an in-game save and reload, with no cheat-check message. ## Follow-up requests (2026-10-20) - Numbered files (`CharacterNN.sacredK`), one list row per character; the difficulty is chosen when loading, in a dialog (or a newer autosave, marked). - Rename a character from the load window. - Every difficulty below the highest played one is always available; one without a world starts a new game with the shared hero. - The save key and the Esc menu ("Spielstände" becomes "Speichern") save directly, without the save window. ## Implementation (`src/mods/characters.cpp`, `charfile.{h,cpp}`) ### Files - `Documents\Sacred\save\characters\CharacterNN.sacredK`: "SKCH", version 1, section count, then `{name[16], offset, size}` entries. Sections: `meta` (JSON: format, name UTF-8, class, level, lastDifficulty, migratedFrom, heroFrom), `hero` (AMH as `cEngine::SaveHero` writes it), `world` (AMS as `cEngine::Save` writes it). Written to a temp file, then moved over. - `CharacterNN.autosave.sacredK`: same layout, one world (`meta.difficulty`). - The game's save folder is redirected to `characters\_work` (userfolders); the game reads and writes `world.PAK` / `hero.pax` there. - Deleted characters move to `characters\deleted\`. ### Load window (cUI_Savegame) - Before each scan (0x71e330) one preview `GAMEnn.PAK` per character is written into `_work` (header, thumbnail, world time, description "Name - Klasse"); row nn = character nn. The row's second line (call 0x71db2a to SetText 0x736f00) shows "Stufe N - difficulty". - Laden (action 4 / 0xb) opens the difficulty dialog when there is more than one choice: worlds, newer autosaves "(Autosave)", and "(neu)" for every difficulty up to max(highest world, Silber) without a world (needs the shared hero; not in multiplayer, byte 0x182cb6c). - "Umbenennen" (below Löschen; not for the character being played) asks for a name; keys go to the field through the input key capture. The name is only in the hero header chunk 0xc3 (+8, UTF-16, 0x40 chars): rewritten in the hero, every world and the autosave, plus `meta.name`. Confirmed: the game loads renamed characters without complaint. - Dialog buttons are cUI_Button2 copies of the load button (`game::NewButtonLike`); their captions are our own text ids with overrides (texts.cpp checks overrides for our ids too). ### Load `cEngine::Load` (0x61b790) of a listed row: the chosen world and the hero are unpacked to `_work`, the original loads `world.PAK`, then `LoadHeroFile` (0x61d190) puts the shared hero on. The world's portals (hero +0x578) and position (+0x18..+0x28) are put back afterwards: the hero file holds the whole hero object and `loadHero` deserialises it over the hero, position included (measured: the hero stood where it was saved on another difficulty; the hero stays the same object). A migrated character without a shared hero takes it from the first loaded world (`heroFrom`). ### New world on a difficulty As `cUI_Character::executeAction(IMPORT_DO)` (0x70d637): session (0x7d7c40) +0x14 = 1, +0x44 = class, GameSetup (0x417e50) +0 = 1, `g_gameLevel` = d, then cCommand_armaPlay (vtable 0x894ff4, execute 0x755ac0) mode 1, argument `0x80000000 | 1 << 16`. cEngine's constructor runs InitGame and then `LoadHeroFile("Save\Hero00.pax")` (0x60bf2c); our hook loads the character's hero instead, keeps the start position, clears the portals and makes the game the character's. ### Save `cEngine::Save` (0x619c90) writes `world.PAK`, then `SaveHero` writes `hero.pax`; both go into the current character (a new one gets the next free number). Story autosaves (engine event 0xf subtype 2 with the flag at +0x18, noted in `cEngine::ReceiveEvent`) go into the autosave file. Direct save (K key, Esc menu "Speichern"): on the next engine tick, as the quicksave does (0x618ebb): `cUI_Manager::CanOpenWindow` (0x75a690), `cEngine::CaptureScreen` (0x613710, 1), description +0x6a, row +0x68 = 0, flag +0x54 |= 0x40. The Esc menu line keeps its action (Show `UI_WND_SAVEGAME`); `cUI_Manager::ReceiveEvent` (0x757120) turns that into the direct save while a game runs. Its text is patched at 0x6bc1f7 (id 0x41b is used by other windows too). Loading is from the main menu only. ### Migration On first start (no `characters\` folder): after a backup in `Documents\Sacred\backup\_characters_migration`, `GAME*.PAK` and `Hero*.pax` are grouped by (name, class); the newest world per difficulty is kept, the newest file overall is the shared hero. ## Open - Unlocking by story completion (counters at save header +0x88..). - Reset a difficulty. - Remove the export / import UI. - Silent autosave every 10 minutes. - Quickload (row 0) does not know characters. - Multiplayer (see `todo.md`).