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