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
+153
-41
No files matched your search
+127
-38
@@ -358,6 +358,7 @@ struct Row {
|
||||
std::vector<Choice> choices;
|
||||
size_t chosen = 0;
|
||||
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]
|
||||
|
||||
@@ -415,6 +416,7 @@ void RebuildList() {
|
||||
const int heroFrom = character.meta.value("heroFrom", -1);
|
||||
const int heroFromCampaign = character.meta.value("heroFromCampaign", kCampaignAncaria);
|
||||
Row row{character.Path()};
|
||||
row.hasHero = hasHero;
|
||||
row.name = FromUtf8(character.meta.value("name", std::string()));
|
||||
row.level = character.meta.value("level", 0u);
|
||||
int highest = kAlwaysOpenDifficulty;
|
||||
@@ -618,14 +620,21 @@ constexpr int kSavegameButtonPitch = 64;
|
||||
void __fastcall Savegame_Action(uint8_t* self, void* edx, int 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 {
|
||||
uint8_t* window = nullptr; // open while set
|
||||
uint8_t* window = nullptr; // open while set
|
||||
DialogKind kind = DialogKind::Difficulty;
|
||||
size_t row = 0; // index into g_rows
|
||||
std::vector<uint8_t*> buttons; // made once, reused
|
||||
std::vector<uint32_t> captions; // their text ids, overridden
|
||||
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<uint32_t> captions; // their text ids, overridden
|
||||
} g_dialog;
|
||||
|
||||
// Rename: the name typed so far and a pending Enter / Esc, from the window
|
||||
@@ -634,15 +643,9 @@ std::mutex g_nameLock;
|
||||
std::wstring g_name;
|
||||
std::atomic<int> g_nameKey = 0;
|
||||
|
||||
// Entries: the row's choices, "Umbenennen" or the campaigns, then "Abbrechen".
|
||||
// The entries and "Abbrechen".
|
||||
size_t DialogEntries() {
|
||||
if (g_dialog.kind == DialogKind::NewCharacter) {
|
||||
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;
|
||||
return g_dialog.entries.size() + 1;
|
||||
}
|
||||
|
||||
int DialogFieldSpace() {
|
||||
@@ -670,16 +673,56 @@ game::UiRect DialogButtonRect(size_t index) {
|
||||
}
|
||||
|
||||
std::wstring DialogText(size_t index) {
|
||||
if (index + 1 == DialogEntries()) {
|
||||
return L"Abbrechen";
|
||||
return index < g_dialog.entries.size() ? g_dialog.entries[index] : L"Abbrechen";
|
||||
}
|
||||
|
||||
// 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::Rename) {
|
||||
return L"Umbenennen";
|
||||
for (const Choice& choice : row.choices) {
|
||||
if (!choice.newWorld && !choice.autosave) {
|
||||
worlds.push_back(choice);
|
||||
}
|
||||
}
|
||||
if (g_dialog.kind == DialogKind::NewCharacter) {
|
||||
return CampaignName(static_cast<int>(index));
|
||||
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.
|
||||
@@ -712,6 +755,7 @@ void OpenDialog(uint8_t* window, DialogKind kind, size_t row) {
|
||||
g_dialog.window = window;
|
||||
g_dialog.kind = kind;
|
||||
g_dialog.row = row;
|
||||
FillDialog(kind, row);
|
||||
for (size_t i = 0; i < DialogEntries(); ++i) {
|
||||
if (i == g_dialog.buttons.size()) {
|
||||
const uint32_t caption = game::RegisterText(L"");
|
||||
@@ -779,9 +823,10 @@ bool RenameCharacter(const Row& row, const std::wstring& name) {
|
||||
return done;
|
||||
}
|
||||
|
||||
// The row "Umbenennen" applies to: the selected character, unless it is the
|
||||
// one being played (its name is in the running game too).
|
||||
const Row* RenameTarget(uint8_t* window) {
|
||||
// The row "Umbenennen" and "Zurücksetzen" apply to: the selected character,
|
||||
// unless it is the one being played (its name and world are in the running
|
||||
// game too).
|
||||
const Row* EditableRow(uint8_t* window) {
|
||||
const uint32_t mode = *reinterpret_cast<uint32_t*>(window + kSavegameMode);
|
||||
if (mode != kSavegameModeLoad && mode != kSavegameModeSave) {
|
||||
return nullptr;
|
||||
@@ -802,20 +847,27 @@ bool NewCharacterAllowed(uint8_t* window) {
|
||||
!*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 {
|
||||
int slot; // places below delete
|
||||
const wchar_t* caption;
|
||||
const char* name;
|
||||
bool (*shown)(uint8_t* window);
|
||||
DialogKind opens;
|
||||
uint8_t* control = nullptr;
|
||||
};
|
||||
SideButton g_sideButtons[] = {
|
||||
{1, L"Umbenennen", "UI_BTN_CHAR_RENAME", [](uint8_t* window) { return RenameTarget(window) != nullptr; }},
|
||||
{2, L"Neuer Charakter", "UI_BTN_CHAR_NEW", &NewCharacterAllowed},
|
||||
{1, L"Umbenennen", "UI_BTN_CHAR_RENAME", [](uint8_t* window) { return EditableRow(window) != nullptr; },
|
||||
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.
|
||||
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, panel, kDialogBackColour);
|
||||
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"
|
||||
: g_dialog.kind == DialogKind::NewCharacter ? L"Kampagne wählen"
|
||||
: L"Welt wählen";
|
||||
game::DrawText(device, kDialogFont, game::CenterLineVertically(kDialogFont, title), heading, kDialogTitleColour,
|
||||
game::kTextCenter);
|
||||
game::DrawText(device, kDialogFont, game::CenterLineVertically(kDialogFont, title), g_dialog.title.c_str(),
|
||||
kDialogTitleColour, game::kTextCenter);
|
||||
if (g_dialog.kind == DialogKind::Rename) {
|
||||
const game::UiRect field = DialogField();
|
||||
FillBox(device, Grown(field, 1), kDialogEdgeColour);
|
||||
@@ -988,6 +1037,41 @@ void OpenCharacterCreation(int 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.
|
||||
void DialogChoose(uint8_t* window, size_t index) {
|
||||
if (index + 1 == DialogEntries()) {
|
||||
@@ -997,6 +1081,14 @@ void DialogChoose(uint8_t* window, size_t index) {
|
||||
} else if (g_dialog.kind == DialogKind::NewCharacter) {
|
||||
CloseDialog();
|
||||
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) {
|
||||
CloseDialog();
|
||||
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))) {
|
||||
continue;
|
||||
}
|
||||
if (&side == &g_renameButton) {
|
||||
OpenDialog(window, DialogKind::Rename, static_cast<size_t>(RenameTarget(window) - g_rows.data()));
|
||||
} else {
|
||||
OpenDialog(window, DialogKind::NewCharacter, 0);
|
||||
}
|
||||
const Row* row = EditableRow(window);
|
||||
OpenDialog(window, side.opens, row ? static_cast<size_t>(row - g_rows.data()) : 0);
|
||||
return true;
|
||||
}
|
||||
return false;
|
||||
|
||||
Reference in new issue
Block a user