From f5805fc740c320dd63d5c913b652829a8ed71e2a Mon Sep 17 00:00:00 2001 From: Jacobwasbeast Date: Fri, 6 Mar 2026 23:20:31 -0600 Subject: [PATCH] Fix mod item names by injecting strings directly into game's StringTable CMinecraftApp::GetString is inlined by the MSVC linker at call sites like Item::getHoverName, so the MinHook-based GetString hook never fires for item name lookups. The game's StringTable::getString(int) does a simple vector index lookup, and mod IDs (10000+) are beyond the vector size, returning empty strings. Fix: parse the GetString function's x64 machine code before hooking to locate the RIP-relative reference to app.m_stringTable, then after mods register their strings, resize m_stringsVec and inject mod strings at the correct indices. Also adds GetString fallback to CConsoleMinecraftApp variant and diagnostic logging. --- LegacyForgeRuntime/src/GameHooks.cpp | 19 ++- LegacyForgeRuntime/src/HookManager.cpp | 5 + LegacyForgeRuntime/src/ModStrings.cpp | 178 ++++++++++++++++++++++ LegacyForgeRuntime/src/ModStrings.h | 15 +- LegacyForgeRuntime/src/SymbolResolver.cpp | 6 + 5 files changed, 217 insertions(+), 6 deletions(-) diff --git a/LegacyForgeRuntime/src/GameHooks.cpp b/LegacyForgeRuntime/src/GameHooks.cpp index 6e27be3..db7428a 100644 --- a/LegacyForgeRuntime/src/GameHooks.cpp +++ b/LegacyForgeRuntime/src/GameHooks.cpp @@ -79,17 +79,29 @@ namespace GameHooks return Original_GetResourceAsStream ? Original_GetResourceAsStream(fileName) : nullptr; } + static bool s_loggedGetString = false; const wchar_t* Hooked_GetString(int id) { if (ModStrings::IsModId(id)) { const wchar_t* modStr = ModStrings::Get(id); - if (modStr) + LogUtil::Log("[LegacyForge] GetString(id=%d) -> mod '%ls'", id, + (modStr && modStr[0]) ? modStr : L""); + if (modStr && modStr[0]) return modStr; + return L"[Mod]"; + } + if (!s_loggedGetString && id > 0) + { + s_loggedGetString = true; + const wchar_t* r = Original_GetString ? Original_GetString(id) : L""; + LogUtil::Log("[LegacyForge] GetString(id=%d) -> vanilla '%ls' (first call sample)", id, r ? r : L""); + return r; } return Original_GetString ? Original_GetString(id) : L""; } + void Hooked_RunStaticCtors() { LogUtil::Log("[LegacyForge] Hook: RunStaticCtors -- calling PreInit"); @@ -99,6 +111,11 @@ namespace GameHooks LogUtil::Log("[LegacyForge] Hook: RunStaticCtors complete -- calling Init"); DotNetHost::CallInit(); + + // Inject mod strings directly into the game's StringTable vector. + // This is necessary because the compiler inlines GetString at call + // sites like Item::getHoverName, bypassing our GetString hook. + ModStrings::InjectAllIntoGameTable(); } void __fastcall Hooked_MinecraftTick(void* thisPtr, bool bFirst, bool bUpdateTextures) diff --git a/LegacyForgeRuntime/src/HookManager.cpp b/LegacyForgeRuntime/src/HookManager.cpp index 7fc86a6..4e31c3d 100644 --- a/LegacyForgeRuntime/src/HookManager.cpp +++ b/LegacyForgeRuntime/src/HookManager.cpp @@ -1,6 +1,7 @@ #include "HookManager.h" #include "GameHooks.h" #include "ModAtlas.h" +#include "ModStrings.h" #include "SymbolResolver.h" #include "CreativeInventory.h" #include "MainMenuOverlay.h" @@ -152,6 +153,9 @@ bool HookManager::Install(const SymbolResolver& symbols) if (symbols.pGetString) { + // Read GetString prologue bytes BEFORE MinHook overwrites them. + ModStrings::CaptureStringTableRef(symbols.pGetString); + if (MH_CreateHook(symbols.pGetString, reinterpret_cast(&GameHooks::Hooked_GetString), reinterpret_cast(&GameHooks::Original_GetString)) != MH_OK) @@ -164,6 +168,7 @@ bool HookManager::Install(const SymbolResolver& symbols) } } + if (symbols.pGetResourceAsStream) { if (MH_CreateHook(symbols.pGetResourceAsStream, diff --git a/LegacyForgeRuntime/src/ModStrings.cpp b/LegacyForgeRuntime/src/ModStrings.cpp index c2db87d..105e0ff 100644 --- a/LegacyForgeRuntime/src/ModStrings.cpp +++ b/LegacyForgeRuntime/src/ModStrings.cpp @@ -2,6 +2,7 @@ #include "LogUtil.h" #include #include +#include namespace ModStrings { @@ -9,6 +10,12 @@ namespace ModStrings static std::unordered_map s_strings; static int s_nextId = MOD_DESC_ID_BASE; + // ---- Game string table injection ---- + // Points to the field inside 'app' that holds the StringTable* pointer. + static void** s_pStringTableField = nullptr; + // Offset of m_stringsVec inside the StringTable object (found by heuristic scan). + static int s_vecOffset = -1; + void Register(int descriptionId, const wchar_t* value) { if (!value) return; @@ -36,4 +43,175 @@ namespace ModStrings { return id >= MOD_DESC_ID_BASE; } + + // ---- Machine code parsing to locate string table ---- + + void CaptureStringTableRef(void* pGetStringFunc) + { + if (!pGetStringFunc) return; + + const uint8_t* code = static_cast(pGetStringFunc); + LogUtil::Log("[LegacyForge] ModStrings: scanning GetString prologue at %p", pGetStringFunc); + + // Log first 32 bytes for diagnostics + char hexBuf[200]; + for (int i = 0; i < 32 && i < 200/3; i++) + sprintf(hexBuf + i * 3, "%02X ", code[i]); + LogUtil::Log("[LegacyForge] ModStrings: bytes: %s", hexBuf); + + // Search for RIP-relative memory accesses in first 80 bytes. + // GetString is: return app.m_stringTable->getString(iID); + // The compiler will load app.m_stringTable via a RIP-relative MOV. + for (int i = 0; i < 74; i++) + { + // REX.W prefix (0x48 or 0x4C for r8-r15) + if ((code[i] & 0xF8) != 0x48) continue; + uint8_t rex = code[i]; + + // MOV reg, [RIP + disp32] => 0x8B modrm + // LEA reg, [RIP + disp32] => 0x8D modrm + if (code[i + 1] != 0x8B && code[i + 1] != 0x8D) continue; + + uint8_t modrm = code[i + 2]; + // mod=00, rm=101 means [RIP + disp32] + if ((modrm & 0xC7) != 0x05) continue; + + int32_t disp = *reinterpret_cast(code + i + 3); + uintptr_t effectiveAddr = reinterpret_cast(code + i + 7) + disp; + + if (code[i + 1] == 0x8B) + { + // MOV reg, [RIP+disp] - directly loads a pointer value. + // This is likely loading app.m_stringTable (or &app if it's a pointer global). + // For a static member or global struct, the compiler often uses a direct + // RIP-relative MOV to load the m_stringTable pointer field. + s_pStringTableField = reinterpret_cast(effectiveAddr); + LogUtil::Log("[LegacyForge] ModStrings: MOV [RIP+disp] -> field at %p", s_pStringTableField); + break; + } + else // LEA + { + // LEA reg, [RIP+disp] -> &app + // Next instruction should load m_stringTable from app + offset + uintptr_t appAddr = effectiveAddr; + int j = i + 7; + // Look for MOV reg, [reg + disp8/disp32] + if (j + 3 < 80 && (code[j] & 0xF8) == 0x48 && code[j + 1] == 0x8B) + { + uint8_t modrm2 = code[j + 2]; + uint8_t mod2 = modrm2 >> 6; + if (mod2 == 1) + { + int8_t off = static_cast(code[j + 3]); + s_pStringTableField = reinterpret_cast(appAddr + off); + LogUtil::Log("[LegacyForge] ModStrings: LEA+MOV [reg+%d] -> field at %p", (int)off, s_pStringTableField); + break; + } + else if (mod2 == 2) + { + int32_t off = *reinterpret_cast(code + j + 3); + s_pStringTableField = reinterpret_cast(appAddr + off); + LogUtil::Log("[LegacyForge] ModStrings: LEA+MOV [reg+%d] -> field at %p", off, s_pStringTableField); + break; + } + } + } + } + + if (!s_pStringTableField) + LogUtil::Log("[LegacyForge] ModStrings: WARNING - could not locate string table reference"); + } + + // Heuristic: find the vector inside a StringTable object. + static std::vector* FindStringsVec(void* stringTable) + { + char* base = static_cast(stringTable); + + // StringTable layout (MSVC x64): + // +0x00: bool isStatic + // +0x08: unordered_map (size varies, typically 64 bytes) + // +0x??: vector m_stringsVec + // We scan pointer-aligned offsets looking for a valid vector triple. + for (int off = 0x08; off < 0x120; off += 8) + { + uintptr_t* ptrs = reinterpret_cast(base + off); + uintptr_t begin_ = ptrs[0]; + uintptr_t end_ = ptrs[1]; + uintptr_t cap_ = ptrs[2]; + + if (begin_ == 0 || end_ == 0 || cap_ == 0) continue; + if (begin_ > end_ || end_ > cap_) continue; + + size_t sizeBytes = end_ - begin_; + // sizeof(std::wstring) is 32 on MSVC x64 (SSO buffer + size + capacity) + if (sizeBytes == 0 || sizeBytes % 32 != 0) continue; + + size_t count = sizeBytes / 32; + if (count < 50 || count > 50000) continue; + + // Quick validation: first element should be a valid wstring + const std::wstring* first = reinterpret_cast(begin_); + if (first->size() > 0 && first->size() < 10000) + { + s_vecOffset = off; + LogUtil::Log("[LegacyForge] ModStrings: found m_stringsVec at StringTable+0x%X (%zu entries)", + off, count); + return reinterpret_cast*>(base + off); + } + } + return nullptr; + } + + void InjectAllIntoGameTable() + { + if (!s_pStringTableField) + { + LogUtil::Log("[LegacyForge] ModStrings: no string table ref - cannot inject"); + return; + } + + void* stringTable = *s_pStringTableField; + if (!stringTable) + { + LogUtil::Log("[LegacyForge] ModStrings: m_stringTable pointer is NULL"); + return; + } + + LogUtil::Log("[LegacyForge] ModStrings: StringTable object at %p", stringTable); + + std::vector* vec = FindStringsVec(stringTable); + if (!vec) + { + LogUtil::Log("[LegacyForge] ModStrings: FAILED to locate m_stringsVec in StringTable"); + return; + } + + std::lock_guard lock(s_mutex); + if (s_strings.empty()) + { + LogUtil::Log("[LegacyForge] ModStrings: no mod strings to inject"); + return; + } + + // Find the highest ID we need + int maxId = 0; + for (auto& kv : s_strings) + if (kv.first > maxId) maxId = kv.first; + + size_t oldSize = vec->size(); + if (static_cast(maxId) >= oldSize) + { + vec->resize(maxId + 1); + LogUtil::Log("[LegacyForge] ModStrings: resized m_stringsVec %zu -> %zu", oldSize, vec->size()); + } + + int count = 0; + for (auto& kv : s_strings) + { + (*vec)[kv.first] = kv.second; + count++; + } + + LogUtil::Log("[LegacyForge] ModStrings: injected %d mod strings into game string table", count); + } } diff --git a/LegacyForgeRuntime/src/ModStrings.h b/LegacyForgeRuntime/src/ModStrings.h index fd28c22..a02011f 100644 --- a/LegacyForgeRuntime/src/ModStrings.h +++ b/LegacyForgeRuntime/src/ModStrings.h @@ -4,11 +4,6 @@ #include #include -/// -/// Stores mod-registered display names for blocks and items. -/// Maps description IDs (allocated from MOD_DESC_ID_BASE) to wide strings. -/// Hooked into app.GetString() so the game displays mod names. -/// namespace ModStrings { constexpr int MOD_DESC_ID_BASE = 10000; @@ -17,4 +12,14 @@ namespace ModStrings const wchar_t* Get(int descriptionId); int AllocateId(); bool IsModId(int id); + + // Parse CMinecraftApp::GetString machine code to locate the game's + // string table pointer. Must be called BEFORE MinHook overwrites + // the function prologue. + void CaptureStringTableRef(void* pGetStringFunc); + + // After the string table is loaded (e.g. during PreInit), call this + // to inject all previously registered mod strings into the game's + // m_stringsVec so that inlined GetString calls find them. + void InjectAllIntoGameTable(); } diff --git a/LegacyForgeRuntime/src/SymbolResolver.cpp b/LegacyForgeRuntime/src/SymbolResolver.cpp index 4310d1a..3e47f53 100644 --- a/LegacyForgeRuntime/src/SymbolResolver.cpp +++ b/LegacyForgeRuntime/src/SymbolResolver.cpp @@ -77,6 +77,12 @@ bool SymbolResolver::ResolveGameFunctions() pMainMenuCustomDraw = Resolve(SYM_MAINMENU_CUSTOMDRAW); pPresent = Resolve(SYM_PRESENT); pGetString = Resolve(SYM_GET_STRING); + if (!pGetString) + { + pGetString = Resolve("?GetString@CConsoleMinecraftApp@@SAPEB_WH@Z"); + if (!pGetString) + PdbParser::DumpMatching("GetString"); + } pGetResourceAsStream = Resolve(SYM_GET_RESOURCE_AS_STREAM); pLoadUVs = Resolve(SYM_LOAD_UVS); pSimpleIconCtor = Resolve(SYM_SIMPLE_ICON_CTOR);