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:
Lorenz Wrobel 2024-10-29 01:03:55 +01:00 committed by Matt Worzala
parent e0939f089b
commit 202889b854
9 changed files with 625 additions and 9 deletions

View File

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

View File

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

View File

@ -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);
}
}
}

View File

@ -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()),

View File

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

View File

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

View File

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

View File

@ -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);
}
}

View File

@ -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);
}
}