feat(pickaxe): implement Hooked_PickaxeItemGetDestroySpeed and Hooked_PickaxeItemCanDestroySpecial

- Add hook implementations for custom pickaxe tier support
- Hooked_PickaxeItemGetDestroySpeed: use CustomPickaxeRegistry destroy speed for configured pickaxes when mining effective blocks
- Hooked_PickaxeItemCanDestroySpecial: use CustomPickaxeRegistry effective blocks and harvest level (obsidian requires level 3)
- Add TryReadItemIdFromPickaxe and TryReadTileId helpers for reading item/tile IDs from native pointers
This commit is contained in:
Jacobwasbeast
2026-03-08 00:16:15 -06:00
parent b924105102
commit d2be935ada
33 changed files with 1273 additions and 27 deletions

View File

@@ -10,19 +10,15 @@ namespace ExampleMod;
public class ExampleMod : IMod
{
public static RegisteredBlock? RubyOre;
public static RegisteredBlock? Orichalcum;
public static RegisteredItem? Ruby;
public static RegisteredItem? RubyPickaxeItem;
public static RegisteredItem? RubyShovelItem;
public static RegisteredItem? RubyHoeItem;
public static RegisteredItem? RubyAxeItem;
public static RegisteredItem? RubySwordItem;
public static RegisteredItem? RubyWandItem;
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);
}
}
private sealed class RubyWand : Item
{
private const long CooldownMs = 1500;
@@ -70,6 +66,31 @@ public class ExampleMod : IMod
}
}
private sealed class RubyShovel : ShovelItem
{
}
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);
}
}
private sealed class RubyAxe : AxeItem
{
}
private sealed class RubyHoe : HoeItem
{
}
private sealed class RubySword : SwordItem
{
}
public void OnInitialize()
{
RubyOre = Registry.Block.Register("examplemod:ruby_ore",
@@ -80,6 +101,20 @@ public class ExampleMod : IMod
.Sound(SoundType.Stone)
.Icon("examplemod:ruby_ore") // From assets/blocks/ruby_ore.png
.Name("Ruby Ore")
.RequiredHarvestLevel(2)
.RequiredTool(ToolType.Pickaxe)
.InCreativeTab(CreativeTab.BuildingBlocks));
Orichalcum = Registry.Block.Register("examplemod:orichalcum_ore",
new BlockProperties()
.Material(MaterialType.Metal)
.Hardness(5.0f)
.Resistance(30f)
.Sound(SoundType.Metal)
.Icon("examplemod:orichalcum_ore") // From assets/blocks/orichalcum.png
.Name("Orichalcum Ore")
.RequiredHarvestLevel(4)
.RequiredTool(ToolType.Pickaxe)
.InCreativeTab(CreativeTab.BuildingBlocks));
Ruby = Registry.Item.Register("examplemod:ruby",
@@ -89,14 +124,57 @@ public class ExampleMod : IMod
.Name("Ruby")
.InCreativeTab(CreativeTab.Materials));
RubyPickaxeItem = Registry.Item.Register("examplemod:ruby_pickaxe", new RubyPickaxe(),
Registry.Item.RegisterToolMaterial("examplemod:ruby_material",
new ToolMaterialDefinition()
.BaseTier(ToolTier.Diamond)
.HarvestLevel(4)
.DestroySpeed(10.0f));
RubySwordItem = Registry.Item.Register("examplemod:ruby_sword", new RubySword { CustomMaterialId = "examplemod:ruby_material" },
new ItemProperties()
.MaxStackSize(1)
.MaxDamage(512)
.AttackDamage(8.0f)
.Icon("examplemod:ruby_sword")
.Name("Ruby Sword")
.InCreativeTab(CreativeTab.ToolsAndWeapons));
RubyShovelItem = Registry.Item.Register("examplemod:ruby_shovel", new RubyShovel { CustomMaterialId = "examplemod:ruby_material" },
new ItemProperties()
.MaxStackSize(1)
.MaxDamage(512)
.AttackDamage(4.5f)
.Icon("examplemod:ruby_shovel")
.Name("Ruby Shovel")
.InCreativeTab(CreativeTab.ToolsAndWeapons));
RubyPickaxeItem = Registry.Item.Register("examplemod:ruby_pickaxe", new RubyPickaxe { CustomMaterialId = "examplemod:ruby_material" },
new ItemProperties()
.MaxStackSize(1)
.MaxDamage(512)
.AttackDamage(5.0f)
.Icon("examplemod:ruby_pickaxe") // From assets/items/ruby_pickaxe.png
.Name("Ruby Pickaxe")
.InCreativeTab(CreativeTab.ToolsAndWeapons));
RubyAxeItem = Registry.Item.Register("examplemod:ruby_axe", new RubyAxe { CustomMaterialId = "examplemod:ruby_material" },
new ItemProperties()
.MaxStackSize(1)
.MaxDamage(512)
.AttackDamage(7.0f)
.Icon("examplemod:ruby_axe")
.Name("Ruby Axe")
.InCreativeTab(CreativeTab.ToolsAndWeapons));
RubyHoeItem = Registry.Item.Register("examplemod:ruby_hoe", new RubyHoe { CustomMaterialId = "examplemod:ruby_material" },
new ItemProperties()
.MaxStackSize(1)
.MaxDamage(512)
.AttackDamage(1.0f)
.Icon("examplemod:ruby_hoe")
.Name("Ruby Hoe")
.InCreativeTab(CreativeTab.ToolsAndWeapons));
RubyWandItem = Registry.Item.Register("examplemod:ruby_wand", new RubyWand(),
new ItemProperties()
.MaxStackSize(1)

Binary file not shown.

After

Width:  |  Height:  |  Size: 2.8 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 2.4 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 2.3 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 3.1 KiB

After

Width:  |  Height:  |  Size: 2.9 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 3.6 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 2.3 KiB

View File

@@ -4,4 +4,11 @@
# This file documents the expected format for future multi-locale support.
block.examplemod.ruby_ore=Ruby Ore
block.examplemod.orichalcum_ore=Orichalcum Ore
item.examplemod.ruby=Ruby
item.examplemod.ruby_sword=Ruby Sword
item.examplemod.ruby_shovel=Ruby Shovel
item.examplemod.ruby_pickaxe=Ruby Pickaxe
item.examplemod.ruby_axe=Ruby Axe
item.examplemod.ruby_hoe=Ruby Hoe
item.examplemod.ruby_wand=Ruby Wand

View File

@@ -1,5 +1,20 @@
namespace WeaveLoader.API.Block;
/// <summary>
/// Tool type required to harvest a block. Used with <see cref="BlockProperties.RequiredTool"/>.
/// </summary>
public enum ToolType
{
/// <summary>No specific tool required; any tool or hand can harvest.</summary>
None = 0,
/// <summary>Requires a pickaxe.</summary>
Pickaxe = 1,
/// <summary>Requires an axe.</summary>
Axe = 2,
/// <summary>Requires a shovel.</summary>
Shovel = 3,
}
public enum MaterialType
{
Air = 0,
@@ -48,6 +63,8 @@ public class BlockProperties
internal int LightBlockValue = 255;
internal CreativeTab CreativeTabValue = CreativeTab.None;
internal string? NameValue;
internal int RequiredHarvestLevelValue = -1;
internal ToolType RequiredToolValue = ToolType.None;
public BlockProperties Material(MaterialType material) { MaterialValue = material; return this; }
public BlockProperties Hardness(float hardness) { HardnessValue = hardness; return this; }
@@ -61,4 +78,8 @@ public class BlockProperties
public BlockProperties InCreativeTab(CreativeTab tab) { CreativeTabValue = tab; return this; }
/// <summary>Display name shown in-game (e.g. "Ruby Ore"). Used for localization.</summary>
public BlockProperties Name(string displayName) { NameValue = displayName; return this; }
/// <summary>Minimum harvest level required to properly mine this block (e.g. 3 for obsidian). -1 means no requirement.</summary>
public BlockProperties RequiredHarvestLevel(int level) { RequiredHarvestLevelValue = level; return this; }
/// <summary>Tool type required to harvest this block (e.g. Pickaxe for stone-like blocks).</summary>
public BlockProperties RequiredTool(ToolType tool) { RequiredToolValue = tool; return this; }
}

View File

@@ -46,7 +46,9 @@ public static class BlockRegistry
properties.IconValue,
properties.LightEmissionValue,
properties.LightBlockValue,
properties.NameValue ?? "");
properties.NameValue ?? "",
properties.RequiredHarvestLevelValue,
(int)properties.RequiredToolValue);
if (numericId < 0)
throw new InvalidOperationException($"Failed to register block '{id}'. No free IDs or invalid parameters.");

View File

@@ -63,9 +63,31 @@ public enum ToolTier
/// Managed pickaxe base class.
/// Override callbacks to customize behavior.
/// </summary>
public class PickaxeItem : Item
public abstract class ToolItem : Item
{
public ToolTier Tier { get; init; } = ToolTier.Diamond;
public Identifier? CustomMaterialId { get; init; }
}
public class PickaxeItem : ToolItem
{
public Identifier? CustomTierId { get; init; }
}
public class ShovelItem : ToolItem
{
}
public class HoeItem : ToolItem
{
}
public class AxeItem : ToolItem
{
}
public class SwordItem : ToolItem
{
}
/// <summary>

View File

@@ -7,6 +7,7 @@ public class ItemProperties
{
internal int MaxStackSizeValue = 64;
internal int MaxDamageValue = 0;
internal float AttackDamageValue = 0.0f;
internal string IconValue = "";
internal CreativeTab CreativeTabValue = CreativeTab.None;
internal string? NameValue;
@@ -20,6 +21,8 @@ public class ItemProperties
/// makes the item damageable with a durability bar.
/// </summary>
public ItemProperties MaxDamage(int damage) { MaxDamageValue = damage; MaxStackSizeValue = 1; return this; }
/// <summary>Override the native attack damage value for tool items.</summary>
public ItemProperties AttackDamage(float damage) { AttackDamageValue = damage; return this; }
public ItemProperties InCreativeTab(CreativeTab tab) { CreativeTabValue = tab; return this; }
/// <summary>Display name shown in-game (e.g. "Ruby"). Used for localization.</summary>
public ItemProperties Name(string displayName) { NameValue = displayName; return this; }

View File

@@ -29,6 +29,56 @@ public static class ItemRegistry
private static readonly object s_lock = new();
private static readonly Dictionary<int, Identifier> s_idByNumeric = new();
private enum ToolKind
{
Pickaxe = 1,
Shovel = 2,
Hoe = 3,
Sword = 4,
Axe = 5,
}
private static Identifier? ResolveCustomMaterialId(Item managedItem)
{
if (managedItem is ToolItem toolItem && toolItem.CustomMaterialId is Identifier customMaterialId)
return customMaterialId;
if (managedItem is PickaxeItem pickaxeItem && pickaxeItem.CustomTierId is Identifier legacyPickaxeTierId)
return legacyPickaxeTierId;
return null;
}
private static ToolMaterialDefinition? ResolveToolMaterial(Identifier itemId, Item managedItem)
{
Identifier? customMaterialId = ResolveCustomMaterialId(managedItem);
if (customMaterialId == null)
return null;
Identifier resolvedMaterialId = (Identifier)customMaterialId;
if (!ToolMaterialRegistry.TryGetDefinition(resolvedMaterialId, out ToolMaterialDefinition? definition) || definition == null)
throw new InvalidOperationException($"Unknown tool material '{resolvedMaterialId}' for item '{itemId}'.");
return definition;
}
private static void ConfigureToolMaterial(Identifier itemId, int numericId, ToolKind toolKind, ToolMaterialDefinition? material, ItemProperties properties)
{
if (material == null && properties.AttackDamageValue <= 0.0f)
return;
int configured = NativeInterop.native_configure_custom_tool_item(
numericId,
(int)toolKind,
material?.HarvestLevelValue ?? 0,
material?.DestroySpeedValue ?? 1.0f,
properties.AttackDamageValue);
if (configured == 0)
throw new InvalidOperationException($"Failed to configure custom tool material for item '{itemId}'.");
}
/// <summary>
/// Register a new item with the game engine.
/// </summary>
@@ -57,12 +107,71 @@ public static class ItemRegistry
int numericId;
if (managedItem is PickaxeItem pickaxeItem)
{
ToolMaterialDefinition? material = ResolveToolMaterial(id, pickaxeItem);
ToolTier nativeTier = material?.BaseTierValue ?? pickaxeItem.Tier;
int maxDamage = properties.MaxDamageValue;
numericId = NativeInterop.native_register_pickaxe_item(
id.ToString(),
(int)pickaxeItem.Tier,
(int)nativeTier,
maxDamage,
properties.IconValue,
properties.NameValue ?? "");
if (numericId >= 0)
ConfigureToolMaterial(id, numericId, ToolKind.Pickaxe, material, properties);
}
else if (managedItem is ShovelItem shovelItem)
{
ToolMaterialDefinition? material = ResolveToolMaterial(id, shovelItem);
numericId = NativeInterop.native_register_shovel_item(
id.ToString(),
(int)(material?.BaseTierValue ?? shovelItem.Tier),
properties.MaxDamageValue,
properties.IconValue,
properties.NameValue ?? "");
if (numericId >= 0)
ConfigureToolMaterial(id, numericId, ToolKind.Shovel, material, properties);
}
else if (managedItem is HoeItem hoeItem)
{
ToolMaterialDefinition? material = ResolveToolMaterial(id, hoeItem);
numericId = NativeInterop.native_register_hoe_item(
id.ToString(),
(int)(material?.BaseTierValue ?? hoeItem.Tier),
properties.MaxDamageValue,
properties.IconValue,
properties.NameValue ?? "");
if (numericId >= 0)
ConfigureToolMaterial(id, numericId, ToolKind.Hoe, material, properties);
}
else if (managedItem is AxeItem axeItem)
{
ToolMaterialDefinition? material = ResolveToolMaterial(id, axeItem);
numericId = NativeInterop.native_register_axe_item(
id.ToString(),
(int)(material?.BaseTierValue ?? axeItem.Tier),
properties.MaxDamageValue,
properties.IconValue,
properties.NameValue ?? "");
if (numericId >= 0)
ConfigureToolMaterial(id, numericId, ToolKind.Axe, material, properties);
}
else if (managedItem is SwordItem swordItem)
{
ToolMaterialDefinition? material = ResolveToolMaterial(id, swordItem);
numericId = NativeInterop.native_register_sword_item(
id.ToString(),
(int)(material?.BaseTierValue ?? swordItem.Tier),
properties.MaxDamageValue,
properties.IconValue,
properties.NameValue ?? "");
if (numericId >= 0)
ConfigureToolMaterial(id, numericId, ToolKind.Sword, material, properties);
}
else
{

View File

@@ -0,0 +1,59 @@
namespace WeaveLoader.API.Item;
public sealed class PickaxeTierDefinition
{
private readonly ToolMaterialDefinition _inner = new();
internal ToolMaterialDefinition ToToolMaterialDefinition() => _inner;
public PickaxeTierDefinition BaseTier(ToolTier tier)
{
_inner.BaseTier(tier);
return this;
}
public PickaxeTierDefinition HarvestLevel(int harvestLevel)
{
if (harvestLevel < 0)
throw new ArgumentOutOfRangeException(nameof(harvestLevel));
_inner.HarvestLevel(harvestLevel);
return this;
}
public PickaxeTierDefinition DestroySpeed(float destroySpeed)
{
if (destroySpeed <= 0.0f)
throw new ArgumentOutOfRangeException(nameof(destroySpeed));
_inner.DestroySpeed(destroySpeed);
return this;
}
}
public sealed class RegisteredPickaxeTier
{
public Identifier StringId { get; }
internal RegisteredPickaxeTier(Identifier stringId)
{
StringId = stringId;
}
}
public static class PickaxeTierRegistry
{
public static RegisteredPickaxeTier Register(Identifier id, PickaxeTierDefinition definition)
{
ArgumentNullException.ThrowIfNull(definition);
ToolMaterialRegistry.Register(id, definition.ToToolMaterialDefinition());
return new RegisteredPickaxeTier(id);
}
internal static bool TryGetDefinition(Identifier id, out PickaxeTierDefinition? definition)
{
definition = null;
return false;
}
}

View File

@@ -0,0 +1,72 @@
namespace WeaveLoader.API.Item;
public sealed class ToolMaterialDefinition
{
public ToolTier BaseTierValue { get; private set; } = ToolTier.Diamond;
public int HarvestLevelValue { get; private set; } = 3;
public float DestroySpeedValue { get; private set; } = 8.0f;
public ToolMaterialDefinition BaseTier(ToolTier tier)
{
BaseTierValue = tier;
return this;
}
public ToolMaterialDefinition HarvestLevel(int harvestLevel)
{
if (harvestLevel < 0)
throw new ArgumentOutOfRangeException(nameof(harvestLevel));
HarvestLevelValue = harvestLevel;
return this;
}
public ToolMaterialDefinition DestroySpeed(float destroySpeed)
{
if (destroySpeed <= 0.0f)
throw new ArgumentOutOfRangeException(nameof(destroySpeed));
DestroySpeedValue = destroySpeed;
return this;
}
}
public sealed class RegisteredToolMaterial
{
public Identifier StringId { get; }
internal RegisteredToolMaterial(Identifier stringId)
{
StringId = stringId;
}
}
public static class ToolMaterialRegistry
{
private static readonly object s_lock = new();
private static readonly Dictionary<Identifier, ToolMaterialDefinition> s_materials = new();
public static RegisteredToolMaterial Register(Identifier id, ToolMaterialDefinition definition)
{
ArgumentNullException.ThrowIfNull(definition);
lock (s_lock)
{
if (s_materials.ContainsKey(id))
throw new InvalidOperationException($"Tool material '{id}' is already registered.");
s_materials[id] = definition;
}
return new RegisteredToolMaterial(id);
}
internal static bool TryGetDefinition(Identifier id, out ToolMaterialDefinition? definition)
{
lock (s_lock)
{
return s_materials.TryGetValue(id, out definition);
}
}
}

View File

@@ -20,7 +20,9 @@ internal static class NativeInterop
string iconName,
float lightEmission,
int lightBlock,
string displayName);
string displayName,
int requiredHarvestLevel,
int requiredTool);
[DllImport(RuntimeDll, CallingConvention = CallingConvention.Cdecl, CharSet = CharSet.Ansi)]
internal static extern int native_register_item(
@@ -38,6 +40,52 @@ internal static class NativeInterop
string iconName,
string displayName);
[DllImport(RuntimeDll, CallingConvention = CallingConvention.Cdecl, CharSet = CharSet.Ansi)]
internal static extern int native_register_shovel_item(
string namespacedId,
int tier,
int maxDamage,
string iconName,
string displayName);
[DllImport(RuntimeDll, CallingConvention = CallingConvention.Cdecl, CharSet = CharSet.Ansi)]
internal static extern int native_register_hoe_item(
string namespacedId,
int tier,
int maxDamage,
string iconName,
string displayName);
[DllImport(RuntimeDll, CallingConvention = CallingConvention.Cdecl, CharSet = CharSet.Ansi)]
internal static extern int native_register_axe_item(
string namespacedId,
int tier,
int maxDamage,
string iconName,
string displayName);
[DllImport(RuntimeDll, CallingConvention = CallingConvention.Cdecl, CharSet = CharSet.Ansi)]
internal static extern int native_register_sword_item(
string namespacedId,
int tier,
int maxDamage,
string iconName,
string displayName);
[DllImport(RuntimeDll, CallingConvention = CallingConvention.Cdecl)]
internal static extern int native_configure_custom_pickaxe_item(
int numericItemId,
int harvestLevel,
float destroySpeed);
[DllImport(RuntimeDll, CallingConvention = CallingConvention.Cdecl)]
internal static extern int native_configure_custom_tool_item(
int numericItemId,
int toolKind,
int harvestLevel,
float destroySpeed,
float attackDamage);
[DllImport(RuntimeDll, CallingConvention = CallingConvention.Cdecl, CharSet = CharSet.Ansi)]
internal static extern int native_allocate_description_id();

View File

@@ -27,6 +27,12 @@ public static class Registry
public static RegisteredItem Register(Identifier id, WeaveLoader.API.Item.Item item, ItemProperties properties)
=> ItemRegistry.Register(id, item, properties);
public static RegisteredToolMaterial RegisterToolMaterial(Identifier id, ToolMaterialDefinition definition)
=> ToolMaterialRegistry.Register(id, definition);
public static RegisteredPickaxeTier RegisterPickaxeTier(Identifier id, PickaxeTierDefinition definition)
=> PickaxeTierRegistry.Register(id, definition);
}
/// <summary>Entity registration. Call Register() with a namespaced ID and EntityDefinition.</summary>

