diff --git a/paper-api/src/main/java/io/papermc/paper/event/entity/EntityHarvestBlockEvent.java b/paper-api/src/main/java/io/papermc/paper/event/entity/EntityHarvestBlockEvent.java
new file mode 100644
index 000000000000..ed1aa386bccc
--- /dev/null
+++ b/paper-api/src/main/java/io/papermc/paper/event/entity/EntityHarvestBlockEvent.java
@@ -0,0 +1,91 @@
+package io.papermc.paper.event.entity;
+
+import org.bukkit.block.Block;
+import org.bukkit.entity.Entity;
+import org.bukkit.event.Cancellable;
+import org.bukkit.event.HandlerList;
+import org.bukkit.event.entity.EntityEvent;
+import org.bukkit.inventory.ItemStack;
+import org.jetbrains.annotations.ApiStatus;
+import org.jspecify.annotations.NullMarked;
+import java.util.Collections;
+import java.util.List;
+
+/**
+ * This event is called whenever an entity harvests a block.
+ *
+ * For the player case please use {@link org.bukkit.event.player.PlayerHarvestBlockEvent}
+ *
+ * A 'harvest' is when a block drops an item (usually some sort of crop) and
+ * changes state, but is not broken in order to drop the item.
+ *
+ * This event is not called for when a block is broken, to handle that, listen
+ * for {@link org.bukkit.event.block.BlockBreakEvent} and
+ * {@link org.bukkit.event.block.BlockDropItemEvent}.
+ */
+@NullMarked
+public class EntityHarvestBlockEvent extends EntityEvent implements Cancellable {
+
+ private static final HandlerList HANDLER_LIST = new HandlerList();
+
+ private boolean cancel = false;
+ private final Block harvestedBlock;
+ private List itemsHarvested;
+
+ @ApiStatus.Internal
+ public EntityHarvestBlockEvent(final Entity entity, final Block harvestedBlock, final List itemsHarvested) {
+ super(entity);
+ this.harvestedBlock = harvestedBlock;
+ this.itemsHarvested = itemsHarvested;
+ }
+
+ /**
+ * Gets the block that is being harvested.
+ *
+ * @return The block that is being harvested
+ */
+ public Block getHarvestedBlock() {
+ return this.harvestedBlock;
+ }
+
+ /**
+ * Gets an immutable list of items that are being harvested from this block.
+ *
+ * @return An immutable list of items that are being harvested from this block
+ * @apiNote {@link org.bukkit.entity.Fox} has a behavior where, if it does not have
+ * an item in its mouth ({@link org.bukkit.inventory.EquipmentSlot#HAND}), it will
+ * take one unit from the first ItemStack in this harvest and put it there.
+ * @apiNote This list contains copy of the items; you need to set the items to the list to change them.
+ */
+ public List getItemsHarvested() {
+ return Collections.unmodifiableList(this.itemsHarvested);
+ }
+
+ /**
+ * Sets the items that are being harvested from this block.
+ *
+ * @param itemsHarvested A list of items that are being harvested from this block
+ */
+ public void setItemsHarvested(List itemsHarvested) {
+ this.itemsHarvested = itemsHarvested;
+ }
+
+ @Override
+ public boolean isCancelled() {
+ return this.cancel;
+ }
+
+ @Override
+ public void setCancelled(boolean cancel) {
+ this.cancel = cancel;
+ }
+
+ @Override
+ public HandlerList getHandlers() {
+ return HANDLER_LIST;
+ }
+
+ public static HandlerList getHandlerList() {
+ return HANDLER_LIST;
+ }
+}
diff --git a/paper-api/src/main/java/org/bukkit/event/player/PlayerHarvestBlockEvent.java b/paper-api/src/main/java/org/bukkit/event/player/PlayerHarvestBlockEvent.java
index a7b580033d6c..ce25d2eaa4ce 100644
--- a/paper-api/src/main/java/org/bukkit/event/player/PlayerHarvestBlockEvent.java
+++ b/paper-api/src/main/java/org/bukkit/event/player/PlayerHarvestBlockEvent.java
@@ -5,6 +5,7 @@
import org.bukkit.entity.Player;
import org.bukkit.event.Cancellable;
import org.bukkit.event.HandlerList;
+import io.papermc.paper.event.entity.EntityHarvestBlockEvent;
import org.bukkit.inventory.EquipmentSlot;
import org.bukkit.inventory.ItemStack;
import org.jetbrains.annotations.ApiStatus;
@@ -13,6 +14,8 @@
/**
* This event is called whenever a player harvests a block.
*
+ * For cases involving entities, please use {@link EntityHarvestBlockEvent}.
+ *
* A 'harvest' is when a block drops an item (usually some sort of crop) and
* changes state, but is not broken in order to drop the item.
*
@@ -67,7 +70,7 @@ public EquipmentSlot getHand() {
/**
* Gets a list of items that are being harvested from this block.
*
- * @return A list of items that are being harvested from this block
+ * @return A mutable list of items that are being harvested from this block
*/
@NotNull
public List getItemsHarvested() {
diff --git a/paper-server/patches/sources/net/minecraft/world/entity/animal/Fox.java.patch b/paper-server/patches/sources/net/minecraft/world/entity/animal/Fox.java.patch
index f47760008099..4e3a5beb01b7 100644
--- a/paper-server/patches/sources/net/minecraft/world/entity/animal/Fox.java.patch
+++ b/paper-server/patches/sources/net/minecraft/world/entity/animal/Fox.java.patch
@@ -146,11 +146,39 @@
);
}
}
-@@ -955,6 +_,7 @@
+@@ -955,15 +_,32 @@
private void pickSweetBerries(BlockState state) {
int ageValue = state.getValue(SweetBerryBushBlock.AGE);
state.setValue(SweetBerryBushBlock.AGE, 1);
+ if (!org.bukkit.craftbukkit.event.CraftEventFactory.callEntityChangeBlockEvent(Fox.this, this.blockPos, state.setValue(SweetBerryBushBlock.AGE, 1))) return; // CraftBukkit - call EntityChangeBlockEvent
int i = 1 + Fox.this.level().random.nextInt(2) + (ageValue == 3 ? 1 : 0);
ItemStack itemBySlot = Fox.this.getItemBySlot(EquipmentSlot.MAINHAND);
++ // Paper start - call HarvestBlockEvent for the item dropped
++ ItemStack itemHarvest = new ItemStack(Items.SWEET_BERRIES, i);
++ io.papermc.paper.event.entity.EntityHarvestBlockEvent event = org.bukkit.craftbukkit.event.CraftEventFactory.callEntityHarvestBlockEvent(Fox.this.level(), this.blockPos, Fox.this, java.util.Collections.singletonList(itemHarvest));
++ java.util.ArrayList itemsHarvested = new java.util.ArrayList<>(event.getItemsHarvested());
++ if (event.isCancelled()) {
++ return;
++ }
if (itemBySlot.isEmpty()) {
+- Fox.this.setItemSlot(EquipmentSlot.MAINHAND, new ItemStack(Items.SWEET_BERRIES));
++ if (!itemsHarvested.isEmpty()) {
++ org.bukkit.inventory.ItemStack firstItemForSlot = itemsHarvested.getFirst().clone();
++ if (firstItemForSlot.getAmount() == 1) {
++ itemsHarvested.removeFirst();
++ } else {
++ itemsHarvested.set(0, firstItemForSlot.subtract());
++ }
++ Fox.this.setItemSlot(EquipmentSlot.MAINHAND, org.bukkit.craftbukkit.inventory.CraftItemStack.asNMSCopy(firstItemForSlot.asOne()));
++ }
++ // Paper end
+ i--;
+ }
+
+- if (i > 0) {
+- Block.popResource(Fox.this.level(), this.blockPos, new ItemStack(Items.SWEET_BERRIES, i));
++ for (org.bukkit.inventory.ItemStack itemStack : itemsHarvested) { // Paper - handle harvest items
++ Block.popResource(Fox.this.level(), this.blockPos, org.bukkit.craftbukkit.inventory.CraftItemStack.asNMSCopy(itemStack)); // Paper
+ }
+
+ Fox.this.playSound(SoundEvents.SWEET_BERRY_BUSH_PICK_BERRIES, 1.0F, 1.0F);
diff --git a/paper-server/patches/sources/net/minecraft/world/level/block/CaveVines.java.patch b/paper-server/patches/sources/net/minecraft/world/level/block/CaveVines.java.patch
index deaefa83917c..3c108887c990 100644
--- a/paper-server/patches/sources/net/minecraft/world/level/block/CaveVines.java.patch
+++ b/paper-server/patches/sources/net/minecraft/world/level/block/CaveVines.java.patch
@@ -13,14 +13,14 @@
Block.dropFromBlockInteractLootTable(
serverLevel,
BuiltInLootTables.HARVEST_CAVE_VINE,
-@@ -30,8 +_,25 @@
+@@ -30,8 +_,29 @@
level.getBlockEntity(pos),
null,
entity,
- (serverLevel1, itemStack) -> Block.popResource(serverLevel1, pos, itemStack)
+ (serverLevel1, itemStack) -> drops.add(itemStack) // Paper - call player harvest block event - store drops from loottable
);
-+ // Paper start - call player harvest block event
++ // Paper start - call harvest block event
+ if (entity instanceof net.minecraft.world.entity.player.Player player) {
+ org.bukkit.event.player.PlayerHarvestBlockEvent event = org.bukkit.craftbukkit.event.CraftEventFactory.callPlayerHarvestBlockEvent(
+ level, pos, player, net.minecraft.world.InteractionHand.MAIN_HAND, drops
@@ -32,11 +32,15 @@
+ Block.popResource(level, pos, org.bukkit.craftbukkit.inventory.CraftItemStack.asNMSCopy(itemStack));
+ }
+ } else {
-+ for (net.minecraft.world.item.ItemStack itemStack : drops) {
-+ Block.popResource(level, pos, itemStack);
++ io.papermc.paper.event.entity.EntityHarvestBlockEvent event = org.bukkit.craftbukkit.event.CraftEventFactory.callEntityHarvestBlockEvent(level, pos, entity, drops);
++ if (event.isCancelled()) {
++ return InteractionResult.SUCCESS; // We need to return a success either way, because making it PASS or FAIL will result in a bug where cancelling while harvesting w/ block in hand places block
++ }
++ for (org.bukkit.inventory.ItemStack itemStack : event.getItemsHarvested()) {
++ Block.popResource(level, pos, org.bukkit.craftbukkit.inventory.CraftItemStack.asNMSCopy(itemStack));
+ }
+ }
-+ // Paper end - call player harvest block event
++ // Paper end - call harvest block event
float f = Mth.randomBetween(serverLevel.random, 0.8F, 1.2F);
serverLevel.playSound(null, pos, SoundEvents.CAVE_VINES_PICK_BERRIES, SoundSource.BLOCKS, 1.0F, f);
BlockState blockState = state.setValue(BERRIES, false);
diff --git a/paper-server/patches/sources/net/minecraft/world/level/block/ComposterBlock.java.patch b/paper-server/patches/sources/net/minecraft/world/level/block/ComposterBlock.java.patch
index 6be7c6727b2f..51a2a99f6502 100644
--- a/paper-server/patches/sources/net/minecraft/world/level/block/ComposterBlock.java.patch
+++ b/paper-server/patches/sources/net/minecraft/world/level/block/ComposterBlock.java.patch
@@ -33,7 +33,7 @@
stack.shrink(1);
return blockState;
} else {
-@@ -286,6 +_,14 @@
+@@ -286,11 +_,40 @@
}
public static BlockState extractProduce(Entity entity, BlockState state, Level level, BlockPos pos) {
@@ -48,6 +48,34 @@
if (!level.isClientSide()) {
Vec3 vec3 = Vec3.atLowerCornerWithOffset(pos, 0.5, 1.01, 0.5).offsetRandom(level.random, 0.7F);
ItemEntity itemEntity = new ItemEntity(level, vec3.x(), vec3.y(), vec3.z(), new ItemStack(Items.BONE_MEAL));
+- itemEntity.setDefaultPickUpDelay();
+- level.addFreshEntity(itemEntity);
++ // Paper start - call HarvestBlockEvent for the item dropped
++ // itemEntity.setDefaultPickUpDelay(); // used later
++ // level.addFreshEntity(itemEntity); // used later
++ java.util.List itemsHarvested;
++ if (entity instanceof Player player) {
++ org.bukkit.event.player.PlayerHarvestBlockEvent event = org.bukkit.craftbukkit.event.CraftEventFactory.callPlayerHarvestBlockEvent(level, pos, player, InteractionHand.MAIN_HAND, java.util.Collections.singletonList(itemEntity.getItem()));
++ itemsHarvested = event.getItemsHarvested();
++ if (event.isCancelled()) {
++ return state;
++ }
++ } else {
++ io.papermc.paper.event.entity.EntityHarvestBlockEvent event = org.bukkit.craftbukkit.event.CraftEventFactory.callEntityHarvestBlockEvent(level, pos, entity, java.util.Collections.singletonList(itemEntity.getItem()));
++ itemsHarvested = event.getItemsHarvested();
++ if (event.isCancelled()) {
++ return state;
++ }
++ }
++ for (org.bukkit.inventory.ItemStack itemStack : itemsHarvested) {
++ ItemEntity harvestItemEntity = new ItemEntity(level, vec3.x(), vec3.y(), vec3.z(), org.bukkit.craftbukkit.inventory.CraftItemStack.asNMSCopy(itemStack));
++ harvestItemEntity.setDefaultPickUpDelay();
++ level.addFreshEntity(harvestItemEntity);
++ }
++ // Paper end
+ }
+
+ BlockState blockState = empty(entity, state, level, pos);
@@ -305,14 +_,39 @@
return blockState;
}
diff --git a/paper-server/src/main/java/org/bukkit/craftbukkit/event/CraftEventFactory.java b/paper-server/src/main/java/org/bukkit/craftbukkit/event/CraftEventFactory.java
index d5496e5fbb5f..c3c2bd7ba45e 100644
--- a/paper-server/src/main/java/org/bukkit/craftbukkit/event/CraftEventFactory.java
+++ b/paper-server/src/main/java/org/bukkit/craftbukkit/event/CraftEventFactory.java
@@ -10,6 +10,7 @@
import io.papermc.paper.connection.HorriblePlayerLoginEventHack;
import io.papermc.paper.connection.PlayerConnection;
import io.papermc.paper.event.connection.PlayerConnectionValidateLoginEvent;
+import io.papermc.paper.event.entity.EntityHarvestBlockEvent;
import io.papermc.paper.event.entity.ItemTransportingEntityValidateTargetEvent;
import java.util.ArrayList;
import java.util.Collections;
@@ -319,10 +320,21 @@ public static EntityEnterLoveModeEvent callEntityEnterLoveModeEvent(net.minecraf
return entityEnterLoveModeEvent;
}
+ /**
+ * Entity Harvest Block Event
+ */
+ public static EntityHarvestBlockEvent callEntityHarvestBlockEvent(Level world, BlockPos blockposition, net.minecraft.world.entity.Entity who, List itemsToHarvest) {
+ List bukkitItemsToHarvest = itemsToHarvest.stream().map(CraftItemStack::asBukkitCopy).map(org.bukkit.inventory.ItemStack::clone).collect(Collectors.toList());
+ org.bukkit.entity.Entity entity = who.getBukkitEntity();
+ EntityHarvestBlockEvent entityHarvestBlockEvent = new EntityHarvestBlockEvent(entity, CraftBlock.at(world, blockposition), bukkitItemsToHarvest);
+ entityHarvestBlockEvent.callEvent();
+ return entityHarvestBlockEvent;
+ }
+
public static PlayerHarvestBlockEvent callPlayerHarvestBlockEvent(Level world, BlockPos pos, net.minecraft.world.entity.player.Player player, InteractionHand hand, List itemsToHarvest) {
- List bukkitItemsToHarvest = new ArrayList<>(itemsToHarvest.stream().map(CraftItemStack::asBukkitCopy).collect(Collectors.toList()));
+ List bukkitItemsToHarvest = itemsToHarvest.stream().map(CraftItemStack::asBukkitCopy).collect(Collectors.toList());
PlayerHarvestBlockEvent playerHarvestBlockEvent = new PlayerHarvestBlockEvent((Player) player.getBukkitEntity(), CraftBlock.at(world, pos), CraftEquipmentSlot.getHand(hand), bukkitItemsToHarvest);
- Bukkit.getPluginManager().callEvent(playerHarvestBlockEvent);
+ playerHarvestBlockEvent.callEvent();
return playerHarvestBlockEvent;
}