feat(items): add managed custom item callbacks and native pickaxe support

Introduce a managed custom item API with mine-block callbacks and cancellation semantics, plus native runtime support for registering pickaxe items.

Key changes:

- add WeaveLoader.API Item base/PickaxeItem and dispatcher plumbing

- register managed item instances in ItemRegistry

- add native export for pickaxe registration and wire through GameObjectFactory

- resolve/hook item mineBlock paths (ItemInstance/Item/DiggerItem) and dispatch to managed host

- expose managed OnItemMineBlock entry in WeaveLoader.Core and DotNetHost

- add Ruby Pickaxe example item + placeholder texture

- keep logger usable even before managed handler setup via native fallback
This commit is contained in:
Jacobwasbeast
2026-03-07 13:42:46 -06:00
parent 4119522cde
commit 6464263d12
19 changed files with 572 additions and 13 deletions

View File

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

Binary file not shown.

After

Width:  |  Height:  |  Size: 3.1 KiB

View File

@@ -0,0 +1,128 @@
using System.Runtime.InteropServices;
namespace WeaveLoader.API.Item;
/// <summary>
/// Base class for managed custom items.
/// Mods can inherit and override callbacks for item behavior.
/// </summary>
public abstract class Item
{
/// <summary>The namespaced ID used during registration.</summary>
public Identifier? Id { get; internal set; }
/// <summary>The numeric runtime ID allocated by the game.</summary>
public int NumericId { get; internal set; } = -1;
/// <summary>
/// Called when this item is used to mine a block.
/// Return <see cref="MineBlockResult.ContinueVanilla"/> to run vanilla logic (equivalent to calling super),
/// or <see cref="MineBlockResult.CancelVanilla"/> to skip vanilla handling.
/// </summary>
public virtual MineBlockResult OnMineBlock(MineBlockContext context) => MineBlockResult.ContinueVanilla;
}
/// <summary>
/// Result of managed mine-block callback.
/// </summary>
public enum MineBlockResult
{
ContinueVanilla = 0,
CancelVanilla = 1
}
/// <summary>
/// Tool tier used by native tool constructors.
/// </summary>
public enum ToolTier
{
Wood = 0,
Stone = 1,
Iron = 2,
Diamond = 3,
Gold = 4
}
/// <summary>
/// Managed pickaxe base class.
/// Override callbacks to customize behavior.
/// </summary>
public class PickaxeItem : Item
{
public ToolTier Tier { get; init; } = ToolTier.Diamond;
}
/// <summary>
/// Runtime context for item mine-block callback.
/// </summary>
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<int, Item> 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<MineBlockNativeArgs>())
return 0;
MineBlockNativeArgs nativeArgs = Marshal.PtrToStructure<MineBlockNativeArgs>(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;
}
}

View File

@@ -32,12 +32,42 @@ public static class ItemRegistry
/// <returns>A handle to the registered item.</returns>
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);
}
/// <summary>
/// Register a managed custom item implementation.
/// </summary>
/// <param name="id">Namespaced identifier (e.g. "mymod:ruby_pickaxe").</param>
/// <param name="item">Managed item instance that can override callbacks.</param>
/// <param name="properties">Item properties built with <see cref="ItemProperties"/>.</param>
/// <returns>A handle to the registered item.</returns>
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);
}

View File

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

View File

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

View File

@@ -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);
}
/// <summary>Entity registration. Call Register() with a namespaced ID and EntityDefinition.</summary>

View File

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

View File

@@ -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<void*>(args), sizeBytes);
}
void DotNetHost::Cleanup()
{
}

View File

@@ -15,4 +15,5 @@ namespace DotNetHost
void CallPostInit();
void CallTick();
void CallShutdown();
int CallItemMineBlock(const void* args, int sizeBytes);
}

View File

@@ -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<int*>(static_cast<char*>(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<ItemInstance> object where first field is raw ItemInstance*.
__try
{
void* p = *reinterpret_cast<void**>(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<const std::wstring*>(fileName);

View File

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

View File

@@ -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<int*>(static_cast<char*>(item) + 0x24) = maxStackSize;
}
if (maxDamage > 0)
{
*reinterpret_cast<int*>(static_cast<char*>(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<unsigned int>(descriptionId);
}
LogUtil::Log("[WeaveLoader] Created Item id=%d (ctorParam=%d, icon=%ls, descId=%d)",
itemId, ctorParam, iconName ? iconName : L"<none>", 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"<none>", 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<int*>(static_cast<char*>(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<int*>(static_cast<char*>(item) + 0x3C) = material;
// Tools should always stack to 1.
*reinterpret_cast<int*>(static_cast<char*>(item) + 0x24) = 1;
// Optionally override tier durability.
if (maxDamage > 0)
{
*reinterpret_cast<int*>(static_cast<char*>(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<unsigned int*>(static_cast<char*>(item) + s_itemDescIdOffset) =
static_cast<unsigned int>(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"<none>", descriptionId);
return true;
}
} // namespace GameObjectFactory

View File

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

View File

@@ -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<void*>(&GameHooks::Hooked_ItemInstanceMineBlock),
reinterpret_cast<void**>(&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<void*>(&GameHooks::Hooked_ItemMineBlock),
reinterpret_cast<void**>(&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<void*>(&GameHooks::Hooked_DiggerItemMineBlock),
reinterpret_cast<void**>(&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,

View File

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

View File

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

View File

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

View File

@@ -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<Player>)
void* pItemMineBlock = nullptr; // Item::mineBlock(shared_ptr<ItemInstance>,Level*,int,int,int,int,shared_ptr<LivingEntity>)
void* pDiggerItemMineBlock = nullptr; // DiggerItem::mineBlock(shared_ptr<ItemInstance>,Level*,int,int,int,int,shared_ptr<LivingEntity>)
private:
uintptr_t m_moduleBase = 0;