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>
91 lines
2.8 KiB
C++
91 lines
2.8 KiB
C++
// 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
|