diff --git a/patches/api/Add-BlockBreakBlockEvent.patch b/patches/api/Add-BlockBreakBlockEvent.patch
index f193732703..856ee9f7c7 100644
--- a/patches/api/Add-BlockBreakBlockEvent.patch
+++ b/patches/api/Add-BlockBreakBlockEvent.patch
@@ -15,6 +15,7 @@ index 0000000000000000000000000000000000000000..00000000000000000000000000000000
+import org.bukkit.block.Block;
+import org.bukkit.event.HandlerList;
+import org.bukkit.event.block.BlockEvent;
++import org.bukkit.event.block.BlockExpEvent;
+import org.bukkit.inventory.ItemStack;
+import org.jetbrains.annotations.NotNull;
+
@@ -25,7 +26,7 @@ index 0000000000000000000000000000000000000000..00000000000000000000000000000000
+ *
+ * Currently called for piston's and liquid flows.
+ */
-+public class BlockBreakBlockEvent extends BlockEvent {
++public class BlockBreakBlockEvent extends BlockExpEvent {
+
+ private static final HandlerList HANDLER_LIST = new HandlerList();
+
@@ -33,7 +34,7 @@ index 0000000000000000000000000000000000000000..00000000000000000000000000000000
+ private final Block source;
+
+ public BlockBreakBlockEvent(@NotNull Block block, @NotNull Block source, @NotNull List drops) {
-+ super(block);
++ super(block, 0);
+ this.source = source;
+ this.drops = drops;
+ }
diff --git a/patches/api/BlockDestroyEvent.patch b/patches/api/BlockDestroyEvent.patch
index c06e0c0d26..d7a0d969e6 100644
--- a/patches/api/BlockDestroyEvent.patch
+++ b/patches/api/BlockDestroyEvent.patch
@@ -23,6 +23,7 @@ index 0000000000000000000000000000000000000000..00000000000000000000000000000000
+import org.bukkit.event.Cancellable;
+import org.bukkit.event.HandlerList;
+import org.bukkit.event.block.BlockEvent;
++import org.bukkit.event.block.BlockExpEvent;
+import org.jetbrains.annotations.NotNull;
+
+/**
@@ -36,23 +37,44 @@ index 0000000000000000000000000000000000000000..00000000000000000000000000000000
+ * Events such as leaves decaying, pistons retracting (where the block is moving), does NOT fire this event.
+ *
+ */
-+public class BlockDestroyEvent extends BlockEvent implements Cancellable {
++public class BlockDestroyEvent extends BlockExpEvent implements Cancellable {
+
+ private static final HandlerList handlers = new HandlerList();
+
+ @NotNull private final BlockData newState;
+ private boolean willDrop;
+ private boolean playEffect = true;
++ private BlockData effectBlock;
+
+ private boolean cancelled = false;
+
+ public BlockDestroyEvent(@NotNull Block block, @NotNull BlockData newState, boolean willDrop) {
-+ super(block);
++ super(block, 0);
+ this.newState = newState;
+ this.willDrop = willDrop;
+ }
+
+ /**
++ * Get the effect that will be played when the block is broken.
++ * @return block break effect
++ */
++ @NotNull
++ public BlockData getEffectBlock() {
++ return this.effectBlock;
++ }
++
++ /**
++ * Sets the effect that will be played when the block is broken.
++ * Note: {@link BlockDestroyEvent#playEffect()} must be true in order for this effect to be
++ * played.
++ *
++ * @param effectBlock block effect
++ */
++ public void setEffectBlock(@NotNull BlockData effectBlock) {
++ this.effectBlock = effectBlock;
++ }
++
++ /**
+ * @return The new state of this block (Air, or a Fluid type)
+ */
+ @NotNull
diff --git a/patches/server/BlockDestroyEvent.patch b/patches/server/BlockDestroyEvent.patch
index fdb169699e..ab23f87b8f 100644
--- a/patches/server/BlockDestroyEvent.patch
+++ b/patches/server/BlockDestroyEvent.patch
@@ -30,18 +30,24 @@ index 0000000000000000000000000000000000000000..00000000000000000000000000000000
+ // they are NOT used with same intent and the above should not fire this event. The above method is more of a BlockSetToAirEvent,
+ // it doesn't imply destruction of a block that plays a sound effect / drops an item.
+ boolean playEffect = true;
++ BlockState effectType = iblockdata;
++ int xp = iblockdata.getBlock().getExpDrop(iblockdata, (ServerLevel) this, pos, ItemStack.EMPTY, true);
+ if (com.destroystokyo.paper.event.block.BlockDestroyEvent.getHandlerList().getRegisteredListeners().length > 0) {
+ com.destroystokyo.paper.event.block.BlockDestroyEvent event = new com.destroystokyo.paper.event.block.BlockDestroyEvent(MCUtil.toBukkitBlock(this, pos), fluid.createLegacyBlock().createCraftBlockData(), drop);
+ if (!event.callEvent()) {
+ return false;
+ }
++ effectType = ((CraftBlockData) event.getEffectBlock()).getState();
+ playEffect = event.playEffect();
+ drop = event.willDrop();
++ xp = event.getExpToDrop();
+ }
+ // Paper end
- if (!(iblockdata.getBlock() instanceof BaseFireBlock)) {
-+ if (playEffect && !(iblockdata.getBlock() instanceof BaseFireBlock)) { // Paper
- this.levelEvent(2001, pos, Block.getId(iblockdata));
+- this.levelEvent(2001, pos, Block.getId(iblockdata));
++ if (playEffect && !(effectType.getBlock() instanceof BaseFireBlock)) { // Paper
++ this.levelEvent(2001, pos, Block.getId(effectType)); // Paper
}
+ if (drop) {
diff --git a/patches/server/ExperienceOrbs-API-for-Reason-Source-Triggering-play.patch b/patches/server/ExperienceOrbs-API-for-Reason-Source-Triggering-play.patch
index 592b669825..768454e4ff 100644
--- a/patches/server/ExperienceOrbs-API-for-Reason-Source-Triggering-play.patch
+++ b/patches/server/ExperienceOrbs-API-for-Reason-Source-Triggering-play.patch
@@ -299,14 +299,14 @@ index 0000000000000000000000000000000000000000..00000000000000000000000000000000
}
public void popExperience(ServerLevel world, BlockPos pos, int size) {
-+ // Paper start - add player parameter
++ // Paper start - add entity parameter
+ popExperience(world, pos, size, null);
+ }
-+ public void popExperience(ServerLevel world, BlockPos pos, int size, net.minecraft.server.level.ServerPlayer player) {
-+ // Paper end - add player parameter
++ public void popExperience(ServerLevel world, BlockPos pos, int size, net.minecraft.world.entity.Entity entity) {
++ // Paper end - add entity parameter
if (world.getGameRules().getBoolean(GameRules.RULE_DOBLOCKDROPS)) {
- ExperienceOrb.award(world, Vec3.atCenterOf(pos), size);
-+ ExperienceOrb.award(world, Vec3.atCenterOf(pos), size, org.bukkit.entity.ExperienceOrb.SpawnReason.BLOCK_BREAK, player); // Paper
++ ExperienceOrb.award(world, Vec3.atCenterOf(pos), size, org.bukkit.entity.ExperienceOrb.SpawnReason.BLOCK_BREAK, entity); // Paper
}
}
diff --git a/patches/server/Properly-handle-BlockBreakEvent-isDropItems.patch b/patches/server/Properly-handle-BlockBreakEvent-isDropItems.patch
index 514c7046e3..854836d285 100644
--- a/patches/server/Properly-handle-BlockBreakEvent-isDropItems.patch
+++ b/patches/server/Properly-handle-BlockBreakEvent-isDropItems.patch
@@ -19,7 +19,7 @@ index 0000000000000000000000000000000000000000..00000000000000000000000000000000
- if (flag && flag1 && event.isDropItems()) { // CraftBukkit - Check if block should drop items
- block.playerDestroy(this.level, this.player, pos, iblockdata1, tileentity, itemstack1);
+ if (flag && flag1 && event.isDropItems()/* && event.isDropItems() */) { // CraftBukkit - Check if block should drop items // Paper - fix drops not preventing stats/food exhaustion
-+ block.playerDestroy(this.level, this.player, pos, iblockdata1, tileentity, itemstack1, event.isDropItems()); // Paper
++ block.playerDestroy(this.level, this.player, pos, iblockdata1, tileentity, itemstack1, event.isDropItems(), false); // Paper
}
// return true; // CraftBukkit
@@ -33,8 +33,8 @@ index 0000000000000000000000000000000000000000..00000000000000000000000000000000
@Override
- public void playerDestroy(Level world, Player player, BlockPos pos, BlockState state, @Nullable BlockEntity blockEntity, ItemStack tool) {
- super.playerDestroy(world, player, pos, state, blockEntity, tool);
-+ public void playerDestroy(Level world, Player player, BlockPos pos, BlockState state, @Nullable BlockEntity blockEntity, ItemStack tool, boolean includeDrops) { // Paper
-+ super.playerDestroy(world, player, pos, state, blockEntity, tool, includeDrops); // Paper
++ public void playerDestroy(Level world, Player player, BlockPos pos, BlockState state, @Nullable BlockEntity blockEntity, ItemStack tool, boolean includeDrops, boolean dropExp) { // Paper
++ super.playerDestroy(world, player, pos, state, blockEntity, tool, includeDrops, dropExp); // Paper
if (!world.isClientSide && blockEntity instanceof BeehiveBlockEntity) {
BeehiveBlockEntity tileentitybeehive = (BeehiveBlockEntity) blockEntity;
@@ -49,9 +49,9 @@ index 0000000000000000000000000000000000000000..00000000000000000000000000000000
+ @io.papermc.paper.annotation.DoNotUse // Paper - method below allows better control of item drops
public void playerDestroy(Level world, Player player, BlockPos pos, BlockState state, @Nullable BlockEntity blockEntity, ItemStack tool) {
+ // Paper start
-+ this.playerDestroy(world, player, pos, state, blockEntity, tool, true);
++ this.playerDestroy(world, player, pos, state, blockEntity, tool, true, true);
+ }
-+ public void playerDestroy(Level world, Player player, BlockPos pos, BlockState state, @Nullable BlockEntity blockEntity, ItemStack tool, boolean includeDrops) {
++ public void playerDestroy(Level world, Player player, BlockPos pos, BlockState state, @Nullable BlockEntity blockEntity, ItemStack tool, boolean includeDrops, boolean dropExp) {
+ // Paper end
player.awardStat(Stats.BLOCK_MINED.get(this));
player.causeFoodExhaustion(0.005F, org.bukkit.event.entity.EntityExhaustionEvent.ExhaustionReason.BLOCK_MINED); // CraftBukkit - EntityExhaustionEvent
@@ -71,8 +71,8 @@ index 0000000000000000000000000000000000000000..00000000000000000000000000000000
@Override
- public void playerDestroy(Level world, Player player, BlockPos pos, BlockState state, @Nullable BlockEntity blockEntity, ItemStack tool) {
- super.playerDestroy(world, player, pos, Blocks.AIR.defaultBlockState(), blockEntity, tool);
-+ public void playerDestroy(Level world, Player player, BlockPos pos, BlockState state, @Nullable BlockEntity blockEntity, ItemStack tool, boolean includeDrops) { // Paper
-+ super.playerDestroy(world, player, pos, Blocks.AIR.defaultBlockState(), blockEntity, tool, includeDrops); // Paper
++ public void playerDestroy(Level world, Player player, BlockPos pos, BlockState state, @Nullable BlockEntity blockEntity, ItemStack tool, boolean includeDrops, boolean dropExp) { // Paper
++ super.playerDestroy(world, player, pos, Blocks.AIR.defaultBlockState(), blockEntity, tool, includeDrops, dropExp); // Paper
}
protected static void preventDropFromBottomPart(Level world, BlockPos pos, BlockState state, Player player) {
@@ -86,8 +86,8 @@ index 0000000000000000000000000000000000000000..00000000000000000000000000000000
@Override
- public void playerDestroy(Level world, Player player, BlockPos pos, BlockState state, @Nullable BlockEntity blockEntity, ItemStack tool) {
- super.playerDestroy(world, player, pos, state, blockEntity, tool);
-+ public void playerDestroy(Level world, Player player, BlockPos pos, BlockState state, @Nullable BlockEntity blockEntity, ItemStack tool, boolean includeDrops) { // Paper
-+ super.playerDestroy(world, player, pos, state, blockEntity, tool, includeDrops); // Paper
++ public void playerDestroy(Level world, Player player, BlockPos pos, BlockState state, @Nullable BlockEntity blockEntity, ItemStack tool, boolean includeDrops, boolean dropExp) { // Paper
++ super.playerDestroy(world, player, pos, state, blockEntity, tool, includeDrops, dropExp); // Paper
// Paper start
this.afterDestroy(world, pos, tool);
}
@@ -101,8 +101,8 @@ index 0000000000000000000000000000000000000000..00000000000000000000000000000000
@Override
- public void playerDestroy(Level world, Player player, BlockPos pos, BlockState state, @Nullable BlockEntity blockEntity, ItemStack tool) {
- super.playerDestroy(world, player, pos, state, blockEntity, tool);
-+ public void playerDestroy(Level world, Player player, BlockPos pos, BlockState state, @Nullable BlockEntity blockEntity, ItemStack tool, boolean includeDrops) { // Paper
-+ super.playerDestroy(world, player, pos, state, blockEntity, tool, includeDrops); // Paper
++ public void playerDestroy(Level world, Player player, BlockPos pos, BlockState state, @Nullable BlockEntity blockEntity, ItemStack tool, boolean includeDrops, boolean dropExp) { // Paper
++ super.playerDestroy(world, player, pos, state, blockEntity, tool, includeDrops, dropExp); // Paper
this.decreaseEggs(world, pos, state);
}
diff --git a/patches/server/Properly-handle-experience-dropping-on-block-break.patch b/patches/server/Properly-handle-experience-dropping-on-block-break.patch
new file mode 100644
index 0000000000..a4bea4b784
--- /dev/null
+++ b/patches/server/Properly-handle-experience-dropping-on-block-break.patch
@@ -0,0 +1,94 @@
+From 0000000000000000000000000000000000000000 Mon Sep 17 00:00:00 2001
+From: Owen1212055 <23108066+Owen1212055@users.noreply.github.com>
+Date: Sat, 30 Dec 2023 15:00:06 -0500
+Subject: [PATCH] Properly handle experience dropping on block break
+
+This causes spawnAfterBreak to spawn xp by default, removing the need to manually add xp wherever this method is used.
+For classes that use custom xp amounts, they can drop the resources with disabling
+
+diff --git a/src/main/java/net/minecraft/world/level/Level.java b/src/main/java/net/minecraft/world/level/Level.java
+index 0000000000000000000000000000000000000000..0000000000000000000000000000000000000000 100644
+--- a/src/main/java/net/minecraft/world/level/Level.java
++++ b/src/main/java/net/minecraft/world/level/Level.java
+@@ -0,0 +0,0 @@ public abstract class Level implements LevelAccessor, AutoCloseable {
+ if (drop) {
+ BlockEntity tileentity = iblockdata.hasBlockEntity() ? this.getBlockEntity(pos) : null;
+
+- Block.dropResources(iblockdata, this, pos, tileentity, breakingEntity, ItemStack.EMPTY);
++ Block.dropResources(iblockdata, this, pos, tileentity, breakingEntity, ItemStack.EMPTY, false); // Don't drop xp
++ iblockdata.getBlock().popExperience((ServerLevel) this, pos, xp, breakingEntity); // Paper - handle drop experience logic, custom amount
+ }
+
+ boolean flag1 = this.setBlock(pos, fluid.createLegacyBlock(), 3, maxUpdateDepth);
+diff --git a/src/main/java/net/minecraft/world/level/block/Block.java b/src/main/java/net/minecraft/world/level/block/Block.java
+index 0000000000000000000000000000000000000000..0000000000000000000000000000000000000000 100644
+--- a/src/main/java/net/minecraft/world/level/block/Block.java
++++ b/src/main/java/net/minecraft/world/level/block/Block.java
+@@ -0,0 +0,0 @@ public class Block extends BlockBehaviour implements ItemLike {
+ for (net.minecraft.world.item.ItemStack drop : net.minecraft.world.level.block.Block.getDrops(state, world.getMinecraftWorld(), pos, blockEntity)) {
+ items.add(org.bukkit.craftbukkit.inventory.CraftItemStack.asBukkitCopy(drop));
+ }
++ Block block = state.getBlock();
+ io.papermc.paper.event.block.BlockBreakBlockEvent event = new io.papermc.paper.event.block.BlockBreakBlockEvent(org.bukkit.craftbukkit.block.CraftBlock.at(world, pos), org.bukkit.craftbukkit.block.CraftBlock.at(world, source), items);
++ event.setExpToDrop(block.getExpDrop(state, (ServerLevel) world, pos, net.minecraft.world.item.ItemStack.EMPTY, true));
+ event.callEvent();
+ for (var drop : event.getDrops()) {
+ popResource(world.getMinecraftWorld(), pos, org.bukkit.craftbukkit.inventory.CraftItemStack.asNMSCopy(drop));
+ }
+- state.spawnAfterBreak(world.getMinecraftWorld(), pos, ItemStack.EMPTY, true);
++ state.spawnAfterBreak(world.getMinecraftWorld(), pos, ItemStack.EMPTY, false);
++ block.popExperience((ServerLevel) world, pos, event.getExpToDrop());
+ }
+ return true;
+ }
+ // Paper end
+
+ public static void dropResources(BlockState state, Level world, BlockPos pos, @Nullable BlockEntity blockEntity, @Nullable Entity entity, ItemStack tool) {
++ // Paper start
++ dropResources(state, world, pos, blockEntity, entity, tool, true);
++ }
++ public static void dropResources(BlockState state, Level world, BlockPos pos, @Nullable BlockEntity blockEntity, @Nullable Entity entity, ItemStack tool, boolean dropExperience) {
++ // Paper end
+ if (world instanceof ServerLevel) {
+ Block.getDrops(state, (ServerLevel) world, pos, blockEntity, entity, tool).forEach((itemstack1) -> {
+ Block.popResource(world, pos, itemstack1);
+ });
+- state.spawnAfterBreak((ServerLevel) world, pos, tool, true);
++ state.spawnAfterBreak((ServerLevel) world, pos, tool, dropExperience); // Paper
+ }
+
+ }
+@@ -0,0 +0,0 @@ public class Block extends BlockBehaviour implements ItemLike {
+ player.awardStat(Stats.BLOCK_MINED.get(this));
+ player.causeFoodExhaustion(0.005F, org.bukkit.event.entity.EntityExhaustionEvent.ExhaustionReason.BLOCK_MINED); // CraftBukkit - EntityExhaustionEvent
+ if (includeDrops) { // Paper
+- Block.dropResources(state, world, pos, blockEntity, player, tool);
++ Block.dropResources(state, world, pos, blockEntity, player, tool, dropExp); // Paper
+ } // Paper
+ }
+
+diff --git a/src/main/java/net/minecraft/world/level/block/state/BlockBehaviour.java b/src/main/java/net/minecraft/world/level/block/state/BlockBehaviour.java
+index 0000000000000000000000000000000000000000..0000000000000000000000000000000000000000 100644
+--- a/src/main/java/net/minecraft/world/level/block/state/BlockBehaviour.java
++++ b/src/main/java/net/minecraft/world/level/block/state/BlockBehaviour.java
+@@ -0,0 +0,0 @@ public abstract class BlockBehaviour implements FeatureElement {
+
+ public void spawnAfterBreak(ServerLevel world, BlockPos pos, ItemStack tool, boolean dropExperience) {
+ this.getBlock().spawnAfterBreak(this.asState(), world, pos, tool, dropExperience);
++ if (dropExperience) {getBlock().popExperience(world, pos, this.getBlock().getExpDrop(asState(), world, pos, tool, true));} // Paper - spawn experience
+ }
+
+ public List getDrops(LootParams.Builder builder) {
+diff --git a/src/main/java/org/bukkit/craftbukkit/block/CraftBlock.java b/src/main/java/org/bukkit/craftbukkit/block/CraftBlock.java
+index 0000000000000000000000000000000000000000..0000000000000000000000000000000000000000 100644
+--- a/src/main/java/org/bukkit/craftbukkit/block/CraftBlock.java
++++ b/src/main/java/org/bukkit/craftbukkit/block/CraftBlock.java
+@@ -0,0 +0,0 @@ public class CraftBlock implements Block {
+
+ // Modelled off EntityHuman#hasBlock
+ if (block != Blocks.AIR && (item == null || !iblockdata.requiresCorrectToolForDrops() || nmsItem.isCorrectToolForDrops(iblockdata))) {
+- net.minecraft.world.level.block.Block.dropResources(iblockdata, this.world.getMinecraftWorld(), this.position, this.world.getBlockEntity(this.position), null, nmsItem);
++ net.minecraft.world.level.block.Block.dropResources(iblockdata, this.world.getMinecraftWorld(), this.position, this.world.getBlockEntity(this.position), null, nmsItem, false); // Paper
+ // Paper start - improve Block#breanNaturally
+ if (triggerEffect) {
+ if (iblockdata.getBlock() instanceof net.minecraft.world.level.block.BaseFireBlock) {