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;