View File

@@ -87,6 +87,9 @@ add_library(WeaveLoaderRuntime SHARED
src/DotNetHost.cpp
src/IdRegistry.cpp
src/NativeExports.cpp
src/CustomPickaxeRegistry.cpp
src/CustomToolMaterialRegistry.cpp
src/CustomBlockRegistry.cpp
src/CreativeInventory.cpp
src/FurnaceRecipeRegistry.cpp
src/MainMenuOverlay.cpp

View File

@@ -0,0 +1,26 @@
#include "CustomBlockRegistry.h"
#include <unordered_map>
namespace
{
std::unordered_map<int, CustomBlockRegistry::Definition> g_definitions;
}
namespace CustomBlockRegistry
{
void Register(int blockId, int requiredHarvestLevel, int requiredTool)
{
Definition def;
def.requiredHarvestLevel = requiredHarvestLevel;
def.requiredTool = static_cast<ToolType>(requiredTool);
g_definitions[blockId] = def;
}
const Definition* Find(int blockId)
{
const auto it = g_definitions.find(blockId);
if (it == g_definitions.end())
return nullptr;
return &it->second;
}
}

View File

@@ -0,0 +1,21 @@
#pragma once
namespace CustomBlockRegistry
{
enum class ToolType : int
{
None = 0,
Pickaxe = 1,
Axe = 2,
Shovel = 3,
};
struct Definition
{
int requiredHarvestLevel = -1;
ToolType requiredTool = ToolType::None;
};
void Register(int blockId, int requiredHarvestLevel, int requiredTool);
const Definition* Find(int blockId);
}

View File

@@ -0,0 +1,26 @@
#include "CustomPickaxeRegistry.h"
namespace
{
std::unordered_map<int, CustomPickaxeRegistry::Definition> g_definitions;
}
namespace CustomPickaxeRegistry
{
void Register(int itemId, int harvestLevel, float destroySpeed)
{
Definition definition;
definition.harvestLevel = harvestLevel;
definition.destroySpeed = destroySpeed;
g_definitions[itemId] = std::move(definition);
}
const Definition* Find(int itemId)
{
const auto it = g_definitions.find(itemId);
if (it == g_definitions.end())
return nullptr;
return &it->second;
}
}

View File

@@ -0,0 +1,14 @@
#pragma once
#include <unordered_map>
namespace CustomPickaxeRegistry
{
struct Definition
{
int harvestLevel = 0;
float destroySpeed = 1.0f;
};
void Register(int itemId, int harvestLevel, float destroySpeed);
const Definition* Find(int itemId);
}

View File

@@ -0,0 +1,27 @@
#include "CustomToolMaterialRegistry.h"
namespace
{
std::unordered_map<int, CustomToolMaterialRegistry::Definition> g_definitions;
}
namespace CustomToolMaterialRegistry
{
void Register(int itemId, ToolKind toolKind, int harvestLevel, float destroySpeed, float attackDamage)
{
Definition definition;
definition.toolKind = toolKind;
definition.harvestLevel = harvestLevel;
definition.destroySpeed = destroySpeed;
definition.attackDamage = attackDamage;
g_definitions[itemId] = definition;
}
const Definition* Find(int itemId)
{
const auto it = g_definitions.find(itemId);
if (it == g_definitions.end())
return nullptr;
return &it->second;
}
}

View File

@@ -0,0 +1,26 @@
#pragma once
#include <unordered_map>
namespace CustomToolMaterialRegistry
{
enum class ToolKind : int
{
Pickaxe = 1,
Shovel = 2,
Hoe = 3,
Sword = 4,
Axe = 5,
};
struct Definition
{
ToolKind toolKind = ToolKind::Pickaxe;
int harvestLevel = 0;
float destroySpeed = 1.0f;
float attackDamage = 0.0f;
};
void Register(int itemId, ToolKind toolKind, int harvestLevel, float destroySpeed, float attackDamage);
const Definition* Find(int itemId);
}

View File

@@ -4,6 +4,9 @@
#include "MainMenuOverlay.h"
#include "ModStrings.h"
#include "ModAtlas.h"
#include "CustomPickaxeRegistry.h"
#include "CustomToolMaterialRegistry.h"
#include "CustomBlockRegistry.h"
#include "LogUtil.h"
#include <Windows.h>
#include <string>
@@ -40,6 +43,11 @@ namespace GameHooks
ItemInstanceMineBlock_fn Original_ItemInstanceMineBlock = nullptr;
ItemMineBlock_fn Original_ItemMineBlock = nullptr;
ItemMineBlock_fn Original_DiggerItemMineBlock = nullptr;
PickaxeGetDestroySpeed_fn Original_PickaxeItemGetDestroySpeed = nullptr;
PickaxeCanDestroySpecial_fn Original_PickaxeItemCanDestroySpecial = nullptr;
PickaxeGetDestroySpeed_fn Original_ShovelItemGetDestroySpeed = nullptr;
PickaxeCanDestroySpecial_fn Original_ShovelItemCanDestroySpecial = nullptr;
PlayerCanDestroy_fn Original_PlayerCanDestroy = nullptr;
GameModeUseItem_fn Original_ServerPlayerGameModeUseItem = nullptr;
GameModeUseItem_fn Original_MultiPlayerGameModeUseItem = nullptr;
MinecraftSetLevel_fn Original_MinecraftSetLevel = nullptr;
@@ -66,6 +74,8 @@ namespace GameHooks
// Verified from compiled Player::inventory accesses in this game build.
static constexpr ptrdiff_t kPlayerInventoryOffset = 0x340;
static constexpr ptrdiff_t kLevelIsClientSideOffset = 0x268;
static constexpr ptrdiff_t kItemIdOffset = 0x20;
static constexpr ptrdiff_t kTileIdOffset = 0x28;
static constexpr ptrdiff_t kEntityXOffset = 0x78;
static constexpr ptrdiff_t kEntityYOffset = 0x80;
static constexpr ptrdiff_t kEntityZOffset = 0x88;
@@ -78,6 +88,7 @@ namespace GameHooks
static void* s_textureAtlasLocationItems = nullptr;
static int s_textureAtlasIdBlocks = -1;
static int s_textureAtlasIdItems = -1;
static void* s_tileTilesArray = nullptr;
static thread_local bool s_inLoadTextureByNameHook = false;
static thread_local bool s_hasForcedBillboardRoute = false;
static thread_local int s_forcedBillboardAtlas = -1;
@@ -758,6 +769,11 @@ namespace GameHooks
LogUtil::Log("[WeaveLoader] Atlas IDs: blocks=%d items=%d", s_textureAtlasIdBlocks, s_textureAtlasIdItems);
}
void SetTileTilesArray(void* tilesArray)
{
s_tileTilesArray = tilesArray;
}
static bool TryReadVec3(void* vecPtr, double& x, double& y, double& z)
{
if (!IsReadableRange(vecPtr, sizeof(double) * 3))
@@ -1207,6 +1223,232 @@ namespace GameHooks
return false;
}
static bool TryReadItemIdFromPickaxe(void* pickaxeItemPtr, int& outItemId)
{
if (!pickaxeItemPtr || !IsReadableRange(pickaxeItemPtr, kItemIdOffset + sizeof(int)))
return false;
int itemId = *reinterpret_cast<const int*>(static_cast<const char*>(pickaxeItemPtr) + kItemIdOffset);
if (itemId >= 0 && itemId < 32000)
{
outItemId = itemId;
return true;
}
return false;
}
static constexpr int TILE_NUM_COUNT = 4096;
static bool TryReadTileId(void* tilePtr, int& outTileId)
{
if (!tilePtr)
return false;
if (IsReadableRange(tilePtr, kTileIdOffset + sizeof(int)))
{
int id = *reinterpret_cast<const int*>(static_cast<const char*>(tilePtr) + kTileIdOffset);
if (id >= 0 && id < TILE_NUM_COUNT)
{
outTileId = id;
return true;
}
}
// Fallback: resolve via Tile::tiles (Tile** - pointer to array). Must dereference once.
if (s_tileTilesArray && IsReadableRange(s_tileTilesArray, sizeof(void*)))
{
const void* arrayPtr = *reinterpret_cast<const void* const*>(s_tileTilesArray);
if (arrayPtr && IsReadableRange(arrayPtr, TILE_NUM_COUNT * sizeof(void*)))
{
const void* const* tiles = reinterpret_cast<const void* const*>(arrayPtr);
for (int i = 0; i < TILE_NUM_COUNT; i++)
{
if (tiles[i] == tilePtr)
{
outTileId = i;
return true;
}
}
}
}
return false;
}
static int GetToolHarvestLevel(void* diggerItemPtr, int itemId)
{
const CustomToolMaterialRegistry::Definition* def = CustomToolMaterialRegistry::Find(itemId);
if (def)
return def->harvestLevel;
if (!diggerItemPtr || !IsReadableRange(diggerItemPtr, 0xA8 + sizeof(void*)))
return -1;
const void* tierPtr = *reinterpret_cast<const void* const*>(static_cast<const char*>(diggerItemPtr) + 0xA8);
if (!tierPtr || !IsReadableRange(tierPtr, sizeof(int)))
return -1;
return *reinterpret_cast<const int*>(tierPtr);
}
static float GetToolDestroySpeed(void* diggerItemPtr, int itemId)
{
const CustomToolMaterialRegistry::Definition* def = CustomToolMaterialRegistry::Find(itemId);
if (def)
return def->destroySpeed;
if (!diggerItemPtr || !IsReadableRange(static_cast<const char*>(diggerItemPtr) + 0xA0, sizeof(float)))
return 1.0f;
return *reinterpret_cast<const float*>(static_cast<const char*>(diggerItemPtr) + 0xA0);
}
float __fastcall Hooked_PickaxeItemGetDestroySpeed(void* thisPtr, void* itemInstanceSharedPtr, void* tilePtr)
{
void* itemInstancePtr = DecodeItemInstancePtrFromSharedArg(itemInstanceSharedPtr);
int itemId = 0;
if (!TryReadItemId(itemInstancePtr, itemId))
{
if (Original_PickaxeItemGetDestroySpeed)
return Original_PickaxeItemGetDestroySpeed(thisPtr, itemInstanceSharedPtr, tilePtr);
return 1.0f;
}
int tileId = 0;
if (tilePtr && TryReadTileId(tilePtr, tileId))
{
const CustomBlockRegistry::Definition* blockDef = CustomBlockRegistry::Find(tileId);
if (blockDef && blockDef->requiredTool == CustomBlockRegistry::ToolType::Pickaxe)
{
int harvestLevel = GetToolHarvestLevel(thisPtr, itemId);
if (harvestLevel >= 0 &&
(blockDef->requiredHarvestLevel < 0 || harvestLevel >= blockDef->requiredHarvestLevel))
{
return GetToolDestroySpeed(thisPtr, itemId);
}
// Block requires pickaxe but harvest level insufficient - return slow speed
return 1.0f;
}
}
if (Original_PickaxeItemGetDestroySpeed)
return Original_PickaxeItemGetDestroySpeed(thisPtr, itemInstanceSharedPtr, tilePtr);
return 1.0f;
}
bool __fastcall Hooked_PickaxeItemCanDestroySpecial(void* thisPtr, void* tilePtr)
{
int itemId = 0;
if (!TryReadItemIdFromPickaxe(thisPtr, itemId))
{
if (Original_PickaxeItemCanDestroySpecial)
return Original_PickaxeItemCanDestroySpecial(thisPtr, tilePtr);
return false;
}
int tileId = 0;
if (tilePtr && TryReadTileId(tilePtr, tileId))
{
const CustomBlockRegistry::Definition* blockDef = CustomBlockRegistry::Find(tileId);
if (blockDef && blockDef->requiredTool == CustomBlockRegistry::ToolType::Pickaxe)
{
int harvestLevel = GetToolHarvestLevel(thisPtr, itemId);
if (harvestLevel >= 0 &&
(blockDef->requiredHarvestLevel < 0 || harvestLevel >= blockDef->requiredHarvestLevel))
{
return true;
}
return false;
}
}
if (Original_PickaxeItemCanDestroySpecial)
return Original_PickaxeItemCanDestroySpecial(thisPtr, tilePtr);
return false;
}
float __fastcall Hooked_ShovelItemGetDestroySpeed(void* thisPtr, void* itemInstanceSharedPtr, void* tilePtr)
{
void* itemInstancePtr = DecodeItemInstancePtrFromSharedArg(itemInstanceSharedPtr);
int itemId = 0;
if (!TryReadItemId(itemInstancePtr, itemId))
{
if (Original_ShovelItemGetDestroySpeed)
return Original_ShovelItemGetDestroySpeed(thisPtr, itemInstanceSharedPtr, tilePtr);
return 1.0f;
}
int tileId = 0;
if (tilePtr && TryReadTileId(tilePtr, tileId))
{
const CustomBlockRegistry::Definition* blockDef = CustomBlockRegistry::Find(tileId);
if (blockDef && blockDef->requiredTool == CustomBlockRegistry::ToolType::Shovel)
{
int harvestLevel = GetToolHarvestLevel(thisPtr, itemId);
if (harvestLevel >= 0 &&
(blockDef->requiredHarvestLevel < 0 || harvestLevel >= blockDef->requiredHarvestLevel))
{
return GetToolDestroySpeed(thisPtr, itemId);
}
return 1.0f;
}
}
if (Original_ShovelItemGetDestroySpeed)
return Original_ShovelItemGetDestroySpeed(thisPtr, itemInstanceSharedPtr, tilePtr);
return 1.0f;
}
bool __fastcall Hooked_ShovelItemCanDestroySpecial(void* thisPtr, void* tilePtr)
{
int itemId = 0;
if (!TryReadItemIdFromPickaxe(thisPtr, itemId))
{
if (Original_ShovelItemCanDestroySpecial)
return Original_ShovelItemCanDestroySpecial(thisPtr, tilePtr);
return false;
}
int tileId = 0;
if (tilePtr && TryReadTileId(tilePtr, tileId))
{
const CustomBlockRegistry::Definition* blockDef = CustomBlockRegistry::Find(tileId);
if (blockDef && blockDef->requiredTool == CustomBlockRegistry::ToolType::Shovel)
{
int harvestLevel = GetToolHarvestLevel(thisPtr, itemId);
if (harvestLevel >= 0 &&
(blockDef->requiredHarvestLevel < 0 || harvestLevel >= blockDef->requiredHarvestLevel))
{
return true;
}
return false;
}
}
if (Original_ShovelItemCanDestroySpecial)
return Original_ShovelItemCanDestroySpecial(thisPtr, tilePtr);
return false;
}
// Inventory layout: items.data at +0x8, selected at +0x28. shared_ptr is 16 bytes.
static void* GetSelectedItemInstanceFromPlayer(void* playerPtr)
{
void* inv = FindInventoryPtrFromPlayer(playerPtr);
if (!inv || !IsReadableRange(inv, 0x30))
return nullptr;
void* itemsData = *reinterpret_cast<void* const*>(static_cast<const char*>(inv) + 0x8);
int selected = *reinterpret_cast<const int*>(static_cast<const char*>(inv) + 0x28);
if (!itemsData || selected < 0 || selected >= 36)
return nullptr;
// shared_ptr<ItemInstance> at itemsData[selected]; raw ptr is first 8 bytes
const char* slotPtr = static_cast<const char*>(itemsData) + selected * 16;
if (!IsReadableRange(slotPtr, 8))
return nullptr;
return *reinterpret_cast<void* const*>(slotPtr);
}
bool __fastcall Hooked_PlayerCanDestroy(void* thisPtr, void* tilePtr)
{
// For pickaxe harvest rules, Inventory::canDestroy -> ItemInstance::canDestroySpecial
// already gives the correct source behavior:
// proper tool/tier => normal speed + drops
// insufficient tool/tier => slow break + no drops
if (Original_PlayerCanDestroy)
return Original_PlayerCanDestroy(thisPtr, tilePtr);
return false;
}
bool __fastcall Hooked_ServerPlayerGameModeUseItem(void* thisPtr, void* playerSharedPtr, void* level, void* itemInstanceSharedPtr, bool bTestUseOnly)
{
void* previousLevel = s_activeUseLevel;

View File

@@ -25,6 +25,9 @@ typedef void (__fastcall *AnimatedTextureCycleFrames_fn)(void* thisPtr);
typedef int (__fastcall *TextureGetSourceDim_fn)(void* thisPtr);
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);
typedef float (__fastcall *PickaxeGetDestroySpeed_fn)(void* thisPtr, void* itemInstanceSharedPtr, void* tilePtr);
typedef bool (__fastcall *PickaxeCanDestroySpecial_fn)(void* thisPtr, void* tilePtr);
typedef bool (__fastcall *PlayerCanDestroy_fn)(void* thisPtr, void* tilePtr);
typedef bool (__fastcall *GameModeUseItem_fn)(void* thisPtr, void* playerSharedPtr, void* level, void* itemInstanceSharedPtr, bool bTestUseOnly);
typedef void (__fastcall *MinecraftSetLevel_fn)(void* thisPtr, void* level, int message, void* forceInsertPlayerSharedPtr, bool doForceStatsSave, bool bPrimaryPlayerSignedOut);
typedef bool (__fastcall *LevelAddEntity_fn)(void* thisPtr, void* entitySharedPtr);
@@ -68,6 +71,11 @@ namespace GameHooks
extern ItemInstanceMineBlock_fn Original_ItemInstanceMineBlock;
extern ItemMineBlock_fn Original_ItemMineBlock;
extern ItemMineBlock_fn Original_DiggerItemMineBlock;
extern PickaxeGetDestroySpeed_fn Original_PickaxeItemGetDestroySpeed;
extern PickaxeCanDestroySpecial_fn Original_PickaxeItemCanDestroySpecial;
extern PickaxeGetDestroySpeed_fn Original_ShovelItemGetDestroySpeed;
extern PickaxeCanDestroySpecial_fn Original_ShovelItemCanDestroySpecial;
extern PlayerCanDestroy_fn Original_PlayerCanDestroy;
extern GameModeUseItem_fn Original_ServerPlayerGameModeUseItem;
extern GameModeUseItem_fn Original_MultiPlayerGameModeUseItem;
extern MinecraftSetLevel_fn Original_MinecraftSetLevel;
@@ -103,6 +111,11 @@ namespace GameHooks
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);
float __fastcall Hooked_PickaxeItemGetDestroySpeed(void* thisPtr, void* itemInstanceSharedPtr, void* tilePtr);
bool __fastcall Hooked_PickaxeItemCanDestroySpecial(void* thisPtr, void* tilePtr);
float __fastcall Hooked_ShovelItemGetDestroySpeed(void* thisPtr, void* itemInstanceSharedPtr, void* tilePtr);
bool __fastcall Hooked_ShovelItemCanDestroySpecial(void* thisPtr, void* tilePtr);
bool __fastcall Hooked_PlayerCanDestroy(void* thisPtr, void* tilePtr);
bool __fastcall Hooked_ServerPlayerGameModeUseItem(void* thisPtr, void* playerSharedPtr, void* level, void* itemInstanceSharedPtr, bool bTestUseOnly);
bool __fastcall Hooked_MultiPlayerGameModeUseItem(void* thisPtr, void* playerSharedPtr, void* level, void* itemInstanceSharedPtr, bool bTestUseOnly);
void __fastcall Hooked_MinecraftSetLevel(void* thisPtr, void* level, int message, void* forceInsertPlayerSharedPtr, bool doForceStatsSave, bool bPrimaryPlayerSignedOut);
@@ -114,6 +127,7 @@ namespace GameHooks
float __fastcall Hooked_StitchedGetV0(void* thisPtr, bool adjust);
float __fastcall Hooked_StitchedGetV1(void* thisPtr, bool adjust);
void SetAtlasLocationPointers(void* blocksLocation, void* itemsLocation);
void SetTileTilesArray(void* tilesArray);
void SetSummonSymbols(void* levelAddEntity,
void* entityIoNewById,
void* entityMoveTo,

View File

@@ -5,6 +5,7 @@
#include <Windows.h>
#include <cstring>
#include <string>
#include <unordered_map>
// Tile::Tile(int id, Material* material, bool isSolidRender) — protected ctor
typedef void (__fastcall *TileCtor_fn)(void* thisPtr, int id, void* material, bool isSolidRender);
@@ -24,6 +25,10 @@ typedef void (__fastcall *TileItemCtor_fn)(void* thisPtr, int id);
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);
typedef void (__fastcall *ShovelCtor_fn)(void* thisPtr, int id, const void* tier);
typedef void (__fastcall *HoeCtor_fn)(void* thisPtr, int id, const void* tier);
typedef void (__fastcall *HatchetCtor_fn)(void* thisPtr, int id, const void* tier);
typedef void (__fastcall *WeaponCtor_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
@@ -40,6 +45,10 @@ static TileItemCtor_fn fnTileItemCtor = nullptr;
static ItemCtor_fn fnItemCtor = nullptr;
static PickaxeCtor_fn fnPickaxeCtor = nullptr;
static ShovelCtor_fn fnShovelCtor = nullptr;
static HoeCtor_fn fnHoeCtor = nullptr;
static HatchetCtor_fn fnHatchetCtor = nullptr;
static WeaponCtor_fn fnWeaponCtor = nullptr;
static ItemSetIconName_fn fnItemSetIconName= nullptr;
static int s_itemDescIdOffset = -1; // offset of descriptionId field in Item, extracted from getDescriptionId
@@ -54,6 +63,20 @@ static const int ITEM_ALLOC_SIZE = 1024;
static const int TILEITEM_ALLOC_SIZE = 1024;
static bool s_resolved = false;
static std::unordered_map<int, void*> s_createdItems;
static int MapTierMaterial(int tier)
{
switch (tier)
{
case 0: return 1; // wood
case 1: return 2; // stone
case 2: return 3; // iron
case 3: return 5; // diamond
case 4: return 4; // gold
default: return 5;
}
}
static void* GetMaterial(int idx)
{
@@ -101,6 +124,10 @@ 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");
fnShovelCtor = (ShovelCtor_fn)resolver.Resolve("??0ShovelItem@@QEAA@HPEBVTier@Item@@@Z");
fnHoeCtor = (HoeCtor_fn)resolver.Resolve("??0HoeItem@@QEAA@HPEBVTier@Item@@@Z");
fnHatchetCtor = (HatchetCtor_fn)resolver.Resolve("??0HatchetItem@@QEAA@HPEBVTier@Item@@@Z");
fnWeaponCtor = (WeaponCtor_fn)resolver.Resolve("??0WeaponItem@@QEAA@HPEBVTier@Item@@@Z");
// Item::setIconName
fnItemSetIconName = (ItemSetIconName_fn)resolver.Resolve(
@@ -190,6 +217,10 @@ bool ResolveSymbols(SymbolResolver& resolver)
logSym("TileItem::TileItem", (void*)fnTileItemCtor);
logSym("Item::Item", (void*)fnItemCtor);
logSym("PickaxeItem::PickaxeItem", (void*)fnPickaxeCtor);
logSym("ShovelItem::ShovelItem", (void*)fnShovelCtor);
logSym("HoeItem::HoeItem", (void*)fnHoeCtor);
logSym("HatchetItem::HatchetItem", (void*)fnHatchetCtor);
logSym("WeaponItem::WeaponItem", (void*)fnWeaponCtor);
logSym("Item::setIconName", (void*)fnItemSetIconName);
logSym("Material::stone addr", (void*)s_materialAddrs[1]);
logSym("SOUND_STONE addr", (void*)s_soundAddrs[1]);
@@ -320,6 +351,7 @@ bool CreateItem(int itemId, int maxStackSize, int maxDamage, const wchar_t* icon
itemId, ctorParam, maxStackSize, maxDamage,
iconName ? iconName : L"<none>", descriptionId);
s_createdItems[itemId] = item;
return true;
}
@@ -346,17 +378,7 @@ bool CreatePickaxeItem(int itemId, int tier, int maxDamage, const wchar_t* iconN
// 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;
*reinterpret_cast<int*>(static_cast<char*>(item) + 0x3C) = MapTierMaterial(tier);
// Tools should always stack to 1.
*reinterpret_cast<int*>(static_cast<char*>(item) + 0x24) = 1;
@@ -381,7 +403,89 @@ bool CreatePickaxeItem(int itemId, int tier, int maxDamage, const wchar_t* iconN
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);
s_createdItems[itemId] = item;
return true;
}
static bool CreateTieredItem(
const char* typeName,
void* ctorRaw,
int baseType,
int itemId,
int tier,
int maxDamage,
const wchar_t* iconName,
int descriptionId)
{
if (!s_resolved || !ctorRaw)
{
LogUtil::Log("[WeaveLoader] %s: symbols not resolved", typeName);
return false;
}
const void* tierPtr = GetTier(tier);
if (!tierPtr)
{
LogUtil::Log("[WeaveLoader] %s: invalid tier %d", typeName, tier);
return false;
}
int ctorParam = itemId - 256;
void* item = ::operator new(ITEM_ALLOC_SIZE);
memset(item, 0, ITEM_ALLOC_SIZE);
reinterpret_cast<PickaxeCtor_fn>(ctorRaw)(item, ctorParam, tierPtr);
*reinterpret_cast<int*>(static_cast<char*>(item) + 0x38) = baseType;
*reinterpret_cast<int*>(static_cast<char*>(item) + 0x3C) = MapTierMaterial(tier);
*reinterpret_cast<int*>(static_cast<char*>(item) + 0x24) = 1;
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 %s id=%d (ctorParam=%d, tier=%d, damage=%d, icon=%ls, descId=%d)",
typeName, itemId, ctorParam, tier, maxDamage, iconName ? iconName : L"<none>", descriptionId);
s_createdItems[itemId] = item;
return true;
}
bool CreateShovelItem(int itemId, int tier, int maxDamage, const wchar_t* iconName, int descriptionId)
{
return CreateTieredItem("ShovelItem", fnShovelCtor, 2, itemId, tier, maxDamage, iconName, descriptionId);
}
bool CreateHoeItem(int itemId, int tier, int maxDamage, const wchar_t* iconName, int descriptionId)
{
return CreateTieredItem("HoeItem", fnHoeCtor, 5, itemId, tier, maxDamage, iconName, descriptionId);
}
bool CreateAxeItem(int itemId, int tier, int maxDamage, const wchar_t* iconName, int descriptionId)
{
return CreateTieredItem("HatchetItem", fnHatchetCtor, 4, itemId, tier, maxDamage, iconName, descriptionId);
}
bool CreateSwordItem(int itemId, int tier, int maxDamage, const wchar_t* iconName, int descriptionId)
{
return CreateTieredItem("WeaponItem", fnWeaponCtor, 1, itemId, tier, maxDamage, iconName, descriptionId);
}
void* FindItem(int itemId)
{
const auto it = s_createdItems.find(itemId);
if (it == s_createdItems.end())
return nullptr;
return it->second;
}
} // namespace GameObjectFactory

