Quest book as one list: story, then side quests by region with ??? rows
The game's list builder runs on a reordered copy of the book: the story, then one section per region (id blocks of 100 grouped by the region their quests lie in, named as the megamap names regions), unknown quests as "???", the rest joined to the nearest area, undiscovered counts. Act tabs hidden, page turning kept. "Erledigte anzeigen" row toggles completed quests ([questbook] ShowCompleted). Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
This commit is contained in:
1 parent
34cacefcfc
commit
1d16fc65c4
7 files changed
+675
-14
No files matched your search
@@ -0,0 +1,499 @@
|
||||
// The quest book as one list: the story, then the side quests by area,
|
||||
// unknown ones as "???" (requests of 2026-10-25: a player sees how many
|
||||
// quests an area holds without being told where they are; then "get rid of
|
||||
// the tabs ... a section for the story and a section per region", the
|
||||
// game's page turning kept: "pagination is already there anyways").
|
||||
//
|
||||
// Measured (docs/quests.md, "Quest book"): the book's quest mode had up to
|
||||
// seven act tabs (bookmarks I, II, ...), an entry's tab being its chapter
|
||||
// (+0x16c), copied from the current main quest when the quest is accepted
|
||||
// (0x46c450); the game knows no area for a quest. The side quests' ids come
|
||||
// in blocks of 100 (1xx .. 25xx) whose accepted quests lie together on the
|
||||
// map: the areas. The other side quests (5xxx, 6xxx, 9xxx, 102xx, 15xxx)
|
||||
// carry no area in their id; once known they join the area nearest to them.
|
||||
//
|
||||
// The book (cUI_Book, one per book mode): the left page shows the current
|
||||
// page's rows, the right page the selected row's detail record, both drawn by
|
||||
// 0x6b27f0 from the same page; a click (0x6b3620) selects the row under the
|
||||
// mouse (page +4).
|
||||
//
|
||||
// The mod: the game's list builder (0x6b04c0) runs on a reordered copy of the
|
||||
// book with our rows (headings, "???") as extra entries, all in the first tab
|
||||
// (the game's page breaks and page turning stay); afterwards each row's entry
|
||||
// index is mapped back to the real book, ours to none (a click on them does
|
||||
// nothing, 0x6b3620 checks the index). The bookmarks are hidden, every page is
|
||||
// titled "Quests" and selects its first quest. Fonts, icons and the detail
|
||||
// page stay the game's.
|
||||
//
|
||||
// "Erledigte anzeigen" (request of 2026-10-25: "some kind of 'Show completed'
|
||||
// checkmark. If unchecked, completed quests are not in the list (as well as
|
||||
// completed regions)"): the first row, its icon the solved mark when on, none
|
||||
// when off; a click toggles it (sacredK.ini [questbook] ShowCompleted) and
|
||||
// rebuilds the book. Off leaves out solved quests and the areas whose quests
|
||||
// are all known and solved; failed quests stay (they can be restarted).
|
||||
|
||||
#include <algorithm>
|
||||
#include <array>
|
||||
#include <cstdint>
|
||||
#include <cstdio>
|
||||
#include <cstring>
|
||||
#include <deque>
|
||||
#include <map>
|
||||
#include <string>
|
||||
#include <vector>
|
||||
|
||||
#include "game/engine/quests.h"
|
||||
#include "game/engine/regions.h"
|
||||
#include "game/ui/control.h"
|
||||
#include "game/ui/texts.h"
|
||||
#include "re/config.h"
|
||||
#include "re/hook.h"
|
||||
#include "re/log.h"
|
||||
|
||||
namespace mods::questbook {
|
||||
namespace {
|
||||
|
||||
constexpr int kAreaQuestFirst = 100; // ids 100 .. 2599: area = id / 100
|
||||
constexpr int kAreaQuestEnd = 2600; // 2600 .. 3599: generic quests, cloned by Quest_End
|
||||
constexpr int kGenericQuestEnd = 3600;
|
||||
constexpr int kAreaSize = 100;
|
||||
|
||||
// cUI_Book: the book mode (+0x166, 0 = quests), current tab (+0x15e) and page
|
||||
// (+0x162); per tab a vector of pages (+0x97c + tab * 0xc) and of detail
|
||||
// records (+0x9d0 + tab * 0xc). A page (0x1c bytes): title text +0, selected row +4 (u16), a vector
|
||||
// of rows +0x10. A row (0x14 bytes): u16 flags (4 = spacer), title text +4,
|
||||
// detail record +8, book entry +0xc. The bookmarks: 7 controls at +0x168.
|
||||
constexpr size_t kBookMode = 0x166;
|
||||
constexpr uint16_t kModeQuests = 0;
|
||||
constexpr size_t kBookTab = 0x15e;
|
||||
constexpr size_t kBookPage = 0x162;
|
||||
constexpr size_t kBookPages = 0x97c;
|
||||
constexpr size_t kTabStride = 0xc;
|
||||
constexpr size_t kPageSize = 0x1c;
|
||||
constexpr size_t kPageTitle = 0x0;
|
||||
constexpr size_t kPageSelected = 0x4;
|
||||
constexpr size_t kPageRows = 0x10;
|
||||
constexpr size_t kRowSize = 0x14;
|
||||
constexpr size_t kRowEntry = 0xc;
|
||||
constexpr uint16_t kRowSpacer = 0x4;
|
||||
constexpr size_t kBookmarks = 0x168;
|
||||
constexpr size_t kBookmarkStride = 0xbc;
|
||||
constexpr int kTabs = 7;
|
||||
constexpr uint32_t kNoEntry = 0xffffffff;
|
||||
constexpr uint32_t kToggleEntry = 0xfffffffe; // the "Erledigte anzeigen" row
|
||||
constexpr uint16_t kNoRow = 0xffff;
|
||||
|
||||
constexpr const char* kConfigSection = "questbook";
|
||||
constexpr const char* kConfigShowCompleted = "ShowCompleted";
|
||||
|
||||
// The setting, read from the ini on first use.
|
||||
bool& ShowCompleted() {
|
||||
static bool show = re::ConfigInt(kConfigSection, kConfigShowCompleted, 1) != 0;
|
||||
return show;
|
||||
}
|
||||
|
||||
using Entry = std::array<uint8_t, game::kBookEntrySize>;
|
||||
|
||||
template <typename T>
|
||||
T& Field(uint8_t* p, size_t offset) {
|
||||
return *reinterpret_cast<T*>(p + offset);
|
||||
}
|
||||
|
||||
// Our texts, registered once each (RegisterText keeps the pointer).
|
||||
uint32_t TextId(const std::wstring& text) {
|
||||
static std::map<std::wstring, uint32_t> ids;
|
||||
static std::deque<std::wstring> storage;
|
||||
auto found = ids.find(text);
|
||||
if (found != ids.end()) {
|
||||
return found->second;
|
||||
}
|
||||
storage.push_back(text);
|
||||
return ids[text] = game::RegisterText(storage.back().c_str());
|
||||
}
|
||||
|
||||
std::wstring Format(const wchar_t* format, int a, int b = 0, int c = 0) {
|
||||
wchar_t text[128];
|
||||
swprintf(text, 128, format, a, b, c);
|
||||
return text;
|
||||
}
|
||||
|
||||
// --- the list ------------------------------------------------------------------
|
||||
|
||||
// The quest table's side quests that count, by id: their area (0 = none by
|
||||
// id). Not main quests, and not the ones whose name occurs more than once
|
||||
// (generic quests and their clones, script bookkeeping such as "Region 1
|
||||
// befreit").
|
||||
std::map<int32_t, int> CountedQuests() {
|
||||
std::map<int32_t, int> result;
|
||||
std::map<std::string, int> names;
|
||||
for (size_t i = 0, n = game::QuestCount(); i < n; ++i) {
|
||||
const char* name = game::QuestName(game::Quest(static_cast<int>(i)));
|
||||
if (*name) {
|
||||
++names[name];
|
||||
}
|
||||
}
|
||||
for (size_t i = 0, n = game::QuestCount(); i < n; ++i) {
|
||||
uint8_t* quest = game::Quest(static_cast<int>(i));
|
||||
const int32_t id = game::QuestId(quest);
|
||||
const char* name = game::QuestName(quest);
|
||||
if (id <= game::kMainQuestLast || (*name && names[name] > 1) || (id >= kAreaQuestEnd && id < kGenericQuestEnd)) {
|
||||
continue;
|
||||
}
|
||||
result[id] = id >= kAreaQuestFirst && id < kAreaQuestEnd ? id / kAreaSize : 0;
|
||||
}
|
||||
return result;
|
||||
}
|
||||
|
||||
struct Area {
|
||||
int total = 0;
|
||||
std::vector<int32_t> known; // quest ids, in book order
|
||||
double x = 0, y = 0;
|
||||
int positions = 0;
|
||||
std::map<uint8_t, int> regions; // region number at its quests: count
|
||||
};
|
||||
|
||||
// The area's name: the game's name of the region most of its quests lie in;
|
||||
// else "Gebiet N".
|
||||
std::wstring AreaName(int number, const Area& area) {
|
||||
uint8_t region = 0;
|
||||
int most = 0;
|
||||
for (const auto& [r, n] : area.regions) {
|
||||
if (n > most) {
|
||||
region = r;
|
||||
most = n;
|
||||
}
|
||||
}
|
||||
const char* key = game::RegionNameKey(region);
|
||||
std::wstring name = key ? game::GameTextByKey(key) : L"";
|
||||
return name.empty() ? Format(L"Gebiet %d", number) : name;
|
||||
}
|
||||
|
||||
// The reordered book: entries (copies of the real ones, or ours), all in the
|
||||
// first tab, and for each the real entry index, kNoEntry for ours.
|
||||
struct Order {
|
||||
std::vector<Entry> entries;
|
||||
std::vector<uint32_t> real;
|
||||
|
||||
void Add(const Entry& entry, uint32_t realIndex) {
|
||||
entries.push_back(entry);
|
||||
Field<uint16_t>(entries.back().data(), game::kBookChapter) = 0;
|
||||
real.push_back(realIndex);
|
||||
}
|
||||
void AddReal(uint32_t index) {
|
||||
Entry entry;
|
||||
std::memcpy(entry.data(), game::BookEntry(index), entry.size());
|
||||
Add(entry, index);
|
||||
}
|
||||
void AddOwn(int32_t kind, const std::wstring& title, int32_t state = 0, uint32_t marker = kNoEntry) {
|
||||
Entry entry = {};
|
||||
Field<int32_t>(entry.data(), game::kBookKind) = kind;
|
||||
Field<int32_t>(entry.data(), game::kBookState) = state;
|
||||
Field<uint32_t>(entry.data(), game::kBookTitle) = TextId(title);
|
||||
Field<uint32_t>(entry.data(), game::kBookText) = TextId(L"");
|
||||
Add(entry, marker);
|
||||
}
|
||||
// A heading: a main-quest-style row; the game puts a spacer before a main
|
||||
// quest row only after a side quest row, so after a main quest an empty
|
||||
// side row goes first.
|
||||
void AddHeading(const std::wstring& title) {
|
||||
if (!entries.empty() && Field<int32_t>(entries.back().data(), game::kBookKind) == game::kBookMain) {
|
||||
AddOwn(game::kBookSide, L" ");
|
||||
}
|
||||
AddOwn(game::kBookMain, title);
|
||||
}
|
||||
};
|
||||
|
||||
Order BuildOrder() {
|
||||
const std::map<int32_t, int> counted = CountedQuests();
|
||||
const size_t count = game::BookCount();
|
||||
std::map<int, Area> areas;
|
||||
for (const auto& [id, area] : counted) {
|
||||
if (area) {
|
||||
++areas[area].total;
|
||||
}
|
||||
}
|
||||
|
||||
// Known side quests, in book order, with their area.
|
||||
std::map<int32_t, int> questArea;
|
||||
std::vector<int32_t> known;
|
||||
for (size_t i = 0; i < count; ++i) {
|
||||
uint8_t* entry = game::BookEntry(i);
|
||||
const int32_t id = game::BookQuestId(entry);
|
||||
if (game::BookKind(entry) == game::kBookMain || questArea.count(id)) {
|
||||
continue;
|
||||
}
|
||||
const auto found = counted.find(id);
|
||||
const int area = found != counted.end() ? found->second : 0;
|
||||
questArea[id] = area;
|
||||
known.push_back(id);
|
||||
if (area) {
|
||||
Area& a = areas[area];
|
||||
a.known.push_back(id);
|
||||
const int32_t x = Field<int32_t>(entry, game::kBookX);
|
||||
const int32_t y = Field<int32_t>(entry, game::kBookY);
|
||||
if (x || y) {
|
||||
a.x += x;
|
||||
a.y += y;
|
||||
++a.positions;
|
||||
++a.regions[game::RegionAtCell(x, y)];
|
||||
}
|
||||
}
|
||||
}
|
||||
// The other known side quests join the area nearest to them (by its known
|
||||
// quests' mean position).
|
||||
int unknownOthers = 0;
|
||||
for (const auto& [id, area] : counted) {
|
||||
unknownOthers += !area && !questArea.count(id);
|
||||
}
|
||||
for (int32_t id : known) {
|
||||
if (questArea[id] || !counted.count(id)) {
|
||||
continue;
|
||||
}
|
||||
for (size_t i = 0; i < count; ++i) {
|
||||
uint8_t* entry = game::BookEntry(i);
|
||||
if (game::BookQuestId(entry) != id) {
|
||||
continue;
|
||||
}
|
||||
const double x = Field<int32_t>(entry, game::kBookX);
|
||||
const double y = Field<int32_t>(entry, game::kBookY);
|
||||
double bestDistance = 0;
|
||||
int best = 0;
|
||||
for (const auto& [number, a] : areas) {
|
||||
if (!a.positions || (x == 0 && y == 0)) {
|
||||
continue;
|
||||
}
|
||||
const double dx = x - a.x / a.positions;
|
||||
const double dy = y - a.y / a.positions;
|
||||
const double distance = dx * dx + dy * dy;
|
||||
if (!best || distance < bestDistance) {
|
||||
best = number;
|
||||
bestDistance = distance;
|
||||
}
|
||||
}
|
||||
if (best) {
|
||||
questArea[id] = best;
|
||||
++areas[best].total;
|
||||
areas[best].known.push_back(id);
|
||||
}
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
const bool showCompleted = ShowCompleted();
|
||||
auto shown = [&](uint8_t* entry) { return showCompleted || game::BookState(entry) != game::kBookSolved; };
|
||||
auto hidden = [&](int32_t id) { // no entry of quest `id` in the list
|
||||
for (size_t i = 0; i < count; ++i) {
|
||||
uint8_t* entry = game::BookEntry(i);
|
||||
if (game::BookQuestId(entry) == id && game::BookKind(entry) != game::kBookMain && shown(entry)) {
|
||||
return false;
|
||||
}
|
||||
}
|
||||
return true;
|
||||
};
|
||||
|
||||
// The solved icon when on (the game draws state 100 so), no icon when off.
|
||||
Order order;
|
||||
order.AddOwn(game::kBookSide, L"Erledigte anzeigen", showCompleted ? game::kBookSolved : 0, kToggleEntry);
|
||||
auto addQuest = [&](int32_t id) {
|
||||
for (size_t i = 0; i < count; ++i) {
|
||||
uint8_t* entry = game::BookEntry(i);
|
||||
if (game::BookQuestId(entry) == id && game::BookKind(entry) != game::kBookMain && shown(entry)) {
|
||||
order.AddReal(static_cast<uint32_t>(i));
|
||||
}
|
||||
}
|
||||
};
|
||||
order.AddHeading(L"Story");
|
||||
for (size_t i = 0; i < count; ++i) {
|
||||
uint8_t* entry = game::BookEntry(i);
|
||||
if (game::BookKind(entry) == game::kBookMain && shown(entry)) {
|
||||
order.AddReal(static_cast<uint32_t>(i));
|
||||
}
|
||||
}
|
||||
// Areas with the same name (several id blocks lie in one region) make one
|
||||
// section, in the order of their first area.
|
||||
int undiscovered = 0;
|
||||
std::vector<std::wstring> sectionNames;
|
||||
std::map<std::wstring, Area> sections;
|
||||
for (const auto& [number, a] : areas) {
|
||||
if (a.known.empty()) {
|
||||
undiscovered += a.total > 0;
|
||||
continue;
|
||||
}
|
||||
const std::wstring name = AreaName(number, a);
|
||||
if (!sections.count(name)) {
|
||||
sectionNames.push_back(name);
|
||||
}
|
||||
Area& section = sections[name];
|
||||
section.total += a.total;
|
||||
section.known.insert(section.known.end(), a.known.begin(), a.known.end());
|
||||
}
|
||||
for (const std::wstring& name : sectionNames) {
|
||||
const Area& a = sections[name];
|
||||
if (!showCompleted && static_cast<int>(a.known.size()) >= a.total &&
|
||||
std::all_of(a.known.begin(), a.known.end(), hidden)) {
|
||||
continue;
|
||||
}
|
||||
const int knownCount = static_cast<int>(a.known.size());
|
||||
order.AddHeading(name + Format(L" (%d/%d)", knownCount, a.total));
|
||||
for (int32_t id : a.known) {
|
||||
addQuest(id);
|
||||
}
|
||||
for (int i = knownCount; i < a.total; ++i) {
|
||||
order.AddOwn(game::kBookSide, L"???");
|
||||
}
|
||||
}
|
||||
bool others = false;
|
||||
for (int32_t id : known) {
|
||||
if (!questArea[id] && !hidden(id)) {
|
||||
if (!others) {
|
||||
order.AddHeading(L"Weitere Nebenquests");
|
||||
others = true;
|
||||
}
|
||||
addQuest(id);
|
||||
}
|
||||
}
|
||||
if (undiscovered || unknownOthers) {
|
||||
order.AddHeading(L"Unentdeckt");
|
||||
if (undiscovered) {
|
||||
order.AddOwn(game::kBookSide, Format(undiscovered == 1 ? L"%d Gebiet" : L"%d Gebiete", undiscovered));
|
||||
}
|
||||
if (unknownOthers) {
|
||||
order.AddOwn(game::kBookSide, Format(unknownOthers == 1 ? L"??? (%d weitere Nebenquest)"
|
||||
: L"??? (%d weitere Nebenquests)",
|
||||
unknownOthers));
|
||||
}
|
||||
}
|
||||
return order;
|
||||
}
|
||||
|
||||
// --- the book's pages ------------------------------------------------------------
|
||||
|
||||
uint8_t* CurrentPage(uint8_t* book) {
|
||||
const size_t tab = Field<uint16_t>(book, kBookTab);
|
||||
uint8_t* pages = Field<uint8_t*>(book, kBookPages + tab * kTabStride);
|
||||
uint8_t* pagesEnd = Field<uint8_t*>(book, kBookPages + tab * kTabStride + 4);
|
||||
uint8_t* page = pages + Field<uint16_t>(book, kBookPage) * kPageSize;
|
||||
return pages && page < pagesEnd ? page : nullptr;
|
||||
}
|
||||
|
||||
uint8_t* RowsBegin(uint8_t* page) { return Field<uint8_t*>(page, kPageRows); }
|
||||
uint8_t* RowsEnd(uint8_t* page) { return Field<uint8_t*>(page, kPageRows + 4); }
|
||||
|
||||
// The row's book entry; kNoEntry for spacers and rows past the end.
|
||||
uint32_t RowEntry(uint8_t* page, uint16_t row) {
|
||||
uint8_t* r = RowsBegin(page) + row * kRowSize;
|
||||
return r < RowsEnd(page) && (Field<uint16_t>(r, 0) & kRowSpacer) == 0 ? Field<uint32_t>(r, kRowEntry) : kNoEntry;
|
||||
}
|
||||
|
||||
bool IsQuestRow(uint8_t* page, uint16_t row) { return RowEntry(page, row) < kToggleEntry; }
|
||||
|
||||
void SelectFirstQuest(uint8_t* page) {
|
||||
uint16_t& selected = Field<uint16_t>(page, kPageSelected);
|
||||
if (IsQuestRow(page, selected)) {
|
||||
return;
|
||||
}
|
||||
for (uint16_t row = 0; RowsBegin(page) + row * kRowSize < RowsEnd(page); ++row) {
|
||||
if (IsQuestRow(page, row)) {
|
||||
selected = row;
|
||||
return;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// cUI_Book::Rebuild (0x6b1670, thiscall, no arguments): rebuilds the current
|
||||
// mode's pages, keeping tab and page if the page still exists.
|
||||
void RebuildBook(uint8_t* book) {
|
||||
reinterpret_cast<void(__fastcall*)(uint8_t*, void*)>(0x006b1670)(book, nullptr);
|
||||
const size_t tab = Field<uint16_t>(book, kBookTab);
|
||||
uint8_t* pages = Field<uint8_t*>(book, kBookPages + tab * kTabStride);
|
||||
uint8_t* pagesEnd = Field<uint8_t*>(book, kBookPages + tab * kTabStride + 4);
|
||||
if (!CurrentPage(book) && pages < pagesEnd) {
|
||||
Field<uint16_t>(book, kBookPage) = static_cast<uint16_t>((pagesEnd - pages) / kPageSize - 1);
|
||||
}
|
||||
if (uint8_t* page = CurrentPage(book)) {
|
||||
SelectFirstQuest(page);
|
||||
}
|
||||
}
|
||||
|
||||
// Rows point into the reordered book; point them at the real entries. Every
|
||||
// page titled "Quests", its first quest selected.
|
||||
void FixPages(uint8_t* book, const Order& order) {
|
||||
for (int tab = 0; tab < kTabs; ++tab) {
|
||||
uint8_t* pages = Field<uint8_t*>(book, kBookPages + tab * kTabStride);
|
||||
uint8_t* pagesEnd = Field<uint8_t*>(book, kBookPages + tab * kTabStride + 4);
|
||||
for (uint8_t* page = pages; page && page < pagesEnd; page += kPageSize) {
|
||||
for (uint8_t* row = RowsBegin(page); row && row < RowsEnd(page); row += kRowSize) {
|
||||
uint32_t& index = Field<uint32_t>(row, kRowEntry);
|
||||
if ((Field<uint16_t>(row, 0) & kRowSpacer) == 0 && index < order.real.size()) {
|
||||
index = order.real[index];
|
||||
}
|
||||
}
|
||||
if (tab != 0) {
|
||||
continue;
|
||||
}
|
||||
Field<uint32_t>(page, kPageTitle) = TextId(L"Quests");
|
||||
SelectFirstQuest(page);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
} // namespace
|
||||
} // namespace mods::questbook
|
||||
|
||||
using namespace mods::questbook;
|
||||
|
||||
// cUI_Book's quest list (0x6b04c0, thiscall, no arguments; from the book's
|
||||
// rebuild 0x6b1670 in quest mode, on the same thread as the quest scripts).
|
||||
void __fastcall Book_BuildQuestList(uint8_t* book, void* edx);
|
||||
RE_HOOK(0x006b04c0, Book_BuildQuestList);
|
||||
void __fastcall Book_BuildQuestList(uint8_t* book, void* edx) {
|
||||
Order order = BuildOrder();
|
||||
uint8_t* const begin = game::BookBegin();
|
||||
uint8_t* const end = game::BookEnd();
|
||||
uint8_t* const capacity = game::BookCapacityEnd();
|
||||
uint32_t& selected = game::BookSelected(game::LocalPlayerSlot());
|
||||
const uint32_t realSelected = selected;
|
||||
uint32_t reorderedSelected = static_cast<uint32_t>(order.entries.size());
|
||||
for (size_t i = 0; i < order.real.size(); ++i) {
|
||||
if (order.real[i] == realSelected) {
|
||||
reorderedSelected = static_cast<uint32_t>(i);
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
auto* copy = reinterpret_cast<uint8_t*>(order.entries.data());
|
||||
game::BookBegin() = copy;
|
||||
game::BookEnd() = game::BookCapacityEnd() = copy + order.entries.size() * game::kBookEntrySize;
|
||||
selected = reorderedSelected;
|
||||
Book_BuildQuestList_original(book, edx);
|
||||
game::BookBegin() = begin;
|
||||
game::BookEnd() = end;
|
||||
game::BookCapacityEnd() = capacity;
|
||||
selected = realSelected;
|
||||
|
||||
FixPages(book, order);
|
||||
for (int tab = 0; tab < kTabs; ++tab) {
|
||||
game::SetControlShown(book + kBookmarks + tab * kBookmarkStride, false);
|
||||
}
|
||||
}
|
||||
|
||||
// cUI_Book's event handler (0x6b3620, thiscall (event), ret 4): a click on a
|
||||
// heading or "???" row keeps the selection, one on "Erledigte anzeigen"
|
||||
// toggles it.
|
||||
bool __fastcall Book_HandleEvent(uint8_t* book, void* edx, void* event);
|
||||
RE_HOOK(0x006b3620, Book_HandleEvent);
|
||||
bool __fastcall Book_HandleEvent(uint8_t* book, void* edx, void* event) {
|
||||
uint8_t* page = Field<uint16_t>(book, kBookMode) == kModeQuests ? CurrentPage(book) : nullptr;
|
||||
const uint16_t selected = page ? Field<uint16_t>(page, kPageSelected) : kNoRow;
|
||||
const bool handled = Book_HandleEvent_original(book, edx, event);
|
||||
if (page && page == CurrentPage(book) && !IsQuestRow(page, Field<uint16_t>(page, kPageSelected))) {
|
||||
const bool toggle = RowEntry(page, Field<uint16_t>(page, kPageSelected)) == kToggleEntry;
|
||||
Field<uint16_t>(page, kPageSelected) = selected;
|
||||
if (toggle) {
|
||||
ShowCompleted() = !ShowCompleted();
|
||||
re::SetConfigString(kConfigSection, kConfigShowCompleted, ShowCompleted() ? "1" : "0");
|
||||
RebuildBook(book);
|
||||
}
|
||||
}
|
||||
return handled;
|
||||
}
|
||||
Reference in new issue
Block a user