Minecraft-Console-Client/MinecraftClient/Mapping/MiningCalculator.cs
copilot-swe-agent[bot] 22455905a7 Add automatic mining-speed and dig-duration handling
- Add BlockHardness.cs with hardness data for all 1053 blocks from MC 1.21.11
- Add MiningCalculator.cs with version-aware dig duration computation
  - Tool speed from ToolComponent (1.20.6+) or legacy hardcoded tables
  - Efficiency enchantment (legacy: level^2+1, 1.21.11+: mining_efficiency attribute)
  - Haste/Conduit Power/Mining Fatigue effects
  - BLOCK_BREAK_SPEED attribute (1.20.6+)
  - MINING_EFFICIENCY and SUBMERGED_MINING_SPEED attributes (1.21.11+)
  - Underwater penalty with Aqua Affinity support (legacy) or attribute (modern)
  - Airborne penalty
  - Correct tool for drops check (30 vs 100 divisor)
- Modify McClient.DigBlock to auto-compute duration for survival/adventure mode
- Cache player attributes from OnEntityProperties in McClient
- Expose duration parameter in ChatBot.cs scripting wrapper
- Add /downloads/ to .gitignore

Agent-Logs-Url: https://github.com/MCCTeam/Minecraft-Console-Client/sessions/579df446-a335-4174-9a8b-4d66173c82b1

Co-authored-by: milutinke <441903+milutinke@users.noreply.github.com>
2026-03-30 08:24:00 +00:00

493 lines
20 KiB
C#

