diff --git a/paper-api/src/main/java/io/papermc/paper/event/entity/EntityConstructEvent.java b/paper-api/src/main/java/io/papermc/paper/event/entity/EntityConstructEvent.java new file mode 100644 index 000000000000..cab7cc9ddca1 --- /dev/null +++ b/paper-api/src/main/java/io/papermc/paper/event/entity/EntityConstructEvent.java @@ -0,0 +1,63 @@ +package io.papermc.paper.event.entity; + +import org.bukkit.block.Block; +import org.bukkit.entity.Entity; +import org.bukkit.entity.Golem; +import org.bukkit.event.Cancellable; +import org.bukkit.event.HandlerList; +import org.bukkit.event.entity.EntityEvent; +import org.bukkit.event.entity.EntitySpawnEvent; +import org.jetbrains.annotations.ApiStatus; +import org.jetbrains.annotations.Unmodifiable; +import org.jspecify.annotations.NullMarked; +import java.util.List; + +/** + * Called just before an {@link Entity} spawns due to a pattern of blocks being constructed (golems, the wither, etc.) + *
+ * Note: This event is fired before {@link EntitySpawnEvent}, before the entity is added to the world, + * the success of this event does not guarantee the entity will actually spawn. + */ +@NullMarked +public class EntityConstructEvent extends EntityEvent implements Cancellable { + + private static final HandlerList HANDLER_LIST = new HandlerList(); + + private final List blocks; + private boolean cancelled; + + @ApiStatus.Internal + public EntityConstructEvent(Entity entity, List blocks) { + super(entity); + this.blocks = List.copyOf(blocks); + } + + /** + * Get an immutable list of the blocks required for this construction, including + * any required air blocks. + * + * @return the blocks + */ + public @Unmodifiable List getBlocks() { + return blocks; + } + + @Override + public void setCancelled(final boolean cancel) { + this.cancelled = cancel; + } + + @Override + public boolean isCancelled() { + return cancelled; + } + + @Override + public HandlerList getHandlers() { + return HANDLER_LIST; + } + + public static HandlerList getHandlerList() { + return HANDLER_LIST; + } +} diff --git a/paper-server/patches/sources/net/minecraft/world/level/block/CarvedPumpkinBlock.java.patch b/paper-server/patches/sources/net/minecraft/world/level/block/CarvedPumpkinBlock.java.patch index 53f2111574c8..ba8fff223a58 100644 --- a/paper-server/patches/sources/net/minecraft/world/level/block/CarvedPumpkinBlock.java.patch +++ b/paper-server/patches/sources/net/minecraft/world/level/block/CarvedPumpkinBlock.java.patch @@ -8,7 +8,7 @@ this.replaceCopperBlockWithChest(level, copperGolemMatch); copperGolem.spawn(this.getWeatherStateFromPattern(copperGolemMatch)); } -@@ -105,9 +_,22 @@ +@@ -105,9 +_,25 @@ } private static void spawnGolemInWorld(final Level level, final BlockPattern.BlockPatternMatch match, final Entity golem, final BlockPos spawnPos) { @@ -17,6 +17,9 @@ golem.snapTo(spawnPos.getX() + 0.5, spawnPos.getY() + 0.05, spawnPos.getZ() + 0.5, 0.0F, 0.0F); - level.addFreshEntity(golem); + // Paper start ++ if (!new io.papermc.paper.event.entity.EntityConstructEvent(golem.getBukkitEntity(), org.bukkit.craftbukkit.block.CraftBlock.getMatchingBlocks(level, match)).callEvent()) { ++ return; ++ } + org.bukkit.event.entity.CreatureSpawnEvent.SpawnReason spawnReason; + if (golem.getType() == net.minecraft.world.entity.EntityType.SNOW_GOLEM) { + spawnReason = org.bukkit.event.entity.CreatureSpawnEvent.SpawnReason.BUILD_SNOWMAN; diff --git a/paper-server/patches/sources/net/minecraft/world/level/block/WitherSkullBlock.java.patch b/paper-server/patches/sources/net/minecraft/world/level/block/WitherSkullBlock.java.patch index 5aa9ac611b52..c36b192c8cbc 100644 --- a/paper-server/patches/sources/net/minecraft/world/level/block/WitherSkullBlock.java.patch +++ b/paper-server/patches/sources/net/minecraft/world/level/block/WitherSkullBlock.java.patch @@ -17,10 +17,15 @@ BlockPos spawnPos = match.getBlock(1, 2, 0).getPos(); witherBoss.snapTo( spawnPos.getX() + 0.5, -@@ -68,12 +_,18 @@ +@@ -68,12 +_,23 @@ ); witherBoss.yBodyRot = match.getForwards().getAxis() == Direction.Axis.X ? 0.0F : 90.0F; witherBoss.makeInvulnerable(); ++ // Paper start ++ if (!new io.papermc.paper.event.entity.EntityConstructEvent(witherBoss.getBukkitEntity(), org.bukkit.craftbukkit.block.CraftBlock.getMatchingBlocks(level, match)).callEvent()) { ++ return; ++ } ++ // Paper end + // CraftBukkit start + if (!level.addFreshEntity(witherBoss, org.bukkit.event.entity.CreatureSpawnEvent.SpawnReason.BUILD_WITHER)) { + return; 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 5d6116c55072..bf8654445b81 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 @@ -23,6 +23,8 @@ import net.minecraft.world.level.block.RedStoneWireBlock; import net.minecraft.world.level.block.SaplingBlock; import net.minecraft.world.level.block.state.BlockState; +import net.minecraft.world.level.block.state.pattern.BlockInWorld; +import net.minecraft.world.level.block.state.pattern.BlockPattern; import net.minecraft.world.level.redstone.Redstone; import net.minecraft.world.phys.AABB; import net.minecraft.world.phys.BlockHitResult; @@ -297,6 +299,17 @@ public static BlockFace notchToBlockFace(@Nullable Direction direction) { }; } + public static List getMatchingBlocks(LevelAccessor level, BlockPattern.BlockPatternMatch match) { + List blocks = new ArrayList<>(); + for (int x = 0; x < match.getWidth(); x++) { + for (int y = 0; y < match.getHeight(); y++) { + BlockInWorld block = match.getBlock(x, y, 0); + blocks.add(at(level, block.getPos())); + } + } + return blocks; + } + @Override public org.bukkit.block.BlockState getState() { return CraftBlockStates.getBlockState(this);