diff --git a/ExampleMod/ExampleMod.cs b/ExampleMod/ExampleMod.cs index caba60f..1b45a3d 100644 --- a/ExampleMod/ExampleMod.cs +++ b/ExampleMod/ExampleMod.cs @@ -11,6 +11,16 @@ public class ExampleMod : IMod { public static RegisteredBlock? RubyOre; public static RegisteredItem? Ruby; + public static RegisteredItem? RubyPickaxeItem; + + private sealed class RubyPickaxe : PickaxeItem + { + public override MineBlockResult OnMineBlock(MineBlockContext context) + { + Logger.Info($"RubyPickaxe mined tile={context.TileId} at ({context.X}, {context.Y}, {context.Z})"); + return base.OnMineBlock(context); + } + } public void OnInitialize() { @@ -31,6 +41,14 @@ public class ExampleMod : IMod .Name("Ruby") .InCreativeTab(CreativeTab.Materials)); + RubyPickaxeItem = Registry.Item.Register("examplemod:ruby_pickaxe", new RubyPickaxe(), + new ItemProperties() + .MaxStackSize(1) + .MaxDamage(512) + .Icon("examplemod:ruby_pickaxe") // From assets/items/ruby_pickaxe.png + .Name("Ruby Pickaxe") + .InCreativeTab(CreativeTab.ToolsAndWeapons)); + Registry.Recipe.AddFurnace("examplemod:ruby_ore", "examplemod:ruby", 1.0f); GameEvents.OnBlockBreak += OnBlockBroken; diff --git a/ExampleMod/assets/items/ruby_pickaxe.png b/ExampleMod/assets/items/ruby_pickaxe.png new file mode 100644 index 0000000..b019ba4 Binary files /dev/null and b/ExampleMod/assets/items/ruby_pickaxe.png differ diff --git a/WeaveLoader.API/Item/CustomItem.cs b/WeaveLoader.API/Item/CustomItem.cs new file mode 100644 index 0000000..792521a --- /dev/null +++ b/WeaveLoader.API/Item/CustomItem.cs @@ -0,0 +1,128 @@ +using System.Runtime.InteropServices; + +namespace WeaveLoader.API.Item; + +/// +/// Base class for managed custom items. +/// Mods can inherit and override callbacks for item behavior. +/// +public abstract class Item +{ + /// The namespaced ID used during registration. + public Identifier? Id { get; internal set; } + + /// The numeric runtime ID allocated by the game. + public int NumericId { get; internal set; } = -1; + + /// + /// Called when this item is used to mine a block. + /// Return to run vanilla logic (equivalent to calling super), + /// or to skip vanilla handling. + /// + public virtual MineBlockResult OnMineBlock(MineBlockContext context) => MineBlockResult.ContinueVanilla; +} + +/// +/// Result of managed mine-block callback. +/// +public enum MineBlockResult +{ + ContinueVanilla = 0, + CancelVanilla = 1 +} + +/// +/// Tool tier used by native tool constructors. +/// +public enum ToolTier +{ + Wood = 0, + Stone = 1, + Iron = 2, + Diamond = 3, + Gold = 4 +} + +/// +/// Managed pickaxe base class. +/// Override callbacks to customize behavior. +/// +public class PickaxeItem : Item +{ + public ToolTier Tier { get; init; } = ToolTier.Diamond; +} + +/// +/// Runtime context for item mine-block callback. +/// +public readonly struct MineBlockContext +{ + public int ItemId { get; } + public int TileId { get; } + public int X { get; } + public int Y { get; } + public int Z { get; } + + internal MineBlockContext(int itemId, int tileId, int x, int y, int z) + { + ItemId = itemId; + TileId = tileId; + X = x; + Y = y; + Z = z; + } +} + +[StructLayout(LayoutKind.Sequential)] +internal struct MineBlockNativeArgs +{ + public int ItemId; + public int TileId; + public int X; + public int Y; + public int Z; +} + +internal static class ManagedItemDispatcher +{ + private static readonly object s_lock = new(); + private static readonly Dictionary s_items = new(); + + internal static void RegisterItem(Identifier id, int numericId, Item item) + { + item.Id = id; + item.NumericId = numericId; + + lock (s_lock) + { + s_items[numericId] = item; + } + } + + internal static int HandleMineBlock(IntPtr args, int sizeBytes) + { + if (args == IntPtr.Zero || sizeBytes < Marshal.SizeOf()) + return 0; + + MineBlockNativeArgs nativeArgs = Marshal.PtrToStructure(args); + + Item? item; + lock (s_lock) + { + s_items.TryGetValue(nativeArgs.ItemId, out item); + } + + if (item == null) + return 0; + + var result = item.OnMineBlock(new MineBlockContext( + nativeArgs.ItemId, + nativeArgs.TileId, + nativeArgs.X, + nativeArgs.Y, + nativeArgs.Z)); + + // 0 = no managed item, 1 = continue vanilla, 2 = cancel vanilla. + return result == MineBlockResult.CancelVanilla ? 2 : 1; + } +} diff --git a/WeaveLoader.API/Item/ItemRegistry.cs b/WeaveLoader.API/Item/ItemRegistry.cs index 1f1208c..6384343 100644 --- a/WeaveLoader.API/Item/ItemRegistry.cs +++ b/WeaveLoader.API/Item/ItemRegistry.cs @@ -32,12 +32,42 @@ public static class ItemRegistry /// A handle to the registered item. public static RegisteredItem Register(Identifier id, ItemProperties properties) { - int numericId = NativeInterop.native_register_item( - id.ToString(), - properties.MaxStackSizeValue, - properties.MaxDamageValue, - properties.IconValue, - properties.NameValue ?? ""); + return RegisterInternal(id, properties, null); + } + + /// + /// Register a managed custom item implementation. + /// + /// Namespaced identifier (e.g. "mymod:ruby_pickaxe"). + /// Managed item instance that can override callbacks. + /// Item properties built with . + /// A handle to the registered item. + public static RegisteredItem Register(Identifier id, Item item, ItemProperties properties) + { + return RegisterInternal(id, properties, item); + } + + private static RegisteredItem RegisterInternal(Identifier id, ItemProperties properties, Item? managedItem) + { + int numericId; + if (managedItem is PickaxeItem pickaxeItem) + { + numericId = NativeInterop.native_register_pickaxe_item( + id.ToString(), + (int)pickaxeItem.Tier, + properties.MaxDamageValue, + properties.IconValue, + properties.NameValue ?? ""); + } + else + { + numericId = NativeInterop.native_register_item( + id.ToString(), + properties.MaxStackSizeValue, + properties.MaxDamageValue, + properties.IconValue, + properties.NameValue ?? ""); + } if (numericId < 0) throw new InvalidOperationException($"Failed to register item '{id}'. No free IDs or invalid parameters."); @@ -48,6 +78,12 @@ public static class ItemRegistry Logger.Debug($"Item '{id}' added to creative tab {properties.CreativeTabValue}"); } + if (managedItem != null) + { + ManagedItemDispatcher.RegisterItem(id, numericId, managedItem); + Logger.Debug($"Managed item dispatcher mapped '{id}' -> numeric ID {numericId} ({managedItem.GetType().FullName})"); + } + Logger.Debug($"Registered item '{id}' -> numeric ID {numericId}"); return new RegisteredItem(id, numericId); } diff --git a/WeaveLoader.API/Logger.cs b/WeaveLoader.API/Logger.cs index d3cf1c9..b294477 100644 --- a/WeaveLoader.API/Logger.cs +++ b/WeaveLoader.API/Logger.cs @@ -29,8 +29,21 @@ public static class Logger public static void Log(string message, LogLevel level = LogLevel.Info) { if (LogHandler != null) + { LogHandler(message, level); - else - Console.WriteLine($"[WeaveLoader/{level}] {message}"); + return; + } + + string formatted = $"[WeaveLoader/{level}] {message}"; + try + { + // Fallback path: write directly to native runtime logging so mod logs + // still appear even if the managed log handler was not initialized. + NativeInterop.native_log(formatted, (int)level); + } + catch + { + Console.WriteLine(formatted); + } } } diff --git a/WeaveLoader.API/NativeInterop.cs b/WeaveLoader.API/NativeInterop.cs index adccf7c..7f7a49d 100644 --- a/WeaveLoader.API/NativeInterop.cs +++ b/WeaveLoader.API/NativeInterop.cs @@ -30,6 +30,14 @@ internal static class NativeInterop string iconName, string displayName); + [DllImport(RuntimeDll, CallingConvention = CallingConvention.Cdecl, CharSet = CharSet.Ansi)] + internal static extern int native_register_pickaxe_item( + string namespacedId, + int tier, + int maxDamage, + string iconName, + string displayName); + [DllImport(RuntimeDll, CallingConvention = CallingConvention.Cdecl, CharSet = CharSet.Ansi)] internal static extern int native_allocate_description_id(); diff --git a/WeaveLoader.API/Registry.cs b/WeaveLoader.API/Registry.cs index 5b0c8d1..05b4c37 100644 --- a/WeaveLoader.API/Registry.cs +++ b/WeaveLoader.API/Registry.cs @@ -24,6 +24,9 @@ public static class Registry { public static RegisteredItem Register(Identifier id, ItemProperties properties) => ItemRegistry.Register(id, properties); + + public static RegisteredItem Register(Identifier id, WeaveLoader.API.Item.Item item, ItemProperties properties) + => ItemRegistry.Register(id, item, properties); } /// Entity registration. Call Register() with a namespaced ID and EntityDefinition. diff --git a/WeaveLoader.Core/WeaveLoaderCore.cs b/WeaveLoader.Core/WeaveLoaderCore.cs index dc922c7..6b4be6d 100644 --- a/WeaveLoader.Core/WeaveLoaderCore.cs +++ b/WeaveLoader.Core/WeaveLoaderCore.cs @@ -1,5 +1,6 @@ using System.Runtime.InteropServices; using WeaveLoader.API; +using WeaveLoader.API.Item; namespace WeaveLoader.Core; @@ -92,4 +93,17 @@ public static class WeaveLoaderCore Logger.Info("WeaveLoader shut down."); return 0; } + + public static int OnItemMineBlock(IntPtr args, int sizeBytes) + { + try + { + return ManagedItemDispatcher.HandleMineBlock(args, sizeBytes); + } + catch (Exception ex) + { + Logger.Error($"OnItemMineBlock EXCEPTION: {ex}"); + return 0; + } + } } diff --git a/WeaveLoaderRuntime/src/DotNetHost.cpp b/WeaveLoaderRuntime/src/DotNetHost.cpp index 574f5c2..12f4dbc 100644 --- a/WeaveLoaderRuntime/src/DotNetHost.cpp +++ b/WeaveLoaderRuntime/src/DotNetHost.cpp @@ -22,6 +22,7 @@ static managed_entry_fn fn_Init = nullptr; static managed_entry_fn fn_PostInit = nullptr; static managed_entry_fn fn_Tick = nullptr; static managed_entry_fn fn_Shutdown = nullptr; +static managed_entry_fn fn_ItemMineBlock = nullptr; static bool LoadHostfxr() { @@ -177,6 +178,7 @@ bool DotNetHost::Initialize() ok &= resolve(L"PostInit", &fn_PostInit); ok &= resolve(L"Tick", &fn_Tick); ok &= resolve(L"Shutdown", &fn_Shutdown); + ok &= resolve(L"OnItemMineBlock", &fn_ItemMineBlock); if (!ok) { @@ -227,6 +229,13 @@ void DotNetHost::CallShutdown() if (fn_Shutdown) fn_Shutdown(nullptr, 0); } +int DotNetHost::CallItemMineBlock(const void* args, int sizeBytes) +{ + if (!fn_ItemMineBlock || !args || sizeBytes <= 0) + return 0; + return fn_ItemMineBlock(const_cast(args), sizeBytes); +} + void DotNetHost::Cleanup() { } diff --git a/WeaveLoaderRuntime/src/DotNetHost.h b/WeaveLoaderRuntime/src/DotNetHost.h index c4c4534..9283d88 100644 --- a/WeaveLoaderRuntime/src/DotNetHost.h +++ b/WeaveLoaderRuntime/src/DotNetHost.h @@ -15,4 +15,5 @@ namespace DotNetHost void CallPostInit(); void CallTick(); void CallShutdown(); + int CallItemMineBlock(const void* args, int sizeBytes); } diff --git a/WeaveLoaderRuntime/src/GameHooks.cpp b/WeaveLoaderRuntime/src/GameHooks.cpp index 585504c..46df816 100644 --- a/WeaveLoaderRuntime/src/GameHooks.cpp +++ b/WeaveLoaderRuntime/src/GameHooks.cpp @@ -24,6 +24,86 @@ namespace GameHooks GetResourceAsStream_fn Original_GetResourceAsStream = nullptr; LoadUVs_fn Original_LoadUVs = nullptr; RegisterIcon_fn Original_RegisterIcon = nullptr; + ItemInstanceMineBlock_fn Original_ItemInstanceMineBlock = nullptr; + ItemMineBlock_fn Original_ItemMineBlock = nullptr; + ItemMineBlock_fn Original_DiggerItemMineBlock = nullptr; + static int s_itemMineBlockHookCalls = 0; + + struct MineBlockNativeArgs + { + int itemId; + int tileId; + int x; + int y; + int z; + }; + + 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) + { + __try + { + int id = *reinterpret_cast(static_cast(itemInstancePtr) + off); + if (id > 0 && id < 32000) + { + outItemId = id; + return true; + } + } + __except (EXCEPTION_EXECUTE_HANDLER) {} + } + + 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 void* DecodeItemInstancePtrFromSharedArg(void* sharedArg) + { + if (!sharedArg) + return nullptr; + + // Candidate A: shared_ptr object where first field is raw ItemInstance*. + __try + { + void* p = *reinterpret_cast(sharedArg); + if (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; + } void __fastcall Hooked_LoadUVs(void* thisPtr) { @@ -56,6 +136,48 @@ namespace GameHooks return result; } + 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); + } + + 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; + } + void* Hooked_GetResourceAsStream(const void* fileName) { const std::wstring* path = static_cast(fileName); diff --git a/WeaveLoaderRuntime/src/GameHooks.h b/WeaveLoaderRuntime/src/GameHooks.h index 202f9d5..090433b 100644 --- a/WeaveLoaderRuntime/src/GameHooks.h +++ b/WeaveLoaderRuntime/src/GameHooks.h @@ -18,6 +18,8 @@ typedef const wchar_t* (*GetString_fn)(int); typedef void* (*GetResourceAsStream_fn)(const void* fileName); typedef void (__fastcall *LoadUVs_fn)(void* thisPtr); typedef void* (__fastcall *RegisterIcon_fn)(void* thisPtr, const std::wstring& name); +typedef void (__fastcall *ItemInstanceMineBlock_fn)(void* thisPtr, void* level, int tile, int x, int y, int z, void* ownerSharedPtr); +typedef bool (__fastcall *ItemMineBlock_fn)(void* thisPtr, void* itemInstanceSharedPtr, void* level, int tile, int x, int y, int z, void* ownerSharedPtr); namespace GameHooks { @@ -33,6 +35,9 @@ namespace GameHooks extern GetResourceAsStream_fn Original_GetResourceAsStream; extern LoadUVs_fn Original_LoadUVs; extern RegisterIcon_fn Original_RegisterIcon; + extern ItemInstanceMineBlock_fn Original_ItemInstanceMineBlock; + extern ItemMineBlock_fn Original_ItemMineBlock; + extern ItemMineBlock_fn Original_DiggerItemMineBlock; void Hooked_RunStaticCtors(); void __fastcall Hooked_MinecraftTick(void* thisPtr, bool bFirst, bool bUpdateTextures); @@ -46,4 +51,7 @@ namespace GameHooks void* Hooked_GetResourceAsStream(const void* fileName); void __fastcall Hooked_LoadUVs(void* thisPtr); void* __fastcall Hooked_RegisterIcon(void* thisPtr, const std::wstring& name); + void __fastcall Hooked_ItemInstanceMineBlock(void* thisPtr, void* level, int tile, int x, int y, int z, void* ownerSharedPtr); + bool __fastcall Hooked_ItemMineBlock(void* thisPtr, void* itemInstanceSharedPtr, void* level, int tile, int x, int y, int z, void* ownerSharedPtr); + bool __fastcall Hooked_DiggerItemMineBlock(void* thisPtr, void* itemInstanceSharedPtr, void* level, int tile, int x, int y, int z, void* ownerSharedPtr); } diff --git a/WeaveLoaderRuntime/src/GameObjectFactory.cpp b/WeaveLoaderRuntime/src/GameObjectFactory.cpp index 84a94ae..265fe9e 100644 --- a/WeaveLoaderRuntime/src/GameObjectFactory.cpp +++ b/WeaveLoaderRuntime/src/GameObjectFactory.cpp @@ -22,6 +22,8 @@ typedef void (__fastcall *TileItemCtor_fn)(void* thisPtr, int id); // Item::Item(int id) — protected ctor typedef void (__fastcall *ItemCtor_fn)(void* thisPtr, int id); +// PickaxeItem::PickaxeItem(int id, const Item::Tier* tier) +typedef void (__fastcall *PickaxeCtor_fn)(void* thisPtr, int id, const void* tier); // Item* Item::setIconName(const std::wstring&) typedef void* (__fastcall *ItemSetIconName_fn)(void* thisPtr, const std::wstring& name); // Item::getDescriptionId(int) — used to extract the descriptionId field offset @@ -37,6 +39,7 @@ static TileSetDescriptionId_fn fnTileSetDescriptionId = nullptr; static TileItemCtor_fn fnTileItemCtor = nullptr; static ItemCtor_fn fnItemCtor = nullptr; +static PickaxeCtor_fn fnPickaxeCtor = nullptr; static ItemSetIconName_fn fnItemSetIconName= nullptr; static int s_itemDescIdOffset = -1; // offset of descriptionId field in Item, extracted from getDescriptionId @@ -44,6 +47,7 @@ static int s_itemDescIdOffset = -1; // offset of descriptionId field in Item, ex // (they're NULL at resolve time because staticCtor hasn't run yet). static void** s_materialAddrs[16] = {}; static void** s_soundAddrs[10] = {}; +static void** s_tierAddrs[5] = {}; static const int TILE_ALLOC_SIZE = 1024; static const int ITEM_ALLOC_SIZE = 1024; @@ -63,6 +67,12 @@ static void* GetSound(int idx) return *s_soundAddrs[idx]; } +static const void* GetTier(int idx) +{ + if (idx < 0 || idx >= 5 || !s_tierAddrs[idx]) return nullptr; + return *s_tierAddrs[idx]; +} + namespace GameObjectFactory { @@ -90,6 +100,7 @@ bool ResolveSymbols(SymbolResolver& resolver) // Item constructor — protected (IEAA not QEAA) fnItemCtor = (ItemCtor_fn)resolver.Resolve("??0Item@@IEAA@H@Z"); + fnPickaxeCtor = (PickaxeCtor_fn)resolver.Resolve("??0PickaxeItem@@QEAA@HPEBVTier@Item@@@Z"); // Item::setIconName fnItemSetIconName = (ItemSetIconName_fn)resolver.Resolve( @@ -159,6 +170,13 @@ bool ResolveSymbols(SymbolResolver& resolver) resolveSound(8, "?SOUND_SAND@Tile@@2PEAVSoundType@1@EA"); resolveSound(9, "?SOUND_SNOW@Tile@@2PEAVSoundType@1@EA"); + // Resolve Item::Tier static pointer ADDRESSES + s_tierAddrs[0] = (void**)resolver.Resolve("?WOOD@Tier@Item@@2PEBV12@EB"); + s_tierAddrs[1] = (void**)resolver.Resolve("?STONE@Tier@Item@@2PEBV12@EB"); + s_tierAddrs[2] = (void**)resolver.Resolve("?IRON@Tier@Item@@2PEBV12@EB"); + s_tierAddrs[3] = (void**)resolver.Resolve("?DIAMOND@Tier@Item@@2PEBV12@EB"); + s_tierAddrs[4] = (void**)resolver.Resolve("?GOLD@Tier@Item@@2PEBV12@EB"); + auto logSym = [](const char* name, void* ptr) { if (ptr) LogUtil::Log("[WeaveLoader] GOF %-20s @ %p", name, ptr); else LogUtil::Log("[WeaveLoader] GOF MISSING: %s", name); @@ -171,6 +189,7 @@ bool ResolveSymbols(SymbolResolver& resolver) logSym("Tile::setIconName", (void*)fnTileSetIconName); logSym("TileItem::TileItem", (void*)fnTileItemCtor); logSym("Item::Item", (void*)fnItemCtor); + logSym("PickaxeItem::PickaxeItem", (void*)fnPickaxeCtor); logSym("Item::setIconName", (void*)fnItemSetIconName); logSym("Material::stone addr", (void*)s_materialAddrs[1]); logSym("SOUND_STONE addr", (void*)s_soundAddrs[1]); @@ -258,7 +277,7 @@ bool CreateTile(int tileId, int materialType, float hardness, float resistance, return true; } -bool CreateItem(int itemId, int maxStackSize, const wchar_t* iconName, int descriptionId) +bool CreateItem(int itemId, int maxStackSize, int maxDamage, const wchar_t* iconName, int descriptionId) { if (!s_resolved || !fnItemCtor) { @@ -272,6 +291,17 @@ bool CreateItem(int itemId, int maxStackSize, const wchar_t* iconName, int descr memset(item, 0, ITEM_ALLOC_SIZE); fnItemCtor(item, ctorParam); + // Verified from Item::Item disassembly: + // +0x24 = maxStackSize, +0x28 = maxDamage. + if (maxStackSize > 0) + { + *reinterpret_cast(static_cast(item) + 0x24) = maxStackSize; + } + if (maxDamage > 0) + { + *reinterpret_cast(static_cast(item) + 0x28) = maxDamage; + } + // The game calls __debugbreak() if registerIcons is called with an empty // m_textureName, so always set a non-empty icon name. if (fnItemSetIconName) @@ -286,10 +316,72 @@ bool CreateItem(int itemId, int maxStackSize, const wchar_t* iconName, int descr static_cast(descriptionId); } - LogUtil::Log("[WeaveLoader] Created Item id=%d (ctorParam=%d, icon=%ls, descId=%d)", - itemId, ctorParam, iconName ? iconName : L"", descriptionId); + LogUtil::Log("[WeaveLoader] Created Item id=%d (ctorParam=%d, stack=%d, damage=%d, icon=%ls, descId=%d)", + itemId, ctorParam, maxStackSize, maxDamage, + iconName ? iconName : L"", descriptionId); return true; } +bool CreatePickaxeItem(int itemId, int tier, int maxDamage, const wchar_t* iconName, int descriptionId) +{ + if (!s_resolved || !fnPickaxeCtor) + { + LogUtil::Log("[WeaveLoader] CreatePickaxeItem: symbols not resolved"); + return false; + } + + const void* tierPtr = GetTier(tier); + if (!tierPtr) + { + LogUtil::Log("[WeaveLoader] CreatePickaxeItem: invalid tier %d", tier); + return false; + } + + int ctorParam = itemId - 256; + void* item = ::operator new(ITEM_ALLOC_SIZE); + memset(item, 0, ITEM_ALLOC_SIZE); + fnPickaxeCtor(item, ctorParam, tierPtr); + + // Ensure pickaxe category/material for crafting menus: + // baseType=pickaxe(3), material depends on tier. + *reinterpret_cast(static_cast(item) + 0x38) = 3; + int material = 5; // diamond default + switch (tier) + { + case 0: material = 1; break; // wood + case 1: material = 2; break; // stone + case 2: material = 3; break; // iron + case 3: material = 5; break; // diamond + case 4: material = 4; break; // gold + default: break; + } + *reinterpret_cast(static_cast(item) + 0x3C) = material; + + // Tools should always stack to 1. + *reinterpret_cast(static_cast(item) + 0x24) = 1; + + // Optionally override tier durability. + if (maxDamage > 0) + { + *reinterpret_cast(static_cast(item) + 0x28) = maxDamage; + } + + if (fnItemSetIconName) + { + std::wstring name = (iconName && iconName[0]) ? iconName : L"MISSING_ICON_ITEM"; + fnItemSetIconName(item, name); + } + + if (s_itemDescIdOffset > 0 && descriptionId >= 0) + { + *reinterpret_cast(static_cast(item) + s_itemDescIdOffset) = + static_cast(descriptionId); + } + + LogUtil::Log("[WeaveLoader] Created PickaxeItem id=%d (ctorParam=%d, tier=%d, damage=%d, icon=%ls, descId=%d)", + itemId, ctorParam, tier, maxDamage, iconName ? iconName : L"", descriptionId); + return true; +} + } // namespace GameObjectFactory diff --git a/WeaveLoaderRuntime/src/GameObjectFactory.h b/WeaveLoaderRuntime/src/GameObjectFactory.h index 41a63aa..fd97e21 100644 --- a/WeaveLoaderRuntime/src/GameObjectFactory.h +++ b/WeaveLoaderRuntime/src/GameObjectFactory.h @@ -18,5 +18,12 @@ namespace GameObjectFactory // Create an Item game object. itemId is the FINAL id (256 + constructor param). // The Item is registered in Item::items[itemId]. // descriptionId: if >= 0, call setDescriptionId on the Item for localization. - bool CreateItem(int itemId, int maxStackSize, const wchar_t* iconName, int descriptionId = -1); + bool CreateItem(int itemId, int maxStackSize, int maxDamage, + const wchar_t* iconName, int descriptionId = -1); + + // Create a native PickaxeItem game object. + // tier: 0=wood, 1=stone, 2=iron, 3=diamond, 4=gold. + // maxDamage: if > 0, overrides the tier default durability. + bool CreatePickaxeItem(int itemId, int tier, int maxDamage, + const wchar_t* iconName, int descriptionId = -1); } diff --git a/WeaveLoaderRuntime/src/HookManager.cpp b/WeaveLoaderRuntime/src/HookManager.cpp index 8ce65c8..d9e5636 100644 --- a/WeaveLoaderRuntime/src/HookManager.cpp +++ b/WeaveLoaderRuntime/src/HookManager.cpp @@ -54,6 +54,48 @@ bool HookManager::Install(const SymbolResolver& symbols) LogUtil::Log("[WeaveLoader] Hooked Minecraft::init"); } + if (symbols.pItemInstanceMineBlock) + { + if (MH_CreateHook(symbols.pItemInstanceMineBlock, + reinterpret_cast(&GameHooks::Hooked_ItemInstanceMineBlock), + reinterpret_cast(&GameHooks::Original_ItemInstanceMineBlock)) != MH_OK) + { + LogUtil::Log("[WeaveLoader] Warning: Failed to hook ItemInstance::mineBlock"); + } + else + { + LogUtil::Log("[WeaveLoader] Hooked ItemInstance::mineBlock (managed item callbacks)"); + } + } + + if (symbols.pItemMineBlock) + { + if (MH_CreateHook(symbols.pItemMineBlock, + reinterpret_cast(&GameHooks::Hooked_ItemMineBlock), + reinterpret_cast(&GameHooks::Original_ItemMineBlock)) != MH_OK) + { + LogUtil::Log("[WeaveLoader] Warning: Failed to hook Item::mineBlock"); + } + else + { + LogUtil::Log("[WeaveLoader] Hooked Item::mineBlock (managed item callbacks)"); + } + } + + if (symbols.pDiggerItemMineBlock) + { + if (MH_CreateHook(symbols.pDiggerItemMineBlock, + reinterpret_cast(&GameHooks::Hooked_DiggerItemMineBlock), + reinterpret_cast(&GameHooks::Original_DiggerItemMineBlock)) != MH_OK) + { + LogUtil::Log("[WeaveLoader] Warning: Failed to hook DiggerItem::mineBlock"); + } + else + { + LogUtil::Log("[WeaveLoader] Hooked DiggerItem::mineBlock (managed item callbacks)"); + } + } + if (symbols.pExitGame) { if (MH_CreateHook(symbols.pExitGame, diff --git a/WeaveLoaderRuntime/src/NativeExports.cpp b/WeaveLoaderRuntime/src/NativeExports.cpp index 0cf748a..6790bbb 100644 --- a/WeaveLoaderRuntime/src/NativeExports.cpp +++ b/WeaveLoaderRuntime/src/NativeExports.cpp @@ -106,7 +106,8 @@ int native_register_item( ModStrings::Register(descId, wName.c_str()); } - if (!GameObjectFactory::CreateItem(id, maxStackSize, wIcon.empty() ? nullptr : wIcon.c_str(), descId)) + if (!GameObjectFactory::CreateItem(id, maxStackSize, maxDamage, + wIcon.empty() ? nullptr : wIcon.c_str(), descId)) { LogUtil::Log("[WeaveLoader] Warning: failed to create game Item for '%s' id=%d", namespacedId, id); } @@ -114,6 +115,44 @@ int native_register_item( return id; } +int native_register_pickaxe_item( + const char* namespacedId, + int tier, + int maxDamage, + const char* iconName, + const char* displayName) +{ + if (!namespacedId) return -1; + + int id = IdRegistry::Instance().Register(IdRegistry::Type::Item, namespacedId); + if (id < 0) + { + LogUtil::Log("[WeaveLoader] Failed to allocate pickaxe item ID for '%s'", namespacedId); + return -1; + } + + LogUtil::Log("[WeaveLoader] Registered pickaxe item '%s' -> ID %d (tier=%d, durability=%d)", + namespacedId, id, tier, maxDamage); + + std::wstring wIcon = Utf8ToWide(iconName); + + int descId = -1; + if (displayName && displayName[0]) + { + descId = ModStrings::AllocateId(); + std::wstring wName = Utf8ToWide(displayName); + ModStrings::Register(descId, wName.c_str()); + } + + if (!GameObjectFactory::CreatePickaxeItem(id, tier, maxDamage, + wIcon.empty() ? nullptr : wIcon.c_str(), descId)) + { + LogUtil::Log("[WeaveLoader] Warning: failed to create native PickaxeItem for '%s' id=%d", namespacedId, id); + } + + return id; +} + int native_allocate_description_id() { return ModStrings::AllocateId(); diff --git a/WeaveLoaderRuntime/src/NativeExports.h b/WeaveLoaderRuntime/src/NativeExports.h index 96c9c17..283959b 100644 --- a/WeaveLoaderRuntime/src/NativeExports.h +++ b/WeaveLoaderRuntime/src/NativeExports.h @@ -23,6 +23,13 @@ extern "C" const char* iconName, const char* displayName); + __declspec(dllexport) int native_register_pickaxe_item( + const char* namespacedId, + int tier, + int maxDamage, + const char* iconName, + const char* displayName); + __declspec(dllexport) int native_allocate_description_id(); __declspec(dllexport) void native_register_string(int descriptionId, const char* displayName); diff --git a/WeaveLoaderRuntime/src/SymbolResolver.cpp b/WeaveLoaderRuntime/src/SymbolResolver.cpp index 912f139..8ea0fc3 100644 --- a/WeaveLoaderRuntime/src/SymbolResolver.cpp +++ b/WeaveLoaderRuntime/src/SymbolResolver.cpp @@ -18,6 +18,9 @@ static const char* SYM_LOAD_UVS = "?loadUVs@PreStitchedTextureMap@@AEAAXXZ"; static const char* SYM_SIMPLE_ICON_CTOR = "??0SimpleIcon@@QEAA@AEBV?$basic_string@_WU?$char_traits@_W@std@@V?$allocator@_W@2@@std@@0MMMM@Z"; static const char* SYM_OPERATOR_NEW = "??2@YAPEAX_K@Z"; static const char* SYM_REGISTER_ICON = "?registerIcon@PreStitchedTextureMap@@UEAAPEAVIcon@@AEBV?$basic_string@_WU?$char_traits@_W@std@@V?$allocator@_W@2@@std@@@Z"; +static const char* SYM_ITEMINSTANCE_MINEBLOCK = "?mineBlock@ItemInstance@@QEAAXPEAVLevel@@HHHHV?$shared_ptr@VPlayer@@@std@@@Z"; +static const char* SYM_ITEM_MINEBLOCK = "?mineBlock@Item@@UEAA_NV?$shared_ptr@VItemInstance@@@std@@PEAVLevel@@HHHHV?$shared_ptr@VLivingEntity@@@3@@Z"; +static const char* SYM_DIGGERITEM_MINEBLOCK = "?mineBlock@DiggerItem@@UEAA_NV?$shared_ptr@VItemInstance@@@std@@PEAVLevel@@HHHHV?$shared_ptr@VLivingEntity@@@3@@Z"; bool SymbolResolver::Initialize() { @@ -88,6 +91,9 @@ bool SymbolResolver::ResolveGameFunctions() pSimpleIconCtor = Resolve(SYM_SIMPLE_ICON_CTOR); pOperatorNew = Resolve(SYM_OPERATOR_NEW); pRegisterIcon = Resolve(SYM_REGISTER_ICON); + pItemInstanceMineBlock = Resolve(SYM_ITEMINSTANCE_MINEBLOCK); + pItemMineBlock = Resolve(SYM_ITEM_MINEBLOCK); + pDiggerItemMineBlock = Resolve(SYM_DIGGERITEM_MINEBLOCK); if (!pOperatorNew) pOperatorNew = GetProcAddress(GetModuleHandleA("vcruntime140.dll"), SYM_OPERATOR_NEW); if (!pOperatorNew) pOperatorNew = GetProcAddress(GetModuleHandleA("vcruntime140d.dll"), SYM_OPERATOR_NEW); if (!pOperatorNew) pOperatorNew = GetProcAddress(GetModuleHandle(nullptr), SYM_OPERATOR_NEW); @@ -114,6 +120,9 @@ bool SymbolResolver::ResolveGameFunctions() logSym("SimpleIcon::SimpleIcon", pSimpleIconCtor); logSym("operator new", pOperatorNew); logSym("registerIcon", pRegisterIcon); + logSym("ItemInstance::mineBlock", pItemInstanceMineBlock); + logSym("Item::mineBlock", pItemMineBlock); + logSym("DiggerItem::mineBlock", pDiggerItemMineBlock); bool ok = pRunStaticCtors && pMinecraftTick && pMinecraftInit; if (ok) diff --git a/WeaveLoaderRuntime/src/SymbolResolver.h b/WeaveLoaderRuntime/src/SymbolResolver.h index 968152a..295aac9 100644 --- a/WeaveLoaderRuntime/src/SymbolResolver.h +++ b/WeaveLoaderRuntime/src/SymbolResolver.h @@ -23,6 +23,9 @@ public: void* pSimpleIconCtor = nullptr; // SimpleIcon::SimpleIcon(wstring,wstring,float*4) void* pOperatorNew = nullptr; // global operator new(size_t) - for texture injection void* pRegisterIcon = nullptr; // PreStitchedTextureMap::registerIcon(const wstring&) + void* pItemInstanceMineBlock = nullptr; // ItemInstance::mineBlock(Level*,int,int,int,int,shared_ptr) + void* pItemMineBlock = nullptr; // Item::mineBlock(shared_ptr,Level*,int,int,int,int,shared_ptr) + void* pDiggerItemMineBlock = nullptr; // DiggerItem::mineBlock(shared_ptr,Level*,int,int,int,int,shared_ptr) private: uintptr_t m_moduleBase = 0;