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:
1 parent
2cab6c216d
commit
7d8089c299
11 files changed
+872
-18
No files matched your search
@@ -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
|
||||
Reference in new issue
Block a user