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:
1 parent
5bccb0734e
commit
618288e310
7 files changed
+498
-109
No files matched your search
+35
-6
@@ -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(§ion.offset, entry + kSectionNameSize, 4);
|
||||
std::memcpy(§ion.size, entry + kSectionNameSize + 4, 4);
|
||||
sections_.push_back(std::move(section));
|
||||
|
||||
Reference in new issue
Block a user