mirror of
https://github.com/Minestom/Minestom.git
synced 2025-02-11 01:41:47 +01:00
Fix Instamine (#2340)
* use custom data generator - change back to official when done * initial calculation * use submerged mining speed attribute * use ticks instead of seconds as calculation result * attribute command * testinstabreak command * add Tag.toString(), don't know why this isn't added yet * update method name to reflect field * update and fix calculation * actually use calculation in PlayerDiggingListener * some logging for testing, gotta remove later * remove FIXME comment, mineable should be handled correctly by calculation and enchantments don't affect the calculation, only attributes do. * player.isInstantBreak() is included in calculation * more + better testing logging * add break block logging * use GameMode.CREATIVE instead of isInstantBreak as check. isInstantBreak is named poorly, doesn't do what you'd expect * fix typo * bamboo test * support bamboo * sanity check when finishing digging * me dumb * anvil inventory for testing instabreak ability * remove early GameMode check, included in calculation * add stone to testing command * add bamboo reason comment * update data generator to rv4 * remove aqua affinity check * remove logging * add wiki link * remove logging * remove blank newline * better name * correct fluid height calculation * remove unused imports * more testing setup * improve/correct comment * add tests for stone (with diamond pickaxe) and wool (with shears) * update comment
This commit is contained in:
parent
e0939f089b
commit
202889b854
@ -77,6 +77,8 @@ public class Main {
|
||||
commandManager.register(new PotionCommand());
|
||||
commandManager.register(new CookieCommand());
|
||||
commandManager.register(new WorldBorderCommand());
|
||||
commandManager.register(new TestInstabreakCommand());
|
||||
commandManager.register(new AttributeCommand());
|
||||
|
||||
commandManager.setUnknownCommandCallback((sender, command) -> sender.sendMessage(Component.text("Unknown command", NamedTextColor.RED)));
|
||||
|
||||
|
@ -0,0 +1,115 @@
|
||||
package net.minestom.demo.commands;
|
||||
|
||||
import net.kyori.adventure.text.Component;
|
||||
import net.minestom.server.command.CommandSender;
|
||||
import net.minestom.server.command.builder.Command;
|
||||
import net.minestom.server.command.builder.CommandContext;
|
||||
import net.minestom.server.command.builder.arguments.ArgumentLiteral;
|
||||
import net.minestom.server.command.builder.arguments.ArgumentType;
|
||||
import net.minestom.server.command.builder.arguments.minecraft.ArgumentEntity;
|
||||
import net.minestom.server.command.builder.arguments.minecraft.ArgumentResource;
|
||||
import net.minestom.server.command.builder.arguments.number.ArgumentDouble;
|
||||
import net.minestom.server.entity.Entity;
|
||||
import net.minestom.server.entity.LivingEntity;
|
||||
import net.minestom.server.entity.attribute.Attribute;
|
||||
import net.minestom.server.utils.entity.EntityFinder;
|
||||
import net.minestom.server.utils.identity.NamedAndIdentified;
|
||||
import org.jetbrains.annotations.Contract;
|
||||
import org.jetbrains.annotations.Nullable;
|
||||
|
||||
import static net.kyori.adventure.text.Component.text;
|
||||
import static net.kyori.adventure.text.Component.translatable;
|
||||
|
||||
public class AttributeCommand extends Command {
|
||||
public AttributeCommand() {
|
||||
super("attribute");
|
||||
|
||||
ArgumentEntity target = ArgumentType.Entity("target").singleEntity(true);
|
||||
ArgumentResource attribute = ArgumentType.Resource("attribute", "minecraft:attribute");
|
||||
ArgumentLiteral base = ArgumentType.Literal("base");
|
||||
ArgumentLiteral get = ArgumentType.Literal("get");
|
||||
ArgumentLiteral set = ArgumentType.Literal("set");
|
||||
ArgumentDouble value = ArgumentType.Double("value");
|
||||
|
||||
addSyntax(this::get, target, attribute, get);
|
||||
addSyntax(this::setBase, target, attribute, base, set, value);
|
||||
addSyntax(this::getBase, target, attribute, base, get);
|
||||
}
|
||||
|
||||
private void setBase(CommandSender sender, CommandContext ctx) {
|
||||
LivingEntity target = target(sender, ctx);
|
||||
if (check(target, ctx, sender)) return;
|
||||
Attribute attribute = attribute(ctx);
|
||||
if (check(attribute, ctx, sender)) return;
|
||||
double value = value(ctx);
|
||||
target.getAttribute(attribute).setBaseValue(value);
|
||||
sender.sendMessage(translatable("commands.attribute.base_value.set.success").arguments(description(attribute), name(target), text(value)));
|
||||
}
|
||||
|
||||
private void getBase(CommandSender sender, CommandContext ctx) {
|
||||
LivingEntity target = target(sender, ctx);
|
||||
if (check(target, ctx, sender)) return;
|
||||
Attribute attribute = attribute(ctx);
|
||||
if (check(attribute, ctx, sender)) return;
|
||||
double value = target.getAttribute(attribute).getBaseValue();
|
||||
sender.sendMessage(translatable("commands.attribute.base_value.get.success").arguments(description(attribute), name(target), text(value)));
|
||||
}
|
||||
|
||||
private void get(CommandSender sender, CommandContext ctx) {
|
||||
LivingEntity target = target(sender, ctx);
|
||||
if (check(target, ctx, sender)) return;
|
||||
Attribute attribute = attribute(ctx);
|
||||
if (check(attribute, ctx, sender)) return;
|
||||
double value = target.getAttributeValue(attribute);
|
||||
sender.sendMessage(translatable("commands.attribute.value.get.success").arguments(description(attribute), name(target), text(value)));
|
||||
}
|
||||
|
||||
private Component description(Attribute attribute) {
|
||||
return translatable(attribute.registry().translationKey());
|
||||
}
|
||||
|
||||
private double value(CommandContext ctx) {
|
||||
return ctx.get("value");
|
||||
}
|
||||
|
||||
private LivingEntity target(CommandSender sender, CommandContext ctx) {
|
||||
EntityFinder finder = ctx.get("target");
|
||||
Entity entity = finder.findFirstEntity(sender);
|
||||
if (!(entity instanceof LivingEntity livingEntity)) {
|
||||
return null;
|
||||
}
|
||||
return livingEntity;
|
||||
}
|
||||
|
||||
@Nullable
|
||||
private Attribute attribute(CommandContext ctx) {
|
||||
String namespaceId = ctx.get("attribute");
|
||||
return Attribute.fromNamespaceId(namespaceId);
|
||||
}
|
||||
|
||||
private Component name(Entity entity) {
|
||||
if (entity instanceof NamedAndIdentified named) {
|
||||
return named.getName();
|
||||
}
|
||||
return entity.getCustomName() == null ? entity.getCustomName() : text(entity.getEntityType().name());
|
||||
}
|
||||
|
||||
@Contract("!null, _, _ -> false; null, _, _ -> true")
|
||||
private boolean check(@Nullable LivingEntity livingEntity, CommandContext ctx, CommandSender sender) {
|
||||
if (livingEntity == null) {
|
||||
Entity entity = ctx.get("target");
|
||||
sender.sendMessage(translatable("commands.attribute.failed.entity").arguments(name(entity)));
|
||||
return true;
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
@Contract("!null, _, _ -> false; null, _, _ -> true")
|
||||
private boolean check(@Nullable Attribute attribute, CommandContext ctx, CommandSender sender) {
|
||||
if (attribute == null) {
|
||||
sender.sendMessage(translatable("argument.resource.invalid_type").arguments(text(ctx.<String>get("attribute")), text("minecraft:attribute"), text("minecraft:attribute")));
|
||||
return true;
|
||||
}
|
||||
return false;
|
||||
}
|
||||
}
|
@ -0,0 +1,142 @@
|
||||
package net.minestom.demo.commands;
|
||||
|
||||
import net.kyori.adventure.text.Component;
|
||||
import net.minestom.server.command.builder.Command;
|
||||
import net.minestom.server.command.builder.arguments.ArgumentType;
|
||||
import net.minestom.server.command.builder.arguments.number.ArgumentInteger;
|
||||
import net.minestom.server.entity.GameMode;
|
||||
import net.minestom.server.entity.Player;
|
||||
import net.minestom.server.instance.batch.RelativeBlockBatch;
|
||||
import net.minestom.server.instance.block.Block;
|
||||
import net.minestom.server.inventory.Inventory;
|
||||
import net.minestom.server.inventory.InventoryType;
|
||||
import net.minestom.server.item.ItemComponent;
|
||||
import net.minestom.server.item.ItemStack;
|
||||
import net.minestom.server.item.Material;
|
||||
import net.minestom.server.item.component.EnchantmentList;
|
||||
import net.minestom.server.item.enchant.Enchantment;
|
||||
import net.minestom.server.potion.Potion;
|
||||
import net.minestom.server.potion.PotionEffect;
|
||||
|
||||
import java.util.ArrayList;
|
||||
import java.util.List;
|
||||
|
||||
public class TestInstabreakCommand extends Command {
|
||||
|
||||
public TestInstabreakCommand() {
|
||||
super("testinstabreak");
|
||||
|
||||
ArgumentInteger level = ArgumentType.Integer("level");
|
||||
addConditionalSyntax((sender, commandString) -> sender instanceof Player, (sender, context) -> {
|
||||
Player player = (Player) sender;
|
||||
|
||||
int l = context.get(level);
|
||||
player.removeEffect(PotionEffect.HASTE);
|
||||
if (l != 0) {
|
||||
player.addEffect(new Potion(PotionEffect.HASTE, (byte) (l - 1), -1));
|
||||
}
|
||||
}, ArgumentType.Literal("haste"), level);
|
||||
addConditionalSyntax((sender, commandString) -> sender instanceof Player, (sender, context) -> {
|
||||
Player player = (Player) sender;
|
||||
|
||||
int l = context.get(level);
|
||||
player.removeEffect(PotionEffect.CONDUIT_POWER);
|
||||
if (l != 0) {
|
||||
player.addEffect(new Potion(PotionEffect.CONDUIT_POWER, (byte) (l - 1), -1));
|
||||
}
|
||||
}, ArgumentType.Literal("conduit"), level);
|
||||
addConditionalSyntax((sender, commandString) -> sender instanceof Player, (sender, context) -> {
|
||||
Player player = (Player) sender;
|
||||
|
||||
int l = context.get(level);
|
||||
player.removeEffect(PotionEffect.MINING_FATIGUE);
|
||||
if (l != 0) {
|
||||
player.addEffect(new Potion(PotionEffect.MINING_FATIGUE, (byte) (l - 1), -1));
|
||||
}
|
||||
}, ArgumentType.Literal("fatigue"), level);
|
||||
|
||||
addConditionalSyntax((sender, commandString) -> sender instanceof Player, (sender, context) -> {
|
||||
Player player = (Player) sender;
|
||||
giveItems(player);
|
||||
}, ArgumentType.Literal("giveItems"));
|
||||
|
||||
addConditionalSyntax((sender, commandString) -> sender instanceof Player, (sender, context) -> {
|
||||
Player player = (Player) sender;
|
||||
player.openInventory(new Inventory(InventoryType.ANVIL, Component.translatable("container.repair")));
|
||||
}, ArgumentType.Literal("anvil"));
|
||||
|
||||
RelativeBlockBatch areaBatch = new RelativeBlockBatch();
|
||||
for (int x = -20; x < 21; x++) {
|
||||
for (int z = -20; z < 21; z++) {
|
||||
for (int y = -10; y < 0; y++) {
|
||||
areaBatch.setBlock(x, y, z, Block.WHITE_WOOL);
|
||||
}
|
||||
}
|
||||
}
|
||||
areaBatch.setBlock(2, 0, 0, Block.WATER);
|
||||
areaBatch.setBlock(3, 0, 0, Block.WATER);
|
||||
areaBatch.setBlock(2, 0, 1, Block.WATER);
|
||||
areaBatch.setBlock(3, 0, 1, Block.WATER);
|
||||
areaBatch.setBlock(5, 1, 0, Block.WATER);
|
||||
areaBatch.setBlock(6, 1, 0, Block.WATER);
|
||||
areaBatch.setBlock(5, 1, 1, Block.WATER);
|
||||
areaBatch.setBlock(6, 1, 1, Block.WATER);
|
||||
areaBatch.setBlock(8, 1, 1, Block.WATER.withProperty("level", "0"));
|
||||
areaBatch.setBlock(10, 1, 1, Block.WATER.withProperty("level", "1"));
|
||||
areaBatch.setBlock(8, 1, 3, Block.WATER.withProperty("level", "2"));
|
||||
areaBatch.setBlock(10, 1, 3, Block.WATER.withProperty("level", "3"));
|
||||
areaBatch.setBlock(8, 1, 5, Block.WATER.withProperty("level", "4"));
|
||||
areaBatch.setBlock(10, 1, 5, Block.WATER.withProperty("level", "5"));
|
||||
areaBatch.setBlock(8, 1, 7, Block.WATER.withProperty("level", "6"));
|
||||
areaBatch.setBlock(10, 1, 7, Block.WATER.withProperty("level", "7"));
|
||||
areaBatch.setBlock(8, 1, 9, Block.WATER.withProperty("level", "8"));
|
||||
areaBatch.setBlock(10, 1, 9, Block.WATER.withProperty("level", "13"));
|
||||
for (int x = -3; x < 0; x++) {
|
||||
for (int z = -3; z < 0; z++) {
|
||||
for (int y = 0; y < 4; y++) {
|
||||
areaBatch.setBlock(x, y, z, Block.WATER);
|
||||
}
|
||||
}
|
||||
}
|
||||
for (int x = -9; x < -6; x++) {
|
||||
for (int z = -9; z < -6; z++) {
|
||||
for (int y = 0; y < 3; y++) {
|
||||
areaBatch.setBlock(x, y, z, Block.BAMBOO);
|
||||
}
|
||||
areaBatch.setBlock(x, 3, z, Block.BAMBOO_SAPLING);
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
addConditionalSyntax((sender, commandString) -> sender instanceof Player, (sender, context) -> {
|
||||
Player player = (Player) sender;
|
||||
areaBatch.apply(player.getInstance(), player.getPosition(), null);
|
||||
}, ArgumentType.Literal("placeArea"));
|
||||
|
||||
addConditionalSyntax((sender, commandString) -> sender instanceof Player, (sender, context) -> {
|
||||
Player player = (Player) sender;
|
||||
boolean state = context.get("state");
|
||||
player.setInstantBreak(state);
|
||||
}, ArgumentType.Literal("instabreak"), ArgumentType.Boolean("state"));
|
||||
|
||||
addConditionalSyntax((sender, commandString) -> sender instanceof Player, (sender, context) -> {
|
||||
Player player = (Player) sender;
|
||||
player.setGameMode(GameMode.SURVIVAL);
|
||||
player.getInventory().clear();
|
||||
giveItems(player);
|
||||
areaBatch.apply(player.getInstance(), player.getPosition(), null);
|
||||
});
|
||||
}
|
||||
|
||||
private void giveItems(Player player) {
|
||||
List<ItemStack> items = new ArrayList<>();
|
||||
items.add(ItemStack.builder(Material.SHEARS).set(ItemComponent.ENCHANTMENTS, EnchantmentList.EMPTY.with(Enchantment.EFFICIENCY, 5)).build());
|
||||
items.add(ItemStack.builder(Material.WHITE_WOOL).amount(64).build());
|
||||
items.add(ItemStack.builder(Material.STONE).amount(64).build());
|
||||
items.add(ItemStack.of(Material.DIAMOND_SWORD));
|
||||
items.add(ItemStack.of(Material.DIAMOND_PICKAXE));
|
||||
for (ItemStack item : items) {
|
||||
player.getInventory().addItemStack(item);
|
||||
}
|
||||
}
|
||||
}
|
@ -87,6 +87,11 @@ public final class Tag implements ProtocolObject, Keyed {
|
||||
return name;
|
||||
}
|
||||
|
||||
@Override
|
||||
public String toString() {
|
||||
return "#" + name.asString();
|
||||
}
|
||||
|
||||
public enum BasicType {
|
||||
BLOCKS("minecraft:block", Registry.Resource.BLOCK_TAGS,
|
||||
name -> Objects.requireNonNull(Block.fromNamespaceId(name)).id()),
|
||||
|
@ -3,6 +3,7 @@ package net.minestom.server.item.component;
|
||||
import net.kyori.adventure.nbt.ByteBinaryTag;
|
||||
import net.kyori.adventure.nbt.CompoundBinaryTag;
|
||||
import net.kyori.adventure.nbt.FloatBinaryTag;
|
||||
import net.minestom.server.instance.block.Block;
|
||||
import net.minestom.server.instance.block.predicate.BlockTypeFilter;
|
||||
import net.minestom.server.network.NetworkBuffer;
|
||||
import net.minestom.server.network.NetworkBufferTemplate;
|
||||
@ -52,4 +53,24 @@ public record Tool(@NotNull List<Rule> rules, float defaultMiningSpeed, int dama
|
||||
}
|
||||
);
|
||||
}
|
||||
|
||||
public boolean isCorrectForDrops(@NotNull Block block) {
|
||||
for (Rule rule : rules) {
|
||||
if (rule.correctForDrops != null && rule.blocks.test(block)) {
|
||||
// First matching rule is picked, other rules are ignored
|
||||
return rule.correctForDrops;
|
||||
}
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
public float getSpeed(@NotNull Block block) {
|
||||
for (Rule rule : rules) {
|
||||
if (rule.speed != null && rule.blocks.test(block)) {
|
||||
// First matching rule is picked, other rules are ignored
|
||||
return rule.speed;
|
||||
}
|
||||
}
|
||||
return defaultMiningSpeed;
|
||||
}
|
||||
}
|
||||
|
@ -24,6 +24,7 @@ import net.minestom.server.item.component.BlockPredicates;
|
||||
import net.minestom.server.network.packet.client.play.ClientPlayerDiggingPacket;
|
||||
import net.minestom.server.network.packet.server.play.AcknowledgeBlockChangePacket;
|
||||
import net.minestom.server.network.packet.server.play.BlockEntityDataPacket;
|
||||
import net.minestom.server.utils.block.BlockBreakCalculation;
|
||||
import net.minestom.server.utils.block.BlockUtils;
|
||||
import org.jetbrains.annotations.NotNull;
|
||||
|
||||
@ -70,31 +71,26 @@ public final class PlayerDiggingListener {
|
||||
|
||||
private static DiggingResult startDigging(Player player, Instance instance, Point blockPosition, BlockFace blockFace) {
|
||||
final Block block = instance.getBlock(blockPosition);
|
||||
final GameMode gameMode = player.getGameMode();
|
||||
|
||||
// Prevent spectators and check players in adventure mode
|
||||
if (shouldPreventBreaking(player, block)) {
|
||||
return new DiggingResult(block, false);
|
||||
}
|
||||
|
||||
if (gameMode == GameMode.CREATIVE) {
|
||||
return breakBlock(instance, player, blockPosition, block, blockFace);
|
||||
}
|
||||
|
||||
// Survival digging
|
||||
// FIXME: verify mineable tag and enchantment
|
||||
final boolean instantBreak = player.isInstantBreak() || block.registry().hardness() == 0;
|
||||
final int breakTicks = BlockBreakCalculation.breakTicks(block, player);
|
||||
final boolean instantBreak = breakTicks == 0;
|
||||
if (!instantBreak) {
|
||||
PlayerStartDiggingEvent playerStartDiggingEvent = new PlayerStartDiggingEvent(player, block, new BlockVec(blockPosition), blockFace);
|
||||
EventDispatcher.call(playerStartDiggingEvent);
|
||||
return new DiggingResult(block, !playerStartDiggingEvent.isCancelled());
|
||||
}
|
||||
// Client only send a single STARTED_DIGGING when insta-break is enabled
|
||||
// Client only sends a single STARTED_DIGGING when insta-break is enabled
|
||||
return breakBlock(instance, player, blockPosition, block, blockFace);
|
||||
}
|
||||
|
||||
private static DiggingResult cancelDigging(Player player, Instance instance, Point blockPosition) {
|
||||
final Block block = instance.getBlock(blockPosition);
|
||||
|
||||
PlayerCancelDiggingEvent playerCancelDiggingEvent = new PlayerCancelDiggingEvent(player, block, new BlockVec(blockPosition));
|
||||
EventDispatcher.call(playerCancelDiggingEvent);
|
||||
return new DiggingResult(block, true);
|
||||
@ -107,6 +103,17 @@ public final class PlayerDiggingListener {
|
||||
return new DiggingResult(block, false);
|
||||
}
|
||||
|
||||
final int breakTicks = BlockBreakCalculation.breakTicks(block, player);
|
||||
// Realistically shouldn't happen, but a hacked client can send any packet, also illegal ones
|
||||
// If the block is unbreakable, prevent a hacked client from breaking it!
|
||||
if (breakTicks == BlockBreakCalculation.UNBREAKABLE) {
|
||||
PlayerCancelDiggingEvent playerCancelDiggingEvent = new PlayerCancelDiggingEvent(player, block, new BlockVec(blockPosition));
|
||||
EventDispatcher.call(playerCancelDiggingEvent);
|
||||
return new DiggingResult(block, false);
|
||||
}
|
||||
// TODO maybe add a check if the player has spent enough time mining the block.
|
||||
// a hacked client could send START_DIGGING and FINISH_DIGGING to instamine any block
|
||||
|
||||
PlayerFinishDiggingEvent playerFinishDiggingEvent = new PlayerFinishDiggingEvent(player, block, new BlockVec(blockPosition));
|
||||
EventDispatcher.call(playerFinishDiggingEvent);
|
||||
|
||||
|
@ -263,6 +263,7 @@ public final class Registry {
|
||||
private final boolean solid;
|
||||
private final boolean liquid;
|
||||
private final boolean occludes;
|
||||
private final boolean requiresTool;
|
||||
private final int lightEmission;
|
||||
private final boolean replaceable;
|
||||
private final String blockEntity;
|
||||
@ -288,6 +289,7 @@ public final class Registry {
|
||||
this.solid = main.getBoolean("solid");
|
||||
this.liquid = main.getBoolean("liquid", false);
|
||||
this.occludes = main.getBoolean("occludes", true);
|
||||
this.requiresTool = main.getBoolean("requiresTool", true);
|
||||
this.lightEmission = main.getInt("lightEmission", 0);
|
||||
this.replaceable = main.getBoolean("replaceable", false);
|
||||
{
|
||||
@ -365,6 +367,10 @@ public final class Registry {
|
||||
return occludes;
|
||||
}
|
||||
|
||||
public boolean requiresTool() {
|
||||
return requiresTool;
|
||||
}
|
||||
|
||||
public int lightEmission() {
|
||||
return lightEmission;
|
||||
}
|
||||
|
@ -0,0 +1,185 @@
|
||||
package net.minestom.server.utils.block;
|
||||
|
||||
import net.minestom.server.MinecraftServer;
|
||||
import net.minestom.server.coordinate.Pos;
|
||||
import net.minestom.server.entity.GameMode;
|
||||
import net.minestom.server.entity.Player;
|
||||
import net.minestom.server.entity.attribute.Attribute;
|
||||
import net.minestom.server.gamedata.tags.Tag;
|
||||
import net.minestom.server.gamedata.tags.Tag.BasicType;
|
||||
import net.minestom.server.instance.Instance;
|
||||
import net.minestom.server.instance.block.Block;
|
||||
import net.minestom.server.item.ItemComponent;
|
||||
import net.minestom.server.item.ItemStack;
|
||||
import net.minestom.server.item.component.Tool;
|
||||
import net.minestom.server.potion.PotionEffect;
|
||||
import net.minestom.server.registry.Registry;
|
||||
import org.jetbrains.annotations.NotNull;
|
||||
import org.jetbrains.annotations.Nullable;
|
||||
|
||||
import java.util.Objects;
|
||||
|
||||
public class BlockBreakCalculation {
|
||||
|
||||
public static final int UNBREAKABLE = -1;
|
||||
private static final Tag WATER_TAG = Objects.requireNonNull(MinecraftServer.getTagManager().getTag(BasicType.FLUIDS, "minecraft:water"));
|
||||
// The vanilla client checks for bamboo breaking speed with item instanceof SwordItem.
|
||||
// We could either check all sword ID's, or the sword tag.
|
||||
// Since tags are immutable, checking the tag seems easier to understand
|
||||
private static final Tag SWORD_TAG = Objects.requireNonNull(MinecraftServer.getTagManager().getTag(BasicType.ITEMS, "minecraft:swords"));
|
||||
|
||||
/**
|
||||
* Calculates the block break time in ticks
|
||||
*
|
||||
* @return the block break time in ticks, -1 if the block is unbreakable
|
||||
*/
|
||||
public static int breakTicks(@NotNull Block block, @NotNull Player player) {
|
||||
if (player.getGameMode() == GameMode.CREATIVE) {
|
||||
// Creative can always break blocks instantly
|
||||
return 0;
|
||||
}
|
||||
// Taken from minecraft wiki Breaking#Calculation
|
||||
// https://minecraft.wiki/w/Breaking#Calculation
|
||||
// More information to mimic calculations taken from minecraft's source
|
||||
Registry.BlockEntry registry = block.registry();
|
||||
double blockHardness = registry.hardness();
|
||||
if (blockHardness == -1) {
|
||||
// Bedrock, barrier, and unbreakable blocks
|
||||
return UNBREAKABLE;
|
||||
}
|
||||
ItemStack item = player.getItemInMainHand();
|
||||
// Bamboo is hard-coded in client
|
||||
if (block.id() == Block.BAMBOO.id() || block.id() == Block.BAMBOO_SAPLING.id()) {
|
||||
if (SWORD_TAG.contains(item.material().namespace())) {
|
||||
return 0;
|
||||
}
|
||||
}
|
||||
Tool tool = item.get(ItemComponent.TOOL);
|
||||
boolean isBestTool = canBreakBlock(tool, block);
|
||||
float speedMultiplier;
|
||||
|
||||
if (isBestTool) {
|
||||
speedMultiplier = getMiningSpeed(tool, block);
|
||||
|
||||
// wiki seems to be incorrect here, taken from minecraft's code
|
||||
if (speedMultiplier > 1F) {
|
||||
// since data driven enchantments efficiency uses the PLAYER_MINING_EFFICIENCY attribute
|
||||
// If someone wants faster tools, they have to use player attributes or the TOOL component
|
||||
speedMultiplier += (float) player.getAttributeValue(Attribute.PLAYER_MINING_EFFICIENCY);
|
||||
}
|
||||
} else {
|
||||
speedMultiplier = 1;
|
||||
}
|
||||
|
||||
if (player.hasEffect(PotionEffect.HASTE) || player.hasEffect(PotionEffect.CONDUIT_POWER)) {
|
||||
// Yes, conduit power is same as haste. I also had to go confirm, because I couldn't believe it
|
||||
speedMultiplier *= getHasteMultiplier(player);
|
||||
}
|
||||
|
||||
if (player.hasEffect(PotionEffect.MINING_FATIGUE)) {
|
||||
speedMultiplier *= getMiningFatigueMultiplier(player);
|
||||
}
|
||||
|
||||
speedMultiplier *= (float) player.getAttributeValue(Attribute.PLAYER_BLOCK_BREAK_SPEED);
|
||||
|
||||
if (isInWater(player)) {
|
||||
speedMultiplier *= (float) player.getAttributeValue(Attribute.PLAYER_SUBMERGED_MINING_SPEED);
|
||||
}
|
||||
|
||||
if (!player.isOnGround()) {
|
||||
speedMultiplier /= 5;
|
||||
}
|
||||
|
||||
double damage = speedMultiplier / blockHardness;
|
||||
|
||||
if (isBestTool) {
|
||||
damage /= 30;
|
||||
} else {
|
||||
damage /= 100;
|
||||
}
|
||||
|
||||
if (damage >= 1) {
|
||||
// Instant breaking
|
||||
return 0;
|
||||
}
|
||||
|
||||
return (int) Math.ceil(1 / damage);
|
||||
}
|
||||
|
||||
private static boolean isInWater(@NotNull Player player) {
|
||||
Pos pos = player.getPosition();
|
||||
Instance instance = player.getInstance();
|
||||
double eyeY = pos.y() + player.getEyeHeight();
|
||||
int x = pos.blockX();
|
||||
int y = (int) Math.floor(eyeY);
|
||||
int z = pos.blockZ();
|
||||
Pos eye = player.getPosition().add(0, player.getEyeHeight(), 0);
|
||||
Block block = instance.getBlock(eye);
|
||||
|
||||
if (!WATER_TAG.contains(block.namespace())) {
|
||||
return false;
|
||||
}
|
||||
float fluidHeight = getFluidHeight(player.getInstance(), x, y, z, block);
|
||||
return eyeY < y + fluidHeight;
|
||||
}
|
||||
|
||||
private static float getFluidHeight(Instance instance, int x, int y, int z, Block block) {
|
||||
Block blockAbove = instance.getBlock(x, y + 1, z);
|
||||
if (blockAbove.id() == block.id()) {
|
||||
// Full block if block above is same type
|
||||
return 1F;
|
||||
}
|
||||
// We gotta be extra careful, someone could modify properties of the block!
|
||||
String levelString = block.getProperty("level");
|
||||
if (levelString == null) {
|
||||
// Something is weird, return a full block
|
||||
return 1F;
|
||||
}
|
||||
|
||||
int level;
|
||||
try {
|
||||
level = Integer.parseInt(levelString);
|
||||
} catch (Throwable ignored) {
|
||||
return 1;
|
||||
}
|
||||
if (level >= 8) {
|
||||
// These levels are as high as source blocks, but are for flowing water
|
||||
// Set the level to 0 for full source block calculation
|
||||
level = 0;
|
||||
}
|
||||
return (8 - level) / 9F;
|
||||
}
|
||||
|
||||
private static float getMiningFatigueMultiplier(@NotNull Player player) {
|
||||
int level = player.getEffectLevel(PotionEffect.MINING_FATIGUE) + 1;
|
||||
// Use switch to avoid expensive Math.pow
|
||||
return switch (level) { // 0.3 ^ min(level, 4)
|
||||
case 0 -> 0;
|
||||
case 1 -> 0.3F; // 0.3 ^ 1
|
||||
case 2 -> 0.09F; // 0.3 ^ 2
|
||||
case 3 -> 0.027F; // 0.3 ^ 3
|
||||
default -> 0.0081F; // 0.3 ^ 4
|
||||
};
|
||||
}
|
||||
|
||||
private static float getHasteMultiplier(@NotNull Player player) {
|
||||
// Add 1 to potion level for correct calculation
|
||||
float level = Math.max(player.getEffectLevel(PotionEffect.HASTE), player.getEffectLevel(PotionEffect.CONDUIT_POWER)) + 1;
|
||||
return (1F + 0.2F * level);
|
||||
}
|
||||
|
||||
private static float getMiningSpeed(@Nullable Tool tool, @NotNull Block block) {
|
||||
if (tool == null) {
|
||||
return 1;
|
||||
}
|
||||
return tool.getSpeed(block);
|
||||
}
|
||||
|
||||
private static boolean canBreakBlock(@Nullable Tool tool, @NotNull Block block) {
|
||||
return !block.registry().requiresTool() || isEffective(tool, block);
|
||||
}
|
||||
|
||||
private static boolean isEffective(@Nullable Tool tool, @NotNull Block block) {
|
||||
return tool != null && tool.isCorrectForDrops(block);
|
||||
}
|
||||
}
|
@ -0,0 +1,133 @@
|
||||
package net.minestom.server.utils.block;
|
||||
|
||||
import net.minestom.server.coordinate.Pos;
|
||||
import net.minestom.server.entity.Player;
|
||||
import net.minestom.server.entity.attribute.Attribute;
|
||||
import net.minestom.server.instance.block.Block;
|
||||
import net.minestom.server.item.ItemStack;
|
||||
import net.minestom.server.item.Material;
|
||||
import net.minestom.testing.Env;
|
||||
import net.minestom.testing.EnvTest;
|
||||
import org.junit.jupiter.api.BeforeEach;
|
||||
import org.junit.jupiter.api.Test;
|
||||
|
||||
import static net.minestom.server.utils.block.BlockBreakCalculation.breakTicks;
|
||||
import static org.junit.jupiter.api.Assertions.assertEquals;
|
||||
import static org.junit.jupiter.api.Assertions.assertTrue;
|
||||
|
||||
@EnvTest
|
||||
public class BlockBreakCalculationTest {
|
||||
private Player player;
|
||||
private Runnable assertInstabreak;
|
||||
private Runnable assertNotQuiteInstabreak;
|
||||
|
||||
@Test
|
||||
public void testWool() {
|
||||
player.setItemInMainHand(ItemStack.AIR);
|
||||
assertInstabreak = this::assertWoolInstabreak;
|
||||
assertNotQuiteInstabreak = this::assertWoolNotQuiteInstabreak;
|
||||
assertBreak(24, -1, -1, -1, -1);
|
||||
}
|
||||
|
||||
@Test
|
||||
public void testWoolWithShears() {
|
||||
player.setItemInMainHand(ItemStack.of(Material.SHEARS));
|
||||
assertInstabreak = this::assertWoolInstabreak;
|
||||
assertNotQuiteInstabreak = this::assertWoolNotQuiteInstabreak;
|
||||
assertBreak(4.8, 19, 115, 115, 595);
|
||||
}
|
||||
|
||||
@Test
|
||||
public void testStone() {
|
||||
player.setItemInMainHand(ItemStack.AIR);
|
||||
assertInstabreak = this::assertStoneInstabreak;
|
||||
assertNotQuiteInstabreak = this::assertStoneNotQuiteInstabreak;
|
||||
assertBreak(150, -1, -1, -1, -1);
|
||||
}
|
||||
|
||||
@Test
|
||||
public void testStoneWithDiamondPickaxe() {
|
||||
player.setItemInMainHand(ItemStack.of(Material.DIAMOND_PICKAXE));
|
||||
assertInstabreak = this::assertStoneInstabreak;
|
||||
assertNotQuiteInstabreak = this::assertStoneNotQuiteInstabreak;
|
||||
assertBreak(5.625, 37, 217, 217, -1);
|
||||
}
|
||||
|
||||
@BeforeEach
|
||||
void setupPlayer(Env env) {
|
||||
final var instance = env.createFlatInstance();
|
||||
player = env.createPlayer(instance, new Pos(0, 40, 0));
|
||||
player.refreshOnGround(true);
|
||||
}
|
||||
|
||||
private void assertBreak(double instantBreakSpeed, double efficiency, double efficiencyNotOnGround, double efficiencyInWater, double efficiencyNotOnGroundInWater) {
|
||||
assertBreakSpeed(instantBreakSpeed);
|
||||
assertBreakEfficiency(efficiency);
|
||||
player.refreshOnGround(false);
|
||||
assertBreakSpeed(instantBreakSpeed * 5);
|
||||
assertBreakEfficiency(efficiencyNotOnGround);
|
||||
submerge();
|
||||
player.refreshOnGround(true);
|
||||
assertBreakSpeed(instantBreakSpeed * 5);
|
||||
assertBreakEfficiency(efficiencyInWater);
|
||||
player.refreshOnGround(false);
|
||||
assertBreakSpeed(instantBreakSpeed * 5 * 5);
|
||||
assertBreakEfficiency(efficiencyNotOnGroundInWater);
|
||||
}
|
||||
|
||||
private void assertBreakEfficiency(double instantBreakEfficiency) {
|
||||
if (instantBreakEfficiency == -1) return;
|
||||
resetBreakSpeed();
|
||||
updateEfficiency(instantBreakEfficiency);
|
||||
assertInstabreak.run();
|
||||
updateEfficiency(instantBreakEfficiency - 0.001);
|
||||
assertNotQuiteInstabreak.run();
|
||||
}
|
||||
|
||||
private void assertBreakSpeed(double instantBreakSpeed) {
|
||||
if (instantBreakSpeed > Attribute.PLAYER_BLOCK_BREAK_SPEED.maxValue()) return;
|
||||
resetBreakEfficiency();
|
||||
updateBreakSpeed(instantBreakSpeed);
|
||||
assertInstabreak.run();
|
||||
updateBreakSpeed(instantBreakSpeed - 0.001);
|
||||
assertNotQuiteInstabreak.run();
|
||||
}
|
||||
|
||||
private void assertWoolInstabreak() {
|
||||
assertEquals(0, breakTicks(Block.WHITE_WOOL, player));
|
||||
assertEquals(0, breakTicks(Block.BLACK_WOOL, player));
|
||||
}
|
||||
|
||||
private void assertWoolNotQuiteInstabreak() {
|
||||
assertTrue(breakTicks(Block.WHITE_WOOL, player) > 0);
|
||||
assertTrue(breakTicks(Block.BLACK_WOOL, player) > 0);
|
||||
}
|
||||
|
||||
private void assertStoneInstabreak() {
|
||||
assertEquals(0, breakTicks(Block.STONE, player));
|
||||
}
|
||||
|
||||
private void assertStoneNotQuiteInstabreak() {
|
||||
assertTrue(breakTicks(Block.STONE, player) > 0);
|
||||
}
|
||||
|
||||
private void submerge() {
|
||||
player.getInstance().setBlock(player.getPosition().add(0, player.getEyeHeight(), 0), Block.WATER);
|
||||
}
|
||||
|
||||
private void resetBreakSpeed() {
|
||||
player.getAttribute(Attribute.PLAYER_BLOCK_BREAK_SPEED).setBaseValue(Attribute.PLAYER_BLOCK_BREAK_SPEED.defaultValue());
|
||||
}
|
||||
|
||||
private void resetBreakEfficiency() {
|
||||
player.getAttribute(Attribute.PLAYER_MINING_EFFICIENCY).setBaseValue(Attribute.PLAYER_MINING_EFFICIENCY.defaultValue());
|
||||
}
|
||||
|
||||
private void updateBreakSpeed(double speed) {
|
||||
player.getAttribute(Attribute.PLAYER_BLOCK_BREAK_SPEED).setBaseValue(speed);
|
||||
}
|
||||
|
||||
private void updateEfficiency(double efficiency) {
|
||||
player.getAttribute(Attribute.PLAYER_MINING_EFFICIENCY).setBaseValue(efficiency);
|
||||
}
|
||||
}
|
Loading…
Reference in New Issue
Block a user