From ade4c1491114b274103d3974faf2239fcdd314d5 Mon Sep 17 00:00:00 2001 From: Camotoy <20743703+Camotoy@users.noreply.github.com> Date: Thu, 7 Jan 2021 22:43:36 -0500 Subject: [PATCH] Block breaking refactors (#1336) Client-side block animations and reach checks are now added. This commit also includes cleanup in BlockChangeTranslator as well as proper Netherite tool support for calculating block breaking. Co-authored-by: rtm516 --- ...BedrockInventoryTransactionTranslator.java | 120 +++++++++++++++--- .../player/BedrockActionTranslator.java | 50 +++++++- .../entity/JavaEntityStatusTranslator.java | 15 ++- .../player/JavaPlayerActionAckTranslator.java | 97 +++++++------- .../java/world/JavaBlockChangeTranslator.java | 11 +- .../world/block/BlockTranslator.java | 4 +- .../geysermc/connector/utils/BlockUtils.java | 3 + 7 files changed, 223 insertions(+), 77 deletions(-) diff --git a/connector/src/main/java/org/geysermc/connector/network/translators/bedrock/BedrockInventoryTransactionTranslator.java b/connector/src/main/java/org/geysermc/connector/network/translators/bedrock/BedrockInventoryTransactionTranslator.java index 228b2812f17..8263507b207 100644 --- a/connector/src/main/java/org/geysermc/connector/network/translators/bedrock/BedrockInventoryTransactionTranslator.java +++ b/connector/src/main/java/org/geysermc/connector/network/translators/bedrock/BedrockInventoryTransactionTranslator.java @@ -39,12 +39,11 @@ import com.nukkitx.math.vector.Vector3f; import com.nukkitx.math.vector.Vector3i; import com.nukkitx.protocol.bedrock.data.LevelEventType; +import com.nukkitx.protocol.bedrock.data.entity.EntityFlag; +import com.nukkitx.protocol.bedrock.data.entity.EntityFlags; import com.nukkitx.protocol.bedrock.data.inventory.ContainerId; import com.nukkitx.protocol.bedrock.data.inventory.ContainerType; -import com.nukkitx.protocol.bedrock.packet.ContainerOpenPacket; -import com.nukkitx.protocol.bedrock.packet.InventorySlotPacket; -import com.nukkitx.protocol.bedrock.packet.InventoryTransactionPacket; -import com.nukkitx.protocol.bedrock.packet.LevelEventPacket; +import com.nukkitx.protocol.bedrock.packet.*; import org.geysermc.connector.entity.CommandBlockMinecartEntity; import org.geysermc.connector.entity.Entity; import org.geysermc.connector.entity.ItemFrameEntity; @@ -64,9 +63,18 @@ import java.util.concurrent.TimeUnit; +/** + * BedrockInventoryTransactionTranslator handles most interactions between the client and the world, + * or the client and their inventory. + */ @Translator(packet = InventoryTransactionPacket.class) public class BedrockInventoryTransactionTranslator extends PacketTranslator { + private static final float MAXIMUM_BLOCK_PLACING_DISTANCE = 64f; + private static final int CREATIVE_EYE_HEIGHT_PLACE_DISTANCE = 49; + private static final int SURVIVAL_EYE_HEIGHT_PLACE_DISTANCE = 36; + private static final float MAXIMUM_BLOCK_DESTROYING_DISTANCE = 36f; + @Override public void translate(InventoryTransactionPacket packet, GeyserSession session) { // Send book updates before opening inventories @@ -112,6 +120,46 @@ public void translate(InventoryTransactionPacket packet, GeyserSession session) break; } + Vector3i blockPos = BlockUtils.getBlockPosition(packet.getBlockPosition(), packet.getBlockFace()); + /* + Checks to ensure that the range will be accepted by the server. + "Not in range" doesn't refer to how far a vanilla client goes (that's a whole other mess), + but how much a server will accept from the client maximum + */ + // CraftBukkit+ check - see https://github.com/PaperMC/Paper/blob/458db6206daae76327a64f4e2a17b67a7e38b426/Spigot-Server-Patches/0532-Move-range-check-for-block-placing-up.patch + Vector3f playerPosition = session.getPlayerEntity().getPosition(); + EntityFlags flags = session.getPlayerEntity().getMetadata().getFlags(); + + // Adjust position for current eye height + if (flags.getFlag(EntityFlag.SNEAKING)) { + playerPosition = playerPosition.sub(0, (EntityType.PLAYER.getOffset() - 1.27f), 0); + } else if (flags.getFlag(EntityFlag.SWIMMING) || flags.getFlag(EntityFlag.GLIDING) || flags.getFlag(EntityFlag.DAMAGE_NEARBY_MOBS)) { + // Swimming, gliding, or using the trident spin attack + playerPosition = playerPosition.sub(0, (EntityType.PLAYER.getOffset() - 0.4f), 0); + } else if (flags.getFlag(EntityFlag.SLEEPING)) { + playerPosition = playerPosition.sub(0, (EntityType.PLAYER.getOffset() - 0.2f), 0); + } // else, we don't have to modify the position + + float diffX = playerPosition.getX() - packet.getBlockPosition().getX(); + float diffY = playerPosition.getY() - packet.getBlockPosition().getY(); + float diffZ = playerPosition.getZ() - packet.getBlockPosition().getZ(); + if (((diffX * diffX) + (diffY * diffY) + (diffZ * diffZ)) > + (session.getGameMode().equals(GameMode.CREATIVE) ? CREATIVE_EYE_HEIGHT_PLACE_DISTANCE : SURVIVAL_EYE_HEIGHT_PLACE_DISTANCE)) { + restoreCorrectBlock(session, blockPos, packet); + return; + } + + // Vanilla check + if (!(session.getPlayerEntity().getPosition().sub(0, EntityType.PLAYER.getOffset(), 0) + .distanceSquared(packet.getBlockPosition().toFloat().add(0.5f, 0.5f, 0.5f)) < MAXIMUM_BLOCK_PLACING_DISTANCE)) { + // The client thinks that its blocks have been successfully placed. Restore the server's blocks instead. + restoreCorrectBlock(session, blockPos, packet); + return; + } + /* + Block place checks end - client is good to go + */ + ClientPlayerPlaceBlockPacket blockPacket = new ClientPlayerPlaceBlockPacket( new Position(packet.getBlockPosition().getX(), packet.getBlockPosition().getY(), packet.getBlockPosition().getZ()), BlockFace.values()[packet.getBlockFace()], @@ -159,7 +207,6 @@ else if (packet.getItemInHand() != null && ItemRegistry.BUCKETS.contains(packet. } } - Vector3i blockPos = BlockUtils.getBlockPosition(packet.getBlockPosition(), packet.getBlockFace()); ItemEntry handItem = ItemRegistry.getItem(packet.getItemInHand()); if (handItem.isBlock()) { session.setLastBlockPlacePosition(blockPos); @@ -184,19 +231,32 @@ else if (packet.getItemInHand() != null && ItemRegistry.BUCKETS.contains(packet. session.sendDownstreamPacket(useItemPacket); break; case 2: - int blockState = session.getConnector().getWorldManager().getBlockAt(session, packet.getBlockPosition().getX(), packet.getBlockPosition().getY(), packet.getBlockPosition().getZ()); - double blockHardness = BlockTranslator.JAVA_RUNTIME_ID_TO_HARDNESS.get(blockState); - if (session.getGameMode() == GameMode.CREATIVE || (session.getConnector().getConfig().isCacheChunks() && blockHardness == 0)) { - session.setLastBlockPlacedId(null); - session.setLastBlockPlacePosition(null); + int blockState = session.getGameMode() == GameMode.CREATIVE ? + session.getConnector().getWorldManager().getBlockAt(session, packet.getBlockPosition()) : session.getBreakingBlock(); - LevelEventPacket blockBreakPacket = new LevelEventPacket(); - blockBreakPacket.setType(LevelEventType.PARTICLE_DESTROY_BLOCK); - blockBreakPacket.setPosition(packet.getBlockPosition().toFloat()); - blockBreakPacket.setData(BlockTranslator.getBedrockBlockId(blockState)); - session.sendUpstreamPacket(blockBreakPacket); + session.setLastBlockPlacedId(null); + session.setLastBlockPlacePosition(null); + + // Same deal with vanilla block placing as above. + // This is working out the distance using 3d Pythagoras and the extra value added to the Y is the sneaking height of a java player. + playerPosition = session.getPlayerEntity().getPosition(); + Vector3f floatBlockPosition = packet.getBlockPosition().toFloat(); + diffX = playerPosition.getX() - (floatBlockPosition.getX() + 0.5f); + diffY = (playerPosition.getY() - EntityType.PLAYER.getOffset()) - (floatBlockPosition.getY() + 0.5f) + 1.5f; + diffZ = playerPosition.getZ() - (floatBlockPosition.getZ() + 0.5f); + float distanceSquared = diffX * diffX + diffY * diffY + diffZ * diffZ; + if (distanceSquared > MAXIMUM_BLOCK_DESTROYING_DISTANCE) { + restoreCorrectBlock(session, packet.getBlockPosition(), packet); + return; } + LevelEventPacket blockBreakPacket = new LevelEventPacket(); + blockBreakPacket.setType(LevelEventType.PARTICLE_DESTROY_BLOCK); + blockBreakPacket.setPosition(packet.getBlockPosition().toFloat()); + blockBreakPacket.setData(BlockTranslator.getBedrockBlockId(blockState)); + session.sendUpstreamPacket(blockBreakPacket); + session.setBreakingBlock(BlockTranslator.JAVA_AIR_ID); + long frameEntityId = ItemFrameEntity.getItemFrameEntityId(session, packet.getBlockPosition()); if (frameEntityId != -1 && session.getEntityCache().getEntityByJavaId(frameEntityId) != null) { ClientPlayerInteractEntityPacket attackPacket = new ClientPlayerInteractEntityPacket((int) frameEntityId, InteractAction.ATTACK, session.isSneaking()); @@ -270,4 +330,34 @@ else if (packet.getItemInHand() != null && ItemRegistry.BUCKETS.contains(packet. break; } } + + /** + * Restore the correct block state from the server without updating the chunk cache. + * + * @param session the session of the Bedrock client + * @param blockPos the block position to restore + */ + private void restoreCorrectBlock(GeyserSession session, Vector3i blockPos, InventoryTransactionPacket packet) { + int javaBlockState = session.getConnector().getWorldManager().getBlockAt(session, blockPos); + UpdateBlockPacket updateBlockPacket = new UpdateBlockPacket(); + updateBlockPacket.setDataLayer(0); + updateBlockPacket.setBlockPosition(blockPos); + updateBlockPacket.setRuntimeId(BlockTranslator.getBedrockBlockId(javaBlockState)); + updateBlockPacket.getFlags().addAll(UpdateBlockPacket.FLAG_ALL_PRIORITY); + session.sendUpstreamPacket(updateBlockPacket); + + UpdateBlockPacket updateWaterPacket = new UpdateBlockPacket(); + updateWaterPacket.setDataLayer(1); + updateWaterPacket.setBlockPosition(blockPos); + updateWaterPacket.setRuntimeId(BlockTranslator.isWaterlogged(javaBlockState) ? BlockTranslator.BEDROCK_WATER_ID : BlockTranslator.BEDROCK_AIR_ID); + updateWaterPacket.getFlags().addAll(UpdateBlockPacket.FLAG_ALL_PRIORITY); + session.sendUpstreamPacket(updateWaterPacket); + + // Reset the item in hand to prevent "missing" blocks + InventorySlotPacket slotPacket = new InventorySlotPacket(); + slotPacket.setContainerId(ContainerId.INVENTORY); + slotPacket.setSlot(packet.getHotbarSlot()); + slotPacket.setItem(packet.getItemInHand()); + session.sendUpstreamPacket(slotPacket); + } } diff --git a/connector/src/main/java/org/geysermc/connector/network/translators/bedrock/entity/player/BedrockActionTranslator.java b/connector/src/main/java/org/geysermc/connector/network/translators/bedrock/entity/player/BedrockActionTranslator.java index f6a6557ede8..c4dbbec405e 100644 --- a/connector/src/main/java/org/geysermc/connector/network/translators/bedrock/entity/player/BedrockActionTranslator.java +++ b/connector/src/main/java/org/geysermc/connector/network/translators/bedrock/entity/player/BedrockActionTranslator.java @@ -25,13 +25,16 @@ package org.geysermc.connector.network.translators.bedrock.entity.player; +import com.github.steveice10.mc.protocol.data.game.entity.metadata.ItemStack; import com.github.steveice10.mc.protocol.data.game.entity.metadata.Position; +import com.github.steveice10.mc.protocol.data.game.entity.player.GameMode; import com.github.steveice10.mc.protocol.data.game.entity.player.PlayerAction; import com.github.steveice10.mc.protocol.data.game.entity.player.PlayerState; import com.github.steveice10.mc.protocol.data.game.world.block.BlockFace; import com.github.steveice10.mc.protocol.packet.ingame.client.player.ClientPlayerAbilitiesPacket; import com.github.steveice10.mc.protocol.packet.ingame.client.player.ClientPlayerActionPacket; import com.github.steveice10.mc.protocol.packet.ingame.client.player.ClientPlayerStatePacket; +import com.github.steveice10.opennbt.tag.builtin.CompoundTag; import com.nukkitx.math.vector.Vector3i; import com.nukkitx.protocol.bedrock.data.LevelEventType; import com.nukkitx.protocol.bedrock.data.entity.EntityEventType; @@ -40,9 +43,12 @@ import com.nukkitx.protocol.bedrock.packet.PlayStatusPacket; import com.nukkitx.protocol.bedrock.packet.PlayerActionPacket; import org.geysermc.connector.entity.Entity; +import org.geysermc.connector.inventory.PlayerInventory; import org.geysermc.connector.network.session.GeyserSession; import org.geysermc.connector.network.translators.PacketTranslator; import org.geysermc.connector.network.translators.Translator; +import org.geysermc.connector.network.translators.item.ItemEntry; +import org.geysermc.connector.network.translators.item.ItemRegistry; import org.geysermc.connector.network.translators.world.block.BlockTranslator; import org.geysermc.connector.utils.BlockUtils; @@ -130,6 +136,27 @@ public void translate(PlayerActionPacket packet, GeyserSession session) { break; case START_BREAK: if (session.getConnector().getConfig().isCacheChunks()) { + // Start the block breaking animation + if (session.getGameMode() != GameMode.CREATIVE) { + int blockState = session.getConnector().getWorldManager().getBlockAt(session, vector); + double blockHardness = BlockTranslator.JAVA_RUNTIME_ID_TO_HARDNESS.get(blockState); + LevelEventPacket startBreak = new LevelEventPacket(); + startBreak.setType(LevelEventType.BLOCK_START_BREAK); + startBreak.setPosition(vector.toFloat()); + PlayerInventory inventory = session.getInventory(); + ItemStack item = inventory.getItemInHand(); + ItemEntry itemEntry = null; + CompoundTag nbtData = new CompoundTag(""); + if (item != null) { + itemEntry = ItemRegistry.getItem(item); + nbtData = item.getNbt(); + } + double breakTime = Math.ceil(BlockUtils.getBreakTime(blockHardness, blockState, itemEntry, nbtData, session) * 20); + startBreak.setData((int) (65535 / breakTime)); + session.setBreakingBlock(blockState); + session.sendUpstreamPacket(startBreak); + } + // Account for fire - the client likes to hit the block behind. Vector3i fireBlockPos = BlockUtils.getBlockPosition(packet.getBlockPosition(), packet.getFace()); int blockUp = session.getConnector().getWorldManager().getBlockAt(session, fireBlockPos); @@ -138,24 +165,33 @@ public void translate(PlayerActionPacket packet, GeyserSession session) { ClientPlayerActionPacket startBreakingPacket = new ClientPlayerActionPacket(PlayerAction.START_DIGGING, new Position(fireBlockPos.getX(), fireBlockPos.getY(), fireBlockPos.getZ()), BlockFace.values()[packet.getFace()]); session.sendDownstreamPacket(startBreakingPacket); - break; + if (session.getGameMode() == GameMode.CREATIVE) { + break; + } } } - ClientPlayerActionPacket startBreakingPacket = new ClientPlayerActionPacket(PlayerAction.START_DIGGING, new Position(packet.getBlockPosition().getX(), - packet.getBlockPosition().getY(), packet.getBlockPosition().getZ()), BlockFace.values()[packet.getFace()]); + ClientPlayerActionPacket startBreakingPacket = new ClientPlayerActionPacket(PlayerAction.START_DIGGING, position, BlockFace.values()[packet.getFace()]); session.sendDownstreamPacket(startBreakingPacket); break; case CONTINUE_BREAK: + if (session.getGameMode() == GameMode.CREATIVE) { + break; + } LevelEventPacket continueBreakPacket = new LevelEventPacket(); continueBreakPacket.setType(LevelEventType.PARTICLE_CRACK_BLOCK); - continueBreakPacket.setData(BlockTranslator.getBedrockBlockId(session.getBreakingBlock())); - continueBreakPacket.setPosition(packet.getBlockPosition().toFloat()); + continueBreakPacket.setData((BlockTranslator.getBedrockBlockId(session.getBreakingBlock())) | (packet.getFace() << 24)); + continueBreakPacket.setPosition(vector.toFloat()); session.sendUpstreamPacket(continueBreakPacket); break; case ABORT_BREAK: - ClientPlayerActionPacket abortBreakingPacket = new ClientPlayerActionPacket(PlayerAction.CANCEL_DIGGING, new Position(packet.getBlockPosition().getX(), - packet.getBlockPosition().getY(), packet.getBlockPosition().getZ()), BlockFace.DOWN); + ClientPlayerActionPacket abortBreakingPacket = new ClientPlayerActionPacket(PlayerAction.CANCEL_DIGGING, position, BlockFace.DOWN); session.sendDownstreamPacket(abortBreakingPacket); + LevelEventPacket stopBreak = new LevelEventPacket(); + stopBreak.setType(LevelEventType.BLOCK_STOP_BREAK); + stopBreak.setPosition(vector.toFloat()); + stopBreak.setData(0); + session.setBreakingBlock(BlockTranslator.JAVA_AIR_ID); + session.sendUpstreamPacket(stopBreak); break; case STOP_BREAK: // Handled in BedrockInventoryTransactionTranslator diff --git a/connector/src/main/java/org/geysermc/connector/network/translators/java/entity/JavaEntityStatusTranslator.java b/connector/src/main/java/org/geysermc/connector/network/translators/java/entity/JavaEntityStatusTranslator.java index 1072826487f..59ea29925c7 100644 --- a/connector/src/main/java/org/geysermc/connector/network/translators/java/entity/JavaEntityStatusTranslator.java +++ b/connector/src/main/java/org/geysermc/connector/network/translators/java/entity/JavaEntityStatusTranslator.java @@ -31,8 +31,8 @@ import com.nukkitx.protocol.bedrock.data.entity.EntityData; import com.nukkitx.protocol.bedrock.data.entity.EntityEventType; import com.nukkitx.protocol.bedrock.packet.EntityEventPacket; -import com.nukkitx.protocol.bedrock.packet.LevelSoundEvent2Packet; import com.nukkitx.protocol.bedrock.packet.LevelEventPacket; +import com.nukkitx.protocol.bedrock.packet.LevelSoundEvent2Packet; import com.nukkitx.protocol.bedrock.packet.SetEntityDataPacket; import com.nukkitx.protocol.bedrock.packet.SetEntityMotionPacket; import org.geysermc.connector.entity.Entity; @@ -183,6 +183,19 @@ public void translate(ServerEntityStatusPacket packet, GeyserSession session) { return; } break; + case LIVING_EQUIPMENT_BREAK_HEAD: + case LIVING_EQUIPMENT_BREAK_CHEST: + case LIVING_EQUIPMENT_BREAK_LEGS: + case LIVING_EQUIPMENT_BREAK_FEET: + case LIVING_EQUIPMENT_BREAK_MAIN_HAND: + case LIVING_EQUIPMENT_BREAK_OFF_HAND: + LevelSoundEvent2Packet equipmentBreakPacket = new LevelSoundEvent2Packet(); + equipmentBreakPacket.setSound(SoundEvent.BREAK); + equipmentBreakPacket.setPosition(entity.getPosition()); + equipmentBreakPacket.setExtraData(-1); + equipmentBreakPacket.setIdentifier(""); + session.sendUpstreamPacket(equipmentBreakPacket); + return; } session.sendUpstreamPacket(entityEventPacket); diff --git a/connector/src/main/java/org/geysermc/connector/network/translators/java/entity/player/JavaPlayerActionAckTranslator.java b/connector/src/main/java/org/geysermc/connector/network/translators/java/entity/player/JavaPlayerActionAckTranslator.java index d98f094a241..837adb126ff 100644 --- a/connector/src/main/java/org/geysermc/connector/network/translators/java/entity/player/JavaPlayerActionAckTranslator.java +++ b/connector/src/main/java/org/geysermc/connector/network/translators/java/entity/player/JavaPlayerActionAckTranslator.java @@ -27,19 +27,20 @@ import com.github.steveice10.mc.protocol.data.game.entity.metadata.ItemStack; import com.github.steveice10.mc.protocol.data.game.entity.player.GameMode; +import com.github.steveice10.mc.protocol.data.game.entity.player.PlayerAction; import com.github.steveice10.mc.protocol.packet.ingame.server.entity.player.ServerPlayerActionAckPacket; -import com.github.steveice10.opennbt.tag.builtin.*; +import com.github.steveice10.opennbt.tag.builtin.CompoundTag; import com.nukkitx.math.vector.Vector3f; import com.nukkitx.protocol.bedrock.data.LevelEventType; import com.nukkitx.protocol.bedrock.packet.LevelEventPacket; import org.geysermc.connector.inventory.PlayerInventory; import org.geysermc.connector.network.session.GeyserSession; import org.geysermc.connector.network.translators.PacketTranslator; +import org.geysermc.connector.network.translators.Translator; +import org.geysermc.connector.network.translators.item.ItemEntry; import org.geysermc.connector.network.translators.item.ItemRegistry; import org.geysermc.connector.network.translators.world.block.BlockTranslator; -import org.geysermc.connector.network.translators.item.ItemEntry; import org.geysermc.connector.utils.BlockUtils; -import org.geysermc.connector.network.translators.Translator; import org.geysermc.connector.utils.ChunkUtils; @Translator(packet = ServerPlayerActionAckPacket.class) @@ -47,54 +48,54 @@ public class JavaPlayerActionAckTranslator extends PacketTranslator { @Override public void translate(ServerBlockChangePacket packet, GeyserSession session) { Position pos = packet.getRecord().getPosition(); - boolean updatePlacement = !(session.getConnector().getConfig().isCacheChunks() && session.getConnector().getWorldManager().getBlockAt(session, pos.getX(), pos.getY(), pos.getZ()) == packet.getRecord().getBlock()); - ChunkUtils.updateBlock(session, packet.getRecord().getBlock(), packet.getRecord().getPosition()); - if (updatePlacement && session.getConnector().getPlatformType() != PlatformType.SPIGOT) { + boolean updatePlacement = session.getConnector().getPlatformType() != PlatformType.SPIGOT && // Spigot simply listens for the block place event + !(session.getConnector().getConfig().isCacheChunks() && + session.getConnector().getWorldManager().getBlockAt(session, pos) == packet.getRecord().getBlock()); + ChunkUtils.updateBlock(session, packet.getRecord().getBlock(), pos); + if (updatePlacement) { this.checkPlace(session, packet); } this.checkInteract(session, packet); diff --git a/connector/src/main/java/org/geysermc/connector/network/translators/world/block/BlockTranslator.java b/connector/src/main/java/org/geysermc/connector/network/translators/world/block/BlockTranslator.java index db6f43fea6b..b047999e7d1 100644 --- a/connector/src/main/java/org/geysermc/connector/network/translators/world/block/BlockTranslator.java +++ b/connector/src/main/java/org/geysermc/connector/network/translators/world/block/BlockTranslator.java @@ -87,7 +87,9 @@ public class BlockTranslator { */ public static final int BEDROCK_RUNTIME_COMMAND_BLOCK_ID; - // For block breaking animation math + /** + * A list of all Java runtime wool IDs, for use with block breaking math and shears + */ public static final IntSet JAVA_RUNTIME_WOOL_IDS = new IntOpenHashSet(); public static final int JAVA_RUNTIME_COBWEB_ID; diff --git a/connector/src/main/java/org/geysermc/connector/utils/BlockUtils.java b/connector/src/main/java/org/geysermc/connector/utils/BlockUtils.java index 93909b20f92..c859b9f65fa 100644 --- a/connector/src/main/java/org/geysermc/connector/utils/BlockUtils.java +++ b/connector/src/main/java/org/geysermc/connector/utils/BlockUtils.java @@ -50,6 +50,7 @@ private static double toolBreakTimeBonus(String toolType, String toolTier, boole if (toolType.equals("shears")) return isWoolBlock ? 5.0 : 15.0; if (toolType.equals("")) return 1.0; switch (toolTier) { + // https://minecraft.gamepedia.com/Breaking#Speed case "wooden": return 2.0; case "stone": @@ -58,6 +59,8 @@ private static double toolBreakTimeBonus(String toolType, String toolTier, boole return 6.0; case "diamond": return 8.0; + case "netherite": + return 9.0; case "golden": return 12.0; default: