Characters: reset a difficulty
"Zuruecksetzen" in the load window lists the selected character's worlds and asks for confirmation. The world goes (a copy in characters\deleted), with an autosave of it, and the shared hero's story completion of that difficulty is cleared. The dialog is one generic list (title + entries). Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
This commit is contained in:
1 parent
e0d8d0ce7e
commit
feebe72ab7
5 files changed
+149
-37
No files matched your search
+11
-1
@@ -222,6 +222,17 @@ keeping the gap before "Beenden".
|
|||||||
(`game::NewButtonLike`); their captions are our own text ids with
|
(`game::NewButtonLike`); their captions are our own text ids with
|
||||||
overrides (texts.cpp checks overrides for our ids too).
|
overrides (texts.cpp checks overrides for our ids too).
|
||||||
|
|
||||||
|
- "Zurücksetzen" (below Umbenennen; a selected character with the shared
|
||||||
|
hero and a world, not the one being played) lists the character's worlds,
|
||||||
|
then asks "<Welt> zurücksetzen?". A reset (decided 2026-10-20) removes the
|
||||||
|
world section (a copy goes to `charactersdeletedCharacterNN.sacredK.<section>.PAK`)
|
||||||
|
and an autosave of that world, and clears the shared hero's story
|
||||||
|
completion of that difficulty (hero header +0xed + d); the game counts it
|
||||||
|
per difficulty, so the next difficulty closes in both campaigns unless a
|
||||||
|
world keeps it open. The hero keeps everything else.
|
||||||
|
- The dialog is one generic list: each kind fills a title and entries
|
||||||
|
(FillDialog), "Abbrechen" last.
|
||||||
|
|
||||||
### Load
|
### Load
|
||||||
|
|
||||||
`cEngine::Load` (0x61b790) of a listed row: the chosen world and the hero are
|
`cEngine::Load` (0x61b790) of a listed row: the chosen world and the hero are
|
||||||
@@ -275,7 +286,6 @@ kept, the newest file overall is the shared hero.
|
|||||||
|
|
||||||
## Open
|
## Open
|
||||||
|
|
||||||
- Reset a difficulty.
|
|
||||||
- Silent autosave every 10 minutes.
|
- Silent autosave every 10 minutes.
|
||||||
- Quickload (row 0) does not know characters.
|
- Quickload (row 0) does not know characters.
|
||||||
- Multiplayer (see `todo.md`).
|
- Multiplayer (see `todo.md`).
|
||||||
@@ -184,8 +184,6 @@ From the characters rework (`docs/characters.md`, request of 2026-10-19):
|
|||||||
are one byte per difficulty in cStatsManager +0x5704 (read 0x424be0) and
|
are one byte per difficulty in cStatsManager +0x5704 (read 0x424be0) and
|
||||||
travel in the save header (+0x88..+0x98) and the hero file. Done
|
travel in the save header (+0x88..+0x98) and the hero file. Done
|
||||||
2026-10-20 (docs/characters.md).
|
2026-10-20 (docs/characters.md).
|
||||||
- **Reset a difficulty.** Remove one world from a character (the shared hero
|
|
||||||
stays); a button / dialog entry in the load window.
|
|
||||||
- **Silent autosave every 10 minutes** (a setting later) into the
|
- **Silent autosave every 10 minutes** (a setting later) into the
|
||||||
autosave file, without the busy dialog, like the direct save
|
autosave file, without the busy dialog, like the direct save
|
||||||
(`SaveOnTick`) but marked as an autosave.
|
(`SaveOnTick`) but marked as an autosave.
|
||||||
|
|||||||
+123
-34
@@ -358,6 +358,7 @@ struct Row {
|
|||||||
std::vector<Choice> choices;
|
std::vector<Choice> choices;
|
||||||
size_t chosen = 0;
|
size_t chosen = 0;
|
||||||
uint32_t level = 0; // the shared hero's
|
uint32_t level = 0; // the shared hero's
|
||||||
|
bool hasHero = false; // the shared hero is in the file (migrated ones get it at their first load)
|
||||||
};
|
};
|
||||||
std::vector<Row> g_rows; // row nn of the game = g_rows[nn - 1]
|
std::vector<Row> g_rows; // row nn of the game = g_rows[nn - 1]
|
||||||
|
|
||||||
@@ -415,6 +416,7 @@ void RebuildList() {
|
|||||||
const int heroFrom = character.meta.value("heroFrom", -1);
|
const int heroFrom = character.meta.value("heroFrom", -1);
|
||||||
const int heroFromCampaign = character.meta.value("heroFromCampaign", kCampaignAncaria);
|
const int heroFromCampaign = character.meta.value("heroFromCampaign", kCampaignAncaria);
|
||||||
Row row{character.Path()};
|
Row row{character.Path()};
|
||||||
|
row.hasHero = hasHero;
|
||||||
row.name = FromUtf8(character.meta.value("name", std::string()));
|
row.name = FromUtf8(character.meta.value("name", std::string()));
|
||||||
row.level = character.meta.value("level", 0u);
|
row.level = character.meta.value("level", 0u);
|
||||||
int highest = kAlwaysOpenDifficulty;
|
int highest = kAlwaysOpenDifficulty;
|
||||||
@@ -618,12 +620,19 @@ constexpr int kSavegameButtonPitch = 64;
|
|||||||
void __fastcall Savegame_Action(uint8_t* self, void* edx, int action);
|
void __fastcall Savegame_Action(uint8_t* self, void* edx, int action);
|
||||||
RE_HOOK(0x0071c520, Savegame_Action);
|
RE_HOOK(0x0071c520, Savegame_Action);
|
||||||
|
|
||||||
enum class DialogKind { Difficulty, Rename, NewCharacter };
|
// Difficulty: the row's choices; Rename: "Umbenennen" under the name field;
|
||||||
|
// NewCharacter: the campaigns; Reset: the row's worlds; ConfirmReset:
|
||||||
|
// "Zurücksetzen" for one of them.
|
||||||
|
enum class DialogKind { Difficulty, Rename, NewCharacter, Reset, ConfirmReset };
|
||||||
|
|
||||||
struct Dialog {
|
struct Dialog {
|
||||||
uint8_t* window = nullptr; // open while set
|
uint8_t* window = nullptr; // open while set
|
||||||
DialogKind kind = DialogKind::Difficulty;
|
DialogKind kind = DialogKind::Difficulty;
|
||||||
size_t row = 0; // index into g_rows
|
size_t row = 0; // index into g_rows
|
||||||
|
std::wstring title;
|
||||||
|
std::vector<std::wstring> entries; // then "Abbrechen"
|
||||||
|
std::vector<Choice> worlds; // Reset: the entries' worlds
|
||||||
|
Choice target; // ConfirmReset: the world
|
||||||
std::vector<uint8_t*> buttons; // made once, reused
|
std::vector<uint8_t*> buttons; // made once, reused
|
||||||
std::vector<uint32_t> captions; // their text ids, overridden
|
std::vector<uint32_t> captions; // their text ids, overridden
|
||||||
} g_dialog;
|
} g_dialog;
|
||||||
@@ -634,15 +643,9 @@ std::mutex g_nameLock;
|
|||||||
std::wstring g_name;
|
std::wstring g_name;
|
||||||
std::atomic<int> g_nameKey = 0;
|
std::atomic<int> g_nameKey = 0;
|
||||||
|
|
||||||
// Entries: the row's choices, "Umbenennen" or the campaigns, then "Abbrechen".
|
// The entries and "Abbrechen".
|
||||||
size_t DialogEntries() {
|
size_t DialogEntries() {
|
||||||
if (g_dialog.kind == DialogKind::NewCharacter) {
|
return g_dialog.entries.size() + 1;
|
||||||
return kCampaignCount + 1;
|
|
||||||
}
|
|
||||||
if (g_dialog.row >= g_rows.size()) {
|
|
||||||
return 0;
|
|
||||||
}
|
|
||||||
return g_dialog.kind == DialogKind::Rename ? 2 : g_rows[g_dialog.row].choices.size() + 1;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
int DialogFieldSpace() {
|
int DialogFieldSpace() {
|
||||||
@@ -670,16 +673,56 @@ game::UiRect DialogButtonRect(size_t index) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
std::wstring DialogText(size_t index) {
|
std::wstring DialogText(size_t index) {
|
||||||
if (index + 1 == DialogEntries()) {
|
return index < g_dialog.entries.size() ? g_dialog.entries[index] : L"Abbrechen";
|
||||||
return L"Abbrechen";
|
|
||||||
}
|
}
|
||||||
if (g_dialog.kind == DialogKind::Rename) {
|
|
||||||
return L"Umbenennen";
|
// The worlds a reset can delete: every saved world of the row (not new ones,
|
||||||
|
// not autosaves). Only with the shared hero: before it exists, a world holds it.
|
||||||
|
std::vector<Choice> ResettableWorlds(const Row& row) {
|
||||||
|
std::vector<Choice> worlds;
|
||||||
|
if (!row.hasHero) {
|
||||||
|
return worlds;
|
||||||
}
|
}
|
||||||
if (g_dialog.kind == DialogKind::NewCharacter) {
|
for (const Choice& choice : row.choices) {
|
||||||
return CampaignName(static_cast<int>(index));
|
if (!choice.newWorld && !choice.autosave) {
|
||||||
|
worlds.push_back(choice);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return worlds;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Title and entries of a dialog `kind` for row `row` (and g_dialog.target).
|
||||||
|
void FillDialog(DialogKind kind, size_t row) {
|
||||||
|
g_dialog.entries.clear();
|
||||||
|
switch (kind) {
|
||||||
|
case DialogKind::Difficulty:
|
||||||
|
g_dialog.title = L"Welt wählen";
|
||||||
|
for (const Choice& choice : g_rows[row].choices) {
|
||||||
|
g_dialog.entries.push_back(ChoiceName(choice));
|
||||||
|
}
|
||||||
|
break;
|
||||||
|
case DialogKind::Rename:
|
||||||
|
g_dialog.title = L"Neuer Name";
|
||||||
|
g_dialog.entries = {L"Umbenennen"};
|
||||||
|
break;
|
||||||
|
case DialogKind::NewCharacter:
|
||||||
|
g_dialog.title = L"Kampagne wählen";
|
||||||
|
for (int c = 0; c < kCampaignCount; ++c) {
|
||||||
|
g_dialog.entries.push_back(CampaignName(c));
|
||||||
|
}
|
||||||
|
break;
|
||||||
|
case DialogKind::Reset:
|
||||||
|
g_dialog.title = L"Welt zurücksetzen";
|
||||||
|
g_dialog.worlds = ResettableWorlds(g_rows[row]);
|
||||||
|
for (const Choice& choice : g_dialog.worlds) {
|
||||||
|
g_dialog.entries.push_back(ChoiceName(choice));
|
||||||
|
}
|
||||||
|
break;
|
||||||
|
case DialogKind::ConfirmReset:
|
||||||
|
g_dialog.title = ChoiceName(g_dialog.target) + L" zurücksetzen?";
|
||||||
|
g_dialog.entries = {L"Zurücksetzen"};
|
||||||
|
break;
|
||||||
}
|
}
|
||||||
return ChoiceName(g_rows[g_dialog.row].choices[index]);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// The character typed with key `vk` under the current modifiers, 0 if none.
|
// The character typed with key `vk` under the current modifiers, 0 if none.
|
||||||
@@ -712,6 +755,7 @@ void OpenDialog(uint8_t* window, DialogKind kind, size_t row) {
|
|||||||
g_dialog.window = window;
|
g_dialog.window = window;
|
||||||
g_dialog.kind = kind;
|
g_dialog.kind = kind;
|
||||||
g_dialog.row = row;
|
g_dialog.row = row;
|
||||||
|
FillDialog(kind, row);
|
||||||
for (size_t i = 0; i < DialogEntries(); ++i) {
|
for (size_t i = 0; i < DialogEntries(); ++i) {
|
||||||
if (i == g_dialog.buttons.size()) {
|
if (i == g_dialog.buttons.size()) {
|
||||||
const uint32_t caption = game::RegisterText(L"");
|
const uint32_t caption = game::RegisterText(L"");
|
||||||
@@ -779,9 +823,10 @@ bool RenameCharacter(const Row& row, const std::wstring& name) {
|
|||||||
return done;
|
return done;
|
||||||
}
|
}
|
||||||
|
|
||||||
// The row "Umbenennen" applies to: the selected character, unless it is the
|
// The row "Umbenennen" and "Zurücksetzen" apply to: the selected character,
|
||||||
// one being played (its name is in the running game too).
|
// unless it is the one being played (its name and world are in the running
|
||||||
const Row* RenameTarget(uint8_t* window) {
|
// game too).
|
||||||
|
const Row* EditableRow(uint8_t* window) {
|
||||||
const uint32_t mode = *reinterpret_cast<uint32_t*>(window + kSavegameMode);
|
const uint32_t mode = *reinterpret_cast<uint32_t*>(window + kSavegameMode);
|
||||||
if (mode != kSavegameModeLoad && mode != kSavegameModeSave) {
|
if (mode != kSavegameModeLoad && mode != kSavegameModeSave) {
|
||||||
return nullptr;
|
return nullptr;
|
||||||
@@ -802,20 +847,27 @@ bool NewCharacterAllowed(uint8_t* window) {
|
|||||||
!*reinterpret_cast<const uint8_t*>(kMultiplayer);
|
!*reinterpret_cast<const uint8_t*>(kMultiplayer);
|
||||||
}
|
}
|
||||||
|
|
||||||
// Buttons of ours below the window's delete button, made on first use.
|
bool CanReset(uint8_t* window) {
|
||||||
|
const Row* row = EditableRow(window);
|
||||||
|
return row && !ResettableWorlds(*row).empty();
|
||||||
|
}
|
||||||
|
|
||||||
|
// Buttons of ours below the window's delete button, made on first use; each
|
||||||
|
// opens a dialog (for the selected character, if it needs one).
|
||||||
struct SideButton {
|
struct SideButton {
|
||||||
int slot; // places below delete
|
int slot; // places below delete
|
||||||
const wchar_t* caption;
|
const wchar_t* caption;
|
||||||
const char* name;
|
const char* name;
|
||||||
bool (*shown)(uint8_t* window);
|
bool (*shown)(uint8_t* window);
|
||||||
|
DialogKind opens;
|
||||||
uint8_t* control = nullptr;
|
uint8_t* control = nullptr;
|
||||||
};
|
};
|
||||||
SideButton g_sideButtons[] = {
|
SideButton g_sideButtons[] = {
|
||||||
{1, L"Umbenennen", "UI_BTN_CHAR_RENAME", [](uint8_t* window) { return RenameTarget(window) != nullptr; }},
|
{1, L"Umbenennen", "UI_BTN_CHAR_RENAME", [](uint8_t* window) { return EditableRow(window) != nullptr; },
|
||||||
{2, L"Neuer Charakter", "UI_BTN_CHAR_NEW", &NewCharacterAllowed},
|
DialogKind::Rename},
|
||||||
|
{2, L"Zurücksetzen", "UI_BTN_CHAR_RESET", &CanReset, DialogKind::Reset},
|
||||||
|
{3, L"Neuer Charakter", "UI_BTN_CHAR_NEW", &NewCharacterAllowed, DialogKind::NewCharacter},
|
||||||
};
|
};
|
||||||
SideButton& g_renameButton = g_sideButtons[0];
|
|
||||||
SideButton& g_newCharacterButton = g_sideButtons[1];
|
|
||||||
|
|
||||||
// "Speichern" is not drawn (kNoImport); Löschen moves up into its place.
|
// "Speichern" is not drawn (kNoImport); Löschen moves up into its place.
|
||||||
constexpr size_t kSavegameSaveButton = 0x820; // cUI_Button2 inside the window (action 1)
|
constexpr size_t kSavegameSaveButton = 0x820; // cUI_Button2 inside the window (action 1)
|
||||||
@@ -879,11 +931,8 @@ void RenderDialog(void* device) {
|
|||||||
FillBox(device, Grown(panel, 2), kDialogEdgeColour);
|
FillBox(device, Grown(panel, 2), kDialogEdgeColour);
|
||||||
FillBox(device, panel, kDialogBackColour);
|
FillBox(device, panel, kDialogBackColour);
|
||||||
const game::UiRect title = {panel.x, panel.y, panel.w, static_cast<int16_t>(kDialogTitleHeight)};
|
const game::UiRect title = {panel.x, panel.y, panel.w, static_cast<int16_t>(kDialogTitleHeight)};
|
||||||
const wchar_t* heading = g_dialog.kind == DialogKind::Rename ? L"Neuer Name"
|
game::DrawText(device, kDialogFont, game::CenterLineVertically(kDialogFont, title), g_dialog.title.c_str(),
|
||||||
: g_dialog.kind == DialogKind::NewCharacter ? L"Kampagne wählen"
|
kDialogTitleColour, game::kTextCenter);
|
||||||
: L"Welt wählen";
|
|
||||||
game::DrawText(device, kDialogFont, game::CenterLineVertically(kDialogFont, title), heading, kDialogTitleColour,
|
|
||||||
game::kTextCenter);
|
|
||||||
if (g_dialog.kind == DialogKind::Rename) {
|
if (g_dialog.kind == DialogKind::Rename) {
|
||||||
const game::UiRect field = DialogField();
|
const game::UiRect field = DialogField();
|
||||||
FillBox(device, Grown(field, 1), kDialogEdgeColour);
|
FillBox(device, Grown(field, 1), kDialogEdgeColour);
|
||||||
@@ -988,6 +1037,41 @@ void OpenCharacterCreation(int campaign) {
|
|||||||
re::Log("characters: new character, campaign %d", campaign);
|
re::Log("characters: new character, campaign %d", campaign);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Reset (request of 2026-10-20): the world goes (kept in characters\deleted),
|
||||||
|
// with an autosave of it, and the shared hero's story completion for its
|
||||||
|
// difficulty is cleared (decided: cleared too; the game counts it per
|
||||||
|
// difficulty, so for both campaigns). The hero keeps everything else.
|
||||||
|
bool ResetWorld(const Row& row, const Choice& world) {
|
||||||
|
CharacterFile character(row.file);
|
||||||
|
const std::string section = WorldSection(world.campaign, world.difficulty);
|
||||||
|
std::optional<Bytes> removed = character.Open() ? character.Read(section) : std::nullopt;
|
||||||
|
std::optional<Bytes> hero = removed ? character.Read("hero") : std::nullopt;
|
||||||
|
if (!hero || !SetStoryCompleted(*hero, world.difficulty, 0)) {
|
||||||
|
re::Log("characters: reset of %s in %s failed (no world or no shared hero)", section.c_str(), row.file.c_str());
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
const std::string deleted = CharactersDir() + "deleted\\";
|
||||||
|
CreateDirectoryA(deleted.c_str(), nullptr);
|
||||||
|
const std::string name = row.file.substr(row.file.find_last_of('\\') + 1);
|
||||||
|
WriteFileReplacing(deleted + name + "." + section + ".PAK", *removed);
|
||||||
|
character.Remove(section);
|
||||||
|
character.Set("hero", std::move(*hero));
|
||||||
|
if (character.meta.value("lastCampaign", kCampaignAncaria) == world.campaign &&
|
||||||
|
character.meta.value("lastDifficulty", 0) == world.difficulty) {
|
||||||
|
character.meta.erase("lastCampaign");
|
||||||
|
character.meta.erase("lastDifficulty");
|
||||||
|
}
|
||||||
|
const bool saved = character.Save();
|
||||||
|
CharacterFile autosave(AutosavePath(row.file));
|
||||||
|
if (autosave.Open() && autosave.meta.value("campaign", kCampaignAncaria) == world.campaign &&
|
||||||
|
autosave.meta.value("difficulty", -1) == world.difficulty) {
|
||||||
|
DeleteFileA(autosave.Path().c_str());
|
||||||
|
}
|
||||||
|
re::Log("characters: reset %s of %s (backup in %s)%s", section.c_str(), row.file.c_str(), deleted.c_str(),
|
||||||
|
saved ? "" : " - WRITING FAILED");
|
||||||
|
return saved;
|
||||||
|
}
|
||||||
|
|
||||||
// Button `index` of the dialog chosen.
|
// Button `index` of the dialog chosen.
|
||||||
void DialogChoose(uint8_t* window, size_t index) {
|
void DialogChoose(uint8_t* window, size_t index) {
|
||||||
if (index + 1 == DialogEntries()) {
|
if (index + 1 == DialogEntries()) {
|
||||||
@@ -997,6 +1081,14 @@ void DialogChoose(uint8_t* window, size_t index) {
|
|||||||
} else if (g_dialog.kind == DialogKind::NewCharacter) {
|
} else if (g_dialog.kind == DialogKind::NewCharacter) {
|
||||||
CloseDialog();
|
CloseDialog();
|
||||||
OpenCharacterCreation(static_cast<int>(index));
|
OpenCharacterCreation(static_cast<int>(index));
|
||||||
|
} else if (g_dialog.kind == DialogKind::Reset) {
|
||||||
|
g_dialog.target = g_dialog.worlds[index];
|
||||||
|
OpenDialog(window, DialogKind::ConfirmReset, g_dialog.row);
|
||||||
|
} else if (g_dialog.kind == DialogKind::ConfirmReset) {
|
||||||
|
const Row row = g_rows[g_dialog.row];
|
||||||
|
CloseDialog();
|
||||||
|
ResetWorld(row, g_dialog.target);
|
||||||
|
Savegame_Action_original(window, nullptr, kSavegameActionRescan);
|
||||||
} else if (const Row row = g_rows[g_dialog.row]; row.choices[index].newWorld) {
|
} else if (const Row row = g_rows[g_dialog.row]; row.choices[index].newWorld) {
|
||||||
CloseDialog();
|
CloseDialog();
|
||||||
StartNewWorld(window, row, row.choices[index].campaign, row.choices[index].difficulty);
|
StartNewWorld(window, row, row.choices[index].campaign, row.choices[index].difficulty);
|
||||||
@@ -1049,11 +1141,8 @@ bool HandleSideButtons(uint8_t* window, void* event) {
|
|||||||
game::EventField(event, game::kMouseButtonY))) {
|
game::EventField(event, game::kMouseButtonY))) {
|
||||||
continue;
|
continue;
|
||||||
}
|
}
|
||||||
if (&side == &g_renameButton) {
|
const Row* row = EditableRow(window);
|
||||||
OpenDialog(window, DialogKind::Rename, static_cast<size_t>(RenameTarget(window) - g_rows.data()));
|
OpenDialog(window, side.opens, row ? static_cast<size_t>(row - g_rows.data()) : 0);
|
||||||
} else {
|
|
||||||
OpenDialog(window, DialogKind::NewCharacter, 0);
|
|
||||||
}
|
|
||||||
return true;
|
return true;
|
||||||
}
|
}
|
||||||
return false;
|
return false;
|
||||||
|
|||||||
@@ -123,6 +123,16 @@ bool SetHeroName(Bytes& file, const std::wstring& name) {
|
|||||||
return true;
|
return true;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
bool SetStoryCompleted(Bytes& file, int difficulty, uint8_t count) {
|
||||||
|
const std::optional<Chunk> hero = FindChunk(file, kChunkHero);
|
||||||
|
if (!hero || difficulty < 0 || difficulty >= kDifficultyCount ||
|
||||||
|
hero->size < kHeroStoryCompleted + kDifficultyCount) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
file[hero->offset + kHeroStoryCompleted + difficulty] = count;
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
uint32_t CampaignSaveMode(int campaign) {
|
uint32_t CampaignSaveMode(int campaign) {
|
||||||
return campaign == kCampaignUnderworld ? kSaveModeUnderworld : kSaveModeAncaria;
|
return campaign == kCampaignUnderworld ? kSaveModeUnderworld : kSaveModeAncaria;
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -46,6 +46,11 @@ std::optional<SaveInfo> ReadSaveInfo(const Bytes& file);
|
|||||||
constexpr size_t kHeroNameMaxChars = 63;
|
constexpr size_t kHeroNameMaxChars = 63;
|
||||||
bool SetHeroName(Bytes& file, const std::wstring& name);
|
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
|
// The save header's campaign field (+0x24): 2 Ancaria, 3 Underworld (written
|
||||||
// at 0x61a5c5, read back by cEngine::Load 0x61bafd).
|
// at 0x61a5c5, read back by cEngine::Load 0x61bafd).
|
||||||
uint32_t CampaignSaveMode(int campaign);
|
uint32_t CampaignSaveMode(int campaign);
|
||||||
|
|||||||
Reference in new issue
Block a user