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

+51
View File
@@ -3,6 +3,11 @@
// and sacred_re_crash.dmp next to the DLL. The game may install its own
// filter later; SetUnhandledExceptionFilter is hooked so ours stays first and
// passes the crash on to the game's afterwards.
//
// Hangs: a watchdog thread writes sacred_re_hang.dmp (all threads) when the
// engine tick (re::EngineHeartbeat) has stopped for kHangMs while the game
// window is in front; once per hang. The game freezing on F2 while walking
// (2026-10-18) left no report.
#include "re/crash.h"
@@ -105,6 +110,45 @@ void WriteReport(EXCEPTION_POINTERS* info) {
}
}
constexpr DWORD kHangMs = 10000;
volatile LONG g_lastBeat = 0; // GetTickCount of the last engine tick, 0: no game running
bool GameInFront() {
DWORD process = 0;
GetWindowThreadProcessId(GetForegroundWindow(), &process);
return process == GetCurrentProcessId();
}
void WriteHangDump() {
HANDLE dump = CreateFileW((g_dir + L"sacred_re_hang.dmp").c_str(), GENERIC_WRITE, 0, nullptr, CREATE_ALWAYS,
FILE_ATTRIBUTE_NORMAL, nullptr);
if (dump != INVALID_HANDLE_VALUE) {
MiniDumpWriteDump(GetCurrentProcess(), GetCurrentProcessId(), dump,
static_cast<MINIDUMP_TYPE>(MiniDumpWithIndirectlyReferencedMemory | MiniDumpWithThreadInfo),
nullptr, nullptr, nullptr);
CloseHandle(dump);
}
}
DWORD WINAPI Watchdog(void*) {
bool reported = false;
for (;;) {
Sleep(1000);
const DWORD beat = static_cast<DWORD>(g_lastBeat);
// Signed: a tick may land between reading the beat and the clock.
if (beat == 0 || static_cast<LONG>(GetTickCount() - beat) < static_cast<LONG>(kHangMs)) {
reported = false;
continue;
}
if (!reported && GameInFront()) {
reported = true;
Log("hang: no engine tick for %lu ms, writing sacred_re_hang.dmp", GetTickCount() - beat);
WriteHangDump();
Log("hang: dump written");
}
}
}
LONG WINAPI Filter(EXCEPTION_POINTERS* info) {
if (InterlockedExchange(&g_reported, 1) == 0) {
WriteReport(info);
@@ -117,6 +161,13 @@ LONG WINAPI Filter(EXCEPTION_POINTERS* info) {
void InstallCrashHandler(const std::wstring& dir) {
g_dir = dir;
g_gameFilter = SetUnhandledExceptionFilter(&Filter);
if (HANDLE thread = CreateThread(nullptr, 0, &Watchdog, nullptr, 0, nullptr)) {
CloseHandle(thread);
}
}
void EngineHeartbeat(bool running) {
InterlockedExchange(&g_lastBeat, running ? static_cast<LONG>(GetTickCount() | 1) : 0);
}
} // namespace re
+5 -1
View File
@@ -4,7 +4,11 @@
namespace re {
// Writes sacred_re_crash.txt / .dmp into `dir` when the game crashes.
// Writes sacred_re_crash.txt / .dmp into `dir` when the game crashes, and
// sacred_re_hang.dmp when the engine stops ticking (see EngineHeartbeat).
void InstallCrashHandler(const std::wstring& dir);
// The engine ticked (running), or the game ended (not running).
void EngineHeartbeat(bool running);
} // namespace re