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:
1 parent
5338527b49
commit
16c2844400
8 files changed
+275
-60
No files matched your search
@@ -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.
|
||||||
@@ -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.
|
||||||
@@ -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
|
||||||
@@ -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
@@ -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,19 +68,89 @@ 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);
|
||||||
return;
|
g_target = id;
|
||||||
|
target = id ? game::GetCreature(id) : nullptr;
|
||||||
|
if (!target) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
if (!Idle(hero)) {
|
if (!Idle(hero)) {
|
||||||
g_waitTicks = 0;
|
g_waitTicks = 0;
|
||||||
@@ -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;
|
||||||
}
|
}
|
||||||
|
g_cast = {static_cast<uint16_t>(order.art), IsEnemy(hero, order.target) ? order.target : 0u, false,
|
||||||
|
GetTickCount()};
|
||||||
break;
|
break;
|
||||||
}
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
struct OrderRegistration {
|
struct OrderRegistration {
|
||||||
|
|||||||
+18
-42
@@ -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
@@ -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
@@ -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) {
|
||||||
|
|||||||
Reference in new issue
Block a user