using System;
using System.Collections.Generic;
using System.Linq;
using MinecraftClient.Inventory;
using MinecraftClient.Protocol.Handlers;
using MinecraftClient.Protocol.Handlers.StructuredComponents.Components._1_20_6;
namespace MinecraftClient.Mapping
{
/// <summary>
/// Computes dig duration in ticks for survival-style block breaking.
/// Version-aware across 1.8-1.21.11+, using tool speed, enchantments, effects, and attributes.
/// </summary>
public static class MiningCalculator
{
/// <summary>
/// Compute the number of ticks required to break a block in survival mode.
/// Returns 0 for instant-break blocks, -1 for unbreakable blocks.
/// </summary>
/// <param name="blockMaterial">The block material to break</param>
/// <param name="heldItem">The item in the player's main hand (null for empty hand)</param>
/// <param name="helmetItem">The item in the player's helmet slot (null if empty, used for Aqua Affinity)</param>
/// <param name="effects">Currently active player effects</param>
/// <param name="playerAttributes">Cached player attribute values (from OnEntityProperties)</param>
/// <param name="isUnderwater">Whether the player's eyes are submerged in water</param>
/// <param name="isOnGround">Whether the player is on the ground</param>
/// <param name="protocolVersion">The Minecraft protocol version</param>
/// <returns>Ticks to break the block, 0 for instant, -1 for unbreakable</returns>
public static int ComputeDigTicks(
Material blockMaterial,
Item? heldItem,
Item? helmetItem,
Dictionary<Effects, EffectData> effects,
Dictionary<string, double> playerAttributes,
bool isUnderwater,
bool isOnGround,
int protocolVersion)
{
float hardness = BlockHardness.GetHardness(blockMaterial);
if (hardness < 0)
return -1; // Unbreakable
if (hardness == 0)
return 0; // Instant break
float destroySpeed = GetDestroySpeed(
blockMaterial, heldItem, helmetItem, effects, playerAttributes,
isUnderwater, isOnGround, protocolVersion);
bool correctTool = HasCorrectToolForDrops(blockMaterial, heldItem, protocolVersion);
int divisor = correctTool ? 30 : 100;
float destroyProgress = destroySpeed / hardness / divisor;
if (destroyProgress >= 1.0f)
return 0; // Instant break
return (int)MathF.Ceiling(1.0f / destroyProgress);
}
/// <summary>
/// Compute the player's destroy speed for a given block, following vanilla formulas.
/// </summary>
private static float GetDestroySpeed(
Material blockMaterial,
Item? heldItem,
Item? helmetItem,
Dictionary<Effects, EffectData> effects,
Dictionary<string, double> playerAttributes,
bool isUnderwater,
bool isOnGround,
int protocolVersion)
{
float speed = GetToolSpeed(blockMaterial, heldItem, protocolVersion);
if (protocolVersion >= Protocol18Handler.MC_1_21_11_Version)
{
// 1.21.11+: Efficiency is delivered via the MINING_EFFICIENCY attribute
if (speed > 1.0f && playerAttributes.TryGetValue("player.mining_efficiency", out double miningEff))
speed += (float)miningEff;
}
else
{
// Pre-1.21.11: Efficiency enchantment adds level^2 + 1
int effLevel = GetEnchantmentLevel(heldItem, Enchantments.Efficiency, protocolVersion);
if (speed > 1.0f && effLevel > 0)
speed += effLevel * effLevel + 1;
}
// Haste effect: multiply by 1 + 0.2 * (amplifier + 1)
if (effects.TryGetValue(Effects.Haste, out var hasteData))
speed *= 1.0f + (hasteData.Amplifier + 1) * 0.2f;
// Conduit Power also grants dig speed equivalent when in water
if (effects.TryGetValue(Effects.ConduitPower, out var conduitData))
speed *= 1.0f + (conduitData.Amplifier + 1) * 0.2f;
// Mining Fatigue
if (effects.TryGetValue(Effects.MiningFatigue, out var fatigueData))
{
float multiplier = fatigueData.Amplifier switch
{
0 => 0.3f,
1 => 0.09f,
2 => 0.0027f,
_ => 8.1E-4f
};
speed *= multiplier;
}
// Attribute multipliers for modern versions
if (protocolVersion >= Protocol18Handler.MC_1_20_6_Version)
{
// BLOCK_BREAK_SPEED attribute (default 1.0)
if (playerAttributes.TryGetValue("player.block_break_speed", out double bbs))
speed *= (float)bbs;
}
// Underwater penalty
if (isUnderwater)
{
if (protocolVersion >= Protocol18Handler.MC_1_21_11_Version)
{
// 1.21.11+: Uses SUBMERGED_MINING_SPEED attribute (default 0.2)
double submergedSpeed = 0.2;
if (playerAttributes.TryGetValue("player.submerged_mining_speed", out double sms))
submergedSpeed = sms;
speed *= (float)submergedSpeed;
}
else
{
// Pre-1.21.11: /5 unless Aqua Affinity
bool hasAquaAffinity = GetEnchantmentLevel(helmetItem, Enchantments.AquaAffinity, protocolVersion) > 0;
if (!hasAquaAffinity)
speed /= 5.0f;
}
}
// Airborne penalty
if (!isOnGround)
speed /= 5.0f;
return speed;
}
/// <summary>
/// Get the base tool mining speed for a block.
/// For 1.20.6+ with ToolComponent, uses structured component data.
/// For older versions, uses hardcoded tool speed tables.
/// </summary>
private static float GetToolSpeed(Material blockMaterial, Item? heldItem, int protocolVersion)
{
if (heldItem is null)
return 1.0f;
// Modern path: use ToolComponent from structured components
if (protocolVersion >= Protocol18Handler.MC_1_20_6_Version)
{
var toolComp = heldItem.Components?.OfType<ToolComponent>().FirstOrDefault();
if (toolComp is not null)
{
// Check rules for matching blocks
foreach (var rule in toolComp.Rules)
{
if (rule.HasSpeed && MatchesBlockSet(rule.Blocks, blockMaterial))
return rule.Speed;
}
return toolComp.DefaultMiningSpeed;
}
}
// Legacy path: hardcoded tool speed tables
return GetLegacyToolSpeed(heldItem.Type, blockMaterial);
}
/// <summary>
/// Check whether the tool provides correct drops for a block.
/// </summary>
private static bool HasCorrectToolForDrops(Material blockMaterial, Item? heldItem, int protocolVersion)
{
if (!BlockHardness.RequiresCorrectTool(blockMaterial))
return true;
if (heldItem is null)
return false;
// Modern path: check ToolComponent rules
if (protocolVersion >= Protocol18Handler.MC_1_20_6_Version)
{
var toolComp = heldItem.Components?.OfType<ToolComponent>().FirstOrDefault();
if (toolComp is not null)
{
foreach (var rule in toolComp.Rules)
{
if (rule.HasCorrectDropForBlocks && rule.CorrectDropForBlocks
&& MatchesBlockSet(rule.Blocks, blockMaterial))
return true;
}
}
return false;
}
// Legacy path: check if Material2Tool recommends this tool type
return IsCorrectToolLegacy(heldItem.Type, blockMaterial);
}
/// <summary>
/// Match a block material against a ToolComponent BlockSetSubcomponent.
/// </summary>
private static bool MatchesBlockSet(
Protocol.Handlers.StructuredComponents.Components.Subcomponents._1_20_6.BlockSetSubcomponent blockSet,
Material blockMaterial)
{
if (blockSet.BlockIds is not null)
{
// Check against explicit block state IDs
foreach (int blockId in blockSet.BlockIds)
{
if (Block.Palette.FromId(blockId) == blockMaterial)
return true;
}
}
if (blockSet.TagName is not null)
{
// Match against tag name (e.g., "minecraft:mineable/pickaxe")
return MatchesBlockTag(blockSet.TagName, blockMaterial);
}
return false;
}
/// <summary>
/// Approximate block tag matching using Material2Tool categories.
/// Tags like "minecraft:mineable/pickaxe" map to the appropriate tool categories.
/// </summary>
private static bool MatchesBlockTag(string tagName, Material blockMaterial)
{
// Normalize tag name
string tag = tagName.Replace("minecraft:", "");
ItemType[] tools = Material2Tool.GetCorrectToolForBlock(blockMaterial);
if (tools.Length == 0)
return false;
ItemType firstTool = tools[0];
return tag switch
{
"mineable/pickaxe" => IsPickaxe(firstTool),
"mineable/axe" => IsAxe(firstTool),
"mineable/shovel" => IsShovel(firstTool),
"mineable/hoe" => IsHoe(firstTool),
_ => false
};
}
/// <summary>
/// Get the enchantment level from an item, supporting both legacy NBT and modern structured components.
/// </summary>
public static int GetEnchantmentLevel(Item? item, Enchantments enchantment, int protocolVersion)
{
if (item is null)
return 0;
// Modern path: structured components (1.20.6+)
var enchList = item.EnchantmentList;
if (enchList is not null)
{
var ench = enchList.FirstOrDefault(e => e.Type == enchantment);
if (ench is not null)
return ench.Level;
}
// Legacy path: NBT data
if (item.NBT is not null &&
item.NBT.TryGetValue("Enchantments", out object? enchantments))
{
try
{
string enchNameLower = GetEnchantmentResourceName(enchantment);
foreach (Dictionary<string, object> enchEntry in (object[])enchantments)
{
string id = ((string)enchEntry["id"]).ToLowerInvariant();
if (id == enchNameLower || id == "minecraft:" + enchNameLower)
return (short)enchEntry["lvl"];
}
}
catch
{
// NBT parsing failure - return 0
}
}
return 0;
}
/// <summary>
/// Map Enchantments enum to Minecraft resource name (e.g., "efficiency").
/// </summary>
private static string GetEnchantmentResourceName(Enchantments enchantment)
{
return enchantment switch
{
Enchantments.AquaAffinity => "aqua_affinity",
Enchantments.BaneOfArthropods => "bane_of_arthropods",
Enchantments.BlastProtection => "blast_protection",
Enchantments.Efficiency => "efficiency",
Enchantments.FeatherFalling => "feather_falling",
Enchantments.FireAspect => "fire_aspect",
Enchantments.FireProtection => "fire_protection",
Enchantments.FrostWalker => "frost_walker",
Enchantments.LuckOfTheSea => "luck_of_the_sea",
Enchantments.ProjectileProtection => "projectile_protection",
Enchantments.QuickCharge => "quick_charge",
Enchantments.SilkTouch => "silk_touch",
Enchantments.SoulSpeed => "soul_speed",
Enchantments.SwiftSneak => "swift_sneak",
Enchantments.VanishingCurse => "vanishing_curse",
Enchantments.BindingCurse => "binding_curse",
Enchantments.WindBurst => "wind_burst",
_ => enchantment.ToString().ToUnderscoreCase()
};
}
#region Legacy Tool Speed Tables
/// <summary>
/// Legacy tool speed for pre-1.20.6 versions using hardcoded values.
/// </summary>
private static float GetLegacyToolSpeed(ItemType toolType, Material blockMaterial)
{
ItemType[] recommended = Material2Tool.GetCorrectToolForBlock(blockMaterial);
if (recommended.Length == 0)
return 1.0f;
// Check if the held tool matches the recommended tool category
ToolCategory heldCategory = GetToolCategory(toolType);
ToolCategory neededCategory = GetToolCategory(recommended[0]);
if (heldCategory == ToolCategory.None || heldCategory != neededCategory)
{
// Special cases: sword on cobweb, shears on specific blocks
if (toolType is ItemType.Shears && IsShearable(blockMaterial))
return 1.5f;
if (IsSword(toolType) && blockMaterial == Material.Cobweb)
return 15.0f;
return 1.0f;
}
return GetBaseToolSpeed(toolType);
}
private static float GetBaseToolSpeed(ItemType toolType)
{
return toolType switch
{
// Wooden tools
ItemType.WoodenPickaxe or ItemType.WoodenAxe or ItemType.WoodenShovel or
ItemType.WoodenSword or ItemType.WoodenHoe => 2.0f,
// Stone tools
ItemType.StonePickaxe or ItemType.StoneAxe or ItemType.StoneShovel or
ItemType.StoneSword or ItemType.StoneHoe => 4.0f,
// Iron tools
ItemType.IronPickaxe or ItemType.IronAxe or ItemType.IronShovel or
ItemType.IronSword or ItemType.IronHoe => 6.0f,
// Diamond tools
ItemType.DiamondPickaxe or ItemType.DiamondAxe or ItemType.DiamondShovel or
ItemType.DiamondSword or ItemType.DiamondHoe => 8.0f,
// Netherite tools
ItemType.NetheritePickaxe or ItemType.NetheriteAxe or ItemType.NetheriteShovel or
ItemType.NetheriteSword or ItemType.NetheriteHoe => 9.0f,
// Golden tools
ItemType.GoldenPickaxe or ItemType.GoldenAxe or ItemType.GoldenShovel or
ItemType.GoldenSword or ItemType.GoldenHoe => 12.0f,
// Shears
ItemType.Shears => 2.0f,
_ => 1.0f
};
}
/// <summary>
/// Check if the held tool is the correct tool for drops in legacy versions.
/// Uses Material2Tool's recommendations to determine correctness.
/// </summary>
private static bool IsCorrectToolLegacy(ItemType toolType, Material blockMaterial)
{
ItemType[] recommended = Material2Tool.GetCorrectToolForBlock(blockMaterial);
if (recommended.Length == 0)
return false;
ToolCategory heldCategory = GetToolCategory(toolType);
ToolCategory neededCategory = GetToolCategory(recommended[0]);
if (heldCategory == ToolCategory.None || heldCategory != neededCategory)
return false;
// Check tool tier requirement
int heldTier = GetToolTier(toolType);
int requiredTier = GetRequiredTier(blockMaterial, recommended);
return heldTier >= requiredTier;
}
/// <summary>
/// Get the minimum tool tier required for a block based on Material2Tool's recommendation ordering.
/// </summary>
private static int GetRequiredTier(Material blockMaterial, ItemType[] recommended)
{
if (recommended.Length == 0)
return 0;
// Material2Tool lists tools from highest to lowest tier.
// The last tool in the array is the minimum required tier.
return GetToolTier(recommended[^1]);
}
private enum ToolCategory
{
None,
Pickaxe,
Axe,
Shovel,
Hoe,
Sword,
Shears
}
private static ToolCategory GetToolCategory(ItemType item)
{
if (IsPickaxe(item)) return ToolCategory.Pickaxe;
if (IsAxe(item)) return ToolCategory.Axe;
if (IsShovel(item)) return ToolCategory.Shovel;
if (IsHoe(item)) return ToolCategory.Hoe;
if (IsSword(item)) return ToolCategory.Sword;
if (item == ItemType.Shears) return ToolCategory.Shears;
return ToolCategory.None;
}
private static int GetToolTier(ItemType item)
{
string name = item.ToString();
if (name.StartsWith("Wooden")) return 0;
if (name.StartsWith("Golden")) return 0;
if (name.StartsWith("Stone")) return 1;
if (name.StartsWith("Iron")) return 2;
if (name.StartsWith("Diamond")) return 3;
if (name.StartsWith("Netherite")) return 4;
return 0;
}
private static bool IsPickaxe(ItemType item) =>
item is ItemType.WoodenPickaxe or ItemType.StonePickaxe or ItemType.IronPickaxe
or ItemType.GoldenPickaxe or ItemType.DiamondPickaxe or ItemType.NetheritePickaxe;
private static bool IsAxe(ItemType item) =>
item is ItemType.WoodenAxe or ItemType.StoneAxe or ItemType.IronAxe
or ItemType.GoldenAxe or ItemType.DiamondAxe or ItemType.NetheriteAxe;
private static bool IsShovel(ItemType item) =>
item is ItemType.WoodenShovel or ItemType.StoneShovel or ItemType.IronShovel
or ItemType.GoldenShovel or ItemType.DiamondShovel or ItemType.NetheriteShovel;
private static bool IsHoe(ItemType item) =>
item is ItemType.WoodenHoe or ItemType.StoneHoe or ItemType.IronHoe
or ItemType.GoldenHoe or ItemType.DiamondHoe or ItemType.NetheriteHoe;
private static bool IsSword(ItemType item) =>
item is ItemType.WoodenSword or ItemType.StoneSword or ItemType.IronSword
or ItemType.GoldenSword or ItemType.DiamondSword or ItemType.NetheriteSword;
private static bool IsShearable(Material block) =>
block is Material.Cobweb or Material.OakLeaves or Material.SpruceLeaves
or Material.BirchLeaves or Material.JungleLeaves or Material.AcaciaLeaves
or Material.DarkOakLeaves or Material.CherryLeaves or Material.MangroveLeaves
or Material.AzaleaLeaves or Material.FloweringAzaleaLeaves
or Material.WhiteWool or Material.OrangeWool or Material.MagentaWool
or Material.LightBlueWool or Material.YellowWool or Material.LimeWool
or Material.PinkWool or Material.GrayWool or Material.LightGrayWool
or Material.CyanWool or Material.PurpleWool or Material.BlueWool
or Material.BrownWool or Material.GreenWool or Material.RedWool
or Material.BlackWool or Material.Vine;
#endregion
}
}