Use exact decorated symbol names verified against Minecraft.Client.pdb

Verified all hook targets exist in the compiled game's PDB:
- ?MinecraftWorld_RunStaticCtors@@YAXXZ
- ?tick@Minecraft@@QEAAX_N0@Z
- ?init@Minecraft@@QEAAXXZ
- ?ExitGame@CConsoleMinecraftApp@@UEAAXXZ (replaces destroy which is inlined)

Minecraft::destroy/run_end/stop are not public symbols (likely inlined),
so the shutdown hook now targets CConsoleMinecraftApp::ExitGame instead.
This commit is contained in:
Jacobwasbeast
2026-03-06 15:16:06 -06:00
parent de22a24100
commit 65ef99fc11
6 changed files with 67 additions and 95 deletions

View File

@@ -25,7 +25,7 @@ class Program
if (string.IsNullOrEmpty(config.GameExePath) || !File.Exists(config.GameExePath))
{
Console.Write("Enter path to MinecraftLegacy.exe: ");
Console.Write("Enter path to Minecraft.Client.exe: ");
string? input = Console.ReadLine()?.Trim().Trim('"');
if (string.IsNullOrEmpty(input) || !File.Exists(input))

View File

@@ -5,18 +5,18 @@
namespace GameHooks
{
RunStaticCtors_fn Original_RunStaticCtors = nullptr;
MinecraftTick_fn Original_MinecraftTick = nullptr;
MinecraftInit_fn Original_MinecraftInit = nullptr;
MinecraftDestroy_fn Original_MinecraftDestroy = nullptr;
MinecraftTick_fn Original_MinecraftTick = nullptr;
MinecraftInit_fn Original_MinecraftInit = nullptr;
ExitGame_fn Original_ExitGame = nullptr;
void Hooked_RunStaticCtors()
{
printf("[LegacyForge] RunStaticCtors hook fired -- calling PreInit\n");
printf("[LegacyForge] Hook: RunStaticCtors -- calling PreInit\n");
DotNetHost::CallPreInit();
Original_RunStaticCtors();
printf("[LegacyForge] RunStaticCtors complete -- calling Init\n");
printf("[LegacyForge] Hook: RunStaticCtors complete -- calling Init\n");
DotNetHost::CallInit();
}
@@ -34,15 +34,15 @@ namespace GameHooks
{
Original_MinecraftInit(thisPtr);
printf("[LegacyForge] Minecraft::init complete -- calling PostInit\n");
printf("[LegacyForge] Hook: Minecraft::init complete -- calling PostInit\n");
DotNetHost::CallPostInit();
}
void __fastcall Hooked_MinecraftDestroy(void* thisPtr)
void __fastcall Hooked_ExitGame(void* thisPtr)
{
printf("[LegacyForge] Minecraft::destroy -- calling Shutdown\n");
printf("[LegacyForge] Hook: ExitGame -- calling Shutdown\n");
DotNetHost::CallShutdown();
Original_MinecraftDestroy(thisPtr);
Original_ExitGame(thisPtr);
}
}

View File

@@ -1,22 +1,29 @@
#pragma once
#include <cstdint>
/// Function pointer typedefs matching the game's function signatures.
/// On x64 MSVC, member functions use __fastcall-like convention (this in rcx).
/// Function pointer typedefs matching the game's actual function signatures.
/// x64 MSVC uses __fastcall-like convention (this in rcx, args in rdx/r8/r9).
///
/// Verified against Minecraft.Client.pdb:
/// ?MinecraftWorld_RunStaticCtors@@YAXXZ -- void __cdecl ()
/// ?tick@Minecraft@@QEAAX_N0@Z -- void __thiscall (bool, bool)
/// ?init@Minecraft@@QEAAXXZ -- void __thiscall ()
/// ?ExitGame@CConsoleMinecraftApp@@UEAAXXZ -- void __thiscall ()
typedef void (*RunStaticCtors_fn)();
typedef void (__fastcall *MinecraftTick_fn)(void* thisPtr, bool bFirst, bool bUpdateTextures);
typedef void (__fastcall *MinecraftInit_fn)(void* thisPtr);
typedef void (__fastcall *MinecraftDestroy_fn)(void* thisPtr);
typedef void (__fastcall *ExitGame_fn)(void* thisPtr);
namespace GameHooks
{
extern RunStaticCtors_fn Original_RunStaticCtors;
extern MinecraftTick_fn Original_MinecraftTick;
extern MinecraftInit_fn Original_MinecraftInit;
extern MinecraftDestroy_fn Original_MinecraftDestroy;
extern MinecraftTick_fn Original_MinecraftTick;
extern MinecraftInit_fn Original_MinecraftInit;
extern ExitGame_fn Original_ExitGame;
void Hooked_RunStaticCtors();
void __fastcall Hooked_MinecraftTick(void* thisPtr, bool bFirst, bool bUpdateTextures);
void __fastcall Hooked_MinecraftInit(void* thisPtr);
void __fastcall Hooked_MinecraftDestroy(void* thisPtr);
void __fastcall Hooked_ExitGame(void* thisPtr);
}

View File

@@ -22,6 +22,7 @@ bool HookManager::Install(const SymbolResolver& symbols)
printf("[LegacyForge] Failed to hook RunStaticCtors\n");
return false;
}
printf("[LegacyForge] Hooked RunStaticCtors\n");
}
// Hook Minecraft::tick
@@ -34,6 +35,7 @@ bool HookManager::Install(const SymbolResolver& symbols)
printf("[LegacyForge] Failed to hook Minecraft::tick\n");
return false;
}
printf("[LegacyForge] Hooked Minecraft::tick\n");
}
// Hook Minecraft::init
@@ -46,17 +48,21 @@ bool HookManager::Install(const SymbolResolver& symbols)
printf("[LegacyForge] Failed to hook Minecraft::init\n");
return false;
}
printf("[LegacyForge] Hooked Minecraft::init\n");
}
// Hook Minecraft::destroy
if (symbols.pMinecraftDestroy)
// Hook CConsoleMinecraftApp::ExitGame (optional -- for graceful shutdown)
if (symbols.pExitGame)
{
if (MH_CreateHook(symbols.pMinecraftDestroy,
reinterpret_cast<void*>(&GameHooks::Hooked_MinecraftDestroy),
reinterpret_cast<void**>(&GameHooks::Original_MinecraftDestroy)) != MH_OK)
if (MH_CreateHook(symbols.pExitGame,
reinterpret_cast<void*>(&GameHooks::Hooked_ExitGame),
reinterpret_cast<void**>(&GameHooks::Original_ExitGame)) != MH_OK)
{
printf("[LegacyForge] Failed to hook Minecraft::destroy\n");
return false;
printf("[LegacyForge] Warning: Failed to hook ExitGame (shutdown hook unavailable)\n");
}
else
{
printf("[LegacyForge] Hooked ExitGame\n");
}
}

