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>
This commit is contained in:
khallaandClaude Opus 5.5 committed 2026-10-25 01:44:01 +02:00
1 parent 91bbc52cf0
commit 3f03228c29
4 files changed
+229

No files matched your search

+90
View File
@@ -0,0 +1,90 @@
// Failed quests can be restarted (request of 2026-10-17): a failed quest is not
// final, the quest giver offers it again.
//
// Measured (docs/quests.md): a failed quest keeps flags 3 (triggered + done) in
// the quest table and its quest book entry gets state 101; Quest_Trigger
// (0x46de10) does nothing for a quest with flag 1, so the giver never offers it
// again. The game itself repeats only the generic quests (ids 2600..3599,
// Quest_End copies them under a new id).
//
// The mod: after Quest_End failed a quest (its failure script has run), the
// quest's flags are cleared and its book entries removed, as if it had never
// been triggered. Quests that failed before the mod are reset the same way once
// after loading.
#include <algorithm>
#include <cstdint>
#include "game/engine/events.h"
#include "game/engine/quests.h"
#include "re/hook.h"
#include "re/log.h"
namespace mods::questfail {
namespace {
constexpr uint32_t kResetFlags = game::kQuestTriggered | game::kQuestDone | game::kQuestInProgress | game::kQuestBusy;
bool g_resetAfterLoad = false;
// The quest at table `index` as if it had never been triggered.
void Restart(int index) {
uint8_t* quest = game::Quest(index);
const int32_t id = game::QuestId(quest);
re::Log("questfail: restarting quest %d '%s' (flags %x)", id, game::QuestName(quest), game::QuestFlags(quest));
game::QuestFlags(quest) &= ~kResetFlags;
for (size_t i = game::BookCount(); i-- > 0;) {
if (game::BookQuestId(game::BookEntry(i)) == id) {
game::EraseBookEntry(i);
}
}
}
void RestartFailedInBook() {
for (size_t i = game::BookCount(); i-- > 0;) {
const uint8_t* entry = game::BookEntry(i);
if (game::BookState(entry) != game::kBookFailed) {
continue;
}
const int index = game::QuestIndex(game::BookQuestId(entry));
if (index >= 0) {
Restart(index);
}
i = std::min(i, game::BookCount());
}
}
void OnTick(uint8_t*) {
if (g_resetAfterLoad) {
g_resetAfterLoad = false;
RestartFailedInBook();
}
}
void OnLoaded() {
g_resetAfterLoad = true;
}
} // namespace
// Quest_End (0x46ced0, quest manager thiscall (index, ?, success), ret 0xc):
// LoseQuest passes success 0, ExitQuest 1.
void __fastcall Quest_End(uint8_t* self, void* edx, int index, int a2, char success);
RE_HOOK(0x0046ced0, Quest_End);
void __fastcall Quest_End(uint8_t* self, void* edx, int index, int a2, char success) {
Quest_End_original(self, edx, index, a2, success);
if (!success && index >= 0 && static_cast<size_t>(index) < game::QuestCount()) {
Restart(index);
}
}
namespace {
struct Register {
Register() {
game::OnEngineTick(&OnTick);
game::OnSaveGameLoaded(&OnLoaded);
}
} g_register;
} // namespace
} // namespace mods::questfail