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:
1 parent
91bbc52cf0
commit
3f03228c29
4 files changed
+229
No files matched your search
@@ -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
|
||||
Reference in new issue
Block a user