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