View File

@@ -26,4 +26,13 @@ namespace GameObjectFactory
// maxDamage: if > 0, overrides the tier default durability.
bool CreatePickaxeItem(int itemId, int tier, int maxDamage,
const wchar_t* iconName, int descriptionId = -1);
bool CreateShovelItem(int itemId, int tier, int maxDamage,
const wchar_t* iconName, int descriptionId = -1);
bool CreateHoeItem(int itemId, int tier, int maxDamage,
const wchar_t* iconName, int descriptionId = -1);
bool CreateAxeItem(int itemId, int tier, int maxDamage,
const wchar_t* iconName, int descriptionId = -1);
bool CreateSwordItem(int itemId, int tier, int maxDamage,
const wchar_t* iconName, int descriptionId = -1);
void* FindItem(int itemId);
}

View File

@@ -236,6 +236,76 @@ bool HookManager::Install(const SymbolResolver& symbols)
}
}
if (symbols.pPickaxeItemGetDestroySpeed)
{
if (MH_CreateHook(symbols.pPickaxeItemGetDestroySpeed,
reinterpret_cast<void*>(&GameHooks::Hooked_PickaxeItemGetDestroySpeed),
reinterpret_cast<void**>(&GameHooks::Original_PickaxeItemGetDestroySpeed)) != MH_OK)
{
LogUtil::Log("[WeaveLoader] Warning: Failed to hook PickaxeItem::getDestroySpeed");
}
else
{
LogUtil::Log("[WeaveLoader] Hooked PickaxeItem::getDestroySpeed (custom pickaxe tiers)");
}
}
if (symbols.pPickaxeItemCanDestroySpecial)
{
if (MH_CreateHook(symbols.pPickaxeItemCanDestroySpecial,
reinterpret_cast<void*>(&GameHooks::Hooked_PickaxeItemCanDestroySpecial),
reinterpret_cast<void**>(&GameHooks::Original_PickaxeItemCanDestroySpecial)) != MH_OK)
{
LogUtil::Log("[WeaveLoader] Warning: Failed to hook PickaxeItem::canDestroySpecial");
}
else
{
LogUtil::Log("[WeaveLoader] Hooked PickaxeItem::canDestroySpecial (custom pickaxe tiers)");
}
}
if (symbols.pShovelItemGetDestroySpeed)
{
if (MH_CreateHook(symbols.pShovelItemGetDestroySpeed,
reinterpret_cast<void*>(&GameHooks::Hooked_ShovelItemGetDestroySpeed),
reinterpret_cast<void**>(&GameHooks::Original_ShovelItemGetDestroySpeed)) != MH_OK)
{
LogUtil::Log("[WeaveLoader] Warning: Failed to hook ShovelItem::getDestroySpeed");
}
else
{
LogUtil::Log("[WeaveLoader] Hooked ShovelItem::getDestroySpeed (custom tool materials)");
}
}
if (symbols.pShovelItemCanDestroySpecial)
{
if (MH_CreateHook(symbols.pShovelItemCanDestroySpecial,
reinterpret_cast<void*>(&GameHooks::Hooked_ShovelItemCanDestroySpecial),
reinterpret_cast<void**>(&GameHooks::Original_ShovelItemCanDestroySpecial)) != MH_OK)
{
LogUtil::Log("[WeaveLoader] Warning: Failed to hook ShovelItem::canDestroySpecial");
}
else
{
LogUtil::Log("[WeaveLoader] Hooked ShovelItem::canDestroySpecial (custom tool materials)");
}
}
if (symbols.pPlayerCanDestroy)
{
if (MH_CreateHook(symbols.pPlayerCanDestroy,
reinterpret_cast<void*>(&GameHooks::Hooked_PlayerCanDestroy),
reinterpret_cast<void**>(&GameHooks::Original_PlayerCanDestroy)) != MH_OK)
{
LogUtil::Log("[WeaveLoader] Warning: Failed to hook Player::canDestroy");
}
else
{
LogUtil::Log("[WeaveLoader] Hooked Player::canDestroy (block harvest enforcement)");
}
}
if (symbols.pServerPlayerGameModeUseItem)
{
if (MH_CreateHook(symbols.pServerPlayerGameModeUseItem,
@@ -265,6 +335,7 @@ bool HookManager::Install(const SymbolResolver& symbols)
}
GameHooks::SetAtlasLocationPointers(symbols.pTextureAtlasLocationBlocks, symbols.pTextureAtlasLocationItems);
GameHooks::SetTileTilesArray(symbols.pTileTiles);
if (symbols.pTexturesBindTextureResource)
{

View File

@@ -4,6 +4,9 @@
#include "GameObjectFactory.h"
#include "FurnaceRecipeRegistry.h"
#include "GameHooks.h"
#include "CustomPickaxeRegistry.h"
#include "CustomToolMaterialRegistry.h"
#include "CustomBlockRegistry.h"
#include "ModStrings.h"
#include "LogUtil.h"
#include <Windows.h>
@@ -45,7 +48,9 @@ int native_register_block(
const char* iconName,
float lightEmission,
int lightBlock,
const char* displayName)
const char* displayName,
int requiredHarvestLevel,
int requiredTool)
{
if (!namespacedId) return -1;
@@ -75,6 +80,11 @@ int native_register_block(
LogUtil::Log("[WeaveLoader] Warning: failed to create game Tile for block '%s' id=%d", namespacedId, id);
}
if (requiredHarvestLevel >= 0 || requiredTool != 0)
{
CustomBlockRegistry::Register(id, requiredHarvestLevel, requiredTool);
}
return id;
}
@@ -154,6 +164,171 @@ int native_register_pickaxe_item(
return id;
}
int native_register_shovel_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) return -1;
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::CreateShovelItem(id, tier, maxDamage, wIcon.empty() ? nullptr : wIcon.c_str(), descId))
LogUtil::Log("[WeaveLoader] Warning: failed to create native ShovelItem for '%s' id=%d", namespacedId, id);
return id;
}
int native_register_hoe_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) return -1;
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::CreateHoeItem(id, tier, maxDamage, wIcon.empty() ? nullptr : wIcon.c_str(), descId))
LogUtil::Log("[WeaveLoader] Warning: failed to create native HoeItem for '%s' id=%d", namespacedId, id);
return id;
}
int native_register_axe_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) return -1;
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::CreateAxeItem(id, tier, maxDamage, wIcon.empty() ? nullptr : wIcon.c_str(), descId))
LogUtil::Log("[WeaveLoader] Warning: failed to create native HatchetItem for '%s' id=%d", namespacedId, id);
return id;
}
int native_register_sword_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) return -1;
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::CreateSwordItem(id, tier, maxDamage, wIcon.empty() ? nullptr : wIcon.c_str(), descId))
LogUtil::Log("[WeaveLoader] Warning: failed to create native WeaponItem for '%s' id=%d", namespacedId, id);
return id;
}
int native_configure_custom_pickaxe_item(
int numericItemId,
int harvestLevel,
float destroySpeed)
{
return native_configure_custom_tool_item(
numericItemId,
static_cast<int>(CustomToolMaterialRegistry::ToolKind::Pickaxe),
harvestLevel,
destroySpeed,
0.0f);
}
int native_configure_custom_tool_item(
int numericItemId,
int toolKind,
int harvestLevel,
float destroySpeed,
float attackDamage)
{
if (numericItemId < 0 || harvestLevel < 0 || destroySpeed <= 0.0f)
return 0;
const auto kind = static_cast<CustomToolMaterialRegistry::ToolKind>(toolKind);
CustomToolMaterialRegistry::Register(numericItemId, kind, harvestLevel, destroySpeed, attackDamage);
if (kind == CustomToolMaterialRegistry::ToolKind::Pickaxe)
{
CustomPickaxeRegistry::Register(numericItemId, harvestLevel, destroySpeed);
}
void* itemPtr = GameObjectFactory::FindItem(numericItemId);
if (itemPtr)
{
switch (kind)
{
case CustomToolMaterialRegistry::ToolKind::Pickaxe:
case CustomToolMaterialRegistry::ToolKind::Shovel:
case CustomToolMaterialRegistry::ToolKind::Axe:
if (destroySpeed > 0.0f)
*reinterpret_cast<float*>(static_cast<char*>(itemPtr) + 0xA0) = destroySpeed;
if (attackDamage > 0.0f)
*reinterpret_cast<float*>(static_cast<char*>(itemPtr) + 0xA4) = attackDamage;
break;
case CustomToolMaterialRegistry::ToolKind::Sword:
if (attackDamage > 0.0f)
*reinterpret_cast<float*>(static_cast<char*>(itemPtr) + 0x98) = attackDamage;
break;
case CustomToolMaterialRegistry::ToolKind::Hoe:
break;
}
}
LogUtil::Log("[WeaveLoader] Configured custom tool item id=%d (kind=%d harvest=%d speed=%.2f attack=%.2f)",
numericItemId, toolKind, harvestLevel, destroySpeed, attackDamage);
return 1;
}
int native_allocate_description_id()
{
return ModStrings::AllocateId();

View File

@@ -14,7 +14,9 @@ extern "C"
const char* iconName,
float lightEmission,
int lightBlock,
const char* displayName);
const char* displayName,
int requiredHarvestLevel,
int requiredTool);
__declspec(dllexport) int native_register_item(
const char* namespacedId,
@@ -29,6 +31,41 @@ extern "C"
int maxDamage,
const char* iconName,
const char* displayName);
__declspec(dllexport) int native_register_shovel_item(
const char* namespacedId,
int tier,
int maxDamage,
const char* iconName,
const char* displayName);
__declspec(dllexport) int native_register_hoe_item(
const char* namespacedId,
int tier,
int maxDamage,
const char* iconName,
const char* displayName);
__declspec(dllexport) int native_register_axe_item(
const char* namespacedId,
int tier,
int maxDamage,
const char* iconName,
const char* displayName);
__declspec(dllexport) int native_register_sword_item(
const char* namespacedId,
int tier,
int maxDamage,
const char* iconName,
const char* displayName);
__declspec(dllexport) int native_configure_custom_pickaxe_item(
int numericItemId,
int harvestLevel,
float destroySpeed);
__declspec(dllexport) int native_configure_custom_tool_item(
int numericItemId,
int toolKind,
int harvestLevel,
float destroySpeed,
float attackDamage);
__declspec(dllexport) int native_allocate_description_id();
__declspec(dllexport) void native_register_string(int descriptionId, const char* displayName);

