Replace file overwrite with CreateFileW hook for mod texture loading

Instead of backing up and overwriting vanilla terrain.png/items.png,
hook CreateFileW to redirect file opens to merged atlases in
mods/ModLoader/generated/. The hook is active only during init and
removed once textures are loaded into GPU memory. Vanilla game files
are never modified.
This commit is contained in:
Jacobwasbeast
2026-03-07 00:14:06 -06:00
parent 455abc155a
commit b2664e0f19
4 changed files with 111 additions and 60 deletions

View File

@@ -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

View File

@@ -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();

View File

@@ -1,6 +1,7 @@
#include "ModAtlas.h"
#include "LogUtil.h"
#include <Windows.h>
#include <MinHook.h>
#include <algorithm>
#include <cstring>
#include <fstream>
@@ -281,14 +282,103 @@ namespace ModAtlas
static std::unordered_map<std::wstring, void*> 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<void*>(
GetProcAddress(GetModuleHandleA("kernel32.dll"), "CreateFileW"));
if (!pCreateFileW)
{
LogUtil::Log("[WeaveLoader] ModAtlas: could not find CreateFileW");
return false;
}
if (MH_CreateHook(pCreateFileW, reinterpret_cast<void*>(&Hooked_CreateFileW),
reinterpret_cast<void**>(&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<void*>(
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)

View File

@@ -6,7 +6,9 @@
/// <summary>
/// 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.
/// </summary>
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.