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);