A failed quest gets its flags cleared and its quest book entries removed after its failure script ran, so the quest giver can offer it again; quests failed before the mod are reset once after loading. Quest table and quest book in game/engine/quests.h, measurements in docs/quests.md. Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
97 lines
4.3 KiB
C++
97 lines
4.3 KiB
C++
#pragma once
|
|
// Quests: the quest table (one entry per quest, from the scripts) and the quest
|
|
// manager's quest book (the hero's accepted quests). docs/quests.md.
|
|
|
|
#include <cstddef>
|
|
#include <cstdint>
|
|
#include <cstring>
|
|
|
|
namespace game {
|
|
|
|
// Quest table g_quests: vector begin/end (0xaa9e98 / 0xaa9e9c), 0x124 bytes per quest.
|
|
constexpr uintptr_t kQuestsBegin = 0x00aa9e98;
|
|
constexpr uintptr_t kQuestsEnd = 0x00aa9e9c;
|
|
constexpr size_t kQuestSize = 0x124;
|
|
constexpr size_t kQuestId = 0x00;
|
|
constexpr size_t kQuestName = 0x04; // internal name, ANSI
|
|
constexpr size_t kQuestTriggerScript = 0x10c; // run by Quest_Trigger 0x46de10
|
|
constexpr size_t kQuestSuccessScript = 0x118; // run by Quest_End on success
|
|
constexpr size_t kQuestFailureScript = 0x11c; // run by Quest_End on failure
|
|
constexpr size_t kQuestFlags = 0x120;
|
|
|
|
// Quest flags (+0x120); QuestDone tests 2, QuestInProgress 4.
|
|
constexpr uint32_t kQuestTriggered = 0x1;
|
|
constexpr uint32_t kQuestDone = 0x2;
|
|
constexpr uint32_t kQuestInProgress = 0x4;
|
|
constexpr uint32_t kQuestBusy = 0x8; // while Quest_Trigger runs the scripts
|
|
|
|
inline uint8_t* QuestsBegin() { return *reinterpret_cast<uint8_t**>(kQuestsBegin); }
|
|
inline uint8_t* QuestsEnd() { return *reinterpret_cast<uint8_t**>(kQuestsEnd); }
|
|
inline size_t QuestCount() {
|
|
uint8_t* begin = QuestsBegin();
|
|
return begin && QuestsEnd() > begin ? (QuestsEnd() - begin) / kQuestSize : 0;
|
|
}
|
|
inline uint8_t* Quest(int index) { return QuestsBegin() + index * kQuestSize; }
|
|
inline int32_t& QuestId(uint8_t* quest) { return *reinterpret_cast<int32_t*>(quest + kQuestId); }
|
|
inline uint32_t& QuestFlags(uint8_t* quest) { return *reinterpret_cast<uint32_t*>(quest + kQuestFlags); }
|
|
inline const char* QuestName(const uint8_t* quest) { return reinterpret_cast<const char*>(quest + kQuestName); }
|
|
|
|
// Table index of quest `id`, -1 if unknown.
|
|
inline int QuestIndex(int32_t id) {
|
|
for (size_t i = 0, n = QuestCount(); i < n; ++i) {
|
|
if (QuestId(Quest(static_cast<int>(i))) == id) {
|
|
return static_cast<int>(i);
|
|
}
|
|
}
|
|
return -1;
|
|
}
|
|
|
|
// The quest manager, a static object (script interpreter; ecx of 0x475970 and
|
|
// of Quest_End 0x46ced0). Its quest book is a vector of plain 0x174-byte entries
|
|
// (serialized field by field, Quest_End 0x46d9d7..); per player a selected
|
|
// entry index at +0x3a8 + player * 8 (Quest_End sets it to the count = none).
|
|
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 kBookState = 0x04;
|
|
constexpr size_t kBookQuestId = 0x08;
|
|
constexpr size_t kBookSelected = 0x3a8;
|
|
constexpr size_t kBookSelectedStride = 8;
|
|
constexpr int kBookPlayers = 16; // +0x3a4.. 16 pairs up to the book at +0x424
|
|
|
|
// Book entry states (+4); Quest_End writes 100 / 101.
|
|
constexpr int32_t kBookActive = 2;
|
|
constexpr int32_t kBookSolved = 100;
|
|
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 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); }
|
|
|
|
// Removes book entry `index`; the players' selected indices are kept on their
|
|
// entries (or "none" = the new count).
|
|
inline void EraseBookEntry(size_t index) {
|
|
const size_t count = BookCount();
|
|
if (index >= count) {
|
|
return;
|
|
}
|
|
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;
|
|
}
|
|
}
|
|
}
|
|
|
|
} // namespace game
|