View File

@@ -4,14 +4,22 @@
#pragma comment(lib, "dbghelp.lib")
// Exact MSVC-decorated symbol names from the game's PDB.
// These are verified against the actual Minecraft.Client.pdb.
static const char* SYM_RUN_STATIC_CTORS = "?MinecraftWorld_RunStaticCtors@@YAXXZ";
static const char* SYM_MINECRAFT_TICK = "?tick@Minecraft@@QEAAX_N0@Z";
static const char* SYM_MINECRAFT_INIT = "?init@Minecraft@@QEAAXXZ";
static const char* SYM_EXIT_GAME = "?ExitGame@CConsoleMinecraftApp@@UEAAXXZ";
bool SymbolResolver::Initialize()
{
m_process = GetCurrentProcess();
SymSetOptions(SYMOPT_UNDNAME | SYMOPT_DEFERRED_LOADS | SYMOPT_LOAD_LINES | SYMOPT_DEBUG);
SymSetOptions(SYMOPT_DEFERRED_LOADS | SYMOPT_LOAD_LINES | SYMOPT_DEBUG);
if (!SymInitialize(m_process, nullptr, TRUE))
{
printf("[LegacyForge] SymInitialize failed (error %lu)\n", GetLastError());
return false;
}
@@ -19,7 +27,7 @@ bool SymbolResolver::Initialize()
return true;
}
void* SymbolResolver::Resolve(const char* functionName)
void* SymbolResolver::Resolve(const char* decoratedName)
{
if (!m_initialized) return nullptr;
@@ -30,85 +38,36 @@ void* SymbolResolver::Resolve(const char* functionName)
symbol->SizeOfStruct = sizeof(SYMBOL_INFO);
symbol->MaxNameLen = MAX_SYM_NAME;
if (SymFromName(m_process, functionName, symbol))
if (SymFromName(m_process, decoratedName, symbol))
{
return reinterpret_cast<void*>(symbol->Address);
}
printf("[LegacyForge] SymFromName failed for '%s' (error %lu)\n", decoratedName, GetLastError());
return nullptr;
}
struct EnumContext
{
const char* targetName;
void* result;
};
static BOOL CALLBACK EnumSymbolsCallback(PSYMBOL_INFO pSymInfo, ULONG SymbolSize, PVOID UserContext)
{
auto* ctx = static_cast<EnumContext*>(UserContext);
if (strstr(pSymInfo->Name, ctx->targetName) != nullptr)
{
ctx->result = reinterpret_cast<void*>(pSymInfo->Address);
return FALSE;
}
return TRUE;
}
bool SymbolResolver::ResolveGameFunctions()
{
// MinecraftWorld_RunStaticCtors is a free function (not mangled in complex ways)
pRunStaticCtors = Resolve("MinecraftWorld_RunStaticCtors");
if (!pRunStaticCtors)
{
// Try with wildcard enumeration
EnumContext ctx = { "MinecraftWorld_RunStaticCtors", nullptr };
SymEnumSymbols(m_process, 0, "*MinecraftWorld_RunStaticCtors*", EnumSymbolsCallback, &ctx);
pRunStaticCtors = ctx.result;
}
pRunStaticCtors = Resolve(SYM_RUN_STATIC_CTORS);
pMinecraftTick = Resolve(SYM_MINECRAFT_TICK);
pMinecraftInit = Resolve(SYM_MINECRAFT_INIT);
pExitGame = Resolve(SYM_EXIT_GAME);
// Minecraft::tick -- MSVC mangles this. Try undecorated name first.
pMinecraftTick = Resolve("Minecraft::tick");
if (!pMinecraftTick)
{
EnumContext ctx = { "Minecraft::tick", nullptr };
SymEnumSymbols(m_process, 0, "*Minecraft*tick*", EnumSymbolsCallback, &ctx);
pMinecraftTick = ctx.result;
}
if (pRunStaticCtors) printf("[LegacyForge] RunStaticCtors @ %p\n", pRunStaticCtors);
else printf("[LegacyForge] MISSING: MinecraftWorld_RunStaticCtors\n");
// Minecraft::init
pMinecraftInit = Resolve("Minecraft::init");
if (!pMinecraftInit)
{
EnumContext ctx = { "Minecraft::init", nullptr };
SymEnumSymbols(m_process, 0, "*Minecraft*init*", EnumSymbolsCallback, &ctx);
pMinecraftInit = ctx.result;
}
if (pMinecraftTick) printf("[LegacyForge] Minecraft::tick @ %p\n", pMinecraftTick);
else printf("[LegacyForge] MISSING: Minecraft::tick\n");
// Minecraft::destroy
pMinecraftDestroy = Resolve("Minecraft::destroy");
if (!pMinecraftDestroy)
{
EnumContext ctx = { "Minecraft::destroy", nullptr };
SymEnumSymbols(m_process, 0, "*Minecraft*destroy*", EnumSymbolsCallback, &ctx);
pMinecraftDestroy = ctx.result;
}
if (pMinecraftInit) printf("[LegacyForge] Minecraft::init @ %p\n", pMinecraftInit);
else printf("[LegacyForge] MISSING: Minecraft::init\n");
bool allResolved = pRunStaticCtors && pMinecraftTick && pMinecraftInit && pMinecraftDestroy;
if (pExitGame) printf("[LegacyForge] ExitGame @ %p\n", pExitGame);
else printf("[LegacyForge] MISSING: CConsoleMinecraftApp::ExitGame\n");
if (pRunStaticCtors) printf("[LegacyForge] Resolved RunStaticCtors @ %p\n", pRunStaticCtors);
else printf("[LegacyForge] MISSING: MinecraftWorld_RunStaticCtors\n");
if (pMinecraftTick) printf("[LegacyForge] Resolved Minecraft::tick @ %p\n", pMinecraftTick);
else printf("[LegacyForge] MISSING: Minecraft::tick\n");
if (pMinecraftInit) printf("[LegacyForge] Resolved Minecraft::init @ %p\n", pMinecraftInit);
else printf("[LegacyForge] MISSING: Minecraft::init\n");
if (pMinecraftDestroy) printf("[LegacyForge] Resolved Minecraft::destroy @ %p\n", pMinecraftDestroy);
else printf("[LegacyForge] MISSING: Minecraft::destroy\n");
return allResolved;
// RunStaticCtors, tick, and init are critical. ExitGame is optional (graceful shutdown).
return pRunStaticCtors && pMinecraftTick && pMinecraftInit;
}
void SymbolResolver::Cleanup()

View File

@@ -12,10 +12,10 @@ public:
void* Resolve(const char* functionName);
void* pRunStaticCtors = nullptr;
void* pMinecraftTick = nullptr;
void* pMinecraftInit = nullptr;
void* pMinecraftDestroy = nullptr;
void* pRunStaticCtors = nullptr; // MinecraftWorld_RunStaticCtors
void* pMinecraftTick = nullptr; // Minecraft::tick(bool, bool)
void* pMinecraftInit = nullptr; // Minecraft::init()
void* pExitGame = nullptr; // CConsoleMinecraftApp::ExitGame()
private:
HANDLE m_process = nullptr;