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

+35 -6
View File
@@ -1,5 +1,6 @@
#include "mods/charfile.h"
#include <algorithm>
#include <cstring>
#include <fstream>
@@ -25,8 +26,11 @@ constexpr size_t kHeroLevel = 0x0;
constexpr size_t kHeroClass = 0x4;
constexpr size_t kHeroName = 0x8;
constexpr size_t kHeroNameChars = 0x40;
constexpr size_t kHeroStoryCompleted = 0xed; // one byte per difficulty (written at 0x604442..)
constexpr size_t kWorldMode = 0x24;
constexpr size_t kWorldCampaign = 0x24;
constexpr uint32_t kSaveModeAncaria = 2;
constexpr uint32_t kSaveModeUnderworld = 3;
constexpr size_t kWorldMultiplayer = 0x2c;
constexpr size_t kWorldSavedAt = 0x5c; // year, month, day, weekday, hour, minute, second, ms (dwords)
constexpr size_t kWorldDifficulty = 0x84;
@@ -86,10 +90,13 @@ std::optional<SaveInfo> ReadSaveInfo(const Bytes& file) {
std::memcpy(&info.heroClass, header + kHeroClass, 4);
const auto* name = reinterpret_cast<const wchar_t*>(header + kHeroName);
info.name.assign(name, wcsnlen(name, kHeroNameChars));
if (hero->size >= kHeroStoryCompleted + info.storyCompleted.size()) {
std::memcpy(info.storyCompleted.data(), header + kHeroStoryCompleted, info.storyCompleted.size());
}
info.world = file[2] == 'S';
if (info.world) {
info.difficulty = static_cast<int>(U32(file, kWorldDifficulty));
info.mode = U32(file, kWorldMode);
info.campaign = U32(file, kWorldCampaign) == kSaveModeUnderworld ? kCampaignUnderworld : kCampaignAncaria;
info.multiplayer = U32(file, kWorldMultiplayer) != 0;
SYSTEMTIME time{};
WORD* fields[] = {&time.wYear, &time.wMonth, &time.wDay, &time.wDayOfWeek,
@@ -116,9 +123,14 @@ bool SetHeroName(Bytes& file, const std::wstring& name) {
return true;
}
Bytes BuildPreview(const Bytes& world, const std::string& description) {
uint32_t CampaignSaveMode(int campaign) {
return campaign == kCampaignUnderworld ? kSaveModeUnderworld : kSaveModeAncaria;
}
Bytes BuildPreview(const Bytes& world, const std::string& description, int campaign) {
Bytes preview(world.begin(), world.begin() + std::min(world.size(), kChunkData));
preview.resize(kChunkData);
PutU32(preview, kWorldCampaign, CampaignSaveMode(campaign));
std::memset(preview.data() + kChunkTable, 0, kChunkCount * kChunkEntrySize);
size_t entry = kChunkTable;
auto append = [&](uint32_t type, const uint8_t* data, uint32_t size) {
@@ -161,10 +173,27 @@ bool WriteFileReplacing(const std::string& path, const Bytes& bytes) {
return MoveFileExA(temp.c_str(), path.c_str(), MOVEFILE_REPLACE_EXISTING | MOVEFILE_WRITE_THROUGH) != 0;
}
std::string WorldSection(int difficulty) {
return "world" + std::to_string(difficulty);
std::string WorldSection(int campaign, int difficulty) {
return (campaign == kCampaignUnderworld ? "underworld" : "ancaria") + std::to_string(difficulty);
}
namespace {
// Format of 2026-10-20 (before campaigns): "world<d>" were Ancaria's worlds;
// renamed when read, written under the new name at the next save.
constexpr char kOldWorldPrefix[] = "world";
std::string CurrentSectionName(const std::string& name) {
constexpr size_t length = sizeof(kOldWorldPrefix) - 1;
if (name.size() > length && name.compare(0, length, kOldWorldPrefix) == 0 &&
std::all_of(name.begin() + length, name.end(), [](char c) { return c >= '0' && c <= '9'; })) {
return WorldSection(kCampaignAncaria, std::stoi(name.substr(length)));
}
return name;
}
} // namespace
bool CharacterFile::Open() {
sections_.clear();
std::ifstream file(path_, std::ios::binary);
@@ -181,7 +210,7 @@ bool CharacterFile::Open() {
}
Section section;
const auto* name = reinterpret_cast<const char*>(entry);
section.name.assign(name, strnlen(name, kSectionNameSize));
section.name = CurrentSectionName(std::string(name, strnlen(name, kSectionNameSize)));
std::memcpy(&section.offset, entry + kSectionNameSize, 4);
std::memcpy(&section.size, entry + kSectionNameSize + 4, 4);
sections_.push_back(std::move(section));