Auto-attack: next target, melee/ranged rule after arts; chain range = cast range

- autoattack: when the target dies, the nearest enemy within close range
  (contact distance + a step) takes over; after an art the hero auto-attacks
  if the art was melee (walked up / cast close by), or ranged with a ranged
  weapon, or ranged with an enemy already in melee range; measured action
  codes and weapon classes in game/engine/orders.h
- chainlightning: jumps reach Blitzschlag's cast range (20 ray segments of
  30, the art code's hit check) via the shared NearestHostile search
- guards: burning bone render hook had the wrong signature (device plus a
  4x4 matrix by value, ret 0x44) and corrupted the stack when fire arrows hit
- crash reports follow the ebp chain before the raw stack scan

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
This commit is contained in:
khallaandClaude Opus 5.5 committed 2026-10-16 09:16:47 +02:00
1 parent 5338527b49
commit 16c2844400
8 files changed
+275 -60

No files matched your search

+33
View File
@@ -52,3 +52,36 @@ wind-up, 0.64 s attack, 0.43 s recovery).
with a held button; the tick only restarts the attack after casts. with a held button; the tick only restarts the attack after casts.
- Self / ally arts pointed at an enemy lose their target - Self / ally arts pointed at an enemy lose their target
(`src/mods/arttarget.cpp`) and therefore do not start auto-attack. (`src/mods/arttarget.cpp`) and therefore do not start auto-attack.
## Next target, and auto-attack after arts (2026-10-16)
User requests: when the target dies, go on with the next enemy close by;
after a melee art always auto-attack, after a ranged art only with a ranged
weapon.
Measured with a temporary log (distance hero → target when the hero started
the swing or cast, battlemage with a staff):
- +0x150 low word: 1 walking (also walking up to a target), 3 weapon attack,
4 casting a spell. The "1 art" above was the walk up; 8 (seen after it on
2026-10-15) is presumably a combat move. Names in `game/engine/orders.h`.
- Weapon swings started at 117..149 after walking up from up to 1382: the
game's contact distance (`0x4dbaa0`, mean of both body radii by object
type; heroes 110 with a weapon, 80 without).
- Spells (Blitzschlag 0x21, art 5) started at once from 647..803: no walk-up.
Their cast limit, if any, is beyond that.
Rules (`autoattack.cpp`):
- Close range = `ContactDistance(hero, hero) + 100` (≈ 210) with a melee
weapon; 600 with a ranged one (TODO: measure a ranged hero). Ranged weapon =
weapon class (`0x546df0`) 6 throwing, 7 bow, 12 crossbow, 13 pistol.
- The target died: the nearest living hostile creature within close range
(`game::NearestHostile`) becomes the target, else auto-attack stops.
- An art order (not a buff / ally / corpse art) is watched until the hero
starts it (action 4 or 8). It is melee if the hero walked first, ranged if
it starts farther away than contact distance + 100, else as remembered for
that art (default melee). Melee: auto-attack its enemy target, or the
nearest enemy close by. Ranged: the same with a ranged weapon; with a melee
weapon only an enemy already in close range (the art's target if it is,
else the nearest), otherwise auto-attack stops. Until the art starts, no attack orders are sent.
+12
View File
@@ -55,3 +55,15 @@ ends (a lone enemy is hit once). Two enemies: it bounces between them.
First version (bolts copied for every jump, not using the game's chain) First version (bolts copied for every jump, not using the game's chain)
replaced the same day: it doubled the jumps. replaced the same day: it doubled the jumps.
## Jump range = cast range (2026-10-16)
User request: a jump reaches as far as a cast. Blitzschlag's hit check is in
the art code (cCreature art switch `0x5222c0`, case 0x21): the ray from the
caster towards the target is laid out in segments of 30, at most 20, each on
passable ground (`0x636b60`); the bolt is created on the target only if the
ray reaches it, otherwise the visible ray just stops. So the cast range is
20 × 30 = 600. `CastRange()` in `chainlightning.cpp` returns it for every jump
(also the first hop of extra chains), instead of the game's jump search radius
(squared 100000, about 316). It is the one place to make the range dynamic
later. Jumps do not check the ground in between.
+30
View File
@@ -4,6 +4,7 @@
#include <vector> #include <vector>
#include "game/engine/objects.h"
#include "re/hook.h" #include "re/hook.h"
namespace game { namespace game {
@@ -33,4 +34,33 @@ void __fastcall Creature_ReceiveOrder(void* self, void* edx, CreatureOrder* orde
Creature_ReceiveOrder_original(self, edx, order); Creature_ReceiveOrder_original(self, edx, order);
} }
uint32_t NearestHostile(void* attacker, void* from, double maxDistance, uint32_t except, const uint32_t* avoid,
size_t avoidCount) {
uint32_t best = 0;
uint32_t bestDistance = 0;
bool bestFresh = false;
const int count = NearbyCreatureCount();
for (int i = 0; i < count; ++i) {
const uint32_t id = NearbyCreatureId(i);
if (id == except || IsPlayerHeroId(id) || !IsHostileTo(attacker, GetCreature(id))) {
continue;
}
const uint32_t distance = DistanceTo(from, id);
if (distance > maxDistance) {
continue;
}
bool fresh = true;
for (size_t k = 0; k < avoidCount; ++k) {
fresh = fresh && avoid[k] != id;
}
if (best && ((bestFresh && !fresh) || (bestFresh == fresh && distance >= bestDistance))) {
continue;
}
best = id;
bestDistance = distance;
bestFresh = fresh;
}
return best;
}
} // namespace game } // namespace game
+46 -2
View File
@@ -67,9 +67,20 @@ void OnCreatureOrder(CreatureOrderFn callback);
constexpr size_t kCreatureId = 0xc; constexpr size_t kCreatureId = 0xc;
constexpr size_t kCreatureX = 0x1c; constexpr size_t kCreatureX = 0x1c;
constexpr size_t kCreatureY = 0x20; constexpr size_t kCreatureY = 0x20;
// +0x150: low word the current action (0 none, 1 art, 3 attack, 8 ...), high // +0x150: low word the current action, high word its phase; 0x10000 when
// word its phase; 0x10000 when standing idle. // standing idle. Measured on the hero: 1 walking (also up to a target),
// 3 weapon attack, 4 casting a spell (2026-10-16), 6 dying, 8 an art seen
// after walking up (2026-10-15, presumably a combat move).
constexpr size_t kCreatureAction = 0x150; constexpr size_t kCreatureAction = 0x150;
enum CreatureAction : int32_t {
kActionIdle = 0,
kActionWalk = 1,
kActionAttack = 3,
kActionCastSpell = 4,
kActionDying = 6,
kActionCombatMove = 8,
};
inline int32_t CurrentAction(const void* creature);
// cCreature::IsAlive (0x548ef0): not dying (+0x150 == 6), not dead (+0xfc == // cCreature::IsAlive (0x548ef0): not dying (+0x150 == 6), not dead (+0xfc ==
// 9), health (+0x4d8) left. // 9), health (+0x4d8) left.
@@ -96,4 +107,37 @@ inline int32_t CreatureField(const void* creature, size_t offset) {
return *reinterpret_cast<const int32_t*>(static_cast<const uint8_t*>(creature) + offset); return *reinterpret_cast<const int32_t*>(static_cast<const uint8_t*>(creature) + offset);
} }
inline int32_t CurrentAction(const void* creature) {
return CreatureField(creature, kCreatureAction) & 0xffff;
}
// The distance at which two creatures touch (0x4dbaa0 on one, (the other)):
// the mean of their body radii by object type (heroes 110 with a weapon, 80
// without; monsters 70..480). A melee attacker swings from about there
// (measured: heroes started swings at 117..149).
inline int ContactDistance(void* creature, void* other) {
using Fn = int(__fastcall*)(void*, void*, void*);
return reinterpret_cast<Fn>(0x004dbaa0)(creature, nullptr, other);
}
// The creature's weapon class (0x546df0, (0)): from the weapons in its hands
// (+0x1d4 / +0x1d8, item type +0x1e), in the order of the motion sets:
// 0 bare hands, 1 one-handed, 2 two-handed, 3 two-handed axe, 4 staff,
// 5 dagger, 6 throwing, 7 bow, 8 two weapons, 9 / 10 blades, 11 whip,
// 12 crossbow, 13 pistol.
inline int WeaponClass(void* creature) {
using Fn = short(__fastcall*)(void*, void*, int);
return reinterpret_cast<Fn>(0x00546df0)(creature, nullptr, 0);
}
inline bool HasRangedWeapon(void* creature) {
const int c = WeaponClass(creature);
return c == 6 || c == 7 || c == 12 || c == 13;
}
// The nearest living creature hostile to `attacker` within `maxDistance` of
// `from` (a creature), other than `except` and other player heroes; one not
// in `avoid` if there is any. 0 if none.
uint32_t NearestHostile(void* attacker, void* from, double maxDistance, uint32_t except = 0,
const uint32_t* avoid = nullptr, size_t avoidCount = 0);
} // namespace game } // namespace game
+112 -12
View File
@@ -8,13 +8,22 @@
// becomes the hero's combat target if it is a living creature and not an ally. // becomes the hero's combat target if it is a living creature and not an ally.
// Every engine tick in which the hero is idle, no mouse button and no skill // Every engine tick in which the hero is idle, no mouse button and no skill
// key is held, the attack order a left click sends goes to the target again. // key is held, the attack order a left click sends goes to the target again.
// A walk order (mouse move, WASD, stop) or the target's death ends it. // A walk order (mouse move, WASD, stop) ends it. When the target dies the
// hero takes the nearest enemy within close range (weapon reach and a step).
// After an art: a melee art (the hero walked up to cast it, or casts it from
// close by) always leads to auto-attacking (its target, else the nearest
// enemy close by); a ranged art (cast from beyond close range) only with a
// ranged weapon, or with a melee weapon when an enemy is in melee range. Which arts are ranged is seen when the hero starts them and
// remembered per art. User requests of 2026-10-16.
// Between swings the hero does not go idle at all: the end-of-swing check for // Between swings the hero does not go idle at all: the end-of-swing check for
// a held mouse button is answered "held" for the auto-attack target. // a held mouse button is answered "held" for the auto-attack target.
// Measured with an order log, see docs/auto-attack.md. // Measured with an order log, see docs/auto-attack.md.
#include <atomic> #include <atomic>
#include <cstdint> #include <cstdint>
#include <unordered_map>
#include <windows.h>
#include "game/engine/arts.h" #include "game/engine/arts.h"
#include "game/engine/events.h" #include "game/engine/events.h"
@@ -30,11 +39,27 @@ namespace {
// After an attack order, wait this many ticks for the hero to start it before // After an attack order, wait this many ticks for the hero to start it before
// sending another one (it takes about 2 ticks). // sending another one (it takes about 2 ticks).
constexpr int kStartTicks = 5; constexpr int kStartTicks = 5;
constexpr int32_t kActionAttack = 3; // +0x150 low word // "Close range": where the hero touches a creature (game::ContactDistance,
// heroes swing from 117..149) plus a step.
constexpr int kStep = 100;
// TODO(measure): the reach of ranged weapons (no ranged hero measured yet).
constexpr int kRangedWeaponReach = 600;
// An art order that has not started after this long is forgotten.
constexpr DWORD kCastStartMs = 5000;
std::atomic<uint32_t> g_target{0}; std::atomic<uint32_t> g_target{0};
int g_waitTicks = 0; int g_waitTicks = 0;
// The hero's last art order until the hero starts it.
struct CastWatch {
uint16_t art = 0;
uint32_t target = 0; // an enemy, or 0
bool walked = false;
DWORD since = 0;
};
CastWatch g_cast;
std::unordered_map<uint16_t, bool> g_artRanged;
bool IsEnemy(void* hero, uint32_t id) { bool IsEnemy(void* hero, uint32_t id) {
if (id == 0 || id == static_cast<uint32_t>(game::CreatureField(hero, game::kCreatureId))) { if (id == 0 || id == static_cast<uint32_t>(game::CreatureField(hero, game::kCreatureId))) {
return false; return false;
@@ -43,20 +68,90 @@ bool IsEnemy(void* hero, uint32_t id) {
} }
bool Idle(void* creature) { bool Idle(void* creature) {
return (game::CreatureField(creature, game::kCreatureAction) & 0xffff) == 0; return game::CurrentAction(creature) == game::kActionIdle;
}
// How far the hero looks for its next target.
double CloseRange(void* hero) {
if (game::HasRangedWeapon(hero)) {
return kRangedWeaponReach;
}
return game::ContactDistance(hero, hero) + kStep;
}
uint32_t NearestCloseEnemy(void* hero, uint32_t except) {
return game::NearestHostile(hero, hero, CloseRange(hero), except);
}
// The hero started the watched art: decide what it auto-attacks afterwards.
void OnCastStarted(void* hero) {
const CastWatch cast = g_cast;
g_cast = {};
void* target = cast.target ? game::GetCreature(cast.target) : nullptr;
bool ranged = false;
if (cast.walked) {
g_artRanged[cast.art] = false;
} else if (target && static_cast<int>(game::DistanceTo(hero, cast.target)) >
game::ContactDistance(hero, target) + kStep) {
g_artRanged[cast.art] = ranged = true;
} else {
const auto known = g_artRanged.find(cast.art);
ranged = known != g_artRanged.end() && known->second;
}
if (ranged && !game::HasRangedWeapon(hero)) {
// A ranged art with a melee weapon: auto-attack only what is in melee
// range already (its target if that close, else the nearest enemy).
const bool targetClose = target && game::DistanceTo(hero, cast.target) <= CloseRange(hero);
g_target = targetClose ? cast.target : NearestCloseEnemy(hero, 0);
return;
}
g_target = target ? cast.target : NearestCloseEnemy(hero, 0);
}
void WatchCast(void* hero) {
if (!g_cast.since) {
return;
}
if (GetTickCount() - g_cast.since > kCastStartMs) {
g_cast = {};
return;
}
switch (game::CurrentAction(hero)) {
case game::kActionWalk:
g_cast.walked = true;
break;
case game::kActionCastSpell:
case game::kActionCombatMove:
OnCastStarted(hero);
break;
default:
break;
}
} }
void OnTick(uint8_t* engine) { void OnTick(uint8_t* engine) {
const uint32_t id = g_target.load(); void* hero = game::ControlledCreature(engine);
if (!hero) {
g_target = 0;
return;
}
WatchCast(hero);
if (g_cast.since) {
return; // the art decides once it starts
}
uint32_t id = g_target.load();
if (!id) { if (!id) {
return; return;
} }
void* hero = game::ControlledCreature(engine);
void* target = game::GetCreature(id); void* target = game::GetCreature(id);
if (!hero || !target || !game::IsAlive(target)) { if (!target || !game::IsAlive(target)) {
g_target = 0; id = NearestCloseEnemy(hero, id);
g_target = id;
target = id ? game::GetCreature(id) : nullptr;
if (!target) {
return; return;
} }
}
if (!Idle(hero)) { if (!Idle(hero)) {
g_waitTicks = 0; g_waitTicks = 0;
return; return;
@@ -129,7 +224,7 @@ char __fastcall AttackHeld(void* cursor, void*) {
const uint32_t id = g_target.load(); const uint32_t id = g_target.load();
void* hero = game::GetPlayerHero(); void* hero = game::GetPlayerHero();
if (!id || !hero || mods::skillkeys::KeyHeld() || if (!id || !hero || mods::skillkeys::KeyHeld() ||
(game::CreatureField(hero, game::kCreatureAction) & 0xffff) != kActionAttack) { game::CurrentAction(hero) != game::kActionAttack) {
return 0; return 0;
} }
void* target = game::GetCreature(id); void* target = game::GetCreature(id);
@@ -161,12 +256,17 @@ void OnOrder(void* creature, const game::CreatureOrder& order, void*) {
g_target = static_cast<uint32_t>(order.a); g_target = static_cast<uint32_t>(order.a);
} }
break; break;
case game::kOrderUseArt: case game::kOrderUseArt: {
if (IsEnemy(hero, order.target)) { // Buffs and the like (aimed at the hero or an ally) leave the target.
g_target = order.target; const auto launch = game::ArtLaunchType(static_cast<uint16_t>(order.art));
} if (launch == game::LaunchType::SelfOrAlly || launch == game::LaunchType::Corpse) {
break; break;
} }
g_cast = {static_cast<uint16_t>(order.art), IsEnemy(hero, order.target) ? order.target : 0u, false,
GetTickCount()};
break;
}
}
} }
struct OrderRegistration { struct OrderRegistration {
+18 -42
View File
@@ -11,7 +11,8 @@
// //
// This mod keeps all of that and decides: // This mod keeps all of that and decides:
// - hits per chain N = 3 + level / 5: the first bolt's [14]; // - hits per chain N = 3 + level / 5: the first bolt's [14];
// - the next target: the nearest enemy within the game's range that this // - the next target: the nearest enemy within Blitzschlag's cast range (see
// below; the game's own jump search used about 316) that this
// chain has not hit yet; else another enemy; else the current target again // chain has not hit yet; else another enemy; else the current target again
// (a lone enemy takes all N hits); // (a lone enemy takes all N hits);
// - chains B = 1 + level / 10: B - 1 more chains start at the first target, // - chains B = 1 + level / 10: B - 1 more chains start at the first target,
@@ -43,7 +44,16 @@ namespace {
constexpr uint32_t kBoltObjectType = 0x368; // 872 constexpr uint32_t kBoltObjectType = 0x368; // 872
constexpr uint32_t kJumpSearchCall = 0x005b469f; // in the bolt update constexpr uint32_t kJumpSearchCall = 0x005b469f; // in the bolt update
constexpr uint32_t kFindJumpTarget = 0x006005d0; // cObjectManager::FindJumpTarget constexpr uint32_t kFindJumpTarget = 0x006005d0; // cObjectManager::FindJumpTarget
constexpr uint32_t kJumpRange = 100000; // the game's argument (a squared distance) // Blitzschlag's cast range: the art code (cCreature art switch 0x5222c0,
// case 0x21) lays the ray from the caster towards the target in segments of
// 30, at most 20, each on passable ground (0x636b60); only if the target is
// reached is the bolt created on it. A jump may go as far as a cast. One place,
// so it can become dynamic (levels, attributes) later.
constexpr double kRaySegmentLength = 30.0;
constexpr int kRaySegments = 20;
double CastRange() {
return kRaySegmentLength * kRaySegments;
}
constexpr size_t kObjectOwnerPlayer = 0x39; // u8, copied from the caster like the game does constexpr size_t kObjectOwnerPlayer = 0x39; // u8, copied from the caster like the game does
constexpr size_t kCreaturePosition = 0x18; // 4 dwords, as effects are placed constexpr size_t kCreaturePosition = 0x18; // 4 dwords, as effects are placed
constexpr size_t kEventHitsLeft = 14; constexpr size_t kEventHitsLeft = 14;
@@ -84,44 +94,10 @@ unsigned ArtLevel(void* creature, uint16_t art) {
return entry ? entry[game::kCooldownLevel] + entry[game::kCooldownBonusLevels] : 0; return entry ? entry[game::kCooldownLevel] + entry[game::kCooldownBonusLevels] : 0;
} }
bool Contains(const std::vector<uint32_t>& ids, uint32_t id) { // The nearest living enemy of `caster` within the cast range of `from`, not
for (uint32_t i : ids) { // `current`; one not in `avoid` if there is any.
if (i == id) { uint32_t Nearest(void* caster, void* from, uint32_t current, const std::vector<uint32_t>& avoid) {
return true; return game::NearestHostile(caster, from, CastRange(), current, avoid.data(), avoid.size());
}
}
return false;
}
bool IsTarget(void* caster, uint32_t id) {
return !game::IsPlayerHeroId(id) && game::IsHostileTo(caster, game::GetCreature(id));
}
// The nearest living enemy of `caster` within `range` (squared) of `from`, not
// `current`; one not in `avoid` if there is any. 0 if none.
uint32_t Nearest(void* caster, void* from, uint32_t range, uint32_t current, const std::vector<uint32_t>& avoid) {
uint32_t best = 0;
double bestDistance = 0.0;
bool bestFresh = false;
const int count = game::NearbyCreatureCount();
for (int i = 0; i < count; ++i) {
const uint32_t id = game::NearbyCreatureId(i);
if (id == current || !IsTarget(caster, id)) {
continue;
}
const double d = static_cast<double>(game::DistanceTo(from, id));
if (d * d > static_cast<double>(range)) {
continue;
}
const bool fresh = !Contains(avoid, id);
if (best && ((bestFresh && !fresh) || (bestFresh == fresh && d >= bestDistance))) {
continue;
}
best = id;
bestDistance = d;
bestFresh = fresh;
}
return best;
} }
void Forget() { void Forget() {
@@ -194,7 +170,7 @@ void BeforeFxEvent(uint8_t* fx, uint32_t* event) {
// More chains: each starts with a bolt at its own first jump target. // More chains: each starts with a bolt at its own first jump target.
std::vector<uint32_t> taken{target}; std::vector<uint32_t> taken{target};
for (unsigned c = 1; c < chains && hits > 1; ++c) { for (unsigned c = 1; c < chains && hits > 1; ++c) {
const uint32_t next = Nearest(caster, first, kJumpRange, target, taken); const uint32_t next = Nearest(caster, first, target, taken);
const uint32_t branchTarget = next ? next : target; const uint32_t branchTarget = next ? next : target;
taken.push_back(branchTarget); taken.push_back(branchTarget);
auto branch = std::make_shared<Chain>(); auto branch = std::make_shared<Chain>();
@@ -252,7 +228,7 @@ uint32_t __fastcall NextTarget(void* manager, void*, const int32_t* position, ui
return 0; return 0;
} }
const std::shared_ptr<Chain> chain = it->second.chain; const std::shared_ptr<Chain> chain = it->second.chain;
uint32_t next = Nearest(casterCreature, from, range, current, chain->hit); uint32_t next = Nearest(casterCreature, from, current, chain->hit);
if (!next) { if (!next) {
next = current; // nobody else: hit the same enemy again next = current; // nobody else: hit the same enemy again
} }
+7 -3
View File
@@ -13,9 +13,13 @@ namespace game {
// pressed while walking; Windows reported sacred.exe+0x374e39). Who renders // pressed while walking; Windows reported sacred.exe+0x374e39). Who renders
// it without a device is not known yet (sacred_re_crash.txt will tell). With // it without a device is not known yet (sacred_re_crash.txt will tell). With
// no device there is nothing to draw. // no device there is nothing to draw.
void __fastcall BurningBone_Render(void* self, void* edx, void* device); // (device, a 4x4 matrix by value; ret 0x44).
struct Matrix4 {
float m[16];
};
void __fastcall BurningBone_Render(void* self, void* edx, void* device, Matrix4 transform);
RE_HOOK(0x00774e10, BurningBone_Render); RE_HOOK(0x00774e10, BurningBone_Render);
void __fastcall BurningBone_Render(void* self, void* edx, void* device) { void __fastcall BurningBone_Render(void* self, void* edx, void* device, Matrix4 transform) {
if (!device) { if (!device) {
static bool logged = false; static bool logged = false;
if (!logged) { if (!logged) {
@@ -24,7 +28,7 @@ void __fastcall BurningBone_Render(void* self, void* edx, void* device) {
} }
return; return;
} }
BurningBone_Render_original(self, edx, device); BurningBone_Render_original(self, edx, device, transform);
} }
} // namespace game } // namespace game
+17 -1
View File
@@ -62,7 +62,23 @@ void WriteReport(EXCEPTION_POINTERS* info) {
} }
fprintf(file, "eax %08lx ebx %08lx ecx %08lx edx %08lx esi %08lx edi %08lx ebp %08lx esp %08lx\n", c.Eax, c.Ebx, fprintf(file, "eax %08lx ebx %08lx ecx %08lx edx %08lx esi %08lx edi %08lx ebp %08lx esp %08lx\n", c.Eax, c.Ebx,
c.Ecx, c.Edx, c.Esi, c.Edi, c.Ebp, c.Esp); c.Ecx, c.Edx, c.Esi, c.Edi, c.Ebp, c.Esp);
fprintf(file, "return addresses on the stack (innermost first):\n"); // Frame pointer chain (our debug build and most of sacred.exe keep ebp).
fprintf(file, "frames (ebp chain):\n");
uintptr_t frame = c.Ebp;
for (int i = 0; i < 64 && frame; ++i) {
MEMORY_BASIC_INFORMATION mem;
if (!VirtualQuery(reinterpret_cast<void*>(frame), &mem, sizeof(mem)) || mem.State != MEM_COMMIT) {
break;
}
const auto* slots = reinterpret_cast<const uintptr_t*>(frame);
DescribeAddress(slots[1], where, sizeof(where));
fprintf(file, " %s\n", where);
if (slots[0] <= frame) {
break;
}
frame = slots[0];
}
fprintf(file, "code addresses on the stack (innermost first; may include data):\n");
const auto* stack = reinterpret_cast<const uintptr_t*>(c.Esp); const auto* stack = reinterpret_cast<const uintptr_t*>(c.Esp);
int found = 0; int found = 0;
for (int i = 0; i < 4096 && found < 48; ++i) { for (int i = 0; i < 4096 && found < 48; ++i) {