#pragma once // 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 #include #include #include #include #include namespace mods::characters { using Bytes = std::vector; // 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. 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 storyCompleted{}; bool world = false; // a save game (AMS), not a hero file int difficulty = 0; // AMS header +0x84 (g_gameLevel) int campaign = kCampaignAncaria; // AMS header +0x24 (CampaignSaveMode) bool multiplayer = false; // AMS header +0x2c uint64_t savedAt = 0; // AMS header +0x5c SYSTEMTIME, as FILETIME }; std::optional ReadSaveInfo(const Bytes& file); // Writes `name` as the hero's name (hero header +8, 0x40 UTF-16 characters // with the terminator), the only place the name is stored; false if the file // has no hero header or the name is empty or too long. constexpr size_t kHeroNameMaxChars = 63; bool SetHeroName(Bytes& file, const std::wstring& name); // Sets the hero header's story completions of `difficulty` (+0xed..; in a hero // file they are the hero's, loadHero puts them into cStatsManager); false if // the file has no hero header. bool SetStoryCompleted(Bytes& file, int difficulty, uint8_t count); // 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. 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 ReadFile(const std::string& path); bool WriteFileReplacing(const std::string& path, const Bytes& bytes); // A character file: named sections ("meta" JSON, "hero" .pax, "world" // GAME.PAK). Sections are read from disk on demand; Save writes all of them // to a temporary file and replaces the old one. class CharacterFile { public: explicit CharacterFile(std::string path) : path_(std::move(path)) {} // Reads the section table and the meta; false if the file is missing or // not a character file. bool Open(); const std::string& Path() const { return path_; } bool Has(const std::string& section) const; std::optional Read(const std::string& section) const; void Set(const std::string& section, Bytes bytes); void Remove(const std::string& section); bool Save(); nlohmann::json meta = nlohmann::json::object(); private: struct Section { std::string name; uint32_t offset = 0; // in the file; unused once `data` is set uint32_t size = 0; std::optional data; // set or changed since Open }; std::string path_; std::vector
sections_; }; // A world's section: "ancaria" / "underworld", 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