View File

@@ -30,6 +30,11 @@ static const char* SYM_CLOCK_GETSOURCEHEIGHT = "?getSourceHeight@ClockTexture@@U
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";
static const char* SYM_PICKAXEITEM_GETDESTROYSPEED = "?getDestroySpeed@PickaxeItem@@UEAAMV?$shared_ptr@VItemInstance@@@std@@PEAVTile@@@Z";
static const char* SYM_PICKAXEITEM_CANDESTROYSPECIAL = "?canDestroySpecial@PickaxeItem@@UEAA_NPEAVTile@@@Z";
static const char* SYM_SHOVELITEM_GETDESTROYSPEED = "?getDestroySpeed@ShovelItem@@UEAAMV?$shared_ptr@VItemInstance@@@std@@PEAVTile@@@Z";
static const char* SYM_SHOVELITEM_CANDESTROYSPECIAL = "?canDestroySpecial@ShovelItem@@UEAA_NPEAVTile@@@Z";
static const char* SYM_PLAYER_CANDESTROY = "?canDestroy@Player@@QEAA_NPEAVTile@@@Z";
static const char* SYM_SERVER_PLAYER_GAMEMODE_USEITEM = "?useItem@ServerPlayerGameMode@@QEAA_NV?$shared_ptr@VPlayer@@@std@@PEAVLevel@@V?$shared_ptr@VItemInstance@@@3@_N@Z";
static const char* SYM_MULTI_PLAYER_GAMEMODE_USEITEM = "?useItem@MultiPlayerGameMode@@UEAA_NV?$shared_ptr@VPlayer@@@std@@PEAVLevel@@V?$shared_ptr@VItemInstance@@@3@_N@Z";
static const char* SYM_TEXTURES_BIND_RESOURCE = "?bindTexture@Textures@@QEAAXPEAVResourceLocation@@@Z";
@@ -55,6 +60,7 @@ static const char* SYM_ITEMINSTANCE_HURTANDBREAK = "?hurtAndBreak@ItemInstance@@
static const char* SYM_ABSTRACTCONTAINERMENU_BROADCASTCHANGES = "?broadcastChanges@AbstractContainerMenu@@UEAAXXZ";
static const char* SYM_TEXATLAS_BLOCKS = "?LOCATION_BLOCKS@TextureAtlas@@2VResourceLocation@@A";
static const char* SYM_TEXATLAS_ITEMS = "?LOCATION_ITEMS@TextureAtlas@@2VResourceLocation@@A";
static const char* SYM_TILE_TILES = "?tiles@Tile@@2PEAPEAV1@EA";
bool SymbolResolver::Initialize()
{
@@ -137,6 +143,11 @@ bool SymbolResolver::ResolveGameFunctions()
pItemInstanceMineBlock = Resolve(SYM_ITEMINSTANCE_MINEBLOCK);
pItemMineBlock = Resolve(SYM_ITEM_MINEBLOCK);
pDiggerItemMineBlock = Resolve(SYM_DIGGERITEM_MINEBLOCK);
pPickaxeItemGetDestroySpeed = Resolve(SYM_PICKAXEITEM_GETDESTROYSPEED);
pPickaxeItemCanDestroySpecial = Resolve(SYM_PICKAXEITEM_CANDESTROYSPECIAL);
pShovelItemGetDestroySpeed = Resolve(SYM_SHOVELITEM_GETDESTROYSPEED);
pShovelItemCanDestroySpecial = Resolve(SYM_SHOVELITEM_CANDESTROYSPECIAL);
pPlayerCanDestroy = Resolve(SYM_PLAYER_CANDESTROY);
pServerPlayerGameModeUseItem = Resolve(SYM_SERVER_PLAYER_GAMEMODE_USEITEM);
pMultiPlayerGameModeUseItem = Resolve(SYM_MULTI_PLAYER_GAMEMODE_USEITEM);
pTexturesBindTextureResource = Resolve(SYM_TEXTURES_BIND_RESOURCE);
@@ -164,6 +175,7 @@ bool SymbolResolver::ResolveGameFunctions()
pAbstractContainerMenuBroadcastChanges = Resolve(SYM_ABSTRACTCONTAINERMENU_BROADCASTCHANGES);
pTextureAtlasLocationBlocks = Resolve(SYM_TEXATLAS_BLOCKS);
pTextureAtlasLocationItems = Resolve(SYM_TEXATLAS_ITEMS);
pTileTiles = Resolve(SYM_TILE_TILES);
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);
@@ -202,6 +214,11 @@ bool SymbolResolver::ResolveGameFunctions()
logSym("ItemInstance::mineBlock", pItemInstanceMineBlock);
logSym("Item::mineBlock", pItemMineBlock);
logSym("DiggerItem::mineBlock", pDiggerItemMineBlock);
logSym("PickaxeItem::getDestroySpeed", pPickaxeItemGetDestroySpeed);
logSym("PickaxeItem::canDestroySpecial", pPickaxeItemCanDestroySpecial);
logSym("ShovelItem::getDestroySpeed", pShovelItemGetDestroySpeed);
logSym("ShovelItem::canDestroySpecial", pShovelItemCanDestroySpecial);
logSym("Player::canDestroy", pPlayerCanDestroy);
logSym("ServerPlayerGameMode::useItem", pServerPlayerGameModeUseItem);
logSym("MultiPlayerGameMode::useItem", pMultiPlayerGameModeUseItem);
logSym("Textures::bindTexture(ResourceLocation)", pTexturesBindTextureResource);
@@ -225,6 +242,7 @@ bool SymbolResolver::ResolveGameFunctions()
logSym("AbstractContainerMenu::broadcastChanges", pAbstractContainerMenuBroadcastChanges);
logSym("TextureAtlas::LOCATION_BLOCKS", pTextureAtlasLocationBlocks);
logSym("TextureAtlas::LOCATION_ITEMS", pTextureAtlasLocationItems);
logSym("Tile::tiles", pTileTiles);
bool ok = pRunStaticCtors && pMinecraftTick && pMinecraftInit;
if (ok)

