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:
khallaandClaude Opus 5.5 committed 2026-10-25 11:41:19 +01:00
1 parent 34cacefcfc
commit 1d16fc65c4
7 files changed
+675 -14

No files matched your search

+46 -6
View File
@@ -54,12 +54,34 @@ inline uint8_t* QuestManager() { return reinterpret_cast<uint8_t*>(0x00aaaf00);
constexpr size_t kBookBegin = 0x424;
constexpr size_t kBookEnd = 0x428;
constexpr size_t kBookEntrySize = 0x174;
constexpr size_t kBookCapacity = 0x42c;
constexpr size_t kBookKind = 0x00; // 3 main quest (ids 1..99), 4 side quest (0x46c450)
constexpr size_t kBookState = 0x04;
constexpr size_t kBookQuestId = 0x08;
constexpr size_t kBookTitle = 0x24; // text ids
constexpr size_t kBookText = 0x28;
// The quest compass (the quest's marker on the maps, script QuestkompassPos /
// QuestkompassOBJ 0x49a7a0 / 0x49af70): target +0x10/+0x14 is a map cell
// (x, y), or kCompassObject + object id, or kCompassScriptNpc + script NPC id
// (resolved to an object later); radius +0x20 ("rad:"). The marker's cell
// +0x18/+0x1c is resolved from the target by 0x4a6740 (thiscall (entry), ret 4).
constexpr size_t kBookCompassX = 0x10;
constexpr size_t kBookCompassY = 0x14;
constexpr size_t kBookCompassRadius = 0x20;
constexpr uint32_t kCompassObject = 0xffffffff;
constexpr uint32_t kCompassScriptNpc = 0xeeeeeeee;
constexpr size_t kBookX = 0x18; // marker cell (0x497830)
constexpr size_t kBookY = 0x1c;
constexpr size_t kBookChapter = 0x16c; // u16, book tab + 1 (0 = tab 0), copied from the current main quest
constexpr size_t kBookMainIndex = 0x3a4; // per player: the current main quest's entry
constexpr size_t kBookSelected = 0x3a8;
constexpr size_t kBookSelectedStride = 8;
constexpr int kBookPlayers = 16; // +0x3a4.. 16 pairs up to the book at +0x424
constexpr int32_t kBookMain = 3;
constexpr int32_t kBookSide = 4;
constexpr int kMainQuestLast = 99; // quest ids 1..99 are main quests (0x46c450)
// Book entry states (+4); Quest_End writes 100 / 101.
constexpr int32_t kBookActive = 2;
constexpr int32_t kBookSolved = 100;
@@ -67,15 +89,32 @@ constexpr int32_t kBookFailed = 101;
inline uint8_t*& BookBegin() { return *reinterpret_cast<uint8_t**>(QuestManager() + kBookBegin); }
inline uint8_t*& BookEnd() { return *reinterpret_cast<uint8_t**>(QuestManager() + kBookEnd); }
inline uint8_t*& BookCapacityEnd() { return *reinterpret_cast<uint8_t**>(QuestManager() + kBookCapacity); }
inline size_t BookCount() {
return BookBegin() && BookEnd() > BookBegin() ? (BookEnd() - BookBegin()) / kBookEntrySize : 0;
}
inline uint8_t* BookEntry(size_t index) { return BookBegin() + index * kBookEntrySize; }
inline int32_t BookState(const uint8_t* entry) { return *reinterpret_cast<const int32_t*>(entry + kBookState); }
inline int32_t BookQuestId(const uint8_t* entry) { return *reinterpret_cast<const int32_t*>(entry + kBookQuestId); }
inline int32_t BookKind(const uint8_t* entry) { return *reinterpret_cast<const int32_t*>(entry + kBookKind); }
inline uint16_t BookChapter(const uint8_t* entry) { return *reinterpret_cast<const uint16_t*>(entry + kBookChapter); }
// The local player's slot in the per-player book fields (session +0x14 is the
// player number 1..16; 0x46c450, 0x49dd40).
inline int LocalPlayerSlot() {
using InstanceFn = uint8_t*(__cdecl*)();
const int player = *reinterpret_cast<int32_t*>(reinterpret_cast<InstanceFn>(0x007d7c40)() + 0x14);
return player >= 1 && player <= kBookPlayers ? player - 1 : 0;
}
inline uint32_t& BookSelected(int slot) {
return *reinterpret_cast<uint32_t*>(QuestManager() + kBookSelected + slot * kBookSelectedStride);
}
inline uint32_t& BookMainIndex(int slot) {
return *reinterpret_cast<uint32_t*>(QuestManager() + kBookMainIndex + slot * kBookSelectedStride);
}
// Removes book entry `index`; the players' selected indices are kept on their
// entries (or "none" = the new count).
// entries (or "none" = the new count), and so are their main quest indices.
inline void EraseBookEntry(size_t index) {
const size_t count = BookCount();
if (index >= count) {
@@ -84,11 +123,12 @@ inline void EraseBookEntry(size_t index) {
std::memmove(BookEntry(index), BookEntry(index + 1), (count - index - 1) * kBookEntrySize);
BookEnd() -= kBookEntrySize;
for (int player = 0; player < kBookPlayers; ++player) {
auto& selected = *reinterpret_cast<uint32_t*>(QuestManager() + kBookSelected + player * kBookSelectedStride);
if (selected == index || selected >= count) {
selected = static_cast<uint32_t>(count - 1);
} else if (selected > index) {
--selected;
for (uint32_t* kept : {&BookSelected(player), &BookMainIndex(player)}) {
if (*kept == index || *kept >= count) {
*kept = static_cast<uint32_t>(count - 1);
} else if (*kept > index) {
--*kept;
}
}
}
}
+40
View File
@@ -0,0 +1,40 @@
#pragma once
// World regions: each sector carries a region number; the region name table
// (from scripts/regionsnamen) maps region numbers to name text keys. The
// megamap names the hero's region so (0x6c6d73..): position -> sector
// (0x634970) -> region (0x622e30) -> key (0x425370) -> text.
#include <cstdint>
#include "game/engine/objects.h"
#include "game/engine/orders.h"
namespace game {
// The region number at map cell (x, y) (quest book positions +0x18/+0x1c are
// cells); 0 if none. World_SectorAt 0x634970 (thiscall (position*), ret 4,
// world units) on the object manager's world (manager +0), Sector_Region
// 0x622e30 (thiscall, plain ret).
inline uint8_t RegionAtCell(int32_t x, int32_t y) {
void* manager = ObjectManager();
void* world = manager ? *reinterpret_cast<void**>(manager) : nullptr;
if (!world) {
return 0;
}
MapPosition position;
position.x = static_cast<int32_t>(x * kMapCellSize);
position.y = static_cast<int32_t>(y * kMapCellSize);
using SectorFn = void*(__fastcall*)(void* world, void* edx, MapPosition* position);
using RegionFn = uint32_t(__fastcall*)(void* sector, void* edx);
void* sector = reinterpret_cast<SectorFn>(0x00634970)(world, nullptr, &position);
return sector ? static_cast<uint8_t>(reinterpret_cast<RegionFn>(0x00622e30)(sector, nullptr)) : 0;
}
// The name text key of region `region` (Region_NameKey 0x425370, cdecl), or
// null.
inline const char* RegionNameKey(uint8_t region) {
using Fn = const char*(__cdecl*)(char region);
return region ? reinterpret_cast<Fn>(0x00425370)(static_cast<char>(region)) : nullptr;
}
} // namespace game
+499
View File
@@ -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;
}