--- a/net/minecraft/core/cauldron/CauldronInteraction.java
+++ b/net/minecraft/core/cauldron/CauldronInteraction.java
@@ -42,26 +_,31 @@
 
     static CauldronInteraction.InteractionMap newInteractionMap(String name) {
         Object2ObjectOpenHashMap<Item, CauldronInteraction> map = new Object2ObjectOpenHashMap<>();
-        map.defaultReturnValue((state, level, pos, player, hand, stack) -> InteractionResult.TRY_WITH_EMPTY_HAND);
+        map.defaultReturnValue((state, level, pos, player, hand, stack, hitDirection) -> InteractionResult.TRY_WITH_EMPTY_HAND); // Paper - add hitDirection
         CauldronInteraction.InteractionMap interactionMap = new CauldronInteraction.InteractionMap(name, map);
         INTERACTIONS.put(name, interactionMap);
         return interactionMap;
     }
 
-    InteractionResult interact(BlockState state, Level level, BlockPos pos, Player player, InteractionHand hand, ItemStack stack);
+    InteractionResult interact(BlockState state, Level level, BlockPos pos, Player player, InteractionHand hand, ItemStack stack, final net.minecraft.core.Direction hitDirection); // Paper - add hitDirection
 
     static void bootStrap() {
         Map<Item, CauldronInteraction> map = EMPTY.map();
         addDefaultInteractions(map);
-        map.put(Items.POTION, (state, level, pos, player, hand, stack) -> {
+        map.put(Items.POTION, (state, level, pos, player, hand, stack, hitDirection) -> { // Paper - add hitDirection
             PotionContents potionContents = stack.get(DataComponents.POTION_CONTENTS);
             if (potionContents != null && potionContents.is(Potions.WATER)) {
                 if (!level.isClientSide()) {
+                    // CraftBukkit start
+                    if (!LayeredCauldronBlock.changeLevel(level, pos, Blocks.WATER_CAULDRON.defaultBlockState(), player, org.bukkit.event.block.CauldronLevelChangeEvent.ChangeReason.BOTTLE_EMPTY, false)) { // Paper - Call CauldronLevelChangeEvent
+                        return InteractionResult.SUCCESS;
+                    }
+                    // CraftBukkit end
                     Item item = stack.getItem();
                     player.setItemInHand(hand, ItemUtils.createFilledResult(stack, player, new ItemStack(Items.GLASS_BOTTLE)));
                     player.awardStat(Stats.USE_CAULDRON);
                     player.awardStat(Stats.ITEM_USED.get(item));
-                    level.setBlockAndUpdate(pos, Blocks.WATER_CAULDRON.defaultBlockState());
+                    // level.setBlockAndUpdate(pos, Blocks.WATER_CAULDRON.defaultBlockState()); // CraftBukkit
                     level.playSound(null, pos, SoundEvents.BOTTLE_EMPTY, SoundSource.BLOCKS, 1.0F, 1.0F);
                     level.gameEvent(null, GameEvent.FLUID_PLACE, pos);
                 }
@@ -75,7 +_,7 @@
         addDefaultInteractions(map1);
         map1.put(
             Items.BUCKET,
-            (state, level, pos, player, hand, stack) -> fillBucket(
+            (state, level, pos, player, hand, stack, hitDirection) -> fillBucket( // Paper - add hitDirection
                 state,
                 level,
                 pos,
@@ -84,33 +_,43 @@
                 stack,
                 new ItemStack(Items.WATER_BUCKET),
                 blockState -> blockState.getValue(LayeredCauldronBlock.LEVEL) == 3,
-                SoundEvents.BUCKET_FILL
+                SoundEvents.BUCKET_FILL, hitDirection // Paper - add hitDirection
             )
         );
-        map1.put(Items.GLASS_BOTTLE, (state, level, pos, player, hand, stack) -> {
+        map1.put(Items.GLASS_BOTTLE, (state, level, pos, player, hand, stack, hitDirection) -> { // Paper - add hitDirection
             if (!level.isClientSide()) {
+                // CraftBukkit start
+                if (!LayeredCauldronBlock.lowerFillLevel(state, level, pos, player, org.bukkit.event.block.CauldronLevelChangeEvent.ChangeReason.BOTTLE_FILL)) {
+                    return InteractionResult.SUCCESS;
+                }
+                // CraftBukkit end
                 Item item = stack.getItem();
                 player.setItemInHand(hand, ItemUtils.createFilledResult(stack, player, PotionContents.createItemStack(Items.POTION, Potions.WATER)));
                 player.awardStat(Stats.USE_CAULDRON);
                 player.awardStat(Stats.ITEM_USED.get(item));
-                LayeredCauldronBlock.lowerFillLevel(state, level, pos);
+                // LayeredCauldronBlock.lowerFillLevel(state, level, pos); // CraftBukkit
                 level.playSound(null, pos, SoundEvents.BOTTLE_FILL, SoundSource.BLOCKS, 1.0F, 1.0F);
                 level.gameEvent(null, GameEvent.FLUID_PICKUP, pos);
             }
 
             return InteractionResult.SUCCESS;
         });
-        map1.put(Items.POTION, (state, level, pos, player, hand, stack) -> {
+        map1.put(Items.POTION, (state, level, pos, player, hand, stack, hitDirection) -> { // Paper - add hitDirection
             if (state.getValue(LayeredCauldronBlock.LEVEL) == 3) {
                 return InteractionResult.TRY_WITH_EMPTY_HAND;
             } else {
                 PotionContents potionContents = stack.get(DataComponents.POTION_CONTENTS);
                 if (potionContents != null && potionContents.is(Potions.WATER)) {
                     if (!level.isClientSide()) {
+                        // CraftBukkit start
+                        if (!LayeredCauldronBlock.changeLevel(level, pos, state.cycle(LayeredCauldronBlock.LEVEL), player, org.bukkit.event.block.CauldronLevelChangeEvent.ChangeReason.BOTTLE_EMPTY, false)) { // Paper - Call CauldronLevelChangeEvent
+                            return InteractionResult.SUCCESS;
+                        }
+                        // CraftBukkit end
                         player.setItemInHand(hand, ItemUtils.createFilledResult(stack, player, new ItemStack(Items.GLASS_BOTTLE)));
                         player.awardStat(Stats.USE_CAULDRON);
                         player.awardStat(Stats.ITEM_USED.get(stack.getItem()));
-                        level.setBlockAndUpdate(pos, state.cycle(LayeredCauldronBlock.LEVEL));
+                        // level.setBlockAndUpdate(pos, state.cycle(LayeredCauldronBlock.LEVEL)); // CraftBukkit
                         level.playSound(null, pos, SoundEvents.BOTTLE_EMPTY, SoundSource.BLOCKS, 1.0F, 1.0F);
                         level.gameEvent(null, GameEvent.FLUID_PLACE, pos);
                     }
@@ -162,15 +_,15 @@
         Map<Item, CauldronInteraction> map2 = LAVA.map();
         map2.put(
             Items.BUCKET,
-            (state, level, pos, player, hand, stack) -> fillBucket(
-                state, level, pos, player, hand, stack, new ItemStack(Items.LAVA_BUCKET), blockState -> true, SoundEvents.BUCKET_FILL_LAVA
+            (state, level, pos, player, hand, stack, hitDirection) -> fillBucket( // Paper - add hitDirection
+                state, level, pos, player, hand, stack, new ItemStack(Items.LAVA_BUCKET), blockState -> true, SoundEvents.BUCKET_FILL_LAVA, hitDirection // Paper - add hitDirection
             )
         );
         addDefaultInteractions(map2);
         Map<Item, CauldronInteraction> map3 = POWDER_SNOW.map();
         map3.put(
             Items.BUCKET,
-            (state, level, pos, player, hand, stack) -> fillBucket(
+            (state, level, pos, player, hand, stack, hitDirection) -> fillBucket( // Paper - add hitDirection
                 state,
                 level,
                 pos,
@@ -179,7 +_,7 @@
                 stack,
                 new ItemStack(Items.POWDER_SNOW_BUCKET),
                 blockState -> blockState.getValue(LayeredCauldronBlock.LEVEL) == 3,
-                SoundEvents.BUCKET_FILL_POWDER_SNOW
+                SoundEvents.BUCKET_FILL_POWDER_SNOW, hitDirection // Paper - add hitDirection
             )
         );
         addDefaultInteractions(map3);
@@ -202,15 +_,35 @@
         Predicate<BlockState> statePredicate,
         SoundEvent fillSound
     ) {
+        // Paper start - add hitDirection
+        return fillBucket(state, level, pos, player, hand, emptyStack, filledStack, statePredicate, fillSound, null); // Paper - add hitDirection
+    }
+
+    static InteractionResult fillBucket(BlockState state, Level level, BlockPos pos, Player player, InteractionHand hand, ItemStack emptyStack, ItemStack filledStack, Predicate<BlockState> statePredicate, SoundEvent fillSound, @javax.annotation.Nullable net.minecraft.core.Direction hitDirection) {
+        // Paper end - add hitDirection
         if (!statePredicate.test(state)) {
             return InteractionResult.TRY_WITH_EMPTY_HAND;
         } else {
             if (!level.isClientSide()) {
+                // Paper start - fire PlayerBucketFillEvent
+                if (hitDirection != null) {
+                    org.bukkit.event.player.PlayerBucketEvent event = org.bukkit.craftbukkit.event.CraftEventFactory.callPlayerBucketFillEvent(level, player, pos, pos, hitDirection, emptyStack, filledStack.getItem(), hand);
+                    if (event.isCancelled()) {
+                        return InteractionResult.PASS;
+                    }
+                    filledStack = event.getItemStack() != null ? org.bukkit.craftbukkit.inventory.CraftItemStack.asNMSCopy(event.getItemStack()) : ItemStack.EMPTY;
+                }
+                // Paper end - fire PlayerBucketFillEvent
+                // CraftBukkit start
+                if (!LayeredCauldronBlock.changeLevel(level, pos, Blocks.CAULDRON.defaultBlockState(), player, org.bukkit.event.block.CauldronLevelChangeEvent.ChangeReason.BUCKET_FILL, false)) { // Paper - Call CauldronLevelChangeEvent
+                    return InteractionResult.SUCCESS;
+                }
+                // CraftBukkit end
                 Item item = emptyStack.getItem();
                 player.setItemInHand(hand, ItemUtils.createFilledResult(emptyStack, player, filledStack));
                 player.awardStat(Stats.USE_CAULDRON);
                 player.awardStat(Stats.ITEM_USED.get(item));
-                level.setBlockAndUpdate(pos, Blocks.CAULDRON.defaultBlockState());
+                // level.setBlockAndUpdate(pos, Blocks.CAULDRON.defaultBlockState()); // CraftBukkit
                 level.playSound(null, pos, fillSound, SoundSource.BLOCKS, 1.0F, 1.0F);
                 level.gameEvent(null, GameEvent.FLUID_PICKUP, pos);
             }
@@ -222,12 +_,33 @@
     static InteractionResult emptyBucket(
         Level level, BlockPos pos, Player player, InteractionHand hand, ItemStack filledStack, BlockState state, SoundEvent emptySound
     ) {
+        // Paper start - add hitDirection
+        return emptyBucket(level, pos, player, hand, filledStack, state, emptySound, null);
+    }
+
+    static InteractionResult emptyBucket(Level level, BlockPos pos, Player player, InteractionHand hand, ItemStack filledStack, BlockState state, SoundEvent emptySound, @javax.annotation.Nullable net.minecraft.core.Direction hitDirection) {
+        // Paper end - add hitDirection
         if (!level.isClientSide()) {
+            // Paper start - fire PlayerBucketEmptyEvent
+            ItemStack output = new ItemStack(Items.BUCKET);
+            if (hitDirection != null) {
+                org.bukkit.event.player.PlayerBucketEvent event = org.bukkit.craftbukkit.event.CraftEventFactory.callPlayerBucketEmptyEvent(level, player, pos, pos, hitDirection, filledStack, hand);
+                if (event.isCancelled()) {
+                    return InteractionResult.PASS;
+                }
+                output = event.getItemStack() != null ? org.bukkit.craftbukkit.inventory.CraftItemStack.asNMSCopy(event.getItemStack()) : ItemStack.EMPTY;
+            }
+            // Paper end - fire PlayerBucketEmptyEvent
+            // CraftBukkit start
+            if (!LayeredCauldronBlock.changeLevel(level, pos, state, player, org.bukkit.event.block.CauldronLevelChangeEvent.ChangeReason.BUCKET_EMPTY, false)) { // Paper - Call CauldronLevelChangeEvent
+                return InteractionResult.SUCCESS;
+            }
+            // CraftBukkit end
             Item item = filledStack.getItem();
-            player.setItemInHand(hand, ItemUtils.createFilledResult(filledStack, player, new ItemStack(Items.BUCKET)));
+            player.setItemInHand(hand, ItemUtils.createFilledResult(filledStack, player, output)); // Paper
             player.awardStat(Stats.FILL_CAULDRON);
             player.awardStat(Stats.ITEM_USED.get(item));
-            level.setBlockAndUpdate(pos, state);
+            // level.setBlockAndUpdate(pos, state); // CraftBukkit
             level.playSound(null, pos, emptySound, SoundSource.BLOCKS, 1.0F, 1.0F);
             level.gameEvent(null, GameEvent.FLUID_PLACE, pos);
         }
@@ -236,23 +_,23 @@
     }
 
     private static InteractionResult fillWaterInteraction(
-        BlockState state, Level level, BlockPos pos, Player player, InteractionHand hand, ItemStack filledStack
+        BlockState state, Level level, BlockPos pos, Player player, InteractionHand hand, ItemStack filledStack, final net.minecraft.core.Direction hitDirection // Paper - add hitDirection
     ) {
         return emptyBucket(
-            level, pos, player, hand, filledStack, Blocks.WATER_CAULDRON.defaultBlockState().setValue(LayeredCauldronBlock.LEVEL, 3), SoundEvents.BUCKET_EMPTY
+            level, pos, player, hand, filledStack, Blocks.WATER_CAULDRON.defaultBlockState().setValue(LayeredCauldronBlock.LEVEL, 3), SoundEvents.BUCKET_EMPTY, hitDirection // Paper - add hitDirection
         );
     }
 
     private static InteractionResult fillLavaInteraction(
-        BlockState state, Level level, BlockPos pos, Player player, InteractionHand hand, ItemStack filledStack
+        BlockState state, Level level, BlockPos pos, Player player, InteractionHand hand, ItemStack filledStack, final net.minecraft.core.Direction hitDirection // Paper - add hitDirection
     ) {
         return (InteractionResult)(isUnderWater(level, pos)
             ? InteractionResult.CONSUME
-            : emptyBucket(level, pos, player, hand, filledStack, Blocks.LAVA_CAULDRON.defaultBlockState(), SoundEvents.BUCKET_EMPTY_LAVA));
+            : emptyBucket(level, pos, player, hand, filledStack, Blocks.LAVA_CAULDRON.defaultBlockState(), SoundEvents.BUCKET_EMPTY_LAVA, hitDirection)); // Paper - add hitDirection
     }
 
     private static InteractionResult fillPowderSnowInteraction(
-        BlockState state, Level level, BlockPos pos, Player player, InteractionHand hand, ItemStack filledStack
+        BlockState state, Level level, BlockPos pos, Player player, InteractionHand hand, ItemStack filledStack, final net.minecraft.core.Direction hitDirection // Paper - add hitDirection
     ) {
         return (InteractionResult)(isUnderWater(level, pos)
             ? InteractionResult.CONSUME
@@ -263,53 +_,68 @@
                 hand,
                 filledStack,
                 Blocks.POWDER_SNOW_CAULDRON.defaultBlockState().setValue(LayeredCauldronBlock.LEVEL, 3),
-                SoundEvents.BUCKET_EMPTY_POWDER_SNOW
+                SoundEvents.BUCKET_EMPTY_POWDER_SNOW, hitDirection // Paper - add hitDirection
             ));
     }
 
-    private static InteractionResult shulkerBoxInteraction(BlockState state, Level level, BlockPos pos, Player player, InteractionHand hand, ItemStack stack) {
+    private static InteractionResult shulkerBoxInteraction(BlockState state, Level level, BlockPos pos, Player player, InteractionHand hand, ItemStack stack, final net.minecraft.core.Direction hitDirection) { // Paper - add hitDirection
         Block block = Block.byItem(stack.getItem());
         if (!(block instanceof ShulkerBoxBlock)) {
             return InteractionResult.TRY_WITH_EMPTY_HAND;
         } else {
             if (!level.isClientSide()) {
+                // CraftBukkit start
+                if (!LayeredCauldronBlock.lowerFillLevel(state, level, pos, player, org.bukkit.event.block.CauldronLevelChangeEvent.ChangeReason.SHULKER_WASH)) {
+                    return InteractionResult.SUCCESS;
+                }
+                // CraftBukkit end
                 ItemStack itemStack = stack.transmuteCopy(Blocks.SHULKER_BOX, 1);
                 player.setItemInHand(hand, ItemUtils.createFilledResult(stack, player, itemStack, false));
                 player.awardStat(Stats.CLEAN_SHULKER_BOX);
-                LayeredCauldronBlock.lowerFillLevel(state, level, pos);
+                // LayeredCauldronBlock.lowerFillLevel(state, level, pos); // CraftBukkit
             }
 
             return InteractionResult.SUCCESS;
         }
     }
 
-    private static InteractionResult bannerInteraction(BlockState state, Level level, BlockPos pos, Player player, InteractionHand hand, ItemStack stack) {
+    private static InteractionResult bannerInteraction(BlockState state, Level level, BlockPos pos, Player player, InteractionHand hand, ItemStack stack, final net.minecraft.core.Direction hitDirection) { // Paper - add hitDirection
         BannerPatternLayers bannerPatternLayers = stack.getOrDefault(DataComponents.BANNER_PATTERNS, BannerPatternLayers.EMPTY);
         if (bannerPatternLayers.layers().isEmpty()) {
             return InteractionResult.TRY_WITH_EMPTY_HAND;
         } else {
             if (!level.isClientSide()) {
+                // CraftBukkit start
+                if (!LayeredCauldronBlock.lowerFillLevel(state, level, pos, player, org.bukkit.event.block.CauldronLevelChangeEvent.ChangeReason.BANNER_WASH)) {
+                    return InteractionResult.SUCCESS;
+                }
+                // CraftBukkit end
                 ItemStack itemStack = stack.copyWithCount(1);
                 itemStack.set(DataComponents.BANNER_PATTERNS, bannerPatternLayers.removeLast());
                 player.setItemInHand(hand, ItemUtils.createFilledResult(stack, player, itemStack, false));
                 player.awardStat(Stats.CLEAN_BANNER);
-                LayeredCauldronBlock.lowerFillLevel(state, level, pos);
+                // LayeredCauldronBlock.lowerFillLevel(state, level, pos); // CraftBukkit
             }
 
             return InteractionResult.SUCCESS;
         }
     }
 
-    private static InteractionResult dyedItemIteration(BlockState state, Level level, BlockPos pos, Player player, InteractionHand hand, ItemStack stack) {
+    private static InteractionResult dyedItemIteration(BlockState state, Level level, BlockPos pos, Player player, InteractionHand hand, ItemStack stack, final net.minecraft.core.Direction hitDirection) { // Paper - add hitDirection
         if (!stack.is(ItemTags.DYEABLE)) {
             return InteractionResult.TRY_WITH_EMPTY_HAND;
         } else if (!stack.has(DataComponents.DYED_COLOR)) {
             return InteractionResult.TRY_WITH_EMPTY_HAND;
         } else {
             if (!level.isClientSide()) {
+                // CraftBukkit start
+                if (!LayeredCauldronBlock.lowerFillLevel(state, level, pos, player, org.bukkit.event.block.CauldronLevelChangeEvent.ChangeReason.ARMOR_WASH)) {
+                    return InteractionResult.SUCCESS;
+                }
+                // CraftBukkit end
                 stack.remove(DataComponents.DYED_COLOR);
                 player.awardStat(Stats.CLEAN_ARMOR);
-                LayeredCauldronBlock.lowerFillLevel(state, level, pos);
+                // LayeredCauldronBlock.lowerFillLevel(state, level, pos); // CraftBukkit
             }
 
             return InteractionResult.SUCCESS;
