diff --git a/paper-api/src/main/java/org/bukkit/event/block/BlockIgniteEvent.java b/paper-api/src/main/java/org/bukkit/event/block/BlockIgniteEvent.java index 3b8b88c907fd..dea449829740 100644 --- a/paper-api/src/main/java/org/bukkit/event/block/BlockIgniteEvent.java +++ b/paper-api/src/main/java/org/bukkit/event/block/BlockIgniteEvent.java @@ -10,8 +10,7 @@ import org.jetbrains.annotations.Nullable; /** - * Called when a block is ignited. If you want to catch when a Player places - * fire, you need to use {@link BlockPlaceEvent}. + * Called when a block is ignited. *

* If this event is cancelled, the block will not be ignited. */ diff --git a/paper-api/src/main/java/org/bukkit/event/block/BlockMultiPlaceEvent.java b/paper-api/src/main/java/org/bukkit/event/block/BlockMultiPlaceEvent.java index 0df9dfa44dcc..7f417d5b9f7f 100644 --- a/paper-api/src/main/java/org/bukkit/event/block/BlockMultiPlaceEvent.java +++ b/paper-api/src/main/java/org/bukkit/event/block/BlockMultiPlaceEvent.java @@ -18,18 +18,18 @@ */ public class BlockMultiPlaceEvent extends BlockPlaceEvent { - private final List states; + private final List replacedStates; @ApiStatus.Internal @Deprecated(forRemoval = true) - public BlockMultiPlaceEvent(@NotNull List states, @NotNull Block clicked, @NotNull ItemStack itemInHand, @NotNull Player thePlayer, boolean canBuild) { - this(states, clicked, itemInHand, thePlayer, canBuild, org.bukkit.inventory.EquipmentSlot.HAND); + public BlockMultiPlaceEvent(@NotNull List replacedStates, @NotNull Block clicked, @NotNull ItemStack itemInHand, @NotNull Player thePlayer, boolean canBuild) { + this(replacedStates, clicked, itemInHand, thePlayer, canBuild, org.bukkit.inventory.EquipmentSlot.HAND); } @ApiStatus.Internal - public BlockMultiPlaceEvent(@NotNull List states, @NotNull Block clicked, @NotNull ItemStack itemInHand, @NotNull Player thePlayer, boolean canBuild, @NotNull org.bukkit.inventory.EquipmentSlot hand) { - super(states.get(0).getBlock(), states.get(0), clicked, itemInHand, thePlayer, canBuild, hand); - this.states = ImmutableList.copyOf(states); + public BlockMultiPlaceEvent(@NotNull List replacedStates, @NotNull Block clicked, @NotNull ItemStack itemInHand, @NotNull Player thePlayer, boolean canBuild, @NotNull org.bukkit.inventory.EquipmentSlot hand) { + super(replacedStates.getFirst().getBlock(), replacedStates.getFirst(), clicked, itemInHand, thePlayer, canBuild, hand); + this.replacedStates = ImmutableList.copyOf(replacedStates); } /** @@ -41,6 +41,6 @@ public BlockMultiPlaceEvent(@NotNull List states, @NotNull Block cli */ @NotNull public List getReplacedBlockStates() { - return this.states; + return this.replacedStates; } } diff --git a/paper-api/src/main/java/org/bukkit/event/block/BlockPlaceEvent.java b/paper-api/src/main/java/org/bukkit/event/block/BlockPlaceEvent.java index 81dd17b033ea..394ae727e3ee 100644 --- a/paper-api/src/main/java/org/bukkit/event/block/BlockPlaceEvent.java +++ b/paper-api/src/main/java/org/bukkit/event/block/BlockPlaceEvent.java @@ -22,7 +22,7 @@ public class BlockPlaceEvent extends BlockEvent implements Cancellable { protected Block placedAgainst; protected ItemStack itemInHand; protected Player player; - protected BlockState replacedBlockState; + protected BlockState replacedState; protected boolean canBuild; protected EquipmentSlot hand; @@ -30,17 +30,17 @@ public class BlockPlaceEvent extends BlockEvent implements Cancellable { @ApiStatus.Internal @Deprecated(since = "1.9", forRemoval = true) - public BlockPlaceEvent(@NotNull final Block placedBlock, @NotNull final BlockState replacedBlockState, @NotNull final Block placedAgainst, @NotNull final ItemStack itemInHand, @NotNull final Player thePlayer, final boolean canBuild) { - this(placedBlock, replacedBlockState, placedAgainst, itemInHand, thePlayer, canBuild, EquipmentSlot.HAND); + public BlockPlaceEvent(@NotNull final Block placedBlock, @NotNull final BlockState replacedState, @NotNull final Block placedAgainst, @NotNull final ItemStack itemInHand, @NotNull final Player thePlayer, final boolean canBuild) { + this(placedBlock, replacedState, placedAgainst, itemInHand, thePlayer, canBuild, EquipmentSlot.HAND); } @ApiStatus.Internal - public BlockPlaceEvent(@NotNull final Block placedBlock, @NotNull final BlockState replacedBlockState, @NotNull final Block placedAgainst, @NotNull final ItemStack itemInHand, @NotNull final Player thePlayer, final boolean canBuild, @NotNull final EquipmentSlot hand) { + public BlockPlaceEvent(@NotNull final Block placedBlock, @NotNull final BlockState replacedState, @NotNull final Block placedAgainst, @NotNull final ItemStack itemInHand, @NotNull final Player thePlayer, final boolean canBuild, @NotNull final EquipmentSlot hand) { super(placedBlock); this.placedAgainst = placedAgainst; this.itemInHand = itemInHand; this.player = thePlayer; - this.replacedBlockState = replacedBlockState; + this.replacedState = replacedState; this.canBuild = canBuild; this.hand = hand; } @@ -95,7 +95,7 @@ public Block getBlockPlaced() { */ @NotNull public BlockState getBlockReplacedState() { - return this.replacedBlockState; + return this.replacedState; } /** diff --git a/paper-server/patches/features/0032-Block-Capture-System.patch b/paper-server/patches/features/0032-Block-Capture-System.patch new file mode 100644 index 000000000000..2785e9de6995 --- /dev/null +++ b/paper-server/patches/features/0032-Block-Capture-System.patch @@ -0,0 +1,2147 @@ +From 0000000000000000000000000000000000000000 Mon Sep 17 00:00:00 2001 +From: Owen1212055 <23108066+Owen1212055@users.noreply.github.com> +Date: Thu, 5 Feb 2026 16:31:13 -0500 +Subject: [PATCH] Block Capture System + + +diff --git a/net/minecraft/core/dispenser/DispenseItemBehavior.java b/net/minecraft/core/dispenser/DispenseItemBehavior.java +index bfefb5031544caa59230f0073e8880c2b39ebf4d..d7ea3cfb013c503fe903f64df6d6cd61dd0cd4a7 100644 +--- a/net/minecraft/core/dispenser/DispenseItemBehavior.java ++++ b/net/minecraft/core/dispenser/DispenseItemBehavior.java +@@ -400,46 +400,19 @@ public interface DispenseItemBehavior { + this.setSuccess(true); + Level level = blockSource.level(); + BlockPos blockPos = blockSource.pos().relative(blockSource.state().getValue(DispenserBlock.FACING)); +- // Paper start - Call BlockDispenseEvent +- ItemStack result = org.bukkit.craftbukkit.event.CraftEventFactory.handleBlockDispenseEvent(blockSource, blockPos, item, this); +- if (result != null) { +- this.setSuccess(false); +- return result; +- } +- // Paper end - Call BlockDispenseEvent +- level.captureTreeGeneration = true; // CraftBukkit +- if (!BoneMealItem.growCrop(item, level, blockPos) && !BoneMealItem.growWaterPlant(item, level, blockPos, null)) { ++ // Paper start ++ io.papermc.paper.util.capture.GrowthContext growthContext = io.papermc.paper.util.capture.GrowthContext.usingBoneMeal(null); ++ if (!BoneMealItem.growCrop(item, level, blockPos, growthContext) && !org.bukkit.craftbukkit.event.CraftEventFactory.fertilizeBlock( ++ (ServerLevel) level, ++ blockPos, ++ world -> BoneMealItem.growWaterPlant(item, world, blockPos, null, growthContext), ++ growthContext ++ )) { ++ // Paper end + this.setSuccess(false); + } else if (!level.isClientSide()) { + level.levelEvent(LevelEvent.PARTICLES_AND_SOUND_PLANT_GROWTH, blockPos, 15); + } +- // CraftBukkit start +- level.captureTreeGeneration = false; +- if (!level.capturedBlockStates.isEmpty()) { +- org.bukkit.TreeType treeType = net.minecraft.world.level.block.SaplingBlock.treeType; +- net.minecraft.world.level.block.SaplingBlock.treeType = null; +- org.bukkit.Location location = org.bukkit.craftbukkit.util.CraftLocation.toBukkit(blockPos, level); +- List states = new java.util.ArrayList<>(level.capturedBlockStates.values()); +- level.capturedBlockStates.clear(); +- org.bukkit.event.world.StructureGrowEvent structureEvent = null; +- if (treeType != null) { +- structureEvent = new org.bukkit.event.world.StructureGrowEvent(location, treeType, false, null, states); +- org.bukkit.Bukkit.getPluginManager().callEvent(structureEvent); +- } +- +- org.bukkit.event.block.BlockFertilizeEvent fertilizeEvent = new org.bukkit.event.block.BlockFertilizeEvent(location.getBlock(), null, states); +- fertilizeEvent.setCancelled(structureEvent != null && structureEvent.isCancelled()); +- org.bukkit.Bukkit.getPluginManager().callEvent(fertilizeEvent); +- +- if (!fertilizeEvent.isCancelled()) { +- for (org.bukkit.block.BlockState state : states) { +- org.bukkit.craftbukkit.block.CraftBlockState craftBlockState = (org.bukkit.craftbukkit.block.CraftBlockState) state; +- craftBlockState.place(craftBlockState.getFlags()); +- blockSource.level().checkCapturedTreeStateForObserverNotify(blockPos, craftBlockState); // Paper - notify observers even if grow failed +- } +- } +- } +- // CraftBukkit end + + return item; + } +diff --git a/net/minecraft/server/level/ServerLevel.java b/net/minecraft/server/level/ServerLevel.java +index dc65503a2d785d64d37b76b0303f51cf66d9769a..6035304ef0be21d297acc6ca21e3e14c9f61e3ed 100644 +--- a/net/minecraft/server/level/ServerLevel.java ++++ b/net/minecraft/server/level/ServerLevel.java +@@ -181,7 +181,7 @@ import net.minecraft.world.waypoints.WaypointTransmitter; + import org.jspecify.annotations.Nullable; + import org.slf4j.Logger; + +-public class ServerLevel extends Level implements ServerEntityGetter, WorldGenLevel, ca.spottedleaf.moonrise.patches.chunk_system.level.ChunkSystemServerLevel, ca.spottedleaf.moonrise.patches.chunk_system.level.ChunkSystemLevelReader, ca.spottedleaf.moonrise.patches.chunk_tick_iteration.ChunkTickServerLevel { // Paper - rewrite chunk system // Paper - chunk tick iteration ++public class ServerLevel extends Level implements ServerEntityGetter, WorldGenLevel, ca.spottedleaf.moonrise.patches.chunk_system.level.ChunkSystemServerLevel, ca.spottedleaf.moonrise.patches.chunk_system.level.ChunkSystemLevelReader, ca.spottedleaf.moonrise.patches.chunk_tick_iteration.ChunkTickServerLevel, io.papermc.paper.util.capture.ServerLevelPaperCapturingWorldLevel { // Paper - rewrite chunk system // Paper - chunk tick iteration + public static final BlockPos END_SPAWN_POINT = new BlockPos(100, 50, 0); + public static final IntProvider RAIN_DELAY = UniformInt.of(12000, 180000); + public static final IntProvider RAIN_DURATION = UniformInt.of(12000, 24000); +@@ -1857,13 +1857,11 @@ public class ServerLevel extends Level implements ServerEntityGetter, WorldGenLe + return; + } + // CraftBukkit end +- if (captureBlockStates) { return; } // Paper - Cancel all physics during placement + this.updateNeighborsAt(pos, block, ExperimentalRedstoneUtils.initialOrientation(this, null, null)); + } + + @Override + public void updateNeighborsAt(BlockPos pos, Block block, @Nullable Orientation orientation) { +- if (captureBlockStates) { return; } // Paper - Cancel all physics during placement + this.neighborUpdater.updateNeighborsAtExceptFromFacing(pos, block, null, orientation); + } + +@@ -2683,17 +2681,6 @@ public class ServerLevel extends Level implements ServerEntityGetter, WorldGenLe + return range <= 0 ? 64.0 * 64.0 : range * range; // 64 is taken from default in ServerLevel#levelEvent + } + // Paper end - respect global sound events gamerule +- // Paper start - notify observers even if grow failed +- @Deprecated +- public void checkCapturedTreeStateForObserverNotify(final BlockPos pos, final org.bukkit.craftbukkit.block.CraftBlockState craftBlockState) { +- // notify observers if the block state is the same and the Y level equals the original y level (for mega trees) +- // blocks at the same Y level with the same state can be assumed to be saplings which trigger observers regardless of if the +- // tree grew or not +- if (craftBlockState.getPosition().getY() == pos.getY() && this.getBlockState(craftBlockState.getPosition()) == craftBlockState.getHandle()) { +- this.notifyAndUpdatePhysics(craftBlockState.getPosition(), null, craftBlockState.getHandle(), craftBlockState.getHandle(), craftBlockState.getHandle(), craftBlockState.getFlags(), 512); +- } +- } +- // Paper end - notify observers even if grow failed + + @Override + public CrashReportCategory fillReportDetails(CrashReport report) { +diff --git a/net/minecraft/world/entity/ai/behavior/UseBonemeal.java b/net/minecraft/world/entity/ai/behavior/UseBonemeal.java +index d815149ba20d815ec11fd7d4c203aa407e7aa8b2..01c50bddfe74d927bfbaa94d302fd21ca13cce5d 100644 +--- a/net/minecraft/world/entity/ai/behavior/UseBonemeal.java ++++ b/net/minecraft/world/entity/ai/behavior/UseBonemeal.java +@@ -113,7 +113,7 @@ public class UseBonemeal extends Behavior { + } + } + +- if (!itemStack.isEmpty() && BoneMealItem.growCrop(itemStack, level, blockPos)) { ++ if (!itemStack.isEmpty() && BoneMealItem.growCrop(itemStack, level, blockPos, io.papermc.paper.util.capture.GrowthContext.empty())) { // Paper + level.levelEvent(LevelEvent.PARTICLES_AND_SOUND_PLANT_GROWTH, blockPos, 15); + this.cropPos = this.pickNextTarget(level, owner); + this.setCurrentCropAsTarget(owner); +diff --git a/net/minecraft/world/item/BedItem.java b/net/minecraft/world/item/BedItem.java +index 2818c7ad02a861270283384ceb7ecd4d44f6d624..56823e158da5e852d914b06ed24289038c90d685 100644 +--- a/net/minecraft/world/item/BedItem.java ++++ b/net/minecraft/world/item/BedItem.java +@@ -10,7 +10,7 @@ public class BedItem extends BlockItem { + } + + @Override +- protected boolean placeBlock(BlockPlaceContext context, BlockState state) { +- return context.getLevel().setBlock(context.getClickedPos(), state, Block.UPDATE_CLIENTS | Block.UPDATE_IMMEDIATE | Block.UPDATE_KNOWN_SHAPE); ++ protected boolean placeBlock(BlockPlaceContext context, BlockState state, io.papermc.paper.util.capture.PaperCapturingWorldLevel level) { // Paper - block placement capturing ++ return level.setBlock(context.getClickedPos(), state, Block.UPDATE_CLIENTS | Block.UPDATE_IMMEDIATE | Block.UPDATE_KNOWN_SHAPE); // Paper - block placement capturing + } + } +diff --git a/net/minecraft/world/item/BlockItem.java b/net/minecraft/world/item/BlockItem.java +index 73ce7c82c0bd28c2e43ca40ba35c4603b21375ad..58ee47c73c4f6a21852cf459de90f53205c0dace 100644 +--- a/net/minecraft/world/item/BlockItem.java ++++ b/net/minecraft/world/item/BlockItem.java +@@ -57,59 +57,30 @@ public class BlockItem extends Item { + return InteractionResult.FAIL; + } else { + BlockState placementState = this.getPlacementState(blockPlaceContext); +- // CraftBukkit start - special case for handling block placement with water lilies and snow buckets +- org.bukkit.block.BlockState bukkitState = null; +- if (this instanceof PlaceOnWaterBlockItem || this instanceof SolidBucketItem) { +- bukkitState = org.bukkit.craftbukkit.block.CraftBlockStates.getBlockState(blockPlaceContext.getLevel(), blockPlaceContext.getClickedPos()); +- } +- final org.bukkit.block.BlockState oldBukkitState = bukkitState != null ? bukkitState : org.bukkit.craftbukkit.block.CraftBlockStates.getBlockState(blockPlaceContext.getLevel(), blockPlaceContext.getClickedPos()); // Paper - Reset placed block on exception +- // CraftBukkit end +- + if (placementState == null) { + return InteractionResult.FAIL; +- } else if (!this.placeBlock(blockPlaceContext, placementState)) { +- return InteractionResult.FAIL; +- } else { ++ } ++ // Paper start ++ try (io.papermc.paper.util.capture.SimpleBlockCapture capture = ((net.minecraft.server.level.ServerLevel) context.getLevel()).forkCaptureSession()) { ++ if (!this.placeBlock(blockPlaceContext, placementState, capture.capturingWorldLevel())) { ++ return InteractionResult.FAIL; ++ } ++ // Paper end ++ + BlockPos clickedPos = blockPlaceContext.getClickedPos(); +- Level level = blockPlaceContext.getLevel(); ++ io.papermc.paper.util.capture.PaperCapturingWorldLevel level = capture.capturingWorldLevel(); // Paper + Player player = blockPlaceContext.getPlayer(); + ItemStack itemInHand = blockPlaceContext.getItemInHand(); + BlockState blockState = level.getBlockState(clickedPos); + if (blockState.is(placementState.getBlock())) { + blockState = this.updateBlockStateFromTag(clickedPos, level, itemInHand, blockState); +- // Paper start - Reset placed block on exception +- try { + this.updateCustomBlockEntityTag(clickedPos, level, player, itemInHand, blockState); + updateBlockEntityComponents(level, clickedPos, itemInHand); +- } catch (Exception ex) { +- ((org.bukkit.craftbukkit.block.CraftBlockState) oldBukkitState).revertPlace(); +- if (player instanceof ServerPlayer serverPlayer) { +- org.apache.logging.log4j.LogManager.getLogger().error("Player {} tried placing invalid block", player.getScoreboardName(), ex); +- serverPlayer.getBukkitEntity().kickPlayer("Packet processing error"); +- return InteractionResult.FAIL; +- } +- throw ex; // Rethrow exception if not placed by a player +- } +- // Paper end - Reset placed block on exception + blockState.getBlock().setPlacedBy(level, clickedPos, blockState, player, itemInHand); +- // CraftBukkit start +- if (bukkitState != null) { +- org.bukkit.event.block.BlockPlaceEvent placeEvent = org.bukkit.craftbukkit.event.CraftEventFactory.callBlockPlaceEvent((net.minecraft.server.level.ServerLevel) level, player, blockPlaceContext.getHand(), bukkitState, clickedPos); +- if (placeEvent != null && (placeEvent.isCancelled() || !placeEvent.canBuild())) { +- ((org.bukkit.craftbukkit.block.CraftBlockState) bukkitState).revertPlace(); +- +- player.containerMenu.forceHeldSlot(blockPlaceContext.getHand()); +- return InteractionResult.FAIL; +- } +- } +- // CraftBukkit end +- if (player instanceof ServerPlayer) { +- CriteriaTriggers.PLACED_BLOCK.trigger((ServerPlayer)player, clickedPos, itemInHand); +- } ++ // Paper - move trigger + } + + SoundType soundType = blockState.getSoundType(); +- if (player == null) // Paper - Fix block place logic; reintroduce this for the dispenser (i.e the shulker) + level.playSound( + player, + clickedPos, +@@ -119,8 +90,40 @@ public class BlockItem extends Item { + soundType.getPitch() * 0.8F + ); + level.gameEvent(GameEvent.BLOCK_PLACE, clickedPos, GameEvent.Context.of(player, blockState)); ++ // Paper start ++ if (player != null) { // todo dispensed shulker ++ net.minecraft.world.InteractionHand hand = blockPlaceContext.getHand(); ++ net.minecraft.server.level.ServerLevel serverLevel = (net.minecraft.server.level.ServerLevel) blockPlaceContext.getLevel(); ++ ++ java.util.List blocks = capture.getAffectedBlocks().map(org.bukkit.block.Block::getState).toList(); ++ BlockPos relativePos = context.getHitResult().getBlockPos(); ++ if (this instanceof PlaceOnWaterBlockItem) { // see PlaceOnWaterBlockItem#use ++ relativePos = relativePos.below(); ++ } ++ capture.overlayCaptureOnLevel(); ++ ++ final org.bukkit.event.block.BlockPlaceEvent placeEvent; ++ if (blocks.size() > 1) { ++ placeEvent = org.bukkit.craftbukkit.event.CraftEventFactory.callBlockMultiPlaceEvent(serverLevel, player, hand, blocks, relativePos); ++ } else { ++ placeEvent = org.bukkit.craftbukkit.event.CraftEventFactory.callBlockPlaceEvent(serverLevel, player, hand, blocks.getFirst(), relativePos); ++ } ++ ++ if (placeEvent.isCancelled() || !placeEvent.canBuild()) { ++ // PAIL: Remove this when MC-99075 fixed ++ player.containerMenu.forceHeldSlot(hand); ++ return InteractionResult.FAIL; ++ } ++ // We are good! ++ capture.finalizePlacement(); ++ } ++ if (player instanceof ServerPlayer) { ++ CriteriaTriggers.PLACED_BLOCK.trigger((ServerPlayer)player, clickedPos, itemInHand); ++ } ++ // Paper end ++ + itemInHand.consume(1, player); +- return InteractionResult.SUCCESS.configurePaper(e -> e.placedBlockAt(clickedPos.immutable())); // Paper - track placed block position from block item ++ return InteractionResult.SUCCESS; + } + } + } +@@ -134,7 +137,7 @@ public class BlockItem extends Item { + return context; + } + +- private static void updateBlockEntityComponents(Level level, BlockPos pos, ItemStack stack) { ++ private static void updateBlockEntityComponents(io.papermc.paper.util.capture.PaperCapturingWorldLevel level, BlockPos pos, ItemStack stack) { // Paper - Canceling block placement + BlockEntity blockEntity = level.getBlockEntity(pos); + if (blockEntity != null) { + blockEntity.applyComponentsFromItemStack(stack); +@@ -142,7 +145,7 @@ public class BlockItem extends Item { + } + } + +- protected boolean updateCustomBlockEntityTag(BlockPos pos, Level level, @Nullable Player player, ItemStack stack, BlockState state) { ++ protected boolean updateCustomBlockEntityTag(BlockPos pos, io.papermc.paper.util.capture.PaperCapturingWorldLevel level, @Nullable Player player, ItemStack stack, BlockState state) { // Paper - Canceling block placement + return updateCustomBlockEntityTag(level, player, pos, stack); + } + +@@ -151,7 +154,7 @@ public class BlockItem extends Item { + return stateForPlacement != null && this.canPlace(context, stateForPlacement) ? stateForPlacement : null; + } + +- private BlockState updateBlockStateFromTag(BlockPos pos, Level level, ItemStack stack, BlockState state) { ++ private BlockState updateBlockStateFromTag(BlockPos pos, io.papermc.paper.util.capture.PaperCapturingWorldLevel level, ItemStack stack, BlockState state) { // Paper - Canceling block placement + BlockItemStateProperties blockItemStateProperties = stack.getOrDefault(DataComponents.BLOCK_STATE, BlockItemStateProperties.EMPTY); + if (blockItemStateProperties.isEmpty()) { + return state; +@@ -186,11 +189,11 @@ public class BlockItem extends Item { + return true; + } + +- protected boolean placeBlock(BlockPlaceContext context, BlockState state) { +- return context.getLevel().setBlock(context.getClickedPos(), state, Block.UPDATE_ALL_IMMEDIATE); ++ protected boolean placeBlock(BlockPlaceContext context, BlockState state, io.papermc.paper.util.capture.PaperCapturingWorldLevel level) { // Paper - block placement capturing ++ return level.setBlock(context.getClickedPos(), state, Block.UPDATE_ALL_IMMEDIATE); // Paper - block placement capturing + } + +- public static boolean updateCustomBlockEntityTag(Level level, @Nullable Player player, BlockPos pos, ItemStack stack) { ++ public static boolean updateCustomBlockEntityTag(io.papermc.paper.util.capture.PaperCapturingWorldLevel level, @Nullable Player player, BlockPos pos, ItemStack stack) { // Paper - Canceling block placement + if (level.isClientSide()) { + return false; + } else { +diff --git a/net/minecraft/world/item/BoneMealItem.java b/net/minecraft/world/item/BoneMealItem.java +index 4490e50f02d2c7383e86951cb98a113d5d4f36c2..f1706143f27a3ea8e24d2780954beb3ee7fc0785 100644 +--- a/net/minecraft/world/item/BoneMealItem.java ++++ b/net/minecraft/world/item/BoneMealItem.java +@@ -44,7 +44,10 @@ public class BoneMealItem extends Item { + BlockPos clickedPos = context.getClickedPos(); + BlockPos blockPos = clickedPos.relative(context.getClickedFace()); + ItemStack itemInHand = context.getItemInHand(); +- if (growCrop(itemInHand, level, clickedPos)) { ++ // Paper start ++ io.papermc.paper.util.capture.GrowthContext growthContext = io.papermc.paper.util.capture.GrowthContext.usingBoneMeal(context.getPlayer()); ++ if (growCrop(itemInHand, level, clickedPos, growthContext)) { ++ // Paper end + if (!level.isClientSide()) { + if (context.getPlayer() != null) itemInHand.causeUseVibration(context.getPlayer(), GameEvent.ITEM_INTERACT_FINISH); // CraftBukkit - SPIGOT-7518 + level.levelEvent(LevelEvent.PARTICLES_AND_SOUND_PLANT_GROWTH, clickedPos, 15); +@@ -54,7 +57,14 @@ public class BoneMealItem extends Item { + } else { + BlockState blockState = level.getBlockState(clickedPos); + boolean isFaceSturdy = blockState.isFaceSturdy(level, clickedPos, context.getClickedFace()); +- if (isFaceSturdy && growWaterPlant(itemInHand, level, blockPos, context.getClickedFace())) { ++ // Paper start ++ if (isFaceSturdy && org.bukkit.craftbukkit.event.CraftEventFactory.fertilizeBlock( ++ (ServerLevel) level, ++ blockPos, ++ world -> growWaterPlant(itemInHand, world, blockPos, context.getClickedFace(), growthContext), ++ growthContext ++ )) { ++ // Paper end + if (!level.isClientSide()) { + if (context.getPlayer() != null) itemInHand.causeUseVibration(context.getPlayer(), GameEvent.ITEM_INTERACT_FINISH); // CraftBukkit - SPIGOT-7518 + level.levelEvent(LevelEvent.PARTICLES_AND_SOUND_PLANT_GROWTH, blockPos, 15); +@@ -67,12 +77,24 @@ public class BoneMealItem extends Item { + } + } + +- public static boolean growCrop(ItemStack stack, Level level, BlockPos pos) { ++ public static boolean growCrop(ItemStack stack, Level level, BlockPos pos, io.papermc.paper.util.capture.GrowthContext growthContext) { // Paper + BlockState blockState = level.getBlockState(pos); + if (blockState.getBlock() instanceof BonemealableBlock bonemealableBlock && bonemealableBlock.isValidBonemealTarget(level, pos, blockState)) { + if (level instanceof ServerLevel) { + if (bonemealableBlock.isBonemealSuccess(level, level.random, pos, blockState)) { +- bonemealableBlock.performBonemeal((ServerLevel)level, level.random, pos, blockState); ++ // Paper start ++ if (!org.bukkit.craftbukkit.event.CraftEventFactory.fertilizeBlock( ++ (ServerLevel) level, ++ pos, ++ world -> { ++ bonemealableBlock.performBonemeal(world, level.random, pos, blockState, growthContext); ++ return true; ++ }, ++ growthContext ++ )) { ++ return false; ++ } ++ // Paper end + } + + stack.shrink(1); +@@ -84,9 +106,9 @@ public class BoneMealItem extends Item { + } + } + +- public static boolean growWaterPlant(ItemStack stack, Level level, BlockPos pos, @Nullable Direction clickedSide) { ++ public static boolean growWaterPlant(ItemStack stack, io.papermc.paper.util.capture.PaperCapturingWorldLevel level, BlockPos pos, @Nullable Direction clickedSide, io.papermc.paper.util.capture.GrowthContext growthContext) { // Paper + if (level.getBlockState(pos).is(Blocks.WATER) && level.getFluidState(pos).getAmount() == 8) { +- if (!(level instanceof ServerLevel)) { ++ if (false && !(level instanceof ServerLevel)) { // Paper + return true; + } else { + RandomSource random = level.getRandom(); +@@ -107,7 +129,7 @@ public class BoneMealItem extends Item { + if (biome.is(BiomeTags.PRODUCES_CORALS_FROM_BONEMEAL)) { + if (i == 0 && clickedSide != null && clickedSide.getAxis().isHorizontal()) { + blockState = BuiltInRegistries.BLOCK +- .getRandomElementOf(BlockTags.WALL_CORALS, level.random) ++ .getRandomElementOf(BlockTags.WALL_CORALS, level.getRandom()) // Paper + .map(holder -> holder.value().defaultBlockState()) + .orElse(blockState); + if (blockState.hasProperty(BaseCoralWallFanBlock.FACING)) { +@@ -115,7 +137,7 @@ public class BoneMealItem extends Item { + } + } else if (random.nextInt(4) == 0) { + blockState = BuiltInRegistries.BLOCK +- .getRandomElementOf(BlockTags.UNDERWATER_BONEMEALS, level.random) ++ .getRandomElementOf(BlockTags.UNDERWATER_BONEMEALS, level.getRandom()) // Paper + .map(holder -> holder.value().defaultBlockState()) + .orElse(blockState); + } +@@ -134,12 +156,12 @@ public class BoneMealItem extends Item { + } else if (blockState1.is(Blocks.SEAGRASS) + && ((BonemealableBlock)Blocks.SEAGRASS).isValidBonemealTarget(level, blockPos, blockState1) + && random.nextInt(10) == 0) { +- ((BonemealableBlock)Blocks.SEAGRASS).performBonemeal((ServerLevel)level, random, blockPos, blockState1); ++ ((BonemealableBlock)Blocks.SEAGRASS).performBonemeal(level, random, blockPos, blockState1, growthContext); // Paper + } + } + } + +- stack.shrink(1); ++ level.addTask($ -> stack.shrink(1)); // Paper + return true; + } + } else { +diff --git a/net/minecraft/world/item/DoubleHighBlockItem.java b/net/minecraft/world/item/DoubleHighBlockItem.java +index e6406e4d7ed3c340e3e3137165e9a2fa7e4f3656..6b245cf238851900c409fa2f0c885c6ebee69a91 100644 +--- a/net/minecraft/world/item/DoubleHighBlockItem.java ++++ b/net/minecraft/world/item/DoubleHighBlockItem.java +@@ -13,11 +13,10 @@ public class DoubleHighBlockItem extends BlockItem { + } + + @Override +- protected boolean placeBlock(BlockPlaceContext context, BlockState state) { +- Level level = context.getLevel(); ++ protected boolean placeBlock(BlockPlaceContext context, BlockState state, io.papermc.paper.util.capture.PaperCapturingWorldLevel level) { // Paper - block placement capturing + BlockPos blockPos = context.getClickedPos().above(); + BlockState blockState = level.isWaterAt(blockPos) ? Blocks.WATER.defaultBlockState() : Blocks.AIR.defaultBlockState(); + level.setBlock(blockPos, blockState, Block.UPDATE_ALL_IMMEDIATE | Block.UPDATE_KNOWN_SHAPE); +- return super.placeBlock(context, state); ++ return super.placeBlock(context, state, level); // Paper - block placement capturing + } + } +diff --git a/net/minecraft/world/item/ItemStack.java b/net/minecraft/world/item/ItemStack.java +index ed06cffe8a5eba2ca4a34ade81f8185e21d7b651..25a094bc33546bc71bbe4ad348bd954a0bb6e0e3 100644 +--- a/net/minecraft/world/item/ItemStack.java ++++ b/net/minecraft/world/item/ItemStack.java +@@ -379,166 +379,10 @@ public final class ItemStack implements DataComponentHolder { + return InteractionResult.PASS; + } else { + Item item = this.getItem(); +- // CraftBukkit start - handle all block place event logic here +- DataComponentPatch previousPatch = this.components.asPatch(); +- int oldCount = this.getCount(); +- ServerLevel serverLevel = (ServerLevel) context.getLevel(); +- +- if (!(item instanceof BucketItem/* || item instanceof SolidBucketItem*/)) { // if not bucket // Paper - Fix cancelled powdered snow bucket placement +- serverLevel.captureBlockStates = true; +- // special case bonemeal +- if (item == Items.BONE_MEAL) { +- serverLevel.captureTreeGeneration = true; +- } +- } +- InteractionResult interactionResult; +- try { +- interactionResult = item.useOn(context); +- } finally { +- serverLevel.captureBlockStates = false; +- } +- DataComponentPatch newPatch = this.components.asPatch(); +- int newCount = this.getCount(); +- this.setCount(oldCount); +- this.restorePatch(previousPatch); +- if (interactionResult.consumesAction() && serverLevel.captureTreeGeneration && !serverLevel.capturedBlockStates.isEmpty()) { +- serverLevel.captureTreeGeneration = false; +- org.bukkit.Location location = org.bukkit.craftbukkit.util.CraftLocation.toBukkit(clickedPos, serverLevel); +- org.bukkit.TreeType treeType = net.minecraft.world.level.block.SaplingBlock.treeType; +- net.minecraft.world.level.block.SaplingBlock.treeType = null; +- List blocks = new java.util.ArrayList<>(serverLevel.capturedBlockStates.values()); +- serverLevel.capturedBlockStates.clear(); +- org.bukkit.event.world.StructureGrowEvent structureEvent = null; +- if (treeType != null) { +- boolean isBonemeal = this.getItem() == Items.BONE_MEAL; +- structureEvent = new org.bukkit.event.world.StructureGrowEvent(location, treeType, isBonemeal, (org.bukkit.entity.Player) player.getBukkitEntity(), (List) (List) blocks); +- org.bukkit.Bukkit.getPluginManager().callEvent(structureEvent); +- } +- +- org.bukkit.event.block.BlockFertilizeEvent fertilizeEvent = new org.bukkit.event.block.BlockFertilizeEvent(org.bukkit.craftbukkit.block.CraftBlock.at(serverLevel, clickedPos), (org.bukkit.entity.Player) player.getBukkitEntity(), (List) (List) blocks); +- fertilizeEvent.setCancelled(structureEvent != null && structureEvent.isCancelled()); +- org.bukkit.Bukkit.getPluginManager().callEvent(fertilizeEvent); +- +- if (!fertilizeEvent.isCancelled()) { +- // Change the stack to its new contents if it hasn't been tampered with. +- if (this.getCount() == oldCount && Objects.equals(this.components.asPatch(), previousPatch)) { +- this.restorePatch(newPatch); +- this.setCount(newCount); +- } +- for (org.bukkit.craftbukkit.block.CraftBlockState snapshot : blocks) { +- // SPIGOT-7572 - Move fix for SPIGOT-7248 to CapturedBlockState, to allow bees in bee nest +- snapshot.place(snapshot.getFlags()); +- serverLevel.checkCapturedTreeStateForObserverNotify(clickedPos, snapshot); // Paper - notify observers even if grow failed +- } +- player.awardStat(Stats.ITEM_USED.get(item)); // SPIGOT-7236 - award stat +- } +- +- SignItem.openSign = null; // SPIGOT-6758 - Reset on early return +- return interactionResult; +- } +- serverLevel.captureTreeGeneration = false; ++ InteractionResult interactionResult = item.useOn(context); + if (player != null && interactionResult instanceof InteractionResult.Success success && success.wasItemInteraction()) { +- InteractionHand hand = context.getHand(); +- org.bukkit.event.block.BlockPlaceEvent placeEvent = null; +- List blocks = new java.util.ArrayList<>(serverLevel.capturedBlockStates.values()); +- serverLevel.capturedBlockStates.clear(); +- if (blocks.size() > 1) { +- placeEvent = org.bukkit.craftbukkit.event.CraftEventFactory.callBlockMultiPlaceEvent(serverLevel, player, hand, blocks, clickedPos); +- } else if (blocks.size() == 1 && item != Items.POWDER_SNOW_BUCKET) { // Paper - Fix cancelled powdered snow bucket placement +- placeEvent = org.bukkit.craftbukkit.event.CraftEventFactory.callBlockPlaceEvent(serverLevel, player, hand, blocks.getFirst(), clickedPos); +- } +- +- if (placeEvent != null && (placeEvent.isCancelled() || !placeEvent.canBuild())) { +- interactionResult = InteractionResult.FAIL; // cancel placement +- // PAIL: Remove this when MC-99075 fixed +- player.containerMenu.forceHeldSlot(hand); +- serverLevel.capturedTileEntities.clear(); // Paper - Allow chests to be placed with NBT data; clear out block entities as chests and such will pop loot +- // revert back all captured blocks +- for (org.bukkit.block.BlockState blockstate : blocks) { +- ((org.bukkit.craftbukkit.block.CraftBlockState) blockstate).revertPlace(); +- } +- +- SignItem.openSign = null; // SPIGOT-6758 - Reset on early return +- } else { +- // Change the stack to its new contents if it hasn't been tampered with. +- if (this.getCount() == oldCount && Objects.equals(this.components.asPatch(), previousPatch)) { +- this.restorePatch(newPatch); +- this.setCount(newCount); +- } +- +- for (java.util.Map.Entry e : serverLevel.capturedTileEntities.entrySet()) { +- serverLevel.setBlockEntity(e.getValue()); +- } +- +- for (org.bukkit.block.BlockState blockstate : blocks) { +- int updateFlags = ((org.bukkit.craftbukkit.block.CraftBlockState) blockstate).getFlags(); +- net.minecraft.world.level.block.state.BlockState oldBlock = ((org.bukkit.craftbukkit.block.CraftBlockState) blockstate).getHandle(); +- BlockPos newPos = ((org.bukkit.craftbukkit.block.CraftBlockState) blockstate).getPosition(); +- net.minecraft.world.level.block.state.BlockState block = serverLevel.getBlockState(newPos); +- +- if (!(block.getBlock() instanceof net.minecraft.world.level.block.BaseEntityBlock)) { // Containers get placed automatically +- block.onPlace(serverLevel, newPos, oldBlock, true, context); +- } +- +- serverLevel.notifyAndUpdatePhysics(newPos, null, oldBlock, block, serverLevel.getBlockState(newPos), updateFlags, net.minecraft.world.level.block.Block.UPDATE_LIMIT); // send null chunk as chunk.k() returns false by this point +- } +- +- if (this.item == Items.WITHER_SKELETON_SKULL) { // Special case skulls to allow wither spawns to be cancelled +- BlockPos bp = clickedPos; +- if (!serverLevel.getBlockState(clickedPos).canBeReplaced()) { +- if (!serverLevel.getBlockState(clickedPos).isSolid()) { +- bp = null; +- } else { +- bp = bp.relative(context.getClickedFace()); +- } +- } +- if (bp != null) { +- net.minecraft.world.level.block.entity.BlockEntity te = serverLevel.getBlockEntity(bp); +- if (te instanceof net.minecraft.world.level.block.entity.SkullBlockEntity) { +- net.minecraft.world.level.block.WitherSkullBlock.checkSpawn(serverLevel, bp, (net.minecraft.world.level.block.entity.SkullBlockEntity) te); +- } +- } +- } +- +- // SPIGOT-4678 +- if (this.item instanceof SignItem && SignItem.openSign != null) { +- try { +- if (serverLevel.getBlockEntity(SignItem.openSign) instanceof net.minecraft.world.level.block.entity.SignBlockEntity blockEntity) { +- if (serverLevel.getBlockState(SignItem.openSign).getBlock() instanceof net.minecraft.world.level.block.SignBlock signBlock) { +- signBlock.openTextEdit(player, blockEntity, true, io.papermc.paper.event.player.PlayerOpenSignEvent.Cause.PLACE); // CraftBukkit // Paper - Add PlayerOpenSignEvent +- } +- } +- } finally { +- SignItem.openSign = null; +- } +- } +- +- // SPIGOT-7315: Moved from BedBlock#setPlacedBy +- if (placeEvent != null && this.item instanceof BedItem) { +- BlockPos pos = ((org.bukkit.craftbukkit.block.CraftBlock) placeEvent.getBlock()).getPosition(); +- net.minecraft.world.level.block.state.BlockState state = serverLevel.getBlockState(pos); +- +- if (state.getBlock() instanceof net.minecraft.world.level.block.BedBlock) { +- serverLevel.updateNeighborsAt(pos, net.minecraft.world.level.block.Blocks.AIR); +- state.updateNeighbourShapes(serverLevel, pos, net.minecraft.world.level.block.Block.UPDATE_ALL); +- } +- } +- +- // SPIGOT-1288 - play sound stripped from BlockItem +- if (this.item instanceof BlockItem && success.paperSuccessContext().placedBlockPosition() != null) { +- // Paper start - Fix spigot sound playing for BlockItem ItemStacks +- net.minecraft.world.level.block.state.BlockState state = serverLevel.getBlockState(success.paperSuccessContext().placedBlockPosition()); +- net.minecraft.world.level.block.SoundType soundType = state.getSoundType(); +- // Paper end - Fix spigot sound playing for BlockItem ItemStacks +- serverLevel.playSound(player, clickedPos, soundType.getPlaceSound(), net.minecraft.sounds.SoundSource.BLOCKS, (soundType.getVolume() + 1.0F) / 2.0F, soundType.getPitch() * 0.8F); +- } +- +- player.awardStat(Stats.ITEM_USED.get(item)); +- } ++ player.awardStat(Stats.ITEM_USED.get(item)); + } +- serverLevel.capturedTileEntities.clear(); +- serverLevel.capturedBlockStates.clear(); +- // CraftBukkit end + + return interactionResult; + } +diff --git a/net/minecraft/world/item/SignItem.java b/net/minecraft/world/item/SignItem.java +index 3a41cf80e347ae3b30858879fb91f719625c8bb6..15c5cea7371350115c4d53414cf3f38e66a124f9 100644 +--- a/net/minecraft/world/item/SignItem.java ++++ b/net/minecraft/world/item/SignItem.java +@@ -11,7 +11,7 @@ import net.minecraft.world.level.block.state.BlockState; + import org.jspecify.annotations.Nullable; + + public class SignItem extends StandingAndWallBlockItem { +- public static BlockPos openSign; // CraftBukkit ++ + public SignItem(Block standingBlock, Block wallBlock, Item.Properties properties) { + super(standingBlock, wallBlock, Direction.DOWN, properties); + } +@@ -21,17 +21,14 @@ public class SignItem extends StandingAndWallBlockItem { + } + + @Override +- protected boolean updateCustomBlockEntityTag(BlockPos pos, Level level, @Nullable Player player, ItemStack stack, BlockState state) { ++ protected boolean updateCustomBlockEntityTag(BlockPos pos, io.papermc.paper.util.capture.PaperCapturingWorldLevel level, @Nullable Player player, ItemStack stack, BlockState state) { // Paper + boolean flag = super.updateCustomBlockEntityTag(pos, level, player, stack, state); + if (!level.isClientSide() + && !flag + && player != null + && level.getBlockEntity(pos) instanceof SignBlockEntity signBlockEntity + && level.getBlockState(pos).getBlock() instanceof SignBlock signBlock) { +- // CraftBukkit start - SPIGOT-4678 +- // signBlock.openTextEdit(player, signBlockEntity, true); +- SignItem.openSign = pos; +- // CraftBukkit end ++ level.addTask($ -> signBlock.openTextEdit(player, signBlockEntity, true, io.papermc.paper.event.player.PlayerOpenSignEvent.Cause.PLACE)); // Paper + } + + return flag; +diff --git a/net/minecraft/world/item/context/UseOnContext.java b/net/minecraft/world/item/context/UseOnContext.java +index fc841780fe6e0ca2aca119ce3ba78da8e1c10d77..cd5137fdd17a1403c4dbc5b91a22002478a859e4 100644 +--- a/net/minecraft/world/item/context/UseOnContext.java ++++ b/net/minecraft/world/item/context/UseOnContext.java +@@ -29,7 +29,7 @@ public class UseOnContext { + this.level = level; + } + +- protected final BlockHitResult getHitResult() { ++ public final BlockHitResult getHitResult() { // PAPER: TODO AT + return this.hitResult; + } + +diff --git a/net/minecraft/world/level/Level.java b/net/minecraft/world/level/Level.java +index 579bbba4e823d4d0318e58759ca732b7c8e4d865..cdebff0efb78b98b941a8aac2cd1115b9d593b52 100644 +--- a/net/minecraft/world/level/Level.java ++++ b/net/minecraft/world/level/Level.java +@@ -143,10 +143,7 @@ public abstract class Level implements LevelAccessor, AutoCloseable, ca.spottedl + private final CraftWorld world; + public org.bukkit.generator.@Nullable ChunkGenerator generator; + +- public boolean captureBlockStates = false; +- public boolean captureTreeGeneration = false; +- public Map capturedBlockStates = new java.util.LinkedHashMap<>(); // Paper +- public Map capturedTileEntities = new java.util.LinkedHashMap<>(); // Paper - Retain block place order when capturing blockstates ++ public final io.papermc.paper.util.capture.WorldCapturer capturer = new io.papermc.paper.util.capture.WorldCapturer(this); + @Nullable + public List captureDrops; + public final it.unimi.dsi.fastutil.objects.Object2LongOpenHashMap ticksPerSpawnCategory = new it.unimi.dsi.fastutil.objects.Object2LongOpenHashMap<>(); +@@ -978,14 +975,14 @@ public abstract class Level implements LevelAccessor, AutoCloseable, ca.spottedl + @Override + @Nullable + public final BlockState getBlockStateIfLoaded(BlockPos pos) { +- // CraftBukkit start - tree generation +- if (this.captureTreeGeneration) { +- CraftBlockState previous = this.capturedBlockStates.get(pos); +- if (previous != null) { +- return previous.getHandle(); ++ // Paper start ++ if (this.capturer.isCapturing() && this.capturer.getCapture().isOverlayingCaptureOnLevel()) { ++ BlockState guess = this.capturer.getCapture().getCaptureBlockStateIfLoaded(pos); ++ if (guess != null) { ++ return guess; + } + } +- // CraftBukkit end ++ // Paper end + if (this.isOutsideBuildHeight(pos)) { + return Blocks.VOID_AIR.defaultBlockState(); + } else { +@@ -1043,22 +1040,11 @@ public abstract class Level implements LevelAccessor, AutoCloseable, ca.spottedl + + @Override + public boolean setBlock(BlockPos pos, BlockState state, @Block.UpdateFlags int flags, int recursionLeft) { +- // CraftBukkit start - tree generation +- if (this.captureTreeGeneration) { +- // Paper start - Protect Bedrock and End Portal/Frames from being destroyed +- BlockState type = getBlockState(pos); +- if (!type.isDestroyable()) return false; +- // Paper end - Protect Bedrock and End Portal/Frames from being destroyed +- CraftBlockState blockstate = this.capturedBlockStates.get(pos); +- if (blockstate == null) { +- blockstate = org.bukkit.craftbukkit.block.CapturedBlockState.getTreeBlockState(this, pos, flags); +- this.capturedBlockStates.put(pos.immutable(), blockstate); +- } +- blockstate.setData(state); +- blockstate.setFlags(flags); +- return true; ++ // Paper start ++ if (this.capturer.isCapturing() && this.capturer.getCapture().isOverlayingCaptureOnLevel()) { ++ return this.capturer.getCapture().capturingWorldLevel().setBlockSilent(pos, state, flags, recursionLeft); + } +- // CraftBukkit end ++ // Paper end + if (!this.isInValidBounds(pos)) { + return false; + } else if (!this.isClientSide() && this.isDebug()) { +@@ -1066,32 +1052,12 @@ public abstract class Level implements LevelAccessor, AutoCloseable, ca.spottedl + } else { + LevelChunk chunkAt = this.getChunkAt(pos); + Block block = state.getBlock(); +- // CraftBukkit start - capture blockstates +- boolean captured = false; +- if (this.captureBlockStates) { +- final CraftBlockState snapshot; +- if (!this.capturedBlockStates.containsKey(pos)) { +- snapshot = (CraftBlockState) org.bukkit.craftbukkit.block.CraftBlock.at(this, pos).getState(); // Paper - use CB getState to get a suitable snapshot +- this.capturedBlockStates.put(pos.immutable(), snapshot); +- captured = true; +- } else { +- snapshot = this.capturedBlockStates.get(pos); +- } +- snapshot.setFlags(flags); // Paper - always set the flag of the most recent call to mitigate issues with multiple update at the same pos with different flags +- } + BlockState blockState = chunkAt.setBlockState(pos, state, flags); + this.chunkPacketBlockController.onBlockChange(this, pos, state, blockState, flags, recursionLeft); // Paper - Anti-Xray +- // CraftBukkit end + if (blockState == null) { +- // CraftBukkit start - remove blockstate if failed (or the same) +- if (this.captureBlockStates && captured) { +- this.capturedBlockStates.remove(pos); +- } +- // CraftBukkit end + return false; + } else { + BlockState blockState1 = this.getBlockState(pos); +- /* + if (blockState1 == state) { + if (blockState != blockState1) { + this.setBlocksDirty(pos, blockState, blockState1); +@@ -1113,73 +1079,27 @@ public abstract class Level implements LevelAccessor, AutoCloseable, ca.spottedl + if ((flags & Block.UPDATE_KNOWN_SHAPE) == 0 && recursionLeft > 0) { + int i = flags & ~(Block.UPDATE_SUPPRESS_DROPS | Block.UPDATE_NEIGHBORS); + blockState.updateIndirectNeighbourShapes(this, pos, i, recursionLeft - 1); ++ // Paper start - call BlockPhysicsEvent ++ // Don't call an event for the old block to limit event spam ^ ++ boolean cancelledUpdates = false; ++ if (((ServerLevel)this).hasPhysicsEvent) { ++ org.bukkit.event.block.BlockPhysicsEvent event = new org.bukkit.event.block.BlockPhysicsEvent(org.bukkit.craftbukkit.block.CraftBlock.at(this, pos), CraftBlockData.fromData(state)); ++ cancelledUpdates = !event.callEvent(); ++ } ++ if (!cancelledUpdates) { ++ // Paper end - call BlockPhysicsEvent + state.updateNeighbourShapes(this, pos, i, recursionLeft - 1); + state.updateIndirectNeighbourShapes(this, pos, i, recursionLeft - 1); ++ } // Paper - call BlockPhysicsEvent + } + + this.updatePOIOnBlockStateChange(pos, blockState, blockState1); + } +- */ +- +- // CraftBukkit start +- if (!this.captureBlockStates) { // Don't notify clients or update physics while capturing blockstates +- // Modularize client and physic updates +- // Spigot start +- try { +- this.notifyAndUpdatePhysics(pos, chunkAt, blockState, state, blockState1, flags, recursionLeft); +- } catch (StackOverflowError ex) { +- Level.lastPhysicsProblem = pos.immutable(); +- } +- // Spigot end +- } +- // CraftBukkit end + + return true; + } + } + } +- +- // CraftBukkit start - Split off from above in order to directly send client and physic updates +- public void notifyAndUpdatePhysics(BlockPos pos, LevelChunk chunkAt, BlockState oldState, BlockState newState, BlockState currentState, @Block.UpdateFlags int flags, int recursionLeft) { +- BlockState state = newState; +- BlockState blockState = oldState; +- BlockState blockState1 = currentState; +- if (blockState1 == state) { +- if (blockState != blockState1) { +- this.setBlocksDirty(pos, blockState, blockState1); +- } +- +- if ((flags & Block.UPDATE_CLIENTS) != 0 && (!this.isClientSide() || (flags & Block.UPDATE_INVISIBLE) == 0) && (this.isClientSide() || chunkAt == null || (chunkAt.getFullStatus() != null && chunkAt.getFullStatus().isOrAfter(FullChunkStatus.FULL)))) { // allow chunk to be null here as chunk.isReady() is false when we send our notification during block placement // Paper - rewrite chunk system - change from ticking to full +- this.sendBlockUpdated(pos, blockState, state, flags); +- } +- +- if ((flags & Block.UPDATE_NEIGHBORS) != 0) { +- this.updateNeighborsAt(pos, blockState.getBlock()); +- if (!this.isClientSide() && state.hasAnalogOutputSignal()) { +- this.updateNeighbourForOutputSignal(pos, newState.getBlock()); +- } +- } +- +- if ((flags & Block.UPDATE_KNOWN_SHAPE) == 0 && recursionLeft > 0) { +- int i = flags & ~(Block.UPDATE_SUPPRESS_DROPS | Block.UPDATE_NEIGHBORS); +- +- // CraftBukkit start +- blockState.updateIndirectNeighbourShapes(this, pos, i, recursionLeft - 1); // Don't call an event for the old block to limit event spam +- boolean cancelledUpdates = false; // Paper - Fix block place logic +- if (((ServerLevel)this).hasPhysicsEvent) { // Paper - BlockPhysicsEvent +- org.bukkit.event.block.BlockPhysicsEvent event = new org.bukkit.event.block.BlockPhysicsEvent(org.bukkit.craftbukkit.block.CraftBlock.at(this, pos), CraftBlockData.fromData(state)); +- cancelledUpdates = !event.callEvent(); // Paper - Fix block place logic +- } +- // CraftBukkit end +- if (!cancelledUpdates) { // Paper - Fix block place logic +- state.updateNeighbourShapes(this, pos, i, recursionLeft - 1); +- state.updateIndirectNeighbourShapes(this, pos, i, recursionLeft - 1); +- } // Paper - Fix block place logic +- } +- +- this.updatePOIOnBlockStateChange(pos, blockState, blockState1); +- } +- } + // CraftBukkit end + public void updatePOIOnBlockStateChange(BlockPos pos, BlockState oldState, BlockState newState) { + } +@@ -1287,14 +1207,14 @@ public abstract class Level implements LevelAccessor, AutoCloseable, ca.spottedl + + @Override + public BlockState getBlockState(BlockPos pos) { +- // CraftBukkit start - tree generation +- if (this.captureTreeGeneration) { +- CraftBlockState previous = this.capturedBlockStates.get(pos); // Paper +- if (previous != null) { +- return previous.getHandle(); ++ // Paper start ++ if (this.capturer.isCapturing() && this.capturer.getCapture().isOverlayingCaptureOnLevel()) { ++ BlockState guess = this.capturer.getCapture().getOverlayBlockState(pos); ++ if (guess != null) { ++ return guess; + } + } +- // CraftBukkit end ++ // Paper end + if (!this.isInValidBounds(pos)) { + return Blocks.VOID_AIR.defaultBlockState(); + } else { +@@ -1575,12 +1495,14 @@ public abstract class Level implements LevelAccessor, AutoCloseable, ca.spottedl + + @Override + public @Nullable BlockEntity getBlockEntity(BlockPos pos) { +- // Paper start - Perf: Optimize capturedTileEntities lookup +- net.minecraft.world.level.block.entity.BlockEntity blockEntity; +- if (!this.capturedTileEntities.isEmpty() && (blockEntity = this.capturedTileEntities.get(pos)) != null) { +- return blockEntity; ++ // Paper start ++ if (this.capturer.isCapturing() && this.capturer.getCapture().isOverlayingCaptureOnLevel()) { ++ java.util.Optional guess = this.capturer.getCapture().getOverlayBlockEntity(pos); ++ if (guess != null) { ++ return guess.orElse(null); ++ } + } +- // Paper end - Perf: Optimize capturedTileEntities lookup ++ // Paper end + if (!this.isInValidBounds(pos)) { + return null; + } else { +@@ -1593,12 +1515,13 @@ public abstract class Level implements LevelAccessor, AutoCloseable, ca.spottedl + public void setBlockEntity(BlockEntity blockEntity) { + BlockPos blockPos = blockEntity.getBlockPos(); + if (this.isInValidBounds(blockPos)) { +- // CraftBukkit start +- if (this.captureBlockStates) { +- this.capturedTileEntities.put(blockPos.immutable(), blockEntity); ++ // Paper start ++ if (this.capturer.isCapturing() && this.capturer.getCapture().isOverlayingCaptureOnLevel()) { ++ this.capturer.getCapture().capturingWorldLevel().setBlockEntity(blockEntity); + return; + } +- // CraftBukkit end ++ // Paper end ++ + this.getChunkAt(blockPos).addAndRegisterBlockEntity(blockEntity); + } + } +diff --git a/net/minecraft/world/level/LevelAccessor.java b/net/minecraft/world/level/LevelAccessor.java +index 21d5042282a4eeaadfbb1057f5995e90acc7388e..bb5fa7b510efe46adfc5b7ffe55c446aaabff11a 100644 +--- a/net/minecraft/world/level/LevelAccessor.java ++++ b/net/minecraft/world/level/LevelAccessor.java +@@ -97,6 +97,4 @@ public interface LevelAccessor extends CommonLevelAccessor, LevelReader, Schedul + default void gameEvent(ResourceKey gameEvent, BlockPos pos, GameEvent.Context context) { + this.gameEvent(this.registryAccess().lookupOrThrow(Registries.GAME_EVENT).getOrThrow(gameEvent), pos, context); + } +- +- net.minecraft.server.level.ServerLevel getMinecraftWorld(); // CraftBukkit + } +diff --git a/net/minecraft/world/level/LevelReader.java b/net/minecraft/world/level/LevelReader.java +index fd5a38a9f24c26f8eca738f78180446d354ff3ac..c21dbae3acabfe541711ea5451c8487c511b0f40 100644 +--- a/net/minecraft/world/level/LevelReader.java ++++ b/net/minecraft/world/level/LevelReader.java +@@ -233,4 +233,6 @@ public interface LevelReader extends ca.spottedleaf.moonrise.patches.chunk_syste + } + + EnvironmentAttributeReader environmentAttributes(); ++ ++ net.minecraft.server.level.ServerLevel getMinecraftWorld(); // CraftBukkit + } +diff --git a/net/minecraft/world/level/WorldGenLevel.java b/net/minecraft/world/level/WorldGenLevel.java +index d8f3ed6f5e5cbc045399e38664cd062737481f7b..83336865ad6ab30c3494361c5fa6088607126b6b 100644 +--- a/net/minecraft/world/level/WorldGenLevel.java ++++ b/net/minecraft/world/level/WorldGenLevel.java +@@ -13,4 +13,5 @@ public interface WorldGenLevel extends ServerLevelAccessor { + + default void setCurrentlyGenerating(@Nullable Supplier currentlyGenerating) { + } ++ + } +diff --git a/net/minecraft/world/level/block/AzaleaBlock.java b/net/minecraft/world/level/block/AzaleaBlock.java +index 435a455ad2ec3dfb142d570a51a720bc6c49dac3..b10c72f649a3ba63102afd789d774d5448268f65 100644 +--- a/net/minecraft/world/level/block/AzaleaBlock.java ++++ b/net/minecraft/world/level/block/AzaleaBlock.java +@@ -49,8 +49,8 @@ public class AzaleaBlock extends VegetationBlock implements BonemealableBlock { + } + + @Override +- public void performBonemeal(ServerLevel level, RandomSource random, BlockPos pos, BlockState state) { +- TreeGrower.AZALEA.growTree(level, level.getChunkSource().getGenerator(), pos, state, random); ++ public void performBonemeal(io.papermc.paper.util.capture.PaperCapturingWorldLevel level, RandomSource random, BlockPos pos, BlockState state, io.papermc.paper.util.capture.GrowthContext growthContext) { // Paper ++ TreeGrower.AZALEA.growTree(level, level.getChunkSource().getGenerator(), pos, state, random, growthContext); // Paper + } + + @Override +diff --git a/net/minecraft/world/level/block/BambooSaplingBlock.java b/net/minecraft/world/level/block/BambooSaplingBlock.java +index 88c204ad9d4ead792eb618dbb8611cca863e798c..c0ce6404a8d31669b0e279eafbe5e434b36b824b 100644 +--- a/net/minecraft/world/level/block/BambooSaplingBlock.java ++++ b/net/minecraft/world/level/block/BambooSaplingBlock.java +@@ -84,11 +84,11 @@ public class BambooSaplingBlock extends Block implements BonemealableBlock { + } + + @Override +- public void performBonemeal(ServerLevel level, RandomSource random, BlockPos pos, BlockState state) { ++ public void performBonemeal(io.papermc.paper.util.capture.PaperCapturingWorldLevel level, RandomSource random, BlockPos pos, BlockState state, io.papermc.paper.util.capture.GrowthContext growthContext) { // Paper + this.growBamboo(level, pos); + } + +- protected void growBamboo(Level level, BlockPos state) { ++ protected void growBamboo(io.papermc.paper.util.capture.PaperCapturingWorldLevel level, BlockPos state) { // Paper + org.bukkit.craftbukkit.event.CraftEventFactory.handleBlockSpreadEvent(level, state, state.above(), Blocks.BAMBOO.defaultBlockState().setValue(BambooStalkBlock.LEAVES, BambooLeaves.SMALL), Block.UPDATE_ALL); // CraftBukkit - BlockSpreadEvent + } + } +diff --git a/net/minecraft/world/level/block/BambooStalkBlock.java b/net/minecraft/world/level/block/BambooStalkBlock.java +index 81e2a279d4c29f5fe52387875489239515a8c82b..469f464fe4668e6b65be73a6762a042709e78018 100644 +--- a/net/minecraft/world/level/block/BambooStalkBlock.java ++++ b/net/minecraft/world/level/block/BambooStalkBlock.java +@@ -157,7 +157,7 @@ public class BambooStalkBlock extends Block implements BonemealableBlock { + public boolean isValidBonemealTarget(LevelReader level, BlockPos pos, BlockState state) { + int heightAboveUpToMax = this.getHeightAboveUpToMax(level, pos); + int heightBelowUpToMax = this.getHeightBelowUpToMax(level, pos); +- return heightAboveUpToMax + heightBelowUpToMax + 1 < ((Level) level).paperConfig().maxGrowthHeight.bamboo.max && level.getBlockState(pos.above(heightAboveUpToMax)).getValue(STAGE) != 1; // Paper - Configurable cactus/bamboo/reed growth height ++ return heightAboveUpToMax + heightBelowUpToMax + 1 < level.getMinecraftWorld().paperConfig().maxGrowthHeight.bamboo.max && level.getBlockState(pos.above(heightAboveUpToMax)).getValue(STAGE) != 1; // Paper - Configurable cactus/bamboo/reed growth height + } + + @Override +@@ -166,7 +166,7 @@ public class BambooStalkBlock extends Block implements BonemealableBlock { + } + + @Override +- public void performBonemeal(ServerLevel level, RandomSource random, BlockPos pos, BlockState state) { ++ public void performBonemeal(io.papermc.paper.util.capture.PaperCapturingWorldLevel level, RandomSource random, BlockPos pos, BlockState state, io.papermc.paper.util.capture.GrowthContext growthContext) { // Paper + int heightAboveUpToMax = this.getHeightAboveUpToMax(level, pos); + int heightBelowUpToMax = this.getHeightBelowUpToMax(level, pos); + int i = heightAboveUpToMax + heightBelowUpToMax + 1; +@@ -175,7 +175,7 @@ public class BambooStalkBlock extends Block implements BonemealableBlock { + for (int i2 = 0; i2 < i1; i2++) { + BlockPos blockPos = pos.above(heightAboveUpToMax); + BlockState blockState = level.getBlockState(blockPos); +- if (i >= level.paperConfig().maxGrowthHeight.bamboo.max || !blockState.is(Blocks.BAMBOO) || blockState.getValue(BambooStalkBlock.STAGE) == 1 || !level.isEmptyBlock(blockPos.above())) { // CraftBukkit - If the BlockSpreadEvent was cancelled, we have no bamboo here // Paper - Configurable cactus/bamboo/reed growth height ++ if (i >= level.getMinecraftWorld().paperConfig().maxGrowthHeight.bamboo.max || !blockState.is(Blocks.BAMBOO) || blockState.getValue(BambooStalkBlock.STAGE) == 1 || !level.isEmptyBlock(blockPos.above())) { // CraftBukkit - If the BlockSpreadEvent was cancelled, we have no bamboo here // Paper - Configurable cactus/bamboo/reed growth height + return; + } + +@@ -185,7 +185,7 @@ public class BambooStalkBlock extends Block implements BonemealableBlock { + } + } + +- protected void growBamboo(BlockState state, Level level, BlockPos pos, RandomSource random, int age) { ++ protected void growBamboo(BlockState state, io.papermc.paper.util.capture.PaperCapturingWorldLevel level, BlockPos pos, RandomSource random, int age) { // Paper + BlockState blockState = level.getBlockState(pos.below()); + BlockPos blockPos = pos.below(2); + BlockState blockState1 = level.getBlockState(blockPos); +@@ -207,7 +207,7 @@ public class BambooStalkBlock extends Block implements BonemealableBlock { + } + + int i = state.getValue(AGE) != 1 && !blockState1.is(Blocks.BAMBOO) ? 0 : 1; +- int i1 = (age < level.paperConfig().maxGrowthHeight.bamboo.min || random.nextFloat() >= 0.25F) && age != (level.paperConfig().maxGrowthHeight.bamboo.max - 1) ? 0 : 1; // Paper - Configurable cactus/bamboo/reed growth height ++ int i1 = (age < level.getMinecraftWorld().paperConfig().maxGrowthHeight.bamboo.min || random.nextFloat() >= 0.25F) && age != (level.getMinecraftWorld().paperConfig().maxGrowthHeight.bamboo.max - 1) ? 0 : 1; // Paper - Configurable cactus/bamboo/reed growth height + // CraftBukkit start + if (org.bukkit.craftbukkit.event.CraftEventFactory.handleBlockSpreadEvent(level, pos, pos.above(), this.defaultBlockState().setValue(AGE, i).setValue(LEAVES, bambooLeaves).setValue(STAGE, i1), Block.UPDATE_ALL)) { + if (shouldUpdateOthers) { +@@ -218,20 +218,20 @@ public class BambooStalkBlock extends Block implements BonemealableBlock { + // CraftBukkit end + } + +- protected int getHeightAboveUpToMax(BlockGetter level, BlockPos pos) { ++ protected int getHeightAboveUpToMax(LevelReader level, BlockPos pos) { // Paper + int i = 0; + +- while (i < ((Level) level).paperConfig().maxGrowthHeight.bamboo.max && level.getBlockState(pos.above(i + 1)).is(Blocks.BAMBOO)) { // Paper - Configurable cactus/bamboo/reed growth height ++ while (i < level.getMinecraftWorld().paperConfig().maxGrowthHeight.bamboo.max && level.getBlockState(pos.above(i + 1)).is(Blocks.BAMBOO)) { // Paper - Configurable cactus/bamboo/reed growth height + i++; + } + + return i; + } + +- protected int getHeightBelowUpToMax(BlockGetter level, BlockPos pos) { ++ protected int getHeightBelowUpToMax(LevelReader level, BlockPos pos) { // Paper + int i = 0; + +- while (i < ((Level) level).paperConfig().maxGrowthHeight.bamboo.max && level.getBlockState(pos.below(i + 1)).is(Blocks.BAMBOO)) { // Paper - Configurable cactus/bamboo/reed growth height ++ while (i < level.getMinecraftWorld().paperConfig().maxGrowthHeight.bamboo.max && level.getBlockState(pos.below(i + 1)).is(Blocks.BAMBOO)) { // Paper - Configurable cactus/bamboo/reed growth height + i++; + } + +diff --git a/net/minecraft/world/level/block/BedBlock.java b/net/minecraft/world/level/block/BedBlock.java +index 7bfc62120cae4c5e83cb0d86b759b7ffb2336a95..3d5ed3d705c80772ac8cb7fff7b213bb3e92c41c 100644 +--- a/net/minecraft/world/level/block/BedBlock.java ++++ b/net/minecraft/world/level/block/BedBlock.java +@@ -330,16 +330,11 @@ public class BedBlock extends HorizontalDirectionalBlock implements EntityBlock + } + + @Override +- public void setPlacedBy(Level level, BlockPos pos, BlockState state, @Nullable LivingEntity placer, ItemStack stack) { +- super.setPlacedBy(level, pos, state, placer, stack); ++ public void setPlacedBy(io.papermc.paper.util.capture.PaperCapturingWorldLevel level, BlockPos pos, BlockState state, @Nullable LivingEntity placer, ItemStack stack) { // Paper - block placement capturing ++ super.setPlacedBy(level, pos, state, placer, stack); // Paper - block placement capturing + if (!level.isClientSide()) { + BlockPos blockPos = pos.relative(state.getValue(FACING)); + level.setBlock(blockPos, state.setValue(PART, BedPart.HEAD), Block.UPDATE_ALL); +- // CraftBukkit start - SPIGOT-7315: Don't updated if we capture block states +- if (level.captureBlockStates) { +- return; +- } +- // CraftBukkit end + level.updateNeighborsAt(pos, Blocks.AIR); + state.updateNeighbourShapes(level, pos, Block.UPDATE_ALL); + } +diff --git a/net/minecraft/world/level/block/BeetrootBlock.java b/net/minecraft/world/level/block/BeetrootBlock.java +index dbc912d514120a33f22959d6dc36ccf6ebc6be80..f27840bd23d51b86f00cac7eede270cc39b04c77 100644 +--- a/net/minecraft/world/level/block/BeetrootBlock.java ++++ b/net/minecraft/world/level/block/BeetrootBlock.java +@@ -54,7 +54,7 @@ public class BeetrootBlock extends CropBlock { + } + + @Override +- protected int getBonemealAgeIncrease(Level level) { ++ protected int getBonemealAgeIncrease(net.minecraft.world.level.LevelAccessor level) { // Paper + return super.getBonemealAgeIncrease(level) / 3; + } + +diff --git a/net/minecraft/world/level/block/BigDripleafBlock.java b/net/minecraft/world/level/block/BigDripleafBlock.java +index c2ef71dd9ee070f80d32b8829b80240ff22f0f7f..d2324808a9db1ca701becc9a27577ab6f111f987 100644 +--- a/net/minecraft/world/level/block/BigDripleafBlock.java ++++ b/net/minecraft/world/level/block/BigDripleafBlock.java +@@ -177,7 +177,7 @@ public class BigDripleafBlock extends HorizontalDirectionalBlock implements Bone + } + + @Override +- public void performBonemeal(ServerLevel level, RandomSource random, BlockPos pos, BlockState state) { ++ public void performBonemeal(io.papermc.paper.util.capture.PaperCapturingWorldLevel level, RandomSource random, BlockPos pos, BlockState state, io.papermc.paper.util.capture.GrowthContext growthContext) { // Paper + BlockPos blockPos = pos.above(); + BlockState blockState = level.getBlockState(blockPos); + if (canPlaceAt(level, blockPos, blockState)) { +diff --git a/net/minecraft/world/level/block/BigDripleafStemBlock.java b/net/minecraft/world/level/block/BigDripleafStemBlock.java +index 0ce3e60a94dff03874cad51a936f247d5c3d65ce..d5777bfb65d999cb2706c14a26c57f7dfe66a565 100644 +--- a/net/minecraft/world/level/block/BigDripleafStemBlock.java ++++ b/net/minecraft/world/level/block/BigDripleafStemBlock.java +@@ -119,7 +119,7 @@ public class BigDripleafStemBlock extends HorizontalDirectionalBlock implements + } + + @Override +- public void performBonemeal(ServerLevel level, RandomSource random, BlockPos pos, BlockState state) { ++ public void performBonemeal(io.papermc.paper.util.capture.PaperCapturingWorldLevel level, RandomSource random, BlockPos pos, BlockState state, io.papermc.paper.util.capture.GrowthContext growthContext) { // Paper + Optional topConnectedBlock = BlockUtil.getTopConnectedBlock(level, pos, state.getBlock(), Direction.UP, Blocks.BIG_DRIPLEAF); + if (!topConnectedBlock.isEmpty()) { + BlockPos blockPos = topConnectedBlock.get(); +diff --git a/net/minecraft/world/level/block/Block.java b/net/minecraft/world/level/block/Block.java +index 94b4143449c99ee35db44ab8e2a766d924aa6410..9b9cc245b1fc42d9747de68049189935f2df99b0 100644 +--- a/net/minecraft/world/level/block/Block.java ++++ b/net/minecraft/world/level/block/Block.java +@@ -508,7 +508,7 @@ public class Block extends BlockBehaviour implements ItemLike { + } // Paper - fix drops not preventing stats/food exhaustion + } + +- public void setPlacedBy(Level level, BlockPos pos, BlockState state, @Nullable LivingEntity placer, ItemStack stack) { ++ public void setPlacedBy(io.papermc.paper.util.capture.PaperCapturingWorldLevel level, BlockPos pos, BlockState state, @Nullable LivingEntity placer, ItemStack stack) { // Paper - block placement capturing + } + + public boolean isPossibleToRespawnInThis(BlockState state) { +diff --git a/net/minecraft/world/level/block/BonemealableBlock.java b/net/minecraft/world/level/block/BonemealableBlock.java +index 676cc6cb2ea0ac25c4e4bd6a32856f07784c7b49..965e966e44c8091df61b703f4887963181ea2882 100644 +--- a/net/minecraft/world/level/block/BonemealableBlock.java ++++ b/net/minecraft/world/level/block/BonemealableBlock.java +@@ -15,14 +15,20 @@ public interface BonemealableBlock { + + boolean isBonemealSuccess(Level level, RandomSource random, BlockPos pos, BlockState state); + +- void performBonemeal(ServerLevel level, RandomSource random, BlockPos pos, BlockState state); ++ // Paper start ++ default void performBonemeal(io.papermc.paper.util.capture.PaperCapturingWorldLevel level, RandomSource random, BlockPos pos, BlockState state) { ++ performBonemeal(level, random, pos, state, null); ++ } ++ ++ void performBonemeal(io.papermc.paper.util.capture.PaperCapturingWorldLevel level, RandomSource random, BlockPos pos, BlockState state, io.papermc.paper.util.capture.GrowthContext growthContext); ++ // Paper end + + static boolean hasSpreadableNeighbourPos(LevelReader level, BlockPos pos, BlockState state) { + return getSpreadableNeighbourPos(Direction.Plane.HORIZONTAL.stream().toList(), level, pos, state).isPresent(); + } + +- static Optional findSpreadableNeighbourPos(Level level, BlockPos pos, BlockState state) { +- return getSpreadableNeighbourPos(Direction.Plane.HORIZONTAL.shuffledCopy(level.random), level, pos, state); ++ static Optional findSpreadableNeighbourPos(io.papermc.paper.util.capture.PaperCapturingWorldLevel level, BlockPos pos, BlockState state) { // Paper ++ return getSpreadableNeighbourPos(Direction.Plane.HORIZONTAL.shuffledCopy(level.getRandom()), level, pos, state); // Paper + } + + private static Optional getSpreadableNeighbourPos(List directions, LevelReader level, BlockPos pos, BlockState state) { +diff --git a/net/minecraft/world/level/block/BonemealableFeaturePlacerBlock.java b/net/minecraft/world/level/block/BonemealableFeaturePlacerBlock.java +index ee701a4c5042aec359271533680d292a6169d4db..d856dfbd89df890eeabce86618ab2586900206ae 100644 +--- a/net/minecraft/world/level/block/BonemealableFeaturePlacerBlock.java ++++ b/net/minecraft/world/level/block/BonemealableFeaturePlacerBlock.java +@@ -41,7 +41,7 @@ public class BonemealableFeaturePlacerBlock extends Block implements Bonemealabl + } + + @Override +- public void performBonemeal(ServerLevel level, RandomSource random, BlockPos pos, BlockState state) { ++ public void performBonemeal(io.papermc.paper.util.capture.PaperCapturingWorldLevel level, RandomSource random, BlockPos pos, BlockState state, io.papermc.paper.util.capture.GrowthContext growthContext) { // Paper + level.registryAccess() + .lookup(Registries.CONFIGURED_FEATURE) + .flatMap(registry -> registry.get(this.feature)) +diff --git a/net/minecraft/world/level/block/BushBlock.java b/net/minecraft/world/level/block/BushBlock.java +index 532d2982b520f6f9f7021ec85ef3bcf9bd31e141..4691a3a75935f1109fca36a85f6d62f3b4a05cab 100644 +--- a/net/minecraft/world/level/block/BushBlock.java ++++ b/net/minecraft/world/level/block/BushBlock.java +@@ -41,7 +41,7 @@ public class BushBlock extends VegetationBlock implements BonemealableBlock { + } + + @Override +- public void performBonemeal(ServerLevel level, RandomSource random, BlockPos pos, BlockState state) { ++ public void performBonemeal(io.papermc.paper.util.capture.PaperCapturingWorldLevel level, RandomSource random, BlockPos pos, BlockState state, io.papermc.paper.util.capture.GrowthContext growthContext) { // Paper + BonemealableBlock.findSpreadableNeighbourPos(level, pos, state).ifPresent(blockPos -> level.setBlockAndUpdate(blockPos, this.defaultBlockState())); + } + } +diff --git a/net/minecraft/world/level/block/CaveVinesBlock.java b/net/minecraft/world/level/block/CaveVinesBlock.java +index f4a4dc14012c110e58b1c9272d80d4b89394d090..84c5e9195e3d1e026691ecf0b0e2dd6fae3a9e91 100644 +--- a/net/minecraft/world/level/block/CaveVinesBlock.java ++++ b/net/minecraft/world/level/block/CaveVinesBlock.java +@@ -89,7 +89,7 @@ public class CaveVinesBlock extends GrowingPlantHeadBlock implements CaveVines { + } + + @Override +- public void performBonemeal(ServerLevel level, RandomSource random, BlockPos pos, BlockState state) { ++ public void performBonemeal(io.papermc.paper.util.capture.PaperCapturingWorldLevel level, RandomSource random, BlockPos pos, BlockState state, io.papermc.paper.util.capture.GrowthContext growthContext) { // Paper + level.setBlock(pos, state.setValue(BERRIES, true), Block.UPDATE_CLIENTS); + } + } +diff --git a/net/minecraft/world/level/block/CaveVinesPlantBlock.java b/net/minecraft/world/level/block/CaveVinesPlantBlock.java +index aba65098fb3202e31b07aa2cee52acc5b671fa95..9e3620313456a1ef62c5ce922d4ca21b34f880aa 100644 +--- a/net/minecraft/world/level/block/CaveVinesPlantBlock.java ++++ b/net/minecraft/world/level/block/CaveVinesPlantBlock.java +@@ -65,7 +65,7 @@ public class CaveVinesPlantBlock extends GrowingPlantBodyBlock implements CaveVi + } + + @Override +- public void performBonemeal(ServerLevel level, RandomSource random, BlockPos pos, BlockState state) { ++ public void performBonemeal(io.papermc.paper.util.capture.PaperCapturingWorldLevel level, RandomSource random, BlockPos pos, BlockState state, io.papermc.paper.util.capture.GrowthContext growthContext) { // Paper + level.setBlock(pos, state.setValue(BERRIES, true), Block.UPDATE_CLIENTS); + } + } +diff --git a/net/minecraft/world/level/block/CocoaBlock.java b/net/minecraft/world/level/block/CocoaBlock.java +index d4f82e39bb688e43decfb5c8e72fd6db2bfeb571..8f11658f55980ea9f7f15d4892a027b7db1e03f1 100644 +--- a/net/minecraft/world/level/block/CocoaBlock.java ++++ b/net/minecraft/world/level/block/CocoaBlock.java +@@ -114,7 +114,7 @@ public class CocoaBlock extends HorizontalDirectionalBlock implements Bonemealab + } + + @Override +- public void performBonemeal(ServerLevel level, RandomSource random, BlockPos pos, BlockState state) { ++ public void performBonemeal(io.papermc.paper.util.capture.PaperCapturingWorldLevel level, RandomSource random, BlockPos pos, BlockState state, io.papermc.paper.util.capture.GrowthContext growthContext) { // Paper + org.bukkit.craftbukkit.event.CraftEventFactory.handleBlockGrowEvent(level, pos, state.setValue(AGE, state.getValue(AGE) + 1), Block.UPDATE_CLIENTS); // CraftBukkit + } + +diff --git a/net/minecraft/world/level/block/CommandBlock.java b/net/minecraft/world/level/block/CommandBlock.java +index b5a780a929c2b6db91d7f86d7178419f87815944..aa7a3e7304aef32424d066d6c49e354e33ba1bd2 100644 +--- a/net/minecraft/world/level/block/CommandBlock.java ++++ b/net/minecraft/world/level/block/CommandBlock.java +@@ -63,12 +63,12 @@ public class CommandBlock extends BaseEntityBlock implements GameMasterBlock { + protected void neighborChanged(BlockState state, Level level, BlockPos pos, Block neighborBlock, @Nullable Orientation orientation, boolean movedByPiston) { + if (!level.isClientSide()) { + if (level.getBlockEntity(pos) instanceof CommandBlockEntity commandBlockEntity) { +- this.setPoweredAndUpdate(level, pos, commandBlockEntity, level.hasNeighborSignal(pos)); ++ this.setPoweredAndUpdate((io.papermc.paper.util.capture.PaperCapturingWorldLevel) level, pos, commandBlockEntity, level.hasNeighborSignal(pos)); // Paper - block placement capturing + } + } + } + +- private void setPoweredAndUpdate(Level level, BlockPos pos, CommandBlockEntity blockEntity, boolean powered) { ++ private void setPoweredAndUpdate(io.papermc.paper.util.capture.PaperCapturingWorldLevel level, BlockPos pos, CommandBlockEntity blockEntity, boolean powered) { // Paper - block placement capturing + boolean isPowered = blockEntity.isPowered(); + // CraftBukkit start + org.bukkit.block.Block bukkitBlock = org.bukkit.craftbukkit.block.CraftBlock.at(level, pos); +@@ -76,7 +76,7 @@ public class CommandBlock extends BaseEntityBlock implements GameMasterBlock { + int current = powered ? 15 : 0; + + org.bukkit.event.block.BlockRedstoneEvent eventRedstone = new org.bukkit.event.block.BlockRedstoneEvent(bukkitBlock, old, current); +- level.getCraftServer().getPluginManager().callEvent(eventRedstone); ++ eventRedstone.callEvent(); + powered = eventRedstone.getNewCurrent() > 0; + // CraftBukkit end + if (powered != isPowered) { +@@ -155,12 +155,12 @@ public class CommandBlock extends BaseEntityBlock implements GameMasterBlock { + } + + @Override +- public void setPlacedBy(Level level, BlockPos pos, BlockState state, @Nullable LivingEntity placer, ItemStack stack) { ++ public void setPlacedBy(io.papermc.paper.util.capture.PaperCapturingWorldLevel level, BlockPos pos, BlockState state, @Nullable LivingEntity placer, ItemStack stack) { // Paper - block placement capturing + if (level.getBlockEntity(pos) instanceof CommandBlockEntity commandBlockEntity) { + BaseCommandBlock commandBlock = commandBlockEntity.getCommandBlock(); +- if (level instanceof ServerLevel serverLevel) { ++ if (true || level instanceof ServerLevel serverLevel) { // Paper - block placement capturing + if (!stack.has(DataComponents.BLOCK_ENTITY_DATA)) { +- commandBlock.setTrackOutput(serverLevel.getGameRules().get(GameRules.SEND_COMMAND_FEEDBACK)); ++ commandBlock.setTrackOutput(level.getGameRules().get(GameRules.SEND_COMMAND_FEEDBACK)); // Paper - block placement capturing + commandBlockEntity.setAutomatic(this.automatic); + } + +diff --git a/net/minecraft/world/level/block/CrafterBlock.java b/net/minecraft/world/level/block/CrafterBlock.java +index c5fe15844d405a27cdae18c903dd481c25b437de..abb7d98c6608ec1bbed169327114245d85cb1ef8 100644 +--- a/net/minecraft/world/level/block/CrafterBlock.java ++++ b/net/minecraft/world/level/block/CrafterBlock.java +@@ -122,7 +122,7 @@ public class CrafterBlock extends BaseEntityBlock { + } + + @Override +- public void setPlacedBy(Level level, BlockPos pos, BlockState state, @Nullable LivingEntity placer, ItemStack stack) { ++ public void setPlacedBy(io.papermc.paper.util.capture.PaperCapturingWorldLevel level, BlockPos pos, BlockState state, @Nullable LivingEntity placer, ItemStack stack) { // Paper - block placement capturing + if (state.getValue(TRIGGERED)) { + level.scheduleTick(pos, this, 4); + } +diff --git a/net/minecraft/world/level/block/CropBlock.java b/net/minecraft/world/level/block/CropBlock.java +index 1cf40fafd822d976ef4822335c60d8017659916f..f502b8b5d9fd9e0404f1b5d37e0e3ea74de6900a 100644 +--- a/net/minecraft/world/level/block/CropBlock.java ++++ b/net/minecraft/world/level/block/CropBlock.java +@@ -104,13 +104,13 @@ public class CropBlock extends VegetationBlock implements BonemealableBlock { + } + } + +- public void growCrops(Level level, BlockPos pos, BlockState state) { ++ public void growCrops(net.minecraft.world.level.LevelAccessor level, BlockPos pos, BlockState state) { // Paper + int min = Math.min(this.getMaxAge(), this.getAge(state) + this.getBonemealAgeIncrease(level)); + org.bukkit.craftbukkit.event.CraftEventFactory.handleBlockGrowEvent(level, pos, this.getStateForAge(min), Block.UPDATE_CLIENTS); // CraftBukkit + } + +- protected int getBonemealAgeIncrease(Level level) { +- return Mth.nextInt(level.random, 2, 5); ++ protected int getBonemealAgeIncrease(net.minecraft.world.level.LevelAccessor level) { // Paper ++ return Mth.nextInt(level.getRandom(), 2, 5); // Paper + } + + protected static float getGrowthSpeed(Block block, BlockGetter level, BlockPos pos) { +@@ -196,7 +196,7 @@ public class CropBlock extends VegetationBlock implements BonemealableBlock { + } + + @Override +- public void performBonemeal(ServerLevel level, RandomSource random, BlockPos pos, BlockState state) { ++ public void performBonemeal(io.papermc.paper.util.capture.PaperCapturingWorldLevel level, RandomSource random, BlockPos pos, BlockState state, io.papermc.paper.util.capture.GrowthContext growthContext) { // Paper + this.growCrops(level, pos, state); + } + +diff --git a/net/minecraft/world/level/block/DiodeBlock.java b/net/minecraft/world/level/block/DiodeBlock.java +index 02ffb5569e2405d86b3a4a695dd17c9372169ff7..9a038e60c48942d5b252e0e12f99ca7ce499a85e 100644 +--- a/net/minecraft/world/level/block/DiodeBlock.java ++++ b/net/minecraft/world/level/block/DiodeBlock.java +@@ -164,10 +164,14 @@ public abstract class DiodeBlock extends HorizontalDirectionalBlock { + } + + @Override +- public void setPlacedBy(Level level, BlockPos pos, BlockState state, @Nullable LivingEntity placer, ItemStack stack) { +- if (this.shouldTurnOn(level, pos, state)) { +- level.scheduleTick(pos, this, 1); +- } ++ // Paper start - block placement capturing ++ public void setPlacedBy(io.papermc.paper.util.capture.PaperCapturingWorldLevel level, BlockPos pos, BlockState state, @Nullable LivingEntity placer, ItemStack stack) { ++ level.addTask((serverLevel) -> { ++ if (this.shouldTurnOn(serverLevel, pos, state)) { ++ level.scheduleTick(pos, this, 1); ++ } ++ }); ++ // Paper end - block placement capturing + } + + @Override +diff --git a/net/minecraft/world/level/block/DoorBlock.java b/net/minecraft/world/level/block/DoorBlock.java +index d4239343b15203caafa9da762cbce206cc1d9b33..d662ac3514ad388b32bdcd8dc256b20291a34379 100644 +--- a/net/minecraft/world/level/block/DoorBlock.java ++++ b/net/minecraft/world/level/block/DoorBlock.java +@@ -151,7 +151,7 @@ public class DoorBlock extends Block { + } + + @Override +- public void setPlacedBy(Level level, BlockPos pos, BlockState state, @Nullable LivingEntity placer, ItemStack stack) { ++ public void setPlacedBy(io.papermc.paper.util.capture.PaperCapturingWorldLevel level, BlockPos pos, BlockState state, @Nullable LivingEntity placer, ItemStack stack) { // Paper - block placement capturing + level.setBlock(pos.above(), state.setValue(HALF, DoubleBlockHalf.UPPER), Block.UPDATE_ALL); + } + +diff --git a/net/minecraft/world/level/block/DoublePlantBlock.java b/net/minecraft/world/level/block/DoublePlantBlock.java +index e67f2b0f8e12c99cc5451865487bbec845998619..884693237482ca7b55db16feecc63c589661c78a 100644 +--- a/net/minecraft/world/level/block/DoublePlantBlock.java ++++ b/net/minecraft/world/level/block/DoublePlantBlock.java +@@ -70,7 +70,7 @@ public class DoublePlantBlock extends VegetationBlock { + } + + @Override +- public void setPlacedBy(Level level, BlockPos pos, BlockState state, @Nullable LivingEntity placer, ItemStack stack) { ++ public void setPlacedBy(io.papermc.paper.util.capture.PaperCapturingWorldLevel level, BlockPos pos, BlockState state, @Nullable LivingEntity placer, ItemStack stack) { // Paper - block placement capturing + BlockPos blockPos = pos.above(); + level.setBlock(blockPos, copyWaterloggedFrom(level, blockPos, this.defaultBlockState().setValue(HALF, DoubleBlockHalf.UPPER)), Block.UPDATE_ALL); + } +diff --git a/net/minecraft/world/level/block/DriedGhastBlock.java b/net/minecraft/world/level/block/DriedGhastBlock.java +index e7dbc14cce1d0ec3f410ae7ebbb4dc47301b5176..449083791b18eb4df10a3117f98025940eda0327 100644 +--- a/net/minecraft/world/level/block/DriedGhastBlock.java ++++ b/net/minecraft/world/level/block/DriedGhastBlock.java +@@ -203,7 +203,7 @@ public class DriedGhastBlock extends HorizontalDirectionalBlock implements Simpl + } + + @Override +- public void setPlacedBy(Level level, BlockPos pos, BlockState state, @Nullable LivingEntity placer, ItemStack stack) { ++ public void setPlacedBy(io.papermc.paper.util.capture.PaperCapturingWorldLevel level, BlockPos pos, BlockState state, @Nullable LivingEntity placer, ItemStack stack) { // Paper - block placement capturing + super.setPlacedBy(level, pos, state, placer, stack); + level.playSound( + null, pos, state.getValue(WATERLOGGED) ? SoundEvents.DRIED_GHAST_PLACE_IN_WATER : SoundEvents.DRIED_GHAST_PLACE, SoundSource.BLOCKS, 1.0F, 1.0F +diff --git a/net/minecraft/world/level/block/FireflyBushBlock.java b/net/minecraft/world/level/block/FireflyBushBlock.java +index 635ce3fb2583f6b14355f6137fb6f79dfd959400..138ae70e1dc348f5b804d90ecb10d04febc5ee10 100644 +--- a/net/minecraft/world/level/block/FireflyBushBlock.java ++++ b/net/minecraft/world/level/block/FireflyBushBlock.java +@@ -58,7 +58,7 @@ public class FireflyBushBlock extends VegetationBlock implements BonemealableBlo + } + + @Override +- public void performBonemeal(ServerLevel level, RandomSource random, BlockPos pos, BlockState state) { ++ public void performBonemeal(io.papermc.paper.util.capture.PaperCapturingWorldLevel level, RandomSource random, BlockPos pos, BlockState state, io.papermc.paper.util.capture.GrowthContext growthContext) { // Paper + BonemealableBlock.findSpreadableNeighbourPos(level, pos, state).ifPresent(blockPos -> level.setBlockAndUpdate(blockPos, this.defaultBlockState())); + } + } +diff --git a/net/minecraft/world/level/block/FlowerBedBlock.java b/net/minecraft/world/level/block/FlowerBedBlock.java +index 2a24e71d2b598792ebaa867a2f74fd79e97cc6a2..291a8a5ceb8f907db6fffa038e02f3959719a66d 100644 +--- a/net/minecraft/world/level/block/FlowerBedBlock.java ++++ b/net/minecraft/world/level/block/FlowerBedBlock.java +@@ -92,12 +92,12 @@ public class FlowerBedBlock extends VegetationBlock implements BonemealableBlock + } + + @Override +- public void performBonemeal(ServerLevel level, RandomSource random, BlockPos pos, BlockState state) { ++ public void performBonemeal(io.papermc.paper.util.capture.PaperCapturingWorldLevel level, RandomSource random, BlockPos pos, BlockState state, io.papermc.paper.util.capture.GrowthContext growthContext) { // Paper + int amountValue = state.getValue(AMOUNT); + if (amountValue < 4) { + level.setBlock(pos, state.setValue(AMOUNT, amountValue + 1), Block.UPDATE_CLIENTS); + } else { +- popResource(level, pos, new ItemStack(this)); ++ level.addTask((serverLevel) -> popResource(serverLevel, pos, new ItemStack(this))); // Paper + } + } + } +diff --git a/net/minecraft/world/level/block/FungusBlock.java b/net/minecraft/world/level/block/FungusBlock.java +index 9711efb088bd0da9168e9bcd0496bd7caddd2974..4d09148cdbd81a0a83d0bfcc874c7957fa701329 100644 +--- a/net/minecraft/world/level/block/FungusBlock.java ++++ b/net/minecraft/world/level/block/FungusBlock.java +@@ -71,18 +71,14 @@ public class FungusBlock extends VegetationBlock implements BonemealableBlock { + } + + @Override +- public void performBonemeal(ServerLevel level, RandomSource random, BlockPos pos, BlockState state) { +- this.getFeature(level) +- // CraftBukkit start +- .map((value) -> { +- if (this == Blocks.WARPED_FUNGUS) { +- SaplingBlock.treeType = org.bukkit.TreeType.WARPED_FUNGUS; +- } else if (this == Blocks.CRIMSON_FUNGUS) { +- SaplingBlock.treeType = org.bukkit.TreeType.CRIMSON_FUNGUS; +- } +- return value; +- }) +- .ifPresent(holder -> holder.value().place(level, level.getChunkSource().getGenerator(), random, pos)); +- // CraftBukkit end ++ // Paper start ++ public void performBonemeal(io.papermc.paper.util.capture.PaperCapturingWorldLevel level, RandomSource random, BlockPos pos, BlockState state, io.papermc.paper.util.capture.GrowthContext growthContext) { ++ this.getFeature(level).ifPresent(holder -> { ++ growthContext.feature = holder; ++ org.bukkit.craftbukkit.event.CraftEventFactory.structureEvent(level, pos, $ -> { ++ return holder.value().place(level, level.getChunkSource().getGenerator(), random, pos); ++ }, growthContext); ++ }); ++ // Paper end + } + } +diff --git a/net/minecraft/world/level/block/GlowLichenBlock.java b/net/minecraft/world/level/block/GlowLichenBlock.java +index ba39497a6d7160cde961e339be8028ec131b8019..f6d0940d22f52ff4f465bd9a0978f4626ca2fb4c 100644 +--- a/net/minecraft/world/level/block/GlowLichenBlock.java ++++ b/net/minecraft/world/level/block/GlowLichenBlock.java +@@ -39,7 +39,7 @@ public class GlowLichenBlock extends MultifaceSpreadeableBlock implements Boneme + } + + @Override +- public void performBonemeal(ServerLevel level, RandomSource random, BlockPos pos, BlockState state) { ++ public void performBonemeal(io.papermc.paper.util.capture.PaperCapturingWorldLevel level, RandomSource random, BlockPos pos, BlockState state, io.papermc.paper.util.capture.GrowthContext growthContext) { // Paper + this.spreader.spreadFromRandomFaceTowardRandomDirection(state, level, pos, random); + } + +diff --git a/net/minecraft/world/level/block/GrassBlock.java b/net/minecraft/world/level/block/GrassBlock.java +index 368f60ecce691ea161120743150e87b32efc3ca4..bfd07348e6f93c6d7acad44b708900628f9471cf 100644 +--- a/net/minecraft/world/level/block/GrassBlock.java ++++ b/net/minecraft/world/level/block/GrassBlock.java +@@ -40,7 +40,7 @@ public class GrassBlock extends SpreadingSnowyDirtBlock implements BonemealableB + } + + @Override +- public void performBonemeal(ServerLevel level, RandomSource random, BlockPos pos, BlockState state) { ++ public void performBonemeal(io.papermc.paper.util.capture.PaperCapturingWorldLevel level, RandomSource random, BlockPos pos, BlockState state, io.papermc.paper.util.capture.GrowthContext growthContext) { // Paper + BlockPos blockPos = pos.above(); + BlockState blockState = Blocks.SHORT_GRASS.defaultBlockState(); + Optional> optional = level.registryAccess() +@@ -62,7 +62,7 @@ public class GrassBlock extends SpreadingSnowyDirtBlock implements BonemealableB + if (blockState1.is(blockState.getBlock()) && random.nextInt(10) == 0) { + BonemealableBlock bonemealableBlock = (BonemealableBlock)blockState.getBlock(); + if (bonemealableBlock.isValidBonemealTarget(level, blockPos1, blockState1)) { +- bonemealableBlock.performBonemeal(level, random, blockPos1, blockState1); ++ bonemealableBlock.performBonemeal(level, random, blockPos1, blockState1, growthContext); // Paper + } + } + +diff --git a/net/minecraft/world/level/block/GrowingPlantBodyBlock.java b/net/minecraft/world/level/block/GrowingPlantBodyBlock.java +index 314d198617e34f91f72a2952a2a62ce0a3b9147d..817bd9dafb17817174af118fafae57962e915ac3 100644 +--- a/net/minecraft/world/level/block/GrowingPlantBodyBlock.java ++++ b/net/minecraft/world/level/block/GrowingPlantBodyBlock.java +@@ -74,11 +74,11 @@ public abstract class GrowingPlantBodyBlock extends GrowingPlantBlock implements + } + + @Override +- public void performBonemeal(ServerLevel level, RandomSource random, BlockPos pos, BlockState state) { ++ public void performBonemeal(io.papermc.paper.util.capture.PaperCapturingWorldLevel level, RandomSource random, BlockPos pos, BlockState state, io.papermc.paper.util.capture.GrowthContext growthContext) { // Paper + Optional headPos = this.getHeadPos(level, pos, state.getBlock()); + if (headPos.isPresent()) { + BlockState blockState = level.getBlockState(headPos.get()); +- ((GrowingPlantHeadBlock)blockState.getBlock()).performBonemeal(level, random, headPos.get(), blockState); ++ ((GrowingPlantHeadBlock)blockState.getBlock()).performBonemeal(level, random, headPos.get(), blockState, growthContext); // Paper + } + } + +diff --git a/net/minecraft/world/level/block/GrowingPlantHeadBlock.java b/net/minecraft/world/level/block/GrowingPlantHeadBlock.java +index bac7f990282fd7c676c2f8c40d7fd87badb1e284..bd30c47bab6dd4db3143fde72867721f3634fa8a 100644 +--- a/net/minecraft/world/level/block/GrowingPlantHeadBlock.java ++++ b/net/minecraft/world/level/block/GrowingPlantHeadBlock.java +@@ -135,7 +135,7 @@ public abstract class GrowingPlantHeadBlock extends GrowingPlantBlock implements + } + + @Override +- public void performBonemeal(ServerLevel level, RandomSource random, BlockPos pos, BlockState state) { ++ public void performBonemeal(io.papermc.paper.util.capture.PaperCapturingWorldLevel level, RandomSource random, BlockPos pos, BlockState state, io.papermc.paper.util.capture.GrowthContext growthContext) { // Paper + BlockPos blockPos = pos.relative(this.growthDirection); + int min = Math.min(state.getValue(AGE) + 1, 25); + int blocksToGrowWhenBonemealed = this.getBlocksToGrowWhenBonemealed(random); +diff --git a/net/minecraft/world/level/block/HangingMossBlock.java b/net/minecraft/world/level/block/HangingMossBlock.java +index 9675e0ad00a4a26c38a7388dc3615e0c33b4e802..391fef088a53a2f5433687e164f04d61f5a0b566 100644 +--- a/net/minecraft/world/level/block/HangingMossBlock.java ++++ b/net/minecraft/world/level/block/HangingMossBlock.java +@@ -124,7 +124,7 @@ public class HangingMossBlock extends Block implements BonemealableBlock { + } + + @Override +- public void performBonemeal(ServerLevel level, RandomSource random, BlockPos pos, BlockState state) { ++ public void performBonemeal(io.papermc.paper.util.capture.PaperCapturingWorldLevel level, RandomSource random, BlockPos pos, BlockState state, io.papermc.paper.util.capture.GrowthContext growthContext) { // Paper + BlockPos blockPos = this.getTip(level, pos).below(); + if (this.canGrowInto(level.getBlockState(blockPos))) { + level.setBlockAndUpdate(blockPos, state.setValue(TIP, true)); +diff --git a/net/minecraft/world/level/block/JukeboxBlock.java b/net/minecraft/world/level/block/JukeboxBlock.java +index a4b824dab307705559a76b954a3e47d30dd65889..4fe64b4b445ddceedff0909c917e85d6bc4e7e75 100644 +--- a/net/minecraft/world/level/block/JukeboxBlock.java ++++ b/net/minecraft/world/level/block/JukeboxBlock.java +@@ -42,7 +42,7 @@ public class JukeboxBlock extends BaseEntityBlock { + } + + @Override +- public void setPlacedBy(Level level, BlockPos pos, BlockState state, @Nullable LivingEntity placer, ItemStack stack) { ++ public void setPlacedBy(io.papermc.paper.util.capture.PaperCapturingWorldLevel level, BlockPos pos, BlockState state, @Nullable LivingEntity placer, ItemStack stack) { // Paper - block placement capturing + super.setPlacedBy(level, pos, state, placer, stack); + TypedEntityData> typedEntityData = stack.get(DataComponents.BLOCK_ENTITY_DATA); + if (typedEntityData != null && typedEntityData.contains("RecordItem")) { +diff --git a/net/minecraft/world/level/block/MangroveLeavesBlock.java b/net/minecraft/world/level/block/MangroveLeavesBlock.java +index 6d8154821cd17f7529a44eeb16a78caa07529436..c5427b5de39340f9d3c53ce47c5bffda6d4e9731 100644 +--- a/net/minecraft/world/level/block/MangroveLeavesBlock.java ++++ b/net/minecraft/world/level/block/MangroveLeavesBlock.java +@@ -40,7 +40,7 @@ public class MangroveLeavesBlock extends TintedParticleLeavesBlock implements Bo + } + + @Override +- public void performBonemeal(ServerLevel level, RandomSource random, BlockPos pos, BlockState state) { ++ public void performBonemeal(io.papermc.paper.util.capture.PaperCapturingWorldLevel level, RandomSource random, BlockPos pos, BlockState state, io.papermc.paper.util.capture.GrowthContext growthContext) { // Paper + level.setBlock(pos.below(), MangrovePropaguleBlock.createNewHangingPropagule(), Block.UPDATE_CLIENTS); + } + +diff --git a/net/minecraft/world/level/block/MangrovePropaguleBlock.java b/net/minecraft/world/level/block/MangrovePropaguleBlock.java +index 2fba8534a636491314c760bc338226f6506f0a9a..1652846681e9a3a906dc0ee34bc7fc85f2bfcb47 100644 +--- a/net/minecraft/world/level/block/MangrovePropaguleBlock.java ++++ b/net/minecraft/world/level/block/MangrovePropaguleBlock.java +@@ -123,11 +123,11 @@ public class MangrovePropaguleBlock extends SaplingBlock implements SimpleWaterl + } + + @Override +- public void performBonemeal(ServerLevel level, RandomSource random, BlockPos pos, BlockState state) { ++ public void performBonemeal(io.papermc.paper.util.capture.PaperCapturingWorldLevel level, RandomSource random, BlockPos pos, BlockState state, io.papermc.paper.util.capture.GrowthContext growthContext) { // Paper + if (isHanging(state) && !isFullyGrown(state)) { + level.setBlock(pos, state.cycle(AGE), Block.UPDATE_CLIENTS); + } else { +- super.performBonemeal(level, random, pos, state); ++ super.performBonemeal(level, random, pos, state, growthContext); // Paper + } + } + +diff --git a/net/minecraft/world/level/block/MossyCarpetBlock.java b/net/minecraft/world/level/block/MossyCarpetBlock.java +index 3762d833f17d596e04845a3f391423f9c14a878b..7a10673d12eaddca1caef25f11c0f301a0389c29 100644 +--- a/net/minecraft/world/level/block/MossyCarpetBlock.java ++++ b/net/minecraft/world/level/block/MossyCarpetBlock.java +@@ -176,7 +176,7 @@ public class MossyCarpetBlock extends Block implements BonemealableBlock { + } + + @Override +- public void setPlacedBy(Level level, BlockPos pos, BlockState state, @Nullable LivingEntity placer, ItemStack stack) { ++ public void setPlacedBy(io.papermc.paper.util.capture.PaperCapturingWorldLevel level, BlockPos pos, BlockState state, @Nullable LivingEntity placer, ItemStack stack) { // Paper - block placement capturing + if (!level.isClientSide()) { + RandomSource random = level.getRandom(); + BlockState blockState = createTopperWithSideChance(level, pos, random::nextBoolean); +@@ -274,7 +274,7 @@ public class MossyCarpetBlock extends Block implements BonemealableBlock { + } + + @Override +- public void performBonemeal(ServerLevel level, RandomSource random, BlockPos pos, BlockState state) { ++ public void performBonemeal(io.papermc.paper.util.capture.PaperCapturingWorldLevel level, RandomSource random, BlockPos pos, BlockState state, io.papermc.paper.util.capture.GrowthContext growthContext) { // Paper + BlockState blockState = createTopperWithSideChance(level, pos, () -> true); + if (!blockState.isAir()) { + level.setBlock(pos.above(), blockState, Block.UPDATE_ALL); +diff --git a/net/minecraft/world/level/block/MushroomBlock.java b/net/minecraft/world/level/block/MushroomBlock.java +index b11ff87df7b75f2a3065bbed7b13cc52f7df83fc..42de0f8830cdb41e94ad38564a85a1b9cd4d78ab 100644 +--- a/net/minecraft/world/level/block/MushroomBlock.java ++++ b/net/minecraft/world/level/block/MushroomBlock.java +@@ -87,14 +87,18 @@ public class MushroomBlock extends VegetationBlock implements BonemealableBlock + return blockState.is(BlockTags.MUSHROOM_GROW_BLOCK) || level.getRawBrightness(pos, 0) < 13 && this.mayPlaceOn(blockState, level, blockPos); + } + +- public boolean growMushroom(ServerLevel level, BlockPos pos, BlockState state, RandomSource random) { ++ public boolean growMushroom(io.papermc.paper.util.capture.PaperCapturingWorldLevel level, BlockPos pos, BlockState state, RandomSource random, io.papermc.paper.util.capture.GrowthContext growthContext) { // Paper + Optional>> optional = level.registryAccess().lookupOrThrow(Registries.CONFIGURED_FEATURE).get(this.feature); + if (optional.isEmpty()) { + return false; + } else { + level.removeBlock(pos, false); +- SaplingBlock.treeType = (this == Blocks.BROWN_MUSHROOM) ? org.bukkit.TreeType.BROWN_MUSHROOM : org.bukkit.TreeType.RED_MUSHROOM; // CraftBukkit +- if (optional.get().value().place(level, level.getChunkSource().getGenerator(), random, pos)) { ++ // Paper start ++ growthContext.feature = optional.get(); ++ if (org.bukkit.craftbukkit.event.CraftEventFactory.structureEvent(level, pos, $ -> { ++ return optional.get().value().place(level, level.getChunkSource().getGenerator(), random, pos); ++ }, growthContext)) { ++ // Paper end + return true; + } else { + level.setBlock(pos, state, Block.UPDATE_ALL); +@@ -114,7 +118,7 @@ public class MushroomBlock extends VegetationBlock implements BonemealableBlock + } + + @Override +- public void performBonemeal(ServerLevel level, RandomSource random, BlockPos pos, BlockState state) { +- this.growMushroom(level, pos, state, random); ++ public void performBonemeal(io.papermc.paper.util.capture.PaperCapturingWorldLevel level, RandomSource random, BlockPos pos, BlockState state, io.papermc.paper.util.capture.GrowthContext growthContext) { // Paper ++ this.growMushroom(level, pos, state, random, growthContext); // Paper + } + } +diff --git a/net/minecraft/world/level/block/NetherrackBlock.java b/net/minecraft/world/level/block/NetherrackBlock.java +index 24e166e399bc542522fc832064401ebb6ba2568e..f1f6bad412baf259e219eef269baec76b498bb18 100644 +--- a/net/minecraft/world/level/block/NetherrackBlock.java ++++ b/net/minecraft/world/level/block/NetherrackBlock.java +@@ -43,7 +43,7 @@ public class NetherrackBlock extends Block implements BonemealableBlock { + } + + @Override +- public void performBonemeal(ServerLevel level, RandomSource random, BlockPos pos, BlockState state) { ++ public void performBonemeal(io.papermc.paper.util.capture.PaperCapturingWorldLevel level, RandomSource random, BlockPos pos, BlockState state, io.papermc.paper.util.capture.GrowthContext growthContext) { // Paper + boolean flag = false; + boolean flag1 = false; + +diff --git a/net/minecraft/world/level/block/NyliumBlock.java b/net/minecraft/world/level/block/NyliumBlock.java +index 7357bf27bb0890cefd8f8188a7a326a631cd0ff6..c892e251acf6c2744a4712bcc488ffc5298e2bc4 100644 +--- a/net/minecraft/world/level/block/NyliumBlock.java ++++ b/net/minecraft/world/level/block/NyliumBlock.java +@@ -59,7 +59,7 @@ public class NyliumBlock extends Block implements BonemealableBlock { + } + + @Override +- public void performBonemeal(ServerLevel level, RandomSource random, BlockPos pos, BlockState state) { ++ public void performBonemeal(io.papermc.paper.util.capture.PaperCapturingWorldLevel level, RandomSource random, BlockPos pos, BlockState state, io.papermc.paper.util.capture.GrowthContext growthContext) { // Paper + BlockState blockState = level.getBlockState(pos); + BlockPos blockPos = pos.above(); + ChunkGenerator generator = level.getChunkSource().getGenerator(); +@@ -78,7 +78,7 @@ public class NyliumBlock extends Block implements BonemealableBlock { + private void place( + Registry> featureRegistry, + ResourceKey> featureKey, +- ServerLevel level, ++ io.papermc.paper.util.capture.PaperCapturingWorldLevel level, // Paper + ChunkGenerator chunkGenerator, + RandomSource random, + BlockPos pos +diff --git a/net/minecraft/world/level/block/PitcherCropBlock.java b/net/minecraft/world/level/block/PitcherCropBlock.java +index cbaf7ea236e8689793af65f29af3f1860644d152..b21ffb0aaa27c7443437f3b7cdd64a44e0624f6b 100644 +--- a/net/minecraft/world/level/block/PitcherCropBlock.java ++++ b/net/minecraft/world/level/block/PitcherCropBlock.java +@@ -129,7 +129,7 @@ public class PitcherCropBlock extends DoublePlantBlock implements BonemealableBl + } + + @Override +- public void setPlacedBy(Level level, BlockPos pos, BlockState state, @Nullable LivingEntity placer, ItemStack stack) { ++ public void setPlacedBy(io.papermc.paper.util.capture.PaperCapturingWorldLevel level, BlockPos pos, BlockState state, @Nullable LivingEntity placer, ItemStack stack) { // Paper - block placement capturing + } + + @Override +@@ -146,7 +146,7 @@ public class PitcherCropBlock extends DoublePlantBlock implements BonemealableBl + } + } + +- private void grow(ServerLevel level, BlockState state, BlockPos pos, int ageIncrement) { ++ private void grow(io.papermc.paper.util.capture.PaperCapturingWorldLevel level, BlockState state, BlockPos pos, int ageIncrement) { // Paper + int min = Math.min(state.getValue(AGE) + ageIncrement, 4); + if (this.canGrow(level, pos, state, min)) { + BlockState blockState = state.setValue(AGE, min); +@@ -204,7 +204,7 @@ public class PitcherCropBlock extends DoublePlantBlock implements BonemealableBl + } + + @Override +- public void performBonemeal(ServerLevel level, RandomSource random, BlockPos pos, BlockState state) { ++ public void performBonemeal(io.papermc.paper.util.capture.PaperCapturingWorldLevel level, RandomSource random, BlockPos pos, BlockState state, io.papermc.paper.util.capture.GrowthContext growthContext) { // Paper + PitcherCropBlock.PosAndState lowerHalf = this.getLowerHalf(level, pos, state); + if (lowerHalf != null) { + this.grow(level, lowerHalf.state, lowerHalf.pos, 1); +diff --git a/net/minecraft/world/level/block/RootedDirtBlock.java b/net/minecraft/world/level/block/RootedDirtBlock.java +index db6b32016a1ad4264da9d8812e5ea9356d0601df..a9a2e451e54a616ca5d3e0d47381caba523e07e2 100644 +--- a/net/minecraft/world/level/block/RootedDirtBlock.java ++++ b/net/minecraft/world/level/block/RootedDirtBlock.java +@@ -32,7 +32,7 @@ public class RootedDirtBlock extends Block implements BonemealableBlock { + } + + @Override +- public void performBonemeal(ServerLevel level, RandomSource random, BlockPos pos, BlockState state) { ++ public void performBonemeal(io.papermc.paper.util.capture.PaperCapturingWorldLevel level, RandomSource random, BlockPos pos, BlockState state, io.papermc.paper.util.capture.GrowthContext growthContext) { // Paper + org.bukkit.craftbukkit.event.CraftEventFactory.handleBlockSpreadEvent(level, pos, pos.below(), Blocks.HANGING_ROOTS.defaultBlockState(), Block.UPDATE_ALL); // CraftBukkit + } + +diff --git a/net/minecraft/world/level/block/SaplingBlock.java b/net/minecraft/world/level/block/SaplingBlock.java +index 23e9e5e7ef76fe3d6e1bbc41faf69ee65ca77d80..4155c1eb68e0521e452b070f43d7cb6d4216a31b 100644 +--- a/net/minecraft/world/level/block/SaplingBlock.java ++++ b/net/minecraft/world/level/block/SaplingBlock.java +@@ -25,7 +25,6 @@ public class SaplingBlock extends VegetationBlock implements BonemealableBlock { + public static final IntegerProperty STAGE = BlockStateProperties.STAGE; + private static final VoxelShape SHAPE = Block.column(12.0, 0.0, 12.0); + protected final TreeGrower treeGrower; +- public static @javax.annotation.Nullable org.bukkit.TreeType treeType; // CraftBukkit + + @Override + public MapCodec codec() { +@@ -50,38 +49,17 @@ public class SaplingBlock extends VegetationBlock implements BonemealableBlock { + } + } + ++ // Paper start + public void advanceTree(ServerLevel level, BlockPos pos, BlockState state, RandomSource random) { ++ this.advanceTree(level, pos, state, random, io.papermc.paper.util.capture.GrowthContext.empty()); ++ } ++ ++ public void advanceTree(io.papermc.paper.util.capture.PaperCapturingWorldLevel level, BlockPos pos, BlockState state, RandomSource random, io.papermc.paper.util.capture.GrowthContext growthContext) { ++ // Paper end + if (state.getValue(STAGE) == 0) { + level.setBlock(pos, state.cycle(STAGE), Block.UPDATE_NONE); + } else { +- // CraftBukkit start +- if (level.captureTreeGeneration) { +- this.treeGrower.growTree(level, level.getChunkSource().getGenerator(), pos, state, random); +- } else { +- level.captureTreeGeneration = true; +- this.treeGrower.growTree(level, level.getChunkSource().getGenerator(), pos, state, random); +- level.captureTreeGeneration = false; +- if (!level.capturedBlockStates.isEmpty()) { +- org.bukkit.TreeType treeType = SaplingBlock.treeType; +- SaplingBlock.treeType = null; +- org.bukkit.Location location = org.bukkit.craftbukkit.util.CraftLocation.toBukkit(pos, level); +- java.util.List blocks = new java.util.ArrayList<>(level.capturedBlockStates.values()); +- level.capturedBlockStates.clear(); +- org.bukkit.event.world.StructureGrowEvent event = null; +- if (treeType != null) { +- event = new org.bukkit.event.world.StructureGrowEvent(location, treeType, false, null, blocks); +- org.bukkit.Bukkit.getPluginManager().callEvent(event); +- } +- if (event == null || !event.isCancelled()) { +- for (org.bukkit.block.BlockState blockstate : blocks) { +- org.bukkit.craftbukkit.block.CraftBlockState craftBlockState = (org.bukkit.craftbukkit.block.CraftBlockState) blockstate; +- craftBlockState.place(craftBlockState.getFlags()); +- level.checkCapturedTreeStateForObserverNotify(pos, craftBlockState); // Paper - notify observers even if grow failed +- } +- } +- } +- } +- // CraftBukkit end ++ this.treeGrower.growTree(level, level.getChunkSource().getGenerator(), pos, state, random, growthContext); // Paper + } + } + +@@ -96,8 +74,8 @@ public class SaplingBlock extends VegetationBlock implements BonemealableBlock { + } + + @Override +- public void performBonemeal(ServerLevel level, RandomSource random, BlockPos pos, BlockState state) { +- this.advanceTree(level, pos, state, random); ++ public void performBonemeal(io.papermc.paper.util.capture.PaperCapturingWorldLevel level, RandomSource random, BlockPos pos, BlockState state, io.papermc.paper.util.capture.GrowthContext growthContext) { // Paper ++ this.advanceTree(level, pos, state, random, growthContext); // Paper + } + + @Override +diff --git a/net/minecraft/world/level/block/SeaPickleBlock.java b/net/minecraft/world/level/block/SeaPickleBlock.java +index f59fb2923bbcdf9bc73747b6aa9199fef4c2319f..bcad45255d3e0b117f62e11ec2f79153306be816 100644 +--- a/net/minecraft/world/level/block/SeaPickleBlock.java ++++ b/net/minecraft/world/level/block/SeaPickleBlock.java +@@ -130,7 +130,7 @@ public class SeaPickleBlock extends VegetationBlock implements BonemealableBlock + } + + @Override +- public void performBonemeal(ServerLevel level, RandomSource random, BlockPos pos, BlockState state) { ++ public void performBonemeal(io.papermc.paper.util.capture.PaperCapturingWorldLevel level, RandomSource random, BlockPos pos, BlockState state, io.papermc.paper.util.capture.GrowthContext growthContext) { // Paper + int i = 5; + int i1 = 1; + int i2 = 2; +diff --git a/net/minecraft/world/level/block/SeagrassBlock.java b/net/minecraft/world/level/block/SeagrassBlock.java +index a41270e041ab408dc6ac1207f7b6ec2eaff161d4..7694b7900996bd89fbe4ede78f25be737b4fd417 100644 +--- a/net/minecraft/world/level/block/SeagrassBlock.java ++++ b/net/minecraft/world/level/block/SeagrassBlock.java +@@ -87,7 +87,7 @@ public class SeagrassBlock extends VegetationBlock implements BonemealableBlock, + } + + @Override +- public void performBonemeal(ServerLevel level, RandomSource random, BlockPos pos, BlockState state) { ++ public void performBonemeal(io.papermc.paper.util.capture.PaperCapturingWorldLevel level, RandomSource random, BlockPos pos, BlockState state, io.papermc.paper.util.capture.GrowthContext growthContext) { // Paper + BlockState blockState = Blocks.TALL_SEAGRASS.defaultBlockState(); + BlockState blockState1 = blockState.setValue(TallSeagrassBlock.HALF, DoubleBlockHalf.UPPER); + BlockPos blockPos = pos.above(); +diff --git a/net/minecraft/world/level/block/ShortDryGrassBlock.java b/net/minecraft/world/level/block/ShortDryGrassBlock.java +index 1df47e0ea401267027721342aaf26639b2622e13..910e5424b8f5aa15c665d205afbd38b414d8d346 100644 +--- a/net/minecraft/world/level/block/ShortDryGrassBlock.java ++++ b/net/minecraft/world/level/block/ShortDryGrassBlock.java +@@ -47,7 +47,7 @@ public class ShortDryGrassBlock extends DryVegetationBlock implements Bonemealab + } + + @Override +- public void performBonemeal(ServerLevel level, RandomSource random, BlockPos pos, BlockState state) { ++ public void performBonemeal(io.papermc.paper.util.capture.PaperCapturingWorldLevel level, RandomSource random, BlockPos pos, BlockState state, io.papermc.paper.util.capture.GrowthContext growthContext) { // Paper + level.setBlockAndUpdate(pos, Blocks.TALL_DRY_GRASS.defaultBlockState()); + } + } +diff --git a/net/minecraft/world/level/block/SmallDripleafBlock.java b/net/minecraft/world/level/block/SmallDripleafBlock.java +index d5821239d33aad9213b9a87d225e942934623857..7a0ceb68960101f8fb6c2cd34e32372a6bac43af 100644 +--- a/net/minecraft/world/level/block/SmallDripleafBlock.java ++++ b/net/minecraft/world/level/block/SmallDripleafBlock.java +@@ -64,7 +64,7 @@ public class SmallDripleafBlock extends DoublePlantBlock implements Bonemealable + } + + @Override +- public void setPlacedBy(Level level, BlockPos pos, BlockState state, @Nullable LivingEntity placer, ItemStack stack) { ++ public void setPlacedBy(io.papermc.paper.util.capture.PaperCapturingWorldLevel level, BlockPos pos, BlockState state, @Nullable LivingEntity placer, ItemStack stack) { // Paper - block placement capturing + if (!level.isClientSide()) { + BlockPos blockPos = pos.above(); + BlockState blockState = DoublePlantBlock.copyWaterloggedFrom( +@@ -124,14 +124,14 @@ public class SmallDripleafBlock extends DoublePlantBlock implements Bonemealable + } + + @Override +- public void performBonemeal(ServerLevel level, RandomSource random, BlockPos pos, BlockState state) { ++ public void performBonemeal(io.papermc.paper.util.capture.PaperCapturingWorldLevel level, RandomSource random, BlockPos pos, BlockState state, io.papermc.paper.util.capture.GrowthContext growthContext) { // Paper + if (state.getValue(DoublePlantBlock.HALF) == DoubleBlockHalf.LOWER) { + BlockPos blockPos = pos.above(); + level.setBlock(blockPos, level.getFluidState(blockPos).createLegacyBlock(), Block.UPDATE_CLIENTS | Block.UPDATE_KNOWN_SHAPE); + BigDripleafBlock.placeWithRandomHeight(level, random, pos, state.getValue(FACING)); + } else { + BlockPos blockPos = pos.below(); +- this.performBonemeal(level, random, blockPos, level.getBlockState(blockPos)); ++ this.performBonemeal(level, random, blockPos, level.getBlockState(blockPos), growthContext); // Paper + } + } + +diff --git a/net/minecraft/world/level/block/SpongeBlock.java b/net/minecraft/world/level/block/SpongeBlock.java +index 298e51da2da64fdaa860108cc34e59d214b5f9a7..86d6a41750e15b0c4c49706ffd827e4fbd14b47a 100644 +--- a/net/minecraft/world/level/block/SpongeBlock.java ++++ b/net/minecraft/world/level/block/SpongeBlock.java +@@ -121,7 +121,8 @@ public class SpongeBlock extends Block { + dropResources(blockState, level, blockPos, blockEntity); + } + } +- snapshot.place(snapshot.getFlags()); ++ int flags = blockList.getEffectiveFlags(snapshot.getPosition()); ++ snapshot.place(flags); + } + + return true; +diff --git a/net/minecraft/world/level/block/StemBlock.java b/net/minecraft/world/level/block/StemBlock.java +index 82ea0ce3895108d477d10e901e756a27a3a9cedc..dc963acafd12db0784721b3c6545c191c9aac129 100644 +--- a/net/minecraft/world/level/block/StemBlock.java ++++ b/net/minecraft/world/level/block/StemBlock.java +@@ -113,12 +113,12 @@ public class StemBlock extends VegetationBlock implements BonemealableBlock { + } + + @Override +- public void performBonemeal(ServerLevel level, RandomSource random, BlockPos pos, BlockState state) { +- int min = Math.min(7, state.getValue(AGE) + Mth.nextInt(level.random, 2, 5)); ++ public void performBonemeal(io.papermc.paper.util.capture.PaperCapturingWorldLevel level, RandomSource random, BlockPos pos, BlockState state, io.papermc.paper.util.capture.GrowthContext growthContext) { // Paper ++ int min = Math.min(7, state.getValue(AGE) + Mth.nextInt(level.getRandom(), 2, 5)); // Paper + BlockState blockState = state.setValue(AGE, min); + org.bukkit.craftbukkit.event.CraftEventFactory.handleBlockGrowEvent(level, pos, blockState, Block.UPDATE_CLIENTS); // CraftBukkit + if (min == 7) { +- blockState.randomTick(level, pos, level.random); ++ level.addTask((serverLevel) -> blockState.randomTick(serverLevel, pos, level.getRandom())); // Paper + } + } + +diff --git a/net/minecraft/world/level/block/StructureBlock.java b/net/minecraft/world/level/block/StructureBlock.java +index 5b19c117d7935c6b0c3887083f63cc293bc495e3..9fcbe1a3d741852674ed8942e10bc0cac574a81d 100644 +--- a/net/minecraft/world/level/block/StructureBlock.java ++++ b/net/minecraft/world/level/block/StructureBlock.java +@@ -50,7 +50,7 @@ public class StructureBlock extends BaseEntityBlock implements GameMasterBlock { + } + + @Override +- public void setPlacedBy(Level level, BlockPos pos, BlockState state, @Nullable LivingEntity placer, ItemStack stack) { ++ public void setPlacedBy(io.papermc.paper.util.capture.PaperCapturingWorldLevel level, BlockPos pos, BlockState state, @Nullable LivingEntity placer, ItemStack stack) { // Paper - block placement capturing + if (!level.isClientSide()) { + if (placer != null) { + BlockEntity blockEntity = level.getBlockEntity(pos); +diff --git a/net/minecraft/world/level/block/SweetBerryBushBlock.java b/net/minecraft/world/level/block/SweetBerryBushBlock.java +index 3d2bd187cb9f83f9369d11b21c484c21ceba0569..4712b7faac0612ee85a2c0e88a2c84ba289639c7 100644 +--- a/net/minecraft/world/level/block/SweetBerryBushBlock.java ++++ b/net/minecraft/world/level/block/SweetBerryBushBlock.java +@@ -160,7 +160,7 @@ public class SweetBerryBushBlock extends VegetationBlock implements Bonemealable + } + + @Override +- public void performBonemeal(ServerLevel level, RandomSource random, BlockPos pos, BlockState state) { ++ public void performBonemeal(io.papermc.paper.util.capture.PaperCapturingWorldLevel level, RandomSource random, BlockPos pos, BlockState state, io.papermc.paper.util.capture.GrowthContext growthContext) { // Paper + int min = Math.min(3, state.getValue(AGE) + 1); + level.setBlock(pos, state.setValue(AGE, min), Block.UPDATE_CLIENTS); + } +diff --git a/net/minecraft/world/level/block/TallDryGrassBlock.java b/net/minecraft/world/level/block/TallDryGrassBlock.java +index f0514cd9df52b3a459ff92812ae5ad9b0df85763..7dca920d5702292b01c495a167956616b867cf7f 100644 +--- a/net/minecraft/world/level/block/TallDryGrassBlock.java ++++ b/net/minecraft/world/level/block/TallDryGrassBlock.java +@@ -47,7 +47,7 @@ public class TallDryGrassBlock extends DryVegetationBlock implements Bonemealabl + } + + @Override +- public void performBonemeal(ServerLevel level, RandomSource random, BlockPos pos, BlockState state) { ++ public void performBonemeal(io.papermc.paper.util.capture.PaperCapturingWorldLevel level, RandomSource random, BlockPos pos, BlockState state, io.papermc.paper.util.capture.GrowthContext growthContext) { // Paper + BonemealableBlock.findSpreadableNeighbourPos(level, pos, Blocks.SHORT_DRY_GRASS.defaultBlockState()) + .ifPresent(blockPos -> level.setBlockAndUpdate(blockPos, Blocks.SHORT_DRY_GRASS.defaultBlockState())); + } +diff --git a/net/minecraft/world/level/block/TallFlowerBlock.java b/net/minecraft/world/level/block/TallFlowerBlock.java +index 4be72e53848b13eb68a2149749a88a83a4cb1e5c..c3ab67eb1da188b94fc1e2a4072bd08616391865 100644 +--- a/net/minecraft/world/level/block/TallFlowerBlock.java ++++ b/net/minecraft/world/level/block/TallFlowerBlock.java +@@ -33,7 +33,7 @@ public class TallFlowerBlock extends DoublePlantBlock implements BonemealableBlo + } + + @Override +- public void performBonemeal(ServerLevel level, RandomSource random, BlockPos pos, BlockState state) { +- popResource(level, pos, new ItemStack(this)); ++ public void performBonemeal(io.papermc.paper.util.capture.PaperCapturingWorldLevel level, RandomSource random, BlockPos pos, BlockState state, io.papermc.paper.util.capture.GrowthContext growthContext) { // Paper ++ level.addTask((serverLevel) -> popResource(serverLevel, pos, new ItemStack(this))); // Paper + } + } +diff --git a/net/minecraft/world/level/block/TallGrassBlock.java b/net/minecraft/world/level/block/TallGrassBlock.java +index 6c5259371fb8e2e131b2c69f354521b1e902ac4e..e7978ffe8702b12e3e1d0370237af76b43aeb89f 100644 +--- a/net/minecraft/world/level/block/TallGrassBlock.java ++++ b/net/minecraft/world/level/block/TallGrassBlock.java +@@ -41,7 +41,7 @@ public class TallGrassBlock extends VegetationBlock implements BonemealableBlock + } + + @Override +- public void performBonemeal(ServerLevel level, RandomSource random, BlockPos pos, BlockState state) { ++ public void performBonemeal(io.papermc.paper.util.capture.PaperCapturingWorldLevel level, RandomSource random, BlockPos pos, BlockState state, io.papermc.paper.util.capture.GrowthContext growthContext) { // Paper + DoublePlantBlock.placeAt(level, getGrownBlock(state).defaultBlockState(), pos, Block.UPDATE_CLIENTS); + } + +diff --git a/net/minecraft/world/level/block/TorchflowerCropBlock.java b/net/minecraft/world/level/block/TorchflowerCropBlock.java +index 18f8c389c33fcdc84a48f44cefab9c31b0a3e9ca..4ebd44bb524836abe01e15b1915d565588591a36 100644 +--- a/net/minecraft/world/level/block/TorchflowerCropBlock.java ++++ b/net/minecraft/world/level/block/TorchflowerCropBlock.java +@@ -70,7 +70,7 @@ public class TorchflowerCropBlock extends CropBlock { + } + + @Override +- protected int getBonemealAgeIncrease(Level level) { ++ protected int getBonemealAgeIncrease(net.minecraft.world.level.LevelAccessor level) { // Paper + return 1; + } + } +diff --git a/net/minecraft/world/level/block/TripWireHookBlock.java b/net/minecraft/world/level/block/TripWireHookBlock.java +index 292fe41967db74884e5937cb125f1e022ccfb6bd..5ca452ae97cd1ee8dfd42c4f99971a927c95d320 100644 +--- a/net/minecraft/world/level/block/TripWireHookBlock.java ++++ b/net/minecraft/world/level/block/TripWireHookBlock.java +@@ -101,8 +101,8 @@ public class TripWireHookBlock extends Block { + } + + @Override +- public void setPlacedBy(Level level, BlockPos pos, BlockState state, @Nullable LivingEntity placer, ItemStack stack) { +- calculateState(level, pos, state, false, false, -1, null); ++ public void setPlacedBy(io.papermc.paper.util.capture.PaperCapturingWorldLevel level, BlockPos pos, BlockState state, @Nullable LivingEntity placer, ItemStack stack) { // Paper - block placement capturing ++ level.addTask((serverLevel) -> calculateState(serverLevel, pos, state, false, false, -1, null)); // Paper - block placement capturing + } + + public static void calculateState( +diff --git a/net/minecraft/world/level/block/WitherSkullBlock.java b/net/minecraft/world/level/block/WitherSkullBlock.java +index 24215493601ff724032660178c1261f3c40edd61..e63433a4d72c715d48e4f36011ff113d25f1485d 100644 +--- a/net/minecraft/world/level/block/WitherSkullBlock.java ++++ b/net/minecraft/world/level/block/WitherSkullBlock.java +@@ -38,8 +38,8 @@ public class WitherSkullBlock extends SkullBlock { + } + + @Override +- public void setPlacedBy(Level level, BlockPos pos, BlockState state, @Nullable LivingEntity placer, ItemStack stack) { +- checkSpawn(level, pos); ++ public void setPlacedBy(io.papermc.paper.util.capture.PaperCapturingWorldLevel level, BlockPos pos, BlockState state, @Nullable LivingEntity placer, ItemStack stack) { // Paper - block placement capturing ++ level.addTask((serverLevel) -> checkSpawn(serverLevel, pos)); // Paper - block placement capturing + } + + public static void checkSpawn(Level level, BlockPos pos) { +@@ -49,7 +49,6 @@ public class WitherSkullBlock extends SkullBlock { + } + + public static void checkSpawn(Level level, BlockPos pos, SkullBlockEntity blockEntity) { +- if (level.captureBlockStates) return; // CraftBukkit + if (!level.isClientSide()) { + BlockState blockState = blockEntity.getBlockState(); + boolean flag = blockState.is(Blocks.WITHER_SKELETON_SKULL) || blockState.is(Blocks.WITHER_SKELETON_WALL_SKULL); +diff --git a/net/minecraft/world/level/block/WitherWallSkullBlock.java b/net/minecraft/world/level/block/WitherWallSkullBlock.java +index c57cb7b47a01e313a1be5de769c6766ac2787d95..f34731580bc880dcc5d3e96923a7f2b4125ae9cc 100644 +--- a/net/minecraft/world/level/block/WitherWallSkullBlock.java ++++ b/net/minecraft/world/level/block/WitherWallSkullBlock.java +@@ -22,7 +22,7 @@ public class WitherWallSkullBlock extends WallSkullBlock { + } + + @Override +- public void setPlacedBy(Level level, BlockPos pos, BlockState state, @Nullable LivingEntity placer, ItemStack stack) { +- WitherSkullBlock.checkSpawn(level, pos); ++ public void setPlacedBy(io.papermc.paper.util.capture.PaperCapturingWorldLevel level, BlockPos pos, BlockState state, @Nullable LivingEntity placer, ItemStack stack) { // Paper - block placement capturing ++ level.addTask((serverLevel) -> WitherSkullBlock.checkSpawn(serverLevel, pos)); // Paper - block placement capturing + } + } +diff --git a/net/minecraft/world/level/block/grower/TreeGrower.java b/net/minecraft/world/level/block/grower/TreeGrower.java +index 5471619a0484ece08640e2b3fd26746c351dc3e0..ac5b2e9b10bed4351afdb58c440e1ec72d4b11b5 100644 +--- a/net/minecraft/world/level/block/grower/TreeGrower.java ++++ b/net/minecraft/world/level/block/grower/TreeGrower.java +@@ -122,7 +122,35 @@ public final class TreeGrower { + return this.secondaryMegaTree.isPresent() && random.nextFloat() < this.secondaryChance ? this.secondaryMegaTree.get() : this.megaTree.orElse(null); + } + +- public boolean growTree(ServerLevel level, ChunkGenerator chunkGenerator, BlockPos pos, BlockState state, RandomSource random) { ++ // Paper start ++ @Deprecated @io.papermc.paper.annotation.DoNotUse ++ public boolean growTree(io.papermc.paper.util.capture.PaperCapturingWorldLevel level, ChunkGenerator chunkGenerator, BlockPos pos, BlockState state, RandomSource random) { ++ return this.growTree0(level, chunkGenerator, pos, state, random, null); ++ } ++ ++ public boolean growTree(io.papermc.paper.util.capture.PaperCapturingWorldLevel level, ChunkGenerator chunkGenerator, BlockPos pos, BlockState state, RandomSource random, io.papermc.paper.util.capture.GrowthContext growthContext) { ++ try (io.papermc.paper.util.capture.SimpleBlockCapture capture = level.forkCaptureSession()) { ++ io.papermc.paper.util.capture.MinecraftCaptureBridge captureTreeGeneration = capture.capturingWorldLevel(); ++ if (this.growTree0(captureTreeGeneration, chunkGenerator, pos, state, random, growthContext)) { ++ org.bukkit.Location location = org.bukkit.craftbukkit.util.CraftLocation.toBukkit(pos, level.getLevel()); ++ java.util.List blocks = captureTreeGeneration.calculateLatestSnapshots(level.getLevel()); ++ org.bukkit.event.world.StructureGrowEvent event = new org.bukkit.event.world.StructureGrowEvent(location, growthContext.getTreeSpecies(), growthContext.usedBoneMeal(), growthContext.getBukkitPlayer(), blocks); ++ event.setCancelled(growthContext.cancelled); ++ ++ if (event.callEvent()) { ++ captureTreeGeneration.applyTasks(); // todo block list is mutable ++ return true; ++ } else { ++ growthContext.cancelled = true; ++ } ++ } ++ } ++ ++ return false; ++ } ++ ++ private boolean growTree0(io.papermc.paper.util.capture.PaperCapturingWorldLevel level, ChunkGenerator chunkGenerator, BlockPos pos, BlockState state, RandomSource random, io.papermc.paper.util.capture.GrowthContext growthContext) { ++ // Paper end + ResourceKey> configuredMegaFeature = this.getConfiguredMegaFeature(random); + if (configuredMegaFeature != null) { + Holder> holder = level.registryAccess() +@@ -130,7 +158,7 @@ public final class TreeGrower { + .get(configuredMegaFeature) + .orElse(null); + if (holder != null) { +- this.setTreeType(holder); // CraftBukkit ++ growthContext.feature = holder; // Paper + for (int i = 0; i >= -1; i--) { + for (int i1 = 0; i1 >= -1; i1--) { + if (isTwoByTwoSapling(state, level, pos, i, i1)) { +@@ -163,7 +191,7 @@ public final class TreeGrower { + if (holder1 == null) { + return false; + } else { +- this.setTreeType(holder1); // CraftBukkit ++ growthContext.feature = holder1; // Paper + ConfiguredFeature configuredFeature2 = holder1.value(); + BlockState blockState1 = level.getFluidState(pos).createLegacyBlock(); + level.setBlock(pos, blockState1, Block.UPDATE_NONE); +@@ -198,58 +226,4 @@ public final class TreeGrower { + + return false; + } +- +- // CraftBukkit start +- private void setTreeType(Holder> feature) { +- if (feature.is(TreeFeatures.OAK) || feature.is(TreeFeatures.OAK_BEES_005)) { +- net.minecraft.world.level.block.SaplingBlock.treeType = org.bukkit.TreeType.TREE; +- } else if (feature.is(TreeFeatures.HUGE_RED_MUSHROOM)) { +- net.minecraft.world.level.block.SaplingBlock.treeType = org.bukkit.TreeType.RED_MUSHROOM; +- } else if (feature.is(TreeFeatures.HUGE_BROWN_MUSHROOM)) { +- net.minecraft.world.level.block.SaplingBlock.treeType = org.bukkit.TreeType.BROWN_MUSHROOM; +- } else if (feature.is(TreeFeatures.JUNGLE_TREE)) { +- net.minecraft.world.level.block.SaplingBlock.treeType = org.bukkit.TreeType.COCOA_TREE; +- } else if (feature.is(TreeFeatures.JUNGLE_TREE_NO_VINE)) { +- net.minecraft.world.level.block.SaplingBlock.treeType = org.bukkit.TreeType.SMALL_JUNGLE; +- } else if (feature.is(TreeFeatures.PINE)) { +- net.minecraft.world.level.block.SaplingBlock.treeType = org.bukkit.TreeType.TALL_REDWOOD; +- } else if (feature.is(TreeFeatures.SPRUCE)) { +- net.minecraft.world.level.block.SaplingBlock.treeType = org.bukkit.TreeType.REDWOOD; +- } else if (feature.is(TreeFeatures.ACACIA)) { +- net.minecraft.world.level.block.SaplingBlock.treeType = org.bukkit.TreeType.ACACIA; +- } else if (feature.is(TreeFeatures.BIRCH) || feature.is(TreeFeatures.BIRCH_BEES_005)) { +- net.minecraft.world.level.block.SaplingBlock.treeType = org.bukkit.TreeType.BIRCH; +- } else if (feature.is(TreeFeatures.SUPER_BIRCH_BEES_0002)) { +- net.minecraft.world.level.block.SaplingBlock.treeType = org.bukkit.TreeType.TALL_BIRCH; +- } else if (feature.is(TreeFeatures.SWAMP_OAK)) { +- net.minecraft.world.level.block.SaplingBlock.treeType = org.bukkit.TreeType.SWAMP; +- } else if (feature.is(TreeFeatures.FANCY_OAK) || feature.is(TreeFeatures.FANCY_OAK_BEES_005)) { +- net.minecraft.world.level.block.SaplingBlock.treeType = org.bukkit.TreeType.BIG_TREE; +- } else if (feature.is(TreeFeatures.JUNGLE_BUSH)) { +- net.minecraft.world.level.block.SaplingBlock.treeType = org.bukkit.TreeType.JUNGLE_BUSH; +- } else if (feature.is(TreeFeatures.DARK_OAK)) { +- net.minecraft.world.level.block.SaplingBlock.treeType = org.bukkit.TreeType.DARK_OAK; +- } else if (feature.is(TreeFeatures.MEGA_SPRUCE)) { +- net.minecraft.world.level.block.SaplingBlock.treeType = org.bukkit.TreeType.MEGA_REDWOOD; +- } else if (feature.is(TreeFeatures.MEGA_PINE)) { +- net.minecraft.world.level.block.SaplingBlock.treeType = org.bukkit.TreeType.MEGA_PINE; +- } else if (feature.is(TreeFeatures.MEGA_JUNGLE_TREE)) { +- net.minecraft.world.level.block.SaplingBlock.treeType = org.bukkit.TreeType.JUNGLE; +- } else if (feature.is(TreeFeatures.AZALEA_TREE)) { +- net.minecraft.world.level.block.SaplingBlock.treeType = org.bukkit.TreeType.AZALEA; +- } else if (feature.is(TreeFeatures.MANGROVE)) { +- net.minecraft.world.level.block.SaplingBlock.treeType = org.bukkit.TreeType.MANGROVE; +- } else if (feature.is(TreeFeatures.TALL_MANGROVE)) { +- net.minecraft.world.level.block.SaplingBlock.treeType = org.bukkit.TreeType.TALL_MANGROVE; +- } else if (feature.is(TreeFeatures.CHERRY) || feature.is(TreeFeatures.CHERRY_BEES_005)) { +- net.minecraft.world.level.block.SaplingBlock.treeType = org.bukkit.TreeType.CHERRY; +- } else if (feature.is(TreeFeatures.PALE_OAK) || feature.is(TreeFeatures.PALE_OAK_BONEMEAL)) { +- net.minecraft.world.level.block.SaplingBlock.treeType = org.bukkit.TreeType.PALE_OAK; +- } else if (feature.is(TreeFeatures.PALE_OAK_CREAKING)) { +- net.minecraft.world.level.block.SaplingBlock.treeType = org.bukkit.TreeType.PALE_OAK_CREAKING; +- } else { +- throw new IllegalArgumentException("Unknown tree generator " + feature); +- } +- } +- // CraftBukkit end + } +diff --git a/net/minecraft/world/level/block/piston/PistonBaseBlock.java b/net/minecraft/world/level/block/piston/PistonBaseBlock.java +index 3bbfa079a2a2da3de361352738b6101894acf82c..c743c9fb8a829f57758a0c68139bfdf06e4a299f 100644 +--- a/net/minecraft/world/level/block/piston/PistonBaseBlock.java ++++ b/net/minecraft/world/level/block/piston/PistonBaseBlock.java +@@ -73,9 +73,9 @@ public class PistonBaseBlock extends DirectionalBlock { + } + + @Override +- public void setPlacedBy(Level level, BlockPos pos, BlockState state, @Nullable LivingEntity placer, ItemStack stack) { ++ public void setPlacedBy(io.papermc.paper.util.capture.PaperCapturingWorldLevel level, BlockPos pos, BlockState state, @Nullable LivingEntity placer, ItemStack stack) { // Paper - block placement capturing + if (!level.isClientSide()) { +- this.checkIfExtend(level, pos, state); ++ level.addTask((serverLevel) -> this.checkIfExtend(serverLevel, pos, state)); // Paper - block placement capturing + } + } + +diff --git a/net/minecraft/world/level/chunk/LevelChunk.java b/net/minecraft/world/level/chunk/LevelChunk.java +index 3acc1374a7ef968d88e9f566ce7b812fb8d580af..4c9a8e3efc4621f0aff1f82965a9fa4aa2d7be00 100644 +--- a/net/minecraft/world/level/chunk/LevelChunk.java ++++ b/net/minecraft/world/level/chunk/LevelChunk.java +@@ -421,7 +421,7 @@ public class LevelChunk extends ChunkAccess implements DebugValueSource, ca.spot + if (!section.getBlockState(i, i1, i2).is(block)) { + return null; + } else { +- if (!this.level.isClientSide() && (flags & Block.UPDATE_SKIP_ON_PLACE) == 0 && (!this.level.captureBlockStates || block instanceof net.minecraft.world.level.block.BaseEntityBlock)) { // CraftBukkit - Don't place while processing the BlockPlaceEvent, unless it's a BlockContainer. Prevents blocks such as TNT from activating when cancelled. ++ if (!this.level.isClientSide() && (flags & Block.UPDATE_SKIP_ON_PLACE) == 0) { + state.onPlace(this.level, pos, blockState, flag1); + } + +@@ -472,12 +472,7 @@ public class LevelChunk extends ChunkAccess implements DebugValueSource, ca.spot + } + + public @Nullable BlockEntity getBlockEntity(BlockPos pos, LevelChunk.EntityCreationType creationType) { +- // CraftBukkit start +- BlockEntity blockEntity = this.level.capturedTileEntities.get(pos); +- if (blockEntity == null) { +- blockEntity = this.blockEntities.get(pos); +- } +- // CraftBukkit end ++ BlockEntity blockEntity = this.blockEntities.get(pos); + if (blockEntity == null) { + CompoundTag compoundTag = this.pendingBlockEntities.remove(pos); + if (compoundTag != null) { +diff --git a/net/minecraft/world/level/levelgen/feature/treedecorators/BeehiveDecorator.java b/net/minecraft/world/level/levelgen/feature/treedecorators/BeehiveDecorator.java +index aca28c507cb642ae10c70f8e8393db10c7bf6165..cf5e7cbc7ccb4e749ee74168946dd9fa84aed6c6 100644 +--- a/net/minecraft/world/level/levelgen/feature/treedecorators/BeehiveDecorator.java ++++ b/net/minecraft/world/level/levelgen/feature/treedecorators/BeehiveDecorator.java +@@ -27,7 +27,7 @@ public class BeehiveDecorator extends TreeDecorator { + private final float probability; + + public BeehiveDecorator(float probability) { +- this.probability = probability; ++ this.probability = 1; // Paper - TODO revert + } + + @Override +diff --git a/net/minecraft/world/level/saveddata/maps/MapItemSavedData.java b/net/minecraft/world/level/saveddata/maps/MapItemSavedData.java +index 1af585f9554278983148096c73c86e18166f5471..5852c8cc5cf68be92bcd6d9588007c0911743ce4 100644 +--- a/net/minecraft/world/level/saveddata/maps/MapItemSavedData.java ++++ b/net/minecraft/world/level/saveddata/maps/MapItemSavedData.java +@@ -432,7 +432,7 @@ public class MapItemSavedData extends SavedData { + return true; + } + +- if (!this.isTrackedCountOverLimit(((Level) level).paperConfig().maps.itemFrameCursorLimit)) { // Paper - Limit item frame cursors on maps ++ if (!this.isTrackedCountOverLimit(level.getMinecraftWorld().paperConfig().maps.itemFrameCursorLimit)) { // Paper - Limit item frame cursors on maps + this.bannerMarkers.put(mapBanner.getId(), mapBanner); + this.addDecoration(mapBanner.getDecoration(), level, mapBanner.getId(), d, d1, 180.0, mapBanner.name().orElse(null)); + this.setDirty(); diff --git a/paper-server/src/main/java/io/papermc/paper/util/capture/BlockPlacementPredictor.java b/paper-server/src/main/java/io/papermc/paper/util/capture/BlockPlacementPredictor.java new file mode 100644 index 000000000000..035df5f4f6dc --- /dev/null +++ b/paper-server/src/main/java/io/papermc/paper/util/capture/BlockPlacementPredictor.java @@ -0,0 +1,34 @@ +package io.papermc.paper.util.capture; + +import java.util.Optional; +import net.minecraft.core.BlockPos; +import net.minecraft.world.level.block.entity.BlockEntity; +import net.minecraft.world.level.block.state.BlockState; +import org.jspecify.annotations.Nullable; + +public interface BlockPlacementPredictor { + + Optional getLatestBlockAt(BlockPos pos); + + Optional getLatestBlockAtIfLoaded(BlockPos pos); + + Optional getLatestBlockEntityAt(BlockPos pos); + + record BlockEntityPlacement(boolean removed, @Nullable BlockEntity blockEntity) { + + public static final Optional ABSENT = Optional.of(new BlockEntityPlacement(false, null)); + + public @Nullable BlockEntity res() { + return this.removed ? null : this.blockEntity; + } + } + + record LoadedBlockState(boolean present, @Nullable BlockState state) { + + public static final Optional UNLOADED = Optional.of(new LoadedBlockState(false, null)); + + public @Nullable BlockState res() { + return this.present ? this.state : null; + } + } +} diff --git a/paper-server/src/main/java/io/papermc/paper/util/capture/CaptureRecordMap.java b/paper-server/src/main/java/io/papermc/paper/util/capture/CaptureRecordMap.java new file mode 100644 index 000000000000..2d7cdd9fb9bf --- /dev/null +++ b/paper-server/src/main/java/io/papermc/paper/util/capture/CaptureRecordMap.java @@ -0,0 +1,142 @@ +package io.papermc.paper.util.capture; + +import java.util.ArrayList; +import java.util.HashMap; +import java.util.List; +import java.util.Map; +import java.util.Optional; +import java.util.stream.Stream; +import net.minecraft.core.BlockPos; +import net.minecraft.server.level.ServerLevel; +import net.minecraft.world.level.block.Block; +import net.minecraft.world.level.block.entity.BlockEntity; +import net.minecraft.world.level.block.state.BlockState; +import org.bukkit.craftbukkit.block.CraftBlock; +import org.bukkit.craftbukkit.block.CraftBlockStates; +import org.jspecify.annotations.Nullable; + +/* +The premise of this storage is to essentially "mock" block placement in the game. +This should attempt to mirror block placement the best it can, in order to provide a read solution of possibly what blocks +are going to be placed in the world. + +The reason why we use a custom capture map rather than just storing the blocks placed in the world is to ensure +that blocks are placed in the correct order, and that the vanilla block chain can occur, which includes physics updates. + */ +public final class CaptureRecordMap { + + private final Map recordsByPos = new HashMap<>(); + + public void setLatestBlockStateAt(BlockPos pos, BlockState state, @Block.UpdateFlags int flags) { + this.add(new CaptureRecord(pos, state, flags)); + } + + public void setLatestBlockEntityAt(BlockPos pos, boolean clearPreviousBlockEntity, @Nullable BlockEntity add) { + CaptureRecord oldRecord = this.recordsByPos.get(pos); + if (oldRecord != null) { + oldRecord.setBlockEntity(clearPreviousBlockEntity, add); + } + } + + private void add(CaptureRecord record) { + this.recordsByPos.put(record.pos, record); + } + + public boolean isEmpty() { + return this.recordsByPos.isEmpty(); + } + + public @Nullable BlockState getLatestBlockStateAt(BlockPos pos) { + CaptureRecord record = this.recordsByPos.get(pos); + if (record == null) { + return null; + } + + return record.state; + } + + // Null indicates that it's not present, no override + // Optional empty indicates its being removed + public @Nullable Optional getLatestBlockEntityAt(BlockPos pos) { + CaptureRecord record = this.recordsByPos.get(pos); + if (record == null) { + return null; + } + + return Optional.ofNullable(record.blockEntity); + } + + public void applyBlockEntities(ServerLevel parent) { + this.recordsByPos.keySet().forEach((pos) -> { + Optional res = this.getLatestBlockEntityAt(pos); + if (res != null && res.isPresent()) { + parent.setBlockEntity(res.get()); + } + }); + } + + public void applyApiPatch(ServerLevel level) { + this.recordsByPos.keySet().forEach((pos) -> { + this.recordsByPos.get(pos).applyApiPatch(level); + }); + } + + // TODO: Clean this up + public List calculateLatestSnapshots(ServerLevel level) { + List out = new ArrayList<>(); + + for (Map.Entry entry : this.recordsByPos.entrySet()) { + CaptureRecord captureRecord = entry.getValue(); + out.add(CraftBlockStates.getBlockState(level.getWorld(), entry.getKey(), captureRecord.state, captureRecord.blockEntity)); + } + return out; + } + + public Stream getAffectedBlocks(ServerLevel level) { + return this.recordsByPos.keySet().stream().map(pos -> CraftBlock.at(level, pos)); + } + + public static class CaptureRecord { + + private final BlockPos pos; + + private BlockState state; + private @Nullable BlockEntity blockEntity; + private boolean clearPreviousBlockEntity; + private @Block.UpdateFlags int flags; + + public CaptureRecord(BlockPos pos, BlockState state, BlockEntity blockEntity) { + this.pos = pos; + this.state = state; + this.blockEntity = blockEntity; + } + + public CaptureRecord(BlockPos pos, BlockState state, @Block.UpdateFlags int flags) { + this.pos = pos; + this.state = state; + this.flags = flags; + } + + public CaptureRecord(boolean remove, @Nullable BlockEntity add, BlockPos pos) { + this.clearPreviousBlockEntity = remove; + this.blockEntity = add; + this.pos = pos; + } + + public void applyApiPatch(ServerLevel level) { + if (this.clearPreviousBlockEntity) { + level.removeBlockEntity(this.pos); + } + + level.setBlock(this.pos, this.state, this.flags); + if (this.blockEntity != null) { + level.setBlockEntity(this.blockEntity); + } + } + + public void setBlockEntity(boolean clearPreviousBlockEntity, @Nullable BlockEntity add) { + this.clearPreviousBlockEntity = clearPreviousBlockEntity; + this.blockEntity = add; + } + } +} diff --git a/paper-server/src/main/java/io/papermc/paper/util/capture/GrowthContext.java b/paper-server/src/main/java/io/papermc/paper/util/capture/GrowthContext.java new file mode 100644 index 000000000000..b8bb23d12a8d --- /dev/null +++ b/paper-server/src/main/java/io/papermc/paper/util/capture/GrowthContext.java @@ -0,0 +1,100 @@ +package io.papermc.paper.util.capture; + +import net.minecraft.Optionull; +import net.minecraft.core.Holder; +import net.minecraft.data.worldgen.features.TreeFeatures; +import net.minecraft.world.entity.Entity; +import net.minecraft.world.entity.player.Player; +import net.minecraft.world.level.levelgen.feature.ConfiguredFeature; +import org.bukkit.TreeType; +import org.jspecify.annotations.Nullable; + +public final class GrowthContext { + + public static GrowthContext empty() { + return new GrowthContext(null, false); + } + + public static GrowthContext usingBoneMeal(@Nullable Player user) { + return new GrowthContext(user, true); + } + + private final @Nullable Player player; + private final boolean usedBoneMeal; + + public boolean cancelled = false; + public Holder> feature; // just make this a field + + private GrowthContext(@Nullable Player player, boolean usedBoneMeal) { + this.player = player; + this.usedBoneMeal = usedBoneMeal; + } + + public org.bukkit.entity.@Nullable Player getBukkitPlayer() { + return (org.bukkit.entity.Player) Optionull.map(this.player, Entity::getBukkitEntity); + } + + public boolean usedBoneMeal() { + return this.usedBoneMeal; + } + + public TreeType getTreeSpecies() { + return getTreeSpecies(this.feature); + } + + private static TreeType getTreeSpecies(Holder> feature) { + if (feature.is(TreeFeatures.HUGE_RED_MUSHROOM)) { + return TreeType.RED_MUSHROOM; + } else if (feature.is(TreeFeatures.HUGE_BROWN_MUSHROOM)) { + return TreeType.BROWN_MUSHROOM; + } else if (feature.is(TreeFeatures.WARPED_FUNGUS_PLANTED)) { + return TreeType.WARPED_FUNGUS; + } else if (feature.is(TreeFeatures.CRIMSON_FUNGUS_PLANTED)) { + return TreeType.CRIMSON_FUNGUS; + } else if (feature.is(TreeFeatures.OAK) || feature.is(TreeFeatures.OAK_BEES_005)) { + return TreeType.TREE; + } else if (feature.is(TreeFeatures.JUNGLE_TREE)) { + return TreeType.COCOA_TREE; + } else if (feature.is(TreeFeatures.JUNGLE_TREE_NO_VINE)) { + return TreeType.SMALL_JUNGLE; + } else if (feature.is(TreeFeatures.PINE)) { + return TreeType.TALL_REDWOOD; + } else if (feature.is(TreeFeatures.SPRUCE)) { + return TreeType.REDWOOD; + } else if (feature.is(TreeFeatures.ACACIA)) { + return TreeType.ACACIA; + } else if (feature.is(TreeFeatures.BIRCH) || feature.is(TreeFeatures.BIRCH_BEES_005)) { + return TreeType.BIRCH; + } else if (feature.is(TreeFeatures.SUPER_BIRCH_BEES_0002)) { + return TreeType.TALL_BIRCH; + } else if (feature.is(TreeFeatures.SWAMP_OAK)) { + return TreeType.SWAMP; + } else if (feature.is(TreeFeatures.FANCY_OAK) || feature.is(TreeFeatures.FANCY_OAK_BEES_005)) { + return TreeType.BIG_TREE; + } else if (feature.is(TreeFeatures.JUNGLE_BUSH)) { + return TreeType.JUNGLE_BUSH; + } else if (feature.is(TreeFeatures.DARK_OAK)) { + return TreeType.DARK_OAK; + } else if (feature.is(TreeFeatures.MEGA_SPRUCE)) { + return TreeType.MEGA_REDWOOD; + } else if (feature.is(TreeFeatures.MEGA_PINE)) { + return TreeType.MEGA_PINE; + } else if (feature.is(TreeFeatures.MEGA_JUNGLE_TREE)) { + return TreeType.JUNGLE; + } else if (feature.is(TreeFeatures.AZALEA_TREE)) { + return TreeType.AZALEA; + } else if (feature.is(TreeFeatures.MANGROVE)) { + return TreeType.MANGROVE; + } else if (feature.is(TreeFeatures.TALL_MANGROVE)) { + return TreeType.TALL_MANGROVE; + } else if (feature.is(TreeFeatures.CHERRY) || feature.is(TreeFeatures.CHERRY_BEES_005)) { + return TreeType.CHERRY; + } else if (feature.is(TreeFeatures.PALE_OAK) || feature.is(TreeFeatures.PALE_OAK_BONEMEAL)) { + return TreeType.PALE_OAK; + } else if (feature.is(TreeFeatures.PALE_OAK_CREAKING)) { + return TreeType.PALE_OAK_CREAKING; + } else { + throw new IllegalArgumentException("Unknown tree feature: " + feature); + } + } +} diff --git a/paper-server/src/main/java/io/papermc/paper/util/capture/LayeredBlockPlacementPredictor.java b/paper-server/src/main/java/io/papermc/paper/util/capture/LayeredBlockPlacementPredictor.java new file mode 100644 index 000000000000..6beb6bc83408 --- /dev/null +++ b/paper-server/src/main/java/io/papermc/paper/util/capture/LayeredBlockPlacementPredictor.java @@ -0,0 +1,46 @@ +package io.papermc.paper.util.capture; + +import java.util.Optional; +import net.minecraft.core.BlockPos; +import net.minecraft.world.level.block.state.BlockState; + +public record LayeredBlockPlacementPredictor( + BlockPlacementPredictor... predictors +) implements BlockPlacementPredictor { + + @Override + public Optional getLatestBlockAt(BlockPos pos) { + for (BlockPlacementPredictor predictor : this.predictors) { + Optional state = predictor.getLatestBlockAt(pos); + if (state.isPresent()) { + return state; + } + } + + return Optional.empty(); + } + + @Override + public Optional getLatestBlockAtIfLoaded(BlockPos pos) { + for (BlockPlacementPredictor predictor : this.predictors) { + Optional state = predictor.getLatestBlockAtIfLoaded(pos); + if (state.isPresent()) { + return state; + } + } + + return Optional.empty(); + } + + @Override + public Optional getLatestBlockEntityAt(BlockPos pos) { + for (BlockPlacementPredictor predictor : this.predictors) { + Optional state = predictor.getLatestBlockEntityAt(pos); + if (state.isPresent()) { + return state; + } + } + + return Optional.empty(); + } +} diff --git a/paper-server/src/main/java/io/papermc/paper/util/capture/LiveBlockPlacementLayer.java b/paper-server/src/main/java/io/papermc/paper/util/capture/LiveBlockPlacementLayer.java new file mode 100644 index 000000000000..9ce7bdcf4b8f --- /dev/null +++ b/paper-server/src/main/java/io/papermc/paper/util/capture/LiveBlockPlacementLayer.java @@ -0,0 +1,46 @@ +package io.papermc.paper.util.capture; + +import java.util.Optional; +import java.util.function.Supplier; +import net.minecraft.core.BlockPos; +import net.minecraft.server.level.ServerLevel; +import net.minecraft.world.level.block.entity.BlockEntity; +import net.minecraft.world.level.block.state.BlockState; +import org.jspecify.annotations.Nullable; + +public record LiveBlockPlacementLayer(WorldCapturer capturer, ServerLevel level) implements BlockPlacementPredictor { + + @Override + public Optional getLatestBlockAt(BlockPos pos) { + return Optional.of(provideLive(() -> this.level.getBlockState(pos))); + } + + @Override + public Optional getLatestBlockAtIfLoaded(BlockPos pos) { + BlockState state = provideLive(() -> this.level.getBlockStateIfLoaded(pos)); + if (state == null) { + return LoadedBlockState.UNLOADED; + } + + return Optional.of(new LoadedBlockState(true, state)); + } + + @Override + public Optional getLatestBlockEntityAt(BlockPos pos) { + BlockEntity blockEntity = provideLive(() -> this.level.getBlockEntity(pos)); + if (blockEntity == null) { + return BlockEntityPlacement.ABSENT; + } + + return Optional.of(new BlockEntityPlacement(false, blockEntity)); + } + + public @Nullable T provideLive(Supplier<@Nullable T> valueProvider) { + SimpleBlockCapture blockCapture = this.capturer.getCapture(); + this.capturer.releaseCapture(null); + T value = valueProvider.get(); + this.capturer.releaseCapture(blockCapture); + + return value; + } +} diff --git a/paper-server/src/main/java/io/papermc/paper/util/capture/MinecraftCaptureBridge.java b/paper-server/src/main/java/io/papermc/paper/util/capture/MinecraftCaptureBridge.java new file mode 100644 index 000000000000..0057e91879a7 --- /dev/null +++ b/paper-server/src/main/java/io/papermc/paper/util/capture/MinecraftCaptureBridge.java @@ -0,0 +1,454 @@ +package io.papermc.paper.util.capture; + +import java.util.ArrayList; +import java.util.HashSet; +import java.util.List; +import java.util.Optional; +import java.util.Set; +import java.util.function.Consumer; +import java.util.function.Predicate; +import java.util.stream.Stream; +import net.minecraft.core.BlockPos; +import net.minecraft.core.Direction; +import net.minecraft.core.Holder; +import net.minecraft.core.RegistryAccess; +import net.minecraft.core.particles.ParticleOptions; +import net.minecraft.server.MinecraftServer; +import net.minecraft.server.level.ServerChunkCache; +import net.minecraft.server.level.ServerLevel; +import net.minecraft.sounds.SoundEvent; +import net.minecraft.sounds.SoundSource; +import net.minecraft.util.RandomSource; +import net.minecraft.world.DifficultyInstance; +import net.minecraft.world.attribute.EnvironmentAttributeReader; +import net.minecraft.world.entity.Entity; +import net.minecraft.world.entity.player.Player; +import net.minecraft.world.flag.FeatureFlagSet; +import net.minecraft.world.level.biome.Biome; +import net.minecraft.world.level.biome.BiomeManager; +import net.minecraft.world.level.block.Block; +import net.minecraft.world.level.block.entity.BlockEntity; +import net.minecraft.world.level.block.state.BlockState; +import net.minecraft.world.level.border.WorldBorder; +import net.minecraft.world.level.chunk.ChunkAccess; +import net.minecraft.world.level.chunk.status.ChunkStatus; +import net.minecraft.world.level.dimension.DimensionType; +import net.minecraft.world.level.entity.EntityTypeTest; +import net.minecraft.world.level.gameevent.GameEvent; +import net.minecraft.world.level.gamerules.GameRules; +import net.minecraft.world.level.levelgen.Heightmap; +import net.minecraft.world.level.lighting.LevelLightEngine; +import net.minecraft.world.level.material.Fluid; +import net.minecraft.world.level.material.FluidState; +import net.minecraft.world.level.storage.LevelData; +import net.minecraft.world.phys.AABB; +import net.minecraft.world.phys.Vec3; +import net.minecraft.world.ticks.LevelTickAccess; +import net.minecraft.world.ticks.ScheduledTick; +import org.jspecify.annotations.Nullable; + +public class MinecraftCaptureBridge implements PaperCapturingWorldLevel { + + private final ServerLevel parent; + private final List queuedTasks = new ArrayList<>(); + + // Effective represents plugin set blocks -> predicted blocks -> actual server level + private final BlockPlacementPredictor effectiveReadLayer; + private SimpleBlockPlacementPredictor writeLayer; + + // This is the layer that is written to in the server level potentially. + // Mostly plugin set blocks + private final SimpleBlockPlacementPredictor serverLevelOverlayLayer; + + private final CapturingTickAccess blocks; + private final CapturingTickAccess liquids; + + private Consumer sink = this.queuedTasks::add; + + public MinecraftCaptureBridge(ServerLevel parent, BlockPlacementPredictor baseReadLayer) { + this.parent = parent; + + this.serverLevelOverlayLayer = new SimpleBlockPlacementPredictor(); + SimpleBlockPlacementPredictor predictedBlocks = new SimpleBlockPlacementPredictor(); + + this.effectiveReadLayer = new LayeredBlockPlacementPredictor( + this.serverLevelOverlayLayer, // The overlay layer represents plugin set blocks! + predictedBlocks, // Now predicted blocks + baseReadLayer + ); + + this.writeLayer = predictedBlocks; // Predicting + + this.blocks = new CapturingTickAccess<>(parent.getBlockTicks()); + this.liquids = new CapturingTickAccess<>(parent.getFluidTicks()); + } + + @Override + public long getSeed() { + return this.parent.getSeed(); + } + + @Override + public ServerLevel getLevel() { + return this.parent; + } + + @Override + public DifficultyInstance getCurrentDifficultyAt(BlockPos pos) { + return this.parent.getCurrentDifficultyAt(pos); + } + + @Override + public long nextSubTickCount() { + return this.parent.nextSubTickCount(); + } + + @Override + public LevelData getLevelData() { + return this.parent.getLevelData(); + } + + @Override + public @Nullable MinecraftServer getServer() { + return this.parent.getServer(); + } + + @Override + public ServerChunkCache getChunkSource() { + return this.parent.getChunkSource(); + } + + @Override + public boolean setBlockAndUpdate(BlockPos pos, BlockState state) { + return this.setBlock(pos, state, Block.UPDATE_ALL); + } + + @Override + public SimpleBlockCapture forkCaptureSession() { + return this.parent.capturer.createCaptureSession(new BlockPlacementPredictor() { + @Override + public Optional getLatestBlockAt(BlockPos pos) { + return MinecraftCaptureBridge.this.effectiveReadLayer.getLatestBlockAt(pos); + } + + @Override + public Optional getLatestBlockAtIfLoaded(BlockPos pos) { + return MinecraftCaptureBridge.this.effectiveReadLayer.getLatestBlockAtIfLoaded(pos); + } + + @Override + public Optional getLatestBlockEntityAt(BlockPos pos) { + return MinecraftCaptureBridge.this.effectiveReadLayer.getLatestBlockEntityAt(pos); + } + }); + } + + @Override + public RandomSource getRandom() { + // TODO: Support rolling this back? + return this.parent.getRandom(); + } + + @Override + public void playSound(@Nullable Entity entity, BlockPos pos, SoundEvent sound, SoundSource source, float volume, float pitch) { + this.addTask((level) -> level.playSound(entity, pos, sound, source, volume, pitch)); + } + + @Override + public void addParticle(ParticleOptions options, double x, double y, double z, double xSpeed, double ySpeed, double zSpeed) { + this.addTask((level) -> level.addParticle(options, x, y, z, xSpeed, ySpeed, zSpeed)); + } + + @Override + public void levelEvent(@Nullable Entity entity, int type, BlockPos pos, int data) { + this.addTask((level) -> level.levelEvent(entity, type, pos, data)); + } + + @Override + public void gameEvent(Holder gameEvent, Vec3 pos, GameEvent.Context context) { + this.addTask((level) -> level.gameEvent(gameEvent, pos, context)); + } + + @Override + public float getShade(Direction direction, boolean shade) { + return this.parent.getShade(direction, shade); + } + + @Override + public LevelLightEngine getLightEngine() { + return this.parent.getLightEngine(); + } + + @Override + public WorldBorder getWorldBorder() { + return this.parent.getWorldBorder(); + } + + @Override + public @Nullable BlockEntity getBlockEntity(BlockPos pos) { + return this.effectiveReadLayer.getLatestBlockEntityAt(pos) + .map(BlockPlacementPredictor.BlockEntityPlacement::blockEntity) + .orElse(null); + } + + @Override + public BlockState getBlockState(BlockPos pos) { + return this.effectiveReadLayer.getLatestBlockAt(pos).orElseThrow(); // Should not ever be null, parent should pass value + } + + @Override + public @Nullable BlockState getBlockStateIfLoaded(BlockPos pos) { + return this.effectiveReadLayer.getLatestBlockAtIfLoaded(pos).map(BlockPlacementPredictor.LoadedBlockState::state).orElse(null); + } + + @Override + public @Nullable FluidState getFluidIfLoaded(BlockPos pos) { + return this.getBlockStateIfLoaded(pos).getFluidState(); + } + + @Override + public FluidState getFluidState(BlockPos pos) { + return this.getBlockState(pos).getFluidState(); + } + + @Override + public List getEntities(@Nullable Entity entity, AABB area, Predicate predicate) { + return this.parent.getEntities(entity, area, predicate); + } + + @Override + public List getEntities(EntityTypeTest entityTypeTest, AABB bounds, Predicate predicate) { + return this.parent.getEntities(entityTypeTest, bounds, predicate); + } + + @Override + public List players() { + return this.parent.players(); + } + + @Override + public @Nullable ChunkAccess getChunk(int x, int z, ChunkStatus chunkStatus, boolean requireChunk) { + return this.parent.getChunk(x, z, chunkStatus, requireChunk); + } + + @Override + public @Nullable ChunkAccess getChunkIfLoadedImmediately(int x, int z) { + return this.parent.getChunkIfLoadedImmediately(x, z); + } + + @Override + public int getHeight(Heightmap.Types heightmapType, int x, int z) { + return this.parent.getHeight(heightmapType, x, z); // TODO? + } + + @Override + public int getSkyDarken() { + return this.parent.getSkyDarken(); + } + + @Override + public BiomeManager getBiomeManager() { + return this.parent.getBiomeManager(); + } + + @Override + public Holder getUncachedNoiseBiome(int x, int y, int z) { + return this.parent.getUncachedNoiseBiome(x, y, z); + } + + @Override + public boolean isClientSide() { + return this.parent.isClientSide(); + } + + @Override + public int getSeaLevel() { + return this.parent.getSeaLevel(); + } + + @Override + public DimensionType dimensionType() { + return this.parent.dimensionType(); + } + + @Override + public RegistryAccess registryAccess() { + return this.parent.registryAccess(); + } + + @Override + public FeatureFlagSet enabledFeatures() { + return this.parent.enabledFeatures(); + } + + @Override + public EnvironmentAttributeReader environmentAttributes() { + return this.parent.environmentAttributes(); + } + + @Override + public boolean isStateAtPosition(BlockPos pos, Predicate state) { + return state.test(this.getBlockState(pos)); + } + + @Override + public boolean isFluidAtPosition(BlockPos pos, Predicate predicate) { + return predicate.test(this.getFluidState(pos)); + } + + public boolean silentSet(BlockPos pos, BlockState state, @Block.UpdateFlags int flags) { + return this.writeLayer.setBlockState(this.effectiveReadLayer, pos, state, flags); + } + + @Override + public boolean setBlock(BlockPos pos, BlockState state, @Block.UpdateFlags int flags, int recursionLeft) { + BlockPos copy = pos.immutable(); + this.addTask((level) -> level.setBlock(copy, state, flags, recursionLeft)); + + return this.writeLayer.setBlockState(this.effectiveReadLayer, copy, state, flags); + } + + @Override + public boolean removeBlock(BlockPos pos, boolean movedByPiston) { + BlockPos copy = pos.immutable(); + this.addTask((level) -> level.removeBlock(copy, movedByPiston)); + + FluidState fluidState = this.getFluidState(copy); + return this.silentSet(copy, fluidState.createLegacyBlock(), Block.UPDATE_ALL | (movedByPiston ? Block.UPDATE_MOVE_BY_PISTON : 0)); + } + + @Override + public boolean destroyBlock(BlockPos pos, boolean dropBlock, @Nullable Entity entity, int recursionLeft) { + BlockPos copy = pos.immutable(); + this.addTask((level) -> level.destroyBlock(copy, dropBlock, entity, recursionLeft)); + + BlockState blockState = this.getBlockState(copy); + if (blockState.isAir()) { + return false; + } else { + FluidState fluidState = this.getFluidState(copy); + + return this.silentSet(copy, fluidState.createLegacyBlock(), Block.UPDATE_ALL); + } + } + + @Override + public void sendBlockUpdated(BlockPos pos, BlockState oldState, BlockState newState, @Block.UpdateFlags int flags) { + BlockPos copy = pos.immutable(); + this.addTask((level) -> level.sendBlockUpdated(copy, oldState, newState, flags)); + } + + @Override + public void setBlockEntity(BlockEntity blockEntity) { + this.writeLayer.getRecordMap().setLatestBlockEntityAt(blockEntity.getBlockPos().immutable(), false, blockEntity); + } + + @Override + public boolean setBlockSilent(BlockPos pos, BlockState state, @Block.UpdateFlags int flags, int recursionLeft) { + BlockPos copy = pos.immutable(); + return this.silentSet(copy, state, flags); + } + + @Override + public LevelTickAccess getBlockTicks() { + return this.blocks; + } + + @Override + public LevelTickAccess getFluidTicks() { + return this.liquids; + } + + @Override + public GameRules getGameRules() { + return this.parent.getGameRules(); + } + + @Override + public void addTask(Consumer level) { + this.sink.accept(() -> level.accept(this.parent)); + } + + public net.minecraft.world.level.block.state.@Nullable BlockState getLatestBlockState(BlockPos pos) { + return this.effectiveReadLayer.getLatestBlockAt(pos).orElse(null); + } + + public @Nullable Optional getLatestBlockEntity(BlockPos pos) { + Optional placement = this.effectiveReadLayer.getLatestBlockEntityAt(pos); + if (placement.isEmpty() || (placement.get().blockEntity() == null && !placement.get().removed())) { + return null; + } + + return placement.map(BlockPlacementPredictor.BlockEntityPlacement::blockEntity); + } + + public List calculateLatestSnapshots(ServerLevel level) { + return this.writeLayer.getRecordMap().calculateLatestSnapshots(level); + } + + public Stream getAffectedBlocks(ServerLevel level) { + return this.writeLayer.getRecordMap().getAffectedBlocks(level); + } + + public void applyTasks() { + this.sink = Runnable::run; + for (Runnable runnable : this.queuedTasks) { + runnable.run(); + } + this.blocks.apply(); + this.liquids.apply(); + + // If we have changes that the plugin applied on top of the already existing changes, we know that we can apply them. + // So, do that! + if (!this.serverLevelOverlayLayer.isEmpty()) { + this.serverLevelOverlayLayer.getRecordMap().applyApiPatch(this.parent); + } + + // Apply block entities, those may have been written in our estimation. So just apply them. + // I don't really like this, as I in general don't really want to apply things from the prediction layer. + // But, these may be mutated by anything. + if (!this.writeLayer.getRecordMap().isEmpty()) { + this.writeLayer.getRecordMap().applyBlockEntities(this.parent); + } + } + + public void allowWriteOnLevel() { + this.writeLayer = this.serverLevelOverlayLayer; + } + + public static class CapturingTickAccess implements LevelTickAccess { + + private final LevelTickAccess wrapped; + private final Set scheduled = new HashSet<>(); + private final List> ticks = new ArrayList<>(); + + public CapturingTickAccess(LevelTickAccess wrapped) { + this.wrapped = wrapped; + } + + @Override + public boolean willTickThisTick(BlockPos pos, T type) { + return this.wrapped.willTickThisTick(pos, type); + } + + @Override + public void schedule(ScheduledTick tick) { + this.scheduled.add(tick.pos()); + this.ticks.add(tick); + } + + @Override + public boolean hasScheduledTick(BlockPos pos, T type) { + return this.wrapped.hasScheduledTick(pos, type) || this.scheduled.contains(pos); + } + + @Override + public int count() { + return this.wrapped.count() + this.scheduled.size(); + } + + public void apply() { + this.ticks.forEach(this.wrapped::schedule); + } + } +} diff --git a/paper-server/src/main/java/io/papermc/paper/util/capture/PaperCapturingWorldLevel.java b/paper-server/src/main/java/io/papermc/paper/util/capture/PaperCapturingWorldLevel.java new file mode 100644 index 000000000000..71c37888b0a7 --- /dev/null +++ b/paper-server/src/main/java/io/papermc/paper/util/capture/PaperCapturingWorldLevel.java @@ -0,0 +1,30 @@ +package io.papermc.paper.util.capture; + +import java.util.function.Consumer; +import net.minecraft.core.BlockPos; +import net.minecraft.server.level.ServerChunkCache; +import net.minecraft.server.level.ServerLevel; +import net.minecraft.world.level.WorldGenLevel; +import net.minecraft.world.level.block.Block; +import net.minecraft.world.level.block.entity.BlockEntity; +import net.minecraft.world.level.block.state.BlockState; +import net.minecraft.world.level.gamerules.GameRules; + +public interface PaperCapturingWorldLevel extends WorldGenLevel { + + GameRules getGameRules(); + + void sendBlockUpdated(BlockPos pos, BlockState oldState, BlockState newState, @Block.UpdateFlags int flags); + + void setBlockEntity(BlockEntity blockEntity); + + boolean setBlockSilent(BlockPos pos, BlockState state, @Block.UpdateFlags int flags, int recursionLeft); + + ServerChunkCache getChunkSource(); + + boolean setBlockAndUpdate(BlockPos pos, BlockState state); + + void addTask(Consumer level); + + SimpleBlockCapture forkCaptureSession(); +} diff --git a/paper-server/src/main/java/io/papermc/paper/util/capture/ServerLevelPaperCapturingWorldLevel.java b/paper-server/src/main/java/io/papermc/paper/util/capture/ServerLevelPaperCapturingWorldLevel.java new file mode 100644 index 000000000000..954cdff63d2b --- /dev/null +++ b/paper-server/src/main/java/io/papermc/paper/util/capture/ServerLevelPaperCapturingWorldLevel.java @@ -0,0 +1,47 @@ +package io.papermc.paper.util.capture; + +import java.util.function.Consumer; +import net.minecraft.core.BlockPos; +import net.minecraft.server.level.ServerLevel; +import net.minecraft.world.level.block.Block; +import net.minecraft.world.level.block.entity.BlockEntity; +import net.minecraft.world.level.block.state.BlockState; +import net.minecraft.world.level.gamerules.GameRules; + +public interface ServerLevelPaperCapturingWorldLevel extends PaperCapturingWorldLevel { + + @Override + default GameRules getGameRules() { + return this.getLevel().getGameRules(); + } + + @Override + default void sendBlockUpdated(BlockPos pos, BlockState oldState, BlockState newState, @Block.UpdateFlags int flags) { + this.getLevel().sendBlockUpdated(pos, oldState, newState, flags); + } + + @Override + default void setBlockEntity(BlockEntity blockEntity) { + this.getLevel().setBlockEntity(blockEntity); + } + + @Override + default boolean setBlockSilent(BlockPos pos, BlockState state, @Block.UpdateFlags int flags, int recursionLeft) { + return this.getLevel().setBlock(pos, state, flags, recursionLeft); + } + + @Override + default boolean setBlockAndUpdate(BlockPos pos, BlockState state) { + return this.getLevel().setBlockAndUpdate(pos, state); + } + + @Override + default void addTask(Consumer level) { + level.accept(this.getLevel()); + } + + @Override + default SimpleBlockCapture forkCaptureSession() { + return this.getLevel().capturer.createCaptureSession(); + } +} diff --git a/paper-server/src/main/java/io/papermc/paper/util/capture/SimpleBlockCapture.java b/paper-server/src/main/java/io/papermc/paper/util/capture/SimpleBlockCapture.java new file mode 100644 index 000000000000..a86ff96620f7 --- /dev/null +++ b/paper-server/src/main/java/io/papermc/paper/util/capture/SimpleBlockCapture.java @@ -0,0 +1,74 @@ +package io.papermc.paper.util.capture; + +import java.util.List; +import java.util.Optional; +import java.util.stream.Stream; +import net.minecraft.core.BlockPos; +import net.minecraft.server.level.ServerLevel; +import net.minecraft.world.level.block.entity.BlockEntity; +import org.bukkit.block.Block; +import org.bukkit.block.BlockState; +import org.jspecify.annotations.Nullable; + +public class SimpleBlockCapture implements AutoCloseable { + + private final MinecraftCaptureBridge capturingWorldLevel; + private final ServerLevel level; + private final @Nullable SimpleBlockCapture oldCapture; + + private boolean isOverlayingCaptureOnLevel = false; + + public SimpleBlockCapture(BlockPlacementPredictor base, ServerLevel level, @Nullable SimpleBlockCapture oldCapture) { + this.capturingWorldLevel = new MinecraftCaptureBridge(level, base); + this.level = level; + this.oldCapture = oldCapture; + } + + public MinecraftCaptureBridge capturingWorldLevel() { + return this.capturingWorldLevel; + } + + public boolean isCapturing() { + return true; + } + + public List getCapturedSnapshots() { + return this.capturingWorldLevel.calculateLatestSnapshots(this.level); + } + + public Stream getAffectedBlocks() { + return this.capturingWorldLevel.getAffectedBlocks(this.level); + } + + public net.minecraft.world.level.block.state.@Nullable BlockState getOverlayBlockState(BlockPos pos) { + return this.capturingWorldLevel.getLatestBlockState(pos); + } + + public @Nullable Optional getOverlayBlockEntity(BlockPos pos) { + return this.capturingWorldLevel.getLatestBlockEntity(pos); + } + + // This is done so that the captured blocks appear ontop of the world. + public void overlayCaptureOnLevel() { + this.isOverlayingCaptureOnLevel = true; + this.capturingWorldLevel.allowWriteOnLevel(); + } + + public boolean isOverlayingCaptureOnLevel() { + return this.isOverlayingCaptureOnLevel; + } + + public void finalizePlacement() { + this.level.capturer.releaseCapture(this.oldCapture); + this.capturingWorldLevel.applyTasks(); + } + + @Override + public void close() { + this.level.capturer.releaseCapture(this.oldCapture); + } + + public net.minecraft.world.level.block.state.@Nullable BlockState getCaptureBlockStateIfLoaded(BlockPos pos) { + return this.capturingWorldLevel.getBlockStateIfLoaded(pos); + } +} diff --git a/paper-server/src/main/java/io/papermc/paper/util/capture/SimpleBlockPlacementPredictor.java b/paper-server/src/main/java/io/papermc/paper/util/capture/SimpleBlockPlacementPredictor.java new file mode 100644 index 000000000000..a7575cd94243 --- /dev/null +++ b/paper-server/src/main/java/io/papermc/paper/util/capture/SimpleBlockPlacementPredictor.java @@ -0,0 +1,110 @@ +package io.papermc.paper.util.capture; + +import java.util.Optional; +import net.minecraft.core.BlockPos; +import net.minecraft.world.level.block.Block; +import net.minecraft.world.level.block.Blocks; +import net.minecraft.world.level.block.EntityBlock; +import net.minecraft.world.level.block.entity.BlockEntity; +import net.minecraft.world.level.block.state.BlockState; + +// Block state +class SimpleBlockPlacementPredictor implements BlockPlacementPredictor { + + private final CaptureRecordMap guesstimationMap = new CaptureRecordMap(); + + public boolean setBlockState(BlockPlacementPredictor layer, BlockPos pos, BlockState state, @Block.UpdateFlags int flags) { + BlockState blockState = layer.getLatestBlockAt(pos).orElse(Blocks.AIR.defaultBlockState()); + // Don't do any processing if the same + if (blockState == state) { + return false; + } else { + Block block = state.getBlock(); + + this.setLatestBlockAt(pos, state, flags); + + // TODO: local heightmaps? +// this.heightmaps.get(Heightmap.Types.MOTION_BLOCKING).update(i, y, i2, state); +// this.heightmaps.get(Heightmap.Types.MOTION_BLOCKING_NO_LEAVES).update(i, y, i2, state); +// this.heightmaps.get(Heightmap.Types.OCEAN_FLOOR).update(i, y, i2, state); +// this.heightmaps.get(Heightmap.Types.WORLD_SURFACE).update(i, y, i2, state); + + // LIGHT ENGINE CALCULATIONS + + boolean differentState = !blockState.is(block); + if (differentState && blockState.hasBlockEntity() && !state.shouldChangedStateKeepBlockEntity(blockState)) { + this.removeBlockEntity(pos); + } + +// if ((differentState || block instanceof BaseRailBlock) && ((flags & Block.UPDATE_NEIGHBORS) != 0 || updateMoveByPiston)) { +// BlockState finalBlockState = blockState; +// this.capturingWorldLevel.addTask((level) -> { +// finalBlockState.affectNeighborsAfterRemoval(level, pos, updateMoveByPiston); +// }); +// } + + if (state.hasBlockEntity()) { + BlockEntity blockEntity = this.getLatestBlockEntityAt(pos).map(BlockEntityPlacement::blockEntity).orElse(null); + if (blockEntity != null && !blockEntity.isValidBlockState(state)) { + blockEntity = null; + } + + if (blockEntity == null) { + blockEntity = ((EntityBlock) block).newBlockEntity(pos, state); + if (blockEntity != null) { + this.addAndRegisterBlockEntity(blockEntity); + } + } else { + blockEntity.setBlockState(state); + } + } + + } + + return true; + } + + + private void addAndRegisterBlockEntity(BlockEntity blockEntity) { + this.guesstimationMap.setLatestBlockEntityAt(blockEntity.getBlockPos(), false, blockEntity); + } + + private void removeBlockEntity(BlockPos pos) { + this.guesstimationMap.setLatestBlockEntityAt(pos, true, null); + } + + public boolean isEmpty() { + return this.guesstimationMap.isEmpty(); + } + + @Override + public Optional getLatestBlockEntityAt(BlockPos pos) { + Optional value = this.guesstimationMap.getLatestBlockEntityAt(pos); + if (value == null) { + return Optional.empty(); + } else { + return value + .map(block -> new BlockEntityPlacement(false, block)) + .or(() -> BlockEntityPlacement.ABSENT); + } + } + + @Override + public Optional getLatestBlockAt(BlockPos pos) { + return Optional.ofNullable(this.guesstimationMap.getLatestBlockStateAt(pos)); + } + + @Override + public Optional getLatestBlockAtIfLoaded(BlockPos pos) { + return Optional.ofNullable(this.guesstimationMap.getLatestBlockStateAt(pos)) + .map((state) -> new LoadedBlockState(true, state)); + } + + public void setLatestBlockAt(BlockPos pos, BlockState state, @Block.UpdateFlags int flags) { + this.guesstimationMap.setLatestBlockStateAt(pos, state, flags); + } + + public CaptureRecordMap getRecordMap() { + return this.guesstimationMap; + } +} diff --git a/paper-server/src/main/java/io/papermc/paper/util/capture/WorldCapturer.java b/paper-server/src/main/java/io/papermc/paper/util/capture/WorldCapturer.java new file mode 100644 index 000000000000..f9e182771fe3 --- /dev/null +++ b/paper-server/src/main/java/io/papermc/paper/util/capture/WorldCapturer.java @@ -0,0 +1,38 @@ +package io.papermc.paper.util.capture; + +import net.minecraft.server.level.ServerLevel; +import net.minecraft.world.level.Level; +import org.jspecify.annotations.Nullable; + +// TODO: Cleanup this state, because its held on the server world. I had proper state handling but threw it away at some point +public class WorldCapturer { + + public final ServerLevel level; + + private @Nullable SimpleBlockCapture capture; + + public WorldCapturer(Level level) { + this.level = (ServerLevel) level; + } + + public SimpleBlockCapture createCaptureSession(BlockPlacementPredictor blockPlacementPredictor) { + this.capture = new SimpleBlockCapture(blockPlacementPredictor, this.level, this.capture); + return this.capture; + } + + public SimpleBlockCapture createCaptureSession() { + return this.createCaptureSession(new LiveBlockPlacementLayer(this, this.level)); + } + + public void releaseCapture(@Nullable SimpleBlockCapture oldCapture) { + this.capture = oldCapture; + } + + public @Nullable SimpleBlockCapture getCapture() { + return this.capture; + } + + public boolean isCapturing() { + return this.capture != null; + } +} diff --git a/paper-server/src/main/java/io/papermc/paper/util/capture/package-info.java b/paper-server/src/main/java/io/papermc/paper/util/capture/package-info.java new file mode 100644 index 000000000000..a35a6fb4f7ce --- /dev/null +++ b/paper-server/src/main/java/io/papermc/paper/util/capture/package-info.java @@ -0,0 +1,4 @@ +@NullMarked +package io.papermc.paper.util.capture; + +import org.jspecify.annotations.NullMarked; diff --git a/paper-server/src/main/java/org/bukkit/craftbukkit/CraftWorld.java b/paper-server/src/main/java/org/bukkit/craftbukkit/CraftWorld.java index ebc65e3338c6..ff8a8cdf7cd4 100644 --- a/paper-server/src/main/java/org/bukkit/craftbukkit/CraftWorld.java +++ b/paper-server/src/main/java/org/bukkit/craftbukkit/CraftWorld.java @@ -13,6 +13,8 @@ import io.papermc.paper.raytracing.RayTraceTarget; import io.papermc.paper.registry.RegistryAccess; import io.papermc.paper.registry.RegistryKey; +import io.papermc.paper.util.capture.MinecraftCaptureBridge; +import io.papermc.paper.util.capture.SimpleBlockCapture; import it.unimi.dsi.fastutil.objects.Object2IntOpenHashMap; import java.nio.file.Path; import java.util.ArrayList; @@ -104,7 +106,6 @@ import org.bukkit.boss.DragonBattle; import org.bukkit.craftbukkit.block.CraftBiome; import org.bukkit.craftbukkit.block.CraftBlock; -import org.bukkit.craftbukkit.block.CraftBlockState; import org.bukkit.craftbukkit.block.CraftBlockType; import org.bukkit.craftbukkit.block.data.CraftBlockData; import org.bukkit.craftbukkit.boss.CraftDragonBattle; @@ -124,6 +125,7 @@ import org.bukkit.craftbukkit.util.CraftRayTraceResult; import org.bukkit.craftbukkit.util.CraftSpawnCategory; import org.bukkit.craftbukkit.util.CraftStructureSearchResult; +import org.bukkit.craftbukkit.util.RandomSourceWrapper; import org.bukkit.entity.AbstractArrow; import org.bukkit.entity.Arrow; import org.bukkit.entity.Entity; @@ -738,26 +740,20 @@ public boolean generateTree(Location loc, TreeType type) { @Override public boolean generateTree(Location loc, TreeType type, BlockChangeDelegate delegate) { - this.world.captureTreeGeneration = true; - this.world.captureBlockStates = true; - boolean grownTree = this.generateTree(loc, type); - this.world.captureBlockStates = false; - this.world.captureTreeGeneration = false; - if (grownTree) { // Copy block data to delegate - for (BlockState blockstate : this.world.capturedBlockStates.values()) { - BlockPos position = ((CraftBlockState) blockstate).getPosition(); - net.minecraft.world.level.block.state.BlockState oldBlock = this.world.getBlockState(position); - int flags = ((CraftBlockState) blockstate).getFlags(); - delegate.setBlockData(blockstate.getX(), blockstate.getY(), blockstate.getZ(), blockstate.getBlockData()); - net.minecraft.world.level.block.state.BlockState newBlock = this.world.getBlockState(position); - this.world.notifyAndUpdatePhysics(position, null, oldBlock, newBlock, newBlock, flags, net.minecraft.world.level.block.Block.UPDATE_LIMIT); - } - this.world.capturedBlockStates.clear(); - return true; - } else { - this.world.capturedBlockStates.clear(); - return false; - } + try (SimpleBlockCapture capture = this.world.forkCaptureSession()) { + MinecraftCaptureBridge captureTreeGeneration = capture.capturingWorldLevel(); + + BlockPos pos = CraftLocation.toBlockPosition(loc); + boolean res = this.generateTree(captureTreeGeneration, this.getHandle().getMinecraftWorld().getChunkSource().getGenerator(), pos, new RandomSourceWrapper(CraftWorld.rand), type); + if (res) { + List blocks = captureTreeGeneration.calculateLatestSnapshots(this.world); + for (BlockState state : blocks) { + delegate.setBlockData(state.getX(), state.getY(), state.getZ(), state.getBlockData()); + } + } + + return res; + } } @Override diff --git a/paper-server/src/main/java/org/bukkit/craftbukkit/block/CapturedBlockState.java b/paper-server/src/main/java/org/bukkit/craftbukkit/block/CapturedBlockState.java deleted file mode 100644 index 6b5853ced761..000000000000 --- a/paper-server/src/main/java/org/bukkit/craftbukkit/block/CapturedBlockState.java +++ /dev/null @@ -1,80 +0,0 @@ -package org.bukkit.craftbukkit.block; - -import net.minecraft.core.BlockPos; -import net.minecraft.util.RandomSource; -import net.minecraft.world.level.Level; -import net.minecraft.world.level.WorldGenLevel; -import net.minecraft.world.level.block.entity.BeehiveBlockEntity; -import net.minecraft.world.level.block.entity.BlockEntityType; -import org.bukkit.Location; -import org.bukkit.Material; -import org.bukkit.block.Block; - -@Deprecated(forRemoval = true) -public final class CapturedBlockState extends CraftBlockState { - - private final boolean treeBlock; - - public CapturedBlockState(Block block, @net.minecraft.world.level.block.Block.UpdateFlags int capturedFlags, boolean treeBlock) { - super(block, capturedFlags); - - this.treeBlock = treeBlock; - } - - private CapturedBlockState(CapturedBlockState state, Location location) { - super(state, location); - this.treeBlock = state.treeBlock; - } - - @Override - public boolean update(boolean force, boolean applyPhysics) { - boolean result = super.update(force, applyPhysics); - - if (result) { - this.addBees(); - } - - return result; - } - - @Override - public boolean place(@net.minecraft.world.level.block.Block.UpdateFlags int flags) { - boolean result = super.place(flags); - this.addBees(); - - return result; - } - - private void addBees() { - // SPIGOT-5537: Horrible hack to manually add bees given Level#captureTreeGeneration does not support block entities - if (this.treeBlock && this.getType() == Material.BEE_NEST) { - WorldGenLevel worldGenLevel = this.world.getHandle(); - BlockPos pos = this.getPosition(); - RandomSource randomSource = worldGenLevel.getRandom(); - - // Begin copied block from BeehiveDecorator - worldGenLevel.getBlockEntity(pos, BlockEntityType.BEEHIVE).ifPresent(beehiveBlockEntity -> { - int i1 = 2 + randomSource.nextInt(2); - - for (int i2 = 0; i2 < i1; i2++) { - beehiveBlockEntity.storeBee(BeehiveBlockEntity.Occupant.create(randomSource.nextInt(599))); - } - }); - // End copied block - } - } - - @Override - public CapturedBlockState copy() { - return new CapturedBlockState(this, null); - } - - @Override - public CapturedBlockState copy(Location location) { - return new CapturedBlockState(this, location); - } - - public static CapturedBlockState getTreeBlockState(Level world, BlockPos pos, @net.minecraft.world.level.block.Block.UpdateFlags int flags) { - return new CapturedBlockState(CraftBlock.at(world, pos), flags, true); - } -} diff --git a/paper-server/src/main/java/org/bukkit/craftbukkit/block/CraftBlock.java b/paper-server/src/main/java/org/bukkit/craftbukkit/block/CraftBlock.java index 31e665388bc6..cd24e43b5d2f 100644 --- a/paper-server/src/main/java/org/bukkit/craftbukkit/block/CraftBlock.java +++ b/paper-server/src/main/java/org/bukkit/craftbukkit/block/CraftBlock.java @@ -549,41 +549,10 @@ public boolean breakNaturally(ItemStack item, boolean triggerEffect, boolean dro @Override public boolean applyBoneMeal(BlockFace face) { Direction direction = CraftBlock.blockFaceToNotch(face); - BlockFertilizeEvent event = null; ServerLevel world = this.getCraftWorld().getHandle(); UseOnContext context = new UseOnContext(world, null, InteractionHand.MAIN_HAND, Items.BONE_MEAL.getDefaultInstance(), new BlockHitResult(Vec3.ZERO, direction, this.getPosition(), false)); - // SPIGOT-6895: Call StructureGrowEvent and BlockFertilizeEvent - world.captureTreeGeneration = true; - InteractionResult result = BoneMealItem.applyBonemeal(context); - world.captureTreeGeneration = false; - - if (!world.capturedBlockStates.isEmpty()) { - TreeType treeType = SaplingBlock.treeType; - SaplingBlock.treeType = null; - List states = new ArrayList<>(world.capturedBlockStates.values()); - world.capturedBlockStates.clear(); - StructureGrowEvent structureEvent = null; - - if (treeType != null) { - structureEvent = new StructureGrowEvent(this.getLocation(), treeType, true, null, states); - Bukkit.getPluginManager().callEvent(structureEvent); - } - - event = new BlockFertilizeEvent(CraftBlock.at(world, this.getPosition()), null, states); - event.setCancelled(structureEvent != null && structureEvent.isCancelled()); - Bukkit.getPluginManager().callEvent(event); - - if (!event.isCancelled()) { - for (BlockState state : states) { - CraftBlockState craftBlockState = (CraftBlockState) state; - craftBlockState.place(craftBlockState.getFlags()); - world.checkCapturedTreeStateForObserverNotify(this.position, craftBlockState); // Paper - notify observers even if grow failed - } - } - } - - return result == InteractionResult.SUCCESS && (event == null || !event.isCancelled()); + return BoneMealItem.applyBonemeal(context) == InteractionResult.SUCCESS; } @Override diff --git a/paper-server/src/main/java/org/bukkit/craftbukkit/block/CraftBlockState.java b/paper-server/src/main/java/org/bukkit/craftbukkit/block/CraftBlockState.java index 3036f3fa8b58..64ccc620f266 100644 --- a/paper-server/src/main/java/org/bukkit/craftbukkit/block/CraftBlockState.java +++ b/paper-server/src/main/java/org/bukkit/craftbukkit/block/CraftBlockState.java @@ -28,21 +28,16 @@ public class CraftBlockState implements BlockState { protected final CraftWorld world; private final BlockPos position; protected net.minecraft.world.level.block.state.BlockState data; - @net.minecraft.world.level.block.Block.UpdateFlags - protected int capturedFlags; // todo move out of this class private WeakReference weakWorld; protected CraftBlockState(final Block block) { this(block.getWorld(), ((CraftBlock) block).getPosition(), ((CraftBlock) block).getNMS()); - this.capturedFlags = net.minecraft.world.level.block.Block.UPDATE_ALL; - this.setWorldHandle(((CraftBlock) block).getHandle()); } @Deprecated protected CraftBlockState(final Block block, @net.minecraft.world.level.block.Block.UpdateFlags int capturedFlags) { this(block); - this.capturedFlags = capturedFlags; } // world can be null for non-placed BlockStates. @@ -62,7 +57,6 @@ protected CraftBlockState(CraftBlockState state, @Nullable Location location) { this.position = CraftLocation.toBlockPosition(location); } this.data = state.data; - this.capturedFlags = state.capturedFlags; this.setWorldHandle(state.getWorldHandle()); } @@ -182,14 +176,6 @@ public Material getType() { return this.data.getBukkitMaterial(); } - public void setFlags(@net.minecraft.world.level.block.Block.UpdateFlags int flags) { - this.capturedFlags = flags; - } - - public @net.minecraft.world.level.block.Block.UpdateFlags int getFlags() { - return this.capturedFlags; - } - @Override public byte getLightLevel() { return this.getBlock().getLightLevel(); @@ -253,17 +239,6 @@ public boolean place(@net.minecraft.world.level.block.Block.UpdateFlags int flag return this.getWorldHandle().setBlock(this.position, this.data, flags); } - // used to revert a block placement due to an event being cancelled for example - public boolean revertPlace() { - return this.place( - net.minecraft.world.level.block.Block.UPDATE_CLIENTS | - net.minecraft.world.level.block.Block.UPDATE_KNOWN_SHAPE | - net.minecraft.world.level.block.Block.UPDATE_SUPPRESS_DROPS | - net.minecraft.world.level.block.Block.UPDATE_SKIP_ON_PLACE | - net.minecraft.world.level.block.Block.UPDATE_SKIP_BLOCK_ENTITY_SIDEEFFECTS - ); - } - @Override public byte getRawData() { return CraftMagicNumbers.toLegacyData(this.data); 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 cd83ca2ace1d..512c0cbe2606 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 @@ -6,15 +6,6 @@ import com.google.common.collect.Lists; import com.mojang.authlib.GameProfile; import com.mojang.datafixers.util.Either; -import java.util.ArrayList; -import java.util.Collections; -import java.util.EnumMap; -import java.util.List; -import java.util.Map; -import java.util.Optional; -import java.util.Set; -import java.util.stream.Collectors; -import java.util.stream.Stream; import io.papermc.paper.adventure.PaperAdventure; import io.papermc.paper.block.bed.BedEnterProblem; import io.papermc.paper.connection.HorriblePlayerLoginEventHack; @@ -23,6 +14,19 @@ import io.papermc.paper.event.connection.PlayerConnectionValidateLoginEvent; import io.papermc.paper.event.entity.ItemTransportingEntityValidateTargetEvent; import io.papermc.paper.event.player.PlayerBedFailEnterEvent; +import io.papermc.paper.util.capture.GrowthContext; +import io.papermc.paper.util.capture.MinecraftCaptureBridge; +import io.papermc.paper.util.capture.PaperCapturingWorldLevel; +import io.papermc.paper.util.capture.SimpleBlockCapture; +import java.util.ArrayList; +import java.util.Collections; +import java.util.EnumMap; +import java.util.List; +import java.util.Map; +import java.util.Set; +import java.util.function.Consumer; +import java.util.stream.Collectors; +import java.util.stream.Stream; import net.minecraft.core.BlockPos; import net.minecraft.core.Direction; import net.minecraft.network.Connection; @@ -45,14 +49,9 @@ import net.minecraft.world.entity.Leashable; import net.minecraft.world.entity.Mob; import net.minecraft.world.entity.PathfinderMob; -import net.minecraft.world.entity.animal.fish.AbstractFish; -import net.minecraft.world.entity.animal.golem.AbstractGolem; import net.minecraft.world.entity.animal.Animal; -import net.minecraft.world.entity.animal.fish.WaterAnimal; +import net.minecraft.world.entity.animal.fish.AbstractFish; import net.minecraft.world.entity.item.ItemEntity; -import net.minecraft.world.entity.monster.Ghast; -import net.minecraft.world.entity.monster.Monster; -import net.minecraft.world.entity.monster.Slime; import net.minecraft.world.entity.monster.illager.SpellcasterIllager; import net.minecraft.world.entity.projectile.FireworkRocketEntity; import net.minecraft.world.entity.raid.Raid; @@ -113,6 +112,7 @@ import org.bukkit.craftbukkit.inventory.CraftItemStack; import org.bukkit.craftbukkit.inventory.CraftItemType; import org.bukkit.craftbukkit.potion.CraftPotionUtil; +import org.bukkit.craftbukkit.util.CraftLocation; import org.bukkit.craftbukkit.util.CraftNamespacedKey; import org.bukkit.craftbukkit.util.CraftVector; import org.bukkit.entity.AbstractHorse; @@ -153,6 +153,7 @@ import org.bukkit.event.block.BlockDropItemEvent; import org.bukkit.event.block.BlockExplodeEvent; import org.bukkit.event.block.BlockFadeEvent; +import org.bukkit.event.block.BlockFertilizeEvent; import org.bukkit.event.block.BlockFormEvent; import org.bukkit.event.block.BlockGrowEvent; import org.bukkit.event.block.BlockIgniteEvent; @@ -268,6 +269,7 @@ import org.bukkit.event.world.EntitiesLoadEvent; import org.bukkit.event.world.EntitiesUnloadEvent; import org.bukkit.event.world.LootGenerateEvent; +import org.bukkit.event.world.StructureGrowEvent; import org.bukkit.inventory.CraftingRecipe; import org.bukkit.inventory.EquipmentSlot; import org.bukkit.inventory.InventoryView; @@ -494,20 +496,20 @@ public static Stream handleBellResonate return event.getResonatedEntities().stream().map((bukkitEntity) -> ((CraftLivingEntity) bukkitEntity).getHandle()); } - public static BlockMultiPlaceEvent callBlockMultiPlaceEvent(ServerLevel level, net.minecraft.world.entity.player.Player player, InteractionHand hand, List blockStates, BlockPos clickedPos) { + public static BlockMultiPlaceEvent callBlockMultiPlaceEvent(ServerLevel level, net.minecraft.world.entity.player.Player player, InteractionHand hand, List replacedSnapshots, BlockPos clickedPos) { Player cplayer = (Player) player.getBukkitEntity(); Block clickedBlock = CraftBlock.at(level, clickedPos); boolean canBuild = true; - for (BlockState blockState : blockStates) { - if (!CraftEventFactory.canBuild(level, cplayer, blockState.getX(), blockState.getZ())) { + for (BlockState snapshot : replacedSnapshots) { + if (!CraftEventFactory.canBuild(level, cplayer, snapshot.getX(), snapshot.getZ())) { canBuild = false; break; } } EquipmentSlot handSlot = CraftEquipmentSlot.getHand(hand); - BlockMultiPlaceEvent event = new BlockMultiPlaceEvent(blockStates, clickedBlock, cplayer.getInventory().getItem(handSlot), cplayer, canBuild, handSlot); + BlockMultiPlaceEvent event = new BlockMultiPlaceEvent(replacedSnapshots, clickedBlock, cplayer.getInventory().getItem(handSlot), cplayer, canBuild, handSlot); event.callEvent(); return event; @@ -1291,8 +1293,8 @@ public static PlayerExpChangeEvent callPlayerExpChangeEvent(net.minecraft.world. return event; } - public static boolean handleBlockGrowEvent(Level world, BlockPos pos, net.minecraft.world.level.block.state.BlockState state, @net.minecraft.world.level.block.Block.UpdateFlags int flags) { - CraftBlockState snapshot = CraftBlockStates.getBlockState(world, pos); + public static boolean handleBlockGrowEvent(LevelAccessor level, BlockPos pos, net.minecraft.world.level.block.state.BlockState state, @net.minecraft.world.level.block.Block.UpdateFlags int flags) { + CraftBlockState snapshot = CraftBlockStates.getBlockState(level, pos); snapshot.setData(state); BlockGrowEvent event = new BlockGrowEvent(snapshot.getBlock(), snapshot); @@ -2383,4 +2385,52 @@ public static boolean sendChestLockedNotifications(Vec3 pos) { } return false; } + + // todo block list is sometimes empty for trees in both events + public static boolean structureEvent(PaperCapturingWorldLevel level, BlockPos pos, Function worldGenCapture, GrowthContext context) { + ServerLevel originalLevel = level.getLevel(); + try (SimpleBlockCapture capture = level.forkCaptureSession()) { + MinecraftCaptureBridge captureTreeGeneration = capture.capturingWorldLevel(); + if (worldGenCapture.apply(captureTreeGeneration)) { + Location location = CraftLocation.toBukkit(pos, originalLevel); + List blocks = captureTreeGeneration.calculateLatestSnapshots(originalLevel); + StructureGrowEvent event = new StructureGrowEvent(location, context.getTreeSpecies(), context.usedBoneMeal(), context.getBukkitPlayer(), blocks); + event.setCancelled(context.cancelled); + + if (event.callEvent()) { + capture.finalizePlacement(); // todo block list is mutable + return true; + } else { + context.cancelled = true; + } + } else { + capture.finalizePlacement(); + } + } + + return false; + } + + // todo cancelling fertilize event doesn't work for azalea + public static boolean fertilizeBlock(ServerLevel level, BlockPos pos, Function worldGenCapture, GrowthContext context) { + try (SimpleBlockCapture capture = level.forkCaptureSession()) { + MinecraftCaptureBridge captureTreeGeneration = capture.capturingWorldLevel(); + if (worldGenCapture.apply(captureTreeGeneration)) { + List blocks = captureTreeGeneration.calculateLatestSnapshots(level); + BlockFertilizeEvent event = new BlockFertilizeEvent(CraftBlock.at(level, pos), context.getBukkitPlayer(), blocks); + event.setCancelled(context.cancelled); + + if (event.callEvent()) { + capture.finalizePlacement(); // todo block list is mutable + return true; + } else { + context.cancelled = true; + } + } else { + capture.finalizePlacement(); + } + } + + return false; + } } diff --git a/paper-server/src/main/java/org/bukkit/craftbukkit/util/BlockStateListPopulator.java b/paper-server/src/main/java/org/bukkit/craftbukkit/util/BlockStateListPopulator.java index 989b2f751a59..08fae23da42a 100644 --- a/paper-server/src/main/java/org/bukkit/craftbukkit/util/BlockStateListPopulator.java +++ b/paper-server/src/main/java/org/bukkit/craftbukkit/util/BlockStateListPopulator.java @@ -15,10 +15,10 @@ import net.minecraft.world.level.block.Block; import net.minecraft.world.level.block.EntityBlock; import net.minecraft.world.level.block.entity.BlockEntity; +import net.minecraft.world.level.block.state.BlockState; import net.minecraft.world.level.dimension.DimensionType; import net.minecraft.world.level.material.FluidState; import net.minecraft.world.level.storage.LevelData; -import org.bukkit.block.BlockState; import org.bukkit.craftbukkit.block.CraftBlockState; import org.bukkit.craftbukkit.block.CraftBlockStates; @@ -34,7 +34,7 @@ public BlockStateListPopulator(LevelAccessor level) { } @Override - public net.minecraft.world.level.block.state.BlockState getBlockState(BlockPos pos) { + public BlockState getBlockState(BlockPos pos) { CapturedBlock block = this.blocks.get(pos); return block != null ? block.state() : this.level.getBlockState(pos); } @@ -52,7 +52,7 @@ public BlockEntity getBlockEntity(BlockPos pos) { } @Override - public boolean setBlock(BlockPos pos, net.minecraft.world.level.block.state.BlockState state, @Block.UpdateFlags int flags, int recursionLeft) { + public boolean setBlock(BlockPos pos, BlockState state, @Block.UpdateFlags int flags, int recursionLeft) { pos = pos.immutable(); // remove first to keep last updated order this.blocks.remove(pos); @@ -77,7 +77,7 @@ public boolean setBlock(BlockPos pos, net.minecraft.world.level.block.state.Bloc @Override public boolean destroyBlock(BlockPos pos, boolean dropBlock, Entity entity, int recursionLeft) { - net.minecraft.world.level.block.state.BlockState blockState = this.getBlockState(pos); + BlockState blockState = this.getBlockState(pos); if (blockState.isAir()) { return false; } @@ -97,7 +97,6 @@ private void iterateSnapshots(Consumer callback) { CraftBlockState snapshot = CraftBlockStates.getBlockState( this.getMinecraftWorld().getWorld(), entry.getKey(), block.state(), block.blockEntity() ); - snapshot.setFlags(block.flags()); snapshot.setWorldHandle(this.level); callback.accept(snapshot); } @@ -107,7 +106,7 @@ public void placeBlocks() { this.placeSomeBlocks($ -> true); } - public void placeSomeBlocks(Predicate filter) { + public void placeSomeBlocks(Predicate filter) { this.placeSomeBlocks($ -> {}, filter); } @@ -115,22 +114,28 @@ public void placeBlocks(Consumer beforeRun) { this.placeSomeBlocks(beforeRun, $ -> true); } - public void placeSomeBlocks(Consumer beforeRun, Predicate filter) { + public void placeSomeBlocks(Consumer beforeRun, Predicate filter) { for (CraftBlockState snapshot : this.getSnapshotBlocks()) { if (filter.test(snapshot)) { + int flags = this.getEffectiveFlags(snapshot.getPosition()); beforeRun.accept(snapshot); - snapshot.place(snapshot.getFlags()); + snapshot.place(flags); } } } + public @Block.UpdateFlags int getEffectiveFlags(BlockPos pos) { + CapturedBlock block = this.blocks.get(pos); + return block != null ? block.flags() : Block.UPDATE_ALL; // fallback for new API-added blocks + } + public List getSnapshotBlocks() { if (this.snapshots == null) { List snapshots = new ArrayList<>(); this.iterateSnapshots(snapshots::add); this.snapshots = snapshots; } - return snapshots; + return this.snapshots; } // For tree generation @@ -151,7 +156,7 @@ public int getHeight() { } @Override - public boolean isStateAtPosition(BlockPos pos, Predicate state) { + public boolean isStateAtPosition(BlockPos pos, Predicate state) { return state.test(this.getBlockState(pos)); } diff --git a/test-plugin/src/main/java/io/papermc/testplugin/OwenPlayground.java b/test-plugin/src/main/java/io/papermc/testplugin/OwenPlayground.java new file mode 100644 index 000000000000..371cf2cbf5ff --- /dev/null +++ b/test-plugin/src/main/java/io/papermc/testplugin/OwenPlayground.java @@ -0,0 +1,86 @@ +package io.papermc.testplugin; + +import org.bukkit.Material; +import org.bukkit.TreeType; +import org.bukkit.block.BlockFace; +import org.bukkit.block.BlockState; +import org.bukkit.event.EventHandler; +import org.bukkit.event.Listener; +import org.bukkit.event.block.BlockFertilizeEvent; +import org.bukkit.event.block.BlockMultiPlaceEvent; +import org.bukkit.event.block.BlockPlaceEvent; +import org.bukkit.event.player.PlayerSwapHandItemsEvent; +import org.bukkit.event.world.StructureGrowEvent; + +import static net.kyori.adventure.text.Component.text; + +public record OwenPlayground() implements Listener { + + public static final OwenPlayground INSTANCE = new OwenPlayground(); + + @EventHandler + public void on(BlockFertilizeEvent event) { + //event.setCancelled(true); + +// event.getPlayer().sendBlockChanges(event.getBlocks()); +// +// new BukkitRunnable(){ +// +// @Override +// public void run() { +// event.getBlocks().forEach((state) -> event.getPlayer().sendBlockChange(state.getLocation(), state.getBlock().getType().createBlockData())); +// } +// }.runTaskLater(this, 20 * 2); + } + + + @EventHandler + public void on(StructureGrowEvent event) { +// event.setCancelled(true); +// +// event.getPlayer().sendBlockChanges(event.getBlocks()); +// +// new BukkitRunnable(){ +// +// @Override +// public void run() { +// event.getBlocks().forEach((state) -> event.getPlayer().sendBlockChange(state.getLocation(), state.getBlock().getType().createBlockData())); +// } +// }.runTaskLater(this, 20 * 2); + } + + + @EventHandler + public void on(PlayerSwapHandItemsEvent event) { + event.getPlayer().getTargetBlockExact(5).applyBoneMeal(BlockFace.UP); + + event.getPlayer().getWorld().generateTree(event.getPlayer().getLocation(), TreeType.values()[(int) (TreeType.values().length * Math.random())]); + } + + @EventHandler + public void on(BlockPlaceEvent event) { + event.getPlayer().sendActionBar(text("Replaced: " + event.getBlockReplacedState().getType())); + if (event.getPlayer().getInventory().contains(Material.DIAMOND)) { + event.getBlock().setType(Material.AIR); + } else if (event.getPlayer().getInventory().contains(Material.GOLD_INGOT)) { + event.setCancelled(true); + } else if (event.getPlayer().getInventory().contains(Material.EMERALD)) { + event.getBlock().setType(Material.STONE); + } else if (event.getPlayer().getInventory().contains(Material.GOLD_NUGGET)) { + event.getBlock().getRelative(BlockFace.SOUTH).setType(Material.STONE); + } + event.getPlayer().sendMessage(event.getBlock().getType().toString()); + + event.getBlockAgainst().setType(Material.DIAMOND_BLOCK); + } + + @EventHandler + public void on(BlockMultiPlaceEvent event) { + event.getPlayer().sendActionBar(text("Replaced: " + event.getReplacedBlockStates().stream().map(BlockState::getBlockData).toList())); + + event.getPlayer().sendMessage(event.getBlock().getType().toString()); + if (event.getPlayer().getInventory().contains(Material.DIAMOND)) { + event.setCancelled(true); + } + } +} diff --git a/test-plugin/src/main/java/io/papermc/testplugin/TestPlugin.java b/test-plugin/src/main/java/io/papermc/testplugin/TestPlugin.java index fd891f5b1fad..952349905061 100644 --- a/test-plugin/src/main/java/io/papermc/testplugin/TestPlugin.java +++ b/test-plugin/src/main/java/io/papermc/testplugin/TestPlugin.java @@ -8,6 +8,7 @@ public final class TestPlugin extends JavaPlugin implements Listener { @Override public void onEnable() { this.getServer().getPluginManager().registerEvents(this, this); + this.getServer().getPluginManager().registerEvents(OwenPlayground.INSTANCE, this); // io.papermc.testplugin.brigtests.Registration.registerViaOnEnable(this); }