mirror of
https://github.com/Jacobwasbeast/LegacyWeaveLoader.git
synced 2026-06-08 14:03:09 +00:00
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.
This commit is contained in:
@@ -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"<null/empty>");
|
||||
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"<null>");
|
||||
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)
|
||||
|
||||
@@ -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<void*>(&GameHooks::Hooked_GetString),
|
||||
reinterpret_cast<void**>(&GameHooks::Original_GetString)) != MH_OK)
|
||||
@@ -164,6 +168,7 @@ bool HookManager::Install(const SymbolResolver& symbols)
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
if (symbols.pGetResourceAsStream)
|
||||
{
|
||||
if (MH_CreateHook(symbols.pGetResourceAsStream,
|
||||
|
||||
@@ -2,6 +2,7 @@
|
||||
#include "LogUtil.h"
|
||||
#include <vector>
|
||||
#include <cstring>
|
||||
#include <cstdint>
|
||||
|
||||
namespace ModStrings
|
||||
{
|
||||
@@ -9,6 +10,12 @@ namespace ModStrings
|
||||
static std::unordered_map<int, std::wstring> 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<const uint8_t*>(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<const int32_t*>(code + i + 3);
|
||||
uintptr_t effectiveAddr = reinterpret_cast<uintptr_t>(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<void**>(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<int8_t>(code[j + 3]);
|
||||
s_pStringTableField = reinterpret_cast<void**>(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<const int32_t*>(code + j + 3);
|
||||
s_pStringTableField = reinterpret_cast<void**>(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<wstring> inside a StringTable object.
|
||||
static std::vector<std::wstring>* FindStringsVec(void* stringTable)
|
||||
{
|
||||
char* base = static_cast<char*>(stringTable);
|
||||
|
||||
// StringTable layout (MSVC x64):
|
||||
// +0x00: bool isStatic
|
||||
// +0x08: unordered_map<wstring,wstring> (size varies, typically 64 bytes)
|
||||
// +0x??: vector<wstring> 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<uintptr_t*>(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<const std::wstring*>(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<std::vector<std::wstring>*>(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<std::wstring>* vec = FindStringsVec(stringTable);
|
||||
if (!vec)
|
||||
{
|
||||
LogUtil::Log("[LegacyForge] ModStrings: FAILED to locate m_stringsVec in StringTable");
|
||||
return;
|
||||
}
|
||||
|
||||
std::lock_guard<std::mutex> 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<size_t>(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);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -4,11 +4,6 @@
|
||||
#include <string>
|
||||
#include <mutex>
|
||||
|
||||
/// <summary>
|
||||
/// 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.
|
||||
/// </summary>
|
||||
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();
|
||||
}
|
||||
|
||||
@@ -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);
|
||||
|
||||
Reference in New Issue
Block a user