Files
sacred_k/src/re/crash.cpp
T
khallaandClaude Opus 5.5 184c7058fd 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>
2026-10-18 01:29:29 +02:00

186 lines
7.1 KiB
C++

// Crash report: on an unhandled exception, writes sacred_re_crash.txt (code,
// address as module + offset, registers, return addresses found on the stack)
// 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"
#include <cstdio>
#include <string>
#include <windows.h>
#include <dbghelp.h>
#include "re/hook.h"
#include "re/log.h"
namespace re {
namespace {
std::wstring g_dir;
LPTOP_LEVEL_EXCEPTION_FILTER g_gameFilter = nullptr;
volatile LONG g_reported = 0;
// "module+offset" for a code address.
void DescribeAddress(uintptr_t address, char* out, size_t size) {
HMODULE module = nullptr;
char path[MAX_PATH] = "?";
if (GetModuleHandleExA(GET_MODULE_HANDLE_EX_FLAG_FROM_ADDRESS | GET_MODULE_HANDLE_EX_FLAG_UNCHANGED_REFCOUNT,
reinterpret_cast<LPCSTR>(address), &module)) {
GetModuleFileNameA(module, path, MAX_PATH);
const char* name = strrchr(path, '\\');
snprintf(out, size, "%08x %s+%x", static_cast<unsigned>(address), name ? name + 1 : path,
static_cast<unsigned>(address - reinterpret_cast<uintptr_t>(module)));
} else {
snprintf(out, size, "%08x", static_cast<unsigned>(address));
}
}
bool IsCode(uintptr_t address) {
MEMORY_BASIC_INFORMATION info;
if (!VirtualQuery(reinterpret_cast<void*>(address), &info, sizeof(info)) || info.State != MEM_COMMIT) {
return false;
}
return (info.Protect & (PAGE_EXECUTE | PAGE_EXECUTE_READ | PAGE_EXECUTE_READWRITE)) != 0;
}
void WriteReport(EXCEPTION_POINTERS* info) {
FILE* file = _wfopen((g_dir + L"sacred_re_crash.txt").c_str(), L"w");
if (!file) {
return;
}
const EXCEPTION_RECORD& record = *info->ExceptionRecord;
const CONTEXT& c = *info->ContextRecord;
char where[MAX_PATH + 32];
DescribeAddress(reinterpret_cast<uintptr_t>(record.ExceptionAddress), where, sizeof(where));
fprintf(file, "exception %08lx at %s, thread %lu\n", record.ExceptionCode, where, GetCurrentThreadId());
if (record.ExceptionCode == EXCEPTION_ACCESS_VIOLATION && record.NumberParameters >= 2) {
fprintf(file, "%s address %08x\n", record.ExceptionInformation[0] ? "writing" : "reading",
static_cast<unsigned>(record.ExceptionInformation[1]));
}
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);
// 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);
int found = 0;
for (int i = 0; i < 4096 && found < 48; ++i) {
MEMORY_BASIC_INFORMATION mem;
if (!VirtualQuery(stack + i, &mem, sizeof(mem)) || mem.State != MEM_COMMIT) {
break;
}
const uintptr_t value = stack[i];
if (IsCode(value)) {
DescribeAddress(value, where, sizeof(where));
fprintf(file, " [esp+%04x] %s\n", i * 4, where);
++found;
}
}
fclose(file);
HANDLE dump = CreateFileW((g_dir + L"sacred_re_crash.dmp").c_str(), GENERIC_WRITE, 0, nullptr, CREATE_ALWAYS,
FILE_ATTRIBUTE_NORMAL, nullptr);
if (dump != INVALID_HANDLE_VALUE) {
MINIDUMP_EXCEPTION_INFORMATION exception = {GetCurrentThreadId(), info, FALSE};
MiniDumpWriteDump(GetCurrentProcess(), GetCurrentProcessId(), dump, MiniDumpWithIndirectlyReferencedMemory,
&exception, nullptr, nullptr);
CloseHandle(dump);
}
}
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);
}
return g_gameFilter ? g_gameFilter(info) : EXCEPTION_CONTINUE_SEARCH;
}
} // namespace
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
// The game's own filter goes behind ours.
LPTOP_LEVEL_EXCEPTION_FILTER WINAPI Hooked_SetUnhandledExceptionFilter(LPTOP_LEVEL_EXCEPTION_FILTER filter);
RE_HOOK_API(SetUnhandledExceptionFilter, Hooked_SetUnhandledExceptionFilter);
LPTOP_LEVEL_EXCEPTION_FILTER WINAPI Hooked_SetUnhandledExceptionFilter(LPTOP_LEVEL_EXCEPTION_FILTER filter) {
if (filter == &re::Filter) {
return Hooked_SetUnhandledExceptionFilter_original(filter);
}
LPTOP_LEVEL_EXCEPTION_FILTER previous = re::g_gameFilter;
re::g_gameFilter = filter;
return previous;
}