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

+20 -4
View File
@@ -2,6 +2,7 @@
// Character files (docs/characters.md): one file per character with its shared
// hero and one world per difficulty, plus reading the game's save files.
#include <array>
#include <cstdint>
#include <optional>
#include <string>
@@ -16,6 +17,11 @@ using Bytes = std::vector<uint8_t>;
// Bronze, Silber, Gold, Platin, Niob (g_gameLevel 0..4).
constexpr int kDifficultyCount = 5;
// The campaigns (g_campaign 0x182cdc8): Ancaria 0, Underworld 1.
constexpr int kCampaignAncaria = 0;
constexpr int kCampaignUnderworld = 1;
constexpr int kCampaignCount = 2;
// What the game's save files (GAME.PAK "AMS", hero files .pax "AMH") say about
// their hero: header 0x100 bytes, then a table of 64 chunks {type, offset,
// size}; chunk 0xc3 is the hero header.
@@ -23,9 +29,12 @@ struct SaveInfo {
std::wstring name; // hero header +8, UTF-16
uint32_t heroClass = 0; // hero header +4: 1 Seraphim .. 9 Daemonin
uint32_t level = 0; // hero header +0
// Story completions per difficulty (hero header +0xed.., cStatsManager
// +0x5704; the hero file's are the hero's, loadHero sets them).
std::array<uint8_t, kDifficultyCount> storyCompleted{};
bool world = false; // a save game (AMS), not a hero file
int difficulty = 0; // AMS header +0x84 (g_gameLevel)
uint32_t mode = 0; // AMS header +0x24: 2 normal, 3 hardcore
int campaign = kCampaignAncaria; // AMS header +0x24 (CampaignSaveMode)
bool multiplayer = false; // AMS header +0x2c
uint64_t savedAt = 0; // AMS header +0x5c SYSTEMTIME, as FILETIME
};
@@ -37,10 +46,15 @@ std::optional<SaveInfo> ReadSaveInfo(const Bytes& file);
constexpr size_t kHeroNameMaxChars = 63;
bool SetHeroName(Bytes& file, const std::wstring& name);
// The save header's campaign field (+0x24): 2 Ancaria, 3 Underworld (written
// at 0x61a5c5, read back by cEngine::Load 0x61bafd).
uint32_t CampaignSaveMode(int campaign);
// A small save game the game's load window can list: `world`'s header with its
// date, its thumbnail and world time, and `description` (ANSI, up to 63
// characters) as the save's name.
Bytes BuildPreview(const Bytes& world, const std::string& description);
// characters) as the save's name. The load window lists only saves of the
// running campaign (0x71e2b9): the preview names `campaign`.
Bytes BuildPreview(const Bytes& world, const std::string& description, int campaign);
// Reading and writing whole files (paths in the game's code page).
std::optional<Bytes> ReadFile(const std::string& path);
@@ -77,6 +91,8 @@ private:
std::vector<Section> sections_;
};
std::string WorldSection(int difficulty);
// A world's section: "ancaria<d>" / "underworld<d>", d = g_gameLevel. Sections
// are named, not slotted: more difficulties (or campaigns) are more sections.
std::string WorldSection(int campaign, int difficulty);
} // namespace mods::characters