View File

@@ -35,6 +35,11 @@ public:
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>)
void* pPickaxeItemGetDestroySpeed = nullptr; // PickaxeItem::getDestroySpeed(shared_ptr<ItemInstance>,Tile*)
void* pPickaxeItemCanDestroySpecial = nullptr; // PickaxeItem::canDestroySpecial(Tile*)
void* pShovelItemGetDestroySpeed = nullptr; // ShovelItem::getDestroySpeed(shared_ptr<ItemInstance>,Tile*)
void* pShovelItemCanDestroySpecial = nullptr; // ShovelItem::canDestroySpecial(Tile*)
void* pPlayerCanDestroy = nullptr; // Player::canDestroy(Tile*)
void* pServerPlayerGameModeUseItem = nullptr; // ServerPlayerGameMode::useItem(shared_ptr<Player>,Level*,shared_ptr<ItemInstance>,bool)
void* pMultiPlayerGameModeUseItem = nullptr; // MultiPlayerGameMode::useItem(shared_ptr<Player>,Level*,shared_ptr<ItemInstance>,bool)
void* pTexturesBindTextureResource = nullptr; // Textures::bindTexture(ResourceLocation*)
@@ -58,6 +63,7 @@ public:
void* pAbstractContainerMenuBroadcastChanges = nullptr; // AbstractContainerMenu::broadcastChanges()
void* pTextureAtlasLocationBlocks = nullptr; // TextureAtlas::LOCATION_BLOCKS
void* pTextureAtlasLocationItems = nullptr; // TextureAtlas::LOCATION_ITEMS
void* pTileTiles = nullptr; // Tile::tiles (Tile*[]) for tile id lookup
private:
uintptr_t m_moduleBase = 0;