diff --git a/ExampleMod/ExampleMod.cs b/ExampleMod/ExampleMod.cs index 35b8790..9b5ca1c 100644 --- a/ExampleMod/ExampleMod.cs +++ b/ExampleMod/ExampleMod.cs @@ -26,6 +26,7 @@ public class ExampleMod : IMod Ruby = Registry.Item.Register("examplemod:ruby", new ItemProperties() .MaxStackSize(64) + .Icon("ruby") .InCreativeTab(CreativeTab.Materials)); Registry.Recipe.AddFurnace("examplemod:ruby_ore", "examplemod:ruby", 1.0f); diff --git a/LegacyForge.API/Item/ItemProperties.cs b/LegacyForge.API/Item/ItemProperties.cs index ed57e9a..9f210cb 100644 --- a/LegacyForge.API/Item/ItemProperties.cs +++ b/LegacyForge.API/Item/ItemProperties.cs @@ -7,9 +7,11 @@ public class ItemProperties { internal int MaxStackSizeValue = 64; internal int MaxDamageValue = 0; + internal string IconValue = ""; internal CreativeTab CreativeTabValue = CreativeTab.None; public ItemProperties MaxStackSize(int size) { MaxStackSizeValue = size; return this; } + public ItemProperties Icon(string iconName) { IconValue = iconName; return this; } /// /// Set max damage for a tool/armor item. Setting this to a positive value diff --git a/LegacyForge.API/Item/ItemRegistry.cs b/LegacyForge.API/Item/ItemRegistry.cs index c6c765a..40ac194 100644 --- a/LegacyForge.API/Item/ItemRegistry.cs +++ b/LegacyForge.API/Item/ItemRegistry.cs @@ -35,7 +35,8 @@ public static class ItemRegistry int numericId = NativeInterop.native_register_item( id.ToString(), properties.MaxStackSizeValue, - properties.MaxDamageValue); + properties.MaxDamageValue, + properties.IconValue); if (numericId < 0) throw new InvalidOperationException($"Failed to register item '{id}'. No free IDs or invalid parameters."); diff --git a/LegacyForge.API/NativeInterop.cs b/LegacyForge.API/NativeInterop.cs index 93c4b70..dbcb0e7 100644 --- a/LegacyForge.API/NativeInterop.cs +++ b/LegacyForge.API/NativeInterop.cs @@ -25,7 +25,8 @@ internal static class NativeInterop internal static extern int native_register_item( string namespacedId, int maxStackSize, - int maxDamage); + int maxDamage, + string iconName); [DllImport(RuntimeDll, CallingConvention = CallingConvention.Cdecl, CharSet = CharSet.Ansi)] internal static extern int native_register_entity( diff --git a/LegacyForgeRuntime/CMakeLists.txt b/LegacyForgeRuntime/CMakeLists.txt index 7d3d8e1..945127d 100644 --- a/LegacyForgeRuntime/CMakeLists.txt +++ b/LegacyForgeRuntime/CMakeLists.txt @@ -80,6 +80,7 @@ add_library(LegacyForgeRuntime SHARED src/NativeExports.cpp src/CreativeInventory.cpp src/MainMenuOverlay.cpp + src/GameObjectFactory.cpp ) target_include_directories(LegacyForgeRuntime PRIVATE diff --git a/LegacyForgeRuntime/src/CrashHandler.cpp b/LegacyForgeRuntime/src/CrashHandler.cpp index 17c05db..2d3bee4 100644 --- a/LegacyForgeRuntime/src/CrashHandler.cpp +++ b/LegacyForgeRuntime/src/CrashHandler.cpp @@ -150,6 +150,24 @@ static LONG WINAPI VectoredHandler(EXCEPTION_POINTERS* ep) DWORD code = ep->ExceptionRecord->ExceptionCode; + // The game's debug build uses __debugbreak() (int 3) as assertions throughout + // the texture system and other subsystems. Without a debugger the default + // handler terminates the process. We skip past the 1-byte int 3 instruction + // so the game's fallback code (e.g. missing-texture) can run normally. + if (code == EXCEPTION_BREAKPOINT && s_gameBase != 0) + { + DWORD64 rip = ep->ContextRecord->Rip; + HMODULE hMod = nullptr; + if (GetModuleHandleExA(GET_MODULE_HANDLE_EX_FLAG_FROM_ADDRESS | + GET_MODULE_HANDLE_EX_FLAG_UNCHANGED_REFCOUNT, + reinterpret_cast(rip), &hMod) && + reinterpret_cast(hMod) == s_gameBase) + { + ep->ContextRecord->Rip += 1; + return EXCEPTION_CONTINUE_EXECUTION; + } + } + if (!IsFatalException(code)) return EXCEPTION_CONTINUE_SEARCH; diff --git a/LegacyForgeRuntime/src/GameObjectFactory.cpp b/LegacyForgeRuntime/src/GameObjectFactory.cpp new file mode 100644 index 0000000..e5b9d8f --- /dev/null +++ b/LegacyForgeRuntime/src/GameObjectFactory.cpp @@ -0,0 +1,248 @@ +#include "GameObjectFactory.h" +#include "SymbolResolver.h" +#include "PdbParser.h" +#include "LogUtil.h" +#include +#include +#include + +// Tile::Tile(int id, Material* material, bool isSolidRender) — protected ctor +typedef void (__fastcall *TileCtor_fn)(void* thisPtr, int id, void* material, bool isSolidRender); +// Tile* Tile::setDestroyTime(float) — protected virtual +typedef void* (__fastcall *TileSetFloat_fn)(void* thisPtr, float val); +// Tile* Tile::setSoundType(const SoundType*) — protected virtual +typedef void* (__fastcall *TileSetSoundType_fn)(void* thisPtr, const void* soundType); +// Tile* Tile::setIconName(const std::wstring&) — protected virtual +typedef void* (__fastcall *TileSetIconName_fn)(void* thisPtr, const std::wstring& name); + +// TileItem::TileItem(int id) +typedef void (__fastcall *TileItemCtor_fn)(void* thisPtr, int id); + +// Item::Item(int id) — protected ctor +typedef void (__fastcall *ItemCtor_fn)(void* thisPtr, int id); +// Item* Item::setIconName(const std::wstring&) +typedef void* (__fastcall *ItemSetIconName_fn)(void* thisPtr, const std::wstring& name); + +static TileCtor_fn fnTileCtor = nullptr; +static TileSetFloat_fn fnSetDestroyTime = nullptr; +static TileSetFloat_fn fnSetExplodeable = nullptr; +static TileSetSoundType_fn fnSetSoundType = nullptr; +static TileSetIconName_fn fnTileSetIconName= nullptr; + +static TileItemCtor_fn fnTileItemCtor = nullptr; + +static ItemCtor_fn fnItemCtor = nullptr; +static ItemSetIconName_fn fnItemSetIconName= nullptr; + +// Store ADDRESSES of Material*/SoundType* statics so we can dereference lazily +// (they're NULL at resolve time because staticCtor hasn't run yet). +static void** s_materialAddrs[16] = {}; +static void** s_soundAddrs[10] = {}; + +static const int TILE_ALLOC_SIZE = 1024; +static const int ITEM_ALLOC_SIZE = 1024; +static const int TILEITEM_ALLOC_SIZE = 1024; + +static bool s_resolved = false; + +static void* GetMaterial(int idx) +{ + if (idx < 0 || idx >= 16 || !s_materialAddrs[idx]) return nullptr; + return *s_materialAddrs[idx]; +} + +static void* GetSound(int idx) +{ + if (idx < 0 || idx >= 10 || !s_soundAddrs[idx]) return nullptr; + return *s_soundAddrs[idx]; +} + +namespace GameObjectFactory +{ + +bool ResolveSymbols(SymbolResolver& resolver) +{ + LogUtil::Log("[LegacyForge] GameObjectFactory: resolving symbols..."); + + // Tile constructor — protected (IEAA not QEAA) + fnTileCtor = (TileCtor_fn)resolver.Resolve("??0Tile@@IEAA@HPEAVMaterial@@_N@Z"); + + // Tile setters — protected virtual (MEAA not UEAA) + fnSetDestroyTime = (TileSetFloat_fn)resolver.Resolve( + "?setDestroyTime@Tile@@MEAAPEAV1@M@Z"); + fnSetExplodeable = (TileSetFloat_fn)resolver.Resolve( + "?setExplodeable@Tile@@MEAAPEAV1@M@Z"); + fnSetSoundType = (TileSetSoundType_fn)resolver.Resolve( + "?setSoundType@Tile@@MEAAPEAV1@PEBVSoundType@1@@Z"); + fnTileSetIconName = (TileSetIconName_fn)resolver.Resolve( + "?setIconName@Tile@@MEAAPEAV1@AEBV?$basic_string@_WU?$char_traits@_W@std@@V?$allocator@_W@2@@std@@@Z"); + + // TileItem constructor + fnTileItemCtor = (TileItemCtor_fn)resolver.Resolve("??0TileItem@@QEAA@H@Z"); + + // Item constructor — protected (IEAA not QEAA) + fnItemCtor = (ItemCtor_fn)resolver.Resolve("??0Item@@IEAA@H@Z"); + + // Item::setIconName + fnItemSetIconName = (ItemSetIconName_fn)resolver.Resolve( + "?setIconName@Item@@QEAAPEAV1@AEBV?$basic_string@_WU?$char_traits@_W@std@@V?$allocator@_W@2@@std@@@Z"); + + // Resolve Material* static pointer ADDRESSES (values are NULL until staticCtor runs) + auto resolveMat = [&](int idx, const char* sym) { + s_materialAddrs[idx] = (void**)resolver.Resolve(sym); + }; + resolveMat(0, "?air@Material@@2PEAV1@EA"); + resolveMat(1, "?stone@Material@@2PEAV1@EA"); + resolveMat(2, "?wood@Material@@2PEAV1@EA"); + resolveMat(3, "?cloth@Material@@2PEAV1@EA"); + resolveMat(4, "?plant@Material@@2PEAV1@EA"); + resolveMat(5, "?dirt@Material@@2PEAV1@EA"); + resolveMat(6, "?sand@Material@@2PEAV1@EA"); + resolveMat(7, "?glass@Material@@2PEAV1@EA"); + resolveMat(8, "?water@Material@@2PEAV1@EA"); + resolveMat(9, "?lava@Material@@2PEAV1@EA"); + resolveMat(10, "?ice@Material@@2PEAV1@EA"); + resolveMat(11, "?metal@Material@@2PEAV1@EA"); + resolveMat(12, "?snow@Material@@2PEAV1@EA"); + resolveMat(13, "?clay@Material@@2PEAV1@EA"); + resolveMat(14, "?explosive@Material@@2PEAV1@EA"); + resolveMat(15, "?web@Material@@2PEAV1@EA"); + + // Resolve SoundType* static pointer ADDRESSES + auto resolveSound = [&](int idx, const char* sym) { + s_soundAddrs[idx] = (void**)resolver.Resolve(sym); + }; + resolveSound(0, "?SOUND_NORMAL@Tile@@2PEAVSoundType@1@EA"); + resolveSound(1, "?SOUND_STONE@Tile@@2PEAVSoundType@1@EA"); + resolveSound(2, "?SOUND_WOOD@Tile@@2PEAVSoundType@1@EA"); + resolveSound(3, "?SOUND_GRAVEL@Tile@@2PEAVSoundType@1@EA"); + resolveSound(4, "?SOUND_GRASS@Tile@@2PEAVSoundType@1@EA"); + resolveSound(5, "?SOUND_METAL@Tile@@2PEAVSoundType@1@EA"); + resolveSound(6, "?SOUND_GLASS@Tile@@2PEAVSoundType@1@EA"); + resolveSound(7, "?SOUND_CLOTH@Tile@@2PEAVSoundType@1@EA"); + resolveSound(8, "?SOUND_SAND@Tile@@2PEAVSoundType@1@EA"); + resolveSound(9, "?SOUND_SNOW@Tile@@2PEAVSoundType@1@EA"); + + auto logSym = [](const char* name, void* ptr) { + if (ptr) LogUtil::Log("[LegacyForge] GOF %-20s @ %p", name, ptr); + else LogUtil::Log("[LegacyForge] GOF MISSING: %s", name); + }; + + logSym("Tile::Tile", (void*)fnTileCtor); + logSym("setDestroyTime", (void*)fnSetDestroyTime); + logSym("setExplodeable", (void*)fnSetExplodeable); + logSym("setSoundType", (void*)fnSetSoundType); + logSym("Tile::setIconName", (void*)fnTileSetIconName); + logSym("TileItem::TileItem", (void*)fnTileItemCtor); + logSym("Item::Item", (void*)fnItemCtor); + logSym("Item::setIconName", (void*)fnItemSetIconName); + logSym("Material::stone addr", (void*)s_materialAddrs[1]); + logSym("SOUND_STONE addr", (void*)s_soundAddrs[1]); + + // Diagnostics for missing symbols + if (!fnTileCtor) PdbParser::DumpMatching("??0Tile@@"); + if (!fnTileItemCtor) PdbParser::DumpMatching("??0TileItem@@"); + if (!fnItemCtor) PdbParser::DumpMatching("??0Item@@"); + if (!fnSetDestroyTime) PdbParser::DumpMatching("setDestroyTime@Tile"); + if (!fnSetExplodeable) PdbParser::DumpMatching("setExplodeable@Tile"); + if (!fnSetSoundType) PdbParser::DumpMatching("setSoundType@Tile"); + if (!fnTileSetIconName) PdbParser::DumpMatching("setIconName@Tile"); + if (!fnItemSetIconName) PdbParser::DumpMatching("setIconName@Item"); + if (!s_materialAddrs[1]) PdbParser::DumpMatching("stone@Material"); + if (!s_soundAddrs[1]) PdbParser::DumpMatching("SOUND_STONE@Tile"); + + s_resolved = fnTileCtor && fnTileItemCtor && fnItemCtor; + + if (s_resolved) + LogUtil::Log("[LegacyForge] GameObjectFactory: core symbols resolved OK"); + else + LogUtil::Log("[LegacyForge] GameObjectFactory: MISSING core symbols -- block/item creation disabled"); + + return s_resolved; +} + +bool CreateTile(int tileId, int materialType, float hardness, float resistance, + int soundType, const wchar_t* iconName) +{ + if (!s_resolved || !fnTileCtor) + { + LogUtil::Log("[LegacyForge] CreateTile: symbols not resolved"); + return false; + } + + // Read material pointer lazily (staticCtor has run by the time mods call this) + void* mat = GetMaterial(materialType); + if (!mat) + mat = GetMaterial(1); // stone fallback + + if (!mat) + { + LogUtil::Log("[LegacyForge] CreateTile: no material pointer for type %d", materialType); + return false; + } + + void* tile = ::operator new(TILE_ALLOC_SIZE); + memset(tile, 0, TILE_ALLOC_SIZE); + fnTileCtor(tile, tileId, mat, true); + + if (fnSetDestroyTime) + fnSetDestroyTime(tile, hardness); + + if (fnSetExplodeable) + fnSetExplodeable(tile, resistance); + + void* sound = GetSound(soundType); + if (fnSetSoundType && sound) + fnSetSoundType(tile, sound); + + if (fnTileSetIconName && iconName) + { + std::wstring name(iconName); + fnTileSetIconName(tile, name); + } + + LogUtil::Log("[LegacyForge] Created Tile id=%d (material=%d, icon=%ls)", tileId, materialType, + iconName ? iconName : L""); + + // Create the corresponding TileItem so the block can appear in inventory. + // TileItem(tileId - 256) -> Item::Item(tileId - 256) -> id = tileId, items[tileId] = this + if (fnTileItemCtor) + { + void* tileItem = ::operator new(TILEITEM_ALLOC_SIZE); + memset(tileItem, 0, TILEITEM_ALLOC_SIZE); + fnTileItemCtor(tileItem, tileId - 256); + LogUtil::Log("[LegacyForge] Created TileItem for tile %d", tileId); + } + + return true; +} + +bool CreateItem(int itemId, int maxStackSize, const wchar_t* iconName) +{ + if (!s_resolved || !fnItemCtor) + { + LogUtil::Log("[LegacyForge] CreateItem: symbols not resolved"); + return false; + } + + int ctorParam = itemId - 256; + + void* item = ::operator new(ITEM_ALLOC_SIZE); + memset(item, 0, ITEM_ALLOC_SIZE); + fnItemCtor(item, ctorParam); + + // The game calls __debugbreak() if registerIcons is called with an empty + // m_textureName, so always set a non-empty icon name. + if (fnItemSetIconName) + { + std::wstring name = (iconName && iconName[0]) ? iconName : L"MISSING_ICON_ITEM"; + fnItemSetIconName(item, name); + } + + LogUtil::Log("[LegacyForge] Created Item id=%d (ctorParam=%d, icon=%ls)", + itemId, ctorParam, iconName ? iconName : L""); + + return true; +} + +} // namespace GameObjectFactory diff --git a/LegacyForgeRuntime/src/GameObjectFactory.h b/LegacyForgeRuntime/src/GameObjectFactory.h new file mode 100644 index 0000000..3069f16 --- /dev/null +++ b/LegacyForgeRuntime/src/GameObjectFactory.h @@ -0,0 +1,20 @@ +#pragma once +#include + +class SymbolResolver; + +namespace GameObjectFactory +{ + bool ResolveSymbols(SymbolResolver& resolver); + + // Create a Tile game object at the given tileId, register it in Tile::tiles[], + // and create a matching TileItem in Item::items[]. + // materialType/soundType map to the C# API enums (see BlockProperties.cs). + // iconName is the texture atlas key (e.g. "ruby_ore"). + bool CreateTile(int tileId, int materialType, float hardness, float resistance, + int soundType, const wchar_t* iconName); + + // Create an Item game object. itemId is the FINAL id (256 + constructor param). + // The Item is registered in Item::items[itemId]. + bool CreateItem(int itemId, int maxStackSize, const wchar_t* iconName); +} diff --git a/LegacyForgeRuntime/src/HookManager.cpp b/LegacyForgeRuntime/src/HookManager.cpp index 147f6e4..a03a284 100644 --- a/LegacyForgeRuntime/src/HookManager.cpp +++ b/LegacyForgeRuntime/src/HookManager.cpp @@ -3,6 +3,7 @@ #include "SymbolResolver.h" #include "CreativeInventory.h" #include "MainMenuOverlay.h" +#include "GameObjectFactory.h" #include "LogUtil.h" #include @@ -64,6 +65,8 @@ bool HookManager::Install(const SymbolResolver& symbols) } } + GameObjectFactory::ResolveSymbols(const_cast(symbols)); + if (symbols.pCreativeStaticCtor) { CreativeInventory::ResolveSymbols(const_cast(symbols)); diff --git a/LegacyForgeRuntime/src/IdRegistry.h b/LegacyForgeRuntime/src/IdRegistry.h index 42511a8..5fa7aff 100644 --- a/LegacyForgeRuntime/src/IdRegistry.h +++ b/LegacyForgeRuntime/src/IdRegistry.h @@ -36,9 +36,15 @@ private: int nextFreeId; }; - static constexpr int BLOCK_MOD_START = 256; - static constexpr int BLOCK_MAX = 4095; - static constexpr int ITEM_MOD_START = 1000; + // Tile IDs 174-255 are unused by vanilla (161-169 also free but small). + // Must stay <= 255 so TileItem can map tile ID to Item::items[tileId]. + static constexpr int BLOCK_MOD_START = 174; + static constexpr int BLOCK_MAX = 255; + + // Item IDs here are the FINAL id stored in Item::id (= 256 + constructor param). + // Vanilla records go up to stored ID 2267 (constructor param 2011). + // We start above that to avoid conflicts. + static constexpr int ITEM_MOD_START = 3000; static constexpr int ITEM_MAX = 31999; static constexpr int ENTITY_MOD_START = 1000; static constexpr int ENTITY_MAX = 9999; diff --git a/LegacyForgeRuntime/src/NativeExports.cpp b/LegacyForgeRuntime/src/NativeExports.cpp index 0cd8ddf..12501cb 100644 --- a/LegacyForgeRuntime/src/NativeExports.cpp +++ b/LegacyForgeRuntime/src/NativeExports.cpp @@ -1,8 +1,11 @@ #include "NativeExports.h" #include "IdRegistry.h" #include "CreativeInventory.h" +#include "GameObjectFactory.h" #include "LogUtil.h" +#include #include +#include extern "C" { @@ -29,13 +32,29 @@ int native_register_block( LogUtil::Log("[LegacyForge] Registered block '%s' -> ID %d (hardness=%.1f, resistance=%.1f)", namespacedId, id, hardness, resistance); + // Convert icon name from UTF-8 to wide string + std::wstring wIcon; + if (iconName) + { + int len = MultiByteToWideChar(CP_UTF8, 0, iconName, -1, nullptr, 0); + wIcon.resize(len > 0 ? len - 1 : 0); + MultiByteToWideChar(CP_UTF8, 0, iconName, -1, &wIcon[0], len); + } + + if (!GameObjectFactory::CreateTile(id, materialId, hardness, resistance, + soundType, wIcon.empty() ? nullptr : wIcon.c_str())) + { + LogUtil::Log("[LegacyForge] Warning: failed to create game Tile for block '%s' id=%d", namespacedId, id); + } + return id; } int native_register_item( const char* namespacedId, int maxStackSize, - int maxDamage) + int maxDamage, + const char* iconName) { if (!namespacedId) return -1; @@ -49,6 +68,19 @@ int native_register_item( LogUtil::Log("[LegacyForge] Registered item '%s' -> ID %d (stack=%d, durability=%d)", namespacedId, id, maxStackSize, maxDamage); + std::wstring wIcon; + if (iconName && iconName[0]) + { + int len = MultiByteToWideChar(CP_UTF8, 0, iconName, -1, nullptr, 0); + wIcon.resize(len > 0 ? len - 1 : 0); + MultiByteToWideChar(CP_UTF8, 0, iconName, -1, &wIcon[0], len); + } + + if (!GameObjectFactory::CreateItem(id, maxStackSize, wIcon.empty() ? nullptr : wIcon.c_str())) + { + LogUtil::Log("[LegacyForge] Warning: failed to create game Item for '%s' id=%d", namespacedId, id); + } + return id; } diff --git a/LegacyForgeRuntime/src/NativeExports.h b/LegacyForgeRuntime/src/NativeExports.h index 8b501b1..ae4e01a 100644 --- a/LegacyForgeRuntime/src/NativeExports.h +++ b/LegacyForgeRuntime/src/NativeExports.h @@ -18,7 +18,8 @@ extern "C" __declspec(dllexport) int native_register_item( const char* namespacedId, int maxStackSize, - int maxDamage); + int maxDamage, + const char* iconName); __declspec(dllexport) int native_register_entity( const char* namespacedId,