diff --git a/paper-api/src/main/java/io/papermc/paper/event/entity/BlockPlaceEntityEvent.java b/paper-api/src/main/java/io/papermc/paper/event/entity/BlockPlaceEntityEvent.java
new file mode 100644
index 000000000000..1a658afd2f12
--- /dev/null
+++ b/paper-api/src/main/java/io/papermc/paper/event/entity/BlockPlaceEntityEvent.java
@@ -0,0 +1,50 @@
+package io.papermc.paper.event.entity;
+
+import org.bukkit.block.Block;
+import org.bukkit.block.BlockFace;
+import org.bukkit.block.Dispenser;
+import org.bukkit.entity.Entity;
+import org.bukkit.entity.Player;
+import org.bukkit.inventory.ItemStack;
+import org.jetbrains.annotations.ApiStatus;
+import org.jetbrains.annotations.Contract;
+import org.jspecify.annotations.NullMarked;
+import org.jspecify.annotations.Nullable;
+
+/**
+ * Called when a block, like a dispenser, places an
+ * entity. {@link #getPlayer()} will always be {@code null}.
+ *
+ * @see org.bukkit.event.hanging.HangingPlaceEvent for paintings, item frames, and leashes.
+ * @see org.bukkit.event.entity.EntityPlaceEvent for a player-only version with more context
+ * @see PlaceEntityEvent to listen to both blocks and players placing entities
+ */
+@NullMarked
+public class BlockPlaceEntityEvent extends PlaceEntityEvent {
+
+ private final Dispenser dispenser;
+
+ @ApiStatus.Internal
+ public BlockPlaceEntityEvent(final Entity entity, final Block block, final BlockFace blockFace, final ItemStack spawningStack, final Dispenser dispenser) {
+ super(entity, null, block, blockFace, spawningStack);
+ this.dispenser = dispenser;
+ }
+
+ /**
+ * Get the dispenser responsible for placing the entity.
+ *
+ * @return a non-snapshot Dispenser
+ */
+ public Dispenser getDispenser() {
+ return this.dispenser;
+ }
+
+ /**
+ * Player will always be null on this event.
+ */
+ @Override
+ @Contract("-> null")
+ public @Nullable Player getPlayer() {
+ return null;
+ }
+}
diff --git a/paper-api/src/main/java/io/papermc/paper/event/entity/ItemSpawnEntityEvent.java b/paper-api/src/main/java/io/papermc/paper/event/entity/ItemSpawnEntityEvent.java
new file mode 100644
index 000000000000..81b688bb6bac
--- /dev/null
+++ b/paper-api/src/main/java/io/papermc/paper/event/entity/ItemSpawnEntityEvent.java
@@ -0,0 +1,97 @@
+package io.papermc.paper.event.entity;
+
+import org.bukkit.block.Block;
+import org.bukkit.block.BlockFace;
+import org.bukkit.entity.Entity;
+import org.bukkit.entity.Player;
+import org.bukkit.event.Cancellable;
+import org.bukkit.event.HandlerList;
+import org.bukkit.event.entity.EntityEvent;
+import org.bukkit.inventory.ItemStack;
+import org.jetbrains.annotations.ApiStatus;
+import org.jspecify.annotations.NullMarked;
+import org.jspecify.annotations.Nullable;
+
+/**
+ * When an item causes the spawning of an entity. Most event fires are going to
+ * be the through the sub-event {@link org.bukkit.event.entity.EntityPlaceEvent} but this
+ * event will also be fired for mob spawn eggs from players and dispensers.
+ */
+@NullMarked
+public class ItemSpawnEntityEvent extends EntityEvent implements Cancellable {
+
+ private static final HandlerList HANDLER_LIST = new HandlerList();
+
+ private final @Nullable Player player;
+ private final Block block;
+ private final BlockFace blockFace;
+ private final ItemStack spawningStack;
+ private boolean cancelled;
+
+ @ApiStatus.Internal
+ public ItemSpawnEntityEvent(final Entity entity, final @Nullable Player player, final Block block, final BlockFace blockFace, final ItemStack spawningStack) {
+ super(entity);
+ this.player = player;
+ this.block = block;
+ this.blockFace = blockFace;
+ this.spawningStack = spawningStack;
+ }
+
+ /**
+ * Returns the player placing the entity (if one is available).
+ *
+ * @return the player placing the entity
+ */
+ public @Nullable Player getPlayer() {
+ return this.player;
+ }
+
+ /**
+ * Returns the block that the entity was placed on
+ *
+ * @return the block that the entity was placed on
+ */
+ public Block getBlock() {
+ return this.block;
+ }
+
+ /**
+ * Returns the face of the block that the entity was placed on
+ *
+ * @return the face of the block that the entity was placed on
+ */
+ public BlockFace getBlockFace() {
+ return this.blockFace;
+ }
+
+ /**
+ * Gets the item responsible for spawning the entity. Mutating
+ * this item has no effect.
+ *
+ * May return an empty item if the actual stack isn't available.
+ *
+ * @return the spawning item
+ */
+ public ItemStack getSpawningStack() {
+ return this.spawningStack;
+ }
+
+ @Override
+ public boolean isCancelled() {
+ return this.cancelled;
+ }
+
+ @Override
+ public void setCancelled(final boolean cancel) {
+ this.cancelled = cancel;
+ }
+
+ @Override
+ public HandlerList getHandlers() {
+ return HANDLER_LIST;
+ }
+
+ public static HandlerList getHandlerList() {
+ return HANDLER_LIST;
+ }
+}
diff --git a/paper-api/src/main/java/io/papermc/paper/event/entity/PlaceEntityEvent.java b/paper-api/src/main/java/io/papermc/paper/event/entity/PlaceEntityEvent.java
new file mode 100644
index 000000000000..5bd4b85c8b57
--- /dev/null
+++ b/paper-api/src/main/java/io/papermc/paper/event/entity/PlaceEntityEvent.java
@@ -0,0 +1,30 @@
+package io.papermc.paper.event.entity;
+
+import org.bukkit.block.Block;
+import org.bukkit.block.BlockFace;
+import org.bukkit.entity.Entity;
+import org.bukkit.entity.Player;
+import org.bukkit.inventory.ItemStack;
+import org.jetbrains.annotations.ApiStatus;
+import org.jspecify.annotations.NullMarked;
+import org.jspecify.annotations.Nullable;
+
+/**
+ * Triggered when an entity is created in the world by "placing" an item
+ * on a block from a player or dispenser.
+ *
+ * Note that this event is currently only fired for these specific placements:
+ * armor stands, boats, minecarts, end crystals, mob buckets, and tnt (dispenser only).
+ *
+ * @see org.bukkit.event.hanging.HangingPlaceEvent for paintings, item frames, and leashes.
+ * @see org.bukkit.event.entity.EntityPlaceEvent for a player-only version with more context
+ * @see BlockPlaceEntityEvent for a dispenser-only version with more context
+ */
+@NullMarked
+public abstract class PlaceEntityEvent extends ItemSpawnEntityEvent {
+
+ @ApiStatus.Internal
+ protected PlaceEntityEvent(final Entity entity, final @Nullable Player player, final Block block, final BlockFace blockFace, final ItemStack spawningStack) {
+ super(entity, player, block, blockFace, spawningStack);
+ }
+}
diff --git a/paper-api/src/main/java/org/bukkit/event/entity/EntityPlaceEvent.java b/paper-api/src/main/java/org/bukkit/event/entity/EntityPlaceEvent.java
index 236c1fa78c17..9df6cff53909 100644
--- a/paper-api/src/main/java/org/bukkit/event/entity/EntityPlaceEvent.java
+++ b/paper-api/src/main/java/org/bukkit/event/entity/EntityPlaceEvent.java
@@ -1,77 +1,59 @@
package org.bukkit.event.entity;
+import io.papermc.paper.event.entity.PlaceEntityEvent;
import org.bukkit.block.Block;
import org.bukkit.block.BlockFace;
import org.bukkit.entity.Entity;
import org.bukkit.entity.Player;
import org.bukkit.event.Cancellable;
-import org.bukkit.event.HandlerList;
import org.bukkit.inventory.EquipmentSlot;
+import org.bukkit.inventory.ItemStack;
import org.jetbrains.annotations.ApiStatus;
-import org.jetbrains.annotations.NotNull;
-import org.jetbrains.annotations.Nullable;
+import org.jspecify.annotations.NullMarked;
+import org.jspecify.annotations.Nullable;
/**
* Triggered when an entity is created in the world by a player "placing" an item
* on a block.
*
- * Note that this event is currently only fired for four specific placements:
- * armor stands, boats, minecarts, and end crystals.
+ * Note that this event is currently only fired for these specific placements:
+ * armor stands, boats, minecarts, end crystals, and mob buckets.
+ *
+ * @see org.bukkit.event.hanging.HangingPlaceEvent for paintings, item frames, and leashes.
+ * @see io.papermc.paper.event.entity.BlockPlaceEntityEvent for a dispenser-only version
+ * @see io.papermc.paper.event.entity.PlaceEntityEvent to listen to both blocks and players placing entities
*/
-public class EntityPlaceEvent extends EntityEvent implements Cancellable {
+@NullMarked
+public class EntityPlaceEvent extends PlaceEntityEvent {
- private static final HandlerList HANDLER_LIST = new HandlerList();
-
- private final Player player;
- private final Block block;
- private final BlockFace blockFace;
private final EquipmentSlot hand;
- private boolean cancelled;
-
@ApiStatus.Internal
- public EntityPlaceEvent(@NotNull final Entity entity, @Nullable final Player player, @NotNull final Block block, @NotNull final BlockFace blockFace, @NotNull final EquipmentSlot hand) {
- super(entity);
- this.player = player;
- this.block = block;
- this.blockFace = blockFace;
+ public EntityPlaceEvent(
+ final Entity entity,
+ final @Nullable Player player,
+ final Block block,
+ final BlockFace blockFace,
+ final EquipmentSlot hand,
+ final ItemStack spawningStack
+ ) {
+ super(entity, player, block, blockFace, spawningStack);
this.hand = hand;
}
- @ApiStatus.Internal
- @Deprecated(since = "1.19.2", forRemoval = true)
- public EntityPlaceEvent(@NotNull final Entity entity, @Nullable final Player player, @NotNull final Block block, @NotNull final BlockFace blockFace) {
- this(entity, player, block, blockFace, EquipmentSlot.HAND);
- }
-
- /**
- * Returns the player placing the entity
- *
- * @return the player placing the entity
- */
- @Nullable
- public Player getPlayer() {
- return this.player;
+ @Override
+ public @Nullable Player getPlayer() {
+ return super.getPlayer();
}
- /**
- * Returns the block that the entity was placed on
- *
- * @return the block that the entity was placed on
- */
- @NotNull
+ @Override
public Block getBlock() {
- return this.block;
+ return super.getBlock();
}
- /**
- * Returns the face of the block that the entity was placed on
- *
- * @return the face of the block that the entity was placed on
- */
- @NotNull
+ @Override
public BlockFace getBlockFace() {
- return this.blockFace;
+ return super.getBlockFace();
}
/**
@@ -79,29 +61,17 @@ public BlockFace getBlockFace() {
*
* @return the hand
*/
- @NotNull
public EquipmentSlot getHand() {
return this.hand;
}
@Override
public boolean isCancelled() {
- return this.cancelled;
+ return super.isCancelled();
}
@Override
- public void setCancelled(boolean cancel) {
- this.cancelled = cancel;
- }
-
- @NotNull
- @Override
- public HandlerList getHandlers() {
- return HANDLER_LIST;
- }
-
- @NotNull
- public static HandlerList getHandlerList() {
- return HANDLER_LIST;
+ public void setCancelled(final boolean cancel) {
+ super.setCancelled(cancel);
}
}
diff --git a/paper-server/patches/features/0018-Entity-load-save-limit-per-chunk.patch b/paper-server/patches/features/0018-Entity-load-save-limit-per-chunk.patch
index 74c39921a062..cba46e5fe6f7 100644
--- a/paper-server/patches/features/0018-Entity-load-save-limit-per-chunk.patch
+++ b/paper-server/patches/features/0018-Entity-load-save-limit-per-chunk.patch
@@ -33,10 +33,10 @@ index c2363cfa5e93942fe837efd9f39478698f6d1a98..2dfd412344a0e57f25a08d9c65656a13
scopedCollector.forChild(entity.problemPath()), entity.registryAccess()
);
diff --git a/net/minecraft/world/entity/EntityType.java b/net/minecraft/world/entity/EntityType.java
-index 3cf2378a2ccf117fab9fc6fc60fcb0ecdf638d45..abccad13c2bb3a33e98ad8eb6d7f08c0ef021811 100644
+index da522a51978b3c0f67a533ab2085b3f499134dd1..7f7e2f856550737eb9dfbaf1cdb3ee2f655fc674 100644
--- a/net/minecraft/world/entity/EntityType.java
+++ b/net/minecraft/world/entity/EntityType.java
-@@ -1615,7 +1615,18 @@ public class EntityType implements FeatureElement, EntityTypeT
+@@ -1621,7 +1621,18 @@ public class EntityType implements FeatureElement, EntityTypeT
}
public static Stream loadEntitiesRecursive(ValueInput.ValueInputList input, Level level, EntitySpawnReason spawnReason) {
diff --git a/paper-server/patches/sources/net/minecraft/core/dispenser/BoatDispenseItemBehavior.java.patch b/paper-server/patches/sources/net/minecraft/core/dispenser/BoatDispenseItemBehavior.java.patch
index cdf7f08ddbe2..619bc70b9122 100644
--- a/paper-server/patches/sources/net/minecraft/core/dispenser/BoatDispenseItemBehavior.java.patch
+++ b/paper-server/patches/sources/net/minecraft/core/dispenser/BoatDispenseItemBehavior.java.patch
@@ -1,6 +1,6 @@
--- a/net/minecraft/core/dispenser/BoatDispenseItemBehavior.java
+++ b/net/minecraft/core/dispenser/BoatDispenseItemBehavior.java
-@@ -41,13 +_,36 @@
+@@ -41,13 +_,41 @@
d4 = 0.0;
}
@@ -36,6 +36,11 @@
abstractBoat.setYRot(direction.toYRot());
- serverLevel.addFreshEntity(abstractBoat);
- item.shrink(1);
++ // Paper start
++ if (org.bukkit.craftbukkit.event.CraftEventFactory.callBlockPlaceEntityEvent(blockSource, abstractBoat, item).isCancelled()) {
++ return item;
++ }
++ // Paper end
+ if (serverLevel.addFreshEntity(abstractBoat) && shrink) item.shrink(1); // Paper - if entity add was successful and supposed to shrink
}
diff --git a/paper-server/patches/sources/net/minecraft/core/dispenser/DispenseItemBehavior.java.patch b/paper-server/patches/sources/net/minecraft/core/dispenser/DispenseItemBehavior.java.patch
index ba660f85657e..c7e43c40cb42 100644
--- a/paper-server/patches/sources/net/minecraft/core/dispenser/DispenseItemBehavior.java.patch
+++ b/paper-server/patches/sources/net/minecraft/core/dispenser/DispenseItemBehavior.java.patch
@@ -1,6 +1,6 @@
--- a/net/minecraft/core/dispenser/DispenseItemBehavior.java
+++ b/net/minecraft/core/dispenser/DispenseItemBehavior.java
-@@ -88,10 +_,38 @@
+@@ -88,22 +_,59 @@
if (type == null) {
return item;
} else {
@@ -40,7 +40,19 @@
null,
blockSource.pos().relative(direction),
EntitySpawnReason.DISPENSER,
-@@ -103,7 +_,8 @@
+ direction != Direction.UP,
+ false
++ // Paper start - ItemSpawnEntityEvent
++ , org.bukkit.event.entity.CreatureSpawnEvent.SpawnReason.DISPENSE_EGG,
++ entity -> {
++ if (org.bukkit.craftbukkit.event.CraftEventFactory.callItemSpawnEntityEvent(blockSource.level(), blockSource.pos().relative(direction), direction.getOpposite(), null, entity, item).isCancelled()) {
++ entity.discard(org.bukkit.event.entity.EntityRemoveEvent.Cause.DISCARD);
++ }
++ }
++ // Paper end - ItemSpawnEntityEvent
+ );
+ } catch (Exception var6) {
+ LOGGER.error("Error while dispensing spawn egg from dispenser at {}", blockSource.pos(), var6);
return ItemStack.EMPTY;
}
@@ -50,7 +62,7 @@
blockSource.level().gameEvent(null, GameEvent.ENTITY_PLACE, blockSource.pos());
return item;
}
-@@ -122,12 +_,38 @@
+@@ -122,12 +_,47 @@
Direction direction = blockSource.state().getValue(DispenserBlock.FACING);
BlockPos blockPos = blockSource.pos().relative(direction);
ServerLevel serverLevel = blockSource.level();
@@ -84,7 +96,16 @@
- armorStand1 -> armorStand1.setYRot(direction.toYRot()), serverLevel, item, null
+ armorStand1 -> armorStand1.setYRot(direction.toYRot()), serverLevel, newStack, null // Paper - track changed items in the dispense event
);
++ // Paper start - BlockPlaceEntityEvent
++ final java.util.concurrent.atomic.AtomicBoolean cancelled = new java.util.concurrent.atomic.AtomicBoolean(false);
++ consumer = consumer.andThen(stand -> {
++ if (org.bukkit.craftbukkit.event.CraftEventFactory.callBlockPlaceEntityEvent(blockSource, stand, item).isCancelled()) {
++ cancelled.set(true);
++ }
++ });
++ // Paper end - BlockPlaceEntityEvent
ArmorStand armorStand = EntityType.ARMOR_STAND.spawn(serverLevel, consumer, blockPos, EntitySpawnReason.DISPENSER, false, false);
++ if (cancelled.get()) shrink = false; // Paper
if (armorStand != null) {
- item.shrink(1);
+ if (shrink) item.shrink(1); // Paper
@@ -172,7 +193,7 @@
+
if (dispensibleContainerItem.emptyContents(null, level, blockPos, null)) {
- dispensibleContainerItem.checkExtraContent(null, level, item, blockPos);
-+ dispensibleContainerItem.checkExtraContent(null, level, dispensedItem, blockPos); // Paper - track changed item from dispense event
++ dispensibleContainerItem.checkExtraContent(null, level, dispensedItem, blockPos, null, blockSource.state().getValue(DispenserBlock.FACING).getOpposite()); // Paper - track changed item from dispense event
return this.consumeWithRemainder(blockSource, item, new ItemStack(Items.BUCKET));
} else {
return this.defaultDispenseItemBehavior.dispense(blockSource, item);
@@ -274,7 +295,7 @@
return item;
}
-@@ -281,11 +_,36 @@
+@@ -281,11 +_,41 @@
return item;
} else {
BlockPos blockPos = blockSource.pos().relative(blockSource.state().getValue(DispenserBlock.FACING));
@@ -305,6 +326,11 @@
+
+ PrimedTnt primedTnt = new PrimedTnt(serverLevel, event.getVelocity().getX(), event.getVelocity().getY(), event.getVelocity().getZ(), null);
+ // CraftBukkit end
++ // Paper start - BlockPlaceEntityEvent
++ if (org.bukkit.craftbukkit.event.CraftEventFactory.callBlockPlaceEntityEvent(blockSource, primedTnt, item).isCancelled()) {
++ return item;
++ }
++ // Paper end - BlockPlaceEntityEvent
serverLevel.addFreshEntity(primedTnt);
serverLevel.playSound(null, primedTnt.getX(), primedTnt.getY(), primedTnt.getZ(), SoundEvents.TNT_PRIMED, SoundSource.BLOCKS, 1.0F, 1.0F);
- serverLevel.gameEvent(null, GameEvent.ENTITY_PLACE, blockPos);
diff --git a/paper-server/patches/sources/net/minecraft/world/entity/EntityType.java.patch b/paper-server/patches/sources/net/minecraft/world/entity/EntityType.java.patch
index 54535b61d022..a7ea5cfb4a1b 100644
--- a/paper-server/patches/sources/net/minecraft/world/entity/EntityType.java.patch
+++ b/paper-server/patches/sources/net/minecraft/world/entity/EntityType.java.patch
@@ -8,7 +8,7 @@
private static final Logger LOGGER = LogUtils.getLogger();
private final Holder.Reference> builtInRegistryHolder = BuiltInRegistries.ENTITY_TYPE.createIntrusiveHolder(this);
public static final Codec> CODEC = BuiltInRegistries.ENTITY_TYPE.byNameCodec();
-@@ -1290,6 +_,22 @@
+@@ -1290,14 +_,35 @@
boolean shouldOffsetY,
boolean shouldOffsetYMore
) {
@@ -27,20 +27,25 @@
+ boolean shouldOffsetYMore,
+ org.bukkit.event.entity.CreatureSpawnEvent.SpawnReason createSpawnReason
+ ) {
++ return this.spawn(level, spawnedFrom, owner, pos, spawnReason, shouldOffsetY, shouldOffsetYMore, createSpawnReason, null);
++ }
++
++ public @Nullable T spawn(ServerLevel level, @Nullable ItemStack spawnedFrom, @Nullable LivingEntity owner, BlockPos pos, EntitySpawnReason spawnReason, boolean shouldOffsetY, boolean shouldOffsetYMore, org.bukkit.event.entity.CreatureSpawnEvent.SpawnReason createSpawnReason, final @Nullable Consumer super T> op) {
+ // CraftBukkit end
Consumer consumer;
if (spawnedFrom != null) {
consumer = createDefaultStackConfig(level, spawnedFrom, owner);
-@@ -1297,7 +_,7 @@
+ } else {
consumer = entity -> {};
}
++ if (op != null) consumer = consumer.andThen(op); // Paper
- return this.spawn(level, consumer, pos, spawnReason, shouldOffsetY, shouldOffsetYMore);
+ return this.spawn(level, consumer, pos, spawnReason, shouldOffsetY, shouldOffsetYMore, createSpawnReason); // CraftBukkit
}
public static Consumer createDefaultStackConfig(Level level, ItemStack stack, @Nullable LivingEntity owner) {
-@@ -1314,19 +_,54 @@
+@@ -1314,19 +_,55 @@
public static Consumer appendCustomEntityStackConfig(Consumer consumer, Level level, ItemStack stack, @Nullable LivingEntity owner) {
TypedEntityData> typedEntityData = stack.get(DataComponents.ENTITY_DATA);
@@ -89,6 +94,7 @@
T entity = this.create(level, consumer, pos, spawnReason, shouldOffsetY, shouldOffsetYMore);
if (entity != null) {
- level.addFreshEntityWithPassengers(entity);
++ if (entity.isRemoved()) return null; // Paper - if consumer removed entity, return null
+ // CraftBukkit start
+ level.addFreshEntityWithPassengers(entity, creatureSpawnReason);
+ if (entity.isRemoved()) {
diff --git a/paper-server/patches/sources/net/minecraft/world/item/BoatItem.java.patch b/paper-server/patches/sources/net/minecraft/world/item/BoatItem.java.patch
index ebd369c1cf4c..3700fba9ebd1 100644
--- a/paper-server/patches/sources/net/minecraft/world/item/BoatItem.java.patch
+++ b/paper-server/patches/sources/net/minecraft/world/item/BoatItem.java.patch
@@ -29,7 +29,7 @@
if (!level.isClientSide()) {
- level.addFreshEntity(boat);
+ // CraftBukkit start
-+ if (org.bukkit.craftbukkit.event.CraftEventFactory.callEntityPlaceEvent(level, playerPovHitResult.getBlockPos(), player.getDirection(), player, boat, hand).isCancelled()) {
++ if (org.bukkit.craftbukkit.event.CraftEventFactory.callEntityPlaceEvent(level, playerPovHitResult.getBlockPos(), player.getDirection(), player, boat, hand, itemInHand).isCancelled()) {
+ return InteractionResult.FAIL;
+ }
+
diff --git a/paper-server/patches/sources/net/minecraft/world/item/BucketItem.java.patch b/paper-server/patches/sources/net/minecraft/world/item/BucketItem.java.patch
index 534428c9d894..93d64da07457 100644
--- a/paper-server/patches/sources/net/minecraft/world/item/BucketItem.java.patch
+++ b/paper-server/patches/sources/net/minecraft/world/item/BucketItem.java.patch
@@ -32,16 +32,18 @@
if (!level.isClientSide()) {
CriteriaTriggers.FILLED_BUCKET.trigger((ServerPlayer)player, itemStack);
}
-@@ -75,7 +_,7 @@
+@@ -75,8 +_,8 @@
} else {
BlockState blockState = level.getBlockState(blockPos);
BlockPos blockPos2 = blockState.getBlock() instanceof LiquidBlockContainer && this.content == Fluids.WATER ? blockPos : blockPos1;
- if (this.emptyContents(player, level, blockPos2, playerPovHitResult)) {
+- this.checkExtraContent(player, level, itemInHand, blockPos2);
+ if (this.emptyContents(player, level, blockPos2, playerPovHitResult, playerPovHitResult.getDirection(), blockPos, itemInHand, hand)) { // CraftBukkit
- this.checkExtraContent(player, level, itemInHand, blockPos2);
++ this.checkExtraContent(player, level, itemInHand, blockPos2, hand, playerPovHitResult.getDirection()); // Paper - pass hand and clicked block face
if (player instanceof ServerPlayer) {
CriteriaTriggers.PLACED_BLOCK.trigger((ServerPlayer)player, blockPos2, itemInHand);
-@@ -92,6 +_,13 @@
+ }
+@@ -92,15 +_,26 @@
}
public static ItemStack getEmptySuccessItem(ItemStack bucketStack, Player player) {
@@ -55,7 +57,10 @@
return !player.hasInfiniteMaterials() ? new ItemStack(Items.BUCKET) : bucketStack;
}
-@@ -101,6 +_,12 @@
+- @Override
+- public void checkExtraContent(@Nullable LivingEntity entity, Level level, ItemStack stack, BlockPos pos) {
+- }
++ // Paper - delete checkExtraContents
@Override
public boolean emptyContents(@Nullable LivingEntity entity, Level level, BlockPos pos, @Nullable BlockHitResult hitResult) {
diff --git a/paper-server/patches/sources/net/minecraft/world/item/DispensibleContainerItem.java.patch b/paper-server/patches/sources/net/minecraft/world/item/DispensibleContainerItem.java.patch
new file mode 100644
index 000000000000..b58f4147fab4
--- /dev/null
+++ b/paper-server/patches/sources/net/minecraft/world/item/DispensibleContainerItem.java.patch
@@ -0,0 +1,16 @@
+--- a/net/minecraft/world/item/DispensibleContainerItem.java
++++ b/net/minecraft/world/item/DispensibleContainerItem.java
+@@ -7,8 +_,13 @@
+ import org.jspecify.annotations.Nullable;
+
+ public interface DispensibleContainerItem {
++ @Deprecated @io.papermc.paper.annotation.DoNotUse // Paper
+ default void checkExtraContent(@Nullable LivingEntity entity, Level level, ItemStack stack, BlockPos pos) {
+ }
++ // Paper start
++ default void checkExtraContent(net.minecraft.world.entity.player.@Nullable Player player, Level level, ItemStack stack, BlockPos pos, net.minecraft.world.@Nullable InteractionHand hand, net.minecraft.core.@Nullable Direction direction) {
++ }
++ // Paper end
+
+ boolean emptyContents(@Nullable LivingEntity entity, Level level, BlockPos pos, @Nullable BlockHitResult hitResult);
+ }
diff --git a/paper-server/patches/sources/net/minecraft/world/item/MobBucketItem.java.patch b/paper-server/patches/sources/net/minecraft/world/item/MobBucketItem.java.patch
index 1159ff5092b7..eefa4d81c856 100644
--- a/paper-server/patches/sources/net/minecraft/world/item/MobBucketItem.java.patch
+++ b/paper-server/patches/sources/net/minecraft/world/item/MobBucketItem.java.patch
@@ -1,10 +1,44 @@
--- a/net/minecraft/world/item/MobBucketItem.java
+++ b/net/minecraft/world/item/MobBucketItem.java
-@@ -49,7 +_,7 @@
+@@ -28,9 +_,9 @@
+ }
+
+ @Override
+- public void checkExtraContent(@Nullable LivingEntity entity, Level level, ItemStack stack, BlockPos pos) {
++ public void checkExtraContent(net.minecraft.world.entity.player.@Nullable Player entity, Level level, ItemStack stack, BlockPos pos, net.minecraft.world.@Nullable InteractionHand hand, net.minecraft.core.@Nullable Direction direction) { // Paper - add parameters
+ if (level instanceof ServerLevel) {
+- this.spawn((ServerLevel)level, stack, pos);
++ this.spawn((ServerLevel)level, stack, pos, entity, hand, direction); // Paper - add parameters
+ level.gameEvent(entity, GameEvent.ENTITY_PLACE, pos);
+ }
+ }
+@@ -40,7 +_,7 @@
+ level.playSound(entity, pos, this.emptySound, SoundSource.NEUTRAL, 1.0F, 1.0F);
+ }
+
+- private void spawn(ServerLevel level, ItemStack bucketedMobStack, BlockPos pos) {
++ private void spawn(ServerLevel level, ItemStack bucketedMobStack, BlockPos pos, net.minecraft.world.entity.player.@Nullable Player player, net.minecraft.world.@Nullable InteractionHand hand, net.minecraft.core.@Nullable Direction direction) { // Paper - add more parameters for events
+ Mob mob = this.type.create(level, EntityType.createDefaultStackConfig(level, bucketedMobStack, null), pos, EntitySpawnReason.BUCKET, true, false);
+ if (mob instanceof Bucketable bucketable) {
+ CustomData customData = bucketedMobStack.getOrDefault(DataComponents.BUCKET_ENTITY_DATA, CustomData.EMPTY);
+@@ -49,7 +_,20 @@
}
if (mob != null) {
- level.addFreshEntityWithPassengers(mob);
++ // Paper start - BlockPlaceEntityEvent & EntityPlaceEvent
++ if (direction != null) {
++ if (hand == null) {
++ if (org.bukkit.craftbukkit.event.CraftEventFactory.callBlockPlaceEntityEvent(level, pos, direction, mob, bucketedMobStack).isCancelled()) {
++ return; // mob doesn't exist yet, no need to discard
++ }
++ } else {
++ if (org.bukkit.craftbukkit.event.CraftEventFactory.callEntityPlaceEvent(level, pos, direction, player, mob, hand, bucketedMobStack).isCancelled()) {
++ return; // mob doesn't exist yet, no need to discard
++ }
++ }
++ }
++ // Paper end - BlockPlaceEntityEvent & EntityPlaceEvent
+ level.addFreshEntityWithPassengers(mob, org.bukkit.event.entity.CreatureSpawnEvent.SpawnReason.BUCKET); // Paper - Add SpawnReason
mob.playAmbientSound();
}
diff --git a/paper-server/patches/sources/net/minecraft/world/item/SpawnEggItem.java.patch b/paper-server/patches/sources/net/minecraft/world/item/SpawnEggItem.java.patch
index 9ce8994289c2..6e8180816e5b 100644
--- a/paper-server/patches/sources/net/minecraft/world/item/SpawnEggItem.java.patch
+++ b/paper-server/patches/sources/net/minecraft/world/item/SpawnEggItem.java.patch
@@ -8,7 +8,21 @@
spawner.setEntityId(type, level.getRandom());
level.sendBlockUpdated(clickedPos, blockState, blockState, Block.UPDATE_ALL);
level.gameEvent(context.getPlayer(), GameEvent.BLOCK_CHANGE, clickedPos);
-@@ -96,7 +_,7 @@
+@@ -84,22 +_,29 @@
+ }
+
+ return this.spawnMob(
+- context.getPlayer(), itemInHand, level, blockPos, true, !Objects.equals(clickedPos, blockPos) && clickedFace == Direction.UP
++ context.getPlayer(), itemInHand, level, blockPos, true, !Objects.equals(clickedPos, blockPos) && clickedFace == Direction.UP, clickedFace // Paper - pass clickedFace
+ );
+ }
+ }
+ }
+
+ private InteractionResult spawnMob(
+- @Nullable LivingEntity owner, ItemStack stack, Level level, BlockPos pos, boolean shouldOffsetY, boolean shouldOffsetYMore
++ @Nullable Player owner, ItemStack stack, Level level, BlockPos pos, boolean shouldOffsetY, boolean shouldOffsetYMore, Direction clickedFace
+ ) {
EntityType> type = this.getType(stack);
if (type == null) {
return InteractionResult.FAIL;
@@ -16,7 +30,27 @@
+ } else if (!type.isAllowedInPeaceful(stack.get(DataComponents.ENTITY_DATA).getUnsafe()) && level.getDifficulty() == Difficulty.PEACEFUL) { // Paper - check peaceful override
return InteractionResult.FAIL;
} else {
- if (type.spawn((ServerLevel)level, stack, owner, pos, EntitySpawnReason.SPAWN_ITEM_USE, shouldOffsetY, shouldOffsetYMore) != null) {
+- if (type.spawn((ServerLevel)level, stack, owner, pos, EntitySpawnReason.SPAWN_ITEM_USE, shouldOffsetY, shouldOffsetYMore) != null) {
++ // Paper start - call ItemSpawnEntityEvent
++ java.util.function.Consumer op = e -> {
++ if (org.bukkit.craftbukkit.event.CraftEventFactory.callItemSpawnEntityEvent(level, pos, clickedFace, owner, e, stack).isCancelled()) {
++ e.discard(org.bukkit.event.entity.EntityRemoveEvent.Cause.DISCARD);
++ }
++ };
++ if (type.spawn((ServerLevel)level, stack, owner, pos, EntitySpawnReason.SPAWN_ITEM_USE, shouldOffsetY, shouldOffsetYMore, org.bukkit.event.entity.CreatureSpawnEvent.SpawnReason.SPAWNER_EGG, op) != null) {
++ // Paper end - call ItemSpawnEntityEvent
+ stack.consume(1, owner);
+ level.gameEvent(owner, GameEvent.ENTITY_PLACE, pos);
+ }
+@@ -119,7 +_,7 @@
+ if (!(level.getBlockState(blockPos).getBlock() instanceof LiquidBlock)) {
+ return InteractionResult.PASS;
+ } else if (level.mayInteract(player, blockPos) && player.mayUseItemAt(blockPos, playerPovHitResult.getDirection(), itemInHand)) {
+- InteractionResult interactionResult = this.spawnMob(player, itemInHand, level, blockPos, false, false);
++ InteractionResult interactionResult = this.spawnMob(player, itemInHand, level, blockPos, false, false, playerPovHitResult.getDirection()); // Paper - pass clicked block face
+ if (interactionResult == InteractionResult.SUCCESS) {
+ player.awardStat(Stats.ITEM_USED.get(this));
+ }
@@ -178,7 +_,7 @@
} else {
breedOffspring.snapTo(pos.x(), pos.y(), pos.z(), 0.0F, 0.0F);
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..d24f7d1a76e4 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,6 +6,8 @@
import com.google.common.collect.Lists;
import com.mojang.authlib.GameProfile;
import com.mojang.datafixers.util.Either;
+import io.papermc.paper.event.entity.BlockPlaceEntityEvent;
+import io.papermc.paper.event.entity.ItemSpawnEntityEvent;
import java.util.ArrayList;
import java.util.Collections;
import java.util.EnumMap;
@@ -25,6 +27,7 @@
import io.papermc.paper.event.player.PlayerBedFailEnterEvent;
import net.minecraft.core.BlockPos;
import net.minecraft.core.Direction;
+import net.minecraft.core.dispenser.BlockSource;
import net.minecraft.network.Connection;
import net.minecraft.network.chat.Component;
import net.minecraft.network.protocol.game.ServerboundContainerClosePacket;
@@ -68,6 +71,7 @@
import net.minecraft.world.level.Explosion;
import net.minecraft.world.level.Level;
import net.minecraft.world.level.LevelAccessor;
+import net.minecraft.world.level.block.DispenserBlock;
import net.minecraft.world.level.block.entity.BlockEntity;
import net.minecraft.world.level.block.entity.SignBlockEntity;
import net.minecraft.world.level.block.state.properties.NoteBlockInstrument;
@@ -554,20 +558,45 @@ public static void handleBlockDropItemEvent(Block block, BlockState state, Serve
}
public static EntityPlaceEvent callEntityPlaceEvent(UseOnContext context, Entity entity) {
- return CraftEventFactory.callEntityPlaceEvent(context.getLevel(), context.getClickedPos(), context.getClickedFace(), context.getPlayer(), entity, context.getHand());
+ return CraftEventFactory.callEntityPlaceEvent(context.getLevel(), context.getClickedPos(), context.getClickedFace(), context.getPlayer(), entity, context.getHand(), context.getItemInHand());
}
- public static EntityPlaceEvent callEntityPlaceEvent(Level world, BlockPos clickedPos, Direction clickedFace, net.minecraft.world.entity.player.Player player, Entity entity, InteractionHand hand) {
- Player cplayer = (player == null) ? null : (Player) player.getBukkitEntity();
+ public static EntityPlaceEvent callEntityPlaceEvent(final Level world, final BlockPos clickedPos, final Direction clickedFace, final net.minecraft.world.entity.player.@Nullable Player player, final Entity entity, final InteractionHand hand, final ItemStack spawningStack) {
+ Player bukkitPlayer = (player == null) ? null : (Player) player.getBukkitEntity();
org.bukkit.block.Block clickedBlock = CraftBlock.at(world, clickedPos);
org.bukkit.block.BlockFace blockFace = org.bukkit.craftbukkit.block.CraftBlock.notchToBlockFace(clickedFace);
- EntityPlaceEvent event = new EntityPlaceEvent(entity.getBukkitEntity(), cplayer, clickedBlock, blockFace, CraftEquipmentSlot.getHand(hand));
+ final EntityPlaceEvent event = new EntityPlaceEvent(entity.getBukkitEntity(), bukkitPlayer, clickedBlock, blockFace, CraftEquipmentSlot.getHand(hand), spawningStack.asBukkitCopy());
entity.level().getCraftServer().getPluginManager().callEvent(event);
return event;
}
+ public static ItemSpawnEntityEvent callItemSpawnEntityEvent(final Level world, final BlockPos clickPosition, final Direction clickedFace, @Nullable final net.minecraft.world.entity.player.Player human, final Entity entity, final ItemStack spawningStack) {
+ final Player who = (human == null) ? null : (Player) human.getBukkitEntity();
+ final Block blockClicked = CraftBlock.at(world, clickPosition);
+ final BlockFace blockFace = CraftBlock.notchToBlockFace(clickedFace);
+
+ final ItemSpawnEntityEvent event = new ItemSpawnEntityEvent(entity.getBukkitEntity(), who, blockClicked, blockFace, CraftItemStack.asBukkitCopy(spawningStack));
+ event.callEvent();
+ return event;
+ }
+
+ public static BlockPlaceEntityEvent callBlockPlaceEntityEvent(final BlockSource pointer, final Entity entity, final ItemStack spawningStack) {
+ final Direction direction = pointer.state().getValue(DispenserBlock.FACING);
+ return callBlockPlaceEntityEvent(pointer.level(), pointer.pos().relative(direction), direction.getOpposite(), entity, spawningStack);
+ }
+
+ public static BlockPlaceEntityEvent callBlockPlaceEntityEvent(final Level world, final BlockPos clickedPosition, final Direction clickedFace, final Entity entity, final ItemStack spawningStack) {
+ final Block blockClicked = CraftBlock.at(world, clickedPosition);
+ final BlockFace blockFace = CraftBlock.notchToBlockFace(clickedFace);
+ final org.bukkit.block.Dispenser dispenser = (org.bukkit.block.Dispenser) CraftBlockStates.getBlockState(CraftBlock.at(world, clickedPosition.relative(clickedFace)));
+
+ final BlockPlaceEntityEvent event = new BlockPlaceEntityEvent(entity.getBukkitEntity(), blockClicked, blockFace, CraftItemStack.asBukkitCopy(spawningStack), dispenser);
+ event.callEvent();
+ return event;
+ }
+
public static PlayerBucketEmptyEvent callPlayerBucketEmptyEvent(Level world, net.minecraft.world.entity.player.Player player, BlockPos changed, BlockPos clicked, Direction clickedFace, ItemStack itemInHand, InteractionHand hand) {
return (PlayerBucketEmptyEvent) CraftEventFactory.getPlayerBucketEvent(false, world, player, changed, clicked, clickedFace, itemInHand, Items.BUCKET, hand);
}
diff --git a/paper-server/src/test/java/io/papermc/paper/block/DispensibleContainerItemExtraContentsOverrideTest.java b/paper-server/src/test/java/io/papermc/paper/block/DispensibleContainerItemExtraContentsOverrideTest.java
new file mode 100644
index 000000000000..b1e599a6802e
--- /dev/null
+++ b/paper-server/src/test/java/io/papermc/paper/block/DispensibleContainerItemExtraContentsOverrideTest.java
@@ -0,0 +1,50 @@
+package io.papermc.paper.block;
+
+import io.github.classgraph.ClassGraph;
+import io.github.classgraph.ClassInfo;
+import io.github.classgraph.ClassInfoList;
+import io.github.classgraph.MethodInfo;
+import io.github.classgraph.MethodInfoList;
+import io.github.classgraph.MethodParameterInfo;
+import io.github.classgraph.ScanResult;
+import java.util.ArrayList;
+import java.util.List;
+import java.util.stream.Stream;
+import net.minecraft.world.item.DispensibleContainerItem;
+import org.junit.jupiter.params.ParameterizedTest;
+import org.junit.jupiter.params.provider.MethodSource;
+
+import static org.junit.jupiter.api.Assertions.assertEquals;
+
+class DispensibleContainerItemExtraContentsOverrideTest {
+
+ public static Stream parameters() {
+ final List classInfo = new ArrayList<>();
+ try (final ScanResult scanResult = new ClassGraph()
+ .enableClassInfo()
+ .enableMethodInfo()
+ .whitelistPackages("net.minecraft")
+ .scan()
+ ) {
+ final ClassInfoList classesImplementing = scanResult.getClassesImplementing(DispensibleContainerItem.class.getName());
+ for (final ClassInfo info : classesImplementing) {
+ if (info.hasDeclaredMethod("checkExtraContent")) {
+ classInfo.add(info);
+ }
+ }
+ }
+ return classInfo.stream();
+ }
+
+ @ParameterizedTest
+ @MethodSource("parameters")
+ public void checkCheckExtraContentOverride(final ClassInfo implementsDispensibleContainerItem) {
+ final MethodInfoList checkExtraContent = implementsDispensibleContainerItem.getDeclaredMethodInfo("checkExtraContent");
+ assertEquals(1, checkExtraContent.size(), implementsDispensibleContainerItem.getName() + " has multiple checkExtraContent methods");
+ final MethodInfo next = checkExtraContent.iterator().next();
+ final MethodParameterInfo[] parameterInfo = next.getParameterInfo();
+ assertEquals(6, parameterInfo.length, implementsDispensibleContainerItem.getName() + " doesn't have 6 params for checkExtraContent");
+ assertEquals("InteractionHand", parameterInfo[parameterInfo.length - 2].getTypeDescriptor().toStringWithSimpleNames(), implementsDispensibleContainerItem.getName() + " needs to change its override of checkExtraContent");
+ assertEquals("Direction", parameterInfo[parameterInfo.length - 1].getTypeDescriptor().toStringWithSimpleNames(), implementsDispensibleContainerItem.getName() + " needs to change its override of checkExtraContent");
+ }
+}