Quests: escorts stay on the follower list, quest givers come back

Escorts lost their place on the hero's follower list after a load, so
quest checks like the one in "Furchtbares Verbrechen" failed; they are
added back once after loading (town NPCs that tag along briefly are left
out). Restarted quests stay available (flag 1), which their giver's dialog
and accepting them need. Quest givers are read from the quest scripts:
when a running quest's giver dies, the quest restarts and the giver is
spawned again at her start; an accepted quest without its giver is repaired
on load. Notes on the script format and opcodes 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-26 02:30:41 +01:00
1 parent 2cab6c216d
commit 7d8089c299
11 files changed
+872 -18

No files matched your search

+73
View File
@@ -0,0 +1,73 @@
// Escorted NPCs stay on the hero's follower list (2026-10-26, docs/quests.md).
// "Furchtbares Verbrechen" (quest 9008) did not go on at its target point:
// the NPC walked after the hero, but the script's test there ("@npc", 0x498aa0
// case 0x40) looks the NPC up in the hero's follower list (hero +0x39c), and
// that list was empty (measured on Borf's save: NPC 0x1fa with leader 1 and
// the follow bit, no followers on the hero). The NPC keeps its side of the
// escort (leader +0x251, status bit 4) across a load; the hero's list does
// not come back. The mod adds every NPC that follows a hero but is missing
// from its list back with the game's own AddFollower (0x54b070), with the
// entry the follow order uses ({id, 1, 1}), once after loading.
#include <cstdint>
#include "game/engine/events.h"
#include "game/engine/objects.h"
#include "game/engine/orders.h"
#include "re/log.h"
namespace mods::followers {
namespace {
constexpr int kTicksAfterStart = 30;
// Small town NPCs run after the hero for a few seconds (status 0x404, leader
// the hero) without joining his list (measured 2026-10-26: types 0x2be,
// 0x2c0); they are not escorts.
constexpr int32_t kStatusTagAlong = 0x400;
int g_ticksToCheck = -1;
int32_t Field(const void* creature, size_t offset) {
return *reinterpret_cast<const int32_t*>(static_cast<const uint8_t*>(creature) + offset);
}
void RepairFollowers() {
for (uint32_t id = 1; id < game::ObjectIdEnd(); ++id) {
if (game::IsPlayerHeroId(id) || !game::GetObjectById(id)) {
continue;
}
void* creature = game::GetCreature(id);
const int32_t status = creature ? Field(creature, game::kCreatureStatus) : 0;
if (!(status & game::kStatusFollowing) || (status & kStatusTagAlong)) {
continue;
}
const auto leaderId = static_cast<uint32_t>(Field(creature, game::kCreatureLeader));
void* leader = game::IsPlayerHeroId(leaderId) ? game::GetCreature(leaderId) : nullptr;
if (!leader || game::HasFollower(leader, id)) {
continue;
}
game::AddFollower(leader, {id, 1, 1});
re::Log("followers: %x back on the follower list of hero %x", id, leaderId);
}
}
void OnTick(uint8_t*) {
if (g_ticksToCheck > 0 && --g_ticksToCheck == 0) {
RepairFollowers();
}
}
void OnStart() {
g_ticksToCheck = kTicksAfterStart;
}
struct Registration {
Registration() {
game::OnEngineTick(&OnTick);
game::OnGameStart(&OnStart);
}
} g_registration;
} // namespace
} // namespace mods::followers
+190 -16
View File
@@ -1,43 +1,126 @@
// Failed quests can be restarted (request of 2026-10-17): a failed quest is not
// final, the quest giver offers it again.
// final, the quest giver offers it again. Its marker points back to the quest
// giver, drawn blue like an open side quest (request of 2026-10-25: "quests
// that failed, should have the blue marker point back to the quest giver").
//
// 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).
// Quest_End copies them under a new id). The maps draw a marker for every book
// entry at its quest compass cell (+0x18/+0x1c), coloured by 0x4975d0: blue
// for open side quests, red for failed ones. The game does not keep the giver:
// a quest is accepted in a dialog answer (cDialogManager::Answer 0x49a090 ->
// quest script -> book entry 0x46c450), so the giver is the NPC next to the
// hero then.
//
// 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.
// The mod: when a quest is accepted in a dialog, the nearest NPC's map cell is
// kept as the quest's giver (saved with the hero data, "questGivers"; quest
// ids are the same in every world, and so are the givers). After Quest_End
// failed a quest (its failure script has run), the quest's flags are reset to
// available (not accepted, not done); its book entry stays as failed with the
// compass on the giver, or is removed if the giver is not known. Accepting it
// again removes the failed entry. Quests that failed before the mod are reset
// the same way once after loading.
#include <algorithm>
#include <cstdint>
#include <map>
#include <string>
#include <utility>
#include <nlohmann/json.hpp>
#include "game/engine/events.h"
#include "game/engine/objects.h"
#include "game/engine/orders.h"
#include "game/engine/quests.h"
#include "mods/inventory_save.h"
#include "mods/questfail.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;
// A restarted quest stays available (kQuestTriggered): its labels, the giver's
// dialog too, run only while it is set (Script_RunLabel 0x46bf11; measured
// 2026-10-26, "Dialog faild" without it), and so does accepting it (script
// opcode 0x14, 0x48f320). The game sets it when the hero enters the quest's
// region (Quest_Trigger 0x46de10).
constexpr uint32_t kResetFlags = game::kQuestDone | game::kQuestInProgress | game::kQuestBusy;
// The giver is the nearest NPC within this distance of the hero when the quest
// is accepted (world units; a talk starts at contact distance, about 100).
constexpr uint32_t kGiverMaxDistance = 400;
// Marker colours of 0x4975d0 for an open side quest (state 2): kind 0 the
// marker, kind 1 the second colour (white).
constexpr uint32_t kOpenSideQuestColour = 0xff006cff;
constexpr uint32_t kOpenQuestColour2 = 0xffffffff;
bool g_resetAfterLoad = false;
bool g_inDialogAnswer = 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);
// Quest id -> the giver's map cell.
std::map<int32_t, std::pair<int32_t, int32_t>> g_givers;
// The nearest living, not hostile, non-hero creature near the hero, or 0. All
// objects are searched (the nearby-creature list 0xad1c60 did not hold the
// givers).
uint32_t NearestNpc(void* hero) {
const int32_t heroX = game::CreatureField(hero, game::kCreatureX);
const int32_t heroY = game::CreatureField(hero, game::kCreatureY);
uint32_t best = 0;
uint32_t bestDistance = kGiverMaxDistance + 1;
for (uint32_t id = game::kPlayerHeroIdEnd, end = game::ObjectIdEnd(); id < end; ++id) {
void* creature = game::GetCreature(id);
if (!creature) {
continue;
}
const int32_t dx = game::CreatureField(creature, game::kCreatureX) - heroX;
const int32_t dy = game::CreatureField(creature, game::kCreatureY) - heroY;
if (dx < -1000 || dx > 1000 || dy < -1000 || dy > 1000) {
continue;
}
const uint32_t distance = game::DistanceTo(hero, id);
const bool alive = game::IsAlive(creature);
const bool hostile = game::IsHostileTo(hero, creature);
if (alive && !hostile && distance < bestDistance) {
best = id;
bestDistance = distance;
}
}
return best;
}
void RememberGiver(int32_t questId) {
void* hero = game::GetPlayerHero();
const uint32_t npc = hero ? NearestNpc(hero) : 0;
void* creature = npc ? game::GetCreature(npc) : nullptr;
if (!creature) {
re::Log("questfail: quest %d accepted, no giver near the hero", questId);
return;
}
const auto* position = reinterpret_cast<const game::MapPosition*>(static_cast<uint8_t*>(creature) +
game::kObjectPosition);
const game::MapPosition cell = game::CellAt(creature, position->x, position->y);
g_givers[questId] = {cell.x, cell.y};
re::Log("questfail: quest %d accepted from creature %u at cell %d,%d", questId, npc, cell.x, cell.y);
}
// The entry's compass on its quest's giver (a cell target, no radius). False if
// the giver is not known.
bool PointAtGiver(uint8_t* entry) {
const auto giver = g_givers.find(game::BookQuestId(entry));
if (giver == g_givers.end()) {
return false;
}
auto field = [entry](size_t offset) -> int32_t& { return *reinterpret_cast<int32_t*>(entry + offset); };
field(game::kBookCompassX) = field(game::kBookX) = giver->second.first;
field(game::kBookCompassY) = field(game::kBookY) = giver->second.second;
field(game::kBookCompassRadius) = 0;
return true;
}
void RestartFailedInBook() {
@@ -54,6 +137,15 @@ void RestartFailedInBook() {
}
}
void EraseFailedEntries(int32_t questId) {
for (size_t i = game::BookCount(); i-- > 0;) {
const uint8_t* entry = game::BookEntry(i);
if (game::BookQuestId(entry) == questId && game::BookState(entry) == game::kBookFailed) {
game::EraseBookEntry(i);
}
}
}
void OnTick(uint8_t*) {
if (g_resetAfterLoad) {
g_resetAfterLoad = false;
@@ -65,8 +157,48 @@ void OnLoaded() {
g_resetAfterLoad = true;
}
// --- saved with the hero: {"quest id": [x, y]} ------------------------------------
nlohmann::json WriteGivers(unsigned) {
if (g_givers.empty()) {
return nlohmann::json();
}
nlohmann::json all = nlohmann::json::object();
for (const auto& [id, cell] : g_givers) {
all[std::to_string(id)] = {cell.first, cell.second};
}
return all;
}
// Merged: the hero file and the save game both carry the list.
void ReadGivers(unsigned, const nlohmann::json& data) {
if (!data.is_object()) {
return;
}
for (const auto& [key, cell] : data.items()) {
if (cell.is_array() && cell.size() == 2 && cell[0].is_number_integer() && cell[1].is_number_integer()) {
g_givers[std::stoi(key)] = {cell[0].get<int32_t>(), cell[1].get<int32_t>()};
}
}
}
} // namespace
// The quest at table `index` as if it had never been triggered; its failed book
// entries point at the giver (or go).
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) = (game::QuestFlags(quest) & ~kResetFlags) | game::kQuestTriggered;
for (size_t i = game::BookCount(); i-- > 0;) {
uint8_t* entry = game::BookEntry(i);
if (game::BookQuestId(entry) == id && !PointAtGiver(entry)) {
game::EraseBookEntry(i);
}
}
}
// 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);
@@ -78,11 +210,53 @@ void __fastcall Quest_End(uint8_t* self, void* edx, int index, int a2, char succ
}
}
// cDialogManager::Answer (0x49a090, thiscall (owner, answer), ret 8): runs the
// quest script for the answer.
void __fastcall Dialog_Answer(uint8_t* self, void* edx, uint32_t owner, uint32_t answer);
RE_HOOK(0x0049a090, Dialog_Answer);
void __fastcall Dialog_Answer(uint8_t* self, void* edx, uint32_t owner, uint32_t answer) {
g_inDialogAnswer = true;
Dialog_Answer_original(self, edx, owner, answer);
g_inDialogAnswer = false;
}
// QuestManager_AddBookEntry (0x46c450, quest manager thiscall (quest index, ?), ret 8): adds
// the quest's book entry (state 2).
void __fastcall QuestManager_AddBookEntry(uint8_t* self, void* edx, int index, int a2);
RE_HOOK(0x0046c450, QuestManager_AddBookEntry);
void __fastcall QuestManager_AddBookEntry(uint8_t* self, void* edx, int index, int a2) {
const bool known = index >= 0 && static_cast<size_t>(index) < game::QuestCount();
const int32_t id = known ? game::QuestId(game::Quest(index)) : 0;
if (known) {
EraseFailedEntries(id);
}
QuestManager_AddBookEntry_original(self, edx, index, a2);
if (known && g_inDialogAnswer && id > game::kMainQuestLast) {
RememberGiver(id);
}
}
// QuestManager_MarkerColour (0x4975d0, quest manager thiscall (entry, kind), ret 8): a
// failed side quest pointing at its giver is drawn as an open one.
uint32_t __fastcall QuestManager_MarkerColour(uint8_t* self, void* edx, uint32_t index, int16_t kind);
RE_HOOK(0x004975d0, QuestManager_MarkerColour);
uint32_t __fastcall QuestManager_MarkerColour(uint8_t* self, void* edx, uint32_t index, int16_t kind) {
if (index < game::BookCount() && (kind == 0 || kind == 1)) {
const uint8_t* entry = game::BookEntry(index);
if (game::BookState(entry) == game::kBookFailed && game::BookKind(entry) == game::kBookSide &&
g_givers.count(game::BookQuestId(entry))) {
return kind == 0 ? kOpenSideQuestColour : kOpenQuestColour2;
}
}
return QuestManager_MarkerColour_original(self, edx, index, kind);
}
namespace {
struct Register {
Register() {
game::OnEngineTick(&OnTick);
game::OnSaveGameLoaded(&OnLoaded);
inventory_save::RegisterHeroData("questGivers", &WriteGivers, &ReadGivers);
}
} g_register;
} // namespace
+10
View File
@@ -0,0 +1,10 @@
#pragma once
// Failed quests can be restarted (questfail.cpp).
namespace mods::questfail {
// The quest at table `index` as if it had never been triggered; its failed book
// entries point at the giver (or go).
void Restart(int index);
} // namespace mods::questfail
+340
View File
@@ -0,0 +1,340 @@
// Quest givers come back (request of 2026-10-26, docs/quests.md): "Jagd auf
// die Wilderer" (quest 1501): the giver, an elf, follows the hero and fights;
// when she died the quest could not be finished ("change the death script to
// spawn her back to her starting point"; "on load we need a repair ... if not
// completed or correctly running").
//
// Measured: the quest's script (FunkCode.bin) gives the giver a death label
// when she starts following ("ex1501": quest variable "1501" = 1, then the
// poachers' death label "tot1501", which ends the quest without a reward once
// all poachers are dead; until then it stays in progress with no giver). The
// body is removed. The giver is placed by her region's start script
// (Region15Init: spawn instruction 1 "Res:17013", type 134, at
// pos_auftrag1501, dialog auftrag1501), which the game runs once.
//
// The mod finds each quest's giver in the script code: the NPC whose spawn
// instruction carries a dialog that has a "Dialog:<name>" label of the quest.
// When a label of that quest runs for its dead giver (her death label), the
// label is skipped; the quest restarts (questfail::Restart, its book entry
// failed and pointing at the giver), the variables only this quest's labels
// set go back to 0, and once the body is gone the game runs the giver's
// spawn instruction again: she stands at her start, offering the quest.
// After a load, a quest in progress whose giver no longer exists is repaired
// the same way.
#include <cstdint>
#include <cstdlib>
#include <cstring>
#include <map>
#include <set>
#include <string>
#include <vector>
#include <windows.h>
#include "game/engine/events.h"
#include "game/engine/objects.h"
#include "game/engine/orders.h"
#include "game/engine/quests.h"
#include "mods/questfail.h"
#include "re/hook.h"
#include "re/log.h"
namespace mods::questgiver {
namespace {
constexpr size_t kInstructionHeader = 4;
constexpr size_t kMaxInstruction = 0x800; // 0x475970 refuses larger ones
constexpr size_t kReadChunk = 1 << 20;
constexpr DWORD kBodyWaitMs = 30000; // spawn anyway if the body is still there
constexpr uint16_t kScriptStartQuest = 0x14; // {0x0b quest id}
constexpr size_t kStartQuestSize = 9;
constexpr uint16_t kScriptAnswer = 0x3c; // dialog answer {0x01 text, 0x01 label}
struct Giver {
uint32_t scriptId = 0;
std::vector<uint8_t> spawn; // the spawn instruction
};
struct Pending {
int questIndex;
void* body; // the dead giver's object
uint32_t bodyId;
DWORD since;
};
bool g_scanned = false;
std::vector<uint8_t> g_code; // FunkCode.bin
std::map<int32_t, Giver> g_givers; // quest id -> giver
std::map<int32_t, std::vector<std::string>> g_variables; // quest id -> variables only it sets
std::vector<Pending> g_pending;
bool g_repairAfterLoad = false;
uint16_t U16(size_t at) { return static_cast<uint16_t>(g_code[at] | (g_code[at + 1] << 8)); }
bool ReadCode() {
g_code.clear();
for (uint32_t offset = 0;; offset += kReadChunk) {
g_code.resize(offset + kReadChunk);
const size_t read = game::ReadScriptFile(offset, g_code.data() + offset, kReadChunk);
g_code.resize(offset + read);
if (read < kReadChunk) {
return !g_code.empty();
}
}
}
// Calls `fn(at, opcode, size)` for each instruction of a label's code.
template <typename Fn>
void ForEachInstruction(const uint8_t* label, Fn fn) {
const size_t begin = game::ScriptLabelCode(label);
const size_t end = begin + game::ScriptLabelCodeSize(label);
for (size_t at = begin; at + kInstructionHeader <= end && end <= g_code.size();) {
const uint16_t size = U16(at + 2);
if (size < kInstructionHeader || at + size > end) {
return;
}
fn(at, U16(at), size);
at += size;
}
}
// The spawn instructions in the code: "Res:<number>" with a dialog.
void ScanSpawns(const std::map<std::string, int32_t>& dialogs) {
static const char kRes[] = "res:";
for (size_t at = 0; at + 9 < g_code.size(); ++at) {
if (U16(at) != game::kScriptSpawn || g_code[at + 4] != game::kScriptArgString ||
_strnicmp(reinterpret_cast<const char*>(&g_code[at + 5]), kRes, 4) != 0) {
continue;
}
const uint16_t size = U16(at + 2);
if (size <= 9 || size > kMaxInstruction || at + size > g_code.size()) {
continue;
}
char* numberEnd = nullptr;
const unsigned long scriptId = std::strtoul(reinterpret_cast<const char*>(&g_code[at + 9]), &numberEnd, 10);
if (scriptId == 0 || *numberEnd != 0) {
continue;
}
for (size_t tag = at + 9; tag + 1 < at + size; ++tag) {
if (g_code[tag] != game::kScriptArgDialog) {
continue;
}
const char* name = reinterpret_cast<const char*>(&g_code[tag + 1]);
if (!memchr(name, 0, at + size - tag - 1)) {
break;
}
const auto dialog = dialogs.find(name);
if (dialog != dialogs.end() && !g_givers.count(dialog->second)) {
g_givers[dialog->second] = {static_cast<uint32_t>(scriptId),
std::vector<uint8_t>(g_code.begin() + at, g_code.begin() + at + size)};
}
}
}
}
// The variables each quest's labels set; a variable set by several quests
// (or by labels of none) is not any one quest's.
void ScanVariables() {
std::map<std::string, std::set<int32_t>> setters;
for (const uint8_t* label = game::ScriptLabelsBegin(); label && label < game::ScriptLabelsEnd();
label += game::kScriptLabelSize) {
const int32_t quest = game::ScriptLabelQuest(label);
ForEachInstruction(label, [&](size_t at, uint16_t opcode, uint16_t size) {
if (opcode == game::kScriptSetVariable && g_code[at + 4] == game::kScriptArgString &&
memchr(&g_code[at + 5], 0, size - 5)) {
setters[reinterpret_cast<const char*>(&g_code[at + 5])].insert(quest);
}
});
}
for (const auto& [name, quests] : setters) {
if (quests.size() == 1 && *quests.begin() >= 0 && g_givers.count(*quests.begin())) {
g_variables[*quests.begin()].push_back(name);
}
}
}
void Scan() {
if (g_scanned) {
return;
}
g_scanned = true;
if (!ReadCode()) {
re::Log("questgiver: script code not readable");
return;
}
// Labels by name, and the accept labels: code starting with opcode 0x14
// (start quest #id, 0x48f320; e.g. "trigger1501").
std::map<std::string, const uint8_t*> byName;
std::map<std::string, int32_t> accepts; // label name -> quest it starts
for (const uint8_t* label = game::ScriptLabelsBegin(); label && label < game::ScriptLabelsEnd();
label += game::kScriptLabelSize) {
const std::string name(game::ScriptLabelName(label), strnlen(game::ScriptLabelName(label), game::kScriptLabelNameSize));
byName.emplace(name, label);
const size_t at = game::ScriptLabelCode(label);
if (game::ScriptLabelCodeSize(label) >= kStartQuestSize && at + kStartQuestSize <= g_code.size() &&
U16(at) == kScriptStartQuest && g_code[at + 4] == game::kScriptArgInt) {
int32_t quest;
std::memcpy(&quest, &g_code[at + 5], sizeof(quest));
accepts.emplace(name, quest);
}
}
// The givers' dialogs: "Dialog:<name>" labels with an answer (opcode
// 0x3c, {0x01 text, 0x01 label}) that accepts their quest.
std::map<std::string, int32_t> dialogs; // dialog name -> quest
static const char kDialog[] = "Dialog:";
for (const auto& [name, label] : byName) {
const int32_t quest = game::ScriptLabelQuest(label);
if (quest < 0 || name.compare(0, sizeof(kDialog) - 1, kDialog) != 0) {
continue;
}
bool offers = false;
ForEachInstruction(label, [&](size_t at, uint16_t opcode, uint16_t size) {
if (opcode != kScriptAnswer || g_code[at + 4] != game::kScriptArgString) {
return;
}
const char* text = reinterpret_cast<const char*>(&g_code[at + 5]);
const size_t textEnd = 5 + strnlen(text, size - 5) + 1;
if (textEnd + 1 < size && g_code[at + textEnd] == game::kScriptArgString) {
const auto accept = accepts.find(reinterpret_cast<const char*>(&g_code[at + textEnd + 1]));
offers = offers || (accept != accepts.end() && accept->second == quest);
}
});
if (offers) {
dialogs.emplace(name.substr(sizeof(kDialog) - 1), quest);
}
}
ScanSpawns(dialogs);
ScanVariables();
re::Log("questgiver: %u quest givers in %u bytes of script code", static_cast<unsigned>(g_givers.size()),
static_cast<unsigned>(g_code.size()));
}
// The living giver of `giver`, or null.
void* LivingGiver(const Giver& giver) {
const uint32_t id = game::ScriptObjectId(giver.scriptId);
void* creature = id && game::GetObjectById(id) ? game::GetCreature(id) : nullptr;
return creature && game::ObjectScriptId(creature) == giver.scriptId && game::IsAlive(creature) ? creature
: nullptr;
}
void ResetVariables(int32_t questId) {
for (const std::string& name : g_variables[questId]) {
std::vector<uint8_t> code(kInstructionHeader);
code.push_back(game::kScriptArgString);
code.insert(code.end(), name.begin(), name.end());
code.push_back(0);
code.push_back(game::kScriptArgInt);
code.insert(code.end(), 4, 0);
code[0] = static_cast<uint8_t>(game::kScriptSetVariable);
code[1] = 0;
code[2] = static_cast<uint8_t>(code.size());
code[3] = static_cast<uint8_t>(code.size() >> 8);
game::RunScriptInstruction(code.data());
re::Log("questgiver: quest %d variable '%s' = 0", questId, name.c_str());
}
}
// Restarts quest `index`: flags (available again), its book entry (failed,
// at the giver) and its variables.
void ResetQuest(int index) {
const int32_t id = game::QuestId(game::Quest(index));
questfail::Restart(index);
for (size_t i = 0; i < game::BookCount(); ++i) {
uint8_t* entry = game::BookEntry(i);
if (game::BookQuestId(entry) == id) {
*reinterpret_cast<int32_t*>(entry + game::kBookState) = game::kBookFailed;
}
}
ResetVariables(id);
}
void Spawn(int index) {
const int32_t id = game::QuestId(game::Quest(index));
const Giver& giver = g_givers[id];
const int result = game::RunScriptInstruction(giver.spawn.data());
void* creature = LivingGiver(giver);
re::Log("questgiver: quest %d giver Res:%u spawned again (%d): %s", id, giver.scriptId, result,
creature ? "back" : "NOT FOUND");
}
// Accepted and not done (kQuestTriggered alone: available, set when the hero
// enters the quest's region).
bool IsRunning(int index) {
const uint32_t flags = game::QuestFlags(game::Quest(index));
return (flags & game::kQuestInProgress) && !(flags & game::kQuestDone);
}
void RepairAfterLoad() {
for (const auto& [id, giver] : g_givers) {
const int index = game::QuestIndex(id);
if (index < 0 || !IsRunning(index) || LivingGiver(giver)) {
continue;
}
re::Log("questgiver: quest %d '%s' in progress without its giver Res:%u", id,
game::QuestName(game::Quest(index)), giver.scriptId);
ResetQuest(index);
Spawn(index);
}
}
void OnTick(uint8_t*) {
if (g_repairAfterLoad) {
g_repairAfterLoad = false;
Scan();
RepairAfterLoad();
}
for (auto it = g_pending.begin(); it != g_pending.end();) {
const bool gone = game::GetObjectById(it->bodyId) != it->body;
if (!gone && GetTickCount() - it->since < kBodyWaitMs) {
++it;
continue;
}
Spawn(it->questIndex);
it = g_pending.erase(it);
}
}
void OnLoaded() {
g_repairAfterLoad = true;
g_pending.clear();
}
struct Registration {
Registration() {
game::OnEngineTick(&OnTick);
game::OnSaveGameLoaded(&OnLoaded);
}
} g_registration;
} // namespace
// Script_RunLabel (0x46bd80, cdecl (label, object, flag), plain ret): a label
// of a running quest run for its dead giver (her death label) is skipped and
// the quest restarted.
uint32_t __cdecl Script_RunLabel(uint32_t label, uint8_t* object, int flag);
RE_HOOK(0x0046bd80, Script_RunLabel);
uint32_t __cdecl Script_RunLabel(uint32_t label, uint8_t* object, int flag) {
const uint8_t* labels = game::ScriptLabelsBegin();
if (object && labels && label < (game::ScriptLabelsEnd() - labels) / game::kScriptLabelSize) {
const int32_t questId = game::ScriptLabelQuest(labels + label * game::kScriptLabelSize);
const int index = questId >= 0 ? game::QuestIndex(questId) : -1;
void* creature = index >= 0 ? game::DynamicCast(object, game::kTypeObject, game::kTypeCreature) : nullptr;
if (creature && IsRunning(index) && !game::IsAlive(creature)) {
Scan();
const auto giver = g_givers.find(questId);
if (giver != g_givers.end() && game::ObjectScriptId(creature) == giver->second.scriptId) {
re::Log("questgiver: giver Res:%u of quest %d died; '%s' skipped, quest restarts",
giver->second.scriptId, questId,
game::ScriptLabelName(labels + label * game::kScriptLabelSize));
ResetQuest(index);
const uint32_t bodyId = *reinterpret_cast<uint32_t*>(object + game::kCreatureId);
g_pending.push_back({index, game::GetObjectById(bodyId), bodyId, GetTickCount()});
return 0;
}
}
}
return Script_RunLabel_original(label, object, flag);
}
} // namespace mods::questgiver