Followers: escorts left behind are teleported to the hero

A follower that fell more than 40 cells behind was removed when its sector
was switched off, and the game ran its death label (the husband of "Die
Sklaven der Orks"). Every 500 ms followers of the local hero that far away
are teleported to him with cCreature::Teleport.

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 23:39:21 +01:00
1 parent 42b99526a4
commit e539cab604
5 files changed
+107 -15

No files matched your search

+18 -5
View File
@@ -97,16 +97,29 @@ inline void AddFollower(void* leader, const FollowerEntry& entry) {
reinterpret_cast<Fn>(0x0054b070)(leader, nullptr, &entry);
}
inline bool HasFollower(const void* leader, uint32_t id) {
// The ids on the leader's follower list.
template <typename Fn>
void ForEachFollower(const void* leader, Fn&& fn) {
const auto* creature = static_cast<const uint8_t*>(leader);
const auto* begin = *reinterpret_cast<const uint8_t* const*>(creature + kCreatureFollowersBegin);
const auto* end = *reinterpret_cast<const uint8_t* const*>(creature + kCreatureFollowersEnd);
for (const uint8_t* entry = begin; entry && entry < end; entry += kFollowerEntrySize) {
if (*reinterpret_cast<const uint32_t*>(entry) == id) {
return true;
}
fn(*reinterpret_cast<const uint32_t*>(entry));
}
return false;
}
inline bool HasFollower(const void* leader, uint32_t id) {
bool found = false;
ForEachFollower(leader, [&](uint32_t follower) { found = found || follower == id; });
return found;
}
// cCreature::Teleport (0x54d840, thiscall (cell x, cell y, 0, 1), ret 0x10):
// as the portals move the hero (EE_USEPORTAL 0x6197c3); the game's log line
// "Teleport (type): <type>, ... (x,y)".
inline void Teleport(void* creature, int32_t cellX, int32_t cellY) {
using Fn = void(__fastcall*)(void*, void*, int32_t, int32_t, int32_t, int32_t);
reinterpret_cast<Fn>(0x0054d840)(creature, nullptr, cellX, cellY, 0, 1);
}
// The walk path (measured 2026-10-18): waypoints {x, y} at +0x2ed + i * 8,
+1
View File
@@ -5,6 +5,7 @@
#include <cstddef>
#include <cstdint>
#include <cstring>
#include <initializer_list>
namespace game {
+65 -10
View File
@@ -1,15 +1,30 @@
// 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.
// Escorted NPCs stay with the hero (2026-10-26, docs/quests.md).
//
// On the follower list: "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.
//
// Never left behind: in "Die Sklaven der Orks" (2412) the husband fell
// behind (the game's own catch-up teleport moved him once), and when the hero
// walked on, his sector was switched off: the game removed him and ran his
// spawn's death label (drop2412: quest variable 5, a token item dropped where
// he was), with him alive (measured 2026-10-26 in the log: his object id
// reused right after). Sectors are switched off at 2 sectors (65 tiles and
// more) from the hero's; the mod teleports every follower of the local hero
// that is more than kLeftBehindCells away to the hero (cCreature::Teleport,
// as the portals), checked every kCheckMs.
#include <windows.h>
#include <cstdint>
#include <cstdlib>
#include "game/engine/events.h"
#include "game/engine/objects.h"
@@ -20,6 +35,8 @@ namespace mods::followers {
namespace {
constexpr int kTicksAfterStart = 30;
constexpr int32_t kLeftBehindCells = 40;
constexpr DWORD kCheckMs = 500;
// 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,
@@ -27,11 +44,20 @@ constexpr int kTicksAfterStart = 30;
constexpr int32_t kStatusTagAlong = 0x400;
int g_ticksToCheck = -1;
DWORD g_lastCheck = 0;
int32_t Field(const void* creature, size_t offset) {
return *reinterpret_cast<const int32_t*>(static_cast<const uint8_t*>(creature) + offset);
}
const game::MapPosition& Position(void* object) {
return *reinterpret_cast<const game::MapPosition*>(static_cast<uint8_t*>(object) + game::kObjectPosition);
}
int32_t Cell(int32_t units) {
return static_cast<int32_t>(static_cast<float>(units) / game::kMapCellSize);
}
void RepairFollowers() {
for (uint32_t id = 1; id < game::ObjectIdEnd(); ++id) {
if (game::IsPlayerHeroId(id) || !game::GetObjectById(id)) {
@@ -52,10 +78,39 @@ void RepairFollowers() {
}
}
// Followers of the local hero too far behind are teleported to him.
void BringAlong() {
void* hero = game::GetPlayerHero();
if (!hero) {
return;
}
const game::MapPosition& at = Position(hero);
const int32_t heroX = Cell(at.x), heroY = Cell(at.y);
game::ForEachFollower(hero, [&](uint32_t id) {
void* follower = game::GetObjectById(id) ? game::GetCreature(id) : nullptr;
if (!follower || !game::IsAlive(follower)) {
return;
}
const game::MapPosition& position = Position(follower);
const int32_t x = Cell(position.x), y = Cell(position.y);
if (position.level != at.level ||
(std::abs(x - heroX) <= kLeftBehindCells && std::abs(y - heroY) <= kLeftBehindCells)) {
return;
}
re::Log("followers: %x left behind at %d,%d, brought to the hero at %d,%d", id, x, y, heroX, heroY);
game::Teleport(follower, heroX + 1, heroY + 1);
});
}
void OnTick(uint8_t*) {
if (g_ticksToCheck > 0 && --g_ticksToCheck == 0) {
RepairFollowers();
}
const DWORD now = GetTickCount();
if (g_ticksToCheck <= 0 && now - g_lastCheck >= kCheckMs) {
g_lastCheck = now;
BringAlong();
}
}
void OnStart() {