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

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