Files
sacred_k/docs/characters.md
T
khallaandClaude Opus 5.5 db87e276bf Menu screens: rock background, centred frame, logo, 3D hero
- Spielen hero panel shows the selected hero in 3D (cUI_NetGranny,
  game/ui/granny.h).
- All menu screens draw on the options' rock wall; the main menu's 1024
  frame is drawn centred over it, mouse and cursor shifted to match.
- The main menu's and options' own pictures and the main menu's 3D scene
  are left out (textures hidden by surface, scene call redirected).
- Logo top left: FX_SACREDLOGO.TGA, drawn added.
- Game textures by name (game/ui/texture.h), textured and added quads.

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
2026-10-21 00:05:55 +02:00

319 lines
18 KiB
Markdown
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
# 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 (dwords at +0x88.., loader
0x61bb19..) and in the hero header chunk 0xc3 (bytes at +0xed.., written by
0x604360, read by loadHero 0x606632..; +0xec is `g_gameLevel`). Checked in
a character file (2026-10-20): Silber and Gold completed, played on Platin =
[0, 1, 1, 0, 0] in the hero and the Platin world. 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.
- Main menu: "Kampagne Ancaria", "Kampagne Underworld" and "Spielstände"
become one "Spielen": the characters, loaded or started in either campaign
on any difficulty; new characters from there too. A character file holds a
world per campaign and difficulty; the format must take more difficulties
later. Decided: no level requirement for the Underworld (vanilla: hero level
25, 0x70c48b), story completions open the next difficulty in both campaigns,
"Neuer Charakter" is a button in the Spielen window.
### Campaigns (measured / read 2026-10-20)
- `g_campaign` 0x182cdc8 (u16): 0 Ancaria, 1 Underworld. The main menu's
event handler (0x711fa0) sets it by entry name before an entry handles an
event (UI_LABEL_NEWGAME 0, UI_LABEL_NEWADDON 1); both entries show
UI_CHARACTER. `cEngine::Save` writes it to the save header +0x24 (2
Ancaria, 3 Underworld, 0x61a5c5), `cEngine::Load` sets it from there
(0x61bafd). The load window lists only saves of the running campaign
(0x71e2b9).
- Story completions are counted per difficulty only (the same counter for
both campaigns).
- Main menu (cUI_MainMenu, entries built by 0x712f30): cUI_StaticText64FX
entries (constructor 0x753d70), name +0x30, rect +0x24 (0x20 apart), text
id +0x78, command +0x88; main page vector +0x158, children +0x90.
## 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, lastCampaign, lastDifficulty,
migratedFrom, heroFrom, heroFromCampaign), `hero` (AMH as
`cEngine::SaveHero` writes it), one world per campaign and difficulty,
`ancaria<d>` / `underworld<d>` with d = `g_gameLevel` (AMS as
`cEngine::Save` writes it). Sections are named, not slotted: a higher
difficulty (or another campaign) is one more section. Files of the first
format named Ancaria's worlds `world<d>`; they are renamed when read
(charfile.cpp) and written under the new name at the next save. Written to
a temp file, then moved over.
- `CharacterNN.autosave.sacredK`: same layout, one world (`meta.campaign`,
`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\`.
### Main menu (`src/mods/mainmenu.cpp`)
After the build (0x712f30) the NEWGAME and NEWADDON entries are removed (out
of both vectors, deleted) and UI_LABEL_LOADGAME ("Spielstände", shows
UI_WND_SAVEGAME) moves to the top with the text "Spielen"; the others move up,
keeping the gap before "Beenden".
### Spielen screen (cUI_Savegame)
Request 2026-10-20: redesign the load window, 16:9, using the space (layout
agreed: characters left, the character in the middle, worlds right, buttons
below; our own background). Built on the shared screen kit
(docs/screens.md); the window's own drawing and mouse handling are not used.
- The window still scans: before each scan (0x71e330) one preview
`GAMEnn.PAK` per character is written into `_work`; row nn = character nn.
Its entries are only needed for the load.
- Left: the characters, newest save first (name, class, level; "im Spiel" for
the one being played). Click selects, double click plays.
- Middle: name, class and level, the world played last with its date, and
the hero in 3D with its equipment (2026-10-20): the game's cUI_NetGranny
(game/ui/granny.h), the control the export screen and the multiplayer
lobby showed heroes with. The character's hero file goes to
`_workpreview.pax`, LoadHeroFile (path, 0, info) reads its hero info
(class, equipment looks) without touching the player hero. Drawn without
its name label, with the white light set, size factor 3.8 (game 2.5); its
x is moved by (1365 - 1024) / 2, as it centres the model as in a 1024
layout. Migrated characters without the shared hero show none until
played.
- Right: a grid campaigns x difficulties. A cell shows the save's date,
"+ Autosave" when the autosave is newer, "Story abgeschlossen", "Neu" for a
difficulty that is open but has no world, "–" when closed. Open: Bronze and
Silber, every difficulty up to the highest with a world in either campaign,
and the one after each difficulty whose story the shared hero completed
(hero header +0xed..); new worlds need the shared hero, not in multiplayer
(byte 0x182cb6c). Click selects, double click plays. With a newer autosave
"Laden von:" picks the save or the autosave (default: the autosave).
"Welt zurücksetzen" for a saved world (not the character being played).
- Buttons: Neuer Charakter (main menu only), Umbenennen, Löschen (not for the
character being played), Zurück (action 0xc), Spielen.
- Spielen: a saved world loads through the window's load action (4, with its
"really load?" confirmation in a game): the window's selection is set to
its first entry and `g_loadRow` names the row, which `Engine_Load` takes
instead of the entry's. A new world starts a game (below; in a game after
stopping the running one). The window's own load / default actions (Enter)
run Spielen too; its delete actions (7, 8) are ignored.
- Dialogs over the screen: rename (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`), campaign of a new character, confirmations of reset and
delete. Buttons are cUI_Button2 copies of the load button
(`game::NewButtonLike`) with our own text ids and overrides.
- Delete moves the character file and its autosave to `charactersdeleted`.
- Neuer Charakter: sets `g_campaign`, hides the window and shows UI_CHARACTER
(the game's character creation, as the main menu's campaign entries did).
The first save makes the character file.
- Reset (decided 2026-10-20) removes the world section (a copy goes to
`charactersdeletedCharacterNN.sacredK.<section>.PAK`) and an autosave of
that world, and clears the shared hero's story completion of that
difficulty (hero header +0xed + d); the game counts it per difficulty, so
the next difficulty closes in both campaigns unless a world keeps it open.
### Character window (cUI_Character)
- Vanilla made no new heroes for the Underworld: `cUI_Character::Show`
(vf24 0x70e2a0) switches to mode 7 in single player with `g_campaign` 1
(0x70e3e8), the import of an exported hero of level 25+ (0x70eedc); with no
exported heroes its slots were empty, and slots below level 25 never lit
up (render 0x70f4df / 0x70f57c) (reported 2026-10-20). All eight campaign
checks of the window (0x70c473, 0x70d199, 0x70e3e8, 0x70eade, 0x70eeb2,
0x70f4df, 0x70f57c, 0x70f6aa) read a constant Ancaria instead: creation
with all classes in both campaigns; the start (ExecuteAction 0x70d0f0,
action 1) uses `g_campaign` as set.
- Class selection without "Importieren" (request 2026-10-20): button +0x3a0
(caption UI_IMPORT in modes 0/1); its modes-0/1 draw (0x70f67e) is removed,
its click (0x70eab4 / 0x70eac7, SetMode(7)) jumps to the handler's end.
### 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.
Silent autosave: 10 minutes after the last save of any kind (or the game's
start), `SaveOnTick` does the direct save marked as an autosave, so it goes
into the autosave file. While a window is open or a save runs it waits for
the next tick. A character never saved yet gets its character file from it.
Interval: options window, *Allgemein*, *Autosave (Min.)* (docs/options.md); kept in
`sacredK.ini` `[characters] AutosaveMinutes` (default 10, 0 off).
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).
The Esc menu: "Exportieren" became "Laden" (requests 2026-10-20). The line
(text id 0x419 pushed at 0x6bc272, now "Laden") keeps its command, which shows
UI_CHARACTER (the export screen) and hides the menu; in a game
`UiManager_ReceiveEvent` turns that into showing the load window. The load
window always opens in load mode (`cUI_Savegame::Show` vf24 0x71cfb0 picks
save mode in a game; `SetMode` 0x71c4f0 (2) after it). In a game every
world can be loaded, the current one too (a reload from its last save; the
game's in-game load, engine flag 0x200). A "(neu)" world needs a new engine:
the running game stops as "Beenden" stops it (`cCommand_armaStop`, execute
0x755b30, vtable 0x88ee2c: posts 0x8001 to the main window, whose handler
deletes the engine), and when the engine is gone (`OnGameEnd`) the new game's
start command follows (0x8002), without the main menu. The character being
played cannot be deleted. The export screen (cUI_Character
mode 5) is unreachable; its mod (charexport.cpp, an erase button for export slots) was deleted.
### Migration
On first start (no `characters\` folder): after a backup in
`Documents\Sacred\backup\<stamp>_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
- Quickload (row 0) does not know characters (not needed, user 2026-10-20).
- Spielen screen: scrolling the list beyond 9 characters.
- Multiplayer (see `todo.md`).