diff --git a/README.md b/README.md index c970cad..5d34ced 100644 --- a/README.md +++ b/README.md @@ -9,7 +9,7 @@ A runtime mod loader for Minecraft Legacy Edition (Xbox 360 / PS3 / Windows 64-b - **Function hooking** -- MinHook detours on game lifecycle functions (init, tick, static constructors, rendering) - **Full .NET hosting** -- .NET 8 CoreCLR is loaded inside the game process via hostfxr; mods are standard C# class libraries - **Block and item registration** -- Create real game objects (Tile, TileItem, Item) by calling the game's own constructors through resolved PDB symbols -- **Dynamic texture atlas merging** -- Mod textures are stitched into the game's `terrain.png` and `items.png` atlases at runtime using empty cells +- **Dynamic texture atlas merging** -- Mod textures are merged into copies of the game's atlases at runtime using empty cells; vanilla game files are never touched - **Creative inventory injection** -- Mod items appear in the correct creative tabs with proper pagination - **Localized display names** -- Mod strings are injected directly into the game's `StringTable` vector, bypassing inlined `GetString` calls - **Crash reporting** -- Vectored exception handler produces detailed crash logs with register dumps, symbolicated stack traces, and loaded module lists @@ -342,8 +342,8 @@ The runtime opens the game's PDB file and parses it using [raw_pdb](https://gith 2. The vanilla `terrain.png` (16x32 grid) and `items.png` (16x16 grid) are loaded via stb_image 3. Empty cells are identified by checking for fully transparent pixels 4. Mod textures are placed into empty cells -5. The merged atlas is written to a temp directory and swapped in before `Minecraft::init` -6. After the game loads textures, the original files are restored from backup +5. The merged atlas is written to `mods/ModLoader/generated/` -- **vanilla game files are never modified** +6. A `CreateFileW` hook temporarily redirects the game's file opens to the merged atlases during init, then is removed once textures are loaded into GPU memory 7. `SimpleIcon` objects are created for each mod texture with correct UV coordinates ### String Table Injection diff --git a/WeaveLoaderRuntime/src/GameHooks.cpp b/WeaveLoaderRuntime/src/GameHooks.cpp index fe6ff5b..585504c 100644 --- a/WeaveLoaderRuntime/src/GameHooks.cpp +++ b/WeaveLoaderRuntime/src/GameHooks.cpp @@ -157,12 +157,15 @@ namespace GameHooks ModAtlas::BuildAtlases(base + "mods", gameResPath); atlas_done: - // Overwrite the game's atlas PNGs with our merged versions BEFORE init - // loads them. The originals are backed up and restored after init. - ModAtlas::InstallAtlasFiles(gameResPath); + // Redirect terrain.png/items.png file opens to our merged atlases + // so the game loads mod textures without modifying vanilla files. + ModAtlas::InstallCreateFileHook(gameResPath); Original_MinecraftInit(thisPtr); + // Textures are loaded into GPU memory now; remove the redirect. + ModAtlas::RemoveCreateFileHook(); + // After init, vanilla icons have their source-image pointer (field_0x48) // fully populated. Copy it to our mod icons so getSourceHeight() works. ModAtlas::FixupModIcons(); diff --git a/WeaveLoaderRuntime/src/ModAtlas.cpp b/WeaveLoaderRuntime/src/ModAtlas.cpp index 13b5970..1277a7e 100644 --- a/WeaveLoaderRuntime/src/ModAtlas.cpp +++ b/WeaveLoaderRuntime/src/ModAtlas.cpp @@ -1,6 +1,7 @@ #include "ModAtlas.h" #include "LogUtil.h" #include +#include #include #include #include @@ -281,14 +282,103 @@ namespace ModAtlas static std::unordered_map s_modIcons; static RegisterIcon_fn s_originalRegisterIcon = nullptr; - static std::string s_gameResPath; - static std::string s_backupTerrainPath; - static std::string s_backupItemsPath; // Per-atlas-type textureMap pointers, saved during CreateModIcons for FixupModIcons. static void* s_terrainTextureMap = nullptr; static void* s_itemsTextureMap = nullptr; + // CreateFileW hook: redirect game file opens to merged atlases + static std::wstring s_mergedTerrainW; + static std::wstring s_mergedItemsW; + static std::wstring s_vanillaTerrainW; + static std::wstring s_vanillaItemsW; + + typedef HANDLE (WINAPI *CreateFileW_fn)(LPCWSTR, DWORD, DWORD, LPSECURITY_ATTRIBUTES, DWORD, DWORD, HANDLE); + static CreateFileW_fn s_originalCreateFileW = nullptr; + + static bool EndsWith(const wchar_t* path, const wchar_t* suffix) + { + size_t pathLen = wcslen(path); + size_t suffLen = wcslen(suffix); + if (suffLen > pathLen) return false; + return _wcsicmp(path + pathLen - suffLen, suffix) == 0; + } + + static HANDLE WINAPI Hooked_CreateFileW( + LPCWSTR lpFileName, DWORD dwDesiredAccess, DWORD dwShareMode, + LPSECURITY_ATTRIBUTES lpSecurityAttributes, DWORD dwCreationDisposition, + DWORD dwFlagsAndAttributes, HANDLE hTemplateFile) + { + if (lpFileName && s_hasModTextures) + { + if (!s_mergedTerrainW.empty() && EndsWith(lpFileName, L"\\terrain.png")) + { + LogUtil::Log("[WeaveLoader] CreateFileW: redirecting terrain.png to merged atlas"); + return s_originalCreateFileW(s_mergedTerrainW.c_str(), dwDesiredAccess, dwShareMode, + lpSecurityAttributes, dwCreationDisposition, dwFlagsAndAttributes, hTemplateFile); + } + if (!s_mergedItemsW.empty() && EndsWith(lpFileName, L"\\items.png")) + { + LogUtil::Log("[WeaveLoader] CreateFileW: redirecting items.png to merged atlas"); + return s_originalCreateFileW(s_mergedItemsW.c_str(), dwDesiredAccess, dwShareMode, + lpSecurityAttributes, dwCreationDisposition, dwFlagsAndAttributes, hTemplateFile); + } + } + return s_originalCreateFileW(lpFileName, dwDesiredAccess, dwShareMode, + lpSecurityAttributes, dwCreationDisposition, dwFlagsAndAttributes, hTemplateFile); + } + + bool InstallCreateFileHook(const std::string& gameResPath) + { + if (!s_hasModTextures) return false; + + std::string mergedTerrain = GetMergedTerrainPath(); + std::string mergedItems = GetMergedItemsPath(); + + if (!mergedTerrain.empty()) + s_mergedTerrainW = std::wstring(mergedTerrain.begin(), mergedTerrain.end()); + if (!mergedItems.empty()) + s_mergedItemsW = std::wstring(mergedItems.begin(), mergedItems.end()); + + void* pCreateFileW = reinterpret_cast( + GetProcAddress(GetModuleHandleA("kernel32.dll"), "CreateFileW")); + if (!pCreateFileW) + { + LogUtil::Log("[WeaveLoader] ModAtlas: could not find CreateFileW"); + return false; + } + + if (MH_CreateHook(pCreateFileW, reinterpret_cast(&Hooked_CreateFileW), + reinterpret_cast(&s_originalCreateFileW)) != MH_OK) + { + LogUtil::Log("[WeaveLoader] ModAtlas: failed to hook CreateFileW"); + return false; + } + if (MH_EnableHook(pCreateFileW) != MH_OK) + { + LogUtil::Log("[WeaveLoader] ModAtlas: failed to enable CreateFileW hook"); + return false; + } + + LogUtil::Log("[WeaveLoader] ModAtlas: CreateFileW hook installed (terrain=%s, items=%s)", + mergedTerrain.c_str(), mergedItems.c_str()); + return true; + } + + void RemoveCreateFileHook() + { + void* pCreateFileW = reinterpret_cast( + GetProcAddress(GetModuleHandleA("kernel32.dll"), "CreateFileW")); + if (pCreateFileW) + { + MH_DisableHook(pCreateFileW); + MH_RemoveHook(pCreateFileW); + } + s_mergedTerrainW.clear(); + s_mergedItemsW.clear(); + LogUtil::Log("[WeaveLoader] ModAtlas: CreateFileW hook removed"); + } + void SetInjectSymbols(void* simpleIconCtor, void* operatorNew) { s_simpleIconCtor = simpleIconCtor; @@ -300,37 +390,6 @@ namespace ModAtlas s_originalRegisterIcon = fn; } - void InstallAtlasFiles(const std::string& gameResPath) - { - if (!s_hasModTextures) return; - s_gameResPath = gameResPath; - - std::string vanillaTerrain = gameResPath + "\\terrain.png"; - std::string vanillaItems = gameResPath + "\\items.png"; - std::string mergedTerrain = GetMergedTerrainPath(); - std::string mergedItems = GetMergedItemsPath(); - - if (!mergedTerrain.empty() && !s_blockEntries.empty()) - { - s_backupTerrainPath = vanillaTerrain + ".weaveloader_backup"; - CopyFileA(vanillaTerrain.c_str(), s_backupTerrainPath.c_str(), FALSE); - if (CopyFileA(mergedTerrain.c_str(), vanillaTerrain.c_str(), FALSE)) - LogUtil::Log("[WeaveLoader] ModAtlas: installed merged terrain.png over game file"); - else - LogUtil::Log("[WeaveLoader] ModAtlas: WARNING - failed to copy merged terrain.png (err=%lu)", GetLastError()); - } - - if (!mergedItems.empty() && !s_itemEntries.empty()) - { - s_backupItemsPath = vanillaItems + ".weaveloader_backup"; - CopyFileA(vanillaItems.c_str(), s_backupItemsPath.c_str(), FALSE); - if (CopyFileA(mergedItems.c_str(), vanillaItems.c_str(), FALSE)) - LogUtil::Log("[WeaveLoader] ModAtlas: installed merged items.png over game file"); - else - LogUtil::Log("[WeaveLoader] ModAtlas: WARNING - failed to copy merged items.png (err=%lu)", GetLastError()); - } - } - void CreateModIcons(void* textureMap) { if (!s_hasModTextures || !s_simpleIconCtor || !textureMap) return; @@ -421,22 +480,6 @@ namespace ModAtlas fixForAtlas(s_terrainTextureMap, 0, L"stone"); fixForAtlas(s_itemsTextureMap, 1, L"diamond"); - - // Restore backed-up vanilla atlas files - if (!s_backupTerrainPath.empty()) - { - std::string vanillaTerrain = s_gameResPath + "\\terrain.png"; - if (MoveFileExA(s_backupTerrainPath.c_str(), vanillaTerrain.c_str(), MOVEFILE_REPLACE_EXISTING)) - LogUtil::Log("[WeaveLoader] ModAtlas: restored original terrain.png"); - s_backupTerrainPath.clear(); - } - if (!s_backupItemsPath.empty()) - { - std::string vanillaItems = s_gameResPath + "\\items.png"; - if (MoveFileExA(s_backupItemsPath.c_str(), vanillaItems.c_str(), MOVEFILE_REPLACE_EXISTING)) - LogUtil::Log("[WeaveLoader] ModAtlas: restored original items.png"); - s_backupItemsPath.clear(); - } } void* LookupModIcon(const std::wstring& name) diff --git a/WeaveLoaderRuntime/src/ModAtlas.h b/WeaveLoaderRuntime/src/ModAtlas.h index 6879891..2ad3e9f 100644 --- a/WeaveLoaderRuntime/src/ModAtlas.h +++ b/WeaveLoaderRuntime/src/ModAtlas.h @@ -6,7 +6,9 @@ /// /// Builds merged terrain.png and items.png atlases from mod assets. -/// Scans mods/*/assets/blocks/*.png and items/*.png, stitches into vanilla atlases. +/// Scans mods/*/assets/blocks/*.png and items/*.png, stitches into copies of the +/// vanilla atlases stored in mods/ModLoader/generated/. A CreateFileW hook redirects +/// the game's file opens to the merged copies so vanilla files are never modified. /// namespace ModAtlas { @@ -41,9 +43,12 @@ namespace ModAtlas /// Set the original registerIcon function for vanilla icon lookups void SetRegisterIconFn(RegisterIcon_fn fn); - /// Copy merged atlas PNGs over the game's vanilla atlas files. - /// Call before Minecraft::init so the game loads our textures. - void InstallAtlasFiles(const std::string& gameResPath); + /// Install a CreateFileW hook that redirects terrain.png/items.png reads to + /// the merged atlases. Call after BuildAtlases, before Minecraft::init. + bool InstallCreateFileHook(const std::string& gameResPath); + + /// Remove the CreateFileW hook after init has loaded textures into memory. + void RemoveCreateFileHook(); /// Create SimpleIcon objects for our mod textures after loadUVs runs. /// Call from loadUVs hook after original returns. atlasType is read from textureMap.