Auto-attack: bow shots chain at full speed, arts start by their cooldown; hang watchdog

- The shot end's button check (0x4def52) answers "held" for the auto-attack
  target, like the melee swing end.
- An art order counts as started when the art's cooldown rises, so bow arts
  and arts ordered mid-swing hand over to auto-attack.
- re/crash.cpp: a watchdog writes sacred_re_hang.dmp when the engine tick
  stops for 10 s while the game is in front.

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
This commit is contained in:
khallaandClaude Opus 5.5 committed 2026-10-18 01:29:29 +02:00
1 parent 432cac2ccf
commit 184c7058fd
6 files changed
+125 -13

No files matched your search

+38 -11
View File
@@ -58,6 +58,8 @@ struct CastWatch {
uint32_t target = 0; // an enemy, or 0
bool walked = false;
DWORD since = 0;
float cooldown = 0.0f; // the art's remaining cooldown when ordered
int32_t action = 0; // the hero's action when ordered (and while it lasts)
};
CastWatch g_cast;
std::unordered_map<uint16_t, bool> g_artRanged;
@@ -118,17 +120,24 @@ void WatchCast(void* hero) {
g_cast = {};
return;
}
switch (game::CurrentAction(hero)) {
case game::kActionWalk:
g_cast.walked = true;
break;
case game::kActionCastSpell:
case game::kActionCombatMove:
// Started: the art's cooldown began (spells, combat moves and bow arts
// alike), or for an art without one, the hero went into a new action
// other than walking (arts ordered during an attack or a cast were taken
// as started by that one, measured 2026-10-18).
const int32_t action = game::CurrentAction(hero);
const game::ArtCooldown cooldown = game::GetArtCooldown(hero, g_cast.art);
if (cooldown.remaining > g_cast.cooldown) {
OnCastStarted(hero);
break;
default:
break;
return;
}
g_cast.cooldown = cooldown.remaining; // regenerating meanwhile
if (action == game::kActionWalk) {
g_cast.walked = true;
} else if (action != g_cast.action && action != game::kActionIdle && cooldown.total <= 0.0f) {
OnCastStarted(hero);
return;
}
g_cast.action = action;
}
void OnTick(uint8_t* engine) {
@@ -233,8 +242,26 @@ char __fastcall AttackHeld(void* cursor, void*) {
return target && game::IsAlive(target) ? 1 : 0;
}
// A shot with a projectile weapon (action 5) checks the button the same way
// before the next shot (call at 0x4def52; shooter in esi, target creature in
// edi): held -> the next shot at a hostile target follows right away
// (measured: a held button fired for 2 s without new orders, auto-attack
// orders one shot per 0.67 s). The check after it (0x4def89) retargets to the
// creature under the cursor and is left alone.
constexpr uint32_t kShotEndButtonCheck = 0x004def52;
__declspec(naked) void ShotContinuesThunk() {
__asm {
push edi
push esi
call SwingContinues
ret
}
}
void Startup() {
re::RedirectCall(kSwingEndButtonCheck, kIsButtonHeld, reinterpret_cast<const void*>(&SwingContinuesThunk));
re::RedirectCall(kShotEndButtonCheck, kIsButtonHeld, reinterpret_cast<const void*>(&ShotContinuesThunk));
re::RedirectCall(kSwingButtonCheck, kIsButtonHeld, reinterpret_cast<const void*>(&AttackHeld));
}
RE_STARTUP(AutoAttackStartup, Startup);
@@ -265,7 +292,8 @@ void OnOrder(void* creature, const game::CreatureOrder& order, void*) {
break;
}
g_cast = {static_cast<uint16_t>(order.art), IsEnemy(hero, order.target) ? order.target : 0u, false,
GetTickCount()};
GetTickCount(), game::GetArtCooldown(hero, static_cast<uint16_t>(order.art)).remaining,
game::CurrentAction(hero)};
break;
}
}
@@ -278,4 +306,3 @@ struct OrderRegistration {
} // namespace
} // namespace mods::autoattack