From 65ef99fc11962177d42251cc5e207f7af9f33713 Mon Sep 17 00:00:00 2001 From: Jacobwasbeast Date: Fri, 6 Mar 2026 15:16:06 -0600 Subject: [PATCH] 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. --- LegacyForge.Launcher/Program.cs | 2 +- LegacyForgeRuntime/src/GameHooks.cpp | 18 ++--- LegacyForgeRuntime/src/GameHooks.h | 21 +++-- LegacyForgeRuntime/src/HookManager.cpp | 20 +++-- LegacyForgeRuntime/src/SymbolResolver.cpp | 93 +++++++---------------- LegacyForgeRuntime/src/SymbolResolver.h | 8 +- 6 files changed, 67 insertions(+), 95 deletions(-) diff --git a/LegacyForge.Launcher/Program.cs b/LegacyForge.Launcher/Program.cs index 012dace..ca6f770 100644 --- a/LegacyForge.Launcher/Program.cs +++ b/LegacyForge.Launcher/Program.cs @@ -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)) diff --git a/LegacyForgeRuntime/src/GameHooks.cpp b/LegacyForgeRuntime/src/GameHooks.cpp index cf463d8..0baad5a 100644 --- a/LegacyForgeRuntime/src/GameHooks.cpp +++ b/LegacyForgeRuntime/src/GameHooks.cpp @@ -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); } } diff --git a/LegacyForgeRuntime/src/GameHooks.h b/LegacyForgeRuntime/src/GameHooks.h index 78c43b3..53d59f3 100644 --- a/LegacyForgeRuntime/src/GameHooks.h +++ b/LegacyForgeRuntime/src/GameHooks.h @@ -1,22 +1,29 @@ #pragma once #include -/// 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); } diff --git a/LegacyForgeRuntime/src/HookManager.cpp b/LegacyForgeRuntime/src/HookManager.cpp index 975e221..709e3f8 100644 --- a/LegacyForgeRuntime/src/HookManager.cpp +++ b/LegacyForgeRuntime/src/HookManager.cpp @@ -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(&GameHooks::Hooked_MinecraftDestroy), - reinterpret_cast(&GameHooks::Original_MinecraftDestroy)) != MH_OK) + if (MH_CreateHook(symbols.pExitGame, + reinterpret_cast(&GameHooks::Hooked_ExitGame), + reinterpret_cast(&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"); } } diff --git a/LegacyForgeRuntime/src/SymbolResolver.cpp b/LegacyForgeRuntime/src/SymbolResolver.cpp index c36e7f3..8ffd76f 100644 --- a/LegacyForgeRuntime/src/SymbolResolver.cpp +++ b/LegacyForgeRuntime/src/SymbolResolver.cpp @@ -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(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(UserContext); - if (strstr(pSymInfo->Name, ctx->targetName) != nullptr) - { - ctx->result = reinterpret_cast(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() diff --git a/LegacyForgeRuntime/src/SymbolResolver.h b/LegacyForgeRuntime/src/SymbolResolver.h index 3fb1eab..fa84de9 100644 --- a/LegacyForgeRuntime/src/SymbolResolver.h +++ b/LegacyForgeRuntime/src/SymbolResolver.h @@ -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;