Characters: one file per character with a world per difficulty

- CharacterNN.sacredK holds the shared hero and one world per difficulty,
  plus an autosave file; the game works in save\characters\_work
- Load window: one row per character, a difficulty dialog on Laden
  (worlds, newer autosaves, "(neu)" starts a new game with the shared
  hero), Umbenennen, delete moves the character to characters\deleted
- The world keeps its own portals and hero position when the shared hero
  is put on
- K and the Esc menu's "Speichern" save directly (quicksave path)
- One-time migration of the old save rows and exported heroes, after a
  backup
- Startup no longer hangs in the game's foreground loop (0x810af0)
- Text overrides apply to our own text ids too; shared cUI_Button2
  helpers in game/ui/button.h

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 01:41:42 +02:00
1 parent 5df7efc198
commit 5bccb0734e
16 files changed
+2052 -64

No files matched your search

+82
View File
@@ -0,0 +1,82 @@
#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 <cstdint>
#include <optional>
#include <string>
#include <vector>
#include <nlohmann/json.hpp>
namespace mods::characters {
using Bytes = std::vector<uint8_t>;
// Bronze, Silber, Gold, Platin, Niob (g_gameLevel 0..4).
constexpr int kDifficultyCount = 5;
// 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
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
bool multiplayer = false; // AMS header +0x2c
uint64_t savedAt = 0; // AMS header +0x5c SYSTEMTIME, as FILETIME
};
std::optional<SaveInfo> 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);
// 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);
// Reading and writing whole files (paths in the game's code page).
std::optional<Bytes> ReadFile(const std::string& path);
bool WriteFileReplacing(const std::string& path, const Bytes& bytes);
// A character file: named sections ("meta" JSON, "hero" .pax, "world<d>"
// 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<Bytes> 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<Bytes> data; // set or changed since Open
};
std::string path_;
std::vector<Section> sections_;
};
std::string WorldSection(int difficulty);
} // namespace mods::characters