mirror of
https://github.com/Jacobwasbeast/LegacyWeaveLoader.git
synced 2026-06-30 08:11:30 +00:00
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:
@@ -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;
|
||||
|
||||
BIN
ExampleMod/assets/items/ruby_pickaxe.png
Normal file
BIN
ExampleMod/assets/items/ruby_pickaxe.png
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 3.1 KiB |
128
WeaveLoader.API/Item/CustomItem.cs
Normal file
128
WeaveLoader.API/Item/CustomItem.cs
Normal 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;
|
||||
}
|
||||
}
|
||||
@@ -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);
|
||||
}
|
||||
|
||||
@@ -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);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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();
|
||||
|
||||
|
||||
@@ -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>
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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()
|
||||
{
|
||||
}
|
||||
|
||||
@@ -15,4 +15,5 @@ namespace DotNetHost
|
||||
void CallPostInit();
|
||||
void CallTick();
|
||||
void CallShutdown();
|
||||
int CallItemMineBlock(const void* args, int sizeBytes);
|
||||
}
|
||||
|
||||
@@ -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);
|
||||
|
||||
@@ -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);
|
||||
}
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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);
|
||||
}
|
||||
|
||||
@@ -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,
|
||||
|
||||
@@ -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();
|
||||
|
||||
@@ -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);
|
||||
|
||||
|
||||
@@ -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)
|
||||
|
||||
@@ -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;
|
||||
|
||||
Reference in New Issue
Block a user