Characters: campaigns, "Spielen" main menu, story unlocks

- One world per campaign and difficulty in a character file (sections
  ancaria<d> / underworld<d>; old world<d> renamed when read). Difficulty
  unlocks by the shared hero's story completions (hero header +0xed..),
  shared by both campaigns.
- Main menu: "Kampagne Ancaria", "Kampagne Underworld" and "Spielstaende"
  become "Spielen" (mods/mainmenu.cpp). The load window lists all
  characters, the world dialog offers both campaigns, "Neuer Charakter"
  asks for the campaign and opens character creation.
- Character window: no Underworld import mode (new Underworld heroes, all
  classes, slots light up), no "Importieren"; load window without
  "Speichern".

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
This commit is contained in:
khallaandClaude Opus 5.5 committed 2026-10-20 21:48:47 +02:00
1 parent 5bccb0734e
commit 618288e310
7 files changed
+498 -109

No files matched your search

+77 -13
View File
@@ -78,8 +78,11 @@ Decided (2026-10-19, follow-up):
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
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
@@ -123,6 +126,28 @@ Decided (2026-10-19, follow-up):
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}`)
@@ -130,24 +155,64 @@ Decided (2026-10-19, follow-up):
- `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<d>` (AMS as
`cEngine::Save` writes it). Written to a temp file, then moved over.
- `CharacterNN.autosave.sacredK`: same layout, one world (`meta.difficulty`).
(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".
### 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).
"Name - Klasse"); row nn = character nn. The preview's campaign field is
the running campaign, so the window lists every character. The row's
second line (call 0x71db2a to SetText 0x736f00) shows
"Stufe N - Campaign - difficulty".
- Laden (action 4 / 0xb) opens the world dialog when there is more than one
choice: per campaign worlds, newer autosaves "(Autosave)", and "(neu)" for
every difficulty without a world up to the highest open one (needs the
shared hero; not in multiplayer, byte 0x182cb6c). 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..). `g_campaign` is set to the chosen world's before loading
or starting.
- "Neuer Charakter" (two places below Löschen; main menu only) asks for the
campaign, 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.
- 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 window without "Speichern" (request 2026-10-20; saving is direct):
button +0x820 (action 1, disabled in load mode, so its click falls through)
is not drawn (jump over 0x71ddbc); Löschen (+0x8dc) takes its rect, our
buttons follow below it.
- "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
@@ -202,7 +267,6 @@ 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.