Files
sacred_k/src/game/engine/quests.h
T
khallaandClaude Opus 5.5 3f03228c29 Failed quests restartable
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>
2026-10-25 01:44:01 +02:00

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