diff --git a/paper-api/src/main/java/io/papermc/paper/event/block/BlockDropResourcesEvent.java b/paper-api/src/main/java/io/papermc/paper/event/block/BlockDropResourcesEvent.java new file mode 100644 index 000000000000..d944954105ee --- /dev/null +++ b/paper-api/src/main/java/io/papermc/paper/event/block/BlockDropResourcesEvent.java @@ -0,0 +1,97 @@ +package io.papermc.paper.event.block; + +import org.bukkit.block.Block; +import org.bukkit.block.BlockState; +import org.bukkit.entity.Entity; +import org.bukkit.event.Cancellable; +import org.bukkit.event.HandlerList; +import org.bukkit.event.block.BlockEvent; +import org.bukkit.inventory.ItemStack; +import org.jetbrains.annotations.ApiStatus; +import org.jetbrains.annotations.NotNull; +import org.jetbrains.annotations.Nullable; +import java.util.List; + +/** + Called when a block is about to drop items + +

+ This event will be called once for every block broken, even if multiple blocks are broken simultaneously. + For example, this event will be called twice when breaking a block with a torch on top. +

+

+ This event will also be called when a block is dropping items as a result of being consumed, for example a cake dropping its candle after being eaten. +

+

+ If you do not need the drops of each individual block, use {@link org.bukkit.event.block.BlockBreakEvent}. +

+ */ +public class BlockDropResourcesEvent extends BlockEvent implements Cancellable { + private static final HandlerList HANDLER_LIST = new HandlerList(); + private boolean cancelled; + private final List drops; + private final Entity breaker; + private final ItemStack tool; + private final BlockState state; + + @ApiStatus.Internal + public BlockDropResourcesEvent(final @NotNull Block block, final @NotNull BlockState state, final @NotNull List drops, final @Nullable Entity breaker, final @Nullable ItemStack tool) { + super(block); + this.cancelled = false; + this.drops = drops; + this.breaker = breaker; + this.tool = tool; + this.state = state; + } + + /** + * Get the entity that caused the block to drop resources + * @return The responsible entity, or null if no entity was involved + */ + public @Nullable Entity getEntity() { + return breaker; + } + + /** + * Get the tool used to break the block + * @return The tool used, or null + */ + public @Nullable ItemStack getTool() { + return tool; + } + + /** + * Get the list of items to be dropped + * + * @return A mutable list of items to be dropped + */ + public @NotNull List getItems() { + return drops; + } + + /** + * Get the block state of the block that is about to drop resources + * @return The block state + */ + public @NotNull BlockState getBlockState() { + return state; + } + + @Override + public boolean isCancelled() { + return cancelled; + } + + @Override + public void setCancelled(final boolean cancel) { + cancelled = cancel; + } + + @Override + public @NotNull HandlerList getHandlers() { + return HANDLER_LIST; + } + public @NotNull static HandlerList getHandlerList() { + return HANDLER_LIST; + } +} diff --git a/paper-server/patches/sources/net/minecraft/world/level/block/Block.java.patch b/paper-server/patches/sources/net/minecraft/world/level/block/Block.java.patch index b3fcedc0af12..8111d4bfe960 100644 --- a/paper-server/patches/sources/net/minecraft/world/level/block/Block.java.patch +++ b/paper-server/patches/sources/net/minecraft/world/level/block/Block.java.patch @@ -58,7 +58,7 @@ private @Nullable Item item; private static final int CACHE_SIZE = 256; private static final ThreadLocal> OCCLUSION_CACHE = ThreadLocal.withInitial(() -> { -@@ -382,6 +_,27 @@ +@@ -382,19 +_,46 @@ return state.getDrops(params); } @@ -85,20 +85,28 @@ + public static void dropResources(final BlockState state, final Level level, final BlockPos pos) { if (level instanceof ServerLevel serverLevel) { - getDrops(state, serverLevel, pos, null).forEach(stack -> popResource(level, pos, stack)); -@@ -396,6 +_,12 @@ +- getDrops(state, serverLevel, pos, null).forEach(stack -> popResource(level, pos, stack)); ++ org.bukkit.craftbukkit.event.CraftEventFactory.callBlockDropResourcesEvent(getDrops(state, serverLevel, pos, null), level, pos, state, null, null).forEach(stack -> popResource(level, pos, stack)); // Paper - implement BlockDropResourcesEvent + state.spawnAfterBreak(serverLevel, pos, ItemStack.EMPTY, true); } } + public static void dropResources(final BlockState state, final LevelAccessor level, final BlockPos pos, final @Nullable BlockEntity blockEntity) { + if (level instanceof ServerLevel serverLevel) { +- getDrops(state, serverLevel, pos, blockEntity).forEach(stack -> popResource(serverLevel, pos, stack)); ++ org.bukkit.craftbukkit.event.CraftEventFactory.callBlockDropResourcesEvent(getDrops(state, serverLevel, pos, blockEntity), level, pos, state, null, null).forEach(stack -> popResource(serverLevel, pos, stack)); // Paper - implement BlockDropResourcesEvent + state.spawnAfterBreak(serverLevel, pos, ItemStack.EMPTY, true); + } + } ++ + // Paper start - Properly handle xp dropping + public static void dropResources(final BlockState state, final Level level, final BlockPos pos, final @Nullable BlockEntity blockEntity, final @Nullable Entity breaker, final ItemStack tool) { + dropResources(state, level, pos, blockEntity, breaker, tool, true); + } + // Paper end - Properly handle xp dropping -+ + public static void dropResources( final BlockState state, - final Level level, @@ -403,10 +_,11 @@ final @Nullable BlockEntity blockEntity, final @Nullable Entity breaker, @@ -106,8 +114,9 @@ + , final boolean dropExperience // Paper - Properly handle xp dropping ) { if (level instanceof ServerLevel serverLevel) { - getDrops(state, serverLevel, pos, blockEntity, breaker, tool).forEach(stack -> popResource(level, pos, stack)); +- getDrops(state, serverLevel, pos, blockEntity, breaker, tool).forEach(stack -> popResource(level, pos, stack)); - state.spawnAfterBreak(serverLevel, pos, tool, true); ++ org.bukkit.craftbukkit.event.CraftEventFactory.callBlockDropResourcesEvent(getDrops(state, serverLevel, pos, blockEntity, breaker, tool), level, pos, state, breaker, tool).forEach(stack -> popResource(level, pos, stack)); // Paper - implement BlockDropResourcesEvent + state.spawnAfterBreak(serverLevel, pos, tool, dropExperience); // Paper - Properly handle xp dropping } } 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 a5ea91906abe..73b1dab91ded 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 @@ -9,6 +9,7 @@ import io.papermc.paper.block.bed.BedEnterProblem; import io.papermc.paper.connection.HorriblePlayerLoginEventHack; import io.papermc.paper.connection.PlayerConnection; +import io.papermc.paper.event.block.BlockDropResourcesEvent; import io.papermc.paper.event.block.BlockLockCheckEvent; import io.papermc.paper.event.connection.PlayerConnectionValidateLoginEvent; import io.papermc.paper.event.entity.EntityIgniteEvent; @@ -2425,4 +2426,31 @@ public static int callEntityIgniteEvent(Entity entity, int fuseTime) { } return event.getFuseTime(); } + + public static List callBlockDropResourcesEvent(List drops, LevelAccessor level, BlockPos pos, net.minecraft.world.level.block.state.BlockState state, @Nullable Entity breaker, @Nullable ItemStack tool) { + if (BlockDropResourcesEvent.getHandlerList().getRegisteredListeners().length == 0) { + return drops; // No listeners, skip event creation + } + + var converted = new ArrayList(); + for (var drop : drops) { + converted.add(CraftItemStack.asCraftMirror(drop)); + } + + var stateSnapshot = CraftBlockStates.getBlockState(level, pos); + stateSnapshot.setBlock(state); + + var event = new BlockDropResourcesEvent( + CraftBlock.at(level, pos), + stateSnapshot, + converted, breaker == null ? null : breaker.getBukkitEntity(), + tool == null ? null : CraftItemStack.asCraftMirror(tool) + ); + if (event.callEvent()) { + // convert items back + return event.getItems().stream().map(CraftItemStack::asNMSCopy).toList(); + } + + return List.of(); // return nothing if event was cancelled + } }