Files
LegacyWeaveLoader/WeaveLoaderRuntime/src/GameHooks.cpp
2026-03-10 19:44:17 -05:00

2680 lines
100 KiB
C++

#include "GameHooks.h"
#include "DotNetHost.h"
#include "CreativeInventory.h"
#include "MainMenuOverlay.h"
#include "ModStrings.h"
#include "ModAtlas.h"
#include "NativeExports.h"
#include "CustomPickaxeRegistry.h"
#include "CustomToolMaterialRegistry.h"
#include "CustomBlockRegistry.h"
#include "ManagedBlockRegistry.h"
#include "CustomSlabRegistry.h"
#include "LogUtil.h"
#include "WorldIdRemap.h"
#include <Windows.h>
#include <string>
#include <cstdio>
#include <cstring>
#include <cwchar>
#include <cwctype>
#include <cctype>
#include <memory>
#include <mutex>
#include <unordered_map>
#include <cstddef>
#include <vector>
namespace GameHooks
{
RunStaticCtors_fn Original_RunStaticCtors = nullptr;
MinecraftTick_fn Original_MinecraftTick = nullptr;
MinecraftInit_fn Original_MinecraftInit = nullptr;
ExitGame_fn Original_ExitGame = nullptr;
CreativeStaticCtor_fn Original_CreativeStaticCtor = nullptr;
MainMenuCustomDraw_fn Original_MainMenuCustomDraw = nullptr;
Present_fn Original_Present = nullptr;
OutputDebugStringA_fn Original_OutputDebugStringA = nullptr;
GetString_fn Original_GetString = nullptr;
GetResourceAsStream_fn Original_GetResourceAsStream = nullptr;
LoadUVs_fn Original_LoadUVs = nullptr;
PreStitchedTextureMapStitch_fn Original_PreStitchedTextureMapStitch = nullptr;
RegisterIcon_fn Original_RegisterIcon = nullptr;
ItemInstanceGetIcon_fn Original_ItemInstanceGetIcon = nullptr;
EntityRendererBindTextureResource_fn Original_EntityRendererBindTextureResource = nullptr;
ItemRendererRenderItemBillboard_fn Original_ItemRendererRenderItemBillboard = nullptr;
AnimatedTextureCycleFrames_fn Original_CompassTextureCycleFrames = nullptr;
AnimatedTextureCycleFrames_fn Original_ClockTextureCycleFrames = nullptr;
TextureGetSourceDim_fn Original_CompassTextureGetSourceWidth = nullptr;
TextureGetSourceDim_fn Original_CompassTextureGetSourceHeight = nullptr;
TextureGetSourceDim_fn Original_ClockTextureGetSourceWidth = nullptr;
TextureGetSourceDim_fn Original_ClockTextureGetSourceHeight = nullptr;
ItemInstanceMineBlock_fn Original_ItemInstanceMineBlock = nullptr;
ItemInstanceSave_fn Original_ItemInstanceSave = nullptr;
ItemInstanceLoad_fn Original_ItemInstanceLoad = nullptr;
ItemMineBlock_fn Original_ItemMineBlock = nullptr;
ItemMineBlock_fn Original_DiggerItemMineBlock = nullptr;
PickaxeGetDestroySpeed_fn Original_PickaxeItemGetDestroySpeed = nullptr;
PickaxeCanDestroySpecial_fn Original_PickaxeItemCanDestroySpecial = nullptr;
PickaxeGetDestroySpeed_fn Original_ShovelItemGetDestroySpeed = nullptr;
PickaxeCanDestroySpecial_fn Original_ShovelItemCanDestroySpecial = nullptr;
TileOnPlace_fn Original_TileOnPlace = nullptr;
TileNeighborChanged_fn Original_TileNeighborChanged = nullptr;
TileTick_fn Original_TileTick = nullptr;
LevelSetTileAndDataDispatch_fn Original_LevelSetTileAndData = nullptr;
LevelSetDataDispatch_fn Original_LevelSetData = nullptr;
LevelUpdateNeighborsAtDispatch_fn Original_LevelUpdateNeighborsAt = nullptr;
ServerLevelTickPendingTicks_fn Original_ServerLevelTickPendingTicks = nullptr;
TileGetResource_fn Original_TileGetResource = nullptr;
McRegionChunkStorageLoad_fn Original_McRegionChunkStorageLoad = nullptr;
McRegionChunkStorageSave_fn Original_McRegionChunkStorageSave = nullptr;
TileCloneTileId_fn Original_TileCloneTileId = nullptr;
TileGetTextureFaceData_fn Original_StoneSlabGetTexture = nullptr;
TileGetTextureFaceData_fn Original_WoodSlabGetTexture = nullptr;
TileGetResource_fn Original_StoneSlabGetResource = nullptr;
TileGetResource_fn Original_WoodSlabGetResource = nullptr;
TileGetDescriptionId_fn Original_StoneSlabGetDescriptionId = nullptr;
TileGetDescriptionId_fn Original_WoodSlabGetDescriptionId = nullptr;
TileGetAuxName_fn Original_StoneSlabGetAuxName = nullptr;
TileGetAuxName_fn Original_WoodSlabGetAuxName = nullptr;
TileRegisterIcons_fn Original_StoneSlabRegisterIcons = nullptr;
TileRegisterIcons_fn Original_WoodSlabRegisterIcons = nullptr;
StoneSlabItemGetIcon_fn Original_StoneSlabItemGetIcon = nullptr;
StoneSlabItemGetDescriptionId_fn Original_StoneSlabItemGetDescriptionId = nullptr;
TileCloneTileId_fn Original_HalfSlabCloneTileId = nullptr;
PlayerCanDestroy_fn Original_PlayerCanDestroy = nullptr;
GameModeUseItem_fn Original_ServerPlayerGameModeUseItem = nullptr;
GameModeUseItem_fn Original_MultiPlayerGameModeUseItem = nullptr;
MinecraftSetLevel_fn Original_MinecraftSetLevel = nullptr;
TexturesBindTextureResource_fn Original_TexturesBindTextureResource = nullptr;
TexturesLoadTextureByName_fn Original_TexturesLoadTextureByName = nullptr;
TexturesLoadTextureByIndex_fn Original_TexturesLoadTextureByIndex = nullptr;
TexturesReadImage_fn Original_TexturesReadImage = nullptr;
StitchedTextureUV_fn Original_StitchedGetU0 = nullptr;
StitchedTextureUV_fn Original_StitchedGetU1 = nullptr;
StitchedTextureUV_fn Original_StitchedGetV0 = nullptr;
StitchedTextureUV_fn Original_StitchedGetV1 = nullptr;
BufferedImageCtorFile_fn Original_BufferedImageCtorFile = nullptr;
BufferedImageCtorDLCPack_fn Original_BufferedImageCtorDLCPack = nullptr;
TextureManagerCreateTexture_fn Original_TextureManagerCreateTexture = nullptr;
TextureTransferFromImage_fn Original_TextureTransferFromImage = nullptr;
TexturePackGetImageResource_fn Original_AbstractTexturePackGetImageResource = nullptr;
TexturePackGetImageResource_fn Original_DLCTexturePackGetImageResource = nullptr;
static int s_itemMineBlockHookCalls = 0;
static void* s_currentLevel = nullptr;
static thread_local void* s_activeUseLevel = nullptr;
static LevelAddEntity_fn s_levelAddEntity = nullptr;
static EntityIONewById_fn s_entityIoNewById = nullptr;
static EntityMoveTo_fn s_entityMoveTo = nullptr;
static EntitySetPos_fn s_entitySetPos = nullptr;
static EntityGetLookAngle_fn s_entityGetLookAngle = nullptr;
static LivingEntityGetViewVector_fn s_livingEntityGetViewVector = nullptr;
static EntityLerpMotion_fn s_entityLerpMotion = nullptr;
static InventoryRemoveResource_fn s_inventoryRemoveResource = nullptr;
static void* s_inventoryVtable = nullptr;
static ItemInstanceHurtAndBreak_fn s_itemInstanceHurtAndBreak = nullptr;
static std::string s_modsPath;
static std::unordered_map<std::string, std::string> s_modAssetRoots;
static bool s_modAssetsIndexed = false;
static std::mutex s_modAssetsMutex;
// Verified from compiled Player::inventory accesses in this game build.
static constexpr ptrdiff_t kPlayerInventoryOffset = 0x340;
static constexpr ptrdiff_t kLevelIsClientSideOffset = 0x268;
static constexpr ptrdiff_t kItemIdOffset = 0x20;
static constexpr ptrdiff_t kTileIdOffset = 0x28;
static constexpr ptrdiff_t kTileIconOffset = 0x78;
static constexpr ptrdiff_t kEntityXOffset = 0x78;
static constexpr ptrdiff_t kEntityYOffset = 0x80;
static constexpr ptrdiff_t kEntityZOffset = 0x88;
static constexpr ptrdiff_t kEntityRemovedOffset = 0xC7;
static constexpr ptrdiff_t kFireballOwnerOffset = 0x1D0;
static constexpr ptrdiff_t kFireballXPowerOffset = 0x1E8;
static constexpr ptrdiff_t kFireballYPowerOffset = 0x1F0;
static constexpr ptrdiff_t kFireballZPowerOffset = 0x1F8;
static void* s_textureAtlasLocationBlocks = nullptr;
static void* s_textureAtlasLocationItems = nullptr;
static int s_textureAtlasIdBlocks = -1;
static int s_textureAtlasIdItems = -1;
static void* s_tileTilesArray = nullptr;
static thread_local bool s_inLoadTextureByNameHook = false;
static thread_local bool s_hasForcedBillboardRoute = false;
static thread_local int s_forcedBillboardAtlas = -1;
static thread_local int s_forcedBillboardPage = 0;
static thread_local int s_activeStitchAtlasType = -1;
static int s_animatedTextureGuardLogCount = 0;
struct TextureNameArrayNative
{
int* data;
unsigned int length;
};
struct ResourceLocationNative
{
TextureNameArrayNative textures;
std::wstring path;
bool preloaded;
};
static ResourceLocationNative s_pageResource;
static bool s_pageResourceInit = false;
static int s_pageRouteLogCount = 0;
static int s_forcedTerrainRouteLogCount = 0;
static uintptr_t s_gameModuleBase = 0;
static uintptr_t s_gameModuleEnd = 0;
static std::vector<void*> s_spawnedEntities;
static int s_outOfWorldGuardLogCount = 0;
static int s_pendingServerUseItemId = -1;
static LevelGetTile_fn s_levelGetTile = nullptr;
struct ManagedScheduledTick
{
void* levelPtr;
int x;
int y;
int z;
int blockId;
int remainingTicks;
};
static std::vector<ManagedScheduledTick> s_managedScheduledTicks;
static ULONGLONG s_pendingServerUseExpiryMs = 0;
static TileGetTextureFaceData_fn s_tileGetTextureFaceData = nullptr;
static bool s_preInitCalled = false;
static bool s_initCalled = false;
static bool s_postInitCalled = false;
static void EnsurePageResourcesInitialized()
{
if (s_pageResourceInit)
return;
s_pageResource.textures = { nullptr, 0 };
s_pageResource.path = L"";
s_pageResource.preloaded = false;
s_pageResourceInit = true;
}
static void EnsureGameModuleRange()
{
if (s_gameModuleBase != 0 && s_gameModuleEnd > s_gameModuleBase)
return;
HMODULE exe = GetModuleHandleW(nullptr);
if (!exe)
return;
auto* dos = reinterpret_cast<IMAGE_DOS_HEADER*>(exe);
if (!dos || dos->e_magic != IMAGE_DOS_SIGNATURE)
return;
auto* nt = reinterpret_cast<IMAGE_NT_HEADERS*>(
reinterpret_cast<unsigned char*>(exe) + dos->e_lfanew);
if (!nt || nt->Signature != IMAGE_NT_SIGNATURE)
return;
s_gameModuleBase = reinterpret_cast<uintptr_t>(exe);
s_gameModuleEnd = s_gameModuleBase + nt->OptionalHeader.SizeOfImage;
}
static bool IsCanonicalUserPtr(const void* ptr)
{
uintptr_t p = reinterpret_cast<uintptr_t>(ptr);
if (p < 0x10000ULL)
return false;
// User-mode canonical range on x64 Windows.
if (p >= 0x0000800000000000ULL)
return false;
return true;
}
static bool IsGameCodePtr(const void* ptr)
{
if (!ptr)
return false;
EnsureGameModuleRange();
if (s_gameModuleBase == 0 || s_gameModuleEnd <= s_gameModuleBase)
return false;
uintptr_t p = reinterpret_cast<uintptr_t>(ptr);
return p >= s_gameModuleBase && p < s_gameModuleEnd;
}
static bool IsReadableRange(const void* ptr, size_t bytes)
{
if (!ptr || bytes == 0 || !IsCanonicalUserPtr(ptr))
return false;
MEMORY_BASIC_INFORMATION mbi{};
if (VirtualQuery(ptr, &mbi, sizeof(mbi)) != sizeof(mbi))
return false;
if (mbi.State != MEM_COMMIT || mbi.Protect == PAGE_NOACCESS || (mbi.Protect & PAGE_GUARD))
return false;
const DWORD readMask = PAGE_READONLY | PAGE_READWRITE | PAGE_WRITECOPY |
PAGE_EXECUTE_READ | PAGE_EXECUTE_READWRITE | PAGE_EXECUTE_WRITECOPY;
if ((mbi.Protect & readMask) == 0)
return false;
uintptr_t p = reinterpret_cast<uintptr_t>(ptr);
uintptr_t end = reinterpret_cast<uintptr_t>(mbi.BaseAddress) + mbi.RegionSize;
if (p + bytes < p)
return false;
return (p + bytes) <= end;
}
static void* TryReadTileIcon(void* tilePtr)
{
if (!tilePtr)
return nullptr;
const void* iconSlot = static_cast<const char*>(tilePtr) + kTileIconOffset;
if (!IsReadableRange(iconSlot, sizeof(void*)))
return nullptr;
void* iconPtr = *reinterpret_cast<void* const*>(iconSlot);
if (!IsCanonicalUserPtr(iconPtr))
return nullptr;
return iconPtr;
}
static void PatchSingleSlabIcon(const CustomSlabRegistry::Definition& def, void*)
{
if (def.iconName.empty() || !s_tileTilesArray || !IsReadableRange(s_tileTilesArray, sizeof(void*)))
return;
void* modIcon = ModAtlas::LookupModIcon(def.iconName);
if (!modIcon)
return;
const void* arrayPtr = *reinterpret_cast<const void* const*>(s_tileTilesArray);
if (!arrayPtr || !IsReadableRange(arrayPtr, sizeof(void*) * 4096))
return;
auto* tiles = reinterpret_cast<void* const*>(const_cast<void*>(arrayPtr));
const int ids[] = { def.halfBlockId, def.fullBlockId };
for (int blockId : ids)
{
if (blockId < 0 || blockId >= 4096)
continue;
void* tilePtr = const_cast<void*>(tiles[blockId]);
if (!tilePtr || !IsReadableRange(static_cast<const char*>(tilePtr) + kTileIconOffset, sizeof(void*)))
continue;
*reinterpret_cast<void**>(static_cast<char*>(tilePtr) + kTileIconOffset) = modIcon;
}
}
static void PatchCustomSlabIcons()
{
CustomSlabRegistry::ForEachUnique(&PatchSingleSlabIcon, nullptr);
}
static std::wstring BuildVirtualAtlasPath(int atlasType, int page)
{
std::wstring base = L"/modloader/";
if (atlasType == 0)
{
if (page <= 0) return base + L"terrain.png";
return base + L"terrain_p" + std::to_wstring(page) + L".png";
}
if (page <= 0) return base + L"items.png";
return base + L"items_p" + std::to_wstring(page) + L".png";
}
static std::wstring NormalizeLowerPath(const std::wstring& path)
{
std::wstring lower;
lower.reserve(path.size());
for (wchar_t ch : path)
{
wchar_t c = (ch == L'\\') ? L'/' : ch;
lower.push_back((wchar_t)towlower(c));
}
return lower;
}
static bool EndsWithPath(const std::wstring& path, const wchar_t* suffix)
{
size_t pathLen = path.size();
size_t suffLen = wcslen(suffix);
if (suffLen > pathLen) return false;
return path.compare(pathLen - suffLen, suffLen, suffix) == 0;
}
static bool TryParseMipmapLevel(const std::wstring& lowerPath, const wchar_t* stem, int& outLevel)
{
outLevel = 0;
if (!stem) return false;
std::wstring key = std::wstring(stem) + L"mipmaplevel";
size_t pos = lowerPath.rfind(key);
if (pos == std::wstring::npos)
return false;
size_t numStart = pos + key.size();
size_t numEnd = lowerPath.find(L".png", numStart);
if (numEnd == std::wstring::npos || numEnd <= numStart)
return false;
int value = 0;
for (size_t i = numStart; i < numEnd; i++)
{
wchar_t ch = lowerPath[i];
if (ch < L'0' || ch > L'9')
return false;
value = value * 10 + (ch - L'0');
}
if (value <= 1)
return false;
outLevel = value;
return true;
}
static std::string ToLowerAscii(const std::string& value)
{
std::string out;
out.reserve(value.size());
for (char ch : value)
out.push_back((char)tolower((unsigned char)ch));
return out;
}
static std::string WStringToLowerAscii(const std::wstring& value)
{
std::string out;
out.reserve(value.size());
for (wchar_t ch : value)
{
if (ch > 0x7F)
return std::string();
out.push_back((char)tolower((unsigned char)ch));
}
return out;
}
static void BuildModAssetIndexLocked()
{
s_modAssetRoots.clear();
s_modAssetsIndexed = true;
if (s_modsPath.empty())
return;
WIN32_FIND_DATAA fd;
std::string search = s_modsPath + "\\*";
HANDLE h = FindFirstFileA(search.c_str(), &fd);
if (h == INVALID_HANDLE_VALUE)
return;
do
{
if (!(fd.dwFileAttributes & FILE_ATTRIBUTE_DIRECTORY)) continue;
if (fd.cFileName[0] == '.') continue;
std::string modFolder = fd.cFileName;
std::string assetsPath = s_modsPath + "\\" + modFolder + "\\assets";
DWORD attr = GetFileAttributesA(assetsPath.c_str());
if (attr == INVALID_FILE_ATTRIBUTES || !(attr & FILE_ATTRIBUTE_DIRECTORY))
continue;
WIN32_FIND_DATAA nsfd;
std::string nsSearch = assetsPath + "\\*";
HANDLE hNs = FindFirstFileA(nsSearch.c_str(), &nsfd);
if (hNs == INVALID_HANDLE_VALUE)
continue;
do
{
if (!(nsfd.dwFileAttributes & FILE_ATTRIBUTE_DIRECTORY)) continue;
if (nsfd.cFileName[0] == '.') continue;
std::string nsName = ToLowerAscii(nsfd.cFileName);
if (nsName.empty())
continue;
if (s_modAssetRoots.find(nsName) == s_modAssetRoots.end())
{
s_modAssetRoots.emplace(nsName, assetsPath);
}
else
{
LogUtil::Log("[WeaveLoader] ModAssets: duplicate namespace '%s' (folder=%s) ignored",
nsName.c_str(), modFolder.c_str());
}
} while (FindNextFileA(hNs, &nsfd));
FindClose(hNs);
} while (FindNextFileA(h, &fd));
FindClose(h);
}
static void EnsureModAssetIndex()
{
if (s_modAssetsIndexed)
return;
std::lock_guard<std::mutex> guard(s_modAssetsMutex);
if (!s_modAssetsIndexed)
BuildModAssetIndexLocked();
}
static bool FileExistsW(const std::wstring& path)
{
DWORD attr = GetFileAttributesW(path.c_str());
return (attr != INVALID_FILE_ATTRIBUTES) && !(attr & FILE_ATTRIBUTE_DIRECTORY);
}
static bool TryResolveModAssetPath(const std::wstring& requestPath, std::wstring& outPath)
{
if (s_modsPath.empty())
return false;
std::wstring lower = NormalizeLowerPath(requestPath);
if (lower.find(L"://") != std::wstring::npos)
return false;
const std::wstring kAssets = L"/assets/";
size_t assetsPos = lower.find(kAssets);
if (assetsPos == std::wstring::npos)
return false;
size_t nsStart = assetsPos + kAssets.size();
if (nsStart >= lower.size())
return false;
size_t nsEnd = lower.find(L'/', nsStart);
if (nsEnd == std::wstring::npos || nsEnd <= nsStart)
return false;
std::wstring ns = lower.substr(nsStart, nsEnd - nsStart);
if (ns.empty())
return false;
size_t relStart = nsEnd + 1;
if (relStart >= lower.size())
return false;
std::wstring rel = lower.substr(relStart);
std::string nsKey = WStringToLowerAscii(ns);
if (nsKey.empty())
return false;
EnsureModAssetIndex();
auto it = s_modAssetRoots.find(nsKey);
if (it == s_modAssetRoots.end())
return false;
std::wstring rootW(it->second.begin(), it->second.end());
std::wstring relW = ns + L"/" + rel;
for (wchar_t& ch : relW)
{
if (ch == L'/')
ch = L'\\';
}
std::wstring fullPath = rootW + L"\\" + relW;
if (!FileExistsW(fullPath))
return false;
outPath = fullPath;
return true;
}
static int DetectAtlasTypeFromResource(void* resourcePtr)
{
if (!resourcePtr)
return -1;
if (resourcePtr == s_textureAtlasLocationBlocks) return 0;
if (resourcePtr == s_textureAtlasLocationItems) return 1;
const ResourceLocationNative* res = reinterpret_cast<const ResourceLocationNative*>(resourcePtr);
if (res->textures.data && res->textures.length > 0)
{
const int texId = res->textures.data[0];
if (texId == s_textureAtlasIdBlocks) return 0;
if (texId == s_textureAtlasIdItems) return 1;
}
if (!res->path.empty())
{
std::wstring p = NormalizeLowerPath(res->path);
if (p.find(L"terrain") != std::wstring::npos) return 0;
if (p.find(L"items") != std::wstring::npos) return 1;
}
return -1;
}
static bool ParseVirtualAtlasRequest(const std::wstring& path, int& outAtlasType, int& outPage)
{
outAtlasType = -1;
outPage = 0;
std::wstring lower;
lower.reserve(path.size());
for (wchar_t ch : path)
{
wchar_t c = (ch == L'\\') ? L'/' : ch;
lower.push_back((wchar_t)towlower(c));
}
size_t fileStart = std::wstring::npos;
const std::wstring kPrefixA = L"/modloader/";
const std::wstring kPrefixB = L"/mods/modloader/generated/";
size_t prefixPosA = lower.find(kPrefixA);
size_t prefixPosB = lower.find(kPrefixB);
if (prefixPosA != std::wstring::npos)
fileStart = prefixPosA + kPrefixA.size();
else if (prefixPosB != std::wstring::npos)
fileStart = prefixPosB + kPrefixB.size();
else
return false;
if (fileStart >= lower.size())
return false;
std::wstring file = lower.substr(fileStart);
auto parseStem = [&](const wchar_t* stem, int atlasType) -> bool
{
std::wstring stemStr(stem);
if (file == stemStr + L".png")
{
outAtlasType = atlasType;
outPage = 0;
return true;
}
std::wstring prefix = stemStr + L"_p";
if (file.rfind(prefix, 0) != 0)
return false;
size_t dot = file.find(L".png", prefix.size());
if (dot == std::wstring::npos || dot <= prefix.size())
return false;
std::wstring num = file.substr(prefix.size(), dot - prefix.size());
for (wchar_t c : num)
{
if (c < L'0' || c > L'9')
return false;
}
outAtlasType = atlasType;
outPage = _wtoi(num.c_str());
if (outPage < 0) outPage = 0;
return true;
};
if (parseStem(L"terrain", 0)) return true;
if (parseStem(L"items", 1)) return true;
return false;
}
struct MineBlockNativeArgs
{
int itemId;
int tileId;
int x;
int y;
int z;
};
struct UseItemNativeArgs
{
int itemId;
int isTestUseOnly;
int isClientSide;
void* itemInstancePtr;
void* playerPtr;
void* playerSharedPtr;
};
static bool IsFireballFamilyEntityId(int entityNumericId);
static void* DecodeItemInstancePtrFromSharedArg(void* sharedArg);
static void* DecodePlayerPtrFromSharedArg(void* sharedArg);
static bool TryReadItemId(void* itemInstancePtr, int& outItemId)
{
if (!itemInstancePtr)
return false;
// ItemInstance inherits enable_shared_from_this, so id is not guaranteed at +0x10.
// Probe known layouts observed across builds.
static const int kCandidateOffsets[] = { 0x20, 0x18, 0x10, 0x28 };
for (int off : kCandidateOffsets)
{
const char* idPtr = static_cast<char*>(itemInstancePtr) + off;
if (!IsReadableRange(idPtr, sizeof(int)))
continue;
int id = *reinterpret_cast<const int*>(idPtr);
if (id > 0 && id < 32000)
{
outItemId = id;
return true;
}
}
return false;
}
static int TryDispatchMineBlockFromItemInstancePtr(void* itemInstancePtr, int tile, int x, int y, int z, const char* sourceTag)
{
if (!itemInstancePtr)
return 0;
int itemId = 0;
if (!TryReadItemId(itemInstancePtr, itemId))
return 0;
MineBlockNativeArgs args{ itemId, tile, x, y, z };
int action = DotNetHost::CallItemMineBlock(&args, sizeof(args));
return action;
}
static int TryDispatchUseItemFromSharedItemArg(void* itemInstanceSharedPtr, void* playerSharedPtr, bool bTestUseOnly, const char* sourceTag)
{
void* itemInstancePtr = DecodeItemInstancePtrFromSharedArg(itemInstanceSharedPtr);
void* playerPtr = DecodePlayerPtrFromSharedArg(playerSharedPtr);
if (!itemInstancePtr)
return 0;
int itemId = 0;
if (!TryReadItemId(itemInstancePtr, itemId))
return 0;
int isClientSide = 0;
if (s_activeUseLevel &&
IsReadableRange(static_cast<char*>(s_activeUseLevel) + kLevelIsClientSideOffset, sizeof(bool)))
{
isClientSide =
*reinterpret_cast<bool*>(static_cast<char*>(s_activeUseLevel) + kLevelIsClientSideOffset) ? 1 : 0;
}
UseItemNativeArgs args{ itemId, bTestUseOnly ? 1 : 0, isClientSide, itemInstancePtr, playerPtr, playerSharedPtr };
return DotNetHost::CallItemUse(&args, sizeof(args));
}
void SetSummonSymbols(void* levelAddEntity,
void* entityIoNewById,
void* entityMoveTo,
void* entitySetPos)
{
s_levelAddEntity = reinterpret_cast<LevelAddEntity_fn>(levelAddEntity);
s_entityIoNewById = reinterpret_cast<EntityIONewById_fn>(entityIoNewById);
s_entityMoveTo = reinterpret_cast<EntityMoveTo_fn>(entityMoveTo);
s_entitySetPos = reinterpret_cast<EntitySetPos_fn>(entitySetPos);
}
void SetUseActionSymbols(void* inventoryRemoveResource,
void* inventoryVtable,
void* itemInstanceHurtAndBreak,
void* containerBroadcastChanges,
void* entityGetLookAngle,
void* livingEntityGetViewVector,
void* entityLerpMotion,
void* entitySetPos)
{
s_inventoryRemoveResource = reinterpret_cast<InventoryRemoveResource_fn>(inventoryRemoveResource);
s_inventoryVtable = inventoryVtable;
s_itemInstanceHurtAndBreak = reinterpret_cast<ItemInstanceHurtAndBreak_fn>(itemInstanceHurtAndBreak);
s_entityGetLookAngle = reinterpret_cast<EntityGetLookAngle_fn>(entityGetLookAngle);
s_livingEntityGetViewVector = reinterpret_cast<LivingEntityGetViewVector_fn>(livingEntityGetViewVector);
s_entityLerpMotion = reinterpret_cast<EntityLerpMotion_fn>(entityLerpMotion);
s_entitySetPos = reinterpret_cast<EntitySetPos_fn>(entitySetPos);
}
void SetBlockHelperSymbols(void* tileGetTextureFaceData)
{
s_tileGetTextureFaceData = reinterpret_cast<TileGetTextureFaceData_fn>(tileGetTextureFaceData);
}
void SetManagedBlockDispatchSymbols(void* levelGetTile)
{
s_levelGetTile = reinterpret_cast<LevelGetTile_fn>(levelGetTile);
}
void EnqueueManagedBlockTick(void* levelPtr, int x, int y, int z, int blockId, int delay)
{
if (!levelPtr || blockId < 0 || !ManagedBlockRegistry::IsManaged(blockId))
return;
const int normalizedDelay = delay > 0 ? delay : 1;
for (ManagedScheduledTick& tick : s_managedScheduledTicks)
{
if (tick.levelPtr == levelPtr &&
tick.x == x &&
tick.y == y &&
tick.z == z &&
tick.blockId == blockId)
{
if (normalizedDelay < tick.remainingTicks)
tick.remainingTicks = normalizedDelay;
return;
}
}
s_managedScheduledTicks.push_back({ levelPtr, x, y, z, blockId, normalizedDelay });
}
static bool IsInventoryObjectPtr(void* objectPtr)
{
if (!objectPtr || !s_inventoryVtable || !IsReadableRange(objectPtr, sizeof(void*)))
return false;
void* vt = *reinterpret_cast<void**>(objectPtr);
if (!IsCanonicalUserPtr(vt))
return false;
return vt == s_inventoryVtable;
}
static void* FindInventoryPtrFromPlayer(void* playerPtr)
{
if (!playerPtr || !s_inventoryRemoveResource || !IsCanonicalUserPtr(playerPtr))
return nullptr;
const char* base = static_cast<const char*>(playerPtr);
const void* slotPtr = base + kPlayerInventoryOffset;
if (!IsReadableRange(slotPtr, sizeof(void*)))
return nullptr;
void* inventoryPtr = *reinterpret_cast<void* const*>(slotPtr);
if (!IsInventoryObjectPtr(inventoryPtr))
return nullptr;
return inventoryPtr;
}
bool ConsumePlayerResource(void* playerPtr, int itemId, int count)
{
static int s_consumeFailLogCount = 0;
if (!playerPtr || !s_inventoryRemoveResource || !s_inventoryVtable || itemId < 0 || count <= 0)
return false;
void* inventoryPtr = FindInventoryPtrFromPlayer(playerPtr);
if (!inventoryPtr)
{
if (s_consumeFailLogCount < 20)
{
LogUtil::Log("[WeaveLoader] ConsumePlayerResource: no inventory ptr (player=%p item=%d count=%d)",
playerPtr, itemId, count);
s_consumeFailLogCount++;
}
return false;
}
for (int i = 0; i < count; ++i)
{
__try
{
if (!s_inventoryRemoveResource(inventoryPtr, itemId))
{
if (s_consumeFailLogCount < 20)
{
LogUtil::Log("[WeaveLoader] ConsumePlayerResource: removeResource false (inv=%p item=%d)",
inventoryPtr, itemId);
s_consumeFailLogCount++;
}
return false;
}
}
__except (EXCEPTION_EXECUTE_HANDLER)
{
if (s_consumeFailLogCount < 20)
{
LogUtil::Log("[WeaveLoader] ConsumePlayerResource: exception (inv=%p item=%d)", inventoryPtr, itemId);
s_consumeFailLogCount++;
}
return false;
}
}
return true;
}
bool DamageItemInstance(void* itemInstancePtr, int amount, void* ownerSharedPtr)
{
if (!itemInstancePtr || !ownerSharedPtr || amount <= 0 || !s_itemInstanceHurtAndBreak)
return false;
__try
{
s_itemInstanceHurtAndBreak(itemInstancePtr, amount, ownerSharedPtr);
return true;
}
__except (EXCEPTION_EXECUTE_HANDLER)
{
return false;
}
}
static bool TryReadLookVector(void* playerPtr, double& dx, double& dy, double& dz)
{
dx = 0.0;
dy = 0.0;
dz = 0.0;
if (!playerPtr)
return false;
void* vec = nullptr;
if (s_livingEntityGetViewVector)
vec = s_livingEntityGetViewVector(playerPtr, 1.0f);
else if (s_entityGetLookAngle)
vec = s_entityGetLookAngle(playerPtr);
if (!IsReadableRange(vec, sizeof(double) * 3))
return false;
dx = *reinterpret_cast<double*>(static_cast<char*>(vec) + 0x00);
dy = *reinterpret_cast<double*>(static_cast<char*>(vec) + 0x08);
dz = *reinterpret_cast<double*>(static_cast<char*>(vec) + 0x10);
return true;
}
static bool LooksLikeEntityPtr(void* candidate)
{
if (!candidate || !IsReadableRange(candidate, sizeof(void*)))
return false;
void* vt = *reinterpret_cast<void**>(candidate);
if (!IsCanonicalUserPtr(vt))
return false;
if (!IsGameCodePtr(vt))
return false;
return true;
}
static bool TryReadPlayerPos(void* playerPtr, double& x, double& y, double& z)
{
x = y = z = 0.0;
if (!playerPtr)
return false;
char* base = static_cast<char*>(playerPtr);
if (!IsReadableRange(base + kEntityXOffset, sizeof(double)) ||
!IsReadableRange(base + kEntityYOffset, sizeof(double)) ||
!IsReadableRange(base + kEntityZOffset, sizeof(double)))
return false;
x = *reinterpret_cast<double*>(base + kEntityXOffset);
y = *reinterpret_cast<double*>(base + kEntityYOffset);
z = *reinterpret_cast<double*>(base + kEntityZOffset);
return x > -32000000.0 && x < 32000000.0 &&
y > -2048.0 && y < 4096.0 &&
z > -32000000.0 && z < 32000000.0;
}
static bool IsEntityMarkedRemoved(void* entityPtr)
{
if (!entityPtr || !IsReadableRange(static_cast<char*>(entityPtr) + kEntityRemovedOffset, sizeof(bool)))
return true;
return *reinterpret_cast<bool*>(static_cast<char*>(entityPtr) + kEntityRemovedOffset);
}
static void MarkEntityRemoved(void* entityPtr)
{
if (!entityPtr || !IsReadableRange(static_cast<char*>(entityPtr) + kEntityRemovedOffset, sizeof(bool)))
return;
*reinterpret_cast<bool*>(static_cast<char*>(entityPtr) + kEntityRemovedOffset) = true;
}
static void TrackSpawnedEntity(void* entityPtr)
{
if (!entityPtr)
return;
s_spawnedEntities.push_back(entityPtr);
}
static void CullSpawnedEntitiesBelowWorld()
{
if (s_spawnedEntities.empty())
return;
size_t write = 0;
for (size_t read = 0; read < s_spawnedEntities.size(); ++read)
{
void* entityPtr = s_spawnedEntities[read];
if (!entityPtr || !LooksLikeEntityPtr(entityPtr))
continue;
if (IsEntityMarkedRemoved(entityPtr))
continue;
double x = 0.0, y = 0.0, z = 0.0;
if (!TryReadPlayerPos(entityPtr, x, y, z))
continue;
if (y < -1.0)
{
MarkEntityRemoved(entityPtr);
if (s_outOfWorldGuardLogCount < 20)
{
LogUtil::Log("[WeaveLoader] OutOfWorldGuard: removed spawned entity=%p at y=%.3f", entityPtr, y);
s_outOfWorldGuardLogCount++;
}
continue;
}
s_spawnedEntities[write++] = entityPtr;
}
s_spawnedEntities.resize(write);
}
bool SummonEntityFromPlayerLook(void* playerPtr,
void* playerSharedPtr,
int entityNumericId,
double speed,
double spawnForward,
double spawnUp)
{
static int s_summonFailLogCount = 0;
void* levelPtr = s_activeUseLevel ? s_activeUseLevel : s_currentLevel;
if (!levelPtr || !s_levelAddEntity || !playerPtr)
{
if (s_summonFailLogCount < 20)
{
LogUtil::Log("[WeaveLoader] SummonFromLook fail: preconditions level=%p add=%p player=%p shared=%p id=%d",
levelPtr, s_levelAddEntity, playerPtr, playerSharedPtr, entityNumericId);
s_summonFailLogCount++;
}
return false;
}
double dx = 0.0, dy = 0.0, dz = 0.0;
if (!TryReadLookVector(playerPtr, dx, dy, dz))
{
if (s_summonFailLogCount < 20)
{
LogUtil::Log("[WeaveLoader] SummonFromLook fail: look vector unavailable (player=%p)", playerPtr);
s_summonFailLogCount++;
}
return false;
}
double px = 0.0, py = 0.0, pz = 0.0;
if (!TryReadPlayerPos(playerPtr, px, py, pz))
{
if (s_summonFailLogCount < 20)
{
LogUtil::Log("[WeaveLoader] SummonFromLook fail: player position unavailable (player=%p)", playerPtr);
s_summonFailLogCount++;
}
return false;
}
const double sx = px + (dx * spawnForward);
const double sy = py + spawnUp + (dy * spawnForward);
const double sz = pz + (dz * spawnForward);
std::shared_ptr<void> entity;
if (!s_entityIoNewById)
{
if (s_summonFailLogCount < 20)
{
LogUtil::Log("[WeaveLoader] SummonFromLook fail: newById unavailable (id=%d)", entityNumericId);
s_summonFailLogCount++;
}
return false;
}
s_entityIoNewById(&entity, entityNumericId, levelPtr);
if (!entity)
{
if (s_summonFailLogCount < 20)
{
LogUtil::Log("[WeaveLoader] SummonFromLook fail: EntityIO::newById returned null (id=%d)", entityNumericId);
s_summonFailLogCount++;
}
return false;
}
if (s_entityMoveTo)
s_entityMoveTo(entity.get(), sx, sy, sz, 0.0f, 0.0f);
else if (s_entitySetPos)
s_entitySetPos(entity.get(), sx, sy, sz);
if (s_entityLerpMotion)
s_entityLerpMotion(entity.get(), dx * speed, dy * speed, dz * speed);
if (IsFireballFamilyEntityId(entityNumericId) &&
IsReadableRange(entity.get(), kFireballZPowerOffset + sizeof(double)))
{
// Source + PDB: Fireball packets initialize xPower/yPower/zPower separately from xd/yd/zd.
// EntityIO::newById gives us a game-owned shared_ptr<Entity>; patch the fireball fields in place.
*reinterpret_cast<void**>(static_cast<char*>(entity.get()) + kFireballOwnerOffset) = nullptr;
*reinterpret_cast<double*>(static_cast<char*>(entity.get()) + kFireballXPowerOffset) = dx * 0.10;
*reinterpret_cast<double*>(static_cast<char*>(entity.get()) + kFireballYPowerOffset) = dy * 0.10;
*reinterpret_cast<double*>(static_cast<char*>(entity.get()) + kFireballZPowerOffset) = dz * 0.10;
}
bool added = s_levelAddEntity(levelPtr, &entity);
if (!added && s_summonFailLogCount < 20)
{
LogUtil::Log("[WeaveLoader] SummonFromLook fail: Level::addEntity returned false (id=%d)", entityNumericId);
s_summonFailLogCount++;
}
if (added)
{
TrackSpawnedEntity(entity.get());
DotNetHost::CallEntitySummoned(entityNumericId, static_cast<float>(sx), static_cast<float>(sy), static_cast<float>(sz));
}
return added;
}
bool SummonEntityByNumericId(int entityNumericId, double x, double y, double z)
{
void* levelPtr = s_activeUseLevel ? s_activeUseLevel : s_currentLevel;
if (!levelPtr || !s_levelAddEntity || !s_entityIoNewById)
return false;
std::shared_ptr<void> entity;
s_entityIoNewById(&entity, entityNumericId, levelPtr);
if (!entity)
return false;
if (s_entityMoveTo)
s_entityMoveTo(entity.get(), x, y, z, 0.0f, 0.0f);
bool added = s_levelAddEntity(levelPtr, &entity);
if (added)
{
TrackSpawnedEntity(entity.get());
DotNetHost::CallEntitySummoned(entityNumericId, static_cast<float>(x), static_cast<float>(y), static_cast<float>(z));
}
return added;
}
void SetAtlasLocationPointers(void* blocksLocation, void* itemsLocation)
{
s_textureAtlasLocationBlocks = blocksLocation;
s_textureAtlasLocationItems = itemsLocation;
s_textureAtlasIdBlocks = -1;
s_textureAtlasIdItems = -1;
const ResourceLocationNative* blocks = reinterpret_cast<const ResourceLocationNative*>(blocksLocation);
const ResourceLocationNative* items = reinterpret_cast<const ResourceLocationNative*>(itemsLocation);
if (blocks && blocks->textures.data && blocks->textures.length > 0)
s_textureAtlasIdBlocks = blocks->textures.data[0];
if (items && items->textures.data && items->textures.length > 0)
s_textureAtlasIdItems = items->textures.data[0];
LogUtil::Log("[WeaveLoader] Atlas IDs: blocks=%d items=%d", s_textureAtlasIdBlocks, s_textureAtlasIdItems);
}
void SetTileTilesArray(void* tilesArray)
{
s_tileTilesArray = tilesArray;
}
static bool TryReadVec3(void* vecPtr, double& x, double& y, double& z)
{
if (!IsReadableRange(vecPtr, sizeof(double) * 3))
return false;
x = *reinterpret_cast<double*>(static_cast<char*>(vecPtr) + 0x00);
y = *reinterpret_cast<double*>(static_cast<char*>(vecPtr) + 0x08);
z = *reinterpret_cast<double*>(static_cast<char*>(vecPtr) + 0x10);
return true;
}
static bool IsFireballFamilyEntityId(int entityNumericId)
{
return entityNumericId == 12
|| entityNumericId == 13
|| entityNumericId == 19
|| entityNumericId == 1000;
}
static void* DecodeItemInstancePtrFromSharedArg(void* sharedArg)
{
if (!sharedArg || !IsCanonicalUserPtr(sharedArg))
return nullptr;
// Candidate A: shared_ptr<ItemInstance> object where first field is raw ItemInstance*.
__try
{
void* p = *reinterpret_cast<void**>(sharedArg);
if (p && IsCanonicalUserPtr(p))
{
int id = 0;
if (TryReadItemId(p, id)) return p;
}
}
__except (EXCEPTION_EXECUTE_HANDLER) {}
// Candidate B: argument itself is already ItemInstance*.
__try
{
int id = 0;
if (TryReadItemId(sharedArg, id)) return sharedArg;
}
__except (EXCEPTION_EXECUTE_HANDLER) {}
return nullptr;
}
static void* DecodePlayerPtrFromSharedArg(void* sharedArg)
{
static int s_decodePlayerLogCount = 0;
if (!sharedArg || !IsCanonicalUserPtr(sharedArg))
return nullptr;
// shared_ptr<Player> is passed by reference; first field is raw Player*.
__try
{
void* p = *reinterpret_cast<void**>(sharedArg);
if (LooksLikeEntityPtr(p))
{
if (s_decodePlayerLogCount < 8)
{
LogUtil::Log("[WeaveLoader] DecodePlayer: shared=%p -> player=%p", sharedArg, p);
s_decodePlayerLogCount++;
}
return p;
}
}
__except (EXCEPTION_EXECUTE_HANDLER) {}
if (s_decodePlayerLogCount < 8)
{
LogUtil::Log("[WeaveLoader] DecodePlayer: failed shared=%p", sharedArg);
s_decodePlayerLogCount++;
}
return nullptr;
}
static int TryReadTileId(void* tilePtr)
{
if (!tilePtr || !IsReadableRange(static_cast<char*>(tilePtr) + kTileIdOffset, sizeof(int)))
return -1;
return *reinterpret_cast<int*>(static_cast<char*>(tilePtr) + kTileIdOffset);
}
static void DispatchManagedBlockUpdate(void* tilePtr, void* level, int x, int y, int z, int eventKind, int neighborBlockId)
{
const int blockId = TryReadTileId(tilePtr);
if (!ManagedBlockRegistry::IsManaged(blockId))
return;
int isClientSide = 0;
if (level && IsReadableRange(static_cast<char*>(level) + kLevelIsClientSideOffset, sizeof(bool)))
isClientSide = *reinterpret_cast<bool*>(static_cast<char*>(level) + kLevelIsClientSideOffset) ? 1 : 0;
struct BlockUpdateNativeArgs
{
int blockId;
int isClientSide;
void* levelPtr;
int x;
int y;
int z;
};
struct BlockNeighborChangedNativeArgs
{
BlockUpdateNativeArgs block;
int neighborBlockId;
};
if (eventKind == 0)
{
BlockUpdateNativeArgs args{ blockId, isClientSide, level, x, y, z };
DotNetHost::CallBlockOnPlace(&args, sizeof(args));
}
else if (eventKind == 1)
{
BlockNeighborChangedNativeArgs args{};
args.block = { blockId, isClientSide, level, x, y, z };
args.neighborBlockId = neighborBlockId;
DotNetHost::CallBlockNeighborChanged(&args, sizeof(args));
}
else if (eventKind == 2)
{
BlockUpdateNativeArgs args{ blockId, isClientSide, level, x, y, z };
DotNetHost::CallBlockTick(&args, sizeof(args));
}
}
static void DispatchManagedBlockById(int blockId, void* level, int x, int y, int z, int eventKind, int neighborBlockId)
{
if (!ManagedBlockRegistry::IsManaged(blockId))
return;
int isClientSide = 0;
if (level && IsReadableRange(static_cast<char*>(level) + kLevelIsClientSideOffset, sizeof(bool)))
isClientSide = *reinterpret_cast<bool*>(static_cast<char*>(level) + kLevelIsClientSideOffset) ? 1 : 0;
struct BlockUpdateNativeArgs
{
int blockId;
int isClientSide;
void* levelPtr;
int x;
int y;
int z;
};
struct BlockNeighborChangedNativeArgs
{
BlockUpdateNativeArgs block;
int neighborBlockId;
};
if (eventKind == 0)
{
BlockUpdateNativeArgs args{ blockId, isClientSide, level, x, y, z };
DotNetHost::CallBlockOnPlace(&args, sizeof(args));
}
else if (eventKind == 1)
{
BlockNeighborChangedNativeArgs args{};
args.block = { blockId, isClientSide, level, x, y, z };
args.neighborBlockId = neighborBlockId;
DotNetHost::CallBlockNeighborChanged(&args, sizeof(args));
}
else if (eventKind == 2)
{
BlockUpdateNativeArgs args{ blockId, isClientSide, level, x, y, z };
DotNetHost::CallBlockTick(&args, sizeof(args));
}
}
void __fastcall Hooked_TileOnPlace(void* thisPtr, void* level, int x, int y, int z)
{
if (Original_TileOnPlace)
Original_TileOnPlace(thisPtr, level, x, y, z);
DispatchManagedBlockUpdate(thisPtr, level, x, y, z, 0, 0);
}
void __fastcall Hooked_TileNeighborChanged(void* thisPtr, void* level, int x, int y, int z, int type)
{
if (Original_TileNeighborChanged)
Original_TileNeighborChanged(thisPtr, level, x, y, z, type);
DispatchManagedBlockUpdate(thisPtr, level, x, y, z, 1, type);
}
void __fastcall Hooked_TileTick(void* thisPtr, void* level, int x, int y, int z, void* random)
{
if (Original_TileTick)
Original_TileTick(thisPtr, level, x, y, z, random);
DispatchManagedBlockUpdate(thisPtr, level, x, y, z, 2, 0);
}
bool __fastcall Hooked_LevelSetTileAndData(void* thisPtr, int x, int y, int z, int tile, int data, int updateFlags)
{
const bool result = Original_LevelSetTileAndData
? Original_LevelSetTileAndData(thisPtr, x, y, z, tile, data, updateFlags)
: false;
if (result && tile > 0)
DispatchManagedBlockById(tile, thisPtr, x, y, z, 0, 0);
return result;
}
bool __fastcall Hooked_LevelSetData(void* thisPtr, int x, int y, int z, int data, int updateFlags, bool forceUpdate)
{
const bool result = Original_LevelSetData
? Original_LevelSetData(thisPtr, x, y, z, data, updateFlags, forceUpdate)
: false;
return result;
}
void __fastcall Hooked_LevelUpdateNeighborsAt(void* thisPtr, int x, int y, int z, int type)
{
if (Original_LevelUpdateNeighborsAt)
Original_LevelUpdateNeighborsAt(thisPtr, x, y, z, type);
if (!s_levelGetTile)
return;
static const int kNeighborOffsets[6][3] = {
{-1, 0, 0}, {1, 0, 0},
{0, -1, 0}, {0, 1, 0},
{0, 0, -1}, {0, 0, 1}
};
for (const auto& offset : kNeighborOffsets)
{
const int nx = x + offset[0];
const int ny = y + offset[1];
const int nz = z + offset[2];
const int neighborBlockId = s_levelGetTile(thisPtr, nx, ny, nz);
DispatchManagedBlockById(neighborBlockId, thisPtr, nx, ny, nz, 1, type);
}
}
bool __fastcall Hooked_ServerLevelTickPendingTicks(void* thisPtr, bool force)
{
const bool originalResult = Original_ServerLevelTickPendingTicks
? Original_ServerLevelTickPendingTicks(thisPtr, force)
: false;
bool anyPending = false;
for (auto it = s_managedScheduledTicks.begin(); it != s_managedScheduledTicks.end();)
{
if (it->levelPtr != thisPtr)
{
++it;
continue;
}
--it->remainingTicks;
if (it->remainingTicks <= 0)
{
DispatchManagedBlockById(it->blockId, it->levelPtr, it->x, it->y, it->z, 2, 0);
it = s_managedScheduledTicks.erase(it);
}
else
{
anyPending = true;
++it;
}
}
return originalResult || anyPending;
}
void* __fastcall Hooked_McRegionChunkStorageLoad(void* thisPtr, void* level, int x, int z)
{
static int s_chunkLoadLogCount = 0;
void* levelChunk = Original_McRegionChunkStorageLoad
? Original_McRegionChunkStorageLoad(thisPtr, level, x, z)
: nullptr;
if (levelChunk)
{
const int remapped = WorldIdRemap::RemapChunkBlockIds(thisPtr, levelChunk, x, z);
if (s_chunkLoadLogCount < 64)
{
++s_chunkLoadLogCount;
LogUtil::Log("[WeaveLoader] WorldIdRemap chunk load: x=%d z=%d remapped=%d", x, z, remapped);
}
}
return levelChunk;
}
void __fastcall Hooked_McRegionChunkStorageSave(void* thisPtr, void* level, void* levelChunk)
{
WorldIdRemap::SaveChunkBlockNamespaces(thisPtr, levelChunk);
if (Original_McRegionChunkStorageSave)
Original_McRegionChunkStorageSave(thisPtr, level, levelChunk);
}
int __fastcall Hooked_TileGetResource(void* thisPtr, int data, void* random, int playerBonusLevel)
{
const ManagedBlockRegistry::Definition* def = ManagedBlockRegistry::Find(TryReadTileId(thisPtr));
if (def && def->dropBlockId >= 0)
return def->dropBlockId;
return Original_TileGetResource ? Original_TileGetResource(thisPtr, data, random, playerBonusLevel) : 0;
}
int __fastcall Hooked_TileCloneTileId(void* thisPtr, void* level, int x, int y, int z)
{
const ManagedBlockRegistry::Definition* def = ManagedBlockRegistry::Find(TryReadTileId(thisPtr));
if (def && def->cloneBlockId >= 0)
return def->cloneBlockId;
return Original_TileCloneTileId ? Original_TileCloneTileId(thisPtr, level, x, y, z) : TryReadTileId(thisPtr);
}
void* __fastcall Hooked_StoneSlabGetTexture(void* thisPtr, int face, int data)
{
if (CustomSlabRegistry::Find(TryReadTileId(thisPtr)))
{
if (void* iconPtr = TryReadTileIcon(thisPtr))
return iconPtr;
}
return Original_StoneSlabGetTexture ? Original_StoneSlabGetTexture(thisPtr, face, data) : nullptr;
}
void* __fastcall Hooked_WoodSlabGetTexture(void* thisPtr, int face, int data)
{
if (CustomSlabRegistry::Find(TryReadTileId(thisPtr)))
{
if (void* iconPtr = TryReadTileIcon(thisPtr))
return iconPtr;
}
return Original_WoodSlabGetTexture ? Original_WoodSlabGetTexture(thisPtr, face, data) : nullptr;
}
int __fastcall Hooked_StoneSlabGetResource(void* thisPtr, int data, void* random, int playerBonusLevel)
{
const CustomSlabRegistry::Definition* def = CustomSlabRegistry::Find(TryReadTileId(thisPtr));
if (def)
return def->halfBlockId;
return Original_StoneSlabGetResource ? Original_StoneSlabGetResource(thisPtr, data, random, playerBonusLevel) : 0;
}
int __fastcall Hooked_WoodSlabGetResource(void* thisPtr, int data, void* random, int playerBonusLevel)
{
const CustomSlabRegistry::Definition* def = CustomSlabRegistry::Find(TryReadTileId(thisPtr));
if (def)
return def->halfBlockId;
return Original_WoodSlabGetResource ? Original_WoodSlabGetResource(thisPtr, data, random, playerBonusLevel) : 0;
}
unsigned int __fastcall Hooked_StoneSlabGetDescriptionId(void* thisPtr, int data)
{
const CustomSlabRegistry::Definition* def = CustomSlabRegistry::Find(TryReadTileId(thisPtr));
if (def && def->descriptionId >= 0)
return static_cast<unsigned int>(def->descriptionId);
return Original_StoneSlabGetDescriptionId ? Original_StoneSlabGetDescriptionId(thisPtr, data) : 0;
}
unsigned int __fastcall Hooked_WoodSlabGetDescriptionId(void* thisPtr, int data)
{
const CustomSlabRegistry::Definition* def = CustomSlabRegistry::Find(TryReadTileId(thisPtr));
if (def && def->descriptionId >= 0)
return static_cast<unsigned int>(def->descriptionId);
return Original_WoodSlabGetDescriptionId ? Original_WoodSlabGetDescriptionId(thisPtr, data) : 0;
}
int __fastcall Hooked_StoneSlabGetAuxName(void* thisPtr, int auxValue)
{
const CustomSlabRegistry::Definition* def = CustomSlabRegistry::Find(TryReadTileId(thisPtr));
if (def && def->descriptionId >= 0)
return def->descriptionId;
return Original_StoneSlabGetAuxName ? Original_StoneSlabGetAuxName(thisPtr, auxValue) : 0;
}
int __fastcall Hooked_WoodSlabGetAuxName(void* thisPtr, int auxValue)
{
const CustomSlabRegistry::Definition* def = CustomSlabRegistry::Find(TryReadTileId(thisPtr));
if (def && def->descriptionId >= 0)
return def->descriptionId;
return Original_WoodSlabGetAuxName ? Original_WoodSlabGetAuxName(thisPtr, auxValue) : 0;
}
void __fastcall Hooked_StoneSlabRegisterIcons(void* thisPtr, void* iconRegister)
{
if (Original_StoneSlabRegisterIcons)
Original_StoneSlabRegisterIcons(thisPtr, iconRegister);
}
void __fastcall Hooked_WoodSlabRegisterIcons(void* thisPtr, void* iconRegister)
{
if (Original_WoodSlabRegisterIcons)
Original_WoodSlabRegisterIcons(thisPtr, iconRegister);
}
void* __fastcall Hooked_StoneSlabItemGetIcon(void* thisPtr, int auxValue)
{
if (thisPtr && IsReadableRange(static_cast<const char*>(thisPtr) + kItemIdOffset, sizeof(int)))
{
const int itemId = *reinterpret_cast<const int*>(static_cast<const char*>(thisPtr) + kItemIdOffset);
const CustomSlabRegistry::Definition* def = CustomSlabRegistry::Find(itemId);
if (def && s_tileTilesArray && IsReadableRange(s_tileTilesArray, sizeof(void*)))
{
const void* arrayPtr = *reinterpret_cast<const void* const*>(s_tileTilesArray);
if (arrayPtr && IsReadableRange(arrayPtr, sizeof(void*) * 4096))
{
auto* tiles = reinterpret_cast<void* const*>(const_cast<void*>(arrayPtr));
if (def->halfBlockId >= 0 && def->halfBlockId < 4096)
{
void* halfTile = const_cast<void*>(tiles[def->halfBlockId]);
void* iconPtr = TryReadTileIcon(halfTile);
if (iconPtr)
return iconPtr;
}
}
}
}
return Original_StoneSlabItemGetIcon
? Original_StoneSlabItemGetIcon(thisPtr, auxValue)
: nullptr;
}
unsigned int __fastcall Hooked_StoneSlabItemGetDescriptionId(void* thisPtr, void* itemInstanceSharedPtr)
{
if (thisPtr && IsReadableRange(static_cast<const char*>(thisPtr) + kItemIdOffset, sizeof(int)))
{
const int itemId = *reinterpret_cast<const int*>(static_cast<const char*>(thisPtr) + kItemIdOffset);
const CustomSlabRegistry::Definition* def = CustomSlabRegistry::Find(itemId);
if (def && def->descriptionId >= 0)
return static_cast<unsigned int>(def->descriptionId);
}
return Original_StoneSlabItemGetDescriptionId
? Original_StoneSlabItemGetDescriptionId(thisPtr, itemInstanceSharedPtr)
: 0;
}
int __fastcall Hooked_HalfSlabCloneTileId(void* thisPtr, void* level, int x, int y, int z)
{
const CustomSlabRegistry::Definition* def = CustomSlabRegistry::Find(TryReadTileId(thisPtr));
if (def)
return def->halfBlockId;
return Original_HalfSlabCloneTileId ? Original_HalfSlabCloneTileId(thisPtr, level, x, y, z) : TryReadTileId(thisPtr);
}
void __fastcall Hooked_PreStitchedTextureMapStitch(void* thisPtr)
{
const int prevAtlasType = s_activeStitchAtlasType;
int iconType = -1;
if (thisPtr && IsReadableRange(static_cast<const char*>(thisPtr) + 8, sizeof(int)))
iconType = *reinterpret_cast<const int*>(static_cast<const char*>(thisPtr) + 8);
s_activeStitchAtlasType = iconType;
if (Original_PreStitchedTextureMapStitch)
Original_PreStitchedTextureMapStitch(thisPtr);
s_activeStitchAtlasType = prevAtlasType;
}
void __fastcall Hooked_LoadUVs(void* thisPtr)
{
LogUtil::Log("[WeaveLoader] Hooked_LoadUVs: ENTER (textureMap=%p)", thisPtr);
if (thisPtr && IsReadableRange(static_cast<const char*>(thisPtr) + 8, sizeof(int)))
s_activeStitchAtlasType = *reinterpret_cast<const int*>(static_cast<const char*>(thisPtr) + 8);
if (Original_LoadUVs)
Original_LoadUVs(thisPtr);
LogUtil::Log("[WeaveLoader] Hooked_LoadUVs: original returned, creating mod icons");
ModAtlas::CreateModIcons(thisPtr);
PatchCustomSlabIcons();
LogUtil::Log("[WeaveLoader] Hooked_LoadUVs: DONE");
}
static float ApplyAtlasScale(void* iconPtr, float value, bool isU, bool isModIcon)
{
if (isModIcon)
return value;
int atlasType = -1;
if (!ModAtlas::GetIconAtlasType(iconPtr, atlasType))
{
if (s_activeStitchAtlasType >= 0)
atlasType = s_activeStitchAtlasType;
else
return value;
}
float uScale = 1.0f;
float vScale = 1.0f;
if (!ModAtlas::GetAtlasScale(atlasType, uScale, vScale))
return value;
const float scale = isU ? uScale : vScale;
if (scale > 0.9995f && scale < 1.0005f)
return value;
return value * scale;
}
float __fastcall Hooked_StitchedGetU0(void* thisPtr, bool adjust)
{
int atlasType = -1;
int page = 0;
const bool isModIcon = ModAtlas::TryGetIconRoute(thisPtr, atlasType, page);
if (isModIcon && atlasType == 0 && page > 0)
ModAtlas::NotifyIconSampled(thisPtr);
float value = Original_StitchedGetU0 ? Original_StitchedGetU0(thisPtr, adjust) : 0.0f;
return ApplyAtlasScale(thisPtr, value, true, isModIcon);
}
float __fastcall Hooked_StitchedGetU1(void* thisPtr, bool adjust)
{
int atlasType = -1;
int page = 0;
const bool isModIcon = ModAtlas::TryGetIconRoute(thisPtr, atlasType, page);
if (isModIcon && atlasType == 0 && page > 0)
ModAtlas::NotifyIconSampled(thisPtr);
float value = Original_StitchedGetU1 ? Original_StitchedGetU1(thisPtr, adjust) : 0.0f;
return ApplyAtlasScale(thisPtr, value, true, isModIcon);
}
float __fastcall Hooked_StitchedGetV0(void* thisPtr, bool adjust)
{
int atlasType = -1;
int page = 0;
const bool isModIcon = ModAtlas::TryGetIconRoute(thisPtr, atlasType, page);
if (isModIcon && atlasType == 0 && page > 0)
ModAtlas::NotifyIconSampled(thisPtr);
float value = Original_StitchedGetV0 ? Original_StitchedGetV0(thisPtr, adjust) : 0.0f;
return ApplyAtlasScale(thisPtr, value, false, isModIcon);
}
float __fastcall Hooked_StitchedGetV1(void* thisPtr, bool adjust)
{
int atlasType = -1;
int page = 0;
const bool isModIcon = ModAtlas::TryGetIconRoute(thisPtr, atlasType, page);
if (isModIcon && atlasType == 0 && page > 0)
ModAtlas::NotifyIconSampled(thisPtr);
float value = Original_StitchedGetV1 ? Original_StitchedGetV1(thisPtr, adjust) : 0.0f;
return ApplyAtlasScale(thisPtr, value, false, isModIcon);
}
void __fastcall Hooked_TexturesBindTextureResource(void* thisPtr, void* resourcePtr)
{
if (!Original_TexturesBindTextureResource)
return;
const int boundAtlas = DetectAtlasTypeFromResource(resourcePtr);
// Terrain/world rendering binds LOCATION_BLOCKS once for an entire pass.
// Route terrain binds to merged page 1 so vanilla+modded block UVs work together.
if (boundAtlas == 0)
{
std::string terrainPage1 = ModAtlas::GetMergedPagePath(0, 1);
if (!terrainPage1.empty() && resourcePtr)
{
EnsurePageResourcesInitialized();
const ResourceLocationNative* originalRes =
reinterpret_cast<const ResourceLocationNative*>(resourcePtr);
s_pageResource.textures = originalRes->textures;
s_pageResource.path = BuildVirtualAtlasPath(0, 1);
s_pageResource.preloaded = false;
if (s_forcedTerrainRouteLogCount < 20)
{
LogUtil::Log("[WeaveLoader] AtlasRoute: forced terrain page=1 path=%ls",
s_pageResource.path.c_str());
s_forcedTerrainRouteLogCount++;
}
ModAtlas::ClearPendingPage();
Original_TexturesBindTextureResource(thisPtr, &s_pageResource);
return;
}
}
int pendingAtlas = -1;
int pendingPage = 0;
const bool hasPending = ModAtlas::PeekPendingPage(pendingAtlas, pendingPage);
if (hasPending && pendingPage > 0 && resourcePtr)
{
const bool atlasMatches = (boundAtlas == pendingAtlas);
if (atlasMatches)
{
EnsurePageResourcesInitialized();
// Preserve texture-name metadata from the original atlas location.
// Some codepaths consult this even when `preloaded` is false.
const ResourceLocationNative* originalRes =
reinterpret_cast<const ResourceLocationNative*>(resourcePtr);
s_pageResource.textures = originalRes->textures;
s_pageResource.path = BuildVirtualAtlasPath(pendingAtlas, pendingPage);
s_pageResource.preloaded = false;
if (s_pageRouteLogCount < 40)
{
LogUtil::Log("[WeaveLoader] AtlasRoute: atlas=%d page=%d path=%ls",
pendingAtlas, pendingPage, s_pageResource.path.c_str());
}
s_pageRouteLogCount++;
ModAtlas::ClearPendingPage();
Original_TexturesBindTextureResource(thisPtr, &s_pageResource);
return;
}
}
Original_TexturesBindTextureResource(thisPtr, resourcePtr);
}
int __fastcall Hooked_TexturesLoadTextureByName(void* thisPtr, int texId, const std::wstring& resourceName)
{
if (!Original_TexturesLoadTextureByName)
return 0;
if (s_inLoadTextureByNameHook)
return Original_TexturesLoadTextureByName(thisPtr, texId, resourceName);
int atlasType = -1;
int page = 0;
if (ParseVirtualAtlasRequest(resourceName, atlasType, page) && page > 0)
{
const int remappedTexId =
(atlasType == 0) ? s_textureAtlasIdBlocks :
(atlasType == 1) ? s_textureAtlasIdItems : -1;
if (remappedTexId >= 0)
{
s_inLoadTextureByNameHook = true;
int routed = Original_TexturesLoadTextureByName(thisPtr, remappedTexId, resourceName);
s_inLoadTextureByNameHook = false;
return routed;
}
}
return Original_TexturesLoadTextureByName(thisPtr, texId, resourceName);
}
int __fastcall Hooked_TexturesLoadTextureByIndex(void* thisPtr, int idx)
{
if (!Original_TexturesLoadTextureByIndex)
return 0;
// Some world/render paths bind TN_TERRAIN directly via loadTexture(int).
// Route those binds to terrain page 1 so modded placed blocks remain visible.
if (idx == s_textureAtlasIdBlocks && Original_TexturesLoadTextureByName)
{
std::string terrainPage1 = ModAtlas::GetMergedPagePath(0, 1);
if (!terrainPage1.empty())
{
const std::wstring virtualPath = BuildVirtualAtlasPath(0, 1);
return Original_TexturesLoadTextureByName(thisPtr, idx, virtualPath);
}
}
return Original_TexturesLoadTextureByIndex(thisPtr, idx);
}
static bool IsModLoaderGeneratedPath(const std::wstring& lower)
{
return (lower.find(L"/mods/modloader/generated/") != std::wstring::npos) ||
(lower.find(L"/modloader/") != std::wstring::npos);
}
static std::wstring ToLowerSimple(const std::wstring& s)
{
std::wstring lower;
lower.reserve(s.size());
for (wchar_t ch : s)
lower.push_back((wchar_t)towlower(ch));
return lower;
}
static std::unordered_map<void*, std::wstring> s_textureNames;
void* __fastcall Hooked_TexturesReadImage(void* thisPtr, int texId, const std::wstring& name)
{
if (!Original_TexturesReadImage)
return nullptr;
void* img = Original_TexturesReadImage(thisPtr, texId, name);
if (!img)
return img;
std::wstring lower = NormalizeLowerPath(name);
if (IsModLoaderGeneratedPath(lower))
return img;
if (EndsWithPath(lower, L"terrain.png"))
{
ModAtlas::OverrideAtlasFromBufferedImage(0, img);
}
else if (EndsWithPath(lower, L"items.png"))
{
ModAtlas::OverrideAtlasFromBufferedImage(1, img);
}
return img;
}
void __fastcall Hooked_BufferedImageCtorFile(void* thisPtr, const std::wstring& file, bool filenameHasExtension, bool bTitleUpdateTexture, const std::wstring& drive)
{
if (Original_BufferedImageCtorFile)
Original_BufferedImageCtorFile(thisPtr, file, filenameHasExtension, bTitleUpdateTexture, drive);
// Intentionally left empty: handled in Textures::readImage hook to avoid
// constructor-level crashes during boot.
}
void __fastcall Hooked_BufferedImageCtorDLCPack(void* thisPtr, void* dlcPack, const std::wstring& file, bool filenameHasExtension)
{
if (Original_BufferedImageCtorDLCPack)
Original_BufferedImageCtorDLCPack(thisPtr, dlcPack, file, filenameHasExtension);
// Intentionally left empty: handled in Textures::readImage hook to avoid
// constructor-level crashes during boot.
}
void* __fastcall Hooked_TextureManagerCreateTexture(void* thisPtr, const std::wstring& name, int mode, int width, int height, int wrap, int format, int minFilter, int magFilter, bool mipmap, void* image)
{
if (!Original_TextureManagerCreateTexture)
return nullptr;
void* tex = Original_TextureManagerCreateTexture(thisPtr, name, mode, width, height, wrap, format, minFilter, magFilter, mipmap, image);
if (tex)
{
std::wstring lower = ToLowerSimple(name);
if (lower == L"terrain" || lower == L"items")
s_textureNames[tex] = lower;
}
return tex;
}
void __fastcall Hooked_TextureTransferFromImage(void* thisPtr, void* image)
{
if (!Original_TextureTransferFromImage)
return;
auto it = s_textureNames.find(thisPtr);
if (it != s_textureNames.end() && image)
{
if (it->second == L"terrain")
ModAtlas::OverrideAtlasFromBufferedImage(0, image);
else if (it->second == L"items")
ModAtlas::OverrideAtlasFromBufferedImage(1, image);
}
Original_TextureTransferFromImage(thisPtr, image);
}
static void TryOverrideAtlasFromPackImage(const std::wstring& file, void* image)
{
if (!image) return;
std::wstring lower = NormalizeLowerPath(file);
if (IsModLoaderGeneratedPath(lower))
return;
if (EndsWithPath(lower, L"terrain.png"))
{
ModAtlas::OverrideAtlasFromBufferedImage(0, image);
}
else if (EndsWithPath(lower, L"items.png"))
{
ModAtlas::OverrideAtlasFromBufferedImage(1, image);
}
}
void* __fastcall Hooked_AbstractTexturePackGetImageResource(void* thisPtr, const std::wstring& file, bool filenameHasExtension, bool bTitleUpdateTexture, const std::wstring& drive)
{
if (!Original_AbstractTexturePackGetImageResource)
return nullptr;
void* img = Original_AbstractTexturePackGetImageResource(thisPtr, file, filenameHasExtension, bTitleUpdateTexture, drive);
TryOverrideAtlasFromPackImage(file, img);
return img;
}
void* __fastcall Hooked_DLCTexturePackGetImageResource(void* thisPtr, const std::wstring& file, bool filenameHasExtension, bool bTitleUpdateTexture, const std::wstring& drive)
{
if (!Original_DLCTexturePackGetImageResource)
return nullptr;
void* img = Original_DLCTexturePackGetImageResource(thisPtr, file, filenameHasExtension, bTitleUpdateTexture, drive);
TryOverrideAtlasFromPackImage(file, img);
return img;
}
static int s_registerIconCallCount = 0;
void* __fastcall Hooked_RegisterIcon(void* thisPtr, const std::wstring& name)
{
s_registerIconCallCount++;
int iconType = -1;
if (thisPtr && IsReadableRange(static_cast<const char*>(thisPtr) + 8, sizeof(int)))
iconType = *reinterpret_cast<const int*>(static_cast<const char*>(thisPtr) + 8);
void* modIcon = ModAtlas::LookupModIcon(name);
if (modIcon)
{
if (iconType >= 0)
ModAtlas::NoteIconAtlasType(modIcon, iconType);
LogUtil::Log("[WeaveLoader] registerIcon #%d: '%ls' -> MOD ICON %p",
s_registerIconCallCount, name.c_str(), modIcon);
return modIcon;
}
void* result = Original_RegisterIcon ? Original_RegisterIcon(thisPtr, name) : nullptr;
if (result && iconType >= 0)
ModAtlas::NoteIconAtlasType(result, iconType);
if (s_registerIconCallCount <= 30 || !result)
{
LogUtil::Log("[WeaveLoader] registerIcon #%d: '%ls' -> vanilla %p",
s_registerIconCallCount, name.c_str(), result);
}
return result;
}
void* __fastcall Hooked_ItemInstanceGetIcon(void* thisPtr)
{
if (!Original_ItemInstanceGetIcon)
return nullptr;
void* icon = Original_ItemInstanceGetIcon(thisPtr);
if (icon)
ModAtlas::NotifyIconSampled(icon);
else
ModAtlas::ClearPendingPage();
return icon;
}
void __fastcall Hooked_EntityRendererBindTextureResource(void* thisPtr, void* resourcePtr)
{
if (!Original_EntityRendererBindTextureResource)
return;
const int boundAtlas = DetectAtlasTypeFromResource(resourcePtr);
if (s_hasForcedBillboardRoute && resourcePtr && boundAtlas == s_forcedBillboardAtlas && s_forcedBillboardPage > 0)
{
EnsurePageResourcesInitialized();
const ResourceLocationNative* originalRes =
reinterpret_cast<const ResourceLocationNative*>(resourcePtr);
s_pageResource.textures = originalRes->textures;
s_pageResource.path = BuildVirtualAtlasPath(s_forcedBillboardAtlas, s_forcedBillboardPage);
s_pageResource.preloaded = false;
Original_EntityRendererBindTextureResource(thisPtr, &s_pageResource);
return;
}
Original_EntityRendererBindTextureResource(thisPtr, resourcePtr);
}
void __fastcall Hooked_ItemRendererRenderItemBillboard(void* thisPtr, void* entitySharedPtr, void* iconPtr, int count, float a, float red, float green, float blue)
{
const bool hadForcedRoute = s_hasForcedBillboardRoute;
const int prevAtlas = s_forcedBillboardAtlas;
const int prevPage = s_forcedBillboardPage;
int atlasType = -1;
int page = 0;
if (iconPtr && ModAtlas::TryGetIconRoute(iconPtr, atlasType, page) && page > 0)
{
s_hasForcedBillboardRoute = true;
s_forcedBillboardAtlas = atlasType;
s_forcedBillboardPage = page;
}
if (Original_ItemRendererRenderItemBillboard)
Original_ItemRendererRenderItemBillboard(thisPtr, entitySharedPtr, iconPtr, count, a, red, green, blue);
s_hasForcedBillboardRoute = hadForcedRoute;
s_forcedBillboardAtlas = prevAtlas;
s_forcedBillboardPage = prevPage;
}
static void LogAnimatedTextureGuard(const char* what, void* thisPtr)
{
if (s_animatedTextureGuardLogCount >= 12)
return;
LogUtil::Log("[WeaveLoader] AnimatedTextureGuard: %s fallback for %p", what, thisPtr);
s_animatedTextureGuardLogCount++;
}
void __fastcall Hooked_CompassTextureCycleFrames(void* thisPtr)
{
if (!Original_CompassTextureCycleFrames)
return;
__try
{
Original_CompassTextureCycleFrames(thisPtr);
}
__except (EXCEPTION_EXECUTE_HANDLER)
{
LogAnimatedTextureGuard("CompassTexture::cycleFrames", thisPtr);
}
}
void __fastcall Hooked_ClockTextureCycleFrames(void* thisPtr)
{
if (!Original_ClockTextureCycleFrames)
return;
__try
{
Original_ClockTextureCycleFrames(thisPtr);
}
__except (EXCEPTION_EXECUTE_HANDLER)
{
LogAnimatedTextureGuard("ClockTexture::cycleFrames", thisPtr);
}
}
int __fastcall Hooked_CompassTextureGetSourceWidth(void* thisPtr)
{
if (!Original_CompassTextureGetSourceWidth)
return 16;
__try
{
return Original_CompassTextureGetSourceWidth(thisPtr);
}
__except (EXCEPTION_EXECUTE_HANDLER)
{
LogAnimatedTextureGuard("CompassTexture::getSourceWidth", thisPtr);
return 16;
}
}
int __fastcall Hooked_CompassTextureGetSourceHeight(void* thisPtr)
{
if (!Original_CompassTextureGetSourceHeight)
return 16;
__try
{
return Original_CompassTextureGetSourceHeight(thisPtr);
}
__except (EXCEPTION_EXECUTE_HANDLER)
{
LogAnimatedTextureGuard("CompassTexture::getSourceHeight", thisPtr);
return 16;
}
}
int __fastcall Hooked_ClockTextureGetSourceWidth(void* thisPtr)
{
if (!Original_ClockTextureGetSourceWidth)
return 16;
__try
{
return Original_ClockTextureGetSourceWidth(thisPtr);
}
__except (EXCEPTION_EXECUTE_HANDLER)
{
LogAnimatedTextureGuard("ClockTexture::getSourceWidth", thisPtr);
return 16;
}
}
int __fastcall Hooked_ClockTextureGetSourceHeight(void* thisPtr)
{
if (!Original_ClockTextureGetSourceHeight)
return 16;
__try
{
return Original_ClockTextureGetSourceHeight(thisPtr);
}
__except (EXCEPTION_EXECUTE_HANDLER)
{
LogAnimatedTextureGuard("ClockTexture::getSourceHeight", thisPtr);
return 16;
}
}
void __fastcall Hooked_ItemInstanceMineBlock(void* thisPtr, void* level, int tile, int x, int y, int z, void* ownerSharedPtr)
{
s_itemMineBlockHookCalls++;
int action = TryDispatchMineBlockFromItemInstancePtr(thisPtr, tile, x, y, z, "ItemInstance::mineBlock");
if (action == 2)
{
// Managed item explicitly canceled vanilla mineBlock behavior.
return;
}
if (Original_ItemInstanceMineBlock)
Original_ItemInstanceMineBlock(thisPtr, level, tile, x, y, z, ownerSharedPtr);
}
void* __fastcall Hooked_ItemInstanceSave(void* thisPtr, void* compoundTagPtr)
{
// Namespace marker now lives on ItemInstance::tag, so it must be present
// before vanilla serialization copies that nested tag into the output.
WorldIdRemap::TagModdedItemInstance(thisPtr, compoundTagPtr);
void* outTag = Original_ItemInstanceSave
? Original_ItemInstanceSave(thisPtr, compoundTagPtr)
: compoundTagPtr;
return outTag;
}
void __fastcall Hooked_ItemInstanceLoad(void* thisPtr, void* compoundTagPtr)
{
if (Original_ItemInstanceLoad)
Original_ItemInstanceLoad(thisPtr, compoundTagPtr);
WorldIdRemap::RemapItemInstanceFromTag(thisPtr, compoundTagPtr);
}
bool __fastcall Hooked_ItemMineBlock(void* thisPtr, void* itemInstanceSharedPtr, void* level, int tile, int x, int y, int z, void* ownerSharedPtr)
{
s_itemMineBlockHookCalls++;
void* itemInstancePtr = DecodeItemInstancePtrFromSharedArg(itemInstanceSharedPtr);
int action = TryDispatchMineBlockFromItemInstancePtr(itemInstancePtr, tile, x, y, z, "Item::mineBlock");
if (action == 2)
return true;
if (Original_ItemMineBlock)
return Original_ItemMineBlock(thisPtr, itemInstanceSharedPtr, level, tile, x, y, z, ownerSharedPtr);
return false;
}
bool __fastcall Hooked_DiggerItemMineBlock(void* thisPtr, void* itemInstanceSharedPtr, void* level, int tile, int x, int y, int z, void* ownerSharedPtr)
{
s_itemMineBlockHookCalls++;
void* itemInstancePtr = DecodeItemInstancePtrFromSharedArg(itemInstanceSharedPtr);
int action = TryDispatchMineBlockFromItemInstancePtr(itemInstancePtr, tile, x, y, z, "DiggerItem::mineBlock");
if (action == 2)
return true;
if (Original_DiggerItemMineBlock)
return Original_DiggerItemMineBlock(thisPtr, itemInstanceSharedPtr, level, tile, x, y, z, ownerSharedPtr);
return false;
}
static bool TryReadItemIdFromPickaxe(void* pickaxeItemPtr, int& outItemId)
{
if (!pickaxeItemPtr || !IsReadableRange(pickaxeItemPtr, kItemIdOffset + sizeof(int)))
return false;
int itemId = *reinterpret_cast<const int*>(static_cast<const char*>(pickaxeItemPtr) + kItemIdOffset);
if (itemId >= 0 && itemId < 32000)
{
outItemId = itemId;
return true;
}
return false;
}
static constexpr int TILE_NUM_COUNT = 4096;
static bool TryReadTileId(void* tilePtr, int& outTileId)
{
if (!tilePtr)
return false;
if (IsReadableRange(tilePtr, kTileIdOffset + sizeof(int)))
{
int id = *reinterpret_cast<const int*>(static_cast<const char*>(tilePtr) + kTileIdOffset);
if (id >= 0 && id < TILE_NUM_COUNT)
{
outTileId = id;
return true;
}
}
// Fallback: resolve via Tile::tiles (Tile** - pointer to array). Must dereference once.
if (s_tileTilesArray && IsReadableRange(s_tileTilesArray, sizeof(void*)))
{
const void* arrayPtr = *reinterpret_cast<const void* const*>(s_tileTilesArray);
if (arrayPtr && IsReadableRange(arrayPtr, TILE_NUM_COUNT * sizeof(void*)))
{
const void* const* tiles = reinterpret_cast<const void* const*>(arrayPtr);
for (int i = 0; i < TILE_NUM_COUNT; i++)
{
if (tiles[i] == tilePtr)
{
outTileId = i;
return true;
}
}
}
}
return false;
}
static int GetToolHarvestLevel(void* diggerItemPtr, int itemId)
{
const CustomToolMaterialRegistry::Definition* def = CustomToolMaterialRegistry::Find(itemId);
if (def)
return def->harvestLevel;
if (!diggerItemPtr || !IsReadableRange(diggerItemPtr, 0xA8 + sizeof(void*)))
return -1;
const void* tierPtr = *reinterpret_cast<const void* const*>(static_cast<const char*>(diggerItemPtr) + 0xA8);
if (!tierPtr || !IsReadableRange(tierPtr, sizeof(int)))
return -1;
return *reinterpret_cast<const int*>(tierPtr);
}
static float GetToolDestroySpeed(void* diggerItemPtr, int itemId)
{
const CustomToolMaterialRegistry::Definition* def = CustomToolMaterialRegistry::Find(itemId);
if (def)
return def->destroySpeed;
if (!diggerItemPtr || !IsReadableRange(static_cast<const char*>(diggerItemPtr) + 0xA0, sizeof(float)))
return 1.0f;
return *reinterpret_cast<const float*>(static_cast<const char*>(diggerItemPtr) + 0xA0);
}
float __fastcall Hooked_PickaxeItemGetDestroySpeed(void* thisPtr, void* itemInstanceSharedPtr, void* tilePtr)
{
void* itemInstancePtr = DecodeItemInstancePtrFromSharedArg(itemInstanceSharedPtr);
int itemId = 0;
if (!TryReadItemId(itemInstancePtr, itemId))
{
if (Original_PickaxeItemGetDestroySpeed)
return Original_PickaxeItemGetDestroySpeed(thisPtr, itemInstanceSharedPtr, tilePtr);
return 1.0f;
}
int tileId = 0;
if (tilePtr && TryReadTileId(tilePtr, tileId))
{
const CustomBlockRegistry::Definition* blockDef = CustomBlockRegistry::Find(tileId);
if (blockDef && blockDef->requiredTool == CustomBlockRegistry::ToolType::Pickaxe)
{
int harvestLevel = GetToolHarvestLevel(thisPtr, itemId);
if (harvestLevel >= 0 &&
(blockDef->requiredHarvestLevel < 0 || harvestLevel >= blockDef->requiredHarvestLevel))
{
return GetToolDestroySpeed(thisPtr, itemId);
}
// Block requires pickaxe but harvest level insufficient - return slow speed
return 1.0f;
}
}
if (Original_PickaxeItemGetDestroySpeed)
return Original_PickaxeItemGetDestroySpeed(thisPtr, itemInstanceSharedPtr, tilePtr);
return 1.0f;
}
bool __fastcall Hooked_PickaxeItemCanDestroySpecial(void* thisPtr, void* tilePtr)
{
int itemId = 0;
if (!TryReadItemIdFromPickaxe(thisPtr, itemId))
{
if (Original_PickaxeItemCanDestroySpecial)
return Original_PickaxeItemCanDestroySpecial(thisPtr, tilePtr);
return false;
}
int tileId = 0;
if (tilePtr && TryReadTileId(tilePtr, tileId))
{
const CustomBlockRegistry::Definition* blockDef = CustomBlockRegistry::Find(tileId);
if (blockDef && blockDef->requiredTool == CustomBlockRegistry::ToolType::Pickaxe)
{
int harvestLevel = GetToolHarvestLevel(thisPtr, itemId);
if (harvestLevel >= 0 &&
(blockDef->requiredHarvestLevel < 0 || harvestLevel >= blockDef->requiredHarvestLevel))
{
return true;
}
return false;
}
}
if (Original_PickaxeItemCanDestroySpecial)
return Original_PickaxeItemCanDestroySpecial(thisPtr, tilePtr);
return false;
}
float __fastcall Hooked_ShovelItemGetDestroySpeed(void* thisPtr, void* itemInstanceSharedPtr, void* tilePtr)
{
void* itemInstancePtr = DecodeItemInstancePtrFromSharedArg(itemInstanceSharedPtr);
int itemId = 0;
if (!TryReadItemId(itemInstancePtr, itemId))
{
if (Original_ShovelItemGetDestroySpeed)
return Original_ShovelItemGetDestroySpeed(thisPtr, itemInstanceSharedPtr, tilePtr);
return 1.0f;
}
int tileId = 0;
if (tilePtr && TryReadTileId(tilePtr, tileId))
{
const CustomBlockRegistry::Definition* blockDef = CustomBlockRegistry::Find(tileId);
if (blockDef && blockDef->requiredTool == CustomBlockRegistry::ToolType::Shovel)
{
int harvestLevel = GetToolHarvestLevel(thisPtr, itemId);
if (harvestLevel >= 0 &&
(blockDef->requiredHarvestLevel < 0 || harvestLevel >= blockDef->requiredHarvestLevel))
{
return GetToolDestroySpeed(thisPtr, itemId);
}
return 1.0f;
}
}
if (Original_ShovelItemGetDestroySpeed)
return Original_ShovelItemGetDestroySpeed(thisPtr, itemInstanceSharedPtr, tilePtr);
return 1.0f;
}
bool __fastcall Hooked_ShovelItemCanDestroySpecial(void* thisPtr, void* tilePtr)
{
int itemId = 0;
if (!TryReadItemIdFromPickaxe(thisPtr, itemId))
{
if (Original_ShovelItemCanDestroySpecial)
return Original_ShovelItemCanDestroySpecial(thisPtr, tilePtr);
return false;
}
int tileId = 0;
if (tilePtr && TryReadTileId(tilePtr, tileId))
{
const CustomBlockRegistry::Definition* blockDef = CustomBlockRegistry::Find(tileId);
if (blockDef && blockDef->requiredTool == CustomBlockRegistry::ToolType::Shovel)
{
int harvestLevel = GetToolHarvestLevel(thisPtr, itemId);
if (harvestLevel >= 0 &&
(blockDef->requiredHarvestLevel < 0 || harvestLevel >= blockDef->requiredHarvestLevel))
{
return true;
}
return false;
}
}
if (Original_ShovelItemCanDestroySpecial)
return Original_ShovelItemCanDestroySpecial(thisPtr, tilePtr);
return false;
}
// Inventory layout: items.data at +0x8, selected at +0x28. shared_ptr is 16 bytes.
static void* GetSelectedItemInstanceFromPlayer(void* playerPtr)
{
void* inv = FindInventoryPtrFromPlayer(playerPtr);
if (!inv || !IsReadableRange(inv, 0x30))
return nullptr;
void* itemsData = *reinterpret_cast<void* const*>(static_cast<const char*>(inv) + 0x8);
int selected = *reinterpret_cast<const int*>(static_cast<const char*>(inv) + 0x28);
if (!itemsData || selected < 0 || selected >= 36)
return nullptr;
// shared_ptr<ItemInstance> at itemsData[selected]; raw ptr is first 8 bytes
const char* slotPtr = static_cast<const char*>(itemsData) + selected * 16;
if (!IsReadableRange(slotPtr, 8))
return nullptr;
return *reinterpret_cast<void* const*>(slotPtr);
}
bool __fastcall Hooked_PlayerCanDestroy(void* thisPtr, void* tilePtr)
{
// For pickaxe harvest rules, Inventory::canDestroy -> ItemInstance::canDestroySpecial
// already gives the correct source behavior:
// proper tool/tier => normal speed + drops
// insufficient tool/tier => slow break + no drops
if (Original_PlayerCanDestroy)
return Original_PlayerCanDestroy(thisPtr, tilePtr);
return false;
}
bool __fastcall Hooked_ServerPlayerGameModeUseItem(void* thisPtr, void* playerSharedPtr, void* level, void* itemInstanceSharedPtr, bool bTestUseOnly)
{
void* previousLevel = s_activeUseLevel;
s_activeUseLevel = level;
static int s_serverUseLogCount = 0;
void* itemInstancePtr = DecodeItemInstancePtrFromSharedArg(itemInstanceSharedPtr);
int itemId = -1;
TryReadItemId(itemInstancePtr, itemId);
bool effectiveTestUseOnly = bTestUseOnly;
const ULONGLONG nowMs = GetTickCount64();
if (effectiveTestUseOnly &&
itemId >= 0 &&
s_pendingServerUseItemId == itemId &&
nowMs <= s_pendingServerUseExpiryMs)
{
effectiveTestUseOnly = false;
s_pendingServerUseItemId = -1;
s_pendingServerUseExpiryMs = 0;
}
if (s_serverUseLogCount < 40)
{
LogUtil::Log("[WeaveLoader] UseHook ServerPlayerGameMode::useItem test=%d effective=%d item=%d itemPtr=%p playerShared=%p level=%p",
bTestUseOnly ? 1 : 0, effectiveTestUseOnly ? 1 : 0, itemId, itemInstancePtr, playerSharedPtr, level);
s_serverUseLogCount++;
}
int action = TryDispatchUseItemFromSharedItemArg(itemInstanceSharedPtr, playerSharedPtr, effectiveTestUseOnly, "ServerPlayerGameMode::useItem");
s_activeUseLevel = previousLevel;
if (action == 2)
return true;
if (Original_ServerPlayerGameModeUseItem)
return Original_ServerPlayerGameModeUseItem(thisPtr, playerSharedPtr, level, itemInstanceSharedPtr, bTestUseOnly);
return false;
}
bool __fastcall Hooked_MultiPlayerGameModeUseItem(void* thisPtr, void* playerSharedPtr, void* level, void* itemInstanceSharedPtr, bool bTestUseOnly)
{
static int s_multiUseLogCount = 0;
void* itemInstancePtr = DecodeItemInstancePtrFromSharedArg(itemInstanceSharedPtr);
int itemId = -1;
TryReadItemId(itemInstancePtr, itemId);
if (!bTestUseOnly && itemId >= 0)
{
s_pendingServerUseItemId = itemId;
s_pendingServerUseExpiryMs = GetTickCount64() + 1000;
}
if (s_multiUseLogCount < 40)
{
LogUtil::Log("[WeaveLoader] UseHook MultiPlayerGameMode::useItem test=%d item=%d itemPtr=%p playerShared=%p level=%p",
bTestUseOnly ? 1 : 0, itemId, itemInstancePtr, playerSharedPtr, level);
s_multiUseLogCount++;
}
void* previousLevel = s_activeUseLevel;
s_activeUseLevel = level;
int action = TryDispatchUseItemFromSharedItemArg(itemInstanceSharedPtr, playerSharedPtr, bTestUseOnly, "MultiPlayerGameMode::useItem");
s_activeUseLevel = previousLevel;
if (action == 2)
return true;
if (Original_MultiPlayerGameModeUseItem)
return Original_MultiPlayerGameModeUseItem(thisPtr, playerSharedPtr, level, itemInstanceSharedPtr, bTestUseOnly);
return false;
}
void __fastcall Hooked_MinecraftSetLevel(void* thisPtr, void* level, int message, void* forceInsertPlayerSharedPtr, bool doForceStatsSave, bool bPrimaryPlayerSignedOut)
{
if (Original_MinecraftSetLevel)
{
Original_MinecraftSetLevel(thisPtr, level, message, forceInsertPlayerSharedPtr, doForceStatsSave, bPrimaryPlayerSignedOut);
}
s_currentLevel = level;
}
void* Hooked_GetResourceAsStream(const void* fileName)
{
const std::wstring* path = static_cast<const std::wstring*>(fileName);
if (!Original_GetResourceAsStream || !path)
return Original_GetResourceAsStream ? Original_GetResourceAsStream(fileName) : nullptr;
int atlasType = -1;
int page = 0;
if (ParseVirtualAtlasRequest(*path, atlasType, page))
{
std::string atlasPath = ModAtlas::GetVirtualPagePath(atlasType, page);
if (atlasPath.empty())
atlasPath = ModAtlas::GetMergedPagePath(atlasType, page);
if (!atlasPath.empty())
{
std::wstring ourPath(atlasPath.begin(), atlasPath.end());
return Original_GetResourceAsStream(&ourPath);
}
}
std::wstring lower = NormalizeLowerPath(*path);
const bool isGenerated =
(lower.find(L"/modloader/") != std::wstring::npos) ||
(lower.find(L"/mods/modloader/generated/") != std::wstring::npos);
if (!isGenerated)
{
int mipLevel = 0;
if (TryParseMipmapLevel(lower, L"terrain", mipLevel))
{
std::string mipPath = ModAtlas::GetMergedMipmapPath(0, mipLevel);
if (!mipPath.empty())
{
std::wstring ourPath(mipPath.begin(), mipPath.end());
return Original_GetResourceAsStream(&ourPath);
}
}
if (TryParseMipmapLevel(lower, L"items", mipLevel))
{
std::string mipPath = ModAtlas::GetMergedMipmapPath(1, mipLevel);
if (!mipPath.empty())
{
std::wstring ourPath(mipPath.begin(), mipPath.end());
return Original_GetResourceAsStream(&ourPath);
}
}
if (EndsWithPath(lower, L"terrain.png"))
{
ModAtlas::SetOverrideAtlasPath(0, std::string(path->begin(), path->end()));
ModAtlas::EnsureAtlasesBuilt();
std::string terrainPath = ModAtlas::GetMergedTerrainPath();
if (!terrainPath.empty())
{
std::wstring ourPath(terrainPath.begin(), terrainPath.end());
LogUtil::Log("[WeaveLoader] getResourceAsStream: redirecting terrain.png to merged atlas");
return Original_GetResourceAsStream(&ourPath);
}
}
if (EndsWithPath(lower, L"items.png"))
{
ModAtlas::SetOverrideAtlasPath(1, std::string(path->begin(), path->end()));
ModAtlas::EnsureAtlasesBuilt();
std::string itemsPath = ModAtlas::GetMergedItemsPath();
if (!itemsPath.empty())
{
std::wstring ourPath(itemsPath.begin(), itemsPath.end());
LogUtil::Log("[WeaveLoader] getResourceAsStream: redirecting items.png to merged atlas");
return Original_GetResourceAsStream(&ourPath);
}
}
}
std::wstring modAssetPath;
if (TryResolveModAssetPath(*path, modAssetPath))
{
LogUtil::Log("[WeaveLoader] getResourceAsStream: redirecting %ls -> %ls",
path->c_str(), modAssetPath.c_str());
return Original_GetResourceAsStream(&modAssetPath);
}
return Original_GetResourceAsStream(fileName);
}
static bool s_loggedGetString = false;
const wchar_t* Hooked_GetString(int id)
{
if (ModStrings::IsModId(id))
{
const wchar_t* modStr = ModStrings::Get(id);
LogUtil::Log("[WeaveLoader] 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("[WeaveLoader] 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("[WeaveLoader] Hook: RunStaticCtors -- calling PreInit");
DotNetHost::CallPreInit();
s_preInitCalled = true;
Original_RunStaticCtors();
WorldIdRemap::EnsureMissingPlaceholders();
LogUtil::Log("[WeaveLoader] Hook: RunStaticCtors complete -- calling Init");
DotNetHost::CallInit();
s_initCalled = true;
// 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)
{
CullSpawnedEntitiesBelowWorld();
Original_MinecraftTick(thisPtr, bFirst, bUpdateTextures);
CullSpawnedEntitiesBelowWorld();
if (bFirst)
{
DotNetHost::CallTick();
}
}
void __fastcall Hooked_MinecraftInit(void* thisPtr)
{
char baseDir[MAX_PATH] = { 0 };
GetModuleFileNameA(nullptr, baseDir, MAX_PATH);
std::string base(baseDir);
size_t pos = base.find_last_of("\\/");
if (pos != std::string::npos) base.resize(pos + 1);
std::string gameResPath = base + "Common\\res\\TitleUpdate\\res";
std::string virtualAtlasDir = gameResPath + "\\modloader";
ModAtlas::SetVirtualAtlasDirectory(virtualAtlasDir);
HMODULE hMod = nullptr;
std::string modsPath;
if (GetModuleHandleExA(GET_MODULE_HANDLE_EX_FLAG_FROM_ADDRESS, (LPCSTR)&Hooked_MinecraftInit, &hMod) && hMod)
{
char dllPath[MAX_PATH] = { 0 };
if (GetModuleFileNameA(hMod, dllPath, MAX_PATH))
{
std::string dllDir(dllPath);
size_t dllPos = dllDir.find_last_of("\\/");
if (dllPos != std::string::npos)
{
dllDir.resize(dllPos + 1);
modsPath = dllDir + "mods";
goto atlas_done;
}
}
}
modsPath = base + "mods";
atlas_done:
s_modsPath = modsPath;
{
std::lock_guard<std::mutex> guard(s_modAssetsMutex);
s_modAssetsIndexed = false;
s_modAssetRoots.clear();
}
NativeExports::SetModsPath(modsPath);
ModAtlas::SetBasePaths(modsPath, gameResPath);
ModAtlas::EnsureAtlasesBuilt();
// 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();
if (!s_preInitCalled)
{
LogUtil::Log("[WeaveLoader] Hook: Minecraft::init -- late PreInit fallback");
DotNetHost::CallPreInit();
s_preInitCalled = true;
}
if (!s_initCalled)
{
LogUtil::Log("[WeaveLoader] Hook: Minecraft::init -- late Init fallback");
DotNetHost::CallInit();
s_initCalled = true;
ModStrings::InjectAllIntoGameTable();
}
if (!s_postInitCalled)
{
LogUtil::Log("[WeaveLoader] Hook: Minecraft::init complete -- calling PostInit");
DotNetHost::CallPostInit();
s_postInitCalled = true;
}
}
void __fastcall Hooked_ExitGame(void* thisPtr)
{
LogUtil::Log("[WeaveLoader] Hook: ExitGame -- calling Shutdown");
DotNetHost::CallShutdown();
Original_ExitGame(thisPtr);
}
void Hooked_CreativeStaticCtor()
{
LogUtil::Log("[WeaveLoader] Hook: CreativeStaticCtor -- building vanilla creative lists");
Original_CreativeStaticCtor();
CreativeInventory::SetCreativeReady();
// Inject AFTER vanilla lists so modded entries are appended to the end
// of each creative category.
LogUtil::Log("[WeaveLoader] Hook: CreativeStaticCtor -- injecting modded items last");
CreativeInventory::InjectItems();
// Recalculate TabSpec page counts after appending mod items.
LogUtil::Log("[WeaveLoader] Hook: CreativeStaticCtor -- updating tab page counts");
CreativeInventory::UpdateTabPageCounts();
}
void __fastcall Hooked_MainMenuCustomDraw(void* thisPtr, void* region)
{
MainMenuOverlay::NotifyOnMainMenu();
Original_MainMenuCustomDraw(thisPtr, region);
}
void __fastcall Hooked_Present(void* thisPtr)
{
MainMenuOverlay::RenderBranding();
Original_Present(thisPtr);
}
void WINAPI Hooked_OutputDebugStringA(const char* lpOutputString)
{
if (lpOutputString && lpOutputString[0] != '\0')
{
// Strip trailing newlines/carriage returns for clean log output
size_t len = strlen(lpOutputString);
while (len > 0 && (lpOutputString[len - 1] == '\n' || lpOutputString[len - 1] == '\r'))
len--;
if (len > 0)
LogUtil::LogGameOutput(lpOutputString, len);
}
Original_OutputDebugStringA(lpOutputString);
}
}