From c068010b34151ed68073d48573a6169543e5882d Mon Sep 17 00:00:00 2001
From: Ineusia <ineusia@yahoo.com>
Date: Mon, 26 Oct 2020 11:48:06 -0500
Subject: [PATCH] Add Destroy Speed API

Co-authored-by: Jake Potrebic <jake.m.potrebic@gmail.com>
---
 .../attributes/AttributeInstance.java.patch   |  27 ++++
 .../block/data/CraftBlockData.java            |  31 ++++
 .../block/CraftBlockDataDestroySpeedTest.java | 138 ++++++++++++++++++
 3 files changed, 196 insertions(+)
 create mode 100644 paper-server/patches/sources/net/minecraft/world/entity/ai/attributes/AttributeInstance.java.patch
 create mode 100644 paper-server/src/test/java/io/papermc/paper/block/CraftBlockDataDestroySpeedTest.java

diff --git a/paper-server/patches/sources/net/minecraft/world/entity/ai/attributes/AttributeInstance.java.patch b/paper-server/patches/sources/net/minecraft/world/entity/ai/attributes/AttributeInstance.java.patch
new file mode 100644
index 0000000000..d6d3313220
--- /dev/null
+++ b/paper-server/patches/sources/net/minecraft/world/entity/ai/attributes/AttributeInstance.java.patch
@@ -0,0 +1,27 @@
+--- a/net/minecraft/world/entity/ai/attributes/AttributeInstance.java
++++ b/net/minecraft/world/entity/ai/attributes/AttributeInstance.java
+@@ -153,20 +153,20 @@
+         double d = this.getBaseValue();
+ 
+         for (AttributeModifier attributeModifier : this.getModifiersOrEmpty(AttributeModifier.Operation.ADD_VALUE)) {
+-            d += attributeModifier.amount();
++            d += attributeModifier.amount(); // Paper - destroy speed API - diff on change
+         }
+ 
+         double e = d;
+ 
+         for (AttributeModifier attributeModifier2 : this.getModifiersOrEmpty(AttributeModifier.Operation.ADD_MULTIPLIED_BASE)) {
+-            e += d * attributeModifier2.amount();
++            e += d * attributeModifier2.amount(); // Paper - destroy speed API - diff on change
+         }
+ 
+         for (AttributeModifier attributeModifier3 : this.getModifiersOrEmpty(AttributeModifier.Operation.ADD_MULTIPLIED_TOTAL)) {
+-            e *= 1.0 + attributeModifier3.amount();
++            e *= 1.0 + attributeModifier3.amount(); // Paper - destroy speed API - diff on change
+         }
+ 
+-        return this.attribute.value().sanitizeValue(e);
++        return attribute.value().sanitizeValue(e); // Paper - destroy speed API - diff on change
+     }
+ 
+     private Collection<AttributeModifier> getModifiersOrEmpty(AttributeModifier.Operation operation) {
diff --git a/paper-server/src/main/java/org/bukkit/craftbukkit/block/data/CraftBlockData.java b/paper-server/src/main/java/org/bukkit/craftbukkit/block/data/CraftBlockData.java
index 4982950b13..78071859e9 100644
--- a/paper-server/src/main/java/org/bukkit/craftbukkit/block/data/CraftBlockData.java
+++ b/paper-server/src/main/java/org/bukkit/craftbukkit/block/data/CraftBlockData.java
@@ -730,4 +730,35 @@ public class CraftBlockData implements BlockData {
     public BlockState createBlockState() {
         return CraftBlockStates.getBlockState(this.state, null);
     }
+
+    // Paper start - destroy speed API
+    @Override
+    public float getDestroySpeed(final ItemStack itemStack, final boolean considerEnchants) {
+        net.minecraft.world.item.ItemStack nmsItemStack = CraftItemStack.unwrap(itemStack);
+        float speed = nmsItemStack.getDestroySpeed(this.state);
+        if (speed > 1.0F && considerEnchants) {
+            final net.minecraft.core.Holder<net.minecraft.world.entity.ai.attributes.Attribute> attribute = net.minecraft.world.entity.ai.attributes.Attributes.MINING_EFFICIENCY;
+            // Logic sourced from AttributeInstance#calculateValue
+            final double initialBaseValue = attribute.value().getDefaultValue();
+            final org.apache.commons.lang3.mutable.MutableDouble modifiedBaseValue = new org.apache.commons.lang3.mutable.MutableDouble(initialBaseValue);
+            final org.apache.commons.lang3.mutable.MutableDouble baseValMul = new org.apache.commons.lang3.mutable.MutableDouble(1);
+            final org.apache.commons.lang3.mutable.MutableDouble totalValMul = new org.apache.commons.lang3.mutable.MutableDouble(1);
+
+            net.minecraft.world.item.enchantment.EnchantmentHelper.forEachModifier(
+                nmsItemStack, net.minecraft.world.entity.EquipmentSlot.MAINHAND, (attributeHolder, attributeModifier) -> {
+                    switch (attributeModifier.operation()) {
+                        case ADD_VALUE -> modifiedBaseValue.add(attributeModifier.amount());
+                        case ADD_MULTIPLIED_BASE -> baseValMul.add(attributeModifier.amount());
+                        case ADD_MULTIPLIED_TOTAL -> totalValMul.setValue(totalValMul.doubleValue() * (1D + attributeModifier.amount()));
+                    }
+                }
+            );
+
+            final double actualModifier = modifiedBaseValue.doubleValue() * baseValMul.doubleValue() * totalValMul.doubleValue();
+
+            speed += (float) attribute.value().sanitizeValue(actualModifier);
+        }
+        return speed;
+    }
+    // Paper end - destroy speed API
 }
diff --git a/paper-server/src/test/java/io/papermc/paper/block/CraftBlockDataDestroySpeedTest.java b/paper-server/src/test/java/io/papermc/paper/block/CraftBlockDataDestroySpeedTest.java
new file mode 100644
index 0000000000..32d38205a5
--- /dev/null
+++ b/paper-server/src/test/java/io/papermc/paper/block/CraftBlockDataDestroySpeedTest.java
@@ -0,0 +1,138 @@
+package io.papermc.paper.block;
+
+import java.util.List;
+import java.util.Optional;
+import net.minecraft.core.Holder;
+import net.minecraft.core.HolderSet;
+import net.minecraft.core.component.DataComponentMap;
+import net.minecraft.core.component.DataComponents;
+import net.minecraft.network.chat.Component;
+import net.minecraft.world.entity.EquipmentSlot;
+import net.minecraft.world.entity.EquipmentSlotGroup;
+import net.minecraft.world.entity.ai.attributes.AttributeInstance;
+import net.minecraft.world.entity.ai.attributes.AttributeModifier;
+import net.minecraft.world.entity.ai.attributes.Attributes;
+import net.minecraft.world.item.Items;
+import net.minecraft.world.item.enchantment.Enchantment;
+import net.minecraft.world.item.enchantment.EnchantmentEffectComponents;
+import net.minecraft.world.item.enchantment.EnchantmentHelper;
+import net.minecraft.world.item.enchantment.ItemEnchantments;
+import net.minecraft.world.item.enchantment.LevelBasedValue;
+import net.minecraft.world.item.enchantment.effects.EnchantmentAttributeEffect;
+import net.minecraft.world.level.block.Blocks;
+import net.minecraft.world.level.block.state.BlockState;
+import org.bukkit.craftbukkit.block.data.CraftBlockData;
+import org.bukkit.craftbukkit.inventory.CraftItemStack;
+import org.bukkit.inventory.ItemStack;
+import org.bukkit.support.environment.AllFeatures;
+import org.bukkit.util.Vector;
+import org.jetbrains.annotations.NotNull;
+import org.junit.jupiter.api.Assertions;
+import org.junit.jupiter.api.Test;
+
+import static net.minecraft.resources.ResourceLocation.fromNamespaceAndPath;
+
+/**
+ * CraftBlockData's {@link org.bukkit.craftbukkit.block.data.CraftBlockData#getDestroySpeed(ItemStack, boolean)}
+ * uses a reimplementation of AttributeValue without any map to avoid attribute instance allocation and mutation
+ * for 0 gain.
+ * <p>
+ * This test is responsible for ensuring that said logic emits the expected destroy speed under heavy attribute
+ * modifier use.
+ */
+@AllFeatures
+public class CraftBlockDataDestroySpeedTest {
+
+    @Test
+    public void testCorrectEnchantmentDestroySpeedComputation() {
+        // Construct fake enchantment that has *all and multiple of* operations
+        final Enchantment speedEnchantment = speedEnchantment();
+        final BlockState blockStateToMine = Blocks.STONE.defaultBlockState();
+
+        final ItemEnchantments.Mutable mutable = new ItemEnchantments.Mutable(ItemEnchantments.EMPTY);
+        mutable.set(Holder.direct(speedEnchantment), 1);
+
+        final net.minecraft.world.item.ItemStack itemStack = new net.minecraft.world.item.ItemStack(Items.DIAMOND_PICKAXE);
+        itemStack.set(DataComponents.ENCHANTMENTS, mutable.toImmutable());
+
+        // Compute expected value by running the entire attribute instance chain
+        final AttributeInstance dummyInstance = new AttributeInstance(Attributes.MINING_EFFICIENCY, $ -> {
+        });
+        EnchantmentHelper.forEachModifier(itemStack, EquipmentSlot.MAINHAND, (attributeHolder, attributeModifier) -> {
+            if (attributeHolder.is(Attributes.MINING_EFFICIENCY)) dummyInstance.addTransientModifier(attributeModifier);
+        });
+
+        final double toolSpeed = itemStack.getDestroySpeed(blockStateToMine);
+        final double expectedSpeed = toolSpeed <= 1.0F ? toolSpeed : toolSpeed + dummyInstance.getValue();
+
+        // API stack + computation
+        final CraftItemStack craftMirror = CraftItemStack.asCraftMirror(itemStack);
+        final CraftBlockData data = CraftBlockData.createData(blockStateToMine);
+        final float actualSpeed = data.getDestroySpeed(craftMirror, true);
+
+        Assertions.assertEquals(expectedSpeed, actualSpeed, Vector.getEpsilon());
+    }
+
+    /**
+     * Complex enchantment that holds attribute modifiers for the mining efficiency.
+     * The enchantment holds 2 of each operation to also ensure that such behaviour works correctly.
+     *
+     * @return the enchantment.
+     */
+    private static @NotNull Enchantment speedEnchantment() {
+        return new Enchantment(
+            Component.empty(),
+            new Enchantment.EnchantmentDefinition(
+                HolderSet.empty(),
+                Optional.empty(),
+                0, 0,
+                Enchantment.constantCost(0),
+                Enchantment.constantCost(0),
+                0,
+                List.of(EquipmentSlotGroup.ANY)
+            ),
+            HolderSet.empty(),
+            DataComponentMap.builder()
+                .set(EnchantmentEffectComponents.ATTRIBUTES, List.of(
+                    new EnchantmentAttributeEffect(
+                        fromNamespaceAndPath("paper", "base1"),
+                        Attributes.MINING_EFFICIENCY,
+                        LevelBasedValue.constant(1),
+                        AttributeModifier.Operation.ADD_VALUE
+                    ),
+                    new EnchantmentAttributeEffect(
+                        fromNamespaceAndPath("paper", "base2"),
+                        Attributes.MINING_EFFICIENCY,
+                        LevelBasedValue.perLevel(3),
+                        AttributeModifier.Operation.ADD_VALUE
+                    ),
+                    new EnchantmentAttributeEffect(
+                        fromNamespaceAndPath("paper", "base-mul1"),
+                        Attributes.MINING_EFFICIENCY,
+                        LevelBasedValue.perLevel(7),
+                        AttributeModifier.Operation.ADD_MULTIPLIED_BASE
+                    ),
+                    new EnchantmentAttributeEffect(
+                        fromNamespaceAndPath("paper", "base-mul2"),
+                        Attributes.MINING_EFFICIENCY,
+                        LevelBasedValue.constant(10),
+                        AttributeModifier.Operation.ADD_MULTIPLIED_BASE
+                    ),
+                    new EnchantmentAttributeEffect(
+                        fromNamespaceAndPath("paper", "total-mul1"),
+                        Attributes.MINING_EFFICIENCY,
+                        LevelBasedValue.constant(.2f),
+                        AttributeModifier.Operation.ADD_MULTIPLIED_TOTAL
+                    ),
+                    new EnchantmentAttributeEffect(
+                        fromNamespaceAndPath("paper", "total-mul2"),
+                        Attributes.MINING_EFFICIENCY,
+                        LevelBasedValue.constant(-.5F),
+                        AttributeModifier.Operation.ADD_MULTIPLIED_TOTAL
+                    )
+                ))
+                .build()
+        );
+    }
+
+}